AWS ECS: Running Containers at Scale
Amazon ECS (Elastic Container Service) is a fully managed container orchestration service that lets you run, stop, and scale containers on AWS without managing a control plane.
What You’ll Learn
- ECS concepts: clusters, task definitions, services
- Fargate vs EC2 launch types — when to use each
- Autoscaling, service discovery, and CI/CD pipelines
- Secrets management, CloudWatch logging, and IAM roles
Why ECS Matters
Running containers on raw EC2 means managing the Docker daemon, container lifecycle, networking, and scaling yourself. ECS handles all of that — you define what containers to run, and ECS decides where and how to run them. With Fargate, you don’t even manage the underlying servers. DodaTech runs Durga Antivirus Pro’s backend API on ECS with Fargate — each API service runs in its own task, scales based on request count, and costs nothing when idle.
flowchart LR
A[Docker & AWS Basics] --> B[Amazon ECS]
B --> C[Clusters]
B --> D[Task Definitions]
B --> E[Services]
B --> F[Autoscaling]
C --> G[Fargate / EC2]
D --> H[Container Definitions]
E --> I[Desired Count / Scheduling]
style B fill:#f90,color:#fff
ECS Concepts
Task Definition
A task definition is the blueprint for your container — like a docker-compose.yml for ECS:
{
"family": "dodatech-api",
"networkMode": "awsvpc",
"requiresCompatibilities": ["FARGATE"],
"cpu": "512",
"memory": "1024",
"executionRoleArn": "arn:aws:iam::123456789012:role/ecsTaskExecutionRole",
"containerDefinitions": [
{
"name": "api",
"image": "123456789012.dkr.ecr.us-east-1.amazonaws.com/dodatech-api:latest",
"essential": true,
"portMappings": [{
"containerPort": 8080,
"protocol": "tcp"
}],
"environment": [
{"name": "NODE_ENV", "value": "production"},
{"name": "DB_HOST", "value": "database.internal"}
],
"secrets": [
{"name": "DB_PASSWORD", "valueFrom": "arn:aws:ssm:us-east-1:123456789012:parameter/prod/db-password"}
],
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/ecs/dodatech-api",
"awslogs-region": "us-east-1",
"awslogs-stream-prefix": "api"
}
}
}
]
}# Register task definition via CLI
aws ecs register-task-definition --cli-input-json file://task-def.json
# Output:
# {
# "taskDefinition": {
# "taskDefinitionArn": "arn:aws:ecs:us-east-1:123456789012:task-definition/dodatech-api:1",
# "family": "dodatech-api",
# "revision": 1
# }
# }Service
A service ensures a desired number of tasks are always running:
# ecs-service.tf
resource "aws_ecs_service" "api" {
name = "dodatech-api"
cluster = aws_ecs_cluster.main.id
task_definition = aws_ecs_task_definition.api.arn
desired_count = 3
launch_type = "FARGATE"
network_configuration {
subnets = aws_subnets.private[*].id
security_groups = [aws_security_group.ecs_tasks.id]
}
load_balancer {
target_group_arn = aws_lb_target_group.api.arn
container_name = "api"
container_port = 8080
}
depends_on = [aws_lb_listener.api]
}Fargate vs EC2
| Aspect | Fargate | EC2 |
|---|---|---|
| Server management | None (serverless) | You manage EC2 instances |
| Pricing | Per-task (CPU + memory) | Per-instance (reserved/spot) |
| Scaling | Add/remove tasks instantly | Add/remove instances |
| Best for | Variable workloads, small tasks | Steady state, GPU, large tasks |
| Cost optimization | Harder (premium for serverless) | Easier (reserved/spot instances) |
# fargate_vs_ec2_cost.py
def compare_cost(task_cpu=512, task_mem=1024, task_count=5, hours=730):
# Fargate pricing (us-east-1)
fargate_cpu = 0.04048 # per vCPU per hour
fargate_mem = 0.004445 # per GB per hour
fargate_per_task = (task_cpu/1024 * fargate_cpu) + (task_mem/1024 * fargate_mem)
fargate_monthly = fargate_per_task * task_count * hours
# EC2 (m5.large — 2 vCPU, 8GB, can run ~4 tasks)
ec2_instance_cost = 0.096 # per hour (3yr reserved)
instances_needed = -(-task_count // 4) # ceil division
ec2_monthly = ec2_instance_cost * instances_needed * hours
print(f"Fargate: {task_count} tasks → ${fargate_monthly:.0f}/mo")
print(f"EC2: {instances_needed} instances → ${ec2_monthly:.0f}/mo")
saving = (1 - ec2_monthly / fargate_monthly) * 100
print(f"EC2 saves ~{saving:.0f}% but requires instance management")
compare_cost()
# Output:
# Fargate: 5 tasks → $354/mo
# EC2: 2 instances → $140/mo
# EC2 saves ~60% but requires instance managementAutoscaling
Scale based on CPU, memory, or custom metrics:
# ecs-autoscaling.tf
resource "aws_appautoscaling_target" "api" {
service_namespace = "ecs"
resource_id = "service/${aws_ecs_cluster.main.name}/${aws_ecs_service.api.name}"
scalable_dimension = "ecs:service:DesiredCount"
min_capacity = 2
max_capacity = 20
}
resource "aws_appautoscaling_policy" "api_cpu" {
name = "api-cpu-autoscale"
service_namespace = "ecs"
resource_id = aws_appautoscaling_target.api.resource_id
scalable_dimension = aws_appautoscaling_target.api.scalable_dimension
step_scaling_policy_configuration {
adjustment_type = "ChangeInCapacity"
cooldown = 60
metric_aggregation_type = "Average"
step_adjustment {
scaling_adjustment = 2
metric_interval_lower_bound = 0
metric_interval_upper_bound = 80
}
step_adjustment {
scaling_adjustment = -1
metric_interval_lower_bound = -80
metric_interval_upper_bound = 0
}
}
}CI/CD with ECS
# .github/workflows/deploy-ecs.yml
name: Deploy to ECS
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-east-1
- name: Login to ECR
uses: aws-actions/amazon-ecr-login@v1
- name: Build and push image
run: |
docker build -t dodatech-api .
docker tag dodatech-api:latest ${{ secrets.ECR_REPO }}:latest
docker push ${{ secrets.ECR_REPO }}:latest
- name: Deploy to ECS
run: |
aws ecs update-service \
--cluster dodatech-prod \
--service dodatech-api \
--force-new-deploymentSecrets Management
Never hardcode secrets in task definitions:
# Store in SSM Parameter Store
aws ssm put-parameter \
--name "/prod/db-password" \
--value "SuperSecret123!" \
--type "SecureString"
# Reference in task definition:
# "secrets": [
# {"name": "DB_PASSWORD", "valueFrom": "arn:aws:ssm:.../prod/db-password"}
# ]
# IAM execution role must have: ssm:GetParameters permissionIAM Roles
| Role | Purpose | Required Permissions |
|---|---|---|
| Task Execution Role | ECS pulls images, sends logs | ECR pull, CloudWatch logs, SSM get params |
| Task Role | Container accesses AWS services | Whatever the app needs (S3, DynamoDB, etc.) |
Common Mistakes
No health check on the load balancer: Without a health check path, the ALB sends traffic to unhealthy containers. Always define a
/healthendpoint and configure the target group health check.Over-provisioning memory for Fargate tasks: Fargate charges for allocated memory, not used memory. If your container uses 200MB but you allocate 2GB, you pay for 2GB. Rightsize memory.
Hardcoding secrets in task definitions: Task definitions can be viewed by anyone with
DescribeTaskDefinitionpermission. Use SSM Parameter Store or Secrets Manager with IAM controls.Not setting task-level resource limits: Without CPU/memory limits, one container can starve others on the same instance (EC2 launch type). Always set both hard and soft limits.
Forgetting log group creation: ECS creates log streams only if the CloudWatch log group exists. Create the log group in Terraform first, or ECS will fail to start tasks.
Practice Questions
What is the difference between a task definition and a service? Answer: A task definition is the blueprint (what container to run). A service maintains the desired count of running tasks (how many to run) and handles load balancing and scaling.
When should you use Fargate vs EC2 launch type? Answer: Fargate for variable workloads, small tasks, and when you don’t want to manage servers. EC2 for steady state, GPU workloads, large tasks, and cost optimization with reserved instances.
How does ECS handle service discovery? Answer: ECS integrates with AWS Cloud Map for service discovery. Tasks register with a namespace, and other services resolve them via DNS or API.
What IAM roles does ECS require? Answer: Task execution role (ECS pulls images, sends logs) and task role (container accesses AWS services). Both should follow least privilege.
Challenge
Deploy a containerized API on ECS with Fargate: create a Dockerfile for a Node.js/Python API, push the image to ECR, define a task definition with CloudWatch logging and SSM secrets, create an ECS service behind an ALB with health checks, configure autoscaling based on CPU (min 2, max 10), set up a CI/CD pipeline with GitHub Actions, and test a blue/green deployment.
FAQ
Mini Project: ECS Deploy Script
#!/bin/bash
# deploy-ecs.sh — automated ECS deployment
set -e
CLUSTER="dodatech-prod"
SERVICE="dodatech-api"
REGION="us-east-1"
echo "Building image..."
docker build -t dodatech-api .
echo "Tagging and pushing to ECR..."
ECR_REPO=$(aws ecr describe-repositories --repository-names dodatech-api \
--query 'repositories[0].repositoryUri' --output text)
docker tag dodatech-api:latest ${ECR_REPO}:latest
docker push ${ECR_REPO}:latest
echo "Updating ECS service..."
aws ecs update-service --cluster $CLUSTER --service $SERVICE \
--force-new-deployment --region $REGION
echo "Waiting for stable deployment..."
aws ecs wait services-stable --cluster $CLUSTER --services $SERVICE --region $REGION
echo "Deployment complete!"What’s Next
| Topic | Description |
|---|---|
| Kubernetes on AWS | |
| Serverless applications |
Related topics: Docker, AWS, CI/CD, ECS
What’s Next
Congratulations on completing this ECS tutorial! Here’s where to go from here:
- Practice daily — Deploy a containerized app on ECS Fargate
- Build a project — Set up CI/CD pipeline with ECS blue/green deployments
- Explore related topics — Check out Amazon EKS for Kubernetes
Remember: every expert was once a beginner. Keep coding!
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro