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.tfPaso 1: Setup
source ~/route53-arc-tf/global.env
cd ~/route53-arc-tf/02_secondaryPaso 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"
}
EOF2. 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
}
}
EOF3. 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
}
EOFPaso 3: Inicializar y desplegar
terraform init
terraform plan
terraform apply -auto-approvePaso 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.tfPaso 1: Setup
source ~/route53-arc-tf/global.env
cd ~/route53-arc-tf/02_secondaryPaso 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"
}
EOF2. 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")
}
EOF3. 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
}
EOFPaso 3: Inicializar y desplegar
terraform init
terraform plan
terraform apply -auto-approveInformació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