Lab 3: Route 53 Health Checks + ARC

Lab 3: Route 53 Health Checks + ARC Routing Controls

El corazón del workshop. Configuramos la detección automática de caídas y el enrutamiento inteligente del tráfico.


Conceptos clave

Route 53 Health Check

Monitorea tu endpoint (el ALB) cada 30 segundos desde múltiples ubicaciones globales de AWS. Cuando detecta que el primario no responde, puede cambiar el DNS automáticamente.

Route 53 ARC – Application Recovery Controller

Componente Función
Cluster Plano de control global con alta disponibilidad
Control Panel Agrupa los Routing Controls lógicamente
Routing Control Interruptor on/off por sitio
Safety Rule Garantiza que siempre haya al menos un sitio activo
ARC Cluster
  └── Control Panel
        ├── Routing Control: "primary"    ON = tráfico al primario
        └── Routing Control: "secondary"  ON = tráfico al secundario
              └── Safety Rule: mínimo 1 siempre ON

Paso 1: Variables base

export AWS_REGION="us-east-1"
export AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)

# Recuperar outputs de labs anteriores
export PRIMARY_ALB_DNS=$(aws cloudformation describe-stacks \
  --stack-name route53-arc-primary --region $AWS_REGION \
  --query 'Stacks[0].Outputs[?OutputKey==`ALBDNSName`].OutputValue' \
  --output text)

export PRIMARY_ALB_HZ=$(aws cloudformation describe-stacks \
  --stack-name route53-arc-primary --region $AWS_REGION \
  --query 'Stacks[0].Outputs[?OutputKey==`ALBHostedZoneID`].OutputValue' \
  --output text)

export CF_DOMAIN=$(aws cloudformation describe-stacks \
  --stack-name route53-arc-secondary --region $AWS_REGION \
  --query 'Stacks[0].Outputs[?OutputKey==`CloudFrontDomain`].OutputValue' \
  --output text)

echo "ALB DNS:   $PRIMARY_ALB_DNS"
echo "CF Domain: $CF_DOMAIN"

Paso 2: ¿Usás un dominio propio? (opcional)

Tenés dos modos para este lab. Elegí el que corresponde a tu situación:


Modo A – Sin dominio propio (demo / prueba rápida)

El failover funciona exactamente igual sin dominio. El tráfico real en producción usaría un dominio, pero para aprender el mecanismo no es necesario. Los Routing Controls, Health Checks y la alarma CloudWatch funcionan independientemente del DNS.

# Modo sin dominio
export USE_DOMAIN="false"
export DOMAIN_NAME="workshop.internal"
export APP_SUBDOMAIN="app"
export HOSTED_ZONE_ID=""   # se creará una HZ privada

echo "Modo: sin dominio (demo)"

Modo B – Con dominio en Route 53 (experiencia real)

Si tenés un dominio registrado en AWS o delegado a Route 53, podés ver el failover funcionando en un browser real apuntando a tu URL.

# Modo con dominio real
export USE_DOMAIN="true"

# ─── EDITÁ ESTOS VALORES ───────────────────────────────────────
export DOMAIN_NAME="tudominio.com"      # ej: rofriday.com
export APP_SUBDOMAIN="app"              # resultado: app.tudominio.com
# ───────────────────────────────────────────────────────────────

# Buscar la Hosted Zone pública
export HOSTED_ZONE_ID=$(aws route53 list-hosted-zones-by-name \
  --dns-name "$DOMAIN_NAME." \
  --query "HostedZones[?Name=='${DOMAIN_NAME}.'].Id" \
  --output text | sed 's|/hostedzone/||')

if [ -z "$HOSTED_ZONE_ID" ] || [ "$HOSTED_ZONE_ID" = "None" ]; then
  echo "⚠️  No se encontró Hosted Zone para $DOMAIN_NAME"
  echo "   Opciones:"
  echo "   1. Registrá el dominio en Route 53 (Route 53 → Registered domains)"
  echo "   2. Creá una Hosted Zone y apuntá los NS desde tu registrador"
  echo "   3. Usá un subdominio: export DOMAIN_NAME='arc.tudominio.com'"
  echo "   4. Cambiá a Modo A (sin dominio) con: export USE_DOMAIN='false'"
else
  echo "✅ Hosted Zone: $HOSTED_ZONE_ID"
  echo "   URL del workshop: http://$APP_SUBDOMAIN.$DOMAIN_NAME"
fi
Consejo

¿Primera vez con Route 53? La forma más simple es ir a Route 53 → Registered domains → Register domain y comprar un dominio directamente en AWS (desde ~$12/año). La Hosted Zone se crea automáticamente y los NS quedan configurados solos.


Paso 3: Crear el template CloudFormation

El template adapta el comportamiento según si usás dominio o no:

cat > /tmp/route53-arc.yaml << 'TEMPLATE_EOF'
AWSTemplateFormatVersion: '2010-09-09'
Description: 'roLearning - Route53 ARC Workshop - Health Checks + ARC Routing Controls'

Parameters:

  ALBDNSName:
    Type: String

  ALBHostedZoneId:
    Type: String

  CloudFrontDomain:
    Type: String

  UseDomain:
    Type: String
    Default: "false"
    AllowedValues: ["true", "false"]
    Description: "true = usar dominio real con Public HZ | false = modo demo sin DNS público"

  DomainName:
    Type: String
    Default: "workshop.internal"

  AppSubdomain:
    Type: String
    Default: "app"

  HostedZoneId:
    Type: String
    Default: ""
    Description: "ID de la Public Hosted Zone (solo si UseDomain=true)"

Conditions:
  WithDomain:    !Equals [!Ref UseDomain, "true"]
  WithoutDomain: !Equals [!Ref UseDomain, "false"]

Resources:

  # ──────────────────────────────────────────────
  # Route 53 Health Check sobre el ALB
  # ──────────────────────────────────────────────
  PrimaryHealthCheck:
    Type: AWS::Route53::HealthCheck
    Properties:
      HealthCheckConfig:
        Type: HTTP
        FullyQualifiedDomainName: !Ref ALBDNSName
        Port: 80
        ResourcePath: /health
        RequestInterval: 30
        FailureThreshold: 3
        EnableSNI: false
      HealthCheckTags:
        - Key: Name
          Value: route53-arc-primary-hc
        - Key: Workshop
          Value: route53-arc

  # ──────────────────────────────────────────────
  # CloudWatch Alarm sobre el Health Check
  # ──────────────────────────────────────────────
  PrimaryHealthCheckAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmName: route53-arc-primary-unhealthy
      AlarmDescription: "Sitio primario no responde al Health Check de Route 53"
      Namespace: AWS/Route53
      MetricName: HealthCheckStatus
      Dimensions:
        - Name: HealthCheckId
          Value: !Ref PrimaryHealthCheck
      Statistic: Minimum
      Period: 60
      EvaluationPeriods: 3
      Threshold: 1
      ComparisonOperator: LessThanThreshold
      TreatMissingData: breaching

  # ──────────────────────────────────────────────
  # ARC Cluster
  # ──────────────────────────────────────────────
  ARCCluster:
    Type: AWS::Route53RecoveryControl::Cluster
    Properties:
      Name: route53-arc-workshop-cluster
      Tags:
        - Key: Workshop
          Value: route53-arc

  ARCControlPanel:
    Type: AWS::Route53RecoveryControl::ControlPanel
    Properties:
      Name: route53-arc-workshop-panel
      ClusterArn: !GetAtt ARCCluster.ClusterArn
      Tags:
        - Key: Workshop
          Value: route53-arc

  PrimaryRoutingControl:
    Type: AWS::Route53RecoveryControl::RoutingControl
    Properties:
      Name: primary-routing-control
      ControlPanelArn: !Ref ARCControlPanel
      ClusterArn: !GetAtt ARCCluster.ClusterArn

  SecondaryRoutingControl:
    Type: AWS::Route53RecoveryControl::RoutingControl
    Properties:
      Name: secondary-routing-control
      ControlPanelArn: !Ref ARCControlPanel
      ClusterArn: !GetAtt ARCCluster.ClusterArn

  MinAvailabilitySafetyRule:
    Type: AWS::Route53RecoveryControl::SafetyRule
    Properties:
      Name: min-one-active-rule
      ControlPanelArn: !Ref ARCControlPanel
      AssertionRule:
        WaitPeriodMs: 5000
        AssertedControls:
          - !GetAtt PrimaryRoutingControl.RoutingControlArn
          - !GetAtt SecondaryRoutingControl.RoutingControlArn
      RuleConfig:
        Type: ATLEAST
        Value: 1
        Inverted: false

  # ──────────────────────────────────────────────
  # Health Checks tipo RECOVERY_CONTROL
  # (vinculan el DNS failover a los Routing Controls)
  # ──────────────────────────────────────────────
  PrimaryARCHealthCheck:
    Type: AWS::Route53::HealthCheck
    DependsOn: PrimaryRoutingControl
    Properties:
      HealthCheckConfig:
        Type: RECOVERY_CONTROL
        RoutingControlArn: !GetAtt PrimaryRoutingControl.RoutingControlArn
      HealthCheckTags:
        - Key: Name
          Value: route53-arc-primary-rc-hc

  SecondaryARCHealthCheck:
    Type: AWS::Route53::HealthCheck
    DependsOn: SecondaryRoutingControl
    Properties:
      HealthCheckConfig:
        Type: RECOVERY_CONTROL
        RoutingControlArn: !GetAtt SecondaryRoutingControl.RoutingControlArn
      HealthCheckTags:
        - Key: Name
          Value: route53-arc-secondary-rc-hc

  # ──────────────────────────────────────────────
  # Hosted Zone de prueba (solo si NO hay dominio)
  # ──────────────────────────────────────────────
  DemoHostedZone:
    Type: AWS::Route53::HostedZone
    Condition: WithoutDomain
    Properties:
      Name: !Ref DomainName
      HostedZoneConfig:
        Comment: "Hosted Zone de demo – route53-arc-workshop"
      HostedZoneTags:
        - Key: Workshop
          Value: route53-arc

  # ──────────────────────────────────────────────
  # DNS Records – CON dominio real (Public HZ)
  # ──────────────────────────────────────────────
  PrimaryDNSRecordReal:
    Type: AWS::Route53::RecordSet
    Condition: WithDomain
    Properties:
      HostedZoneId: !Ref HostedZoneId
      Name: !Sub "${AppSubdomain}.${DomainName}"
      Type: A
      SetIdentifier: primary
      Failover: PRIMARY
      HealthCheckId: !Ref PrimaryARCHealthCheck
      AliasTarget:
        DNSName: !Ref ALBDNSName
        EvaluateTargetHealth: true
        HostedZoneId: !Ref ALBHostedZoneId

  SecondaryDNSRecordReal:
    Type: AWS::Route53::RecordSet
    Condition: WithDomain
    Properties:
      HostedZoneId: !Ref HostedZoneId
      Name: !Sub "${AppSubdomain}.${DomainName}"
      Type: CNAME
      TTL: "60"
      SetIdentifier: secondary
      Failover: SECONDARY
      HealthCheckId: !Ref SecondaryARCHealthCheck
      ResourceRecords:
        - !Ref CloudFrontDomain

  # ──────────────────────────────────────────────
  # DNS Records – SIN dominio (Hosted Zone demo)
  # ──────────────────────────────────────────────
  PrimaryDNSRecordDemo:
    Type: AWS::Route53::RecordSet
    Condition: WithoutDomain
    Properties:
      HostedZoneId: !Ref DemoHostedZone
      Name: !Sub "${AppSubdomain}.${DomainName}"
      Type: A
      SetIdentifier: primary
      Failover: PRIMARY
      HealthCheckId: !Ref PrimaryARCHealthCheck
      AliasTarget:
        DNSName: !Ref ALBDNSName
        EvaluateTargetHealth: true
        HostedZoneId: !Ref ALBHostedZoneId

  SecondaryDNSRecordDemo:
    Type: AWS::Route53::RecordSet
    Condition: WithoutDomain
    Properties:
      HostedZoneId: !Ref DemoHostedZone
      Name: !Sub "${AppSubdomain}.${DomainName}"
      Type: CNAME
      TTL: "60"
      SetIdentifier: secondary
      Failover: SECONDARY
      HealthCheckId: !Ref SecondaryARCHealthCheck
      ResourceRecords:
        - !Ref CloudFrontDomain

Outputs:

  AppURL:
    Description: "URL del sitio (dominio real o demo)"
    Value: !If
      - WithDomain
      - !Sub "http://${AppSubdomain}.${DomainName}"
      - !Sub "http://${PRIMARY_ALB_DNS} (modo demo: accedé por el ALB DNS directamente)"

  HealthCheckId:
    Description: "ID del Health Check sobre el ALB"
    Value: !Ref PrimaryHealthCheck
    Export:
      Name: route53-arc-hc-id

  ARCClusterArn:
    Description: "ARN del Cluster ARC"
    Value: !GetAtt ARCCluster.ClusterArn
    Export:
      Name: route53-arc-cluster-arn

  ARCControlPanelArn:
    Description: "ARN del Control Panel ARC"
    Value: !Ref ARCControlPanel
    Export:
      Name: route53-arc-panel-arn

  PrimaryRoutingControlArn:
    Description: "ARN del Routing Control primario"
    Value: !GetAtt PrimaryRoutingControl.RoutingControlArn
    Export:
      Name: route53-arc-primary-rc-arn

  SecondaryRoutingControlArn:
    Description: "ARN del Routing Control secundario"
    Value: !GetAtt SecondaryRoutingControl.RoutingControlArn
    Export:
      Name: route53-arc-secondary-rc-arn

  CloudWatchAlarmName:
    Description: "Nombre de la alarma CloudWatch"
    Value: !Ref PrimaryHealthCheckAlarm
    Export:
      Name: route53-arc-cw-alarm
TEMPLATE_EOF

echo "✅ Template ARC creado: $(wc -l < /tmp/route53-arc.yaml) líneas"

Paso 4: Desplegar el stack

# Preparar parámetros según el modo elegido
if [ "$USE_DOMAIN" = "true" ]; then
  DOMAIN_PARAMS="UseDomain=true DomainName=$DOMAIN_NAME AppSubdomain=$APP_SUBDOMAIN HostedZoneId=$HOSTED_ZONE_ID"
  echo "Modo: dominio real → $APP_SUBDOMAIN.$DOMAIN_NAME"
else
  DOMAIN_PARAMS="UseDomain=false"
  echo "Modo: demo (sin dominio público)"
fi

aws cloudformation deploy \
  --template-file /tmp/route53-arc.yaml \
  --stack-name route53-arc-routing \
  --capabilities CAPABILITY_IAM \
  --region $AWS_REGION \
  --parameter-overrides \
    ALBDNSName=$PRIMARY_ALB_DNS \
    ALBHostedZoneId=$PRIMARY_ALB_HZ \
    CloudFrontDomain=$CF_DOMAIN \
    $DOMAIN_PARAMS \
  --tags Workshop=route53-arc

echo "Desplegando ARC... (5-8 min — el Cluster tarda más)"
Información

El ARC Cluster se despliega en múltiples regiones automáticamente para garantizar alta disponibilidad del plano de control. Eso es lo que tarda 3-5 minutos.

Monitoreá el progreso:

watch -n 15 "aws cloudformation describe-stacks \
  --stack-name route53-arc-routing --region $AWS_REGION \
  --query 'Stacks[0].StackStatus' --output text"

Presioná Ctrl+C cuando veas CREATE_COMPLETE.


Paso 5: Activar los Routing Controls

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 CLUSTER_ENDPOINT=$(aws route53-recovery-control-config describe-cluster \
  --cluster-arn $ARC_CLUSTER_ARN --region $AWS_REGION \
  --query 'Cluster.ClusterEndpoints[0].Endpoint' --output text)

# Estado normal: primario ON, secundario OFF
aws route53-recovery-cluster update-routing-control-states \
  --update-routing-control-state-entries \
    "[{\"RoutingControlArn\":\"$PRIMARY_RC_ARN\",\"RoutingControlState\":\"On\"},
      {\"RoutingControlArn\":\"$SECONDARY_RC_ARN\",\"RoutingControlState\":\"Off\"}]" \
  --endpoint-url $CLUSTER_ENDPOINT \
  --region $AWS_REGION

echo "✅ Routing Controls: Primario=ON | Secundario=OFF"

✅ Verificación del Lab 3

echo "=== Verificación Lab 3: Route 53 ARC ==="

STATUS=$(aws cloudformation describe-stacks \
  --stack-name route53-arc-routing --region $AWS_REGION \
  --query 'Stacks[0].StackStatus' --output text)
[ "$STATUS" = "CREATE_COMPLETE" ] \
  && echo "✅ Stack ARC: $STATUS" \
  || echo "❌ Stack: $STATUS"

CLUSTER_STATUS=$(aws route53-recovery-control-config describe-cluster \
  --cluster-arn $ARC_CLUSTER_ARN --region $AWS_REGION \
  --query 'Cluster.Status' --output text)
[ "$CLUSTER_STATUS" = "DEPLOYED" ] \
  && echo "✅ ARC Cluster: $CLUSTER_STATUS" \
  || echo "⚠️  Cluster: $CLUSTER_STATUS"

RC_STATE=$(aws route53-recovery-cluster get-routing-control-state \
  --routing-control-arn $PRIMARY_RC_ARN \
  --endpoint-url $CLUSTER_ENDPOINT --region $AWS_REGION \
  --query 'RoutingControlState' --output text)
[ "$RC_STATE" = "On" ] \
  && echo "✅ Routing Control Primario: $RC_STATE" \
  || echo "⚠️  Routing Control Primario: $RC_STATE"

HC_ID=$(aws cloudformation describe-stacks \
  --stack-name route53-arc-routing --region $AWS_REGION \
  --query 'Stacks[0].Outputs[?OutputKey==`HealthCheckId`].OutputValue' \
  --output text)
echo "ℹ️  Health Check ID: $HC_ID"

# Si usás dominio, verificar DNS
if [ "$USE_DOMAIN" = "true" ]; then
  APP_FQDN="${APP_SUBDOMAIN}.${DOMAIN_NAME}"
  RESOLVED=$(dig +short $APP_FQDN 2>/dev/null | head -1)
  [ -n "$RESOLVED" ] \
    && echo "✅ DNS $APP_FQDN$RESOLVED" \
    || echo "⚠️  DNS $APP_FQDN aún no propaga (espera 1-5 min)"
else
  echo "ℹ️  Modo demo: verificá el sitio en http://$PRIMARY_ALB_DNS"
fi

echo ""
echo "📌 Guarda estos valores:"
echo "   PRIMARY_RC_ARN   = $PRIMARY_RC_ARN"
echo "   SECONDARY_RC_ARN = $SECONDARY_RC_ARN"
echo "   CLUSTER_ENDPOINT = $CLUSTER_ENDPOINT"
echo "   ARC_CLUSTER_ARN  = $ARC_CLUSTER_ARN"

Siguiente paso → Lab 4: Notificaciones SNS