ECS (Elastic Container Service) is AWS's managed Docker container orchestration. ECR (Elastic Container Registry) is the private Docker registry. Together they let you run containers without managing Kubernetes.
Real-World: Your Node.js microservices run in Docker containers. ECR stores the images. ECS runs them, auto-scales them, and health-checks them. No Kubernetes expertise required.
ECS Cluster
├── Service: checkout-service (maintains 3 running tasks)
│ ├── Task: checkout-task-abc (running: container checkout:v2)
│ ├── Task: checkout-task-def (running: container checkout:v2)
│ └── Task: checkout-task-ghi (running: container checkout:v2)
└── Service: payment-service (maintains 2 running tasks)
├── Task: payment-task-xyz
└── Task: payment-task-uvw
- Cluster: logical grouping of compute resources
- Task Definition: blueprint for a container (image, CPU, memory, ports, env vars, IAM role)
- Task: running instance of a task definition
- Service: maintains N running tasks, handles health checks and restarts
| Feature | EC2 Launch Type | Fargate |
|---|---|---|
| Infrastructure | You manage EC2 instances | AWS manages infrastructure |
| Pricing | Pay for EC2 hours (even when idle) | Pay per vCPU + memory per second |
| Control | Full OS access, custom AMIs | No OS access |
| Scaling | Must scale EC2 capacity + containers | Just scale containers |
| Use when | Cost optimization at scale, GPU, specific AMIs | Simplicity, small teams, variable load |
Rule: Start with Fargate (simpler). Move to EC2 if cost becomes significant at large scale.
{
"family": "checkout-service",
"networkMode": "awsvpc", // Required for Fargate
"requiresCompatibilities": ["FARGATE"],
"cpu": "256", // 0.25 vCPU
"memory": "512", // 512MB
"executionRoleArn": "arn:aws:iam::123:role/ecsTaskExecutionRole",
"taskRoleArn": "arn:aws:iam::123:role/checkoutTaskRole",
"containerDefinitions": [
{
"name": "checkout",
"image": "123456789.dkr.ecr.us-east-1.amazonaws.com/checkout:v2.1",
"portMappings": [{"containerPort": 8080}],
"environment": [
{"name": "NODE_ENV", "value": "production"}
],
"secrets": [
{
"name": "DB_PASSWORD",
"valueFrom": "arn:aws:secretsmanager:us-east-1:123:secret:prod/db"
}
],
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/ecs/checkout",
"awslogs-region": "us-east-1",
"awslogs-stream-prefix": "checkout"
}
},
"healthCheck": {
"command": ["CMD-SHELL", "curl -f http://localhost:8080/health || exit 1"],
"interval": 30,
"timeout": 5,
"retries": 3
}
}
]
}| Role | Purpose |
|---|---|
| Execution Role | ECS agent permissions — pull ECR image, write CloudWatch logs, get Secrets Manager secrets |
| Task Role | Application permissions — what your code can call (DynamoDB, S3, SQS, etc.) |
Execution Role → needs:
ecr:GetAuthorizationToken
ecr:BatchGetImage
logs:CreateLogStream
logs:PutLogEvents
secretsmanager:GetSecretValue (if using secrets)
Task Role → needs whatever your app does:
dynamodb:Query
s3:GetObject
sqs:SendMessage
| Mode | Description | Use with |
|---|---|---|
awsvpc |
Each task gets its own ENI + IP | Fargate (required), EC2 (recommended) |
bridge |
Docker bridge network (NAT) | EC2 only, legacy |
host |
Container uses EC2's network interface | EC2 only, high performance |
none |
No network | EC2 only, isolated tasks |
Fargate always uses awsvpc — each task has its own private IP in your VPC.
Scale tasks based on metrics:
Scaling Policy: Target Tracking
Metric: ECS Service Average CPU Utilization = 70%
Min Tasks: 2
Max Tasks: 20
→ ECS automatically adjusts task count to maintain 70% CPU
Scale-out trigger: CPU > 70% for 3 minutes → add tasks Scale-in trigger: CPU < 70% for 15 minutes → remove tasks
For EC2 launch type: also need to scale the EC2 instances (use Capacity Provider + ASG).
How do services find each other?
checkout-service → calls payment-service.prod.local:8080
Route 53 Auto Naming Service maintains DNS:
payment-service.prod.local → [10.0.1.5, 10.0.1.6, 10.0.1.7]
Updates automatically when tasks start/stop
Internet → ALB → Target Group → ECS Service Tasks
For awsvpc mode: each task registers directly with ALB target group (different port fine because different IP).
For bridge mode: ALB uses dynamic port mapping (host port randomized).
Current (Blue): Service running v1 tasks behind ALB
New (Green): CodeDeploy creates new task set with v2
ALB test traffic on port 8080 → validate green
Shift production traffic → green becomes production
Terminate blue
All configured in CodeDeploy appspec.yaml:
version: 0.0
Resources:
- TargetService:
Type: AWS::ECS::Service
Properties:
TaskDefinition: "arn:aws:ecs:us-east-1:123:task-definition/checkout:5"
LoadBalancerInfo:
ContainerName: checkout
ContainerPort: 8080# Authenticate Docker to ECR
aws ecr get-login-password --region us-east-1 | \
docker login --username AWS --password-stdin \
123456789.dkr.ecr.us-east-1.amazonaws.com
# Create repository
aws ecr create-repository --repository-name checkout
# Tag and push image
docker build -t checkout:v2 .
docker tag checkout:v2 123456789.dkr.ecr.us-east-1.amazonaws.com/checkout:v2
docker push 123456789.dkr.ecr.us-east-1.amazonaws.com/checkout:v2
# Pull image
docker pull 123456789.dkr.ecr.us-east-1.amazonaws.com/checkout:v2{
"rules": [
{
"rulePriority": 1,
"description": "Keep only last 10 production images",
"selection": {
"tagStatus": "tagged",
"tagPrefixList": ["prod"],
"countType": "imageCountMoreThan",
"countNumber": 10
},
"action": {"type": "expire"}
},
{
"rulePriority": 2,
"description": "Delete untagged images after 1 day",
"selection": {
"tagStatus": "untagged",
"countType": "sinceImagePushed",
"countUnit": "days",
"countNumber": 1
},
"action": {"type": "expire"}
}
]
}# Enable scan on push
aws ecr put-image-scanning-configuration \
--repository-name checkout \
--image-scanning-configuration scanOnPush=trueScans for CVEs (Common Vulnerabilities and Exposures). Results in Console/EventBridge.
// ECR Repository Policy (Account A)
{
"Effect": "Allow",
"Principal": {"AWS": "arn:aws:iam::ACCOUNT_B:root"},
"Action": [
"ecr:GetDownloadUrlForLayer",
"ecr:BatchGetImage"
]
}AWS manages the Kubernetes control plane. You manage (or Fargate manages) worker nodes.
When to use EKS over ECS:
- Team already knows Kubernetes
- Need Kubernetes ecosystem (Helm, Istio, operators)
- Multi-cloud portability
- Advanced networking (service mesh)
EKS Fargate: Kubernetes pods run on Fargate (no nodes to manage).
EKS Cluster
├── Control plane (managed by AWS)
└── Worker nodes
├── Option A: EC2 managed node groups
└── Option B: Fargate profiles (pods → Fargate automatically)
| Practice | Reason |
|---|---|
| Use Fargate for new workloads | No infrastructure management |
| Separate Execution Role from Task Role | Least privilege |
| Use ECR lifecycle policies | Prevent image storage explosion |
| Enable container health checks | ECS replaces unhealthy containers |
| Use awsvpc networking | Each task gets own IP, simpler security groups |
| Use Secrets Manager for secrets in task def | Secure, rotatable |
| Enable ECR image scanning | Catch vulnerabilities before deploy |
| Pin image tags (use SHA digest) | Avoid latest tag in production |
| Anti-Pattern | Impact | Fix |
|---|---|---|
| Running as root inside container | Security risk | Use USER directive in Dockerfile |
Using latest tag in production |
Unpredictable deploys | Pin to specific version tag or SHA |
| Not setting resource limits | Container uses all host resources | Set CPU and memory limits in task definition |
| Storing secrets in environment variables (hardcoded) | Visible in task definition | Use secrets with Secrets Manager ARN |
| No health check | Unhealthy container keeps receiving traffic | Add healthCheck to container definition |
- Execution Role = ECS agent pulling image/logs. Task Role = application code calling AWS APIs.
- Fargate requires
awsvpcnetwork mode — no choice. - ECS on EC2 requires ECS Agent running on each instance.
- ECR authentication token expires after 12 hours — refresh before long-running builds.
- ECS + CodeDeploy Blue/Green = AppSpec + two target groups in ALB.
- ECS Service Auto Scaling is separate from EC2 ASG (if using EC2 launch type, need both).
- ECS Exec: SSH into running containers for debugging:
aws ecs execute-command. - Capacity Provider: links ECS to an ASG, manages EC2 scaling automatically.
Q: Container needs to read from S3 — how to give it permissions? → Add an IAM Task Role to the task definition with S3 permissions.
Q: Secrets Manager secret in ECS container environment?
→ Use secrets array in task definition with valueFrom pointing to Secrets Manager ARN. Requires permissions in Execution Role.
Q: Zero-downtime deployment for ECS service? → CodeDeploy Blue/Green with ECS or configure rolling update in ECS service.
Q: ECS task failing health checks — where to look? → Check CloudWatch Logs (if awslogs configured), or use ECS Exec for interactive debugging.
Q: Run a container once (batch job) not a long-running service? → Use ECS Task (not Service) — invoke as a standalone task, perhaps triggered by EventBridge.