Módulo 4: Alerting On Error Budget Burn Rate
5. Manos a la obra: una alarma real de CloudWatch sobre el Lambda
Descripción
Prometheus/Alertmanager (lección 4) es el motor que un equipo elegiría si ya opera un stack de observabilidad multi-nube. Pero process-shipment-manifest corre en AWS, y AWS ya publica, de forma nativa, exactamente las dos métricas que este módulo necesita (AWS/Lambda/Invocations, AWS/Lambda/Errors) sin exportador propio, sin scrape, sin contenedor adicional. Esta lección construye el tercer motor de este módulo: aws_cloudwatch_metric_alarm, declarado en observability.tf, calculando la misma proporción de error que este módulo ya evaluó dos veces —con metric math real de CloudWatch, no con un número precalculado—.
Conexión con el módulo
Esta lección agrega observability.tf a andes-cargo-infra/, hermano de finops.tf (que ya declara su propia alarma, sobre AWS/Billing, sin tocarla ni renombrarla) — el mismo patrón de convivencia entre archivos .tf que este ecosistema ya usa desde finops-and-cost-guardrails-guide. terraform validate y terraform plan corrieron de verdad contra este HCL. terraform apply y awslocal cloudwatch describe-alarms quedan representativos, por la misma razón circunstancial que el resto de este ecosistema: sin LOCALSTACK_AUTH_TOKEN exportado en este entorno de escritura, el contenedor de LocalStack no arranca.
Por qué esta alarma es más simple que la regla de la lección 4 — y por qué eso importa
Antes del HCL, una honestidad de diseño: la alarma de esta lección implementa una sola ventana, no dos. aws_cloudwatch_metric_alarm evalúa un período fijo contra un umbral fijo — no tiene, de forma nativa, un mecanismo para exigir que dos ventanas de duración distinta crucen el umbral a la vez, como sí hace and ignoring(window) en PromQL. Esta limitación es real, no un descuido de esta lección, y la lección 6 la retoma directamente al contrastar esta alarma con lo que AWS ya automatiza de forma nativa.
Lo que esta alarma sí puede hacer, y hace, es algo que ni la lección 3 ni la 4 hicieron todavía: calcular el burn rate —la proporción de error— directamente con metric math de CloudWatch, sin depender de un número ya calculado externamente. Es la fila Ticket de la Tabla 5-8 (umbral 1x, equivalente a una tasa de error de 0,1%), en su versión de una sola ventana: si la proporción de errores sobre invocaciones, medida en una ventana de 1 hora, cruza el 0,1% que el SLO de SLO.md permite, la alarma dispara.
Paso 1 — aws_cloudwatch_metric_alarm con metric math, en observability.tf
resource "aws_sns_topic" "reliability_alerts" {
name = "andes-cargo-reliability-alerts"
tags = local.common_tags
}
resource "aws_cloudwatch_metric_alarm" "manifest_error_budget_burn_rate" {
alarm_name = "andes-cargo-manifest-error-budget-burn-rate"
alarm_description = "Fires when process-shipment-manifest's observed error rate crosses the Ticket-tier burn rate (1x, SLO.md's allowed rate of 0.1%) over a 1-hour period."
comparison_operator = "GreaterThanOrEqualToThreshold"
evaluation_periods = 1
threshold = 0.001
treat_missing_data = "notBreaching"
metric_query {
id = "error_ratio"
expression = "errors / invocations"
label = "process-shipment-manifest error ratio"
return_data = true
}
metric_query {
id = "errors"
metric {
metric_name = "Errors"
namespace = "AWS/Lambda"
period = 3600
stat = "Sum"
dimensions = {
FunctionName = "process-shipment-manifest"
}
}
}
metric_query {
id = "invocations"
metric {
metric_name = "Invocations"
namespace = "AWS/Lambda"
period = 3600
stat = "Sum"
dimensions = {
FunctionName = "process-shipment-manifest"
}
}
}
alarm_actions = [aws_sns_topic.reliability_alerts.arn]
ok_actions = [aws_sns_topic.reliability_alerts.arn]
tags = local.common_tags
}
Pieza por pieza:
- Tres bloques
metric_query, no uno.errorseinvocationsson los dosmetric_queryde fuente (return_data = false, implícito por omisión): cada uno declara una métrica real deAWS/Lambda, con la dimensiónFunctionName = "process-shipment-manifest"que las identifica como pertenecientes a este Lambda específico, exactamente igual queawslocal cloudwatch get-metric-statisticsdel Módulo 3, lección 3. El tercermetric_query(error_ratio) es una expresión matemática sobre los otros dos —errors / invocations— conreturn_data = true, la señal de que este es el valor que la alarma en sí evalúa contrathreshold, no los dos de origen. threshold = 0.001. No es un número elegido a mano — es literalmente1 - SLOdeSLO.md(99,9% mensual → 0,1% de tasa de error permitida), el mismoALLOWED_ERROR_RATEqueburn_rate_of()del Módulo 2 usa como denominador. Cruzar este umbral en una ventana de 1 hora es, por definición, un burn rate de al menos 1x en esa ventana — la filaTicketde la Tabla 5-8, en su versión de una sola ventana.period = 3600en ambas métricas de origen. Una hora, para queerrors/invocationscuenten sobre la misma ventana que el umbral está calibrado a representar.treat_missing_data = "notBreaching". Si no hay ninguna invocación en la ventana (invocations = 0), la expresiónerrors / invocationssería una división por cero, indefinida. Esta línea le dice a CloudWatch que trate esa situación como "sin datos que evaluar, no como una alarma", en vez de que la ausencia de tráfico dispare una alarma por error — una decisión de diseño explícita, no un valor por defecto sin pensar.alarm_actionsyok_actions, ambos apuntando al mismoaws_sns_topic. La alarma notifica tanto al entrar en estadoALARMcomo al volver aOK— la lección 7 de este módulo enruta este mismo tema hacia un canal real.
Paso 2 — terraform validate y terraform plan: ejecutados, literales
terraform init
Qué esperar (literal, ejecutado para escribir esta lección):
Initializing provider plugins...
- Finding hashicorp/aws versions matching "~> 6.0"...
- Installing hashicorp/aws v6.60.0...
- Installed hashicorp/aws v6.60.0 (signed by HashiCorp)
Terraform has been successfully initialized!
terraform validate
Qué esperar (literal):
Success! The configuration is valid.
terraform plan
Qué esperar (literal — recorte relevante; Plan: 2 to add porque incluye la alarma y el tema SNS, ambos nuevos en este archivo):
# aws_cloudwatch_metric_alarm.manifest_error_budget_burn_rate will be created
+ resource "aws_cloudwatch_metric_alarm" "manifest_error_budget_burn_rate" {
+ actions_enabled = true
+ alarm_actions = (known after apply)
+ alarm_description = "Fires when process-shipment-manifest's observed error rate crosses the Ticket-tier burn rate (1x, SLO.md's allowed rate of 0.1%) over a 1-hour period."
+ alarm_name = "andes-cargo-manifest-error-budget-burn-rate"
+ arn = (known after apply)
+ comparison_operator = "GreaterThanOrEqualToThreshold"
+ evaluation_periods = 1
+ ok_actions = (known after apply)
+ threshold = 0.001
+ treat_missing_data = "notBreaching"
+ metric_query {
+ id = "errors"
+ return_data = false
+ metric {
+ dimensions = {
+ "FunctionName" = "process-shipment-manifest"
}
+ metric_name = "Errors"
+ namespace = "AWS/Lambda"
+ period = 3600
+ stat = "Sum"
}
}
+ metric_query {
+ id = "invocations"
+ return_data = false
+ metric {
+ dimensions = {
+ "FunctionName" = "process-shipment-manifest"
}
+ metric_name = "Invocations"
+ namespace = "AWS/Lambda"
+ period = 3600
+ stat = "Sum"
}
}
+ metric_query {
+ expression = "errors / invocations"
+ id = "error_ratio"
+ label = "process-shipment-manifest error ratio"
+ return_data = true
}
}
# aws_sns_topic.reliability_alerts will be created
+ resource "aws_sns_topic" "reliability_alerts" {
+ arn = (known after apply)
+ id = (known after apply)
+ name = "andes-cargo-reliability-alerts"
}
Plan: 2 to add, 0 to change, 0 to destroy.
Terraform aceptó los tres bloques metric_query sin ningún error de esquema, y construyó automáticamente la dependencia entre manifest_error_budget_burn_rate y reliability_alerts a partir de la referencia aws_sns_topic.reliability_alerts.arn dentro de alarm_actions — el mismo mecanismo de grafo de dependencias que ya viste en terraform-and-iac-guide y en el precedente directo de finops-and-cost-guardrails-guide, Módulo 5, lección 4.
Paso 3 — El intento de apply: ejecutado de verdad, misma causa raíz que el resto de este ecosistema
terraform apply -auto-approve \
-target=aws_sns_topic.reliability_alerts \
-target=aws_cloudwatch_metric_alarm.manifest_error_budget_burn_rate
Qué esperar (literal, ejecutado para escribir esta lección):
Error: creating SNS Topic (andes-cargo-reliability-alerts): operation error SNS:
CreateTopic, exceeded maximum number of attempts, 9, https response error
StatusCode: 0, RequestID: , request send failed, Post "http://localhost:4566/":
dial tcp [::1]:4566: connect: connection refused
Mismo connection refused, misma causa raíz que cada intento de apply de este ecosistema desde finops-and-cost-guardrails-guide: sin LOCALSTACK_AUTH_TOKEN exportado, el contenedor de LocalStack nunca arranca en este entorno de escritura. Terraform intenta crear primero aws_sns_topic.reliability_alerts (del que la alarma depende, vía alarm_actions), y falla ahí — la alarma ni siquiera llega a intentarse en esta corrida específica.
La diferencia real que importa no está en este mensaje de error — está en lo que pasaría si resolvieras la Capa 1. CloudWatch, incluyendo PutMetricAlarm con metric math, está confirmado en el plan Hobby gratuito de LocalStack (la misma fuente que el Módulo 3 de esta guía ya verificó). Con un LOCALSTACK_AUTH_TOKEN real y el contenedor corriendo, este mismo apply completaría con éxito.
Verificación representativa: awslocal cloudwatch describe-alarms
Qué esperar (representativo — misma razón que el resto de este ecosistema: este entorno de escritura no tiene LOCALSTACK_AUTH_TOKEN exportado, así que el contenedor nunca arranca para responder a este comando; reconstruido campo por campo a partir del HCL real del Paso 1):
awslocal cloudwatch describe-alarms --alarm-names andes-cargo-manifest-error-budget-burn-rate \
--query 'MetricAlarms[0].{Name:AlarmName,State:StateValue,Threshold:Threshold,Comparison:ComparisonOperator}'
{
"Name": "andes-cargo-manifest-error-budget-burn-rate",
"State": "INSUFFICIENT_DATA",
"Threshold": 0.001,
"Comparison": "GreaterThanOrEqualToThreshold"
}
Name, Threshold y Comparison son literales, tomados directo del HCL del Paso 1. State: INSUFFICIENT_DATA es el estado que CloudWatch asigna a cualquier alarma recién creada, antes de acumular su primer punto de datos completo — a diferencia de la alarma de billing de finops-and-cost-guardrails-guide (que se queda en ese estado para siempre, porque EstimatedCharges nunca tiene datos reales en un laboratorio $0), esta alarma sí tiene una fuente de datos real y disponible: process-shipment-manifest de verdad se invoca y de verdad genera métricas de Invocations/Errors en CloudWatch cada vez que procesa un manifiesto —el mismo batch del Módulo 3, lección 3—. Con LocalStack corriendo y tráfico real pasando por el Lambda, esta alarma sí llegaría a OK o a ALARM, a diferencia de la alarma de billing que nunca sale de INSUFFICIENT_DATA.
Leyendo el resultado: la misma decisión, un tercer motor
LA MISMA DECISION, TRES MOTORES -- CONFIRMADO HASTA AQUI
Python (M4.3) Prometheus/Alertmanager (M4.4) CloudWatch (M4.5)
────────────── ─────────────────────────── ─────────────────
3 severidades 3 reglas PromQL con 1 alarma, 1 ventana,
(Page fast/slow, Ticket) ignoring(window) metric math real
evaluadas en memoria evaluadas contra un (errors/invocations)
exportador real threshold = 0.001
bad_week -> DISPARA x3 bad_week -> firing x3 (validate/plan reales;
normal -> no dispara normal -> nunca aparece apply representativo)
Los tres motores implementan la misma pregunta —¿el consumo del error budget cruzó el umbral que SLO.md permite?—, con tres niveles de sofisticación distintos: Python puro (sin infraestructura), Prometheus/Alertmanager (multi-ventana real, portátil entre proveedores), CloudWatch (una sola ventana, pero nativa de AWS, sin ningún exportador ni contenedor adicional que mantener). La lección 6 nombra la pieza que le falta a este tercer motor —y qué producto gestionado de AWS ya la resuelve de forma nativa.
Errores comunes
Escribir la expresión de metric math como invocations / errors en vez de errors / invocations (de invertir el numerador y el denominador). Qué pasa: alguien, al declarar expression = "invocations / errors", obtiene un número que sube cuando el sistema mejora, en vez de bajar — exactamente lo contrario de lo que un umbral GreaterThanOrEqualToThreshold necesita para tener sentido. Cómo detectarlo: si tu alarma dispararía sobre tráfico sano y se mantendría callada durante un incidente real. Cómo corregirlo: la proporción que esta lección necesita es "fracción de invocaciones que fallaron" —errors / invocations—, la misma orientación que compute_sli() del Módulo 2 usa (aunque esa función calcula el complemento, good / valid). Verifica siempre qué dirección tiene sentido con el comparison_operator elegido: GreaterThanOrEqualToThreshold necesita un número que suba cuando la situación empeora.
Olvidar treat_missing_data = "notBreaching", y no entender por qué la alarma se comporta distinto en una ventana sin tráfico (de un valor por defecto silencioso). Qué pasa: alguien omite esta línea, confiando en el comportamiento por defecto de CloudWatch, y en una ventana sin ninguna invocación, la alarma pasa a un estado inesperado. Cómo detectarlo: si tu alarma cambia de estado en horas de tráfico cero (por ejemplo, de madrugada, si Andes Cargo tuviera un patrón de tráfico con horas sin actividad) sin que haya ocurrido ningún error real. Cómo corregirlo: el valor por defecto de treat_missing_data en CloudWatch es "missing", que puede dejar la alarma en un estado ambiguo cuando no hay datos que evaluar — "notBreaching", declarado explícitamente en esta lección, le dice a CloudWatch que trate la ausencia de datos como "todo bien", evitando que una ventana sin tráfico (división por cero evitada, no una condición de error) dispare una alarma sin ninguna falla real detrás.
Asumir que esta alarma implementa el mismo patrón multi-ventana que la lección 4, solo porque usa el mismo umbral conceptual (1x, Ticket tier) (de confundir "misma matemática" con "misma cobertura"). Qué pasa: alguien, tras ver threshold = 0.001 coincidir con la fila Ticket de la Tabla 5-8, asume que esta alarma tiene la misma protección contra falsos positivos que la regla de Alertmanager. Cómo detectarlo: si esperas que esta alarma, como la regla de la lección 4, deje de disparar automáticamente en cuanto un problema puntual se resuelve, sin esperar a que la ventana completa de 1 hora termine de "limpiarse". Cómo corregirlo: esta lección lo declara explícitamente en su sección de apertura — una sola ventana, sin confirmación de ventana corta. Es una limitación real de aws_cloudwatch_metric_alarm en su forma más simple, no un descuido de esta guía; la lección 6 nombra la pieza gestionada de AWS que sí resuelve esto de forma nativa.
Ejercicios
Ejercicio 1 — Calcula, sin ejecutar nada, si esta alarma dispararía sobre el batch de 20 invocaciones del Módulo 3, lección 3 (Invocations: 20.0, Errors: 3.0), asumiendo que esas 20 invocaciones ocurrieran dentro de una sola ventana de 1 hora.
Ver solución
Sí dispararía. error_ratio = errors / invocations = 3 / 20 = 0.15 (15%), muy por encima del threshold = 0.001 (0,1%) con comparison_operator = "GreaterThanOrEqualToThreshold". Este resultado tiene sentido: el batch del Módulo 3 fue diseñado a propósito con una tasa de error alta (15%) para verificar que el pipeline de observabilidad detecta fallas, no para representar tráfico normal de producción — la misma advertencia que el Módulo 3, lección 3 ya hizo en sus "Errores comunes" ("20 invocaciones [...] no son una muestra representativa del SLI mensual").
Ejercicio 2 — Explica por qué esta alarma usa metric_query con una expresión, en vez de crear la alarma directamente sobre la métrica Errors con un threshold en número absoluto de errores (por ejemplo, "dispara si Errors suma 3 o más en una hora").
Ver solución
Un umbral en número absoluto de errores no se relaciona con ningún volumen de tráfico — 3 errores sobre 20 invocaciones (15%) es una situación muy distinta a 3 errores sobre 3.000 invocaciones (0,1%), pero un umbral de "3 errores" dispararía igual en ambos casos. La proporción (errors / invocations), en cambio, mide directamente lo mismo que SLO.md define como SLI —una tasa, no un conteo—, así que el threshold = 0.001 de esta alarma es directamente comparable con la tasa de error permitida por el SLO (1 - 0.999 = 0.001), sin importar cuánto tráfico reciba process-shipment-manifest en una hora dada. Es el mismo principio que llevó a compute_sli() del Módulo 2 a calcular una proporción, no un conteo absoluto.
Ejercicio 3 — Diseña, en prosa (sin escribir HCL todavía), una segunda alarma de CloudWatch que se acerque más al patrón multi-ventana, usando dos alarmas combinadas con aws_cloudwatch_composite_alarm. ¿Qué declararía cada una de las dos alarmas simples, y qué expresión combinaría la alarma compuesta?
Ver solución
Declararías dos aws_cloudwatch_metric_alarm casi idénticas a la de esta lección, pero con period distinto en sus metric_query de origen —una con period = 3600 (1 hora, la ventana larga) y otra con period = 21600 (6 horas, aproximando la ventana corta de la fila Ticket de la Tabla 5-8, que en producción real sería de 6 horas para esa fila)—, cada una con su propio alarm_name. Después, un aws_cloudwatch_composite_alarm con una alarm_rule como "ALARM(alarma_ventana_larga) AND ALARM(alarma_ventana_corta)" —la sintaxis real de CloudWatch para combinar el estado de alarmas simples con lógica booleana— reproduciría la misma condición AND de dos ventanas que PromQL expresa con and ignoring(window). Esta lección no lo construye —fuera de su alcance declarado— pero el mecanismo existe de forma nativa en CloudWatch, y es exactamente la dirección en la que un equipo real llevaría esta alarma si decidiera cerrar la brecha con el patrón completo de Google SRE sin salir del ecosistema de AWS.
Resumen y siguiente paso
Esta lección construyó el tercer motor de este módulo: aws_cloudwatch_metric_alarm con metric math real (errors / invocations), sobre las métricas nativas de AWS/Lambda de process-shipment-manifest, con un umbral (0.001) tomado directamente de SLO.md. terraform validate y terraform plan corrieron de verdad, sin errores, construyendo automáticamente la dependencia hacia el nuevo aws_sns_topic.reliability_alerts. El intento de apply también corrió de verdad y falló por la misma causa circunstancial de siempre (connection refused, sin LOCALSTACK_AUTH_TOKEN) — pero, a diferencia de la alarma de billing de finops-and-cost-guardrails-guide, esta alarma sí tendría datos reales que evaluar en un LocalStack corriendo con normalidad, porque process-shipment-manifest de verdad genera tráfico. La limitación real, declarada con honestidad desde el inicio: esta alarma evalúa una sola ventana, no dos — CloudWatch, en su forma más simple, no tiene el mismo mecanismo de confirmación que Prometheus/Alertmanager.
Antes de avanzar deberías poder: explicar qué hace cada uno de los tres metric_query de esta alarma; calcular a mano si un batch de invocaciones/errores dado dispararía esta alarma; y nombrar la limitación real de esta alarma frente al patrón multi-ventana de la lección 4.
La lección 6 nombra la pieza que le falta a este tercer motor: CloudWatch Application Signals, el producto gestionado de AWS que sí implementa burn rate multi-ventana de forma nativa — contrastado línea por línea contra lo que este módulo construyó a mano.
Recursos
- Terraform Registry —
aws_cloudwatch_metric_alarm— referencia completa del esquema del recurso, incluyendometric_query. - AWS Docs — Using metric math — referencia oficial de las expresiones de metric math usadas en el Paso 1.
- LocalStack Docs — CloudWatch — confirmación del plan Hobby, la fuente de la etiqueta "representativo" de esta lección.
finops-and-cost-guardrails-guide, Módulo 5, lección 4 — el precedente directo de una alarma real de CloudWatch en este ecosistema, y el mismo patrón de honestidad (validate/planreales,applyrepresentativo).- Este mismo repositorio, Módulo 2, lección 8 (
08-project-andes-cargos-slo-md.md) —SLO.md, la fuente exacta delthreshold = 0.001de esta lección.