Amazon ECS (Elastic Container Service)

Updated 5 min read index source
On this page11
  1. Core objects
  2. Task definition
  3. Service
  4. Service Discovery
  5. Autoscaling
  6. Networking modes
  7. Logging
  8. Deployments
  9. Common gotchas
  10. ECS vs EKS
  11. Interview angle

Amazon ECS (Elastic Container Service)

AWS’s managed container orchestrator. Simpler than EKS (Kubernetes), more proprietary. Two launch types:

  • EC2 launch type — you manage EC2 nodes; ECS schedules tasks onto them. More config, lower cost per CPU/RAM.
  • Fargate launch type — serverless containers; AWS runs the nodes. Pay per task vCPU/RAM. Less ops.

For Fargate specifics see AWS Fargate.

Core objects

Object Equivalent
Task definition k8s Pod spec — image, CPU/RAM, env, networking, volumes
Task k8s Pod — running instance of a task definition
Service k8s Deployment + Service — keeps N task copies running, rolling updates
Cluster k8s namespace + node-pool combination
Cluster (logical) grouping of services/tasks; no resource cost on Fargate

Task definition

json
{
  "family": "orders",
  "networkMode": "awsvpc",
  "requiresCompatibilities": ["FARGATE"],
  "cpu": "512",
  "memory": "1024",
  "executionRoleArn": "arn:aws:iam::...:role/ecsTaskExecution",
  "taskRoleArn":      "arn:aws:iam::...:role/ordersTaskRole",
  "containerDefinitions": [{
    "name": "app",
    "image": "1234.dkr.ecr.us-east-1.amazonaws.com/orders:1.0.0",
    "portMappings": [{"containerPort": 8000}],
    "environment": [{"name": "ENV", "value": "prod"}],
    "secrets": [
      {"name": "DB_PASSWORD", "valueFrom": "arn:aws:secretsmanager:...:secret:db-password"}
    ],
    "healthCheck": {
      "command": ["CMD-SHELL", "curl -f http://localhost:8000/health || exit 1"],
      "interval": 30, "timeout": 5, "retries": 3, "startPeriod": 30
    },
    "logConfiguration": {
      "logDriver": "awslogs",
      "options": {
        "awslogs-group": "/ecs/orders",
        "awslogs-region": "us-east-1",
        "awslogs-stream-prefix": "orders"
      }
    }
  }]
}

executionRoleArn vs taskRoleArn

  • Execution role — used by ECS itself to pull the image (ECR auth), fetch secrets, write logs to CloudWatch. Boring, granted once.
  • Task role — used by your application code inside the container (boto3 calls AWS APIs as this role). This is your app’s IAM identity. Equivalent to IRSA on EKS.
python
# Inside the container — boto3 picks up the task role automatically
import boto3
s3 = boto3.client("s3")
# uses task role
s3.put_object(Bucket="...", Key="...", Body=...)

No access keys in env, no instance profile lookups, no static creds. This is the right pattern.

Service

json
{
  "serviceName": "orders",
  "cluster": "prod",
  "taskDefinition": "orders:5",
  "desiredCount": 3,
  "launchType": "FARGATE",
  "networkConfiguration": {
    "awsvpcConfiguration": {
      "subnets": ["subnet-a", "subnet-b"],
      "securityGroups": ["sg-orders"],
      "assignPublicIp": "DISABLED"
    }
  },
  "loadBalancers": [{
    "targetGroupArn": "arn:aws:elasticloadbalancing:...:targetgroup/orders/...",
    "containerName": "orders",
    "containerPort": 8000
  }],
  "deploymentConfiguration": {
    "maximumPercent": 200,
    "minimumHealthyPercent": 100,
    "deploymentCircuitBreaker": {"enable": true, "rollback": true}
  }
}

Service keeps desiredCount tasks running; rolling updates respect maximumPercent / minimumHealthyPercent. Circuit breaker auto-rolls-back failed deploys. Use it.

Service Discovery

Two patterns:

Via ALB

Service registers tasks as targets in a Target Group. Clients hit the ALB; ALB routes to a healthy task.

text
client → ALB → target group → task IP

AWS Cloud Map (private DNS)

Service creates a DNS record orders.prod.local that resolves to task IPs. For pure service-to-service east-west without ALB hop.

python
# In another service in the same cluster
import httpx
r = httpx.get("http://orders.prod.local/v1/orders")

Autoscaling

Service autoscaling targets:

  • CPU / memory utilization.
  • ALB request count per target.
  • SQS queue depth (custom metric).
  • Any CloudWatch metric via target tracking.
bash
aws application-autoscaling register-scalable-target \
  --service-namespace ecs \
  --resource-id service/prod/orders \
  --scalable-dimension ecs:service:DesiredCount \
  --min-capacity 3 --max-capacity 50

aws application-autoscaling put-scaling-policy \
  --service-namespace ecs \
  --resource-id service/prod/orders \
  --scalable-dimension ecs:service:DesiredCount \
  --policy-name cpu70 \
  --policy-type TargetTrackingScaling \
  --target-tracking-scaling-policy-configuration \
    'TargetValue=70,PredefinedMetricSpecification={PredefinedMetricType=ECSServiceAverageCPUUtilization}'

Celery worker autoscaling on SQS depth

The classic “scale workers on backlog”:

bash
# Custom CloudWatch metric: SQS ApproximateNumberOfMessagesVisible
aws application-autoscaling put-scaling-policy \
  --policy-name sqs-scale \
  --policy-type TargetTrackingScaling \
  --target-tracking-scaling-policy-configuration \
    "TargetValue=50,
     CustomizedMetricSpecification={
       MetricName=ApproximateNumberOfMessagesVisible,
       Namespace=AWS/SQS,
       Statistic=Average,
       Dimensions=[{Name=QueueName,Value=orders-tasks}]
     }"

“Scale so each task has on average 50 queued messages.” Backlog grows → scale up.

Networking modes

Mode Notes
awsvpc Default for Fargate; each task gets its own ENI + IP. Right for security groups per task.
bridge EC2 only; Docker bridge; ports shared on the host.
host EC2 only; container uses host network namespace.
none no networking.

Use awsvpc. The others are EC2-only and constrain you.

Logging

awslogs driver writes container stdout/stderr to a CloudWatch Logs group. Each task gets a stream. Pair with CloudWatch Logs Insights for querying.

For structured / centralized logging, push from container → Firehose → S3 / Elasticsearch via awsfirelens driver:

json
{"logDriver": "awsfirelens", "options": {"Name": "firehose", "delivery_stream": "logs"}}

Deployments

Rolling (default)

Service launches new tasks, drains old; respects minimumHealthyPercent. Use the deployment circuit breaker to auto-rollback on failed rollouts.

Blue/green (via CodeDeploy)

Two target groups; CodeDeploy shifts traffic between them. Linear, canary, or all-at-once. Good for risk-averse rollouts.

yaml
deploymentController:
  type: CODE_DEPLOY

Common gotchas

  • awsvpc and ENI limits per EC2 instance. Each task = an ENI; instance types have ENI caps. Mostly a Fargate-irrelevant issue.
  • Fargate ephemeral storage default is 20 GB. Raise it for tasks that download large files.
  • Cold start of Fargate tasks ~30-60s. Provisioned tasks are warm; if you scale from 3 to 30, the new ones take a minute to come up. Not a hot-path concern usually but matters for response to traffic spikes.
  • CloudWatch Logs costs at scale. Ingestion is the killer; log retention can be cheap. Use awsfirelens to ship to cheaper storage if volume is high.
  • Stopping vs draining tasks. A Stop is abrupt. For graceful shutdown, your container must handle SIGTERM (ECS sends it before SIGKILL); set stopTimeout long enough for in-flight requests to drain.

ECS vs EKS

ECS EKS
Control plane AWS-managed, free AWS-managed Kubernetes ($73/month/cluster)
Mental model AWS-native, simpler Kubernetes, portable
Ecosystem AWS tools Vast K8s ecosystem
Polyglot orgs each team learns ECS most orgs already know K8s
When to pick small-medium AWS shops, simplicity multi-cloud, K8s expertise on staff

For a team committed to AWS without K8s expertise, ECS wins on simplicity. For teams with K8s skill or multi-cloud requirements, EKS.

Interview angle 6

  • “ECS vs EKS — when each?” — ECS for AWS-native simplicity, no K8s control plane cost, faster for small teams. EKS for K8s expertise on staff, multi-cloud portability, broader ecosystem. Both can run on Fargate.
  • “Task role vs execution role?” — execution role: used by ECS to pull image, write logs, fetch secrets. Task role: used by your app code (boto3 calls inside the container). Equivalent to IRSA on EKS.
  • “How do you scale ECS services on queue depth?” — application-autoscaling target tracking with a custom metric (SQS ApproximateNumberOfMessagesVisible). Set a target value (e.g. 50 messages per task) — backlog grows → service adds tasks.
  • “How do you do service discovery?” — ALB target groups (north-south, with HTTP routing), or AWS Cloud Map private DNS (east-west, orders.prod.local). Cloud Map = registry; Service registers task IPs automatically.
  • “What’s the deployment circuit breaker?” — ECS feature that auto-rolls-back a deploy if new tasks fail to become healthy. Without it, a broken image just sits at half-rolled-out forever. Enable it.
  • “How do you secure secrets in ECS?”containerDefinitions.secrets[] pulls from Secrets Manager / SSM Parameter Store at task start (resolved by the execution role) and injects as env vars. Don’t put secrets in plain env vars.

Contents 1