Gontzal Bilbao

Gontzal Bilbao

  • Email Email
  • Ubicación País Vasco, ES

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


Introducción

Una vez definido el enfoque de observabilidad y rollout, el siguiente paso es convertirlo en código reutilizable. En esta guía implementamos un baseline de alertas EC2 con Terraform usando una arquitectura simple:

  • SNS para notificaciones
  • CloudWatch Alarms para evaluación de métricas
  • módulo 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 validación), revisa el artículo conceptual:

Objetivo del tutorial

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

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

Arquitectura mínima

El flujo es directo:

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

Es una base excelente para:

  • validar naming
  • validar canales
  • validar thresholds
  • preparar una extensión 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 suscripción

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 módulo):

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 opción 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 notificación (recibir OK tras recuperación).

Paso 3: Alarmas para API detras de Auto Scaling Group

Cuando la API vive detras de un ASG, una opción 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 patrón 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: Integración del módulo en el root

Ejemplo de integración 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 patrón mantiene el root como orquestador y evita hardcodear IDs en el módulo.

Paso 6: Flujo de validación 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 suscripción email
  • La suscripción esta confirmada
  • Existen las alarmas stg-ec2-*

Verificación funcional (sin exponer datos reales)

Una vez aplicado:

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

Después 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 validación desde consola/CLI antes de automatizarlo con Terraform, puedes apoyarte en este post:

Errores comunes de implementación (genéricos)

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

Solución:

  • inputs del código -> Terraform Variables
  • credenciales y runtime -> Environment Variables

2) Crear alarmas sin validar la suscripción SNS

Si el email no esta confirmado, la alarma puede disparar y aún así no recibir nada.

3) Naming inconsistente entre entornos

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

Extensión 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 patrón:

  • módulo reutilizable
  • naming estándar
  • 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 artículo 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.