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.env

Dominio: Modo Demo vs Dominio Real

Este laboratorio soporta dos modos de operación:

  1. 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%.
  2. Modo Real (Con dominio en Route 53): Si tenés un dominio real en Route 53, configurás DOMAIN_NAME en ~/global.env y 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
fi

2. 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_EOF

3. 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=false

4. 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 }
EOF

2. 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
done

Obtener 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