Implementar alarmas de infraestructura con Terraform en AWS (SNS + CloudWatch) paso a paso


Introduccion

Una vez definido el enfoque de observabilidad y rollout, el siguiente paso es convertirlo en codigo reutilizable. En esta guia implementamos un baseline de alertas EC2 con Terraform usando una arquitectura simple:

  • SNS para notificaciones
  • CloudWatch Alarms para evaluacion de metricas
  • modulo reutilizable para despliegue por entorno

Todo el contenido usa ejemplos sanitizados (sin datos reales, IDs reales ni variables sensibles).

Si quieres primero el marco de decisiones y trade-offs (naming, rollout, InstanceId vs ASG, estrategia de validacion), revisa el articulo conceptual:

Objetivo del tutorial

Implementar una base de monitorizacion EC2 reusable para un entorno (por ejemplo stg) con:

  1. Topic SNS de alertas de infraestructura.
  2. Suscripcion por email.
  3. Alarmas CloudWatch para:
    • CPUUtilization (warning/critical)
    • StatusCheckFailed
    • CPUCreditBalance (si aplica)

Arquitectura minima

El flujo es directo:

EC2 metrics -> CloudWatch Alarms -> SNS -> Email

Es una base excelente para:

  • validar naming
  • validar canales
  • validar thresholds
  • preparar una extension posterior a RDS / Redis / ALB

Estructura Terraform recomendada

Una estructura sencilla y mantenible:

terraform/
  main/
    main.tf
    variables.tf
  modules/
    monitoring_ec2_alerts/
      variables.tf
      sns.tf
      alarms_bastion.tf
      alarms_api_asg.tf
      outputs.tf

Variables de entrada (sanitizadas)

En el root module, define inputs claros. Ejemplo:

variable "infra_alerts_email" {
  description = "Email endpoint for infrastructure alerts SNS subscription"
  type        = string
}

En Terraform Cloud:

  • infra_alerts_email -> Terraform variable
  • credenciales AWS_* -> Environment variables

Paso 1: Crear el topic SNS y la suscripcion

Archivo ejemplo modules/monitoring_ec2_alerts/sns.tf:

resource "aws_sns_topic" "infra_alerts" {
  name = var.topic_name
}

resource "aws_sns_topic_subscription" "infra_alerts_email" {
  topic_arn = aws_sns_topic.infra_alerts.arn
  protocol  = "email"
  endpoint  = var.alert_email_endpoint
}

Inputs asociados (variables.tf del modulo):

variable "topic_name" {
  type    = string
  default = "stg-infra-alerts"
}

variable "alert_email_endpoint" {
  type = string
}

Paso 2: Alarmas para una instancia fija (bastion)

Para una instancia fija suele ser buena opcion usar InstanceId.

Ejemplo de alarma de CPU warning:

resource "aws_cloudwatch_metric_alarm" "bastion_cpu_warning" {
  alarm_name          = "stg-ec2-bastion-cpu-warning"
  comparison_operator = "GreaterThanThreshold"
  evaluation_periods  = 1
  datapoints_to_alarm = 1
  metric_name         = "CPUUtilization"
  namespace           = "AWS/EC2"
  period              = 300
  statistic           = "Average"
  threshold           = 75
  treat_missing_data  = "missing"

  dimensions = {
    InstanceId = var.bastion_instance_id
  }

  alarm_actions = [aws_sns_topic.infra_alerts.arn]
  ok_actions    = [aws_sns_topic.infra_alerts.arn]
}

Puntos a destacar:

  • period=300 encaja bien cuando quieres una ventana de 5 minutos.
  • ok_actions ayuda a cerrar el ciclo de notificacion (recibir OK tras recuperacion).

Paso 3: Alarmas para API detras de Auto Scaling Group

Cuando la API vive detras de un ASG, una opcion robusta es usar AutoScalingGroupName en las dimensiones para no depender de una instancia concreta.

Ejemplo de CPU warning por ASG:

resource "aws_cloudwatch_metric_alarm" "api_cpu_warning" {
  alarm_name          = "stg-ec2-api-cpu-warning"
  comparison_operator = "GreaterThanThreshold"
  evaluation_periods  = 1
  datapoints_to_alarm = 1
  metric_name         = "CPUUtilization"
  namespace           = "AWS/EC2"
  period              = 300
  statistic           = "Average"
  threshold           = 75
  treat_missing_data  = "missing"

  dimensions = {
    AutoScalingGroupName = var.api_asg_name
  }

  alarm_actions = [aws_sns_topic.infra_alerts.arn]
  ok_actions    = [aws_sns_topic.infra_alerts.arn]
}

Ejemplo de StatusCheckFailed por ASG:

resource "aws_cloudwatch_metric_alarm" "api_statuscheckfailed_critical" {
  alarm_name          = "stg-ec2-api-statuscheckfailed-critical"
  comparison_operator = "GreaterThanThreshold"
  evaluation_periods  = 1
  datapoints_to_alarm = 1
  metric_name         = "StatusCheckFailed"
  namespace           = "AWS/EC2"
  period              = 60
  statistic           = "Maximum"
  threshold           = 0

  dimensions = {
    AutoScalingGroupName = var.api_asg_name
  }

  alarm_actions = [aws_sns_topic.infra_alerts.arn]
}

Paso 4: Nombrado y severidades

Usa un patron consistente:

<env>-<service>-<resource>-<metric>-<severity>

Ejemplos:

  • stg-ec2-bastion-cpu-warning
  • stg-ec2-bastion-cpu-critical
  • stg-ec2-api-cpucreditbalance-warning
  • stg-ec2-api-statuscheckfailed-critical

Esto facilita:

  • busquedas en CloudWatch
  • filtros por severidad
  • mantenimiento por entorno

Paso 5: Integracion del modulo en el root

Ejemplo de integracion en terraform/main/main.tf:

module "monitoring_ec2_alerts" {
  source               = "../modules/monitoring_ec2_alerts"
  alert_email_endpoint = var.infra_alerts_email

  bastion_instance_id = module.ec2_instances.output_bastion_instance_id
  api_asg_name        = module.autoscaling_groups.output_asg_apiasg_name
}

Este patron mantiene el root como orquestador y evita hardcodear IDs en el modulo.

Paso 6: Flujo de validacion y despliegue

Secuencia recomendada:

  1. terraform fmt
  2. terraform validate
  3. terraform plan
  4. revisar diff
  5. terraform apply

Checklist operativo tras apply:

  • Existe el topic SNS esperado
  • Existe la suscripcion email
  • La suscripcion esta confirmada
  • Existen las alarmas stg-ec2-*

Verificacion funcional (sin exponer datos reales)

Una vez aplicado:

  1. Publica un mensaje de prueba en el topic SNS.
  2. Verifica recepcion por email.
  3. Comprueba que CloudWatch muestra las alarmas creadas con el naming esperado.

Despues de validar el canal, puedes preparar una prueba controlada de CPU en un entorno no productivo para confirmar el flujo end-to-end.

Si quieres aprender esa validacion desde consola/CLI antes de automatizarlo con Terraform, puedes apoyarte en este post:

Errores comunes de implementacion (genericos)

1) Mezclar variables de Terraform con variables de entorno en Terraform Cloud

Solucion:

  • inputs del codigo -> Terraform Variables
  • credenciales y runtime -> Environment Variables

2) Crear alarmas sin validar la suscripcion SNS

Si el email no esta confirmado, la alarma puede disparar y aun asi no recibir nada.

3) Naming inconsistente entre entornos

Si no normalizas nombres desde el inicio, luego es dificil filtrar, auditar y automatizar reportes.

Extension natural del baseline

Con esta base validada, el siguiente paso suele ser ampliar cobertura a:

  • RDS (CPU, conexiones, almacenamiento)
  • Redis/ElastiCache (memoria, evictions, CPU)
  • ALB (latencia y 5xx)

La clave es mantener el mismo patron:

  • modulo reutilizable
  • naming estandar
  • rollout por entorno

Cierre

Implementar alertas con Terraform no consiste solo en “traducir clicks a HCL”. La mejora real viene de tener un flujo repetible, revisable y desplegable por entornos.

Este baseline (SNS + CloudWatch + naming + rollout) es una base muy buena para escalar observabilidad operativa sin introducir complejidad innecesaria.

Si quieres reforzar la parte de diseño y decisiones antes de implementar, revisa el articulo complementario:

¿Evolucionamos tu plataforma de datos?

Si quieres mejorar arquitectura, calidad y coste de tu pipeline, puedo ayudarte a aterrizar una hoja de ruta por fases.