Lab práctico · Semana 10: Resiliencia, operaciones y costes: Well-Architected, DR, observabilidad, IaC y FinOps

Monitorización con CloudWatch y recuperación en otra región con AWS Backup

⏱ 120-150 minDificultad: avanzadaTask statements: 2.24.1

Qué vas a construir

Un mini entorno de producción en eu-south-2 (España) con observabilidad y una estrategia de DR backup and restore hacia eu-west-1 (Irlanda):

  1. Un tema SNS que te avisa por correo.
  2. Una instancia EC2 y alarmas de CloudWatch: CPU, status check con acción de recuperación automática, una métrica personalizada de negocio y una alarma compuesta.
  3. CloudWatch Logs con un metric filter que cuenta errores y una consulta de Logs Insights.
  4. Un dashboard de CloudWatch.
  5. Una copia con AWS Backup del volumen EBS, copiada a otra región y restaurada allí: la prueba de DR que casi nadie hace.
flowchart LR
  subgraph P["eu-south-2 (primaria)"]
    EC2["EC2 t3.micro"] --> CW["CloudWatch: métricas y alarmas"]
    APP["Logs de la app"] --> LG["CloudWatch Logs + metric filter"]
    CW --> SNS["SNS: correo"]
    LG --> CW
    EC2 --> VOL["Volumen EBS"]
    VOL --> BK["AWS Backup: bóveda primaria"]
  end
  subgraph DR["eu-west-1 (DR)"]
    BK2["Bóveda DR"] --> RV["Volumen restaurado"]
  end
  BK -->|"copy job entre regiones"| BK2

Antes de empezar

  • Usuario de IAM Identity Center o de IAM con MFA (nunca el raíz) con permisos sobre EC2, CloudWatch, CloudWatch Logs, SNS, AWS Backup e IAM.
  • AWS CloudShell abierto en eu-south-2. La región de DR será eu-west-1; ambas admiten todos los servicios del lab.
  • Un correo al que tengas acceso para confirmar la suscripción de SNS.
export AWS_REGION=eu-south-2
export REGION_DR=eu-west-1
export CUENTA=$(aws sts get-caller-identity --query Account --output text)
export CORREO="tu-correo@ejemplo.com"
mkdir -p ~/lab20 && cd ~/lab20

Cambia CORREO por tu dirección real.

Paso 1: tema SNS para las alertas

export TEMA_ARN=$(aws sns create-topic --name lab20-alertas --query TopicArn --output text)
aws sns subscribe --topic-arn $TEMA_ARN --protocol email --notification-endpoint $CORREO

Abre el correo de AWS Notifications y pulsa Confirm subscription. Sin confirmar, no recibirás nada.

Paso 2: la instancia de «producción»

Qué y por qué: una instancia Amazon Linux 2023 en la subred por defecto de una AZ, sin IP pública, con IMDSv2 obligatorio. La AMI se resuelve con el parámetro público de Systems Manager, así siempre usas la última.

export SUBRED=$(aws ec2 describe-subnets --filters Name=default-for-az,Values=true \
  --query 'Subnets[0].SubnetId' --output text)

export INSTANCIA=$(aws ec2 run-instances \
  --image-id resolve:ssm:/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64 \
  --instance-type t3.micro \
  --network-interfaces DeviceIndex=0,SubnetId=$SUBRED,AssociatePublicIpAddress=false \
  --metadata-options HttpTokens=required \
  --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=lab20-web},{Key=Proyecto,Value=lab20}]' \
                       'ResourceType=volume,Tags=[{Key=Proyecto,Value=lab20}]' \
  --query 'Instances[0].InstanceId' --output text)

aws ec2 wait instance-running --instance-ids $INSTANCIA

export VOLUMEN=$(aws ec2 describe-instances --instance-ids $INSTANCIA \
  --query 'Reservations[0].Instances[0].BlockDeviceMappings[0].Ebs.VolumeId' --output text)
echo "Instancia $INSTANCIA, volumen $VOLUMEN"

Si tu cuenta indica que t3.micro no es apta para la capa gratuita en esta región, usa t4g.micro con la AMI al2023-ami-kernel-default-arm64.

Paso 3: alarmas de CloudWatch

3.1 CPU alta (métrica estándar de EC2)

Con basic monitoring, EC2 publica cada 5 minutos, así que el periodo de la alarma debe ser de al menos 300 s. Pedimos 2 de 2 periodos por encima del 80 % para evitar falsas alarmas.

aws cloudwatch put-metric-alarm --alarm-name lab20-cpu-alta \
  --alarm-description "CPU media superior al 80 % durante 10 minutos" \
  --namespace AWS/EC2 --metric-name CPUUtilization \
  --dimensions Name=InstanceId,Value=$INSTANCIA \
  --statistic Average --period 300 \
  --evaluation-periods 2 --datapoints-to-alarm 2 \
  --threshold 80 --comparison-operator GreaterThanThreshold \
  --treat-missing-data missing \
  --alarm-actions $TEMA_ARN --ok-actions $TEMA_ARN

3.2 Recuperación automática ante fallos del hardware

Si falla la comprobación de estado del sistema (StatusCheckFailed_System: problema del host físico, no de tu sistema operativo), la acción recover migra la instancia a otro hardware conservando ID, IP privada, volúmenes EBS y metadatos. Es una forma de mejorar la fiabilidad de una aplicación sin tocar su código.

aws cloudwatch put-metric-alarm --alarm-name lab20-recuperar-sistema \
  --alarm-description "Recupera la instancia si falla la status check del sistema" \
  --namespace AWS/EC2 --metric-name StatusCheckFailed_System \
  --dimensions Name=InstanceId,Value=$INSTANCIA \
  --statistic Maximum --period 60 --evaluation-periods 2 \
  --threshold 0 --comparison-operator GreaterThanThreshold \
  --alarm-actions arn:aws:automate:$AWS_REGION:ec2:recover $TEMA_ARN

3.3 Métrica personalizada de negocio

La guía pide identificar métricas según los requisitos de negocio. Publicamos PedidosPendientes (lo que haría tu aplicación con PutMetricData) y creamos una alarma que salta con más de 100 pedidos atascados.

aws cloudwatch put-metric-alarm --alarm-name lab20-pedidos-pendientes \
  --alarm-description "Más de 100 pedidos pendientes de procesar" \
  --namespace Lab20/Tienda --metric-name PedidosPendientes \
  --statistic Maximum --period 60 --evaluation-periods 1 \
  --threshold 100 --comparison-operator GreaterThanThreshold \
  --treat-missing-data notBreaching \
  --alarm-actions $TEMA_ARN --ok-actions $TEMA_ARN

for v in 20 45 150; do
  aws cloudwatch put-metric-data --namespace Lab20/Tienda \
    --metric-name PedidosPendientes --value $v --unit Count
  sleep 20
done

En 2-3 minutos la alarma pasa a ALARM con datos reales y recibes el correo. Compruébalo:

aws cloudwatch describe-alarms --alarm-names lab20-pedidos-pendientes \
  --query 'MetricAlarms[0].[StateValue,StateReason]'

3.4 Probar una alarma y combinar señales

Fuerza la alarma de CPU para verificar que la cadena de notificación funciona (el estado dura hasta la siguiente evaluación):

aws cloudwatch set-alarm-state --alarm-name lab20-cpu-alta \
  --state-value ALARM --state-reason "Prueba manual de la cadena de avisos"

Una alarma compuesta reduce el ruido: solo avisa si se cumplen a la vez varias condiciones. Aquí, CPU alta y pedidos atascados (síntoma de que el sistema no da abasto):

aws cloudwatch put-composite-alarm --alarm-name lab20-compuesta \
  --alarm-rule 'ALARM("lab20-cpu-alta") AND ALARM("lab20-pedidos-pendientes")' \
  --alarm-actions $TEMA_ARN

aws cloudwatch describe-alarms --alarm-name-prefix lab20 \
  --alarm-types MetricAlarm CompositeAlarm \
  --query '[MetricAlarms[*].[AlarmName,StateValue], CompositeAlarms[*].[AlarmName,StateValue]]'

Paso 4: logs, metric filter y Logs Insights

Qué y por qué: los errores de la aplicación están en los logs. Un metric filter los convierte en una métrica (y así puedes poner alarmas) y Logs Insights permite investigarlos. Creamos el filtro antes de enviar eventos: solo se aplica a los eventos nuevos.

aws logs create-log-group --log-group-name /lab20/app
aws logs put-retention-policy --log-group-name /lab20/app --retention-in-days 1
aws logs create-log-stream --log-group-name /lab20/app --log-stream-name web-1

aws logs put-metric-filter --log-group-name /lab20/app \
  --filter-name errores-app --filter-pattern "ERROR" \
  --metric-transformations metricName=ErroresApp,metricNamespace=Lab20/Tienda,metricValue=1,defaultValue=0

AHORA=$(date +%s%3N)
cat > eventos.json <<EOF
[
  {"timestamp": $AHORA, "message": "INFO pedido 1001 creado"},
  {"timestamp": $((AHORA + 1)), "message": "ERROR pago rechazado pedido 1002"},
  {"timestamp": $((AHORA + 2)), "message": "INFO pedido 1003 creado"},
  {"timestamp": $((AHORA + 3)), "message": "ERROR tiempo de espera agotado con el proveedor de pagos"},
  {"timestamp": $((AHORA + 4)), "message": "ERROR pago rechazado pedido 1005"}
]
EOF
aws logs put-log-events --log-group-name /lab20/app --log-stream-name web-1 \
  --log-events file://eventos.json

Consulta con Logs Insights (espera unos segundos a que los eventos estén indexados):

CONSULTA=$(aws logs start-query --log-group-name /lab20/app \
  --start-time $(( $(date +%s) - 3600 )) --end-time $(( $(date +%s) + 300 )) \
  --query-string 'fields @timestamp, @message | filter @message like /ERROR/ | sort @timestamp desc' \
  --query queryId --output text)
sleep 10
aws logs get-query-results --query-id $CONSULTA --query 'results[*][?field==`@message`].value'

Deberías ver los tres errores. En unos minutos, la métrica ErroresApp del namespace Lab20/Tienda mostrará un valor de 3 en la consola de CloudWatch.

Paso 5: dashboard

cat > dashboard.json <<EOF
{
  "widgets": [
    {
      "type": "metric", "x": 0, "y": 0, "width": 12, "height": 6,
      "properties": {
        "title": "CPU de lab20-web", "region": "$AWS_REGION", "stat": "Average", "period": 300,
        "metrics": [["AWS/EC2", "CPUUtilization", "InstanceId", "$INSTANCIA"]]
      }
    },
    {
      "type": "metric", "x": 12, "y": 0, "width": 12, "height": 6,
      "properties": {
        "title": "Negocio: pedidos pendientes y errores", "region": "$AWS_REGION", "stat": "Maximum", "period": 60,
        "metrics": [["Lab20/Tienda", "PedidosPendientes"], ["Lab20/Tienda", "ErroresApp"]]
      }
    }
  ]
}
EOF
aws cloudwatch put-dashboard --dashboard-name lab20 --dashboard-body file://dashboard.json

Ábrelo en la consola (CloudWatch → Dashboards → lab20). Un dashboard puede mezclar métricas de varias regiones y cuentas: útil para ver a la vez la región primaria y la de DR.

Paso 6: copia de seguridad con AWS Backup

Qué y por qué: AWS Backup centraliza las copias y permite copiarlas a otra región (DR regional) y a otra cuenta (protección frente a una cuenta comprometida). Necesita un rol de servicio y una bóveda (backup vault) en cada región.

cat > confianza-backup.json <<'EOF'
{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": { "Service": "backup.amazonaws.com" },
    "Action": "sts:AssumeRole"
  }]
}
EOF
aws iam create-role --role-name lab20-backup-rol \
  --assume-role-policy-document file://confianza-backup.json
aws iam attach-role-policy --role-name lab20-backup-rol \
  --policy-arn arn:aws:iam::aws:policy/service-role/AWSBackupServiceRolePolicyForBackup
aws iam attach-role-policy --role-name lab20-backup-rol \
  --policy-arn arn:aws:iam::aws:policy/service-role/AWSBackupServiceRolePolicyForRestores
export ROL_BACKUP=$(aws iam get-role --role-name lab20-backup-rol --query Role.Arn --output text)
sleep 15

aws backup create-backup-vault --backup-vault-name lab20-primaria

# Clave AWS managed de EBS en la región de DR (describe-key la crea si aún no existe)
export CLAVE_EBS_DR=$(aws kms describe-key --key-id alias/aws/ebs --region $REGION_DR \
  --query KeyMetadata.Arn --output text)
aws backup create-backup-vault --backup-vault-name lab20-dr --region $REGION_DR \
  --encryption-key-arn $CLAVE_EBS_DR
export BOVEDA_DR_ARN=$(aws backup describe-backup-vault --backup-vault-name lab20-dr \
  --region $REGION_DR --query BackupVaultArn --output text)

¿Por qué la bóveda de DR usa la clave aws/ebs? La copia a otra región se cifra con la clave de la bóveda de destino. EBS no es un recurso fully managed por AWS Backup (sus copias heredan el cifrado del volumen), y para esos recursos la clave de la bóveda de destino debe ser una clave customer managed o la clave AWS managed del propio servicio (aws/ebs); con la clave por defecto de las bóvedas (aws/backup) el trabajo de copia falla. La copia entre regiones sí funciona con claves AWS managed; la copia entre cuentas no, y exige claves customer managed.

Lanza una copia bajo demanda del volumen. La regla de ciclo de vida DeleteAfterDays=2 es una red de seguridad por si olvidas limpiar:

export TRABAJO=$(aws backup start-backup-job --backup-vault-name lab20-primaria \
  --resource-arn arn:aws:ec2:$AWS_REGION:$CUENTA:volume/$VOLUMEN \
  --iam-role-arn $ROL_BACKUP \
  --lifecycle DeleteAfterDays=2 \
  --query BackupJobId --output text)

until estado=$(aws backup describe-backup-job --backup-job-id $TRABAJO --query State --output text); \
  [[ "$estado" =~ ^(COMPLETED|FAILED|ABORTED|EXPIRED|PARTIAL)$ ]]; do
  echo "Backup: $estado"; sleep 20
done
echo "Backup: $estado"

export PUNTO=$(aws backup describe-backup-job --backup-job-id $TRABAJO \
  --query RecoveryPointArn --output text)
echo $PUNTO

Para un volumen EBS, el punto de recuperación es un snapshot (su ARN es del tipo arn:aws:ec2:...:snapshot/snap-...). Suele tardar unos minutos.

Paso 7: copiar a la región de DR

export COPIA=$(aws backup start-copy-job \
  --recovery-point-arn $PUNTO \
  --source-backup-vault-name lab20-primaria \
  --destination-backup-vault-arn $BOVEDA_DR_ARN \
  --iam-role-arn $ROL_BACKUP \
  --lifecycle DeleteAfterDays=2 \
  --query CopyJobId --output text)

until estado=$(aws backup describe-copy-job --copy-job-id $COPIA --query CopyJob.State --output text); \
  [[ "$estado" =~ ^(COMPLETED|FAILED|PARTIAL)$ ]]; do
  echo "Copia: $estado"; sleep 30
done
echo "Copia: $estado"

export PUNTO_DR=$(aws backup describe-copy-job --copy-job-id $COPIA \
  --query CopyJob.DestinationRecoveryPointArn --output text)
echo $PUNTO_DR

La copia entre regiones puede tardar de varios minutos a más de media hora. Mientras esperas: con esta copia ya tienes una estrategia backup and restore con un RPO igual a la frecuencia de tus copias (aquí, una manual; en producción, un backup plan con reglas diarias u horarias y copia automática a la otra región).

Paso 8: restaurar en la región de DR

Qué y por qué: una copia que nunca se ha restaurado no es una copia fiable (principio Test recovery procedures del pilar de fiabilidad). Restauramos el volumen en una AZ de Irlanda:

export TAMANO=$(aws ec2 describe-volumes --volume-ids $VOLUMEN --query 'Volumes[0].Size' --output text)

export RESTAURACION=$(aws backup start-restore-job --region $REGION_DR \
  --recovery-point-arn $PUNTO_DR \
  --iam-role-arn $ROL_BACKUP \
  --resource-type EBS \
  --metadata "{\"availabilityZone\":\"eu-west-1a\",\"volumeType\":\"gp3\",\"volumeSize\":\"$TAMANO\"}" \
  --query RestoreJobId --output text)

until estado=$(aws backup describe-restore-job --region $REGION_DR \
  --restore-job-id $RESTAURACION --query Status --output text); \
  [[ "$estado" =~ ^(COMPLETED|FAILED|ABORTED)$ ]]; do
  echo "Restauración: $estado"; sleep 15
done
echo "Restauración: $estado"

export VOL_DR=$(aws backup describe-restore-job --region $REGION_DR \
  --restore-job-id $RESTAURACION --query CreatedResourceArn --output text | awk -F/ '{print $NF}')

aws ec2 describe-volumes --region $REGION_DR --volume-ids $VOL_DR \
  --query 'Volumes[0].[VolumeId,AvailabilityZone,Size,VolumeType,State]'

Tienes un volumen available en eu-west-1a con el contenido de tu servidor de España. En un DR real, lo adjuntarías a una instancia lanzada con CloudFormation (lab 19) o restaurarías directamente la instancia EC2 completa desde AWS Backup, y cambiarías el tráfico con un registro failover de Route 53.

Alternativa sin AWS Backup: snapshots de EBS

El mismo resultado con comandos de EC2 (útil para entender qué hace AWS Backup por debajo):

SNAP=$(aws ec2 create-snapshot --volume-id $VOLUMEN --description "lab20 manual" \
  --query SnapshotId --output text)
aws ec2 wait snapshot-completed --snapshot-ids $SNAP

SNAP_DR=$(aws ec2 copy-snapshot --region $REGION_DR --source-region $AWS_REGION \
  --source-snapshot-id $SNAP --description "lab20 copia DR" --query SnapshotId --output text)
aws ec2 wait snapshot-completed --region $REGION_DR --snapshot-ids $SNAP_DR

aws ec2 create-volume --region $REGION_DR --snapshot-id $SNAP_DR \
  --availability-zone eu-west-1b --volume-type gp3

Para restaurar en otra AZ de la misma región basta con create-volume con otra --availability-zone: un volumen EBS vive en una sola AZ, pero su snapshot es regional. Si usas esta alternativa, borra también esos snapshots y el volumen en la limpieza.

Comprueba que funciona

  • Has recibido correos de SNS: confirmación, lab20-pedidos-pendientes en ALARM y la prueba de lab20-cpu-alta.
  • aws cloudwatch describe-alarms --alarm-name-prefix lab20 lista 3 alarmas de métrica y 1 compuesta.
  • Logs Insights devolvió los tres mensajes ERROR y existe la métrica Lab20/Tienda ErroresApp.
  • El dashboard lab20 muestra CPU y métricas de negocio.
  • En AWS Backup → Backup vaults hay un punto de recuperación en lab20-primaria (eu-south-2) y otro en lab20-dr (eu-west-1), y el volumen restaurado existe en eu-west-1.

Limpieza

Orden: primero la región de DR, luego la primaria y por último IAM. Borrar un punto de recuperación de EBS elimina su snapshot.

aws ec2 delete-volume --region $REGION_DR --volume-id $VOL_DR
aws backup delete-recovery-point --region $REGION_DR \
  --backup-vault-name lab20-dr --recovery-point-arn $PUNTO_DR
aws backup delete-recovery-point \
  --backup-vault-name lab20-primaria --recovery-point-arn $PUNTO

sleep 30
aws backup delete-backup-vault --region $REGION_DR --backup-vault-name lab20-dr
aws backup delete-backup-vault --backup-vault-name lab20-primaria

aws cloudwatch delete-alarms --alarm-names lab20-compuesta
aws cloudwatch delete-alarms --alarm-names lab20-cpu-alta lab20-recuperar-sistema lab20-pedidos-pendientes
aws cloudwatch delete-dashboards --dashboard-names lab20
aws logs delete-log-group --log-group-name /lab20/app

aws ec2 terminate-instances --instance-ids $INSTANCIA
aws ec2 wait instance-terminated --instance-ids $INSTANCIA

aws sns delete-topic --topic-arn $TEMA_ARN

aws iam detach-role-policy --role-name lab20-backup-rol \
  --policy-arn arn:aws:iam::aws:policy/service-role/AWSBackupServiceRolePolicyForBackup
aws iam detach-role-policy --role-name lab20-backup-rol \
  --policy-arn arn:aws:iam::aws:policy/service-role/AWSBackupServiceRolePolicyForRestores
aws iam delete-role --role-name lab20-backup-rol

cd ~ && rm -rf ~/lab20

Notas: si delete-backup-vault falla porque la bóveda no está vacía, espera un minuto (el borrado del punto de recuperación es asíncrono) y repite. La alarma compuesta se borra antes que las alarmas que la componen. Las métricas personalizadas no se pueden borrar: caducan solas. Comprueba que no quedan volúmenes ni snapshots del lab:

aws ec2 describe-volumes --filters Name=tag:Proyecto,Values=lab20 --query 'Volumes[*].VolumeId'
aws ec2 describe-volumes --region $REGION_DR --query 'Volumes[*].[VolumeId,State]'
aws ec2 describe-snapshots --owner-ids self --region $REGION_DR --query 'Snapshots[*].SnapshotId'
aws ec2 describe-snapshots --owner-ids self --query 'Snapshots[*].[SnapshotId,Description]'

Preguntas para pensar como arquitecto

  1. El negocio pide RPO de 1 hora y RTO de 8 horas para esta aplicación, al menor coste. ¿Qué configurarías con lo que has usado hoy?
Respuesta

Backup and restore: un backup plan de AWS Backup con una regla horaria (RPO de 1 hora), con copia automática a la bóveda de eu-west-1 y retención acorde; la infraestructura descrita en CloudFormation para recrearla en DR; AMI y cuotas preparadas en la región de DR; y pruebas periódicas de restauración (AWS Backup tiene restore testing). Un RTO de 8 horas no justifica pagar un pilot light ni un warm standby.

  1. ¿Por qué la alarma de CPU usa un periodo de 300 s y qué harías para detectar picos de un minuto?
Respuesta

Con basic monitoring, EC2 publica cada 5 minutos: un periodo menor no tendría datos. Para granularidad de 1 minuto activarías detailed monitoring (de pago). Para resolución inferior al minuto necesitarías una métrica personalizada de alta resolución publicada por el agente o la aplicación.

  1. Quieres una alarma cuando la memoria de la instancia supere el 90 %. ¿Qué te falta?
Respuesta

EC2 no publica la memoria (el hipervisor no la ve). Instala y configura el CloudWatch agent en la instancia (por ejemplo, distribuido con Systems Manager) para enviar mem_used_percent al namespace CWAgent, y crea la alarma sobre esa métrica.

  1. Un atacante con credenciales de administración de la cuenta de producción podría borrar las copias de seguridad. ¿Cómo lo mitigas?
Respuesta

Copia los puntos de recuperación a una cuenta de backup separada (copias entre cuentas de AWS Backup dentro de AWS Organizations) y protege las bóvedas con AWS Backup Vault Lock en modo compliance (WORM: nadie, ni el raíz, puede borrar antes de la retención). Añade SCP que impidan desactivar o borrar recursos de backup y alertas de CloudTrail/EventBridge sobre esas acciones.

  1. La alarma de recuperación usa StatusCheckFailed_System. ¿Por qué no StatusCheckFailed_Instance?
Respuesta

La acción recover resuelve problemas del host físico (pérdida de red o de energía, fallo de hardware), que son los que detecta la comprobación del sistema. Un fallo de la comprobación de instancia (sistema operativo bloqueado, disco lleno, configuración de red errónea) no se arregla moviendo la instancia a otro hardware: la acción adecuada sería reboot o investigar el sistema operativo.


Volver al módulo