Lab 1: Sitio Web Primario
Lab 1: Sitio Web Primario (EC2 + ALB)
En este laboratorio desplegaremos el sitio web principal compuesto por una instancia EC2 ejecutando Amazon Linux 2023 con servidor Nginx, frente a un Application Load Balancer (ALB) público.
Usuario → Application Load Balancer (puerto 80) → EC2 Amazon Linux 2023 + NginxCargar variables de entorno
source ~/global.envOpción A: CloudFormation (Base)
Elegí esta opción si preferís usar plantillas YAML declarativas desplegadas con AWS CLI.
1. Crear el template CloudFormation
cat > /tmp/primary-site.yaml << 'TEMPLATE_EOF'
AWSTemplateFormatVersion: '2010-09-09'
Description: 'roLearning - Route53 ARC Workshop - Sitio Primario (EC2 + ALB)'
Parameters:
UseDefaultVPC:
Type: String
Default: "true"
AllowedValues: ["true", "false"]
Description: "true = usar VPC default existente | false = crear VPC nueva"
VpcId:
Type: String
Default: ""
Description: "VPC ID de la VPC default (requerido si UseDefaultVPC=true)"
SubnetId1:
Type: String
Default: ""
Description: "Subnet ID pública 1 (requerido si UseDefaultVPC=true)"
SubnetId2:
Type: String
Default: ""
Description: "Subnet ID pública 2 (requerido si UseDefaultVPC=true)"
InstanceType:
Type: String
Default: t4g.micro
AllowedValues: [t4g.micro, t3.micro]
Description: "t4g.micro (ARM/Graviton, recomendado) o t3.micro (x86)"
Conditions:
CreateNewVPC: !Equals [!Ref UseDefaultVPC, "false"]
UseExistingVPC: !Equals [!Ref UseDefaultVPC, "true"]
IsARM: !Equals [!Ref InstanceType, "t4g.micro"]
Resources:
# ── VPC Nueva (si UseDefaultVPC = false) ──
WorkshopVPC:
Type: AWS::EC2::VPC
Condition: CreateNewVPC
Properties:
CidrBlock: 10.0.0.0/16
EnableDnsSupport: true
EnableDnsHostnames: true
Tags: [{ Key: Name, Value: route53-arc-vpc }]
InternetGateway:
Type: AWS::EC2::InternetGateway
Condition: CreateNewVPC
Properties:
Tags: [{ Key: Name, Value: route53-arc-igw }]
AttachGateway:
Type: AWS::EC2::VPCGatewayAttachment
Condition: CreateNewVPC
Properties:
VpcId: !Ref WorkshopVPC
InternetGatewayId: !Ref InternetGateway
PublicSubnet1:
Type: AWS::EC2::Subnet
Condition: CreateNewVPC
Properties:
VpcId: !Ref WorkshopVPC
CidrBlock: 10.0.1.0/24
AvailabilityZone: !Select [0, !GetAZs '']
MapPublicIpOnLaunch: true
PublicSubnet2:
Type: AWS::EC2::Subnet
Condition: CreateNewVPC
Properties:
VpcId: !Ref WorkshopVPC
CidrBlock: 10.0.2.0/24
AvailabilityZone: !Select [1, !GetAZs '']
MapPublicIpOnLaunch: true
PublicRouteTable:
Type: AWS::EC2::RouteTable
Condition: CreateNewVPC
Properties:
VpcId: !Ref WorkshopVPC
PublicRoute:
Type: AWS::EC2::Route
Condition: CreateNewVPC
DependsOn: AttachGateway
Properties:
RouteTableId: !Ref PublicRouteTable
DestinationCidrBlock: 0.0.0.0/0
GatewayId: !Ref InternetGateway
Subnet1RouteAssoc:
Type: AWS::EC2::SubnetRouteTableAssociation
Condition: CreateNewVPC
Properties:
SubnetId: !Ref PublicSubnet1
RouteTableId: !Ref PublicRouteTable
Subnet2RouteAssoc:
Type: AWS::EC2::SubnetRouteTableAssociation
Condition: CreateNewVPC
Properties:
SubnetId: !Ref PublicSubnet2
RouteTableId: !Ref PublicRouteTable
# ── Security Groups ──
ALBSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: "SG ALB - HTTP publico"
VpcId: !If [CreateNewVPC, !Ref WorkshopVPC, !Ref VpcId]
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 80
ToPort: 80
CidrIp: 0.0.0.0/0
Tags: [{ Key: Name, Value: route53-arc-alb-sg }]
EC2SecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: "SG EC2 - Acepta trafico solo del ALB"
VpcId: !If [CreateNewVPC, !Ref WorkshopVPC, !Ref VpcId]
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 80
ToPort: 80
SourceSecurityGroupId: !Ref ALBSecurityGroup
Tags: [{ Key: Name, Value: route53-arc-ec2-sg }]
# ── IAM Role para SSM ──
EC2SSMRole:
Type: AWS::IAM::Role
Properties:
RoleName: route53-arc-ec2-ssm-role-cfn
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal: { Service: ec2.amazonaws.com }
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
EC2InstanceProfile:
Type: AWS::IAM::InstanceProfile
Properties:
InstanceProfileName: route53-arc-ec2-profile-cfn
Roles: [!Ref EC2SSMRole]
# ── Instancia EC2 ──
PrimaryWebServer:
Type: AWS::EC2::Instance
Properties:
InstanceType: !Ref InstanceType
ImageId: !If
- IsARM
- !Sub "{{resolve:ssm:/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-arm64}}"
- !Sub "{{resolve:ssm:/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64}}"
SubnetId: !If [CreateNewVPC, !Ref PublicSubnet1, !Ref SubnetId1]
SecurityGroupIds: [!Ref EC2SecurityGroup]
IamInstanceProfile: !Ref EC2InstanceProfile
UserData:
Fn::Base64: !Sub |
#!/bin/bash
set -e
dnf install -y nginx
systemctl enable nginx
systemctl start nginx
cat > /usr/share/nginx/html/index.html << 'HTML'
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<title>Sitio Primario – roLearning</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,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: 0.85em; line-height: 1.8; }
</style>
</head>
<body>
<div class="card">
<h1>✅ Sitio Primario</h1>
<h2>roLearning – AWS Route 53 ARC Workshop</h2>
<div class="badge">ONLINE</div>
<p class="meta">Desplegado con <strong>CloudFormation</strong></p>
</div>
</body>
</html>
HTML
echo "OK" > /usr/share/nginx/html/health
systemctl restart nginx
Tags: [{ Key: Name, Value: route53-arc-primary-web }]
# ── ALB y Target Group ──
PrimaryALB:
Type: AWS::ElasticLoadBalancingV2::LoadBalancer
Properties:
Name: route53-arc-primary-alb
Scheme: internet-facing
Type: application
Subnets: !If
- CreateNewVPC
- [!Ref PublicSubnet1, !Ref PublicSubnet2]
- [!Ref SubnetId1, !Ref SubnetId2]
SecurityGroups: [!Ref ALBSecurityGroup]
PrimaryTargetGroup:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Properties:
Name: route53-arc-primary-tg
Port: 80
Protocol: HTTP
VpcId: !If [CreateNewVPC, !Ref WorkshopVPC, !Ref VpcId]
Targets: [{ Id: !Ref PrimaryWebServer, Port: 80 }]
HealthCheckPath: /health
HealthCheckIntervalSeconds: 30
HealthCheckTimeoutSeconds: 5
HealthyThresholdCount: 2
UnhealthyThresholdCount: 3
PrimaryListener:
Type: AWS::ElasticLoadBalancingV2::Listener
Properties:
LoadBalancerArn: !Ref PrimaryALB
Port: 80
Protocol: HTTP
DefaultActions: [{ Type: forward, TargetGroupArn: !Ref PrimaryTargetGroup }]
Outputs:
ALBDNSName:
Value: !GetAtt PrimaryALB.DNSName
Export: { Name: route53-arc-alb-dns }
ALBHostedZoneID:
Value: !GetAtt PrimaryALB.CanonicalHostedZoneID
Export: { Name: route53-arc-alb-hz-id }
EC2InstanceID:
Value: !Ref PrimaryWebServer
Export: { Name: route53-arc-ec2-id }
TEMPLATE_EOF2. Desplegar el Stack
El script resuelve el VPC ID y las subnets reales con aws ec2 describe-* antes de llamar a CloudFormation, evitando dependencias de parámetros SSM que no existen para la VPC default.
Información
⏱️ El deploy de CloudFormation tarda 3-5 minutos. El cursor queda quieto durante ese tiempo — es normal. El comando imprimirá Successfully created/updated stack al finalizar.
# Resolver VPC y subnets para la VPC default (si aplica)
if [ "$USE_DEFAULT_VPC" = "true" ]; then
DEFAULT_VPC_ID=$(aws ec2 describe-vpcs \
--filters "Name=is-default,Values=true" \
--query "Vpcs[0].VpcId" --output text --region $AWS_REGION)
# Obtener las primeras 2 subnets de la VPC default
SUBNET_IDS=$(aws ec2 describe-subnets \
--filters "Name=vpc-id,Values=${DEFAULT_VPC_ID}" "Name=default-for-az,Values=true" \
--query "Subnets[0:2].SubnetId" --output text --region $AWS_REGION)
SUBNET_1=$(echo $SUBNET_IDS | awk '{print $1}')
SUBNET_2=$(echo $SUBNET_IDS | awk '{print $2}')
echo "✅ VPC Default: $DEFAULT_VPC_ID"
echo "✅ Subnets: $SUBNET_1 | $SUBNET_2"
EXTRA_PARAMS="VpcId=$DEFAULT_VPC_ID SubnetId1=$SUBNET_1 SubnetId2=$SUBNET_2"
else
EXTRA_PARAMS=""
fi
# Validar el template antes de desplegarlo
aws cloudformation validate-template \
--template-body file:///tmp/primary-site.yaml \
--region $AWS_REGION > /dev/null \
&& echo "✅ Template válido" \
|| { echo "❌ Error en el template — revisá el bloque de código anterior"; exit 1; }
aws cloudformation deploy \
--template-file /tmp/primary-site.yaml \
--stack-name route53-arc-primary \
--capabilities CAPABILITY_NAMED_IAM \
--region $AWS_REGION \
--parameter-overrides UseDefaultVPC=$USE_DEFAULT_VPC InstanceType=t4g.micro $EXTRA_PARAMS
export PRIMARY_ALB_DNS=$(aws cloudformation describe-stacks \
--stack-name route53-arc-primary --region $AWS_REGION \
--query 'Stacks[0].Outputs[?OutputKey==`ALBDNSName`].OutputValue' --output text)
echo "🌐 ALB DNS (CloudFormation): http://$PRIMARY_ALB_DNS"Opción B: Terraform (Segunda Opción)
Detalles
🚫 Opción B requiere terminal local — no uses CloudShell con Terraform. Ver Lab 0 para más detalles.
1. Crear los archivos del módulo
cd ~/route53-arc-tf/01_primary
# variables.tf
cat > variables.tf << 'EOF'
variable "aws_region" { type = string; default = "us-east-1" }
variable "vpc_mode" { type = string; default = "default" }
variable "instance_type" { type = string; default = "t4g.micro" }
variable "project_tag" { type = string; default = "route53-arc" }
EOF
# userdata.sh
cat > userdata.sh << 'EOF'
#!/bin/bash
set -e
dnf install -y nginx
systemctl enable nginx
systemctl start nginx
cat > /usr/share/nginx/html/index.html << 'HTML'
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<title>Sitio Primario – roLearning</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,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: 0.85em; line-height: 1.8; }
</style>
</head>
<body>
<div class="card">
<h1>✅ Sitio Primario</h1>
<h2>roLearning – AWS Route 53 ARC Workshop</h2>
<div class="badge">ONLINE</div>
<p class="meta">Desplegado con <strong>Terraform</strong></p>
</div>
</body>
</html>
HTML
echo "OK" > /usr/share/nginx/html/health
systemctl restart nginx
EOF
# main.tf
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" } }
}
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}"] }
}
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] }
filter { name = "default-for-az", values = ["true"] }
}
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
}
resource "aws_internet_gateway" "workshop" {
count = var.vpc_mode == "new" ? 1 : 0
vpc_id = aws_vpc.workshop[0].id
}
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
}
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 }
}
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 {
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 : slice(data.aws_subnets.default_public[0].ids, 0, 2)
}
resource "aws_security_group" "alb" {
name = "route53-arc-alb-sg"
description = "SG ALB HTTP"
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"] }
}
resource "aws_security_group" "ec2" {
name = "route53-arc-ec2-sg"
description = "SG EC2 de 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"] }
}
resource "aws_iam_role" "ec2_ssm" {
name = "route53-arc-ec2-ssm-role-tf"
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-tf"
role = aws_iam_role.ec2_ssm.name
}
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" }
}
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
# outputs.tf
cat > outputs.tf << 'EOF'
output "alb_dns_name" { value = aws_lb.primary.dns_name }
output "alb_zone_id" { value = aws_lb.primary.zone_id }
output "ec2_instance_id" { value = aws_instance.primary.id }
output "vpc_id" { value = local.vpc_id }
EOF2. Aplicar los cambios en Terraform
terraform init
terraform apply \
-var="vpc_mode=${VPC_MODE}" \
-var="instance_type=t4g.micro" \
-auto-approve
export PRIMARY_ALB_DNS=$(terraform output -raw alb_dns_name)
echo "🌐 ALB DNS (Terraform): http://$PRIMARY_ALB_DNS"✅ Verificación del Lab 1
Información
⏱️ El ALB tarda 2-5 minutos en registrar la instancia EC2 como saludable. El script siguiente reintenta automáticamente hasta que el sitio responda. También notá que el SSM Agent puede tardar 2-3 minutos adicionales en registrarse después del boot — esto importa en el Lab 5 cuando uses SSM para detener Nginx.
echo "=== Verificación Lab 1: Sitio Primario ==="
echo "ALB DNS: $PRIMARY_ALB_DNS"
echo "⏳ Esperando que el ALB y nginx estén listos (puede tardar hasta 5 minutos)..."
for i in {1..20}; do
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://$PRIMARY_ALB_DNS/ 2>/dev/null)
HEALTH=$(curl -s http://$PRIMARY_ALB_DNS/health 2>/dev/null | tr -d '[:space:]')
echo " [$i/20] HTTP: $HTTP_CODE | /health: ${HEALTH:-vacío}"
if [ "$HTTP_CODE" = "200" ] && [ "$HEALTH" = "OK" ]; then
echo "✅ Sitio primario listo: HTTP $HTTP_CODE | /health: OK"
break
fi
[ "$i" = "20" ] && echo "⚠️ El sitio no respondió en el tiempo esperado. Verificá el estado del stack en la consola de CloudFormation."
sleep 15
doneSiguiente paso → Lab 2: Sitio Secundario