Happy Dussehra! May good always win. Happy Diwali! Wishing you a bright year ahead. Merry Christmas! Warm wishes from Vizag. Happy New Year! Here’s to a great year ahead.

Archive · AWS

Running Containers on ECS with Fargate

Deploy a container behind an Application Load Balancer on ECS Fargate: task definition, IAM roles, networking, autoscaling, circuit breaker rollbacks and debugging with ECS Exec.

4 min read

  • Amazon ECS
  • AWS Fargate
  • Amazon ECR
  • Application Load Balancer
  • AWS CLI 2.8

Amazon ECS is AWS’s own container orchestrator. With Fargate as the launch type you do not manage servers at all: you describe CPU, memory and a container image, and AWS runs it. It is the simplest way to run a web service or worker on AWS when you do not need the Kubernetes ecosystem.

Versions in use on this date. Fargate platform version 1.4.0 (the default for Linux tasks), which supports ECS Exec, up to 200 GiB of ephemeral storage and EFS volumes, plus ARM64 (Graviton) tasks and Fargate Spot. The deployment circuit breaker has been available since late 2020.

Architecture

flowchart TB
  DEV[CI pipeline] -->|docker push| ECR[(Amazon ECR repository)]
  DEV -->|register task definition and update service| ECS
  U[Users] -->|HTTPS 443| ALB[Application Load Balancer<br/>public subnets]
  ALB -->|HTTP 8080 health check /healthz| TG[Target group type ip]
  subgraph PRIV[Private subnets in two AZs]
    TG --> T1[Fargate task 1<br/>awsvpc ENI, app-sg]
    TG --> T2[Fargate task 2]
  end
  ECS[ECS service desired count 2<br/>circuit breaker, autoscaling] --> T1
  ECS --> T2
  T1 -->|execution role| ECR
  T1 -->|awslogs| CWL[CloudWatch Logs]
  T1 -->|task role| DDB[(DynamoDB, S3, Secrets Manager)]

1. Push the image to ECR

aws ecr create-repository --repository-name app --image-scanning-configuration scanOnPush=true \
  --image-tag-mutability IMMUTABLE --region ap-south-1
aws ecr get-login-password --region ap-south-1 | docker login --username AWS \
  --password-stdin 111122223333.dkr.ecr.ap-south-1.amazonaws.com
docker build -t app:1.0 .
docker tag app:1.0 111122223333.dkr.ecr.ap-south-1.amazonaws.com/app:1.0
docker push 111122223333.dkr.ecr.ap-south-1.amazonaws.com/app:1.0

Immutable tags stop someone overwriting 1.0 after it was tested. Build for the architecture you will run (--platform linux/arm64 for Graviton).

2. Task definition

{
  "family": "app",
  "requiresCompatibilities": ["FARGATE"],
  "networkMode": "awsvpc",
  "cpu": "512",
  "memory": "1024",
  "runtimePlatform": { "cpuArchitecture": "X86_64", "operatingSystemFamily": "LINUX" },
  "executionRoleArn": "arn:aws:iam::111122223333:role/appTaskExecutionRole",
  "taskRoleArn": "arn:aws:iam::111122223333:role/appTaskRole",
  "containerDefinitions": [{
    "name": "app",
    "image": "111122223333.dkr.ecr.ap-south-1.amazonaws.com/app:1.0",
    "essential": true,
    "portMappings": [{ "containerPort": 8080 }],
    "readonlyRootFilesystem": true,
    "environment": [{ "name": "APP_ENV", "value": "prod" }],
    "secrets": [{ "name": "DB_PASSWORD", "valueFrom": "arn:aws:secretsmanager:ap-south-1:111122223333:secret:prod/shop/db-AbCdEf:password::" }],
    "healthCheck": { "command": ["CMD-SHELL", "wget -qO- http://localhost:8080/healthz || exit 1"], "interval": 30, "timeout": 5, "retries": 3, "startPeriod": 20 },
    "logConfiguration": {
      "logDriver": "awslogs",
      "options": { "awslogs-group": "/ecs/app", "awslogs-region": "ap-south-1", "awslogs-stream-prefix": "app" }
    }
  }]
}
aws logs create-log-group --log-group-name /ecs/app && aws logs put-retention-policy --log-group-name /ecs/app --retention-in-days 30
aws ecs register-task-definition --cli-input-json file://taskdef.json

Valid Fargate CPU and memory pairs are fixed combinations (for example 0.5 vCPU with 1 to 4 GB); see the documentation table when you change them.

3. The two roles

Role Used by Typical permissions
Task execution role ECS agent, before your code starts Pull the image from ECR, write logs, read the secrets referenced in secrets (managed policy AmazonECSTaskExecutionRolePolicy plus the specific secret)
Task role Your application code Only what the app calls: one DynamoDB table, one S3 prefix, and so on

Mixing them up is the most common first-day failure: CannotPullContainerError or missing logs point to the execution role; AccessDenied from inside the app points to the task role.

4. Service, load balancer and rollback safety

aws ecs create-cluster --cluster-name prod
aws ecs create-service --cluster prod --service-name app --task-definition app \
  --desired-count 2 --launch-type FARGATE --platform-version LATEST \
  --network-configuration "awsvpcConfiguration={subnets=[subnet-0a1,subnet-0b2],securityGroups=[sg-0app],assignPublicIp=DISABLED}" \
  --load-balancers targetGroupArn=arn:aws:elasticloadbalancing:ap-south-1:111122223333:targetgroup/app/abc123,containerName=app,containerPort=8080 \
  --deployment-configuration "deploymentCircuitBreaker={enable=true,rollback=true},minimumHealthyPercent=100,maximumPercent=200" \
  --enable-execute-command --health-check-grace-period-seconds 30
  • The target group is of type ip, because each task has its own network interface. The ALB security group may reach the task security group on 8080 only.
  • With minimumHealthyPercent=100 and maximumPercent=200, new tasks start before old ones stop, so a deployment does not reduce capacity.
  • The deployment circuit breaker marks a deployment failed if new tasks keep failing to start or pass health checks, and rolls back to the last good task definition.
  • Private subnets need a route to ECR and CloudWatch Logs: a NAT gateway, or VPC endpoints for ecr.api, ecr.dkr, logs and an S3 gateway endpoint (cheaper and more private).

Autoscaling

aws application-autoscaling register-scalable-target --service-namespace ecs \
  --resource-id service/prod/app --scalable-dimension ecs:service:DesiredCount --min-capacity 2 --max-capacity 10
aws application-autoscaling put-scaling-policy --service-namespace ecs --resource-id service/prod/app \
  --scalable-dimension ecs:service:DesiredCount --policy-name cpu60 --policy-type TargetTrackingScaling \
  --target-tracking-scaling-policy-configuration '{"TargetValue":60.0,"PredefinedMetricSpecification":{"PredefinedMetricType":"ECSServiceAverageCPUUtilization"},"ScaleInCooldown":120,"ScaleOutCooldown":60}'

For request-driven services, ALBRequestCountPerTarget is often a better signal than CPU. Use Fargate Spot capacity providers for fault-tolerant workers at a discount, never for the only copy of a critical service.

Debugging

aws ecs describe-services --cluster prod --services app --query 'services[0].events[:5]'
aws ecs list-tasks --cluster prod --service-name app --desired-status STOPPED
aws ecs describe-tasks --cluster prod --tasks <task-arn> --query 'tasks[0].{stop:stoppedReason,containers:containers[].reason}'
aws ecs execute-command --cluster prod --task <task-arn> --container app --interactive --command "/bin/sh"
Symptom Likely cause
Task stops at once, exit code 1 or 137 App crash on start, or out of memory (raise memory)
CannotPullContainerError Execution role, wrong tag, or no route to ECR
Targets unhealthy in the ALB Wrong health check path or port, or security group blocks the ALB
Deployment stuck, then rolls back Failed health checks; read the stopped task’s reason and logs

Compare with EKS

ECS has a smaller surface: no control plane to upgrade, no cluster add-ons, simple IAM integration. EKS gives the Kubernetes API, portability, Helm charts and a huge ecosystem, at the cost of more operations. Choose ECS for a handful of services on AWS; choose EKS when you need Kubernetes tooling or multi-cloud consistency.