Saltar al contenido
Gontzal Bilbao
← blog

Alarmas de infraestructura con Terraform

Un módulo Terraform con el topic SNS, la suscripción por email y alarmas de CloudWatch para una instancia fija y para un Auto Scaling Group.

serie · Alertas en AWS como código
Alarmas de infraestructura con Terraform

El problema

Convertí el diseño de alertas en un módulo de Terraform que se reutiliza en cada entorno. Crea el topic SNS, la suscripción por email y las alarmas de un jumpbox fijo y de un servidor web detrás de un ASG.

El porqué de cada decisión (qué alarmas, severidades, naming, InstanceId frente a ASG) está en Alertas preventivas en AWS como código con Terraform. Aquí va solo el cómo.

Lo monté en una cuenta de laboratorio. Los nombres, IDs y correos son de ejemplo.

idea clave

El módulo no sabe nada del entorno: el root le pasa los IDs y el email, y el mismo código sirve para dev, staging y prod.

La estructura

Separé las alarmas por tipo de recurso. Así cada diff del plan dice a qué recurso toca.

# árbol del repositorio
terraform/
  main/
    main.tf
    variables.tf
  modules/
    monitoring_ec2_alerts/
      variables.tf
      sns.tf
      alarms_bastion.tf
      alarms_api_asg.tf
      outputs.tf

El topic y la suscripción

El topic y su suscripción por email son lo primero que creo, porque sin canal ninguna alarma avisa a nadie.

# 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
}
# modules/monitoring_ec2_alerts/variables.tf
variable "topic_name" {
  type    = string
  default = "demo-infra-alerts"
}

variable "alert_email_endpoint" {
  type = string
}
ojo

Terraform crea la suscripción por email, pero no puede confirmarla. Hasta que alguien pulsa el enlace del correo, las alarmas saltan y no llega nada.

Alarmas para una instancia fija

Para el jumpbox uso la dimensión InstanceId. Esta es la alarma de CPU en warning:

# modules/monitoring_ec2_alerts/alarms_bastion.tf
resource "aws_cloudwatch_metric_alarm" "bastion_cpu_warning" {
  alarm_name          = "demo-ec2-jumpbox-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  = "notBreaching"

  dimensions = {
    InstanceId = var.bastion_instance_id
  }

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

Dos detalles. period = 300 encaja con la ventana de 5 minutos y con las métricas de Basic Monitoring. Y ok_actions envía también el aviso de vuelta a OK, así sé cuándo se ha recuperado.

La alarma critical es la misma con threshold = 85, evaluation_periods = 2 y datapoints_to_alarm = 2: dos bloques de 5 minutos seguidos.

Alarmas para lo que vive detrás de un ASG

Para el servidor web cambio la dimensión. La alarma de CPU es idéntica a la anterior salvo el nombre y este bloque:

# modules/monitoring_ec2_alerts/alarms_api_asg.tf
  alarm_name = "demo-ec2-web-cpu-warning"

  dimensions = {
    AutoScalingGroupName = var.api_asg_name
  }

StatusCheckFailed sí cambia más: estadística Maximum, periodo de un minuto y umbral cero.

# modules/monitoring_ec2_alerts/alarms_api_asg.tf
resource "aws_cloudwatch_metric_alarm" "api_statuscheckfailed_critical" {
  alarm_name          = "demo-ec2-web-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]
}

Conectar el módulo en el root

El root le pasa al módulo lo que solo él conoce. Ningún ID va escrito dentro del módulo.

# terraform/main/main.tf
module "monitoring_ec2_alerts" {
  source               = "../modules/monitoring_ec2_alerts"
  alert_email_endpoint = var.infra_alerts_email

  bastion_instance_id = module.bastion.instance_id
  api_asg_name        = module.api.asg_name
}
# terraform/main/variables.tf
variable "infra_alerts_email" {
  description = "Email endpoint for infrastructure alerts SNS subscription"
  type        = string
}

En Terraform Cloud, infra_alerts_email va como Terraform variable, porque es un input del código. Las credenciales AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY y AWS_DEFAULT_REGION van como Environment variables. Mezclarlas da warnings y configuraciones ambiguas.

Aplicar y comprobar

Antes de cada apply paso por el mismo orden:

# terminal, en terraform/main
terraform fmt -recursive
terraform validate
terraform plan
terraform apply

Después compruebo tres cosas con la CLI: que la suscripción está confirmada, que el canal entrega y que las alarmas existen con el nombre esperado.

# terminal
TOPIC_ARN="arn:aws:sns:eu-central-1:123456789012:demo-infra-alerts"

aws sns list-subscriptions-by-topic --topic-arn "$TOPIC_ARN" \
  --query "Subscriptions[].SubscriptionArn"

aws sns publish --topic-arn "$TOPIC_ARN" \
  --subject "Prueba de canal" --message "Mensaje de prueba"

aws cloudwatch describe-alarms --alarm-name-prefix "demo-ec2-" \
  --query "MetricAlarms[].{Name:AlarmName,State:StateValue}" --output table

Si la suscripción aparece como PendingConfirmation, el correo de confirmación sigue sin pulsar. Con el canal validado, toca forzar la alarma con carga real: lo cuento en Alarmas de CloudWatch para EC2, probadas con carga real.

Cómo ampliarlo

Con el módulo validado, el siguiente paso es cubrir más servicios con el mismo patrón:

  • RDS: CPU, conexiones y almacenamiento libre.
  • ElastiCache: memoria, evictions y CPU.
  • ALB: latencia y respuestas 5xx.
  • Memoria y disco de EC2, que necesitan el CloudWatch Agent.

Cada uno en su fichero, con el mismo naming y el mismo topic.

Lo que aprendí

Terraform crea la suscripción por email, pero no la confirma. Desde entonces, list-subscriptions-by-topic va en la lista de comprobaciones de cada apply, antes que cualquier alarma.

¿Te ha servido?

Si tienes dudas o lo estás montando tú, cuéntamelo.

Escríbeme Compartir en LinkedIn

sigue leyendo

Alertas preventivas en AWS como código con Terraform

Cómo diseño alertas preventivas en AWS como código: pocas alarmas calibradas, naming por entorno, módulo dedicado y rollout dev → staging → prod.

24 feb · 4 miniac

Alarmas de CloudWatch para EC2, probadas con carga real

Creo alarmas de CPU y salud para EC2 con CloudWatch y SNS, por consola y CLI, y compruebo con carga real que el aviso llega.

23 feb · 4 minobservabilidad