Cloud Platforms No. 173

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.

  • nameaws-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

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.

~/.claude/skills/aws-ecs-fargate/SKILL.md

--- name: aws-ecs-fargate description: Runs containers on Amazon ECS with Fargate - task definitions with valid CPU and memory sizes, ARM64, execution role versus task role,… license: MIT ---

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 plaintext environment values 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 unitsMemory
256 (.25 vCPU)512, 1024, 2048
5121024-4096 in 1024 steps
10242048-8192 in 1024 steps
20484096-16384 in 1024 steps
40968192-30720 in 1024 steps
819216 GB-60 GB in 4 GB steps
1638432 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 (plus ssmmessages for 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).

StrategyUse when
Rolling with circuit breaker (default)Most services; backward-compatible changes
Built-in blue/greenNeed 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/greenExisting 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}'
WorkloadScaling metric
HTTP APIALBRequestCountPerTarget or CPU target tracking
Queue workerBacklog per task (SQS visible messages / running tasks) via custom metric
Scheduled batchECS 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 / symptomCauseFix
CannotPullContainerError / timeoutNo route to ECR (no NAT/endpoints), wrong tag, execution role lacks ECREndpoints or NAT; check tag exists; execution role policy
ResourceInitializationError: unable to pull secretsExecution role lacks secretsmanager:GetSecretValue/kms:Decrypt, or no endpointFix role/KMS key policy/endpoints
Essential container in task exited, exit 1App crash at startupCloudWatch logs for the task; run the image locally with the same env
Exit 137 / OutOfMemoryErrorMemory limit hitRaise task memory or fix runtime heap limits
Tasks killed by ELB health checksSlow start, wrong path/port, grace period too shortRaise healthCheckGracePeriodSeconds; check path returns 200
Deployment stuck “IN_PROGRESS”New tasks never healthy; circuit breaker not enabledRead service events; enable circuit breaker
502/503 during deploysNo SIGTERM handling, deregistration delay vs stopTimeout mismatchDrain 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=200 needs room for double the tasks (IP addresses in subnets, service quotas).
  • The log group must exist, or set awslogs-create-group: "true" and grant logs:CreateLogGroup to the execution role.
  • Container healthCheck failing marks the task unhealthy and ECS replaces it even if the ALB check passes; keep both consistent.
  • Task-level cpu/memory are 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.

esc