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

EC2 y EBS: instancia segura con IMDSv2, volúmenes, snapshots y Elastic Volumes

⏱ 90-120 minDificultad: mediaTask statements: 3.24.21.1

Qué vas a construir

Una instancia web en eu-south-2 configurada como en producción, y un recorrido práctico por EBS:

  • Rol de IAM con perfil de instancia (sin claves) para gestionar la instancia con Systems Manager Session Manager, sin SSH ni puertos de entrada para administrarla.
  • Instancia t4g.micro (Graviton) con Amazon Linux 2023, IMDSv2 obligatorio, créditos de CPU en modo standard, volumen raíz gp3 cifrado y user data que instala nginx.
  • Un volumen EBS de datos: formatearlo, montarlo, hacer snapshot, restaurarlo en otra AZ y ampliarlo en caliente con Elastic Volumes.
flowchart LR
  CS["CloudShell"] -->|"curl HTTP (solo desde tu IP)"| I["EC2 t4g.micro<br/>eu-south-2a<br/>IMDSv2 required"]
  I --- R["Volumen raíz gp3 8 GiB (cifrado)"]
  I --- D["Volumen datos gp3 1 GiB"]
  D -->|"snapshot"| S["Snapshot (regional)"]
  S -->|"nuevo volumen"| DB["Volumen en eu-south-2b"]
  SSM["Session Manager"] -->|"sin puertos de entrada"| I
  P["Perfil de instancia<br/>Lab03RolEC2"] --> I

Antes de empezar

  • Entra con tu usuario de IAM Identity Center (lab 01), región eu-south-2, y abre CloudShell.
  • Tipos elegibles para la capa gratuita en cuentas creadas desde el 15/07/2025: t3.micro, t3.small, t4g.micro, t4g.small, c7i-flex.large y m7i-flex.large. Usamos t4g.micro (Arm). Si prefieres x86, usa t3.micro y la AMI al2023-ami-kernel-default-x86_64.

Variables de trabajo:

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

# Tipos elegibles para la capa gratuita en esta región
aws ec2 describe-instance-types --filters Name=free-tier-eligible,Values=true \
  --query "InstanceTypes[].InstanceType" --output text

# VPC por defecto (si sale None, créala con: aws ec2 create-default-vpc)
VPC_ID=$(aws ec2 describe-vpcs --filters Name=is-default,Values=true \
  --query "Vpcs[0].VpcId" --output text)

SUBNET_A=$(aws ec2 describe-subnets \
  --filters Name=vpc-id,Values=$VPC_ID Name=availability-zone,Values=eu-south-2a \
  --query "Subnets[0].SubnetId" --output text)

MI_IP=$(curl -s https://checkip.amazonaws.com)
echo "VPC=$VPC_ID SUBNET_A=$SUBNET_A MI_IP=$MI_IP"

MI_IP es la IP pública de salida de tu CloudShell: la usarás para abrir el puerto 80 solo a ti.

Paso 1: Rol de IAM y perfil de instancia

La instancia necesita permisos para hablar con Systems Manager. Nunca con claves: con un rol que asume el servicio EC2.

cat > confianza-ec2.json <<'EOF'
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": { "Service": "ec2.amazonaws.com" },
      "Action": "sts:AssumeRole"
    }
  ]
}
EOF

aws iam create-role --role-name Lab03RolEC2 \
  --assume-role-policy-document file://confianza-ec2.json
aws iam attach-role-policy --role-name Lab03RolEC2 \
  --policy-arn arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
aws iam create-instance-profile --instance-profile-name Lab03PerfilEC2
aws iam add-role-to-instance-profile --instance-profile-name Lab03PerfilEC2 \
  --role-name Lab03RolEC2
sleep 15   # propagación de IAM

Paso 2: Security group mínimo

SG_ID=$(aws ec2 create-security-group --group-name lab03-web \
  --description "Lab03 HTTP solo desde mi IP" --vpc-id $VPC_ID \
  --query GroupId --output text)

aws ec2 authorize-security-group-ingress --group-id $SG_ID \
  --protocol tcp --port 80 --cidr ${MI_IP}/32

No abrimos el 22 (SSH): administrarás la instancia con Session Manager, que sale de la instancia hacia AWS y no necesita puertos de entrada.

Paso 3: Lanza la instancia con user data e IMDSv2

cat > user-data.sh <<'EOF'
#!/bin/bash
dnf install -y nginx
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 03</h1><p>Instancia $ID en $AZ</p>" > /usr/share/nginx/html/index.html
systemctl enable --now nginx
EOF

INSTANCIA=$(aws ec2 run-instances \
  --image-id resolve:ssm:/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-arm64 \
  --instance-type t4g.micro \
  --subnet-id $SUBNET_A \
  --associate-public-ip-address \
  --security-group-ids $SG_ID \
  --iam-instance-profile Name=Lab03PerfilEC2 \
  --metadata-options HttpTokens=required,HttpPutResponseHopLimit=1,HttpEndpoint=enabled \
  --credit-specification CpuCredits=standard \
  --block-device-mappings '[{"DeviceName":"/dev/xvda","Ebs":{"VolumeSize":8,"VolumeType":"gp3","Encrypted":true,"DeleteOnTermination":true}}]' \
  --user-data file://user-data.sh \
  --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=lab03-web},{Key=Proyecto,Value=lab03}]' 'ResourceType=volume,Tags=[{Key=Proyecto,Value=lab03}]' \
  --query "Instances[0].InstanceId" --output text)

echo "Instancia: $INSTANCIA"
aws ec2 wait instance-running --instance-ids $INSTANCIA

Qué hace cada opción importante:

  • resolve:ssm: busca la AMI más reciente de Amazon Linux 2023 para Arm en el parámetro público de SSM: nada de copiar ID de AMI a mano (son distintos en cada región).
  • HttpTokens=required: solo IMDSv2; HttpPutResponseHopLimit=1: el token no sale de la instancia.
  • CpuCredits=standard: una T3/T4g se lanza en unlimited por defecto; en standard no hay recargos por ráfagas prolongadas.
  • Encrypted=true: el volumen raíz se cifra con la clave administrada por AWS para EBS.
  • Las etiquetas Proyecto=lab03 sirven para localizar todo al limpiar y para la asignación de costes.

Paso 4: Comprueba la web

El user data tarda uno o dos minutos en instalar nginx.

IP=$(aws ec2 describe-instances --instance-ids $INSTANCIA \
  --query "Reservations[0].Instances[0].PublicIpAddress" --output text)
sleep 60
curl -s http://$IP

Debe aparecer Instancia i-… en eu-south-2a. Si no responde, espera un poco más; si sigue sin responder, comprueba que MI_IP no ha cambiado (curl -s https://checkip.amazonaws.com).

Paso 5: Entra con Session Manager y comprueba IMDSv2 y el rol

En la consola: EC2 → Instances → lab03-web → Connect → Session Manager → Connect (si no aparece disponible, espera un par de minutos a que el agente de SSM se registre). En el terminal de la instancia:

# Sin token: IMDSv1 está desactivado, debe devolver 401
curl -s -o /dev/null -w "%{http_code}\n" http://169.254.169.254/latest/meta-data/instance-id

# Con token (IMDSv2)
TOKEN=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" \
  -H "X-aws-ec2-metadata-token-ttl-seconds: 300")
curl -s -H "X-aws-ec2-metadata-token: $TOKEN" \
  http://169.254.169.254/latest/meta-data/instance-type; echo

# Nombre del rol del que la instancia obtiene credenciales temporales
curl -s -H "X-aws-ec2-metadata-token: $TOKEN" \
  http://169.254.169.254/latest/meta-data/iam/security-credentials/; echo

# La CLI de la instancia usa ese rol sin que configures nada
aws sts get-caller-identity

La última orden muestra assumed-role/Lab03RolEC2/i-…: así obtiene permisos una aplicación en EC2, sin claves en disco. Deja abierta esta sesión.

Paso 6: Añade un volumen EBS de datos

En CloudShell:

VOL=$(aws ec2 create-volume --availability-zone eu-south-2a --size 1 \
  --volume-type gp3 --encrypted \
  --tag-specifications 'ResourceType=volume,Tags=[{Key=Name,Value=lab03-datos},{Key=Proyecto,Value=lab03}]' \
  --query VolumeId --output text)
aws ec2 wait volume-available --volume-ids $VOL
aws ec2 attach-volume --volume-id $VOL --instance-id $INSTANCIA --device /dev/sdf

En la sesión de la instancia (en instancias Nitro los volúmenes EBS aparecen como dispositivos NVMe):

lsblk                                   # verás un disco nuevo de 1G, p. ej. nvme1n1
sudo mkfs -t xfs /dev/nvme1n1           # ¡comprueba el nombre antes!
sudo mkdir -p /datos && sudo mount /dev/nvme1n1 /datos
echo "dato importante $(date)" | sudo tee /datos/prueba.txt
df -h /datos

Paso 7: Snapshot y restauración en otra AZ

Los volúmenes viven en una AZ; los snapshots son regionales. Así se «mueve» un disco de AZ.

SNAP=$(aws ec2 create-snapshot --volume-id $VOL --description "lab03 datos" \
  --tag-specifications 'ResourceType=snapshot,Tags=[{Key=Proyecto,Value=lab03}]' \
  --query SnapshotId --output text)
aws ec2 wait snapshot-completed --snapshot-ids $SNAP

VOL_B=$(aws ec2 create-volume --availability-zone eu-south-2b \
  --snapshot-id $SNAP --volume-type gp3 \
  --tag-specifications 'ResourceType=volume,Tags=[{Key=Name,Value=lab03-datos-az-b},{Key=Proyecto,Value=lab03}]' \
  --query VolumeId --output text)
aws ec2 wait volume-available --volume-ids $VOL_B

# Intenta conectarlo a la instancia de eu-south-2a: fallará por estar en otra AZ
aws ec2 attach-volume --volume-id $VOL_B --instance-id $INSTANCIA --device /dev/sdg

El último comando falla con un error de zona (InvalidVolume.ZoneMismatch): un volumen solo se conecta a instancias de su AZ. El snapshot sí te permitiría crear el volumen en cualquier AZ de la región (o copiarlo a otra región con aws ec2 copy-snapshot).

Paso 8: Amplía el volumen en caliente con Elastic Volumes

aws ec2 modify-volume --volume-id $VOL --size 2
aws ec2 describe-volumes-modifications --volume-ids $VOL \
  --query "VolumesModifications[0].[ModificationState,OriginalSize,TargetSize]" --output text

Cuando el estado pase a optimizing o completed, en la sesión de la instancia:

lsblk                     # el disco ya muestra 2G
sudo xfs_growfs -d /datos # amplía el sistema de ficheros sin desmontar
df -h /datos

Sin parar la instancia ni desmontar nada. Lo mismo sirve para pasar de gp2 a gp3 o subir IOPS y rendimiento de un gp3 (esto último con coste extra por encima de 3.000 IOPS y 125 MiB/s).

Paso 9: Parar y arrancar: qué se conserva

aws ec2 stop-instances --instance-ids $INSTANCIA
aws ec2 wait instance-stopped --instance-ids $INSTANCIA
aws ec2 start-instances --instance-ids $INSTANCIA
aws ec2 wait instance-running --instance-ids $INSTANCIA
aws ec2 describe-instances --instance-ids $INSTANCIA \
  --query "Reservations[0].Instances[0].PublicIpAddress" --output text

Observa:

  • La IPv4 pública ha cambiado (por eso existen las Elastic IP; y por eso nunca se debe depender de la IP de una instancia individual detrás de un servicio).
  • Si vuelves a abrir Session Manager y montas el volumen (sudo mount /dev/nvme1n1 /datos), el fichero prueba.txt sigue ahí: EBS persiste al parar. Con instance store se habría perdido.
  • Esta instancia no puede hibernar: la hibernación hay que activarla al lanzar (y cumplir sus requisitos).

Comprueba que funciona

  • curl http://$IP devolvía la página con el ID de instancia y la AZ.
  • En la instancia, la petición al IMDS sin token devolvía 401.
  • aws sts get-caller-identity dentro de la instancia mostraba el rol Lab03RolEC2.
  • El volumen restaurado en eu-south-2b no se podía conectar a la instancia de eu-south-2a.
  • El volumen se amplió a 2 GiB sin parar la instancia.

Limpieza

Orden: instancia → volúmenes → snapshot → security group → perfil y rol.

aws ec2 terminate-instances --instance-ids $INSTANCIA
aws ec2 wait instance-terminated --instance-ids $INSTANCIA

# El volumen de datos NO se borra al terminar (DeleteOnTermination=false en volúmenes añadidos)
aws ec2 delete-volume --volume-id $VOL
aws ec2 delete-volume --volume-id $VOL_B
aws ec2 delete-snapshot --snapshot-id $SNAP
aws ec2 delete-security-group --group-id $SG_ID

aws iam remove-role-from-instance-profile --instance-profile-name Lab03PerfilEC2 \
  --role-name Lab03RolEC2
aws iam delete-instance-profile --instance-profile-name Lab03PerfilEC2
aws iam detach-role-policy --role-name Lab03RolEC2 \
  --policy-arn arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
aws iam delete-role --role-name Lab03RolEC2

# Verificación: no debe quedar nada con la etiqueta del lab
aws ec2 describe-volumes --filters Name=tag:Proyecto,Values=lab03 --query "Volumes[].VolumeId"
aws ec2 describe-snapshots --owner-ids self --filters Name=tag:Proyecto,Values=lab03 \
  --query "Snapshots[].SnapshotId"
aws ec2 describe-instances --filters Name=tag:Proyecto,Values=lab03 \
  Name=instance-state-name,Values=pending,running,stopping,stopped \
  --query "Reservations[].Instances[].InstanceId"
cd ~ && rm -rf ~/lab03

Las tres últimas consultas deben devolver listas vacías ([]). Si el delete-volume falla porque el volumen aún está «in-use», espera unos segundos y repite.

Preguntas para pensar como arquitecto

1. La aplicación de esta instancia debe guardar ficheros subidos por los usuarios y mañana habrá cinco instancias detrás de un balanceador. ¿Sigues usando un volumen EBS?

No. Un volumen EBS pertenece a una AZ y, salvo Multi-Attach (io1/io2, misma AZ, sistema de ficheros de clúster), a una instancia. Para ficheros compartidos entre instancias y AZ, usa Amazon S3 (objetos) o Amazon EFS (sistema de ficheros compartido). Así las instancias quedan sin estado y pueden escalar en horizontal.

2. ¿Por qué obligar a IMDSv2 si la aplicación «no usa los metadatos»?

Porque el IMDS está disponible para cualquier proceso de la instancia y expone las credenciales temporales del rol. Si la aplicación tiene una vulnerabilidad SSRF o hay un proxy mal configurado, con IMDSv1 un atacante podría pedir esas credenciales. IMDSv2 exige un PUT previo con cabecera y un hop limit de 1, lo que bloquea ese vector. Es defensa en profundidad sin coste.

3. Necesitas un disco de 500 GiB con 10.000 IOPS sostenidas. ¿gp2, gp3 o io2?

gp3: incluye 3.000 IOPS y puedes aprovisionar hasta 500 IOPS por GiB (hasta 80.000), así que 10.000 IOPS en 500 GiB es posible pagando solo las IOPS extra. Con gp2 tendrías 1.500 IOPS base (3 por GiB) y tendrías que sobredimensionar el disco a unos 3.334 GiB. io2 solo compensaría si necesitas latencia submilisegundo, durabilidad 99,999 % o más de 80.000 IOPS.

4. Un equipo quiere copiar este servidor a la región de Irlanda para tener un entorno de DR. ¿Qué pasos seguirías?

Crear una AMI (que incluye snapshots de los volúmenes), copiarla a eu-west-1 con aws ec2 copy-image (cifrándola con una clave KMS de destino si hace falta), y lanzar desde ella con una launch template en esa región. Automatizarlo con AWS Backup o Data Lifecycle Manager (copias entre regiones) y revisar antes las cuotas de vCPU de la región de destino.


Volver al módulo