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 ONPaso 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"
fiConsejo
¿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