Lab 1: Sitio Web Primario
Lab 1: Sitio Web Primario (EC2 + ALB)
En este lab desplegaremos el sitio web principal usando EC2 con nginx y un Application Load Balancer.
¿Qué vamos a crear?
Internet → Route 53 → ALB (puerto 80) → EC2 Amazon Linux 2023 + nginx- EC2 con Amazon Linux 2023 (ARM) + nginx sirviendo HTML estático
- ALB como punto de entrada con health check propio
- VPC (default existente o nueva, a tu elección)
- Security Groups para el ALB y EC2
Instancia recomendada
| Tipo | Arquitectura | vCPU | RAM | Free Tier |
|---|---|---|---|---|
t4g.micro ⭐ |
ARM (Graviton2) | 2 | 1 GB | ✅ primeros 12 meses |
t3.micro |
x86_64 | 2 | 1 GB | ✅ primeros 12 meses |
Usamos t4g.micro por defecto — mismo precio que t3.micro pero hasta 40% más eficiente. Si por alguna razón no está disponible en tu región o preferís x86, el parámetro del template te permite elegir.
Paso 1: Variables base en CloudShell
export AWS_REGION="us-east-1"
export AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
export STACK_NAME="route53-arc-primary"
echo "Cuenta: $AWS_ACCOUNT_ID | Región: $AWS_REGION"Paso 2: ¿VPC default o nueva?
El template soporta ambas opciones. Primero chequeamos si existe la VPC default en tu cuenta:
DEFAULT_VPC_ID=$(aws ec2 describe-vpcs \
--filters "Name=isDefault,Values=true" \
--region $AWS_REGION \
--query 'Vpcs[0].VpcId' \
--output text)
echo "VPC Default: $DEFAULT_VPC_ID"- Si ves un
vpc-xxxxxxxx→ podés usarla directamente (más rápido, sin costo extra) - Si ves
None→ el template creará una VPC nueva automáticamente
Guarda la decisión:
# Si tenés VPC default y querés usarla:
export USE_DEFAULT_VPC="true"
# Si preferís una VPC nueva dedicada al workshop:
# export USE_DEFAULT_VPC="false"Consejo
La VPC default ya tiene subnets públicas en cada AZ con MapPublicIpOnLaunch=true e Internet Gateway configurado. Es perfecta para este workshop y simplifica bastante el template.
Paso 3: 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 la VPC default existente | false = crear VPC nueva"
InstanceType:
Type: String
Default: t4g.micro
AllowedValues:
- t4g.micro
- t3.micro
Description: "t4g.micro (ARM/Graviton, recomendado) o t3.micro (x86)"
KeyPairName:
Type: String
Default: ""
Description: "Nombre del Key Pair EC2. Dejar vacío para acceso solo via SSM (recomendado)"
Conditions:
CreateNewVPC: !Equals [!Ref UseDefaultVPC, "false"]
UseExistingVPC: !Equals [!Ref UseDefaultVPC, "true"]
HasKeyPair: !Not [!Equals [!Ref KeyPairName, ""]]
IsARM: !Equals [!Ref InstanceType, "t4g.micro"]
Resources:
# ──────────────────────────────────────────────
# VPC NUEVA (solo 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
- Key: Workshop
Value: route53-arc
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 !Ref 'AWS::Region']
MapPublicIpOnLaunch: true
Tags:
- Key: Name
Value: route53-arc-public-1
PublicSubnet2:
Type: AWS::EC2::Subnet
Condition: CreateNewVPC
Properties:
VpcId: !Ref WorkshopVPC
CidrBlock: 10.0.2.0/24
AvailabilityZone: !Select [1, !GetAZs !Ref 'AWS::Region']
MapPublicIpOnLaunch: true
Tags:
- Key: Name
Value: route53-arc-public-2
PublicRouteTable:
Type: AWS::EC2::RouteTable
Condition: CreateNewVPC
Properties:
VpcId: !Ref WorkshopVPC
Tags:
- Key: Name
Value: route53-arc-rt-public
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
# (aplican tanto para VPC nueva como default)
# ──────────────────────────────────────────────
ALBSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: "SG para ALB - permite HTTP publico"
VpcId: !If
- CreateNewVPC
- !Ref WorkshopVPC
- !Sub "{{resolve:ssm:/aws/service/network/default-vpc-id}}"
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 80
ToPort: 80
CidrIp: 0.0.0.0/0
Tags:
- Key: Name
Value: route53-arc-alb-sg
- Key: Workshop
Value: route53-arc
EC2SecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: "SG para EC2 - solo acepta trafico del ALB"
VpcId: !If
- CreateNewVPC
- !Ref WorkshopVPC
- !Sub "{{resolve:ssm:/aws/service/network/default-vpc-id}}"
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 80
ToPort: 80
SourceSecurityGroupId: !Ref ALBSecurityGroup
Tags:
- Key: Name
Value: route53-arc-ec2-sg
- Key: Workshop
Value: route53-arc
# ──────────────────────────────────────────────
# IAM Role para SSM
# ──────────────────────────────────────────────
EC2SSMRole:
Type: AWS::IAM::Role
Properties:
RoleName: route53-arc-ec2-ssm-role
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: ec2.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
Tags:
- Key: Workshop
Value: route53-arc
EC2InstanceProfile:
Type: AWS::IAM::InstanceProfile
Properties:
InstanceProfileName: route53-arc-ec2-profile
Roles:
- !Ref EC2SSMRole
# ──────────────────────────────────────────────
# EC2 Instance – Amazon Linux 2023
# ARM si t4g.micro, x86 si t3.micro
# ──────────────────────────────────────────────
PrimaryWebServer:
Type: AWS::EC2::Instance
Properties:
InstanceType: !Ref InstanceType
# SSM Parameter distinto según arquitectura
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}}"
# Para VPC default usamos la primera subnet pública disponible via SSM
SubnetId: !If
- CreateNewVPC
- !Ref PublicSubnet1
- !Sub "{{resolve:ssm:/aws/service/network/default-vpc-subnet-ids-by-az/a}}"
SecurityGroupIds:
- !Ref EC2SecurityGroup
IamInstanceProfile: !Ref EC2InstanceProfile
KeyName: !If [HasKeyPair, !Ref KeyPairName, !Ref AWS::NoValue]
UserData:
Fn::Base64: !Sub |
#!/bin/bash
set -e
# Instalar nginx en Amazon Linux 2023
dnf install -y nginx
systemctl enable nginx
systemctl start nginx
# Crear página principal del sitio primario
cat > /usr/share/nginx/html/index.html << 'HTML'
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<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;
font-size: 0.9em; letter-spacing: 1px;
}
.meta { color: #475569; margin-top: 28px; 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">
Servidor: EC2 Amazon Linux 2023 + nginx<br>
Región: ${AWS::Region}<br>
Instancia: ${InstanceType}
</p>
</div>
</body>
</html>
HTML
# Endpoint de health check (usado por ALB y Route 53)
echo "OK" > /usr/share/nginx/html/health
systemctl restart nginx
Tags:
- Key: Name
Value: route53-arc-primary-web
- Key: Workshop
Value: route53-arc
# ──────────────────────────────────────────────
# Application Load Balancer
# ──────────────────────────────────────────────
PrimaryALB:
Type: AWS::ElasticLoadBalancingV2::LoadBalancer
Properties:
Name: route53-arc-primary-alb
Scheme: internet-facing
Type: application
# Subnets: nuevas o las de la VPC default (al menos 2 AZs)
Subnets: !If
- CreateNewVPC
- [!Ref PublicSubnet1, !Ref PublicSubnet2]
- !Split
- ","
- !Sub "{{resolve:ssm:/aws/service/network/default-vpc-public-subnet-ids}}"
SecurityGroups:
- !Ref ALBSecurityGroup
Tags:
- Key: Workshop
Value: route53-arc
PrimaryTargetGroup:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Properties:
Name: route53-arc-primary-tg
Port: 80
Protocol: HTTP
VpcId: !If
- CreateNewVPC
- !Ref WorkshopVPC
- !Sub "{{resolve:ssm:/aws/service/network/default-vpc-id}}"
TargetType: instance
Targets:
- Id: !Ref PrimaryWebServer
Port: 80
HealthCheckPath: /health
HealthCheckIntervalSeconds: 30
HealthCheckTimeoutSeconds: 5
HealthyThresholdCount: 2
UnhealthyThresholdCount: 3
Tags:
- Key: Workshop
Value: route53-arc
PrimaryListener:
Type: AWS::ElasticLoadBalancingV2::Listener
Properties:
LoadBalancerArn: !Ref PrimaryALB
Port: 80
Protocol: HTTP
DefaultActions:
- Type: forward
TargetGroupArn: !Ref PrimaryTargetGroup
Outputs:
ALBDNSName:
Description: "DNS del Application Load Balancer primario"
Value: !GetAtt PrimaryALB.DNSName
Export:
Name: route53-arc-alb-dns
ALBHostedZoneID:
Description: "Hosted Zone ID del ALB (para Route 53 alias record)"
Value: !GetAtt PrimaryALB.CanonicalHostedZoneID
Export:
Name: route53-arc-alb-hz-id
EC2InstanceID:
Description: "ID de la instancia EC2"
Value: !Ref PrimaryWebServer
Export:
Name: route53-arc-ec2-id
VPCUsed:
Description: "Modo de VPC utilizado"
Value: !If [CreateNewVPC, "Nueva VPC creada", "VPC Default reutilizada"]
TEMPLATE_EOF
echo "✅ Template creado: $(wc -l < /tmp/primary-site.yaml) líneas"Paso 4: Desplegar el stack
aws cloudformation deploy \
--template-file /tmp/primary-site.yaml \
--stack-name $STACK_NAME \
--capabilities CAPABILITY_NAMED_IAM \
--region $AWS_REGION \
--parameter-overrides \
UseDefaultVPC=$USE_DEFAULT_VPC \
InstanceType=t4g.micro \
KeyPairName="" \
--tags Workshop=route53-arc Environment=primary
echo "Desplegando... (3-5 minutos)"Monitorear progreso:
aws cloudformation describe-stacks \
--stack-name $STACK_NAME \
--region $AWS_REGION \
--query 'Stacks[0].StackStatus' \
--output textPaso 5: Obtener los outputs
aws cloudformation describe-stacks \
--stack-name $STACK_NAME \
--region $AWS_REGION \
--query 'Stacks[0].Outputs' \
--output table
export PRIMARY_ALB_DNS=$(aws cloudformation describe-stacks \
--stack-name $STACK_NAME --region $AWS_REGION \
--query 'Stacks[0].Outputs[?OutputKey==`ALBDNSName`].OutputValue' \
--output text)
echo "ALB DNS: $PRIMARY_ALB_DNS"
echo "🌐 Abre: http://$PRIMARY_ALB_DNS"✅ Verificación del Lab 1
echo "=== Verificación Lab 1: Sitio Primario ==="
STATUS=$(aws cloudformation describe-stacks \
--stack-name $STACK_NAME --region $AWS_REGION \
--query 'Stacks[0].StackStatus' --output text)
[ "$STATUS" = "CREATE_COMPLETE" ] \
&& echo "✅ Stack CloudFormation: $STATUS" \
|| echo "❌ Stack status: $STATUS"
sleep 20
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://$PRIMARY_ALB_DNS/ 2>/dev/null)
[ "$HTTP_CODE" = "200" ] \
&& echo "✅ Sitio primario HTTP: $HTTP_CODE" \
|| echo "⚠️ HTTP: $HTTP_CODE (espera 1-2 min si el ALB acaba de iniciar)"
HEALTH=$(curl -s http://$PRIMARY_ALB_DNS/health 2>/dev/null | tr -d '[:space:]')
[ "$HEALTH" = "OK" ] \
&& echo "✅ Health check endpoint: OK" \
|| echo "⚠️ Health endpoint: '$HEALTH'"
TG_ARN=$(aws elbv2 describe-target-groups \
--names route53-arc-primary-tg --region $AWS_REGION \
--query 'TargetGroups[0].TargetGroupArn' --output text)
TG_HEALTH=$(aws elbv2 describe-target-health \
--target-group-arn $TG_ARN --region $AWS_REGION \
--query 'TargetHealthDescriptions[0].TargetHealth.State' --output text)
[ "$TG_HEALTH" = "healthy" ] \
&& echo "✅ EC2 en Target Group: $TG_HEALTH" \
|| echo "⚠️ Target Health: $TG_HEALTH (espera 1-2 min)"
VPC_USED=$(aws cloudformation describe-stacks \
--stack-name $STACK_NAME --region $AWS_REGION \
--query 'Stacks[0].Outputs[?OutputKey==`VPCUsed`].OutputValue' \
--output text)
echo "ℹ️ $VPC_USED"
echo ""
echo "📌 Guarda este valor:"
echo " PRIMARY_ALB_DNS = $PRIMARY_ALB_DNS"Siguiente paso → Lab 2: Sitio Secundario