Lab 6: Failback – Volver al Sitio Primario

Lab 6: Failback – Restaurar el Tráfico al Sitio Primario

El failover fue automático. El failback es siempre manual e intencional: primero reparamos el sitio primario, verificamos que funciona, y solo entonces devolvemos el tráfico.


¿Por qué el failback es manual?

El failover automático protege la disponibilidad. El failback manual protege contra restauraciones prematuras: nadie quiere que el tráfico vuelva a un servidor que “cree” que está bien pero en realidad sigue fallando.

FAILOVER (automático)    FAILBACK (manual, deliberado)
─────────────────────    ──────────────────────────────
Sistema detecta caída    Operador verifica recuperación
       ↓                          ↓
Lambda actúa solo        Operador ejecuta el script
       ↓                          ↓
Protege al usuario       Confirma que el primario está OK

Paso 1: Verificar el estado actual

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

# Recuperar todos los valores necesarios
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)

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 EC2_ID=$(aws cloudformation describe-stacks \
  --stack-name route53-arc-primary --region $AWS_REGION \
  --query 'Stacks[0].Outputs[?OutputKey==`EC2InstanceID`].OutputValue' \
  --output text)

echo "=== Estado actual (post-failover) ==="
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 \
  | xargs -I{} echo "Primario:   {}"

aws route53-recovery-cluster get-routing-control-state \
  --routing-control-arn $SECONDARY_RC_ARN \
  --endpoint-url $CLUSTER_ENDPOINT --region $AWS_REGION \
  --query 'RoutingControlState' --output text \
  | xargs -I{} echo "Secundario: {}"

Paso 2: Reparar el sitio primario

Si usaste el Lab 5 método B (nginx detenido), debes reiniciar el servicio:

echo "=== Reparando sitio primario: reiniciando nginx ==="

COMMAND_ID=$(aws ssm send-command \
  --instance-ids $EC2_ID \
  --document-name AWS-RunShellScript \
  --parameters 'commands=["systemctl start nginx && systemctl status nginx --no-pager | head -5"]' \
  --region $AWS_REGION \
  --query 'Command.CommandId' \
  --output text)

echo "Comando SSM enviado: $COMMAND_ID"
sleep 10

aws ssm get-command-invocation \
  --command-id $COMMAND_ID \
  --instance-id $EC2_ID \
  --region $AWS_REGION \
  --query '{Status: Status, Output: StandardOutputContent}' \
  --output json

Paso 3: Verificar que el primario responde

Antes de hacer el failback, confirma que el sitio primario está realmente funcionando:

echo "=== Verificando recuperación del sitio primario ==="

# Esperar a que el ALB registre el servidor como healthy
echo "Esperando que el ALB marque el EC2 como healthy..."
for i in $(seq 1 10); do
  TG_ARN=$(aws elbv2 describe-target-groups \
    --names route53-arc-primary-tg --region $AWS_REGION \
    --query 'TargetGroups[0].TargetGroupArn' --output text 2>/dev/null)

  TG_HEALTH=$(aws elbv2 describe-target-health \
    --target-group-arn $TG_ARN --region $AWS_REGION \
    --query 'TargetHealthDescriptions[0].TargetHealth.State' \
    --output text 2>/dev/null)

  HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \
    --connect-timeout 3 --max-time 5 \
    http://$PRIMARY_ALB_DNS/ 2>/dev/null)

  echo "[$(date '+%H:%M:%S')] Target Group: $TG_HEALTH | HTTP: $HTTP_CODE"

  if [ "$TG_HEALTH" = "healthy" ] && [ "$HTTP_CODE" = "200" ]; then
    echo "✅ ¡Sitio primario recuperado y funcionando!"
    break
  fi
  sleep 15
done
Aviso

No hagas el failback hasta ver Target Group: healthy y HTTP: 200. Si vuelves el tráfico antes de que el primario esté listo, los usuarios seguirán viendo errores.


Paso 4: Ejecutar el Failback

Una vez confirmado que el primario está sano, ejecuta el failback:

echo "=== EJECUTANDO FAILBACK ==="
echo "Hora: $(date)"

# Volver a activar el Routing Control primario
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 "✅ Failback ejecutado: Primario=ON, Secundario=OFF"

Paso 5: Restablecer la alarma CloudWatch

ALARM_NAME="route53-arc-primary-unhealthy-with-sns"

aws cloudwatch set-alarm-state \
  --alarm-name $ALARM_NAME \
  --state-value OK \
  --state-reason "Sitio primario recuperado - failback completado" \
  --region $AWS_REGION

echo "✅ Alarma CloudWatch restablecida a OK"

Paso 6: Lambda de Failback (automatizable)

Si quisieras automatizar el failback (con validaciones adicionales), aquí tienes el código de una Lambda de failback que puedes invocar manualmente:

cat > /tmp/failback-manual.py << 'PYTHON_EOF'
"""
Script de Failback para Route53 ARC Workshop
Ejecutar desde CloudShell como: python3 /tmp/failback-manual.py

Verifica que el sitio primario esté saludable antes de cambiar los Routing Controls.
"""
import boto3
import urllib.request
import sys
import time

REGION          = "us-east-1"
PRIMARY_RC_ARN  = "REEMPLAZAR_CON_PRIMARY_RC_ARN"
SECONDARY_RC_ARN = "REEMPLAZAR_CON_SECONDARY_RC_ARN"
CLUSTER_ARN     = "REEMPLAZAR_CON_CLUSTER_ARN"
ALB_DNS         = "REEMPLAZAR_CON_ALB_DNS"


def check_primary_health(alb_dns, retries=3):
    """Verifica que el ALB primario responde con HTTP 200."""
    print(f"Verificando salud del primario: http://{alb_dns}/health")
    for attempt in range(retries):
        try:
            req = urllib.request.urlopen(
                f"http://{alb_dns}/health", timeout=10
            )
            if req.getcode() == 200:
                print(f"  ✅ Intento {attempt+1}: HTTP 200 OK")
                return True
        except Exception as e:
            print(f"  ❌ Intento {attempt+1}: {e}")
        time.sleep(5)
    return False


def get_cluster_endpoint(cluster_arn):
    """Obtiene el primer endpoint del cluster ARC."""
    client = boto3.client('route53-recovery-control-config', region_name=REGION)
    resp = client.describe_cluster(ClusterArn=cluster_arn)
    return resp['Cluster']['ClusterEndpoints'][0]['Endpoint']


def execute_failback(cluster_endpoint):
    """Ejecuta el failback cambiando los Routing Controls."""
    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'}
        ]
    )
    print("✅ Failback completado: Primario=ON, Secundario=OFF")


def main():
    print("=== Failback Manager – roLearning Route53 ARC Workshop ===\n")

    # 1. Verificar salud
    if not check_primary_health(ALB_DNS):
        print("\n❌ El sitio primario NO está saludable. Abortando failback.")
        print("   Corrija el problema antes de intentar el failback.")
        sys.exit(1)

    print(f"\n✅ Sitio primario saludable. Procediendo con failback...\n")

    # 2. Obtener endpoint del cluster
    endpoint = get_cluster_endpoint(CLUSTER_ARN)
    print(f"Cluster endpoint: {endpoint}")

    # 3. Confirmar (en un script real, podrías pedir confirmación)
    print("\n⚠️  ATENCIÓN: Esto redirigirá el tráfico al sitio PRIMARIO.")
    confirm = input("Escribe 'CONFIRMAR' para continuar: ")
    if confirm.strip() != 'CONFIRMAR':
        print("Failback cancelado.")
        sys.exit(0)

    # 4. Ejecutar failback
    execute_failback(endpoint)
    print("\n🎉 Failback completado exitosamente.")
    print("   Los usuarios ahora ven el sitio primario nuevamente.")


if __name__ == '__main__':
    main()
PYTHON_EOF

echo "✅ Script de failback creado en /tmp/failback-manual.py"
echo "   Edita las variables al inicio antes de ejecutarlo."

✅ Verificación del Lab 6

echo "=== Verificación Lab 6: Failback ==="

# 1. Estado post-failback
PRIMARY_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 2>/dev/null)

SECONDARY_STATE=$(aws route53-recovery-cluster get-routing-control-state \
  --routing-control-arn $SECONDARY_RC_ARN \
  --endpoint-url $CLUSTER_ENDPOINT --region $AWS_REGION \
  --query 'RoutingControlState' --output text 2>/dev/null)

[ "$PRIMARY_STATE" = "On" ] \
  && echo "✅ Routing Control Primario: $PRIMARY_STATE" \
  || echo "❌ Routing Control Primario: $PRIMARY_STATE"

[ "$SECONDARY_STATE" = "Off" ] \
  && echo "✅ Routing Control Secundario: $SECONDARY_STATE" \
  || echo "❌ Routing Control Secundario: $SECONDARY_STATE"

# 2. Sitio primario accesible
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \
  --connect-timeout 5 http://$PRIMARY_ALB_DNS/ 2>/dev/null)
[ "$HTTP_CODE" = "200" ] \
  && echo "✅ Sitio primario responde: HTTP $HTTP_CODE" \
  || echo "⚠️  Sitio primario HTTP code: $HTTP_CODE"

echo ""
echo "🎉 ¡Workshop completado! El ciclo completo funcionó:"
echo "   Detección → Notificación → Failover → Reparación → Failback"

Resumen del ciclo completo

Estado Normal    →  Incidente   →   Failover   →  Reparación  →  Failback
─────────────       ─────────       ─────────      ──────────     ────────
Primario: ON        nginx cae       Lambda         nginx start    Manual
Secundario: OFF     HC falla        activa          ALB healthy   RC swap
                    CW Alarm        SNS email       verified      Primario: ON
                    ALARM           RC swap         ✅            Secundario: OFF

Siguiente paso → Lab 7: Limpieza