Lab práctico · Semana 8: Desacoplamiento, serverless y contenedores
Contenedores sin servidores: servicio de ECS sobre Fargate y Fargate Spot
Qué vas a construir
Un servicio web en contenedores sin gestionar servidores:
- Un clúster de ECS con los capacity providers FARGATE y FARGATE_SPOT.
- Una task definition para Fargate (red
awsvpc, 0,25 vCPU, 512 MiB, logs en CloudWatch) con una imagen de nginx de ECR Public, y el task execution role que necesita. - Un servicio que mantiene varias tareas repartidas entre Fargate y Fargate Spot en las subredes públicas de la VPC por defecto.
- Escalado manual del servicio y comprobación de que ECS reemplaza una tarea que muere.
- (Opcional) Un repositorio privado de Amazon ECR con política de ciclo de vida.
flowchart LR
subgraph VPC["VPC por defecto (eu-south-2)"]
subgraph AZa["Subred pública AZ a"]
T1["Tarea web (FARGATE)"]
end
subgraph AZb["Subred pública AZ b"]
T2["Tarea web (FARGATE_SPOT)"]
end
end
SVC["Servicio ECS lab17-web (desired = 2)"] --> T1
SVC --> T2
IMG["ECR Public: nginx"] -->|"pull con task execution role"| T1
IMG --> T2
T1 --> LOGS["CloudWatch Logs /ecs/lab17"]
T2 --> LOGS
U["curl desde CloudShell"] -->|"HTTP 80 a la IP pública"| T1
Para no pagar un balanceador por hora, en este lab accedes a cada tarea por su IP pública. En producción pondrías un ALB delante y las tareas en subredes privadas.
Antes de empezar
- Usuario de IAM Identity Center o IAM con MFA; no uses root.
- Región eu-south-2 (España) en CloudShell. Necesitas la VPC por defecto con sus subredes públicas (si la borraste, créala con
aws ec2 create-default-vpc). - Coste: Fargate se cobra por segundo (mínimo 1 minuto) según vCPU y memoria; dos o tres tareas de 0,25 vCPU durante una hora son céntimos. Cada tarea con IP pública suma 0,005 USD/h por la IPv4 pública. Consulta AWS Fargate pricing para tu región (consultado el 30/09/2026). Lo que cobra mientras exista: las tareas en ejecución. Pon el servicio a 0 y bórralo al terminar.
- Si Fargate Spot no estuviera disponible en tu región o cuenta, quita
FARGATE_SPOTde la estrategia y usa soloFARGATE.
export AWS_REGION=eu-south-2
export CUENTA=$(aws sts get-caller-identity --query Account --output text)
mkdir -p ~/lab17 && cd ~/lab17
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 | tr '\t' ',')
echo "VPC=$VPC_ID SUBNETS=$SUBNETS"
Paso 1: el task execution role
El task execution role lo usa el agente de ECS/Fargate (no tu aplicación) para descargar la imagen y enviar los logs a CloudWatch. Si tu aplicación necesitara llamar a S3 o DynamoDB, crearías además un task role aparte.
cat > confianza-ecs.json <<'EOF'
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": { "Service": "ecs-tasks.amazonaws.com" },
"Action": "sts:AssumeRole"
}
]
}
EOF
EXEC_ROLE_ARN=$(aws iam create-role --role-name lab17-task-execution \
--assume-role-policy-document file://confianza-ecs.json \
--query Role.Arn --output text)
aws iam attach-role-policy --role-name lab17-task-execution \
--policy-arn arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
echo "$EXEC_ROLE_ARN"
Paso 2: grupo de logs, security group y clúster
aws logs create-log-group --log-group-name /ecs/lab17
aws logs put-retention-policy --log-group-name /ecs/lab17 --retention-in-days 1
SG_ID=$(aws ec2 create-security-group --group-name lab17-web \
--description "Lab 17 - HTTP a las tareas" --vpc-id "$VPC_ID" \
--query GroupId --output text)
aws ec2 authorize-security-group-ingress --group-id "$SG_ID" \
--protocol tcp --port 80 --cidr 0.0.0.0/0
aws ecs create-cluster --cluster-name lab17 \
--capacity-providers FARGATE FARGATE_SPOT \
--default-capacity-provider-strategy capacityProvider=FARGATE,weight=1
Abrir el puerto 80 a todo Internet solo es aceptable en un lab efímero con una página de prueba. En producción, el security group de las tareas solo admitiría tráfico desde el security group del ALB.
Paso 3: la task definition
cat > task-def.json <<EOF
{
"family": "lab17-web",
"networkMode": "awsvpc",
"requiresCompatibilities": ["FARGATE"],
"cpu": "256",
"memory": "512",
"runtimePlatform": { "cpuArchitecture": "X86_64", "operatingSystemFamily": "LINUX" },
"executionRoleArn": "$EXEC_ROLE_ARN",
"containerDefinitions": [
{
"name": "web",
"image": "public.ecr.aws/docker/library/nginx:stable-alpine",
"essential": true,
"portMappings": [{ "containerPort": 80, "protocol": "tcp" }],
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/ecs/lab17",
"awslogs-region": "$AWS_REGION",
"awslogs-stream-prefix": "web"
}
}
}
]
}
EOF
aws ecs register-task-definition --cli-input-json file://task-def.json \
--query 'taskDefinition.[family,revision,cpu,memory,networkMode]' --output text
Fíjate en lo que exige Fargate: networkMode awsvpc (cada tarea tiene su propia ENI y su propio security group) y CPU y memoria a nivel de tarea, con combinaciones válidas (0,25 vCPU admite 512 MiB, 1 GB o 2 GB).
Paso 4: el servicio con Fargate y Fargate Spot
La estrategia de capacidad pone una tarea base en FARGATE (siempre bajo demanda) y reparte el resto 1:3 a favor de FARGATE_SPOT (hasta un 70 % más barato, pero AWS puede interrumpir las tareas con un aviso de 2 minutos).
aws ecs create-service --cluster lab17 --service-name lab17-web \
--task-definition lab17-web --desired-count 2 \
--capacity-provider-strategy capacityProvider=FARGATE,weight=1,base=1 capacityProvider=FARGATE_SPOT,weight=3 \
--network-configuration "awsvpcConfiguration={subnets=[$SUBNETS],securityGroups=[$SG_ID],assignPublicIp=ENABLED}" \
--query 'service.[serviceName,status,desiredCount]' --output text
aws ecs wait services-stable --cluster lab17 --services lab17-web
echo "Servicio estable"
Paso 5: comprobar las tareas
Obtén la IP pública de cada tarea (está en su ENI):
ips_tareas() {
for t in $(aws ecs list-tasks --cluster lab17 --service-name lab17-web --query 'taskArns[]' --output text); do
ENI=$(aws ecs describe-tasks --cluster lab17 --tasks "$t" \
--query "tasks[0].attachments[0].details[?name=='networkInterfaceId'].value" --output text)
CP=$(aws ecs describe-tasks --cluster lab17 --tasks "$t" --query 'tasks[0].capacityProviderName' --output text)
IP=$(aws ec2 describe-network-interfaces --network-interface-ids "$ENI" \
--query 'NetworkInterfaces[0].Association.PublicIp' --output text)
echo "$IP $CP ${t##*/}"
done
}
ips_tareas
Prueba una de ellas y mira los logs:
IP=$(ips_tareas | head -1 | cut -d' ' -f1)
curl -s "http://$IP" | grep -i "<title>"
aws logs tail /ecs/lab17 --since 5m
Verás en la salida qué tarea corre en FARGATE y cuál en FARGATE_SPOT.
Paso 6: escalar y autorreparación
Escala a 3 tareas:
aws ecs update-service --cluster lab17 --service lab17-web --desired-count 3 \
--query 'service.desiredCount'
aws ecs wait services-stable --cluster lab17 --services lab17-web
ips_tareas
Mata una tarea «a mano» y observa cómo el servicio la reemplaza para volver al número deseado:
VICTIMA=$(aws ecs list-tasks --cluster lab17 --service-name lab17-web --query 'taskArns[0]' --output text)
aws ecs stop-task --cluster lab17 --task "$VICTIMA" --reason "Prueba de autorreparación" > /dev/null
sleep 45
aws ecs describe-services --cluster lab17 --services lab17-web \
--query 'services[0].[desiredCount,runningCount,pendingCount]' --output text
aws ecs describe-services --cluster lab17 --services lab17-web \
--query 'services[0].events[:5].message' --output text
En producción, en lugar de cambiar desired-count a mano, configurarías service auto scaling (Application Auto Scaling) con target tracking sobre CPU, memoria, peticiones por destino del ALB o la longitud de una cola SQS.
Paso 7 (opcional): repositorio privado en ECR
Crea un repositorio con escaneo al subir y una política de ciclo de vida que conserve solo las 5 imágenes más recientes (ahorra almacenamiento):
aws ecr create-repository --repository-name lab17/web \
--image-scanning-configuration scanOnPush=true \
--image-tag-mutability IMMUTABLE
cat > ciclo-vida.json <<'EOF'
{
"rules": [
{
"rulePriority": 1,
"description": "Conservar solo las 5 imágenes más recientes",
"selection": { "tagStatus": "any", "countType": "imageCountMoreThan", "countNumber": 5 },
"action": { "type": "expire" }
}
]
}
EOF
aws ecr put-lifecycle-policy --repository-name lab17/web --lifecycle-policy-text file://ciclo-vida.json
CloudShell trae Docker preinstalado (con poco espacio en disco para imágenes), aunque no en todas las regiones. Si en la tuya está disponible (docker info no da error), puedes subir la imagen al repositorio privado:
aws ecr get-login-password | docker login --username AWS --password-stdin "$CUENTA.dkr.ecr.$AWS_REGION.amazonaws.com"
docker pull public.ecr.aws/docker/library/nginx:stable-alpine
docker tag public.ecr.aws/docker/library/nginx:stable-alpine "$CUENTA.dkr.ecr.$AWS_REGION.amazonaws.com/lab17/web:v1"
docker push "$CUENTA.dkr.ecr.$AWS_REGION.amazonaws.com/lab17/web:v1"
aws ecr describe-images --repository-name lab17/web --query 'imageDetails[].[imageTags[0],imageSizeInBytes]' --output text
Para que tareas en subredes privadas descargaran esta imagen sin NAT gateway harían falta los VPC endpoints de interfaz ecr.api y ecr.dkr, el endpoint gateway de S3 y el de CloudWatch Logs.
Comprueba que funciona
aws ecs describe-services --cluster lab17 --services lab17-webmuestrarunningCountigual adesiredCount.curla la IP pública de una tarea devuelve la página de bienvenida de nginx.- Los logs de acceso de nginx aparecen en
/ecs/lab17. - Al parar una tarea, el servicio lanzó otra automáticamente (lo ves en los eventos del servicio).
Limpieza
En orden de dependencias. Las tareas en ejecución son lo que cobra: empieza por ellas.
aws ecs update-service --cluster lab17 --service lab17-web --desired-count 0 > /dev/null
aws ecs delete-service --cluster lab17 --service lab17-web --force > /dev/null
aws ecs wait services-inactive --cluster lab17 --services lab17-web
aws ecs delete-cluster --cluster lab17 > /dev/null
for rev in $(aws ecs list-task-definitions --family-prefix lab17-web --query 'taskDefinitionArns[]' --output text); do
aws ecs deregister-task-definition --task-definition "$rev" > /dev/null
aws ecs delete-task-definitions --task-definitions "$rev" > /dev/null
done
# Espera a que las ENI de las tareas desaparezcan antes de borrar el security group
sleep 60
aws ec2 delete-security-group --group-id "$SG_ID"
aws logs delete-log-group --log-group-name /ecs/lab17
aws ecr delete-repository --repository-name lab17/web --force 2>/dev/null || true
aws iam detach-role-policy --role-name lab17-task-execution \
--policy-arn arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
aws iam delete-role --role-name lab17-task-execution
cd ~ && rm -rf ~/lab17
Si el security group no se deja borrar (DependencyViolation), espera un minuto más: las interfaces de red de las tareas tardan en liberarse.
Preguntas para pensar como arquitecto
- El equipo quiere pasar este servicio a producción con alta disponibilidad y sin exponer las tareas a Internet. ¿Qué cambios haces?
Respuesta
Tareas en subredes privadas de al menos dos AZ (assignPublicIp=DISABLED), un ALB en subredes públicas con listener HTTPS y certificado de ACM, el security group de las tareas abierto solo desde el del ALB, salida mediante NAT gateway o, mejor para ECR, CloudWatch Logs y S3, VPC endpoints; y service auto scaling con target tracking. Para el tráfico base, capacidad FARGATE (bajo demanda); para el extra tolerante a interrupciones, FARGATE_SPOT.
- La aplicación del contenedor necesita leer un bucket de S3 y un secreto de Secrets Manager. ¿Qué rol recibe cada permiso?
Respuesta
La lectura del bucket (lo que hace tu código en tiempo de ejecución) va en el task role. Si el secreto se inyecta como variable de entorno al arrancar mediante secrets en la task definition, quien lo lee es el agente, así que secretsmanager:GetSecretValue (y kms:Decrypt si usa una customer managed key) va en el task execution role. Si es tu código el que llama a Secrets Manager, va en el task role.
- Un proceso de transcodificación tarda 40 minutos por fichero y llega en ráfagas impredecibles. ¿Lambda o Fargate?
Respuesta
Fargate (tareas lanzadas por cada trabajo, por ejemplo desde Step Functions con .sync o desde EventBridge, o un servicio que consume de una cola SQS y escala por su longitud). Lambda tiene un máximo de 15 minutos por invocación. Para trabajos batch masivos con colas de trabajos, prioridades y Spot, AWS Batch sobre Fargate o EC2 también es una opción.
- ¿Cuándo elegirías ECS sobre EC2 en lugar de Fargate?
Respuesta
Cuando necesitas GPU, acceso o configuración especial del host (modo privilegiado, daemons, kernel), tipos de instancia concretos o el máximo ahorro en cargas grandes y estables aprovechando Reserved Instances, Savings Plans o Spot con alta ocupación. Fargate gana en LEAST operational overhead: no hay instancias que parchear ni escalar.
- La empresa ya tiene manifiestos de Kubernetes y quiere ejecutar los mismos contenedores en su CPD y en AWS con las mismas herramientas. ¿Qué opciones propones?
Respuesta
Amazon EKS en AWS y, on-premises, EKS Anywhere (clústeres de Kubernetes en vSphere, bare metal, Nutanix… basados en EKS Distro, con el plano de control en el CPD) o EKS Hybrid Nodes (nodos on-premises unidos a un clúster EKS cuyo plano de control gestiona AWS). Si no usaran Kubernetes, ECS Anywhere permitiría gestionar contenedores on-premises desde ECS.