Lab 2: Sitio Secundario (Fallback)

Lab 2: Sitio Secundario – S3 + CloudFront

En este lab creamos la página de fallback: lo que verán los usuarios cuando el sitio primario no esté disponible. Es una página estática muy simple alojada en S3 y distribuida por CloudFront.


¿Qué vamos a crear?

Internet → CloudFront → S3 (estático)
  • S3 Bucket con acceso solo desde CloudFront (OAC)
  • CloudFront Distribution con HTTPS automático
  • Página HTML estilo “mantenimiento” con branding

¿Por qué S3 + CloudFront?

Ventaja Detalle
Costo casi cero S3 cobra centavos por GB; CloudFront tiene free tier
Alta disponibilidad CloudFront tiene SLA de 99.99%
Sin servidores Completamente serverless, no hay nada que “caer”
HTTPS gratis CloudFront incluye certificado SSL automático

Paso 1: Variables de entorno

export AWS_REGION="us-east-1"
export AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
export STACK_NAME_SECONDARY="route53-arc-secondary"
export BUCKET_NAME="route53-arc-fallback-${AWS_ACCOUNT_ID}"
echo "Bucket name: $BUCKET_NAME"

Paso 2: Crear el template CloudFormation

cat > /tmp/secondary-site.yaml << 'TEMPLATE_EOF'
AWSTemplateFormatVersion: '2010-09-09'
Description: 'roLearning - Route53 ARC Workshop - Sitio Secundario (S3 + CloudFront)'

Parameters:
  BucketName:
    Type: String
    Description: "Nombre único para el bucket S3"

Resources:

  # ──────────────────────────────────────────────
  # S3 Bucket para el sitio estático
  # ──────────────────────────────────────────────
  FallbackBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Ref BucketName
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true
      Tags:
        - Key: Workshop
          Value: route53-arc
        - Key: Site
          Value: secondary

  # ──────────────────────────────────────────────
  # Origin Access Control para CloudFront
  # ──────────────────────────────────────────────
  CloudFrontOAC:
    Type: AWS::CloudFront::OriginAccessControl
    Properties:
      OriginAccessControlConfig:
        Name: route53-arc-fallback-oac
        OriginAccessControlOriginType: s3
        SigningBehavior: always
        SigningProtocol: sigv4

  # ──────────────────────────────────────────────
  # Bucket Policy - permite acceso solo desde CloudFront
  # ──────────────────────────────────────────────
  FallbackBucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref FallbackBucket
      PolicyDocument:
        Statement:
          - Sid: AllowCloudFrontServicePrincipal
            Effect: Allow
            Principal:
              Service: cloudfront.amazonaws.com
            Action: s3:GetObject
            Resource: !Sub "${FallbackBucket.Arn}/*"
            Condition:
              StringEquals:
                "AWS:SourceArn": !Sub "arn:aws:cloudfront::${AWS::AccountId}:distribution/${FallbackDistribution}"

  # ──────────────────────────────────────────────
  # CloudFront Distribution
  # ──────────────────────────────────────────────
  FallbackDistribution:
    Type: AWS::CloudFront::Distribution
    Properties:
      DistributionConfig:
        Enabled: true
        DefaultRootObject: index.html
        Comment: "roLearning - Route53 ARC Fallback Site"
        HttpVersion: http2
        Origins:
          - Id: S3FallbackOrigin
            DomainName: !GetAtt FallbackBucket.RegionalDomainName
            OriginAccessControlId: !Ref CloudFrontOAC
            S3OriginConfig:
              OriginAccessIdentity: ""
        DefaultCacheBehavior:
          ViewerProtocolPolicy: redirect-to-https
          TargetOriginId: S3FallbackOrigin
          CachePolicyId: 658327ea-f89d-4fab-a63d-7e88639e58f6  # CachingOptimized
          AllowedMethods: [GET, HEAD]
          CachedMethods: [GET, HEAD]
        CustomErrorResponses:
          - ErrorCode: 403
            ResponseCode: 200
            ResponsePagePath: /index.html
          - ErrorCode: 404
            ResponseCode: 200
            ResponsePagePath: /index.html
        Tags:
          - Key: Workshop
            Value: route53-arc
          - Key: Site
            Value: secondary

Outputs:
  FallbackBucketName:
    Description: "Nombre del bucket S3"
    Value: !Ref FallbackBucket
    Export:
      Name: route53-arc-fallback-bucket

  CloudFrontDomain:
    Description: "Dominio de la distribución CloudFront"
    Value: !GetAtt FallbackDistribution.DomainName
    Export:
      Name: route53-arc-cf-domain

  CloudFrontDistributionId:
    Description: "ID de la distribución CloudFront"
    Value: !Ref FallbackDistribution
    Export:
      Name: route53-arc-cf-distribution-id
TEMPLATE_EOF

echo "✅ Template creado: $(wc -l < /tmp/secondary-site.yaml) líneas"

Paso 3: Desplegar el stack secundario

aws cloudformation deploy \
  --template-file /tmp/secondary-site.yaml \
  --stack-name $STACK_NAME_SECONDARY \
  --capabilities CAPABILITY_IAM \
  --region $AWS_REGION \
  --parameter-overrides BucketName=$BUCKET_NAME \
  --tags Workshop=route53-arc Environment=secondary

echo "Desplegando sitio secundario..."
Información

CloudFront puede tardar 5-10 minutos en desplegarse completamente. El stack dirá CREATE_COMPLETE antes de que CloudFront esté 100% activo. Esto es normal.


Paso 4: Subir la página de fallback a S3

Una vez que el stack está CREATE_COMPLETE, creamos el contenido:

# Obtener nombre del bucket
export CF_BUCKET=$(aws cloudformation describe-stacks \
  --stack-name $STACK_NAME_SECONDARY \
  --region $AWS_REGION \
  --query 'Stacks[0].Outputs[?OutputKey==`FallbackBucketName`].OutputValue' \
  --output text)

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

echo "Bucket: $CF_BUCKET"
echo "CloudFront: $CF_DOMAIN"

Crea el HTML de la página de mantenimiento:

cat > /tmp/fallback-index.html << 'HTML_EOF'
<!DOCTYPE html>
<html lang="es">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Mantenimiento – roLearning</title>
  <style>
    * { margin: 0; padding: 0; box-sizing: border-box; }
    body {
      font-family: 'Segoe UI', Arial, sans-serif;
      background: #0f172a;
      color: #e2e8f0;
      display: flex;
      align-items: center;
      justify-content: center;
      min-height: 100vh;
    }
    .container {
      text-align: center;
      padding: 40px 60px;
      background: #1e293b;
      border-radius: 16px;
      box-shadow: 0 8px 32px rgba(0,0,0,0.5);
      max-width: 600px;
      width: 90%;
    }
    .icon { font-size: 64px; margin-bottom: 20px; }
    h1 { font-size: 2em; color: #f59e0b; margin-bottom: 12px; }
    p { color: #94a3b8; line-height: 1.6; margin-bottom: 10px; }
    .badge {
      display: inline-block;
      background: #f59e0b;
      color: #0f172a;
      padding: 6px 20px;
      border-radius: 20px;
      font-weight: bold;
      font-size: 0.85em;
      margin: 20px 0;
    }
    .footer {
      margin-top: 30px;
      padding-top: 20px;
      border-top: 1px solid #334155;
      color: #475569;
      font-size: 0.8em;
    }
  </style>
</head>
<body>
  <div class="container">
    <div class="icon">⚠️</div>
    <h1>Sitio en Mantenimiento</h1>
    <div class="badge">MANTENIMIENTO PROGRAMADO</div>
    <p>Estamos realizando tareas de mantenimiento para mejorar nuestros servicios.</p>
    <p>Nuestro equipo está trabajando para restablecer el servicio a la brevedad.</p>
    <p><strong>Por favor, inténtelo de nuevo en unos minutos.</strong></p>
    <div class="footer">
      <p>roLearning – AWS Route 53 ARC Workshop</p>
      <p>Sitio de Contingencia | Servido por CloudFront + S3</p>
    </div>
  </div>
</body>
</html>
HTML_EOF

# Subir a S3
aws s3 cp /tmp/fallback-index.html s3://$CF_BUCKET/index.html \
  --content-type "text/html" \
  --region $AWS_REGION

echo "✅ Página de fallback subida a S3"

Paso 5: Invalidar caché de CloudFront (si aplica)

export CF_DIST_ID=$(aws cloudformation describe-stacks \
  --stack-name $STACK_NAME_SECONDARY \
  --region $AWS_REGION \
  --query 'Stacks[0].Outputs[?OutputKey==`CloudFrontDistributionId`].OutputValue' \
  --output text)

aws cloudfront create-invalidation \
  --distribution-id $CF_DIST_ID \
  --paths "/*" \
  --region $AWS_REGION

echo "✅ Invalidación de caché creada"

✅ Verificación del Lab 2

echo "=== Verificación Lab 2: Sitio Secundario ==="

# 1. Stack en CREATE_COMPLETE
STATUS=$(aws cloudformation describe-stacks \
  --stack-name $STACK_NAME_SECONDARY --region $AWS_REGION \
  --query 'Stacks[0].StackStatus' --output text)
[ "$STATUS" = "CREATE_COMPLETE" ] \
  && echo "✅ Stack CloudFormation: $STATUS" \
  || echo "❌ Stack status: $STATUS"

# 2. Bucket existe y tiene el archivo
aws s3 ls s3://$CF_BUCKET/index.html > /dev/null 2>&1 \
  && echo "✅ index.html en S3: OK" \
  || echo "❌ index.html no encontrado en S3"

# 3. CloudFront responde (puede tardar unos minutos)
CF_STATUS=$(curl -s -o /dev/null -w "%{http_code}" https://$CF_DOMAIN/ 2>/dev/null)
[ "$CF_STATUS" = "200" ] \
  && echo "✅ CloudFront responde HTTPS $CF_STATUS" \
  || echo "⚠️  CloudFront HTTP code: $CF_STATUS (puede tardar 5-10 min en activarse)"

echo ""
echo "📌 Guarda estos valores para los próximos labs:"
echo "   CF_BUCKET      = $CF_BUCKET"
echo "   CF_DOMAIN      = $CF_DOMAIN"
echo "   CF_DIST_ID     = $CF_DIST_ID"
echo ""
echo "🌐 Abre en tu browser: https://$CF_DOMAIN"

La página de fallback debería verse con fondo oscuro y el mensaje “⚠️ Sitio en Mantenimiento”.


Siguiente paso → Lab 3: Route 53 Health Checks + ARC