Skip to content
AWS ECS: Running Containers at Scale

AWS ECS: Running Containers at Scale

DodaTech Updated Jun 20, 2026 7 min read

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
  
Prerequisites: Basic Docker knowledge and AWS account. Familiarity with CI/CD concepts helps.

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

AspectFargateEC2
Server managementNone (serverless)You manage EC2 instances
PricingPer-task (CPU + memory)Per-instance (reserved/spot)
ScalingAdd/remove tasks instantlyAdd/remove instances
Best forVariable workloads, small tasksSteady state, GPU, large tasks
Cost optimizationHarder (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 management

Autoscaling

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-deployment

Secrets 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 permission

IAM Roles

RolePurposeRequired Permissions
Task Execution RoleECS pulls images, sends logsECR pull, CloudWatch logs, SSM get params
Task RoleContainer accesses AWS servicesWhatever the app needs (S3, DynamoDB, etc.)

Common Mistakes

  1. No health check on the load balancer: Without a health check path, the ALB sends traffic to unhealthy containers. Always define a /health endpoint and configure the target group health check.

  2. 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.

  3. Hardcoding secrets in task definitions: Task definitions can be viewed by anyone with DescribeTaskDefinition permission. Use SSM Parameter Store or Secrets Manager with IAM controls.

  4. 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.

  5. 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

  1. 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.

  2. 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.

  3. 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.

  4. 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

Is ECS cheaper than EKS?
: For small workloads (1-10 tasks), ECS is simpler and often cheaper. For large-scale deployments (50+ services), EKS with Kubernetes can be more cost-effective due to pod packing efficiency.
Can ECS run Windows containers?
: Yes. ECS supports Windows containers on the EC2 launch type. Fargate does not support Windows.
How does ECS handle task placement?
: ECS uses strategies like binpack (most efficient packing), spread (distribute across AZs), and random. You can also use custom attributes for GPU or instance-type placement.
What is the maximum task size for Fargate?
: 16 vCPU and 120GB memory per task. For larger workloads, use EC2 launch type or EKS.
How do I monitor ECS tasks?
: Use CloudWatch Container Insights for CPU, memory, network, and disk metrics. Enable awslogs driver for application logs. Use Prometheus via the ECS exporter.

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

TopicDescription
Amazon EKS
Kubernetes on AWS
Serverless Framework
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