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 nginxPaso 1: Variables de entorno y Verificación de VPC
source ~/route53-arc-tf/global.env
cd ~/route53-arc-tf/01_primaryAntes 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 utilizarVPC_MODE="default". - Si devuelve
None, asegurate de queVPC_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"
}
EOF2. 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/healthnecesario 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
EOF3. 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 (
arm64parat4g.micro,x86_64parat3.micro).- Red (VPC): Si
vpc_mode = "default", consulta la VPC Default de la región. Sivpc_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
}
}
EOF4. 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 dedata "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
}
EOFPaso 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-approveConsejo
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