Lab 4: Notificaciones SNS + Lambda Failover

Lab 4: Notificaciones SNS + Lambda Failover & 1-Click Failback

En este laboratorio conectamos la automatización completa:

  1. CloudWatch Alarm → SNS Topic: Cuando el sitio primario falla, CloudWatch notifica a SNS.
  2. SNS → Email / SMS: Envía una alerta inmediata al equipo de operaciones.
  3. SNS → Lambda Failover (handler.py): Ejecuta el failover automático cambiando el Routing Control Primario a OFF y Secundario a ON.
  4. Lambda Recovery Detection (OK): Notifica cuando el sitio primario vuelve a estar en verde pero sin hacer failback automático (para prevenir flapping).
  5. Lambda 1-Click Failback (failback_handler.py): Función con Function URL pública que permite realizar el failback con un solo clic desde el correo de notificación, más un documento de SSM Automation.

Cargar variables de entorno

source ~/global.env
echo "Email de notificación: $NOTIFICATION_EMAIL"

Código Python de las Funciones Lambda

1. lambda/handler.py (Failover Handler & Recovery Notifier)

mkdir -p /tmp/lambda-arc

cat > /tmp/lambda-arc/handler.py << 'EOF'
"""
roLearning – Route53 ARC Workshop
Lambda: Failover Handler & Recovery Notifier
"""
import json, os, boto3, logging

logger = logging.getLogger()
logger.setLevel(logging.INFO)

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")
FAILBACK_URL     = os.environ.get("FAILBACK_URL", "")
REGION           = os.environ.get("AWS_REGION", "us-east-1")

def arc_client():
    return boto3.client("route53-recovery-cluster", endpoint_url=CLUSTER_ENDPOINT, region_name=REGION)

def get_state(client, arn):
    return client.get_routing_control_state(RoutingControlArn=arn)["RoutingControlState"]

def execute_failover(client):
    client.update_routing_control_states(
        UpdateRoutingControlStateEntries=[
            {"RoutingControlArn": PRIMARY_RC_ARN,   "RoutingControlState": "Off"},
            {"RoutingControlArn": SECONDARY_RC_ARN, "RoutingControlState": "On"},
        ]
    )
    logger.info("Failover automatico ejecutado")

def notify(subject, message):
    if SNS_TOPIC_ARN:
        boto3.client("sns", region_name=REGION).publish(
            TopicArn=SNS_TOPIC_ARN, Subject=subject, Message=message)

def lambda_handler(event, context):
    logger.info(json.dumps(event))
    alarm_state = "UNKNOWN"
    try:
        for r in event.get("Records", []):
            if r.get("EventSource") == "aws:sns":
                msg_body = json.loads(r["Sns"]["Message"])
                alarm_state = msg_body.get("NewStateValue", "UNKNOWN")
    except Exception:
        alarm_state = event.get("alarm_state", "UNKNOWN")

    arc = arc_client()

    if alarm_state == "OK":
        if get_state(arc, PRIMARY_RC_ARN) == "Off":
            if FAILBACK_URL:
                failback_section = (
                    "\n FAILBACK 1-CLICK:\n" + FAILBACK_URL + "\n\n"
                    "Abri este enlace en tu navegador para devolver el trafico al sitio primario.\n"
                    "No requiere comandos ni acceso a la consola de AWS.\n"
                )
            else:
                failback_section = (
                    "\nPara ejecutar el failback, inicia el documento SSM 'route53-arc-failback-ssm' "
                    "desde AWS Systems Manager -> Automation.\n"
                )
            notify(
                subject="[SITIO PRIMARIO RECUPERADO] Listo para Failback 1-Click",
                message=(
                    "AVISO DE RECUPERACION\n"
                    "======================================\n"
                    "El sitio primario vuelve a estar SALUDABLE (Health Check OK).\n\n"
                    "ATENCION: El trafico SE MANTIENE en el sitio de contingencia por seguridad.\n"
                    "El failback es un proceso deliberado que requiere tu aprobacion.\n"
                    + failback_section +
                    "======================================\n"
                    "roLearning - AWS Route 53 ARC Workshop"
                )
            )
            return {"statusCode": 200, "body": "RECOVERY_NOTIFICATION_SENT"}
        return {"statusCode": 200, "body": "PRIMARY_ALREADY_ON"}

    if alarm_state == "ALARM":
        if get_state(arc, PRIMARY_RC_ARN) == "Off":
            return {"statusCode": 200, "body": "Already failed over"}
        execute_failover(arc)
        notify(
            subject="[FAILOVER ACTIVADO] Sitio primario caido - trafico redirigido",
            message=(
                "ALERTA DE INCIDENTE\n"
                "======================================\n"
                "Failover automatico ejecutado por caida del sitio primario.\n\n"
                "Estado actual de Routing Controls:\n"
                "  - Primario:   OFF\n"
                "  - Secundario: ON\n\n"
                "El trafico fue redirigido al sitio de contingencia.\n"
                "Cuando el sitio primario se recupere, recibiras otro email\n"
                "con el enlace de Failback 1-Click.\n"
                "======================================\n"
                "roLearning - AWS Route 53 ARC Workshop"
            )
        )
        return {"statusCode": 200, "body": "FAILOVER_EXECUTED"}

    return {"statusCode": 200, "body": f"No action for state: {alarm_state}"}
EOF

2. lambda/failback_handler.py (Failback 1-Click Handler)

cat > /tmp/lambda-arc/failback_handler.py << 'EOF'
"""
roLearning – Route53 ARC Workshop
Lambda: Failback 1-Click Handler (Function URL & SSM Automation)
"""
import json, os, boto3, logging

logger = logging.getLogger()
logger.setLevel(logging.INFO)

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")
REGION           = os.environ.get("AWS_REGION", "us-east-1")

def lambda_handler(event, context):
    try:
        arc = boto3.client(
            "route53-recovery-cluster",
            endpoint_url=CLUSTER_ENDPOINT,
            region_name=REGION)
        arc.update_routing_control_states(
            UpdateRoutingControlStateEntries=[
                {"RoutingControlArn": PRIMARY_RC_ARN,   "RoutingControlState": "On"},
                {"RoutingControlArn": SECONDARY_RC_ARN, "RoutingControlState": "Off"},
            ]
        )
        html = """<!DOCTYPE html>
<html lang="es">
<head><meta charset="UTF-8"><title>Failback Exitoso</title>
<style>
  body{font-family:sans-serif;background:#0f172a;color:#e2e8f0;
       display:flex;align-items:center;justify-content:center;height:100vh;margin:0}
  .card{background:#1e293b;padding:40px;border-radius:12px;
        text-align:center;box-shadow:0 4px 20px rgba(0,0,0,.5)}
  h1{color:#22c55e}
</style></head>
<body><div class="card">
  <h1>Failback Exitoso</h1>
  <p>El trafico ha sido devuelto al Sitio Primario (EC2 + ALB).</p>
  <p>Routing Controls: Primario = <strong>ON</strong> | Secundario = <strong>OFF</strong></p>
  <p style="font-size:.8em;color:#64748b">roLearning - AWS Route 53 ARC Workshop</p>
</div></body></html>"""
        return {"statusCode": 200, "headers": {"Content-Type": "text/html"}, "body": html}
    except Exception as e:
        logger.error(f"Error: {e}")
        return {"statusCode": 500, "body": f"Error: {str(e)}"}
EOF

cd /tmp/lambda-arc && zip -q /tmp/failover-handler.zip handler.py failback_handler.py

Opción A: CloudFormation (Base)

1. Subir paquete Lambda a S3 y crear template

LAMBDA_BUCKET="route53-arc-lambda-${AWS_ACCOUNT_ID}"
aws s3 mb s3://$LAMBDA_BUCKET --region $AWS_REGION 2>/dev/null || true
aws s3 cp /tmp/failover-handler.zip s3://$LAMBDA_BUCKET/failover-handler.zip --region $AWS_REGION

cat > /tmp/sns-lambda.yaml << 'TEMPLATE_EOF'
AWSTemplateFormatVersion: '2010-09-09'
Description: 'roLearning - Route53 ARC Workshop - SNS + Lambda Failover & Failback'

Parameters:
  NotificationEmail:   { Type: String }
  PrimaryRCAarn:       { Type: String }
  SecondaryRCAarn:     { Type: String }
  ClusterEndpoint:     { Type: String, Description: "Endpoint del ARC Cluster (obtenido con describe-cluster)" }
  CloudWatchAlarmName: { Type: String }
  LambdaBucket:        { Type: String }

Resources:
  AlertTopic:
    Type: AWS::SNS::Topic
    Properties:
      TopicName: route53-arc-alerts
      DisplayName: "Route53 ARC - Alertas"

  EmailSubscription:
    Type: AWS::SNS::Subscription
    Properties:
      TopicArn: !Ref AlertTopic
      Protocol: email
      Endpoint: !Ref NotificationEmail

  LambdaExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: route53-arc-lambda-failover-role-cfn
      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

  # Failback se crea primero para obtener su URL e inyectarla en el Failover handler
  FailbackLambda:
    Type: AWS::Lambda::Function
    Properties:
      FunctionName: route53-arc-failback-handler
      Runtime: python3.12
      Handler: failback_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: !Ref ClusterEndpoint

  FailbackLambdaUrl:
    Type: AWS::Lambda::Url
    Properties:
      TargetFunctionArn: !GetAtt FailbackLambda.Arn
      AuthType: NONE

  FailbackLambdaUrlPermission:
    Type: AWS::Lambda::Permission
    Properties:
      FunctionName: !GetAtt FailbackLambda.Arn
      Action: lambda:InvokeFunctionUrl
      Principal: "*"
      FunctionUrlAuthType: NONE

  FailoverLambda:
    Type: AWS::Lambda::Function
    DependsOn: FailbackLambdaUrl
    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: !Ref ClusterEndpoint
          SNS_TOPIC_ARN:    !Ref AlertTopic
          FAILBACK_URL:     !GetAtt FailbackLambdaUrl.FunctionUrl

  LambdaSNSPermission:
    Type: AWS::Lambda::Permission
    Properties:
      FunctionName: !GetAtt FailoverLambda.Arn
      Action: lambda:InvokeFunction
      Principal: sns.amazonaws.com
      SourceArn: !Ref AlertTopic

  LambdaSNSSubscription:
    Type: AWS::SNS::Subscription
    Properties:
      TopicArn: !Ref AlertTopic
      Protocol: lambda
      Endpoint: !GetAtt FailoverLambda.Arn

  AlarmSNSAction:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmName: !Sub "${CloudWatchAlarmName}-with-sns"
      AlarmDescription: "Sitio primario caido - 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]

  SSMFailbackDocument:
    Type: AWS::SSM::Document
    Properties:
      Name: route53-arc-failback-ssm
      DocumentType: Automation
      Content:
        schemaVersion: '0.3'
        description: "Ejecutar Failback 1-Click para AWS Route 53 ARC"
        mainSteps:
          - name: InvokeFailbackLambda
            action: aws:invokeLambdaFunction
            inputs:
              FunctionName: !Ref FailbackLambda
              Payload: "{}"

Outputs:
  SNSTopicArn:
    Value: !Ref AlertTopic
    Export: { Name: route53-arc-sns-topic }
  LambdaName:
    Value: !Ref FailoverLambda
    Export: { Name: route53-arc-lambda-name }
  FailbackLambdaName:
    Value: !Ref FailbackLambda
    Export: { Name: route53-arc-failback-lambda-name }
  FailbackUrl:
    Value: !GetAtt FailbackLambdaUrl.FunctionUrl
    Export: { Name: route53-arc-failback-url }
TEMPLATE_EOF

2. Desplegar Stack CloudFormation

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)

# Obtener el endpoint real del ARC Cluster — NO se puede derivar del ARN
export CLUSTER_ENDPOINT=$(aws route53-recovery-control-config describe-cluster \
  --cluster-arn $ARC_CLUSTER_ARN --region $AWS_REGION \
  --query 'Cluster.ClusterEndpoints[?Region==`us-east-1`].Endpoint' \
  --output text)
echo "✅ CLUSTER_ENDPOINT: $CLUSTER_ENDPOINT"

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 \
    ClusterEndpoint=$CLUSTER_ENDPOINT \
    CloudWatchAlarmName=$CW_ALARM_NAME \
    LambdaBucket=$LAMBDA_BUCKET

export LAMBDA_NAME="route53-arc-failover-handler"
export FAILBACK_URL=$(aws cloudformation describe-stacks \
  --stack-name route53-arc-automation --region $AWS_REGION \
  --query 'Stacks[0].Outputs[?OutputKey==`FailbackUrl`].OutputValue' \
  --output text 2>/dev/null || echo "")
echo "🔗 Failback URL: $FAILBACK_URL"

# Persistir en global.env
grep -q "LAMBDA_NAME" ~/global.env 2>/dev/null && \
  { sed -i '' "s|.*LAMBDA_NAME.*|export LAMBDA_NAME=\"${LAMBDA_NAME}\"|" ~/global.env 2>/dev/null || sed -i "s|.*LAMBDA_NAME.*|export LAMBDA_NAME=\"${LAMBDA_NAME}\"|" ~/global.env; } || \
  echo "export LAMBDA_NAME=\"${LAMBDA_NAME}\"" >> ~/global.env
echo "✅ LAMBDA_NAME guardado en ~/global.env"
Aviso

Paso Obligatorio — confirmá la suscripción SNS ahora: Revisá tu casilla de email ($NOTIFICATION_EMAIL). Buscá el correo con asunto “AWS Notification - Subscription Confirmation” y hacé clic en “Confirm subscription”. Sin este paso, las alertas de failover no llegarán. El email puede tardar unos minutos en llegar.


Opción B: Terraform (Segunda Opción)

Detalles

🚫 Opción B requiere terminal local — no uses CloudShell con Terraform. Ver Lab 0 para más detalles.

1. Configurar Módulo Terraform

cd ~/route53-arc-tf/04_automation
mkdir -p lambda
cp /tmp/lambda-arc/handler.py lambda/handler.py
cp /tmp/lambda-arc/failback_handler.py lambda/failback_handler.py
cd lambda && zip -q ../failover.zip handler.py failback_handler.py && cd ..

cat > variables.tf << 'EOF'
variable "aws_region"         { type = string; default = "us-east-1" }
variable "notification_email" { type = string }
variable "project_tag"        { type = string; default = "route53-arc" }
EOF

cat > main.tf << 'EOF'
terraform {
  required_version = ">= 1.5"
  required_providers {
    aws = { source = "hashicorp/aws", version = "~> 5.0" }
  }
}

provider "aws" {
  region = var.aws_region
  default_tags { tags = { Workshop = var.project_tag, ManagedBy = "Terraform" } }
}

data "aws_caller_identity" "current" {}

data "terraform_remote_state" "arc" {
  backend = "local"
  config  = { path = "../03_arc/terraform.tfstate" }
}

locals {
  primary_rc_arn   = data.terraform_remote_state.arc.outputs.primary_rc_arn
  secondary_rc_arn = data.terraform_remote_state.arc.outputs.secondary_rc_arn
  cluster_endpoint = data.terraform_remote_state.arc.outputs.arc_cluster_endpoints[0]["Endpoint"]
  hc_id            = data.terraform_remote_state.arc.outputs.health_check_id
  cw_alarm_base    = data.terraform_remote_state.arc.outputs.cloudwatch_alarm_name
}

resource "aws_sns_topic" "alerts" {
  name         = "route53-arc-alerts"
  display_name = "Route53 ARC - Alertas"
}

resource "aws_sns_topic_subscription" "email" {
  topic_arn = aws_sns_topic.alerts.arn
  protocol  = "email"
  endpoint  = var.notification_email
}

resource "aws_s3_bucket" "lambda_code" {
  bucket        = "route53-arc-lambda-${data.aws_caller_identity.current.account_id}"
  force_destroy = true
}

resource "aws_s3_object" "failover_zip" {
  bucket = aws_s3_bucket.lambda_code.id
  key    = "failover-handler.zip"
  source = "${path.module}/failover.zip"
  etag   = filemd5("${path.module}/failover.zip")
}

resource "aws_iam_role" "lambda" {
  name = "route53-arc-lambda-failover-role-tf"
  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{ Effect = "Allow", Principal = { Service = "lambda.amazonaws.com" }, Action = "sts:AssumeRole" }]
  })
}

resource "aws_iam_role_policy_attachment" "basic" {
  role       = aws_iam_role.lambda.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}

resource "aws_iam_role_policy" "arc_sns" {
  name = "arc-sns-policy"
  role = aws_iam_role.lambda.id
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      { Effect = "Allow",
        Action = ["route53-recovery-cluster:GetRoutingControlState",
                  "route53-recovery-cluster:UpdateRoutingControlStates"],
        Resource = "*" },
      { Effect = "Allow", Action = "sns:Publish", Resource = aws_sns_topic.alerts.arn }
    ]
  })
}

# Failback se crea primero para que su URL pueda inyectarse en el Failover handler
resource "aws_lambda_function" "failback" {
  function_name = "route53-arc-failback-handler"
  runtime       = "python3.12"
  handler       = "failback_handler.lambda_handler"
  role          = aws_iam_role.lambda.arn
  timeout       = 60
  memory_size   = 128
  s3_bucket     = aws_s3_bucket.lambda_code.id
  s3_key        = aws_s3_object.failover_zip.key
  environment {
    variables = {
      PRIMARY_RC_ARN   = local.primary_rc_arn
      SECONDARY_RC_ARN = local.secondary_rc_arn
      CLUSTER_ENDPOINT = local.cluster_endpoint
    }
  }
}

resource "aws_lambda_function_url" "failback_url" {
  function_name      = aws_lambda_function.failback.function_name
  authorization_type = "NONE"
}

resource "aws_lambda_permission" "failback_url_perm" {
  statement_id           = "FunctionURLAllowPublicAccess"
  action                 = "lambda:InvokeFunctionUrl"
  function_name          = aws_lambda_function.failback.function_name
  principal              = "*"
  function_url_auth_type = "NONE"
}

resource "aws_lambda_function" "failover" {
  depends_on    = [aws_lambda_function_url.failback_url]
  function_name = "route53-arc-failover-handler"
  runtime       = "python3.12"
  handler       = "handler.lambda_handler"
  role          = aws_iam_role.lambda.arn
  timeout       = 60
  memory_size   = 128
  s3_bucket     = aws_s3_bucket.lambda_code.id
  s3_key        = aws_s3_object.failover_zip.key
  environment {
    variables = {
      PRIMARY_RC_ARN   = local.primary_rc_arn
      SECONDARY_RC_ARN = local.secondary_rc_arn
      CLUSTER_ENDPOINT = local.cluster_endpoint
      SNS_TOPIC_ARN    = aws_sns_topic.alerts.arn
      FAILBACK_URL     = aws_lambda_function_url.failback_url.function_url
    }
  }
}

resource "aws_lambda_permission" "sns_invoke" {
  statement_id  = "AllowSNSInvoke"
  action        = "lambda:InvokeFunction"
  function_name = aws_lambda_function.failover.function_name
  principal     = "sns.amazonaws.com"
  source_arn    = aws_sns_topic.alerts.arn
}

resource "aws_sns_topic_subscription" "lambda_sub" {
  topic_arn = aws_sns_topic.alerts.arn
  protocol  = "lambda"
  endpoint  = aws_lambda_function.failover.arn
}

resource "aws_ssm_document" "failback" {
  name          = "route53-arc-failback-ssm"
  document_type = "Automation"
  content = jsonencode({
    schemaVersion = "0.3"
    description   = "Ejecutar Failback 1-Click para AWS Route 53 ARC"
    mainSteps = [{
      name   = "InvokeFailbackLambda"
      action = "aws:invokeLambdaFunction"
      inputs = { FunctionName = aws_lambda_function.failback.function_name, Payload = "{}" }
    }]
  })
}

resource "aws_cloudwatch_metric_alarm" "with_action" {
  alarm_name          = "${local.cw_alarm_base}-with-sns"
  alarm_description   = "Sitio primario caido - dispara SNS + Lambda failover"
  namespace           = "AWS/Route53"
  metric_name         = "HealthCheckStatus"
  statistic           = "Minimum"
  period              = 60
  evaluation_periods  = 3
  threshold           = 1
  comparison_operator = "LessThanThreshold"
  treat_missing_data  = "breaching"
  dimensions          = { HealthCheckId = local.hc_id }
  alarm_actions       = [aws_sns_topic.alerts.arn]
  ok_actions          = [aws_sns_topic.alerts.arn]
}
EOF

cat > outputs.tf << 'EOF'
output "sns_topic_arn"        { value = aws_sns_topic.alerts.arn }
output "lambda_name"          { value = aws_lambda_function.failover.function_name }
output "failback_lambda_name" { value = aws_lambda_function.failback.function_name }
output "failback_url"         { value = aws_lambda_function_url.failback_url.function_url }
EOF

2. Aplicar los cambios en Terraform

terraform init
terraform apply -var="notification_email=$NOTIFICATION_EMAIL" -auto-approve

export LAMBDA_NAME=$(terraform output -raw lambda_name)
export FAILBACK_URL=$(terraform output -raw failback_url)
echo "🔗 Failback URL: $FAILBACK_URL"

# Persistir en global.env
grep -q "LAMBDA_NAME" ~/global.env 2>/dev/null && \
  { sed -i '' "s|.*LAMBDA_NAME.*|export LAMBDA_NAME=\"${LAMBDA_NAME}\"|" ~/global.env 2>/dev/null || sed -i "s|.*LAMBDA_NAME.*|export LAMBDA_NAME=\"${LAMBDA_NAME}\"|" ~/global.env; } || \
  echo "export LAMBDA_NAME=\"${LAMBDA_NAME}\"" >> ~/global.env
echo "✅ LAMBDA_NAME guardado en ~/global.env"
Aviso

Paso Obligatorio — confirmá la suscripción SNS ahora: Revisá tu casilla de email ($NOTIFICATION_EMAIL). Buscá el correo con asunto “AWS Notification - Subscription Confirmation” y hacé clic en “Confirm subscription”. Sin este paso, las alertas de failover no llegarán. El email puede tardar unos minutos en llegar.



✅ Verificación del Lab 4

echo "=== Verificación Lab 4: SNS + Lambda Failover & Failback ==="

aws lambda get-function --function-name $LAMBDA_NAME --region $AWS_REGION \
  --query 'Configuration.State' --output text | grep -q Active \
  && echo "✅ Lambda Failover ($LAMBDA_NAME): Active" \
  || echo "❌ Lambda Failover no encontrada"

aws lambda get-function --function-name route53-arc-failback-handler --region $AWS_REGION \
  --query 'Configuration.State' --output text | grep -q Active \
  && echo "✅ Lambda Failback: Active" \
  || echo "❌ Lambda Failback no encontrada"

# Verificar que la FAILBACK_URL esté inyectada en el Failover handler
aws lambda get-function-configuration \
  --function-name $LAMBDA_NAME --region $AWS_REGION \
  --query 'Environment.Variables.FAILBACK_URL' --output text \
  | grep -v "^None$" \
  && echo "✅ FAILBACK_URL inyectada en Lambda Failover" \
  || echo "❌ FAILBACK_URL falta en Lambda Failover"

# Probar invocación en seco (alarm_state=OK, sin failover previo — responderá PRIMARY_ALREADY_ON)
aws lambda invoke \
  --function-name $LAMBDA_NAME \
  --payload '{"alarm_state":"OK"}' \
  --cli-binary-format raw-in-base64-out \
  /tmp/lambda-test.json --region $AWS_REGION > /dev/null

echo "Respuesta del test Lambda:"; cat /tmp/lambda-test.json; echo
echo "ℹ️  Si la respuesta es PRIMARY_ALREADY_ON, es correcto — el primario está UP y no hay failover activo."

Aviso

Antes de avanzar al Lab 5: confirmá la suscripción SNS en tu casilla de email. Sin ese paso, no recibirás las alertas de failover. El email de confirmación puede tardar unos minutos en llegar.

Siguiente paso → Lab 5: Simulación de Incidente