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.tf

Los 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_arc

Dominio: opcional

# Sin dominio (modo demo)
export USE_DOMAIN="false"

# Con dominio real en Route 53
# export USE_DOMAIN="true"
# export DOMAIN_NAME="tudominio.com"
# export APP_SUBDOMAIN="app"
# export HOSTED_ZONE_ID="Z1234ABCDEF"

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

2. 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 (cloudfront_domain) mediante data "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 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
  cf_domain    = data.terraform_remote_state.secondary.outputs.cloudfront_domain
}

# ─── Route 53 Health Check sobre el ALB ───────────────────────────────────────

resource "aws_route53_health_check" "primary_alb" {
  fqdn              = local.alb_dns_name
  port              = 80
  type              = "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_route53recoverycontrol_cluster" "main" {
  name = "route53-arc-workshop-cluster"
}

resource "aws_route53recoverycontrol_control_panel" "main" {
  name        = "route53-arc-workshop-panel"
  cluster_arn = aws_route53recoverycontrol_cluster.main.arn
}

# ─── Routing Controls ─────────────────────────────────────────────────────────

resource "aws_route53recoverycontrol_routing_control" "primary" {
  name              = "primary-routing-control"
  cluster_arn       = aws_route53recoverycontrol_cluster.main.arn
  control_panel_arn = aws_route53recoverycontrol_control_panel.main.arn
}

resource "aws_route53recoverycontrol_routing_control" "secondary" {
  name              = "secondary-routing-control"
  cluster_arn       = aws_route53recoverycontrol_cluster.main.arn
  control_panel_arn = aws_route53recoverycontrol_control_panel.main.arn
}

# ─── Safety Rule ──────────────────────────────────────────────────────────────

resource "aws_route53recoverycontrol_safety_rule" "min_one_active" {
  name              = "min-one-active-rule"
  control_panel_arn = aws_route53recoverycontrol_control_panel.main.arn
  wait_period_ms    = 5000

  asserted_controls = [
    aws_route53recoverycontrol_routing_control.primary.arn,
    aws_route53recoverycontrol_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_route53recoverycontrol_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_route53recoverycontrol_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           = "A"
  set_identifier = "primary"
  health_check_id = aws_route53_health_check.primary_rc.id

  failover_routing_policy {
    type = "PRIMARY"
  }

  alias {
    name                   = local.alb_dns_name
    zone_id                = local.alb_zone_id
    evaluate_target_health = true
  }
}

# ─── DNS Record SECONDARY ─────────────────────────────────────────────────────

resource "aws_route53_record" "secondary" {
  zone_id        = local.hosted_zone_id
  name           = "${var.app_subdomain}.${var.domain_name}"
  type           = "CNAME"
  ttl            = 60
  set_identifier = "secondary"
  health_check_id = aws_route53_health_check.secondary_rc.id

  failover_routing_policy {
    type = "SECONDARY"
  }

  records = [local.cf_domain]
}
EOF

3. 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_route53recoverycontrol_cluster.main.arn
}

output "arc_cluster_endpoints" {
  value = aws_route53recoverycontrol_cluster.main.cluster_endpoints
}

output "primary_rc_arn" {
  value = aws_route53recoverycontrol_routing_control.primary.arn
}

output "secondary_rc_arn" {
  value = aws_route53recoverycontrol_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}"
}
EOF

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

terraform apply $DOMAIN_VARS -auto-approve
Aviso

El ARC Cluster tarda 3-5 minutos en crearse. Es normal.


Paso 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 el endpoint del cluster
export CLUSTER_ENDPOINT=$(terraform output -json arc_cluster_endpoints \
  | python3 -c "import json,sys; eps=json.load(sys.stdin); print(eps[0]['Endpoint'])")

echo "Cluster Endpoint: $CLUSTER_ENDPOINT"

# 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 $AWS_REGION

echo "✅ Primario=ON | Secundario=OFF"

✅ Verificación del Lab 3

echo "=== Verificación Lab 3 ==="

terraform show | grep -q "aws_route53recoverycontrol_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 $AWS_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