Lab 2: Sitio Secundario (Fallback)

Lab 2: Sitio Secundario – S3 + CloudFront


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

¿Para qué sirve este archivo?
Define las variables de entrada para el módulo del sitio de contingencia (región de AWS y tag identificador del workshop).

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

¿Para qué sirve este archivo?
Declara la arquitectura del sitio secundario de respuesta ante desastres:

  • Bucket S3 totalmente privado: Almacena el archivo HTML estático de la página de mantenimiento.
  • Origin Access Control (OAC): Configura una firma SigV4 para que solo CloudFront tenga permiso de leer los objetos del bucket.
  • CloudFront Distribution: Provee entrega global con HTTPS, respuestas personalizadas de error (403/404 -> 200 index.html) y baja latencia.
  • Bucket Policy y Objetos: Aplica la política de lectura restringida e inyecta la página index.html.
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" {}

# ─── S3 Bucket ────────────────────────────────────────────────────────────────

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
}

# ─── CloudFront Origin Access Control ─────────────────────────────────────────

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

# ─── CloudFront Distribution ──────────────────────────────────────────────────

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
  }
}

# ─── Bucket Policy – solo CloudFront puede leer ───────────────────────────────

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
        }
      }
    }]
  })
}

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

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%; }
    .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: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">
    <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><strong>Por favor, inténtelo 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

  # Forzar re-deploy si el contenido cambia
  etag = md5(<<HTML
<!DOCTYPE html>
<html lang="es">fallback
HTML
  )
}
EOF

3. outputs.tf

¿Para qué sirve este archivo?
Exporta el dominio asignado por CloudFront (cloudfront_domain), el ID de la distribución y el nombre del bucket S3. El dominio de CloudFront se utilizará en el Lab 3 para apuntar el registro DNS de contingencia (CNAME).

cat > outputs.tf << 'EOF'
output "cloudfront_domain" {
  description = "Dominio de la distribución CloudFront"
  value       = aws_cloudfront_distribution.fallback.domain_name
}

output "cloudfront_distribution_id" {
  description = "ID de la distribución 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. El apply completa cuando AWS confirma la creación, pero el DNS puede tardar un poco más en propagar.


Paso 4: Outputs

terraform output

export CF_DOMAIN=$(terraform output -raw cloudfront_domain)
export CF_DIST_ID=$(terraform output -raw cloudfront_distribution_id)

echo "CloudFront: https://$CF_DOMAIN"

✅ Verificación del Lab 2

echo "=== Verificación Lab 2 ==="

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

CF_STATUS=$(curl -s -o /dev/null -w "%{http_code}" https://$CF_DOMAIN/)
[ "$CF_STATUS" = "200" ] \
  && echo "✅ CloudFront HTTPS $CF_STATUS" \
  || echo "⚠️  HTTP $CF_STATUS (puede tardar hasta 10 min)"

echo "CF Domain: $CF_DOMAIN"

Siguiente paso → Lab 3: Route 53 ARC