Lab 1: Sitio Web Primario

Lab 1: Sitio Web Primario (EC2 + ALB)


Estructura del módulo

01_primary/
├── main.tf          VPC (nueva u default), EC2, ALB
├── variables.tf     Inputs del módulo
├── outputs.tf       Exports para módulos siguientes
└── userdata.sh      Script de bootstrap nginx

Paso 1: Variables de entorno y Verificación de VPC

source ~/route53-arc-tf/global.env
cd ~/route53-arc-tf/01_primary

Antes de continuar, podés comprobar qué modo de VPC está activo y verificar si tu cuenta posee una VPC Default:

aws ec2 describe-vpcs --filters "Name=is-default,Values=true" --query "Vpcs[0].VpcId" --output text
echo "Modo VPC configurado: $VPC_MODE"
  • Si el comando devuelve un VPC ID (ej: vpc-0a1b2c3d), podés utilizar VPC_MODE="default".
  • Si devuelve None, asegurate de que VPC_MODE="new" para que Terraform cree una VPC completa con sus subredes.

Paso 2: Crear los archivos del módulo

1. variables.tf

¿Para qué sirve este archivo?
Define los parámetros de entrada del módulo (región AWS, tipo de VPC, tipo de instancia EC2 y etiquetas). Esto permite modificar el comportamiento del despliegue sin alterar la lógica de infraestructura.

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

variable "vpc_mode" {
  description = "default = usar VPC existente | new = crear VPC nueva"
  type        = string
  default     = "default"
  validation {
    condition     = contains(["default", "new"], var.vpc_mode)
    error_message = "vpc_mode debe ser 'default' o 'new'."
  }
}

variable "instance_type" {
  description = "Tipo de instancia EC2 (t4g.micro recomendado, t3.micro alternativa)"
  type        = string
  default     = "t4g.micro"
  validation {
    condition     = contains(["t4g.micro", "t3.micro"], var.instance_type)
    error_message = "Solo se permiten t4g.micro o t3.micro."
  }
}

variable "project_tag" {
  description = "Tag de identificación del workshop"
  type        = string
  default     = "route53-arc"
}
EOF

2. userdata.sh

¿Para qué sirve este archivo?
Es el script de inicialización (EC2 User Data) que se ejecuta al arrancar la instancia. Instala el servidor Nginx, consulta la API de metadatos de la instancia (http://169.254.169.254), genera una página HTML de estado y habilita el endpoint /health necesario para los Health Checks de Route 53.

cat > userdata.sh << 'EOF'
#!/bin/bash
set -e
dnf install -y nginx
systemctl enable nginx
systemctl start nginx

INSTANCE_TOKEN=$(curl -s -S -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600" 2>/dev/null || true)
if [ -n "$INSTANCE_TOKEN" ]; then
  INSTANCE_TYPE=$(curl -s -H "X-aws-ec2-metadata-token: $INSTANCE_TOKEN" http://169.254.169.254/latest/meta-data/instance-type)
  REGION=$(curl -s -H "X-aws-ec2-metadata-token: $INSTANCE_TOKEN" http://169.254.169.254/latest/meta-data/placement/region)
else
  INSTANCE_TYPE=$(curl -s http://169.254.169.254/latest/meta-data/instance-type)
  REGION=$(curl -s http://169.254.169.254/latest/meta-data/placement/region)
fi

cat > /usr/share/nginx/html/index.html << HTML
<!DOCTYPE html>
<html lang="es">
<head>
  <meta charset="UTF-8">
  <title>Sitio Primario – 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; }
    .card { background:#1e293b; border-radius:12px; padding:48px 64px;
            text-align:center; box-shadow:0 4px 24px rgba(0,0,0,.4); }
    h1 { color:#22c55e; font-size:2.5em; margin-bottom:8px; }
    h2 { color:#94a3b8; font-size:1.1em; font-weight:normal; margin-bottom:24px; }
    .badge { display:inline-block; background:#22c55e; color:#0f172a;
             padding:6px 20px; border-radius:20px; font-weight:bold; }
    .meta { color:#475569; margin-top:24px; font-size:.85em; line-height:1.8; }
  </style>
</head>
<body>
  <div class="card">
    <h1>✅ Sitio Primario</h1>
    <h2>AWS Route 53 ARC Workshop</h2>
    <div class="badge">ONLINE</div>
    <p class="meta">
      EC2 Amazon Linux 2023 + nginx<br>
      Región: ${REGION} | Instancia: ${INSTANCE_TYPE}<br>
      Desplegado con <strong>Terraform</strong>
    </p>
  </div>
</body>
</html>
HTML

echo "OK" > /usr/share/nginx/html/health
systemctl restart nginx
EOF

3. main.tf

¿Para qué sirve este archivo?
Es el archivo central del módulo donde se declaran todos los recursos de infraestructura en AWS:

  • AMI dinámica: Selecciona automáticamente la imagen de Amazon Linux 2023 adecuada según la arquitectura de la instancia (arm64 para t4g.micro, x86_64 para t3.micro).
  • Red (VPC): Si vpc_mode = "default", consulta la VPC Default de la región. Si vpc_mode = "new", crea una VPC /16, un Internet Gateway y 2 subredes públicas en distintas Zonas de Disponibilidad.
  • Application Load Balancer (ALB): Crea el balanceador de carga público, su Target Group en el puerto 80 y el Listener.
  • Seguridad e Instancia EC2: Define los Security Groups y aprovisiona la instancia EC2 con el script userdata.sh.
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"
      Environment = "primary"
    }
  }
}

# ─── Data sources ──────────────────────────────────────────────────────────────

# AMI: Amazon Linux 2023 — ARM si t4g.micro, x86 si t3.micro
locals {
  is_arm = startswith(var.instance_type, "t4g")
  ami_arch = local.is_arm ? "arm64" : "x86_64"
}

data "aws_ami" "al2023" {
  most_recent = true
  owners      = ["amazon"]

  filter {
    name   = "name"
    values = ["al2023-ami-*-kernel-*-${local.ami_arch}"]
  }

  filter {
    name   = "virtualization-type"
    values = ["hvm"]
  }
}

# VPC default (si vpc_mode = "default")
data "aws_vpc" "default" {
  count   = var.vpc_mode == "default" ? 1 : 0
  default = true
}

data "aws_subnets" "default_public" {
  count = var.vpc_mode == "default" ? 1 : 0

  filter {
    name   = "vpc-id"
    values = [data.aws_vpc.default[0].id]
  }
}

# ─── VPC nueva (solo si vpc_mode = "new") ─────────────────────────────────────

data "aws_availability_zones" "available" {
  state = "available"
}

resource "aws_vpc" "workshop" {
  count = var.vpc_mode == "new" ? 1 : 0

  cidr_block           = "10.0.0.0/16"
  enable_dns_support   = true
  enable_dns_hostnames = true

  tags = { Name = "route53-arc-vpc" }
}

resource "aws_internet_gateway" "workshop" {
  count  = var.vpc_mode == "new" ? 1 : 0
  vpc_id = aws_vpc.workshop[0].id
  tags   = { Name = "route53-arc-igw" }
}

resource "aws_subnet" "public" {
  count = var.vpc_mode == "new" ? 2 : 0

  vpc_id                  = aws_vpc.workshop[0].id
  cidr_block              = "10.0.${count.index + 1}.0/24"
  availability_zone       = data.aws_availability_zones.available.names[count.index]
  map_public_ip_on_launch = true

  tags = { Name = "route53-arc-public-${count.index + 1}" }
}

resource "aws_route_table" "public" {
  count  = var.vpc_mode == "new" ? 1 : 0
  vpc_id = aws_vpc.workshop[0].id

  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.workshop[0].id
  }

  tags = { Name = "route53-arc-rt-public" }
}

resource "aws_route_table_association" "public" {
  count          = var.vpc_mode == "new" ? 2 : 0
  subnet_id      = aws_subnet.public[count.index].id
  route_table_id = aws_route_table.public[0].id
}

# ─── Locals para unificar VPC/subnets ─────────────────────────────────────────

locals {
  vpc_id     = var.vpc_mode == "new" ? aws_vpc.workshop[0].id : data.aws_vpc.default[0].id
  subnet_ids = var.vpc_mode == "new" ? aws_subnet.public[*].id : data.aws_subnets.default_public[0].ids
}

# ─── Security Groups ──────────────────────────────────────────────────────────

resource "aws_security_group" "alb" {
  name        = "route53-arc-alb-sg"
  description = "SG ALB – permite HTTP público"
  vpc_id      = local.vpc_id

  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = { Name = "route53-arc-alb-sg" }
}

resource "aws_security_group" "ec2" {
  name        = "route53-arc-ec2-sg"
  description = "SG EC2 – solo acepta tráfico del ALB"
  vpc_id      = local.vpc_id

  ingress {
    from_port       = 80
    to_port         = 80
    protocol        = "tcp"
    security_groups = [aws_security_group.alb.id]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = { Name = "route53-arc-ec2-sg" }
}

# ─── IAM Role para SSM ────────────────────────────────────────────────────────

resource "aws_iam_role" "ec2_ssm" {
  name = "route53-arc-ec2-ssm-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect    = "Allow"
      Principal = { Service = "ec2.amazonaws.com" }
      Action    = "sts:AssumeRole"
    }]
  })
}

resource "aws_iam_role_policy_attachment" "ssm" {
  role       = aws_iam_role.ec2_ssm.name
  policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
}

resource "aws_iam_instance_profile" "ec2" {
  name = "route53-arc-ec2-profile"
  role = aws_iam_role.ec2_ssm.name
}

# ─── EC2 Instance ─────────────────────────────────────────────────────────────

resource "aws_instance" "primary" {
  ami                    = data.aws_ami.al2023.id
  instance_type          = var.instance_type
  subnet_id              = local.subnet_ids[0]
  vpc_security_group_ids = [aws_security_group.ec2.id]
  iam_instance_profile   = aws_iam_instance_profile.ec2.name
  user_data              = filebase64("${path.module}/userdata.sh")

  tags = { Name = "route53-arc-primary-web" }
}

# ─── Application Load Balancer ────────────────────────────────────────────────

resource "aws_lb" "primary" {
  name               = "route53-arc-primary-alb"
  internal           = false
  load_balancer_type = "application"
  security_groups    = [aws_security_group.alb.id]
  subnets            = local.subnet_ids
}

resource "aws_lb_target_group" "primary" {
  name     = "route53-arc-primary-tg"
  port     = 80
  protocol = "HTTP"
  vpc_id   = local.vpc_id

  health_check {
    path                = "/health"
    interval            = 30
    timeout             = 5
    healthy_threshold   = 2
    unhealthy_threshold = 3
  }
}

resource "aws_lb_target_group_attachment" "primary" {
  target_group_arn = aws_lb_target_group.primary.arn
  target_id        = aws_instance.primary.id
  port             = 80
}

resource "aws_lb_listener" "primary" {
  load_balancer_arn = aws_lb.primary.arn
  port              = 80
  protocol          = "HTTP"

  default_action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.primary.arn
  }
}
EOF

4. outputs.tf

¿Para qué sirve este archivo?
Expone los datos clave generados tras el despliegue (DNS del ALB, Zone ID del ALB, ID de la instancia EC2 e ID de la VPC). Estos valores son exportados para que los laboratorios posteriores puedan consumirlos a través de data "terraform_remote_state".

cat > outputs.tf << 'EOF'
output "alb_dns_name" {
  description = "DNS del ALB primario"
  value       = aws_lb.primary.dns_name
}

output "alb_zone_id" {
  description = "Hosted Zone ID del ALB (para alias record en Route 53)"
  value       = aws_lb.primary.zone_id
}

output "ec2_instance_id" {
  description = "ID de la instancia EC2"
  value       = aws_instance.primary.id
}

output "vpc_id" {
  description = "VPC utilizada"
  value       = local.vpc_id
}
EOF

Paso 3: Inicializar y desplegar

# Inicializar Terraform (descarga provider AWS)
terraform init

# Ver el plan antes de aplicar
terraform plan \
  -var="vpc_mode=${VPC_MODE}" \
  -var="instance_type=t4g.micro"

# Aplicar
terraform apply \
  -var="vpc_mode=${VPC_MODE}" \
  -var="instance_type=t4g.micro" \
  -auto-approve
Consejo

Con terraform plan ves exactamente qué recursos se van a crear o modificar antes de aplicar los cambios en AWS.


Paso 4: Ver outputs

terraform output

export PRIMARY_ALB_DNS=$(terraform output -raw alb_dns_name)
echo "🌐 Abre: http://$PRIMARY_ALB_DNS"

✅ Verificación del Lab 1

echo "=== Verificación Lab 1 ==="

# Terraform state limpio
terraform show | grep -q "aws_lb.primary" \
  && echo "✅ ALB en state" || echo "❌ ALB no encontrado en state"

# Sitio responde
sleep 20
HTTP=$(curl -s -o /dev/null -w "%{http_code}" http://$PRIMARY_ALB_DNS/)
[ "$HTTP" = "200" ] \
  && echo "✅ Sitio primario HTTP $HTTP" \
  || echo "⚠️  HTTP $HTTP (espera 1-2 min)"

HEALTH=$(curl -s http://$PRIMARY_ALB_DNS/health | tr -d '[:space:]')
[ "$HEALTH" = "OK" ] \
  && echo "✅ Health check: OK" \
  || echo "⚠️  Health check: '$HEALTH'"

echo "ALB DNS: $PRIMARY_ALB_DNS"

Siguiente paso → Lab 2: Sitio Secundario