Lab 4: Notificaciones SNS + Lambda Failover
Lab 4: Notificaciones SNS + Lambda Failover
En este lab conectamos todos los piezas: cuando el Health Check detecta que el primario cae, SNS envía un email/SMS y Lambda ejecuta el failover cambiando el Routing Control automáticamente.
Flujo completo
Health Check falla
│
▼
CloudWatch Alarm → "ALARM"
│
├──► SNS Topic → Email (notificación instantánea)
│ → SMS (opcional)
│
└──► Lambda Function "failover-handler"
│
├── Cambia Primary RC → OFF
├── Cambia Secondary RC → ON
└── Publica confirmación en SNSPaso 1: Variables de entorno
export AWS_REGION="us-east-1"
export AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
# Recuperar ARNs del Lab 3
export PRIMARY_RC_ARN=$(aws cloudformation describe-stacks \
--stack-name route53-arc-routing --region $AWS_REGION \
--query 'Stacks[0].Outputs[?OutputKey==`PrimaryRoutingControlArn`].OutputValue' \
--output text)
export SECONDARY_RC_ARN=$(aws cloudformation describe-stacks \
--stack-name route53-arc-routing --region $AWS_REGION \
--query 'Stacks[0].Outputs[?OutputKey==`SecondaryRoutingControlArn`].OutputValue' \
--output text)
export ARC_CLUSTER_ARN=$(aws cloudformation describe-stacks \
--stack-name route53-arc-routing --region $AWS_REGION \
--query 'Stacks[0].Outputs[?OutputKey==`ARCClusterArn`].OutputValue' \
--output text)
export CW_ALARM_NAME=$(aws cloudformation describe-stacks \
--stack-name route53-arc-routing --region $AWS_REGION \
--query 'Stacks[0].Outputs[?OutputKey==`CloudWatchAlarmName`].OutputValue' \
--output text)
# IMPORTANTE: reemplaza con tu email real
export NOTIFICATION_EMAIL="tu-email@ejemplo.com"
# OPCIONAL: reemplaza con tu número (formato internacional)
export NOTIFICATION_PHONE="+54911XXXXXXXX"
echo "Primary RC: $PRIMARY_RC_ARN"
echo "Secondary RC: $SECONDARY_RC_ARN"
echo "CW Alarm: $CW_ALARM_NAME"Aviso
Reemplaza tu-email@ejemplo.com con tu email real antes de continuar. Recibirás un email de confirmación de AWS SNS que debes aceptar para que las notificaciones lleguen.
Paso 2: Crear el código Lambda
mkdir -p /tmp/lambda-failover
cat > /tmp/lambda-failover/handler.py << 'LAMBDA_EOF'
"""
roLearning – Route53 ARC Workshop
Lambda: Failover Handler
Triggered by: CloudWatch Alarm (via SNS)
Action: Cambia el Routing Control primario → OFF y secundario → ON
"""
import json
import os
import boto3
import logging
logger = logging.getLogger()
logger.setLevel(logging.INFO)
# Variables de entorno (inyectadas por CloudFormation)
PRIMARY_RC_ARN = os.environ.get('PRIMARY_RC_ARN')
SECONDARY_RC_ARN = os.environ.get('SECONDARY_RC_ARN')
CLUSTER_ENDPOINT = os.environ.get('CLUSTER_ENDPOINT')
SNS_TOPIC_ARN = os.environ.get('SNS_TOPIC_ARN')
REGION = os.environ.get('AWS_REGION', 'us-east-1')
def get_arc_client():
"""Crea cliente de ARC Recovery Cluster con el endpoint del cluster."""
return boto3.client(
'route53-recovery-cluster',
endpoint_url=CLUSTER_ENDPOINT,
region_name=REGION
)
def get_current_state(arc_client, routing_control_arn):
"""Obtiene el estado actual de un Routing Control."""
response = arc_client.get_routing_control_state(
RoutingControlArn=routing_control_arn
)
return response['RoutingControlState']
def execute_failover(arc_client):
"""Ejecuta el failover: primario OFF, secundario ON."""
logger.info("Ejecutando failover: Primario → OFF, Secundario → ON")
arc_client.update_routing_control_states(
UpdateRoutingControlStateEntries=[
{
'RoutingControlArn': PRIMARY_RC_ARN,
'RoutingControlState': 'Off'
},
{
'RoutingControlArn': SECONDARY_RC_ARN,
'RoutingControlState': 'On'
}
]
)
logger.info("✅ Failover ejecutado exitosamente")
def notify_sns(message, subject):
"""Publica un mensaje en el topic SNS."""
if not SNS_TOPIC_ARN:
logger.warning("SNS_TOPIC_ARN no configurado, omitiendo notificación")
return
sns = boto3.client('sns', region_name=REGION)
sns.publish(
TopicArn=SNS_TOPIC_ARN,
Subject=subject,
Message=message
)
logger.info(f"Notificación SNS enviada: {subject}")
def lambda_handler(event, context):
"""
Handler principal de Lambda.
Recibe eventos de CloudWatch Alarm vía SNS.
"""
logger.info(f"Evento recibido: {json.dumps(event)}")
# Extraer mensaje de la alarma
alarm_state = "UNKNOWN"
alarm_name = "route53-arc-primary-unhealthy"
try:
# El evento viene de SNS → mensaje JSON de CloudWatch
for record in event.get('Records', []):
if record.get('EventSource') == 'aws:sns':
sns_message = json.loads(record['Sns']['Message'])
alarm_state = sns_message.get('NewStateValue', 'UNKNOWN')
alarm_name = sns_message.get('AlarmName', alarm_name)
logger.info(f"Alarma: {alarm_name} | Estado: {alarm_state}")
except (KeyError, json.JSONDecodeError) as e:
logger.warning(f"No se pudo parsear el evento SNS: {e}")
# Si se invoca manualmente, continuamos de todos modos
alarm_state = event.get('alarm_state', 'ALARM')
if alarm_state != 'ALARM':
logger.info(f"Estado no es ALARM ({alarm_state}), sin acción.")
return {
'statusCode': 200,
'body': f'No action needed. Alarm state: {alarm_state}'
}
# Verificar estado actual de los Routing Controls
arc_client = get_arc_client()
primary_state = get_current_state(arc_client, PRIMARY_RC_ARN)
secondary_state = get_current_state(arc_client, SECONDARY_RC_ARN)
logger.info(f"Estado actual - Primario: {primary_state} | Secundario: {secondary_state}")
# Evitar failover redundante
if primary_state == 'Off' and secondary_state == 'On':
logger.info("Failover ya está activo. Sin cambios necesarios.")
return {
'statusCode': 200,
'body': 'Failover already active'
}
# Ejecutar failover
execute_failover(arc_client)
# Notificar resultado
notify_sns(
subject="🚨 [FAILOVER ACTIVADO] Sitio primario caído",
message=(
f"ALERTA: El sitio web primario no responde.\n\n"
f"Acción tomada: Failover automático ejecutado.\n"
f"- Routing Control Primario: OFF\n"
f"- Routing Control Secundario: ON\n\n"
f"El tráfico fue redirigido a la página de mantenimiento (CloudFront + S3).\n\n"
f"Alarma: {alarm_name}\n"
f"Hora: {context.aws_request_id}\n\n"
f"Para recuperar el sitio primario, ejecuta el Lab 5 (Failback).\n"
f"roLearning – Route53 ARC Workshop"
)
)
return {
'statusCode': 200,
'body': json.dumps({
'action': 'FAILOVER_EXECUTED',
'primary_new_state': 'Off',
'secondary_new_state': 'On'
})
}
LAMBDA_EOF
# Empaquetar en ZIP
cd /tmp/lambda-failover && zip -r /tmp/failover-handler.zip handler.py
echo "✅ Lambda package creado: $(ls -lh /tmp/failover-handler.zip | awk '{print $5}')"Paso 3: Subir Lambda a S3 y desplegar el stack
# Crear bucket temporal para el código Lambda
LAMBDA_BUCKET="route53-arc-lambda-${AWS_ACCOUNT_ID}"
aws s3 mb s3://$LAMBDA_BUCKET --region $AWS_REGION 2>/dev/null || true
# Subir el ZIP
aws s3 cp /tmp/failover-handler.zip s3://$LAMBDA_BUCKET/failover-handler.zip \
--region $AWS_REGION
echo "✅ Lambda ZIP subido a s3://$LAMBDA_BUCKET/failover-handler.zip"Paso 4: Crear el template CloudFormation para SNS + Lambda
cat > /tmp/sns-lambda.yaml << 'TEMPLATE_EOF'
AWSTemplateFormatVersion: '2010-09-09'
Description: 'roLearning - Route53 ARC Workshop - SNS + Lambda Failover'
Parameters:
NotificationEmail:
Type: String
Description: "Email para recibir alertas SNS"
NotificationPhone:
Type: String
Default: ""
Description: "Número de teléfono para SMS (formato +54911...) - opcional"
PrimaryRCAarn:
Type: String
Description: "ARN del Routing Control primario"
SecondaryRCAarn:
Type: String
Description: "ARN del Routing Control secundario"
ARCClusterArn:
Type: String
Description: "ARN del Cluster ARC"
CloudWatchAlarmName:
Type: String
Description: "Nombre de la alarma CloudWatch del Lab 3"
LambdaBucket:
Type: String
Description: "Bucket S3 con el código Lambda"
Conditions:
HasPhone: !Not [!Equals [!Ref NotificationPhone, ""]]
Resources:
# ──────────────────────────────────────────────
# SNS Topic para alertas
# ──────────────────────────────────────────────
AlertTopic:
Type: AWS::SNS::Topic
Properties:
TopicName: route53-arc-alerts
DisplayName: "Route53 ARC - Alertas de Disponibilidad"
Tags:
- Key: Workshop
Value: route53-arc
EmailSubscription:
Type: AWS::SNS::Subscription
Properties:
TopicArn: !Ref AlertTopic
Protocol: email
Endpoint: !Ref NotificationEmail
SMSSubscription:
Type: AWS::SNS::Subscription
Condition: HasPhone
Properties:
TopicArn: !Ref AlertTopic
Protocol: sms
Endpoint: !Ref NotificationPhone
# ──────────────────────────────────────────────
# IAM Role para Lambda
# ──────────────────────────────────────────────
LambdaExecutionRole:
Type: AWS::IAM::Role
Properties:
RoleName: route53-arc-lambda-failover-role
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: lambda.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
Policies:
- PolicyName: ARCFailoverPolicy
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- route53-recovery-cluster:GetRoutingControlState
- route53-recovery-cluster:UpdateRoutingControlStates
Resource: "*"
- Effect: Allow
Action:
- sns:Publish
Resource: !Ref AlertTopic
Tags:
- Key: Workshop
Value: route53-arc
# ──────────────────────────────────────────────
# Lambda Function – Failover Handler
# ──────────────────────────────────────────────
FailoverLambda:
Type: AWS::Lambda::Function
Properties:
FunctionName: route53-arc-failover-handler
Runtime: python3.12
Handler: handler.lambda_handler
Role: !GetAtt LambdaExecutionRole.Arn
Timeout: 60
MemorySize: 128
Code:
S3Bucket: !Ref LambdaBucket
S3Key: failover-handler.zip
Environment:
Variables:
PRIMARY_RC_ARN: !Ref PrimaryRCAarn
SECONDARY_RC_ARN: !Ref SecondaryRCAarn
CLUSTER_ENDPOINT: !Sub
- "https://${Endpoint}"
- Endpoint: !Select
[2, !Split ["/", !Select [0, !Split [":", !Ref ARCClusterArn]]]]
SNS_TOPIC_ARN: !Ref AlertTopic
Tags:
- Key: Workshop
Value: route53-arc
# ──────────────────────────────────────────────
# Permiso para SNS → Lambda
# ──────────────────────────────────────────────
LambdaSNSPermission:
Type: AWS::Lambda::Permission
Properties:
FunctionName: !GetAtt FailoverLambda.Arn
Action: lambda:InvokeFunction
Principal: sns.amazonaws.com
SourceArn: !Ref AlertTopic
# ──────────────────────────────────────────────
# Suscripción SNS → Lambda (el trigger)
# ──────────────────────────────────────────────
LambdaSNSSubscription:
Type: AWS::SNS::Subscription
Properties:
TopicArn: !Ref AlertTopic
Protocol: lambda
Endpoint: !GetAtt FailoverLambda.Arn
# ──────────────────────────────────────────────
# Conectar la Alarma CloudWatch al SNS Topic
# ──────────────────────────────────────────────
AlarmSNSAction:
Type: AWS::CloudWatch::Alarm
Properties:
AlarmName: !Sub "${CloudWatchAlarmName}-with-sns"
AlarmDescription: "Sitio primario caído - dispara SNS + Lambda failover"
Namespace: AWS/Route53
MetricName: HealthCheckStatus
Dimensions:
- Name: HealthCheckId
Value:
Fn::ImportValue: route53-arc-hc-id
Statistic: Minimum
Period: 60
EvaluationPeriods: 3
Threshold: 1
ComparisonOperator: LessThanThreshold
TreatMissingData: breaching
AlarmActions:
- !Ref AlertTopic
OKActions:
- !Ref AlertTopic
Outputs:
SNSTopicArn:
Description: "ARN del Topic SNS de alertas"
Value: !Ref AlertTopic
Export:
Name: route53-arc-sns-topic
LambdaArn:
Description: "ARN de la Lambda de failover"
Value: !GetAtt FailoverLambda.Arn
Export:
Name: route53-arc-lambda-arn
LambdaName:
Description: "Nombre de la Lambda"
Value: !Ref FailoverLambda
Export:
Name: route53-arc-lambda-name
TEMPLATE_EOF
echo "✅ Template SNS+Lambda creado"Paso 5: Desplegar el stack
# Necesitamos el cluster endpoint
CLUSTER_ENDPOINT=$(aws route53-recovery-control-config describe-cluster \
--cluster-arn $ARC_CLUSTER_ARN \
--region $AWS_REGION \
--query 'Cluster.ClusterEndpoints[0].Endpoint' \
--output text)
aws cloudformation deploy \
--template-file /tmp/sns-lambda.yaml \
--stack-name route53-arc-automation \
--capabilities CAPABILITY_NAMED_IAM \
--region $AWS_REGION \
--parameter-overrides \
NotificationEmail=$NOTIFICATION_EMAIL \
PrimaryRCAarn=$PRIMARY_RC_ARN \
SecondaryRCAarn=$SECONDARY_RC_ARN \
ARCClusterArn=$ARC_CLUSTER_ARN \
CloudWatchAlarmName=$CW_ALARM_NAME \
LambdaBucket=route53-arc-lambda-${AWS_ACCOUNT_ID} \
--tags Workshop=route53-arc
echo "✅ Stack SNS+Lambda desplegado"Paso 6: Confirmar suscripción de email
Aviso
Paso obligatorio: Revisa tu email $NOTIFICATION_EMAIL. Habrá llegado un mensaje con asunto “AWS Notification - Subscription Confirmation”. Haz clic en “Confirm subscription” para activar las notificaciones.
✅ Verificación del Lab 4
echo "=== Verificación Lab 4: SNS + Lambda Failover ==="
LAMBDA_NAME=$(aws cloudformation describe-stacks \
--stack-name route53-arc-automation --region $AWS_REGION \
--query 'Stacks[0].Outputs[?OutputKey==`LambdaName`].OutputValue' \
--output text)
SNS_ARN=$(aws cloudformation describe-stacks \
--stack-name route53-arc-automation --region $AWS_REGION \
--query 'Stacks[0].Outputs[?OutputKey==`SNSTopicArn`].OutputValue' \
--output text)
# 1. Lambda existe y está activa
LAMBDA_STATE=$(aws lambda get-function \
--function-name $LAMBDA_NAME --region $AWS_REGION \
--query 'Configuration.State' --output text 2>/dev/null)
[ "$LAMBDA_STATE" = "Active" ] \
&& echo "✅ Lambda Function: $LAMBDA_STATE" \
|| echo "❌ Lambda State: $LAMBDA_STATE"
# 2. SNS Topic existe
aws sns get-topic-attributes \
--topic-arn $SNS_ARN --region $AWS_REGION \
--query 'Attributes.TopicArn' --output text > /dev/null 2>&1 \
&& echo "✅ SNS Topic: OK" \
|| echo "❌ SNS Topic no encontrado"
# 3. Test manual de la Lambda (sin failover real)
echo ""
echo "Probando Lambda con evento de prueba (sin cambiar Routing Controls)..."
aws lambda invoke \
--function-name $LAMBDA_NAME \
--region $AWS_REGION \
--payload '{"alarm_state": "OK", "test": true}' \
--cli-binary-format raw-in-base64-out \
/tmp/lambda-test-output.json
echo "Respuesta Lambda:"
cat /tmp/lambda-test-output.json
echo ""
echo "📌 Guarda estos valores:"
echo " LAMBDA_NAME = $LAMBDA_NAME"
echo " SNS_ARN = $SNS_ARN"Siguiente paso → Lab 5: Simulación de Incidente