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 text

Paso 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