Lab 3: Route 53 Health Checks + ARC
Lab 3: Route 53 Health Checks + ARC Routing Controls
En este laboratorio configuramos el núcleo de la alta disponibilidad: los Health Checks de Route 53, la Alarma de CloudWatch, el Cluster de Route 53 ARC, los Routing Controls (primario y secundario) y la Regla de Seguridad (Safety Rule) que garantiza que nunca ambos sitios queden apagados al mismo tiempo.
ARC Cluster
└── Control Panel
├── Routing Control: "primary" (ON = Tráfico a EC2/ALB)
└── Routing Control: "secondary" (OFF = Tráfico a Mantenimiento)
└── Safety Rule: "min-one-active-rule" (ATLEAST 1)Información
¿Qué es un Routing Control? Es un switch global en AWS (parte de Route 53 ARC) que activa o desactiva un registro DNS de failover. Cuando el Routing Control “primary” está en ON, Route 53 envía tráfico al ALB. Cuando está en OFF, Route 53 lo envía al sitio de contingencia. La Safety Rule garantiza que nunca ambos controles queden en OFF al mismo tiempo — evita un blackout total.
Aviso
⏱️ El ARC Cluster tarda entre 5 y 10 minutos en estar disponible después del deploy.
No intentes activar los Routing Controls hasta que el cluster esté en estado DEPLOYED. El script de verificación más abajo incluye un comando para comprobarlo.
Cargar variables de entorno
source ~/global.envDominio: Modo Demo vs Dominio Real
Este laboratorio soporta dos modos de operación:
- Modo Demo (Sin dominio real): Se crea una Hosted Zone pública en Route 53 con el nombre
workshop-arc-demo.internal(funciona como zona de prueba, los registros no resolverán en internet pero permiten demostrar el failover completo vía Routing Controls). Los Routing Controls y Health Checks funcionan al 100%. - Modo Real (Con dominio en Route 53): Si tenés un dominio real en Route 53, configurás
DOMAIN_NAMEen~/global.envy el failover DNS funcionará desde cualquier navegador.
Información
En modo demo el failover DNS no es observable desde el navegador. Los registros workshop-arc-demo.internal no resuelven en internet. Lo que sí podés observar es: el cambio de estado de los Routing Controls (Primario: OFF / Secundario: ON), el email de alerta SNS cuando se dispara el failover, y el email de recuperación con el enlace de Failback 1-Click. Esto es suficiente para experimentar el flujo completo.
Información
¿Por qué una Hosted Zone pública en modo demo?
Las Hosted Zones privadas en Route 53 deben estar asociadas a al menos una VPC para que los registros DNS sean funcionales. Para simplificar el workshop, usamos una Hosted Zone pública con nombre workshop-arc-demo.internal — los registros no resuelven desde internet (nadie tiene ese dominio), pero permiten que CloudFormation y Terraform creen los registros de failover sin errores.
Opción A: CloudFormation (Base)
1. Obtener outputs de los labs anteriores
source ~/global.env
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)
# Para Amplify (Opción default del Lab 2):
export AMPLIFY_APP_ID=$(aws cloudformation describe-stacks \
--stack-name route53-arc-secondary --region $AWS_REGION \
--query 'Stacks[0].Outputs[?OutputKey==`AmplifyAppId`].OutputValue' --output text 2>/dev/null || echo "")
export AMPLIFY_DOMAIN=$(aws cloudformation describe-stacks \
--stack-name route53-arc-secondary --region $AWS_REGION \
--query 'Stacks[0].Outputs[?OutputKey==`AmplifyDefaultDomain`].OutputValue' --output text 2>/dev/null || echo "")
# Para CloudFront (si usaste la alternativa):
export CF_DOMAIN=$(aws cloudformation describe-stacks \
--stack-name route53-arc-secondary --region $AWS_REGION \
--query 'Stacks[0].Outputs[?OutputKey==`CloudFrontDomain`].OutputValue' --output text 2>/dev/null || echo "")
# Determinar el dominio del sitio fallback (Amplify tiene prioridad)
if [ -n "$AMPLIFY_DOMAIN" ] && [ "$AMPLIFY_DOMAIN" != "None" ]; then
export FALLBACK_HOST="main.${AMPLIFY_DOMAIN}"
else
export FALLBACK_HOST="$CF_DOMAIN"
fi
echo "✅ ALB DNS: $PRIMARY_ALB_DNS"
echo "✅ Fallback Host: $FALLBACK_HOST"
if [ -z "$FALLBACK_HOST" ] || [ "$FALLBACK_HOST" = "None" ]; then
echo "❌ FALLBACK_HOST está vacío — asegurate de haber completado el Lab 2 y tener FALLBACK_DOMAIN en ~/global.env"
echo " Podés recuperarlo con: source ~/global.env"
exit 1
fi2. Crear el template CloudFormation
cat > /tmp/route53-arc.yaml << 'TEMPLATE_EOF'
AWSTemplateFormatVersion: '2010-09-09'
Description: 'AWS Route53 ARC Workshop - Health Checks + ARC Routing Controls'
Parameters:
ALBDNSName: { Type: String }
ALBHostedZoneId: { Type: String }
FallbackHost: { Type: String, Description: "Host del sitio fallback (sin https://)" }
UseDomain: { Type: String, Default: "false", AllowedValues: ["true", "false"] }
DomainName: { Type: String, Default: "workshop-arc-demo.internal" }
AppSubdomain: { Type: String, Default: "app" }
HostedZoneId: { Type: String, Default: "" }
Conditions:
WithDomain: !Equals [!Ref UseDomain, "true"]
WithoutDomain: !Equals [!Ref UseDomain, "false"]
Resources:
# ── Health Check HTTP del ALB ──
PrimaryHealthCheck:
Type: AWS::Route53::HealthCheck
Properties:
HealthCheckConfig:
Type: HTTP
FullyQualifiedDomainName: !Ref ALBDNSName
Port: 80
ResourcePath: /health
RequestInterval: 30
FailureThreshold: 3
HealthCheckTags:
- { Key: Name, Value: route53-arc-primary-hc }
# ── CloudWatch Alarm ──
PrimaryHealthCheckAlarm:
Type: AWS::CloudWatch::Alarm
Properties:
AlarmName: route53-arc-primary-unhealthy
AlarmDescription: "Sitio primario no responde al Health Check"
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 + Control Panel ──
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
# ── Routing Controls ──
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
# ── Safety Rule: al menos 1 control activo siempre ──
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 basados en Routing Controls (ARC) ──
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 pública de demo (sin dominio real) ──
# Nota: se crea como pública para evitar el requisito de asociación VPC de las zonas privadas.
# El nombre "workshop-arc-demo.internal" no resuelve en internet pero permite crear los registros DNS.
DemoHostedZone:
Type: AWS::Route53::HostedZone
Condition: WithoutDomain
Properties:
Name: !Ref DomainName
# ── Registros DNS de Failover (modo 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 FallbackHost]
# ── Registros DNS de Failover (dominio real) ──
PrimaryDNSRecord:
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
SecondaryDNSRecord:
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 FallbackHost]
Outputs:
HealthCheckId:
Value: !Ref PrimaryHealthCheck
Export: { Name: route53-arc-hc-id }
ARCClusterArn:
Value: !GetAtt ARCCluster.ClusterArn
Export: { Name: route53-arc-cluster-arn }
PrimaryRoutingControlArn:
Value: !GetAtt PrimaryRoutingControl.RoutingControlArn
Export: { Name: route53-arc-primary-rc-arn }
SecondaryRoutingControlArn:
Value: !GetAtt SecondaryRoutingControl.RoutingControlArn
Export: { Name: route53-arc-secondary-rc-arn }
CloudWatchAlarmName:
Value: !Ref PrimaryHealthCheckAlarm
Export: { Name: route53-arc-cw-alarm }
TEMPLATE_EOF3. Desplegar Stack CloudFormation
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 \
FallbackHost=$FALLBACK_HOST \
UseDomain=false4. Obtener los outputs del stack
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)
echo "✅ PRIMARY_RC_ARN = $PRIMARY_RC_ARN"
echo "✅ SECONDARY_RC_ARN = $SECONDARY_RC_ARN"
echo "✅ ARC_CLUSTER_ARN = $ARC_CLUSTER_ARN"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/03_arc
# variables.tf
cat > variables.tf << 'EOF'
variable "aws_region" { type = string; default = "us-east-1" }
variable "use_domain" { type = bool; default = false }
variable "domain_name" { type = string; default = "workshop-arc-demo.internal" }
variable "app_subdomain" { type = string; default = "app" }
variable "hosted_zone_id"{ type = string; default = "" }
variable "project_tag" { type = string; default = "route53-arc" }
EOF
# main.tf
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 "terraform_remote_state" "primary" {
backend = "local"
config = { path = "../01_primary/terraform.tfstate" }
}
data "terraform_remote_state" "secondary" {
backend = "local"
config = { path = "../02_secondary/terraform.tfstate" }
}
locals {
alb_dns_name = data.terraform_remote_state.primary.outputs.alb_dns_name
alb_zone_id = data.terraform_remote_state.primary.outputs.alb_zone_id
fallback_host = replace(data.terraform_remote_state.secondary.outputs.fallback_domain, "https://", "")
}
resource "aws_route53_health_check" "primary_alb" {
fqdn = local.alb_dns_name
port = 80
type = "HTTP"
resource_path = "/health"
failure_threshold = 3
request_interval = 30
tags = { Name = "route53-arc-primary-hc" }
}
resource "aws_cloudwatch_metric_alarm" "primary_unhealthy" {
alarm_name = "route53-arc-primary-unhealthy"
alarm_description = "Sitio primario no responde al Health Check"
namespace = "AWS/Route53"
metric_name = "HealthCheckStatus"
statistic = "Minimum"
period = 60
evaluation_periods = 3
threshold = 1
comparison_operator = "LessThanThreshold"
treat_missing_data = "breaching"
dimensions = { HealthCheckId = aws_route53_health_check.primary_alb.id }
}
resource "aws_route53recoverycontrolconfig_cluster" "main" {
name = "route53-arc-workshop-cluster"
}
resource "aws_route53recoverycontrolconfig_control_panel" "main" {
name = "route53-arc-workshop-panel"
cluster_arn = aws_route53recoverycontrolconfig_cluster.main.arn
}
resource "aws_route53recoverycontrolconfig_routing_control" "primary" {
name = "primary-routing-control"
cluster_arn = aws_route53recoverycontrolconfig_cluster.main.arn
control_panel_arn = aws_route53recoverycontrolconfig_control_panel.main.arn
}
resource "aws_route53recoverycontrolconfig_routing_control" "secondary" {
name = "secondary-routing-control"
cluster_arn = aws_route53recoverycontrolconfig_cluster.main.arn
control_panel_arn = aws_route53recoverycontrolconfig_control_panel.main.arn
}
resource "aws_route53recoverycontrolconfig_safety_rule" "min_one_active" {
name = "min-one-active-rule"
control_panel_arn = aws_route53recoverycontrolconfig_control_panel.main.arn
wait_period_ms = 5000
asserted_controls = [
aws_route53recoverycontrolconfig_routing_control.primary.arn,
aws_route53recoverycontrolconfig_routing_control.secondary.arn
]
rule_config {
inverted = false
threshold = 1
type = "ATLEAST"
}
}
resource "aws_route53_health_check" "primary_rc" {
type = "RECOVERY_CONTROL"
routing_control_arn = aws_route53recoverycontrolconfig_routing_control.primary.arn
tags = { Name = "route53-arc-primary-rc-hc" }
}
resource "aws_route53_health_check" "secondary_rc" {
type = "RECOVERY_CONTROL"
routing_control_arn = aws_route53recoverycontrolconfig_routing_control.secondary.arn
tags = { Name = "route53-arc-secondary-rc-hc" }
}
# Hosted Zone pública de demo — no requiere asociación VPC
resource "aws_route53_zone" "demo" {
count = var.use_domain ? 0 : 1
name = var.domain_name
}
locals {
hosted_zone_id = var.use_domain ? var.hosted_zone_id : aws_route53_zone.demo[0].zone_id
}
resource "aws_route53_record" "primary" {
zone_id = local.hosted_zone_id
name = "${var.app_subdomain}.${var.domain_name}"
type = "A"
set_identifier = "primary"
health_check_id = aws_route53_health_check.primary_rc.id
failover_routing_policy { type = "PRIMARY" }
alias {
name = local.alb_dns_name
zone_id = local.alb_zone_id
evaluate_target_health = true
}
}
resource "aws_route53_record" "secondary" {
depends_on = [aws_route53_record.primary]
zone_id = local.hosted_zone_id
name = "${var.app_subdomain}.${var.domain_name}"
type = "CNAME"
ttl = 60
set_identifier = "secondary"
health_check_id = aws_route53_health_check.secondary_rc.id
failover_routing_policy { type = "SECONDARY" }
records = [local.fallback_host]
}
EOF
# outputs.tf
cat > outputs.tf << 'EOF'
output "arc_cluster_arn" { value = aws_route53recoverycontrolconfig_cluster.main.arn }
output "arc_cluster_endpoints" { value = aws_route53recoverycontrolconfig_cluster.main.cluster_endpoints }
output "primary_rc_arn" { value = aws_route53recoverycontrolconfig_routing_control.primary.arn }
output "secondary_rc_arn" { value = aws_route53recoverycontrolconfig_routing_control.secondary.arn }
output "health_check_id" { value = aws_route53_health_check.primary_alb.id }
output "cloudwatch_alarm_name" { value = aws_cloudwatch_metric_alarm.primary_unhealthy.alarm_name }
EOF2. Aplicar los cambios en Terraform
Aviso
Este módulo Terraform lee el estado de los módulos anteriores. Antes de continuar verificá que existan los archivos ~/route53-arc-tf/01_primary/terraform.tfstate y ~/route53-arc-tf/02_secondary/terraform.tfstate. Si alguno falta (por ejemplo si borraste la carpeta o si usaste CloudShell), el terraform apply fallará con un error de remote state. En ese caso, debés re-deployar el lab correspondiente o usar la Opción A (CloudFormation) para este lab.
terraform init
terraform apply -auto-approve
export PRIMARY_RC_ARN=$(terraform output -raw primary_rc_arn)
export SECONDARY_RC_ARN=$(terraform output -raw secondary_rc_arn)
export ARC_CLUSTER_ARN=$(terraform output -raw arc_cluster_arn)Esperar a que el ARC Cluster esté DEPLOYED
Aviso
Este paso es obligatorio. Intentar activar los Routing Controls mientras el cluster está en estado PENDING o CREATING resulta en error ClusterEndpointNotAvailableException. Esperá hasta confirmar el estado DEPLOYED.
echo "⏳ Verificando estado del ARC Cluster (puede tardar 5-10 minutos)..."
for i in {1..20}; do
CLUSTER_STATUS=$(aws route53-recovery-control-config describe-cluster \
--cluster-arn $ARC_CLUSTER_ARN \
--region $AWS_REGION \
--query 'Cluster.Status' --output text 2>/dev/null)
echo " [$i/20] Estado del ARC Cluster: $CLUSTER_STATUS"
if [ "$CLUSTER_STATUS" = "DEPLOYED" ]; then
echo "✅ ARC Cluster listo."
break
fi
if [ "$i" = "20" ]; then
echo "⚠️ El cluster no llegó a DEPLOYED en el tiempo esperado. Esperá un par de minutos más y re-ejecutá."
exit 1
fi
sleep 30
doneObtener el Cluster Endpoint y Activar Estado Inicial
El endpoint del cluster se obtiene con describe-cluster — no se puede derivar del ARN.
# Obtener el endpoint del cluster para us-east-1
read CLUSTER_ENDPOINT ENDPOINT_REGION <<< $(aws route53-recovery-control-config describe-cluster \
--cluster-arn $ARC_CLUSTER_ARN --region $AWS_REGION \
--query 'Cluster.ClusterEndpoints' --output json | python3 -c "
import json, sys
eps = json.load(sys.stdin)
target = next((e for e in eps if e.get('Region') == 'us-east-1'), eps[0])
ep = target.get('Endpoint')
rg = target.get('Region')
print(f'{ep} {rg}')
")
echo "✅ Cluster Endpoint: $CLUSTER_ENDPOINT ($ENDPOINT_REGION)"
# Establecer estado inicial: Primario=ON | Secundario=OFF
# Los dos controles se envían en la misma llamada atómica para satisfacer la Safety Rule ATLEAST 1
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 $ENDPOINT_REGION
echo "✅ Estado Inicial Aplicado: Primario=ON | Secundario=OFF"
# Persistir en global.env para labs siguientes
for VAR_PAIR in "CLUSTER_ENDPOINT=$CLUSTER_ENDPOINT" "ENDPOINT_REGION=$ENDPOINT_REGION"; do
VAR_NAME="${VAR_PAIR%%=*}"
VAR_VALUE="${VAR_PAIR#*=}"
if grep -q "$VAR_NAME" ~/global.env 2>/dev/null; then
sed -i '' "s|.*${VAR_NAME}.*|export ${VAR_NAME}=\"${VAR_VALUE}\"|" ~/global.env 2>/dev/null || \
sed -i "s|.*${VAR_NAME}.*|export ${VAR_NAME}=\"${VAR_VALUE}\"|" ~/global.env
else
echo "export ${VAR_NAME}=\"${VAR_VALUE}\"" >> ~/global.env
fi
done
echo "✅ CLUSTER_ENDPOINT y ENDPOINT_REGION guardados en ~/global.env"✅ Verificación del Lab 3
echo "=== Verificación Lab 3: Route 53 ARC ==="
# Verificar estado del cluster
CLUSTER_STATUS=$(aws route53-recovery-control-config describe-cluster \
--cluster-arn $ARC_CLUSTER_ARN --region $AWS_REGION \
--query 'Cluster.Status' --output text 2>/dev/null)
[ "$CLUSTER_STATUS" = "DEPLOYED" ] \
&& echo "✅ ARC Cluster: $CLUSTER_STATUS" \
|| echo "⚠️ ARC Cluster Status: $CLUSTER_STATUS"
# Verificar Routing Control primario
RC_STATE=$(aws route53-recovery-cluster get-routing-control-state \
--routing-control-arn $PRIMARY_RC_ARN \
--endpoint-url $CLUSTER_ENDPOINT --region $ENDPOINT_REGION \
--query 'RoutingControlState' --output text 2>/dev/null)
[ "$RC_STATE" = "On" ] \
&& echo "✅ Routing Control Primario: $RC_STATE" \
|| echo "⚠️ Routing Control Primario: $RC_STATE"
echo ""
echo "📌 PRIMARY_RC_ARN = $PRIMARY_RC_ARN"
echo "📌 SECONDARY_RC_ARN = $SECONDARY_RC_ARN"
echo "📌 CLUSTER_ENDPOINT = $CLUSTER_ENDPOINT"
echo "📌 ENDPOINT_REGION = $ENDPOINT_REGION"Siguiente paso → Lab 4: Notificaciones SNS + Lambda