AWS ECS on Fargate skill for Claude Code
Containers that deploy, drain and roll back cleanly. Claude writes task definitions with valid Fargate sizes, separate execution and task roles, secrets pulled by reference and logs that cannot block the app. Services run in private subnets with the endpoints they need, deploy with a circuit breaker or native blue/green and canary strategies, drain on SIGTERM, and scale on request count or queue backlog instead of guesses.
- name
aws-ecs-fargate - size251 lines · ≈2,900 tokens
- files1
- licenseMIT
- updated
What Claude sees before loading it
This description is the only part of the skill that sits in context all the time. Claude reads it to decide when to load the full playbook below.
description: Runs containers on Amazon ECS with Fargate - task definitions with valid CPU and memory sizes, ARM64, execution role versus task role, secrets from Secrets Manager and SSM, awslogs, container health checks, services behind an ALB with ip target groups, rolling deployments with the circuit breaker, built-in blue/green, linear and canary strategies, Application Auto Scaling, Fargate Spot, private networking with VPC endpoints, and ECS Exec debugging. Use when writing a task definition or ECS service, when tasks stop or fail health checks, when deployments hang, or when moving an app to ECS or Fargate. Try asking
- Move this Dockerized API to ECS Fargate behind an ALB with secrets from Secrets Manager
- Our ECS tasks keep stopping with CannotPullContainerError. Figure out why
- Deploys to our ECS service cause a burst of 502s. Make them zero-downtime
Use it when
- Writing or reviewing an ECS task definition or service
- Tasks stop, fail health checks, or deployments hang
- Setting up autoscaling, Fargate Spot or deployment strategies on ECS
- Deploying to ECS from CI
Skip it when
- The workload runs on Kubernetes or EKS — use kubernetes-deploy
- The job is short and event-driven — use aws-lambda
How to check the result
aws ecs wait services-stable succeeds, all ALB targets are healthy, a deploy under load produces no 5xx, and the circuit breaker rolls back a deliberately broken image in staging.
The common mistake it prevents
Leaving the ALB deregistration delay at 300 seconds while the app ignores SIGTERM, which makes deploys slow and still drops in-flight requests.
An ECS service is done when tasks pull images and secrets through private networking, pass container and load balancer health checks within the grace period, deploy with an automatic rollback path, scale on a metric that tracks load, and shut down gracefully on SIGTERM.
Ground rules
- Two roles, never merged: the execution role lets ECS pull images, read secrets referenced in the task definition, and write logs; the task role is what the application code uses. Scope both to specific ARNs (see aws-iam).
- Deploy immutable image references (a git-SHA tag in an ECR repo with tag immutability,
or a digest). Never deploy
:latest. - Every service has the deployment circuit breaker with rollback (or a built-in blue/green/canary strategy with alarms).
- Updating a production service, changing its desired count to 0, or deleting a service or cluster needs explicit user confirmation.
- Secrets come in via
secrets(valueFrom), never as plaintextenvironmentvalues in the task definition.
Workflow
- [ ] 1. Inspect cluster, service, current task definition, recent events
- [ ] 2. Write or fix the task definition (size, roles, secrets, logs, health)
- [ ] 3. Networking: subnets, security groups, endpoints or NAT
- [ ] 4. Service: ALB target group, deployment strategy, circuit breaker
- [ ] 5. Autoscaling and capacity providers
- [ ] 6. Deploy from CI; wait for steady state
- [ ] 7. Verify health, logs, rollback path
1. Inspect
aws ecs describe-services --cluster prod --services api \
--query 'services[0].{status:status,desired:desiredCount,running:runningCount,td:taskDefinition,deployments:deployments[].{id:id,status:status,rollout:rolloutState,td:taskDefinition}}'
aws ecs describe-services --cluster prod --services api --query 'services[0].events[:15].[createdAt,message]' --output table
aws ecs list-tasks --cluster prod --service-name api --desired-status STOPPED
aws ecs describe-tasks --cluster prod --tasks <task-arn> \
--query 'tasks[0].{stopped:stoppedReason,code:stopCode,containers:containers[].{name:name,exit:exitCode,reason:reason}}'
aws ecs describe-task-definition --task-definition api --query taskDefinition > current-td.json
2. Task definition
{
"family": "api",
"networkMode": "awsvpc",
"requiresCompatibilities": ["FARGATE"],
"cpu": "512",
"memory": "1024",
"runtimePlatform": { "cpuArchitecture": "ARM64", "operatingSystemFamily": "LINUX" },
"executionRoleArn": "arn:aws:iam::123456789012:role/api-execution",
"taskRoleArn": "arn:aws:iam::123456789012:role/api-task",
"containerDefinitions": [
{
"name": "app",
"image": "123456789012.dkr.ecr.eu-west-1.amazonaws.com/api:3f2c1ab",
"essential": true,
"portMappings": [{ "containerPort": 8080, "protocol": "tcp" }],
"environment": [{ "name": "PORT", "value": "8080" }],
"secrets": [
{ "name": "DATABASE_URL", "valueFrom": "arn:aws:secretsmanager:eu-west-1:123456789012:secret:prod/api/db-AbCdEf" },
{ "name": "STRIPE_KEY", "valueFrom": "arn:aws:ssm:eu-west-1:123456789012:parameter/prod/api/stripe_key" }
],
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/ecs/api",
"awslogs-region": "eu-west-1",
"awslogs-stream-prefix": "app",
"mode": "non-blocking",
"max-buffer-size": "25m"
}
},
"healthCheck": {
"command": ["CMD-SHELL", "wget -qO- http://localhost:8080/healthz || exit 1"],
"interval": 15, "timeout": 5, "retries": 3, "startPeriod": 30
},
"stopTimeout": 30,
"readonlyRootFilesystem": true,
"ulimits": [{ "name": "nofile", "softLimit": 65536, "hardLimit": 65536 }]
}
]
}
Fargate CPU/memory combinations (memory in MiB):
| CPU units | Memory |
|---|---|
| 256 (.25 vCPU) | 512, 1024, 2048 |
| 512 | 1024-4096 in 1024 steps |
| 1024 | 2048-8192 in 1024 steps |
| 2048 | 4096-16384 in 1024 steps |
| 4096 | 8192-30720 in 1024 steps |
| 8192 | 16 GB-60 GB in 4 GB steps |
| 16384 | 32 GB-120 GB in 8 GB steps |
Sizing: start from measured usage plus 30% headroom; runtimes need explicit limits
(JVM -XX:MaxRAMPercentage=75, Node --max-old-space-size below the task memory).
ARM64 (Graviton) is cheaper per vCPU-hour; build multi-arch or arm64 images.
Secrets are resolved when the task starts. After rotating a secret, run a new deployment
(--force-new-deployment) or the app keeps the old value. valueFrom can select a JSON
key: <secret-arn>:password::.
If the health check command uses wget/curl, the image must contain it; distroless
images need a tiny health binary or rely on the ALB check only.
3. Networking
- Tasks in private subnets with
assignPublicIp: DISABLED. - Outbound to ECR, Secrets Manager, SSM and CloudWatch Logs needs either a NAT gateway or
VPC endpoints: interface endpoints
ecr.api,ecr.dkr,logs,secretsmanager,ssm(plusssmmessagesfor ECS Exec) and the S3 gateway endpoint (ECR layers live in S3). Endpoints are often cheaper than NAT data processing at volume. - Security groups: ALB SG allows 443 from the internet; task SG allows the container port only from the ALB SG.
4. Service and deployments
aws ecs create-service --cluster prod --service-name api \
--task-definition api:42 --desired-count 3 --launch-type FARGATE \
--network-configuration 'awsvpcConfiguration={subnets=[subnet-a,subnet-b,subnet-c],securityGroups=[sg-task],assignPublicIp=DISABLED}' \
--load-balancers 'targetGroupArn=arn:aws:elasticloadbalancing:...:targetgroup/api/abc,containerName=app,containerPort=8080' \
--health-check-grace-period-seconds 60 \
--deployment-configuration 'minimumHealthyPercent=100,maximumPercent=200,deploymentCircuitBreaker={enable=true,rollback=true}' \
--enable-execute-command
Target group: type ip, health check path /healthz, healthy threshold 2-3, interval
10-15 s, and deregistration delay 30 s (default 300 s makes every deploy slow).
The app must handle SIGTERM: stop accepting, drain in-flight requests within
stopTimeout (max 120 s on Fargate).
| Strategy | Use when |
|---|---|
| Rolling with circuit breaker (default) | Most services; backward-compatible changes |
| Built-in blue/green | Need instant cutover and quick rollback, test traffic before shift, lifecycle hooks |
| Built-in linear / canary (ALB or Service Connect) | Gradual traffic shift with bake time and CloudWatch alarm rollback |
| CodeDeploy blue/green | Existing CodeDeploy setups; new services can use the native strategies |
The native blue/green, linear and canary strategies arrived in 2025; check the current
ECS developer guide for the exact deploymentConfiguration fields (strategy, bake time,
lifecycle hooks, alarms) before writing them.
5. Autoscaling and capacity
aws application-autoscaling register-scalable-target --service-namespace ecs \
--resource-id service/prod/api --scalable-dimension ecs:service:DesiredCount \
--min-capacity 3 --max-capacity 30
aws application-autoscaling put-scaling-policy --service-namespace ecs \
--resource-id service/prod/api --scalable-dimension ecs:service:DesiredCount \
--policy-name cpu60 --policy-type TargetTrackingScaling \
--target-tracking-scaling-policy-configuration '{"TargetValue":60,"PredefinedMetricSpecification":{"PredefinedMetricType":"ECSServiceAverageCPUUtilization"},"ScaleOutCooldown":60,"ScaleInCooldown":300}'
| Workload | Scaling metric |
|---|---|
| HTTP API | ALBRequestCountPerTarget or CPU target tracking |
| Queue worker | Backlog per task (SQS visible messages / running tasks) via custom metric |
| Scheduled batch | ECS scheduled tasks (EventBridge Scheduler), not a service |
Capacity providers: FARGATE for the baseline, FARGATE_SPOT with weight for
interruption-tolerant capacity (base=2 on FARGATE, then weights). Spot tasks get a
SIGTERM about two minutes before reclaim.
6. Deploy from CI
- uses: aws-actions/amazon-ecs-render-task-definition@v1
id: td
with:
task-definition: deploy/taskdef.json
container-name: app
image: ${{ steps.build.outputs.image }} # immutable tag or digest
- uses: aws-actions/amazon-ecs-deploy-task-definition@v2
with:
task-definition: ${{ steps.td.outputs.task-definition }}
service: api
cluster: prod
wait-for-service-stability: true
CLI equivalent:
aws ecs register-task-definition --cli-input-json file://taskdef.json
aws ecs update-service --cluster prod --service api --task-definition api:43 # confirm for prod
aws ecs wait services-stable --cluster prod --services api
Credentials via OIDC (see github-actions); the deploy role needs ecs:RegisterTaskDefinition,
ecs:UpdateService, ecs:Describe*, and iam:PassRole limited to the two task roles.
7. Debugging
| Stopped reason / symptom | Cause | Fix |
|---|---|---|
CannotPullContainerError / timeout | No route to ECR (no NAT/endpoints), wrong tag, execution role lacks ECR | Endpoints or NAT; check tag exists; execution role policy |
ResourceInitializationError: unable to pull secrets | Execution role lacks secretsmanager:GetSecretValue/kms:Decrypt, or no endpoint | Fix role/KMS key policy/endpoints |
Essential container in task exited, exit 1 | App crash at startup | CloudWatch logs for the task; run the image locally with the same env |
Exit 137 / OutOfMemoryError | Memory limit hit | Raise task memory or fix runtime heap limits |
| Tasks killed by ELB health checks | Slow start, wrong path/port, grace period too short | Raise healthCheckGracePeriodSeconds; check path returns 200 |
| Deployment stuck “IN_PROGRESS” | New tasks never healthy; circuit breaker not enabled | Read service events; enable circuit breaker |
| 502/503 during deploys | No SIGTERM handling, deregistration delay vs stopTimeout mismatch | Drain on SIGTERM; deregistration delay <= stopTimeout |
aws ecs execute-command --cluster prod --task <task-id> --container app --interactive --command "/bin/sh"
aws logs tail /ecs/api --since 30m --follow --filter-pattern ERROR
ECS Exec needs enableExecuteCommand, ssmmessages permissions on the task role, and
the Session Manager plugin locally.
Gotchas
- Changing environment, image or secrets requires a new task definition revision and a deployment; editing the secret alone does nothing until tasks restart.
minimumHealthyPercent=100, maximumPercent=200needs room for double the tasks (IP addresses in subnets, service quotas).- The log group must exist, or set
awslogs-create-group: "true"and grantlogs:CreateLogGroupto the execution role. - Container
healthCheckfailing marks the task unhealthy and ECS replaces it even if the ALB check passes; keep both consistent. - Task-level
cpu/memoryare hard limits on Fargate; container-level values only partition them. - Fargate ephemeral storage defaults to 20 GiB (configurable up to 200 GiB); do not write durable data there. Use EFS or S3.
Output
**Service:** prod/api, 3-30 tasks, Fargate ARM64 0.5 vCPU / 1 GiB, private subnets, VPC endpoints
**Roles:** api-execution (ECR, logs, 2 secrets) / api-task (s3:GetObject on uploads bucket)
**Deploy:** rolling + circuit breaker rollback; deregistration delay 30 s; stopTimeout 30 s
**Scaling:** ALBRequestCountPerTarget 500; scale-in cooldown 300 s
**Verified:** services-stable in 4m10s; targets healthy 3/3; no 5xx during deploy; rollback tested in staging About this skill
Written and maintained by the Claude Skills editors to the same editorial standard as every skill in the directory: a selective trigger, a real workflow, known pitfalls and a verification step. First published , last updated . MIT licensed.