ECS Fargate — Where, How, and Internals

Track how many of the knowledge checks below you clear as you go:

0/0 checks

What is ECS Fargate?

ECS (Elastic Container Service) is AWS's container orchestrator. Fargate is its serverless compute engine — you define the container, AWS manages the underlying EC2 instance. You never SSH into a node.

graph TD
    subgraph "ECS on EC2 (you manage nodes)"
        EC2["EC2 instances<br/>you provision, patch, scale"]
        ECS_AGT["ECS Agent<br/>(runs on EC2)"]
        TASK_EC2["ECS Task<br/>(your container)"]
        EC2 --> ECS_AGT --> TASK_EC2
    end

    subgraph "ECS on Fargate (AWS manages everything)"
        FG["Fargate Runtime<br/>AWS-managed microVM"]
        TASK_FG["ECS Task<br/>(your container)"]
        FG --> TASK_FG
    end

Fargate isolation: each Fargate task runs in its own Firecracker microVM — a lightweight KVM-based VM (~125ms boot). This gives hardware-level isolation (unlike shared-kernel containers on EC2).

Why does Fargate give hardware-level isolation between tasks, when a container is normally just a process sharing a host kernel?


Core Concepts

graph LR
    CLUSTER["ECS Cluster<br/>logical grouping"] --> SERVICE
    SERVICE["ECS Service<br/>desired count<br/>rolling updates<br/>ALB integration"] --> TASK
    TASK_DEF["Task Definition<br/>image · CPU · memory<br/>env · IAM role · logging"] --> TASK
    TASK["ECS Task<br/>= running container group<br/>like a K8s Pod"]
Concept K8s equivalent Description
Task Definition Pod spec Blueprint: image, CPU, memory, env, ports, IAM role
Task Pod Running instance of a Task Definition
Service Deployment Maintains desired task count, handles rolling updates
Cluster Namespace/cluster Logical grouping of services

Is an ECS Task the same thing as a Task Definition?


Task Definition

{
  "family": "api-service",
  "networkMode": "awsvpc",
  "requiresCompatibilities": ["FARGATE"],
  "cpu": "512",
  "memory": "1024",
  "executionRoleArn": "arn:aws:iam::123:role/ecsTaskExecutionRole",
  "taskRoleArn": "arn:aws:iam::123:role/ecsTaskRole",
  "containerDefinitions": [{
    "name": "api",
    "image": "123456.dkr.ecr.us-east-1.amazonaws.com/api:v1.2",
    "portMappings": [{"containerPort": 8080, "protocol": "tcp"}],
    "environment": [
      {"name": "ENV", "value": "production"}
    ],
    "secrets": [
      {"name": "DB_PASSWORD", "valueFrom": "arn:aws:secretsmanager:us-east-1:123:secret:db-pass"}
    ],
    "logConfiguration": {
      "logDriver": "awslogs",
      "options": {
        "awslogs-group": "/ecs/api-service",
        "awslogs-region": "us-east-1",
        "awslogs-stream-prefix": "ecs"
      }
    },
    "healthCheck": {
      "command": ["CMD-SHELL", "curl -f http://localhost:8080/health || exit 1"],
      "interval": 30,
      "timeout": 5,
      "retries": 3
    }
  }]
}

Two IAM roles:

The ECS agent needs this — not your application code. It's what lets the agent pull the container image from ECR and push logs to CloudWatch on your behalf, before your app has even started.
Your application code uses this at runtime to call AWS APIs — S3, DynamoDB, etc. This is the role your container's own SDK calls assume, separate from anything the ECS agent does.

A container fails with CannotPullContainerError. Is that a problem with the executionRole or the taskRole?


Networking: awsvpc Mode

Fargate always uses awsvpc network mode. Each task gets its own ENI and private IP — identical to an EC2 instance from the VPC's perspective.

graph LR
    ALB["ALB<br/>:443"] -->|target group| ENI1["ENI: 10.0.1.25<br/>Task 1"]
    ALB -->|target group| ENI2["ENI: 10.0.1.26<br/>Task 2"]
    ENI1 --> CONT1["Container :8080"]
    ENI2 --> CONT2["Container :8080"]

Security groups attach directly to the task ENI — not to a node. This means per-task security group rules, same as EC2.

Do all Fargate tasks in a service share one security group at the node level, the way EC2 instances behind an ASG might?


ECS Service with ALB

# Terraform: ECS Service with ALB
resource "aws_ecs_service" "api" {
  name            = "api"
  cluster         = aws_ecs_cluster.main.id
  task_definition = aws_ecs_task_definition.api.arn
  desired_count   = 3
  launch_type     = "FARGATE"

  network_configuration {
    subnets          = var.private_subnet_ids
    security_groups  = [aws_security_group.task.id]
    assign_public_ip = false   # private subnets, reach internet via NAT GW
  }

  load_balancer {
    target_group_arn = aws_lb_target_group.api.arn
    container_name   = "api"
    container_port   = 8080
  }

  deployment_controller {
    type = "ECS"   # rolling update. Use CODE_DEPLOY for blue/green
  }
}

assign_public_ip = false with tasks in private subnets — how do those tasks reach the internet at all (e.g. to pull an image or call an external API)?


Rolling Update / Deployment

sequenceDiagram
    participant SVC as ECS Service
    participant OLD as Old Tasks (v1)
    participant NEW as New Tasks (v2)
    participant ALB as ALB Target Group

    SVC->>NEW: Start new task (v2)
    NEW-->>ALB: Register when health check passes
    ALB->>NEW: Route % of traffic
    SVC->>OLD: Deregister from ALB
    ALB->>OLD: Drain connections (deregisterDelay=30s)
    SVC->>OLD: Stop old task
    Note over SVC: Repeat for each task

Key config:

  • minimumHealthyPercent: 100 — never go below desired count during deploy (needs extra capacity)
  • maximumPercent: 200 — can run up to 2× desired count during deploy
  • deregistrationDelay on target group — ALB waits N seconds to drain in-flight requests

Same sequence, one action at a time:

1. Start new task (v2). The service launches a new task running the new task definition revision, alongside the existing v1 tasks.
2. Health check passes → register. Once the new task's health check succeeds, it registers with the ALB target group and starts receiving a share of traffic.
3. Deregister an old task. The service deregisters one old (v1) task from the ALB target group.
4. Drain connections. The ALB waits deregistrationDelay seconds (e.g. 30s) for in-flight requests to that old task to finish before treating it as fully removed.
5. Stop the old task. The old task is stopped. This start → register → deregister → drain → stop cycle repeats, one task at a time, until every task is on v2.

A service has desired_count = 3, minimumHealthyPercent: 100, and maximumPercent: 200. During a rolling deploy, what's the minimum and maximum number of tasks running at once?


ECS Service Auto Scaling

graph LR
    CW["CloudWatch Metric<br/>CPUUtilization / RequestCount"] --> ASP["Application Auto Scaling<br/>Policy (Target Tracking)"]
    ASP -->|scale out| SVC["ECS Service<br/>desired count ++"]
    ASP -->|scale in| SVC
# Target tracking: keep average CPU at 70%
aws application-autoscaling put-scaling-policy \
  --service-namespace ecs \
  --resource-id service/my-cluster/api \
  --scalable-dimension ecs:service:DesiredCount \
  --policy-type TargetTrackingScaling \
  --target-tracking-scaling-policy-configuration '{
    "TargetValue": 70.0,
    "PredefinedMetricSpecification": {
      "PredefinedMetricType": "ECSServiceAverageCPUUtilization"
    },
    "ScaleInCooldown": 300,
    "ScaleOutCooldown": 60
  }'

How that policy actually reacts over time:

1. Metric crosses target. Average CPUUtilization across the service climbs above the 70% target set in the policy.
2. Application Auto Scaling evaluates. The TargetTrackingScaling policy computes how much to change desired count by to bring the metric back toward target.
3. Scale out — desired count ++. The ECS Service's desired count is raised; new Fargate tasks are scheduled to bring average CPU back down toward 70%.
4. ScaleOutCooldown (60s). No further scale-out action fires until this cooldown passes, so the policy doesn't keep piling on tasks faster than the last batch can take effect.
5. Metric drops back — scale in. Once the new tasks absorb the load and CPU falls back down, the same policy can trigger a scale-in — desired count decreases, gated by the longer ScaleInCooldown (300s) before it's allowed to scale in again.

This policy sets ScaleOutCooldown to 60s and ScaleInCooldown to 300s. Which action can happen again sooner after the last one — adding capacity or removing it?


Debugging Fargate Tasks

# List running tasks
aws ecs list-tasks --cluster my-cluster --service-name api

# Describe a task (get IP, status, stopped reason)
aws ecs describe-tasks --cluster my-cluster --tasks <task-arn>
# Look for: lastStatus, stoppedReason, containers[].exitCode

# Logs (CloudWatch)
aws logs tail /ecs/api-service --follow

# ECS Exec (SSM-based shell into running task — no SSH needed)
aws ecs execute-command \
  --cluster my-cluster \
  --task <task-arn> \
  --container api \
  --interactive \
  --command "/bin/sh"
# Requires: SSM agent in image + executionRole has ssmmessages permissions

# Common stopped reasons
# "Essential container exited"  → check container exit code
# "CannotPullContainerError"    → ECR permissions or network issue
# "OutOfMemoryError"            → increase memory in task definition

Same three, click through:

Check the container's exit code. The essential container in the task definition stopped running — since it's marked essential, that takes the whole task down with it.
ECR permissions or a network issue. The image pull — done by the ECS agent using the executionRole — failed before the container ever started.
Increase memory in the task definition. The container exceeded the memory limit it was given and got killed.

ECS vs EKS — When to Use

Factor ECS Fargate EKS
Team K8s expertise Not needed Required
Operational overhead Minimal (no nodes) Higher (node groups, upgrades)
Ecosystem AWS-native Full K8s ecosystem
Custom scheduling No Yes
DaemonSets No (Fargate) Yes (EC2 nodes)
Cost at small scale Lower (no control plane fee) +$73/month control plane
Cost at large scale Higher per vCPU than EC2 EC2 Spot can be 90% cheaper
Best for Small-medium teams, AWS-only, quick start Platform teams, complex microservices

You need to run DaemonSets and want custom pod scheduling control. Does ECS Fargate or EKS fit?