Lab 2: Sitio Secundario (Fallback)

Lab 2: Sitio Secundario – Página de Mantenimiento

El sitio secundario es la página que los usuarios verán cuando el sitio primario (EC2 + ALB) esté caído. Tiene que ser simple, liviana y altamente disponible — exactamente lo contrario al sitio principal.

Este lab tiene dos opciones. Usá la que se adapte a tu cuenta AWS:

Opción A: Amplify Opción B: CloudFront + S3
Recomendada ✅ Sí (más simple) Para cuentas con CloudFront habilitado
HTTPS Automático Automático
Deploy ~1 min ~10 min
Costo Free tier Muy bajo
Requisito Ninguno Cuenta verificada para CloudFront
Consejo

¿Cuál elegir? Si es tu primera vez con AWS o no sabés si tu cuenta tiene CloudFront habilitado → usá la Opción A (Amplify). Es más simple, más rápida y no requiere verificación de cuenta. Solo usá la Opción B si ya trabajaste con CloudFront antes.


Opción A: Amplify (recomendada)

Estructura del módulo

02_secondary/
├── main.tf       Amplify app + branch + página de mantenimiento
├── variables.tf
└── outputs.tf

Paso 1: Setup

source ~/route53-arc-tf/global.env
cd ~/route53-arc-tf/02_secondary

Paso 2: Crear los archivos

1. variables.tf

Define los parámetros de entrada para el módulo del sitio de contingencia.

cat > variables.tf << 'EOF'
variable "aws_region" {
  type    = string
  default = "us-east-1"
}

variable "aws_profile" {
  type    = string
  default = "workshop"
}

variable "use_domain" {
  description = "true = asociar dominio real en Amplify con SSL | false = usar dominio por defecto"
  type        = bool
  default     = false
}

variable "domain_name" {
  description = "Dominio base (ej: tudominio.com)"
  type        = string
  default     = ""
}

variable "app_subdomain" {
  description = "Subdominio del workshop (ej: app)"
  type        = string
  default     = "app"
}

variable "project_tag" {
  type    = string
  default = "route53-arc"
}
EOF

2. main.tf

Crea una aplicación Amplify Hosting con una rama que sirve la página de mantenimiento directamente desde el contenido HTML embebido. No requiere repositorio Git ni build steps.

cat > main.tf << 'EOF'
terraform {
  required_version = ">= 1.5"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
    null = {
      source  = "hashicorp/null"
      version = "~> 3.0"
    }
    archive = {
      source  = "hashicorp/archive"
      version = "~> 2.0"
    }
  }
}

provider "aws" {
  region = var.aws_region

  default_tags {
    tags = {
      Workshop  = var.project_tag
      ManagedBy = "Terraform"
    }
  }
}

# ─── Amplify App ──────────────────────────────────────────────────────────────

resource "aws_amplify_app" "fallback" {
  name        = "route53-arc-fallback"
  description = "Sitio de mantenimiento - AWS Route 53 ARC Workshop"
  platform    = "WEB"

  build_spec = <<-YAML
    version: 1
    frontend:
      phases:
        build:
          commands: []
      artifacts:
        baseDirectory: /
        files:
          - "**/*"
      cache:
        paths: []
  YAML
}

resource "aws_amplify_branch" "main" {
  app_id      = aws_amplify_app.fallback.id
  branch_name = "main"
  stage       = "PRODUCTION"

  enable_auto_build = false
}

# ─── Dominio Customizado en Amplify (solo si use_domain = true) ────────────────

resource "aws_amplify_domain_association" "fallback" {
  count                  = var.use_domain ? 1 : 0
  app_id                 = aws_amplify_app.fallback.id
  domain_name            = var.domain_name
  wait_for_verification  = false

  sub_domain {
    branch_name = aws_amplify_branch.main.branch_name
    prefix      = var.app_subdomain
  }

  enable_auto_sub_domain = false
}

# ─── Deployment: página de mantenimiento ──────────────────────────────────────

data "archive_file" "site" {
  type        = "zip"
  output_path = "${path.module}/site.zip"

  source {
    content  = <<HTML
<!DOCTYPE html>
<html lang="es">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width,initial-scale=1.0">
  <title>Mantenimiento - AWS Workshop</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,.5);
                 max-width:600px; width:90%; }
    h1 { font-size:2em; color:#f59e0b; margin-bottom:12px; }
    p { color:#94a3b8; line-height:1.6; margin-bottom:8px; }
    .badge { display:inline-block; background:#f59e0b; color:#0f172a;
             padding:6px 20px; border-radius:20px; font-weight:bold;
             font-size:.85em; margin:16px 0; }
    .footer { margin-top:28px; padding-top:16px;
              border-top:1px solid #334155; color:#475569; font-size:.8em; }
  </style>
</head>
<body>
  <div class="container">
    <h1>Sitio en Mantenimiento</h1>
    <div class="badge">MANTENIMIENTO PROGRAMADO</div>
    <p>Estamos realizando tareas de mantenimiento para mejorar nuestros servicios.</p>
    <p><strong>Por favor, intentelo de nuevo en unos minutos.</strong></p>
    <div class="footer">
      <p>AWS Route 53 ARC Workshop</p>
      <p>Sitio de Contingencia - Terraform + Amplify</p>
    </div>
  </div>
</body>
</html>
HTML
    filename = "index.html"
  }
}

# ⚠️  El provisioner "local-exec" despliega el HTML a Amplify después de crear la infra.
# Si falla, la infra queda creada pero sin contenido. En ese caso ejecutá manualmente:
#   cd ~/route53-arc-tf/02_secondary && terraform apply -auto-approve
resource "null_resource" "amplify_deploy" {
  triggers = {
    zip_hash  = data.archive_file.site.output_base64sha256
    branch_id = aws_amplify_branch.main.id
  }

  provisioner "local-exec" {
    environment = {
      AWS_PROFILE = var.aws_profile
      AWS_REGION  = var.aws_region
    }
    command = <<EOF
python3 -c "
import json, subprocess, urllib.request, os

app_id = '${aws_amplify_app.fallback.id}'
branch = '${aws_amplify_branch.main.branch_name}'
region = os.environ.get('AWS_REGION', '${var.aws_region}')
profile = os.environ.get('AWS_PROFILE', 'WorkshopRoute53ARC')
zip_path = '${data.archive_file.site.output_path}'

cmd = f'aws amplify create-deployment --app-id {app_id} --branch-name {branch} --region {region} --profile {profile}'
res = subprocess.check_output(cmd, shell=True)
data = json.loads(res)
job_id = data['jobId']
upload_url = data['zipUploadUrl']

with open(zip_path, 'rb') as f:
    req = urllib.request.Request(upload_url, data=f.read(), method='PUT')
    req.add_header('Content-Type', 'application/zip')
    with urllib.request.urlopen(req) as resp:
        pass

start_cmd = f'aws amplify start-deployment --app-id {app_id} --branch-name {branch} --job-id {job_id} --region {region} --profile {profile}'
subprocess.check_output(start_cmd, shell=True)
"
EOF
  }
}
EOF

3. outputs.tf

Exporta el dominio asignado por Amplify. Este dominio se usa en el Lab 3 como destino del registro DNS secundario.

cat > outputs.tf << 'EOF'
output "fallback_domain" {
  description = "Dominio del sitio de fallback (Amplify)"
  value       = "https://${aws_amplify_branch.main.branch_name}.${aws_amplify_app.fallback.default_domain}"
}

output "amplify_app_id" {
  description = "ID de la app Amplify"
  value       = aws_amplify_app.fallback.id
}

output "amplify_app_arn" {
  description = "ARN de la app Amplify"
  value       = aws_amplify_app.fallback.arn
}
EOF

Paso 3: Inicializar y desplegar

terraform init

terraform plan

terraform apply -auto-approve

Paso 4: Outputs

terraform output

export FALLBACK_DOMAIN=$(terraform output -raw fallback_domain)
echo "Fallback: $FALLBACK_DOMAIN"

✅ Verificación del Lab 2 (Amplify)

Aviso

⏱️ Amplify puede tardar 2-3 minutos en propagar el dominio. Si la verificación da ⚠️ HTTP 403 o 404, esperá y volvé a ejecutar el bloque.

echo "=== Verificacion Lab 2 ==="

terraform show | grep -q "aws_amplify_app.fallback" \
  && echo "✅ Amplify app en state" || echo "❌ No encontrado en state"

STATUS=$(curl -s -o /dev/null -w "%{http_code}" $FALLBACK_DOMAIN)
[ "$STATUS" = "200" ] \
  && echo "✅ Sitio fallback HTTPS $STATUS" \
  || echo "⚠️  HTTP $STATUS (si ves 403/404 esperá 2-3 min)"

echo "Fallback URL: $FALLBACK_DOMAIN"

Opción B: CloudFront + S3

Aviso

Requisito: Esta opción requiere que tu cuenta AWS esté verificada para usar CloudFront. Si ves el error “Your account must be verified before you can add new CloudFront resources”, usá la Opción A (Amplify) o contactá a AWS Support para verificar tu cuenta.

Estructura del módulo

02_secondary/
├── main.tf       S3 bucket, OAC, CloudFront distribution
├── variables.tf
└── outputs.tf

Paso 1: Setup

source ~/route53-arc-tf/global.env
cd ~/route53-arc-tf/02_secondary

Paso 2: Crear los archivos

1. variables.tf

cat > variables.tf << 'EOF'
variable "aws_region" {
  type    = string
  default = "us-east-1"
}

variable "project_tag" {
  type    = string
  default = "route53-arc"
}
EOF

2. main.tf

Crea un bucket S3 privado con una política que solo permite acceso desde CloudFront via Origin Access Control (OAC). La distribución CloudFront sirve la página de mantenimiento con HTTPS y baja latencia global.

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 "aws_caller_identity" "current" {}

resource "aws_s3_bucket" "fallback" {
  bucket = "route53-arc-fallback-${data.aws_caller_identity.current.account_id}"
}

resource "aws_s3_bucket_public_access_block" "fallback" {
  bucket                  = aws_s3_bucket.fallback.id
  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

resource "aws_cloudfront_origin_access_control" "fallback" {
  name                              = "route53-arc-fallback-oac"
  origin_access_control_origin_type = "s3"
  signing_behavior                  = "always"
  signing_protocol                  = "sigv4"
}

resource "aws_cloudfront_distribution" "fallback" {
  enabled             = true
  default_root_object = "index.html"
  comment             = "AWS Route 53 ARC Fallback Site"
  http_version        = "http2"

  origin {
    domain_name              = aws_s3_bucket.fallback.bucket_regional_domain_name
    origin_id                = "S3FallbackOrigin"
    origin_access_control_id = aws_cloudfront_origin_access_control.fallback.id
  }

  default_cache_behavior {
    target_origin_id       = "S3FallbackOrigin"
    viewer_protocol_policy = "redirect-to-https"
    allowed_methods        = ["GET", "HEAD"]
    cached_methods         = ["GET", "HEAD"]

    forwarded_values {
      query_string = false
      cookies { forward = "none" }
    }

    min_ttl     = 0
    default_ttl = 3600
    max_ttl     = 86400
  }

  custom_error_response {
    error_code         = 403
    response_code      = 200
    response_page_path = "/index.html"
  }

  custom_error_response {
    error_code         = 404
    response_code      = 200
    response_page_path = "/index.html"
  }

  restrictions {
    geo_restriction { restriction_type = "none" }
  }

  viewer_certificate {
    cloudfront_default_certificate = true
  }
}

resource "aws_s3_bucket_policy" "fallback" {
  bucket = aws_s3_bucket.fallback.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Sid       = "AllowCloudFrontServicePrincipal"
      Effect    = "Allow"
      Principal = { Service = "cloudfront.amazonaws.com" }
      Action    = "s3:GetObject"
      Resource  = "${aws_s3_bucket.fallback.arn}/*"
      Condition = {
        StringEquals = {
          "AWS:SourceArn" = aws_cloudfront_distribution.fallback.arn
        }
      }
    }]
  })
}

resource "aws_s3_object" "index" {
  bucket       = aws_s3_bucket.fallback.id
  key          = "index.html"
  content_type = "text/html"

  content = <<HTML
<!DOCTYPE html>
<html lang="es">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width,initial-scale=1.0">
  <title>Mantenimiento - AWS Workshop</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,.5);
                 max-width:600px; width:90%; }
    h1 { font-size:2em; color:#f59e0b; margin-bottom:12px; }
    p { color:#94a3b8; line-height:1.6; margin-bottom:8px; }
    .badge { display:inline-block; background:#f59e0b; color:#0f172a;
             padding:6px 20px; border-radius:20px; font-weight:bold;
             font-size:.85em; margin:16px 0; }
    .footer { margin-top:28px; padding-top:16px;
              border-top:1px solid #334155; color:#475569; font-size:.8em; }
  </style>
</head>
<body>
  <div class="container">
    <h1>Sitio en Mantenimiento</h1>
    <div class="badge">MANTENIMIENTO PROGRAMADO</div>
    <p>Estamos realizando tareas de mantenimiento para mejorar nuestros servicios.</p>
    <p><strong>Por favor, intentelo de nuevo en unos minutos.</strong></p>
    <div class="footer">
      <p>AWS Route 53 ARC Workshop</p>
      <p>Sitio de Contingencia - Terraform + CloudFront + S3</p>
    </div>
  </div>
</body>
</html>
HTML

  etag = md5("route53-arc-fallback-v1")
}
EOF

3. outputs.tf

cat > outputs.tf << 'EOF'
output "fallback_domain" {
  description = "Dominio del sitio de fallback (CloudFront)"
  value       = "https://${aws_cloudfront_distribution.fallback.domain_name}"
}

output "cloudfront_distribution_id" {
  description = "ID de la distribucion CloudFront"
  value       = aws_cloudfront_distribution.fallback.id
}

output "fallback_bucket_name" {
  description = "Nombre del bucket S3"
  value       = aws_s3_bucket.fallback.id
}
EOF

Paso 3: Inicializar y desplegar

terraform init

terraform plan

terraform apply -auto-approve
Información

La distribución CloudFront tarda 5-10 minutos en desplegarse y propagar el DNS globalmente.

Paso 4: Outputs

terraform output

export FALLBACK_DOMAIN=$(terraform output -raw fallback_domain)
echo "Fallback: $FALLBACK_DOMAIN"

✅ Verificación del Lab 2 (CloudFront)

echo "=== Verificacion Lab 2 ==="

terraform show | grep -q "aws_cloudfront_distribution.fallback" \
  && echo "✅ CloudFront en state" || echo "❌ No encontrado en state"

STATUS=$(curl -s -o /dev/null -w "%{http_code}" $FALLBACK_DOMAIN)
[ "$STATUS" = "200" ] \
  && echo "✅ Sitio fallback HTTPS $STATUS" \
  || echo "⚠️  HTTP $STATUS (puede tardar hasta 10 min)"

echo "Fallback URL: $FALLBACK_DOMAIN"

Siguiente paso → Lab 3: Route 53 ARC