Lab 3: Route 53 Health Checks + ARC
Lab 3: Route 53 Health Checks + ARC Routing Controls
Estructura del módulo
03_arc/
├── main.tf ARC Cluster, Routing Controls, Health Checks, DNS
├── variables.tf
└── outputs.tfLos outputs de los labs anteriores se leen via terraform_remote_state.
Paso 1: Setup
source ~/route53-arc-tf/global.env
cd ~/route53-arc-tf/03_arcDominio: opcional pero recomendado
Este lab funciona en dos modos:
- Modo demo (sin dominio): Terraform crea una Hosted Zone privada (
workshop.internal). El DNS no resuelve desde internet, pero podés ver los registros creados en la consola de Route 53 y los Routing Controls funcionan igual. Es suficiente para entender el concepto. - Modo real (con dominio): Si tenés un dominio registrado en Route 53 en esta cuenta, el failover funciona de verdad — el tráfico se redirige automáticamente entre el ALB y CloudFront.
Cómo obtener el Hosted Zone ID de tu dominio
Si tenés un dominio en Route 53, el Hosted Zone ID es el identificador de la zona DNS de ese dominio. Lo obtenés así:
Opción A — Desde la consola:
- Ir a Route 53 → Hosted zones
- Hacé clic en tu dominio
- El Hosted zone ID aparece en el panel derecho (formato:
Z1234ABCDEF)
Opción B — Desde la CLI:
# Listar todas las hosted zones de la cuenta
aws route53 list-hosted-zones \
--query "HostedZones[*].{Nombre:Name, ID:Id, Privada:Config.PrivateZone}" \
--output table
# Buscar por nombre de dominio específico (reemplazá tudominio.com)
aws route53 list-hosted-zones \
--query "HostedZones[?Name=='tudominio.com.'].{ID:Id,Name:Name}" \
--output tableEl ID que devuelve tiene el formato /hostedzone/Z1234ABCDEF — solo necesitás la parte Z1234ABCDEF.
# Extraer solo el ID (sin el prefijo /hostedzone/)
HOSTED_ZONE_ID=$(aws route53 list-hosted-zones \
--query "HostedZones[?Name=='tudominio.com.'].Id" \
--output text | cut -d'/' -f3)
echo "Hosted Zone ID: $HOSTED_ZONE_ID"Configurar variables de dominio
# Sin dominio (modo demo)
export USE_DOMAIN="false"
# ── O ──────────────────────────────────────────────────────────────────────────
# Con dominio real en Route 53
export USE_DOMAIN="true"
export DOMAIN_NAME="tudominio.com" # tu dominio sin punto final
export APP_SUBDOMAIN="app" # quedará como app.tudominio.com
export HOSTED_ZONE_ID="Z1234ABCDEF" # ID obtenido arribaInformación
¿Qué registro DNS se crea? Terraform crea dos registros con política de failover:
app.tudominio.com→ PRIMARY → Alias al ALB (EC2 + nginx)app.tudominio.com→ SECONDARY → CNAME al dominio de fallback (Amplify o CloudFront)
Route 53 sirve el PRIMARY mientras el Health Check esté verde. Si falla, sirve el SECONDARY automáticamente.
💡 Concepto clave: ¿Qué es un Routing Control?
Un Routing Control es como un interruptor en el panel de control del cluster ARC. En este lab creamos dos:
- Primario (ON): Route 53 sirve el tráfico al ALB (EC2 + nginx).
- Secundario (OFF): Route 53 ignorara ese registro DNS.
Cuando el Primario pasa a OFF y el Secundario a ON, Route 53 redirige el tráfico al sitio de contingencia (Amplify). La Safety Rule garantiza que nunca los dos estén OFF al mismo tiempo — evita un corte total.
Paso 2: Crear los archivos
1. variables.tf
¿Para qué sirve este archivo?
Define los parámetros de entrada del orquestador de ARC (región, modo demo o dominio real, nombre de dominio, subdominio e ID de la Hosted Zone en Route 53).
cat > variables.tf << 'EOF'
variable "aws_region" {
type = string
default = "us-east-1"
}
variable "use_domain" {
description = "true = usar dominio real | false = modo demo"
type = bool
default = false
}
variable "domain_name" {
description = "Dominio base (ej: tudominio.com)"
type = string
default = "workshop.internal"
}
variable "app_subdomain" {
description = "Subdominio del workshop (ej: app)"
type = string
default = "app"
}
variable "hosted_zone_id" {
description = "ID de la Public Hosted Zone (solo si use_domain=true)"
type = string
default = ""
}
variable "project_tag" {
type = string
default = "route53-arc"
}
EOF2. main.tf
¿Para qué sirve este archivo?
Es el núcleo de la solución de alta disponibilidad y conmutación de tráfico:
- Lectura de estados locales: Importa los outputs del Lab 1 (
alb_dns_name,alb_zone_id) y del Lab 2 (fallback_domain) mediantedata "terraform_remote_state".- Route 53 Health Check & CloudWatch Alarm: Monitorea continuamente la salud del ALB primario en la ruta
/health.- ARC Cluster & Routing Controls: Crea el cluster redundante de Route 53 ARC con sus dos interruptores (Primario y Secundario).
- Safety Rule (
min-one-active-rule): Regla de protección que garantiza que al menos 1 sitio se mantenga encendido (ATLEAST 1), evitando cortes totales por error humano.- Health Checks de ARC & Políticas de Failover DNS: Asocia los Routing Controls a registros de DNS Primario (Alias ALB) y Secundario (CNAME Amplify / CloudFront).
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"
}
}
}
# ─── Remote state de labs anteriores ──────────────────────────────────────────
data "terraform_remote_state" "primary" {
backend = "local"
config = {
path = "../01_primary/terraform.tfstate"
}
}
data "terraform_remote_state" "secondary" {
backend = "local"
config = {
path = "../02_secondary/terraform.tfstate"
}
}
locals {
alb_dns_name = data.terraform_remote_state.primary.outputs.alb_dns_name
alb_zone_id = data.terraform_remote_state.primary.outputs.alb_zone_id
# fallback_domain viene como "https://main.xxxx.amplifyapp.com" — extraemos solo el hostname
fallback_host = replace(data.terraform_remote_state.secondary.outputs.fallback_domain, "https://", "")
}
# ─── Route 53 Health Check sobre el ALB ───────────────────────────────────────
resource "aws_route53_health_check" "primary_alb" {
fqdn = local.alb_dns_name
port = var.use_domain ? 443 : 80
type = var.use_domain ? "HTTPS" : "HTTP"
resource_path = "/health"
failure_threshold = 3
request_interval = 30
tags = {
Name = "route53-arc-primary-hc"
}
}
# ─── CloudWatch Alarm sobre el Health Check ───────────────────────────────────
resource "aws_cloudwatch_metric_alarm" "primary_unhealthy" {
alarm_name = "route53-arc-primary-unhealthy"
alarm_description = "Sitio primario no responde al Health Check de Route 53"
namespace = "AWS/Route53"
metric_name = "HealthCheckStatus"
statistic = "Minimum"
period = 60
evaluation_periods = 3
threshold = 1
comparison_operator = "LessThanThreshold"
treat_missing_data = "breaching"
dimensions = {
HealthCheckId = aws_route53_health_check.primary_alb.id
}
}
# ─── ARC Cluster ──────────────────────────────────────────────────────────────
resource "aws_route53recoverycontrolconfig_cluster" "main" {
name = "route53-arc-workshop-cluster"
}
resource "aws_route53recoverycontrolconfig_control_panel" "main" {
name = "route53-arc-workshop-panel"
cluster_arn = aws_route53recoverycontrolconfig_cluster.main.arn
}
# ─── Routing Controls ─────────────────────────────────────────────────────────
resource "aws_route53recoverycontrolconfig_routing_control" "primary" {
name = "primary-routing-control"
cluster_arn = aws_route53recoverycontrolconfig_cluster.main.arn
control_panel_arn = aws_route53recoverycontrolconfig_control_panel.main.arn
}
resource "aws_route53recoverycontrolconfig_routing_control" "secondary" {
name = "secondary-routing-control"
cluster_arn = aws_route53recoverycontrolconfig_cluster.main.arn
control_panel_arn = aws_route53recoverycontrolconfig_control_panel.main.arn
}
# ─── Safety Rule ──────────────────────────────────────────────────────────────
resource "aws_route53recoverycontrolconfig_safety_rule" "min_one_active" {
name = "min-one-active-rule"
control_panel_arn = aws_route53recoverycontrolconfig_control_panel.main.arn
wait_period_ms = 5000
asserted_controls = [
aws_route53recoverycontrolconfig_routing_control.primary.arn,
aws_route53recoverycontrolconfig_routing_control.secondary.arn,
]
rule_config {
inverted = false
threshold = 1
type = "ATLEAST"
}
}
# ─── Health Checks tipo RECOVERY_CONTROL ──────────────────────────────────────
resource "aws_route53_health_check" "primary_rc" {
type = "RECOVERY_CONTROL"
routing_control_arn = aws_route53recoverycontrolconfig_routing_control.primary.arn
tags = { Name = "route53-arc-primary-rc-hc" }
}
resource "aws_route53_health_check" "secondary_rc" {
type = "RECOVERY_CONTROL"
routing_control_arn = aws_route53recoverycontrolconfig_routing_control.secondary.arn
tags = { Name = "route53-arc-secondary-rc-hc" }
}
# ─── Hosted Zone demo (si NO hay dominio real) ────────────────────────────────
resource "aws_route53_zone" "demo" {
count = var.use_domain ? 0 : 1
name = var.domain_name
comment = "Hosted Zone demo – route53-arc-workshop"
}
locals {
# Usar la HZ real o la demo según configuración
hosted_zone_id = var.use_domain ? var.hosted_zone_id : aws_route53_zone.demo[0].zone_id
}
# ─── DNS Record PRIMARY ────────────────────────────────────────────────────────
resource "aws_route53_record" "primary" {
zone_id = local.hosted_zone_id
name = "${var.app_subdomain}.${var.domain_name}"
type = "CNAME"
set_identifier = "primary"
health_check_id = aws_route53_health_check.primary_rc.id
ttl = 60
records = [local.alb_dns_name]
allow_overwrite = true
failover_routing_policy {
type = "PRIMARY"
}
}
# ─── DNS Record SECONDARY ─────────────────────────────────────────────────────
# NOTA: Usamos CNAME en lugar de Alias A porque Amplify no soporta Alias nativos en
# dominios .amplifyapp.com frente a Route 53, y la política de Failover exige que
# tanto el primario como secundario sean del mismo tipo (CNAME).
# allow_overwrite soluciona colisiones con el CNAME que Amplify crea automáticamente.
resource "aws_route53_record" "secondary" {
depends_on = [aws_route53_record.primary]
zone_id = local.hosted_zone_id
name = "${var.app_subdomain}.${var.domain_name}"
type = "CNAME"
set_identifier = "secondary"
health_check_id = aws_route53_health_check.secondary_rc.id
ttl = 60
records = [local.fallback_host]
allow_overwrite = true
failover_routing_policy {
type = "SECONDARY"
}
}
EOF3. outputs.tf
¿Para qué sirve este archivo?
Exporta el ARN del cluster ARC, sus endpoints HTTP de gestión, los ARNs de los Routing Controls primario y secundario, el ID del Health Check y el nombre de la alarma CloudWatch. Estos outputs son necesarios para automatizar el failover con Lambda en el Lab 4.
cat > outputs.tf << 'EOF'
output "arc_cluster_arn" {
value = aws_route53recoverycontrolconfig_cluster.main.arn
}
output "arc_cluster_endpoints" {
value = aws_route53recoverycontrolconfig_cluster.main.cluster_endpoints
}
output "primary_rc_arn" {
value = aws_route53recoverycontrolconfig_routing_control.primary.arn
}
output "secondary_rc_arn" {
value = aws_route53recoverycontrolconfig_routing_control.secondary.arn
}
output "health_check_id" {
value = aws_route53_health_check.primary_alb.id
}
output "cloudwatch_alarm_name" {
value = aws_cloudwatch_metric_alarm.primary_unhealthy.alarm_name
}
output "app_url" {
value = "http://${var.app_subdomain}.${var.domain_name}"
}
EOFAviso
⚠️ ATENCIÓN SI USASTE DOMINIO PROPIO CON AMPLIFY (LAB 2)
Amplify es tan proactivo que en el Lab 2 te creó un registro CNAME automático en Route 53 apuntando a tu app (app.tudominio.com).
Para que este Lab 3 funcione y Terraform pueda crear las políticas de Failover, debes borrar manualmente ese CNAME simple.
- Entrá a la consola de AWS Route 53 -> Hosted Zones
- Seleccioná tu dominio.
- Marcá el registro de tipo CNAME que se llame
app.tudominio.comy dale a Delete record.
Paso 3: Desplegar
terraform init
# Preparar variables
if [ "$USE_DOMAIN" = "true" ]; then
DOMAIN_VARS="-var=use_domain=true -var=domain_name=$DOMAIN_NAME -var=app_subdomain=$APP_SUBDOMAIN -var=hosted_zone_id=$HOSTED_ZONE_ID"
else
DOMAIN_VARS="-var=use_domain=false"
fi
terraform plan $DOMAIN_VARS
Aviso
⏱️ El terraform apply puede tardar 3-5 minutos — AWS necesita aprovisionar el ARC Cluster en múltiples regiones. La terminal parecerá congelada durante ese tiempo. No la canceles. Es completamente normal.
terraform apply $DOMAIN_VARS -auto-approvePaso 4: Activar Routing Controls
export PRIMARY_RC_ARN=$(terraform output -raw primary_rc_arn)
export SECONDARY_RC_ARN=$(terraform output -raw secondary_rc_arn)
# Obtener endpoint del cluster para la región activa
# (el cluster ARC tiene endpoints en múltiples regiones para alta disponibilidad)
CLUSTER_ENDPOINT=$(terraform output -json arc_cluster_endpoints | python3 -c "
import json, sys
eps = json.load(sys.stdin)
target = next((e for e in eps if e['region'] == 'us-east-1'), eps[0])
print(target['endpoint'])
")
ENDPOINT_REGION="us-east-1"
echo "Cluster Endpoint: $CLUSTER_ENDPOINT ($ENDPOINT_REGION)"
# Estado normal: primario ON, secundario OFF
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 $ENDPOINT_REGION
echo "✅ Primario=ON | Secundario=OFF"✅ Verificación del Lab 3
echo "=== Verificación Lab 3 ==="
terraform show | grep -q "aws_route53recoverycontrolconfig_cluster.main" \
&& echo "✅ ARC Cluster en state" || echo "❌ No en state"
RC_STATE=$(aws route53-recovery-cluster get-routing-control-state \
--routing-control-arn $PRIMARY_RC_ARN \
--endpoint-url $CLUSTER_ENDPOINT --region $ENDPOINT_REGION \
--query 'RoutingControlState' --output text)
[ "$RC_STATE" = "On" ] \
&& echo "✅ Routing Control Primario: $RC_STATE" \
|| echo "⚠️ RC Primario: $RC_STATE"
echo "📌 PRIMARY_RC_ARN = $PRIMARY_RC_ARN"
echo "📌 CLUSTER_ENDPOINT = $CLUSTER_ENDPOINT"Siguiente paso → Lab 4: SNS + Lambda