Lab práctico · Semana 2: Cómputo con EC2: instancias, EBS, compra, balanceo y escalado

Web altamente disponible con ALB y EC2 Auto Scaling

⏱ 2-3 hDificultad: mediaTask statements: 2.23.24.23.4

Qué vas a construir

La arquitectura web de manual del examen, en eu-south-2:

  • Application Load Balancer internet-facing en todas las AZ de la región, con health checks.
  • Launch template versionada (AMI por parámetro SSM, IMDSv2 obligatorio, sin IP pública en las instancias).
  • Auto Scaling group multi-AZ (mínimo 2, máximo 4) con health checks de ELB, periodo de gracia y default instance warmup.
  • Autorreparación, un lifecycle hook, una política de target tracking sobre ALBRequestCountPerTarget que verás escalar con carga real, y una acción programada.
flowchart TB
  U["CloudShell (tu IP)"] -->|"HTTP 80"| ALB["ALB lab04-alb<br/>sg-alb: 80 desde tu IP"]
  subgraph VPC["VPC por defecto (eu-south-2)"]
    ALB --> TG["Target group lab04-tg<br/>health check /salud"]
    subgraph ASG["Auto Scaling group lab04-asg (min 2, max 4)"]
      I1["t4g.micro AZ a"]
      I2["t4g.micro AZ b"]
      I3["t4g.micro AZ c (al escalar)"]
    end
    TG --> I1
    TG --> I2
    TG --> I3
  end
  CW["CloudWatch<br/>alarmas de target tracking"] -->|"escala"| ASG

Antes de empezar

  • Haber hecho el lab 03 (conceptos de AMI, user data, IMDSv2).
  • Usuario de Identity Center, región eu-south-2, CloudShell abierto.

Variables de trabajo:

export AWS_REGION=eu-south-2
mkdir -p ~/lab04 && cd ~/lab04

VPC_ID=$(aws ec2 describe-vpcs --filters Name=is-default,Values=true \
  --query "Vpcs[0].VpcId" --output text)
SUBNETS=$(aws ec2 describe-subnets \
  --filters Name=vpc-id,Values=$VPC_ID Name=default-for-az,Values=true \
  --query "Subnets[].SubnetId" --output text)
SUBNETS_CSV=$(echo $SUBNETS | tr ' ' ',')
MI_IP=$(curl -s https://checkip.amazonaws.com)
echo "VPC=$VPC_ID"; echo "Subredes: $SUBNETS"; echo "Mi IP: $MI_IP"

Paso 1: Security groups encadenados

El balanceador acepta HTTP solo desde tu IP; las instancias, solo desde el security group del balanceador (no desde Internet).

SG_ALB=$(aws ec2 create-security-group --group-name lab04-alb \
  --description "Lab04 ALB" --vpc-id $VPC_ID --query GroupId --output text)
aws ec2 authorize-security-group-ingress --group-id $SG_ALB \
  --protocol tcp --port 80 --cidr ${MI_IP}/32

SG_WEB=$(aws ec2 create-security-group --group-name lab04-web \
  --description "Lab04 instancias web" --vpc-id $VPC_ID --query GroupId --output text)
aws ec2 authorize-security-group-ingress --group-id $SG_WEB \
  --protocol tcp --port 80 --source-group $SG_ALB

Si quieres abrir la web también en el navegador de tu ordenador, añade otra regla al SG_ALB con tu IP de casa. En producción sería 0.0.0.0/0 en el balanceador (y AWS WAF delante).

Paso 2: Launch template

El servidor web usa python3 -m http.server, que ya viene en Amazon Linux 2023: así las instancias no necesitan salida a Internet y pueden ir sin IP pública.

cat > user-data.sh <<'EOF'
#!/bin/bash
mkdir -p /var/www
TOKEN=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" \
  -H "X-aws-ec2-metadata-token-ttl-seconds: 300")
ID=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" \
  http://169.254.169.254/latest/meta-data/instance-id)
AZ=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" \
  http://169.254.169.254/latest/meta-data/placement/availability-zone)
echo "<h1>Lab 04</h1><p>$ID en $AZ</p>" > /var/www/index.html
echo "ok" > /var/www/salud
cat > /etc/systemd/system/web.service <<'UNIT'
[Unit]
Description=Servidor web del lab 04
After=network-online.target
[Service]
ExecStart=/usr/bin/python3 -m http.server 80 --directory /var/www
Restart=always
[Install]
WantedBy=multi-user.target
UNIT
systemctl daemon-reload
systemctl enable --now web
EOF

UD=$(base64 -w0 user-data.sh)

cat > plantilla.json <<EOF
{
  "ImageId": "resolve:ssm:/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-arm64",
  "InstanceType": "t4g.micro",
  "NetworkInterfaces": [
    { "DeviceIndex": 0, "AssociatePublicIpAddress": false, "Groups": ["$SG_WEB"] }
  ],
  "MetadataOptions": { "HttpTokens": "required", "HttpPutResponseHopLimit": 1, "HttpEndpoint": "enabled" },
  "CreditSpecification": { "CpuCredits": "standard" },
  "Monitoring": { "Enabled": true },
  "UserData": "$UD",
  "TagSpecifications": [
    { "ResourceType": "instance",
      "Tags": [ { "Key": "Name", "Value": "lab04-web" }, { "Key": "Proyecto", "Value": "lab04" } ] }
  ]
}
EOF

aws ec2 create-launch-template --launch-template-name lab04-web \
  --version-description "v1 python http.server" \
  --launch-template-data file://plantilla.json

Fíjate en tres detalles de examen: en una launch template el user data va en base64 (la CLI no lo codifica por ti aquí, a diferencia de run-instances); IMDSv2 va en MetadataOptions; y Monitoring activa la monitorización detallada (métricas de EC2 cada minuto en lugar de cada 5).

Paso 3: Target group y Application Load Balancer

TG_ARN=$(aws elbv2 create-target-group --name lab04-tg \
  --protocol HTTP --port 80 --vpc-id $VPC_ID --target-type instance \
  --health-check-path /salud --health-check-interval-seconds 10 \
  --healthy-threshold-count 2 --unhealthy-threshold-count 2 \
  --query "TargetGroups[0].TargetGroupArn" --output text)

# Menos espera al dar de baja destinos (por defecto son 300 s)
aws elbv2 modify-target-group-attributes --target-group-arn $TG_ARN \
  --attributes Key=deregistration_delay.timeout_seconds,Value=30

ALB_ARN=$(aws elbv2 create-load-balancer --name lab04-alb --type application \
  --scheme internet-facing --subnets $SUBNETS --security-groups $SG_ALB \
  --tags Key=Proyecto,Value=lab04 \
  --query "LoadBalancers[0].LoadBalancerArn" --output text)
aws elbv2 wait load-balancer-available --load-balancer-arns $ALB_ARN

ALB_DNS=$(aws elbv2 describe-load-balancers --load-balancer-arns $ALB_ARN \
  --query "LoadBalancers[0].DNSName" --output text)

aws elbv2 create-listener --load-balancer-arn $ALB_ARN --protocol HTTP --port 80 \
  --default-actions Type=forward,TargetGroupArn=$TG_ARN
echo "http://$ALB_DNS"

El ALB exige al menos dos AZ; le damos todas las subredes por defecto (una por AZ). En el ALB el reparto entre zonas (cross-zone) está siempre activado.

Paso 4: Auto Scaling group

aws autoscaling create-auto-scaling-group --auto-scaling-group-name lab04-asg \
  --launch-template 'LaunchTemplateName=lab04-web,Version=$Latest' \
  --min-size 2 --max-size 4 --desired-capacity 2 \
  --vpc-zone-identifier "$SUBNETS_CSV" \
  --target-group-arns $TG_ARN \
  --health-check-type ELB --health-check-grace-period 90 \
  --default-instance-warmup 60 \
  --tags Key=Proyecto,Value=lab04,PropagateAtLaunch=true

aws autoscaling enable-metrics-collection --auto-scaling-group-name lab04-asg \
  --granularity "1Minute"
  • --health-check-type ELB: si el target group marca una instancia como no sana (la app no responde en /salud), el ASG la sustituye. Con solo EC2 no lo haría.
  • --health-check-grace-period 90: margen para que arranque antes de evaluarla (con la CLI el valor por defecto sería 0).
  • --default-instance-warmup 60: las instancias nuevas no cuentan en las métricas del grupo durante 60 s.

Espera un par de minutos y comprueba:

aws elbv2 describe-target-health --target-group-arn $TG_ARN \
  --query "TargetHealthDescriptions[].[Target.Id,TargetHealth.State]" --output table

for i in $(seq 1 8); do curl -s http://$ALB_DNS/; echo; done

Verás respuestas de dos instancias en AZ distintas: el ASG reparte entre AZ y el ALB reparte el tráfico.

Paso 5: Autorreparación

Simula que la aplicación de una instancia falla:

VICTIMA=$(aws autoscaling describe-auto-scaling-groups --auto-scaling-group-names lab04-asg \
  --query "AutoScalingGroups[0].Instances[0].InstanceId" --output text)
aws autoscaling set-instance-health --instance-id $VICTIMA --health-status Unhealthy

sleep 60
aws autoscaling describe-scaling-activities --auto-scaling-group-name lab04-asg --max-items 3 \
  --query "Activities[].[StatusCode,Description]" --output table

El ASG termina la instancia y lanza otra para volver a la capacidad deseada. Mientras tanto, el ALB sigue sirviendo con la otra: sin punto único de fallo.

Paso 6: Lifecycle hook de lanzamiento

Un hook pausa las instancias nuevas en Pending:Wait hasta que confirmes que están listas (en la vida real lo haría un script o una función Lambda).

aws autoscaling put-lifecycle-hook --lifecycle-hook-name lab04-arranque \
  --auto-scaling-group-name lab04-asg \
  --lifecycle-transition autoscaling:EC2_INSTANCE_LAUNCHING \
  --heartbeat-timeout 300 --default-result CONTINUE

aws autoscaling set-desired-capacity --auto-scaling-group-name lab04-asg --desired-capacity 3
sleep 45
aws autoscaling describe-auto-scaling-instances \
  --query "AutoScalingInstances[?AutoScalingGroupName=='lab04-asg'].[InstanceId,AvailabilityZone,LifecycleState]" \
  --output table

Una instancia aparecerá en Pending:Wait y no recibe tráfico del ALB todavía. Complétala tú:

NUEVA=$(aws autoscaling describe-auto-scaling-instances \
  --query "AutoScalingInstances[?LifecycleState=='Pending:Wait'].InstanceId" --output text)
aws autoscaling complete-lifecycle-action --lifecycle-hook-name lab04-arranque \
  --auto-scaling-group-name lab04-asg --lifecycle-action-result CONTINUE \
  --instance-id $NUEVA

# Retira el hook para el resto del lab y vuelve a 2 instancias
aws autoscaling delete-lifecycle-hook --lifecycle-hook-name lab04-arranque \
  --auto-scaling-group-name lab04-asg
aws autoscaling set-desired-capacity --auto-scaling-group-name lab04-asg --desired-capacity 2

Si no la hubieras completado, a los 300 s se aplicaría el resultado por defecto (CONTINUE). Con ABANDON, el ASG la terminaría y lanzaría otra.

Paso 7: Target tracking con carga real

Política: mantener unas 100 peticiones por minuto y por instancia (ALBRequestCountPerTarget). Necesita identificar el ALB y el target group con un ResourceLabel:

LB_SUFIJO=${ALB_ARN#*loadbalancer/}     # app/lab04-alb/…
TG_SUFIJO=${TG_ARN##*:}                  # targetgroup/lab04-tg/…
RESOURCE_LABEL="$LB_SUFIJO/$TG_SUFIJO"

cat > politica.json <<EOF
{
  "TargetValue": 100.0,
  "PredefinedMetricSpecification": {
    "PredefinedMetricType": "ALBRequestCountPerTarget",
    "ResourceLabel": "$RESOURCE_LABEL"
  }
}
EOF

aws autoscaling put-scaling-policy --auto-scaling-group-name lab04-asg \
  --policy-name lab04-peticiones --policy-type TargetTrackingScaling \
  --target-tracking-configuration file://politica.json

aws cloudwatch describe-alarms --alarm-name-prefix TargetTracking-lab04-asg \
  --query "MetricAlarms[].[AlarmName,StateValue]" --output table

Auto Scaling ha creado dos alarmas (alta y baja) que no debes tocar. Genera carga durante unos 8 minutos (4 bucles en paralelo):

for n in 1 2 3 4; do
  timeout 480 bash -c "while true; do curl -s -o /dev/null http://$ALB_DNS/; done" &
done

# Mientras tanto, cada minuto o dos:
aws autoscaling describe-auto-scaling-groups --auto-scaling-group-names lab04-asg \
  --query "AutoScalingGroups[0].[DesiredCapacity,length(Instances)]" --output text
aws autoscaling describe-scaling-activities --auto-scaling-group-name lab04-asg --max-items 3 \
  --query "Activities[].[StatusCode,Description]" --output table

En pocos minutos la alarma alta pasa a ALARM y la capacidad deseada sube (hasta el máximo de 4). Cuando termina la carga, la reducción es más lenta y conservadora (target tracking prioriza la disponibilidad y la alarma baja necesita más minutos seguidos por debajo del objetivo): no hace falta que esperes a verla.

Paso 8: Acción programada

Para cargas con picos conocidos no hace falta esperar a la métrica. Ejemplo: por la noche, bajar a 1 instancia (un entorno de desarrollo):

aws autoscaling put-scheduled-update-group-action --auto-scaling-group-name lab04-asg \
  --scheduled-action-name lab04-noche --recurrence "0 22 * * *" --time-zone "Europe/Madrid" \
  --min-size 1 --max-size 2 --desired-capacity 1

aws autoscaling describe-scheduled-actions --auto-scaling-group-name lab04-asg \
  --query "ScheduledUpdateGroupActions[].[ScheduledActionName,Recurrence,TimeZone,DesiredCapacity]" \
  --output table

Se borrará con el grupo en la limpieza.

Comprueba que funciona

  • curl al DNS del ALB alternaba entre instancias de AZ distintas.
  • Al marcar una instancia como Unhealthy, el ASG la sustituyó sola.
  • Viste una instancia en Pending:Wait y la pasaste a InService con complete-lifecycle-action.
  • Con carga, la capacidad deseada subió por encima de 2.
  • Las instancias no tienen IP pública (aws ec2 describe-instances --filters Name=tag:Proyecto,Values=lab04 --query "Reservations[].Instances[].PublicIpAddress" devuelve una lista vacía o nulos).

Limpieza

Orden de dependencias: grupo (y sus instancias) → balanceador → target group → plantilla → security groups (primero el que referencia al otro).

# Detén los bucles de carga si siguen activos
kill $(jobs -p) 2>/dev/null

aws autoscaling delete-auto-scaling-group --auto-scaling-group-name lab04-asg --force-delete
while [ "$(aws autoscaling describe-auto-scaling-groups --auto-scaling-group-names lab04-asg \
  --query 'length(AutoScalingGroups)' --output text)" != "0" ]; do sleep 15; done

aws elbv2 delete-load-balancer --load-balancer-arn $ALB_ARN
aws elbv2 wait load-balancers-deleted --load-balancer-arns $ALB_ARN
aws elbv2 delete-target-group --target-group-arn $TG_ARN

aws ec2 delete-launch-template --launch-template-name lab04-web

sleep 60   # las interfaces de red del ALB tardan en liberarse
aws ec2 delete-security-group --group-id $SG_WEB
aws ec2 delete-security-group --group-id $SG_ALB

# Verificación
aws ec2 describe-instances --filters Name=tag:Proyecto,Values=lab04 \
  Name=instance-state-name,Values=pending,running,stopping,stopped \
  --query "Reservations[].Instances[].InstanceId"
aws elbv2 describe-load-balancers --query "LoadBalancers[?LoadBalancerName=='lab04-alb'].LoadBalancerArn"
aws cloudwatch describe-alarms --alarm-name-prefix TargetTracking-lab04-asg --query "MetricAlarms[].AlarmName"
cd ~ && rm -rf ~/lab04

Si un delete-security-group falla con DependencyViolation, espera otro minuto y repítelo. Las alarmas de target tracking se eliminan con la política; la última consulta debe devolver [].

Preguntas para pensar como arquitecto

1. ¿Por qué las instancias pueden no tener IP pública si reciben tráfico de Internet?

Porque quien está en Internet es el ALB (internet-facing, con nodos públicos). El ALB llega a los destinos por su IP privada. En producción las instancias estarían en subredes privadas; si necesitaran salir a Internet (actualizaciones), lo harían a través de un NAT Gateway o de VPC endpoints (módulo 05). Menos superficie de ataque y menos coste de IPv4 públicas.

2. ¿Qué pasaría si el ASG usara solo health checks de EC2 y el proceso web muriera?

La instancia seguiría running y pasando las comprobaciones de estado de EC2, así que el ASG no la sustituiría. El ALB dejaría de enviarle tráfico (su health check falla), pero la capacidad real quedaría reducida indefinidamente. Con --health-check-type ELB el ASG la reemplaza.

3. El tráfico real de esta web sube cada día laborable a las 9:00 y la aplicación tarda 6 minutos en arrancar. ¿Qué políticas combinarías?

Escalado predictivo (o una acción programada si el patrón es fijo) para tener capacidad lista antes de las 9:00, más target tracking para absorber desviaciones. Además, reducir el tiempo de arranque con una AMI dorada o un warm pool. El predictivo solo escala hacia fuera; la política dinámica se encarga de reducir.

4. Un cliente exige que tu servicio tenga una IP fija para su cortafuegos. ¿Cambias el ALB?

Opciones: poner un NLB (IP estática por AZ, se puede asignar Elastic IP) delante, con el ALB como destino del NLB si necesitas las reglas de capa 7; o AWS Global Accelerator delante del ALB, que da dos IP anycast estáticas globales y mejora la latencia. El ALB por sí solo no ofrece IP fijas.

5. ¿Cómo reducirías el coste de este ASG en producción sin perder disponibilidad?

Savings Plans para la capacidad mínima estable, una mixed instances policy con una base On-Demand y el resto en Spot diversificado en varios tipos y AZ (con Capacity Rebalancing), instancias Graviton, rightsizing con Compute Optimizer y, en entornos no productivos, acciones programadas que bajan a 0 fuera de horario.


Volver al módulo