Skip to content
Available for work
Adam
←  All posts

Running My Containers in the Cloud - AWS ECS and Fargate

Where I am right now in my AWS journey: taking the Docker images I built earlier and running them on ECS. Task definitions, services, Fargate vs EC2, and wiring up a load balancer.

5 min readaws · ecs · containers

TL;DR

  • ECS (Elastic Container Service) is AWS's container orchestrator - it runs and keeps your containers alive.
  • A task definition is the blueprint (image, CPU/memory, env, IAM role); a task is a running instance; a service keeps N tasks running.
  • Fargate is serverless containers - no EC2 hosts to patch or scale.
  • An Application Load Balancer + target group routes traffic to healthy tasks.

Where I am right now

This is current - I'm actively learning ECS this week. I have reproducible Docker images from earlier in the journey and a CI/CD pipeline that builds them. The missing piece was running them somewhere that isn't a server I SSH into. ECS is that somewhere, and it ties together almost everything I've learned: containers, statelessness, load balancing, and IAM.

The ECS vocabulary that confused me at first

ECS has four nested concepts, and getting them straight made everything else click:

  1. Cluster - a logical grouping of compute where your containers run.
  2. Task definition - the blueprint. Which image, how much CPU/memory, env vars, ports, logging, and which IAM roles. Think of it like a docker run spec, versioned.
  3. Task - a running instance of a task definition (one or more containers scheduled together).
  4. Service - the supervisor. It maintains a desired count of tasks, replaces unhealthy ones, and integrates with a load balancer for rolling deploys.

So: the service runs N tasks from a task definition inside a cluster. Once I had that sentence memorized, the console stopped being a maze.

A task definition, concretely

{
  "family": "taskflow-api",
  "networkMode": "awsvpc",
  "requiresCompatibilities": ["FARGATE"],
  "cpu": "512",
  "memory": "1024",
  "executionRoleArn": "arn:aws:iam::...:role/ecsTaskExecutionRole",
  "taskRoleArn": "arn:aws:iam::...:role/taskflowAppRole",
  "containerDefinitions": [{
    "name": "api",
    "image": "<account>.dkr.ecr.<region>.amazonaws.com/taskflow:<git-sha>",
    "portMappings": [{ "containerPort": 3000 }],
    "logConfiguration": {
      "logDriver": "awslogs",
      "options": {
        "awslogs-group": "/ecs/taskflow-api",
        "awslogs-region": "us-east-1",
        "awslogs-stream-prefix": "api"
      }
    }
  }]
}

Two IAM roles tripped me up until I separated their jobs:

  • Execution role - permissions ECS itself needs to start the task: pull the image from ECR, write logs to CloudWatch.
  • Task role - permissions my application code needs at runtime: e.g. read/write the S3 bucket from my last post. This is how my container gets AWS credentials without me ever putting access keys in env vars - the SDK picks them up automatically from the task role. That clean separation is one of my favorite things I've learned in AWS so far.

Note the image is tagged by git SHA - exactly the convention from my CI/CD post, now paying off.

Fargate vs. EC2 launch types

ECS can run tasks two ways:

  • EC2 launch type - you provision and manage a fleet of EC2 instances; ECS packs tasks onto them. More control, cheaper at steady high scale, but you patch and scale the hosts.
  • Fargate - serverless. You specify CPU/memory per task and AWS runs it; there are no instances to see or manage.

I chose Fargate. As a solo developer, not babysitting a host fleet is worth the premium, and it fits the stateless model - tasks are disposable, state lives in Postgres/Redis/S3.

Networking: awsvpc mode

In awsvpc mode, each task gets its own Elastic Network Interface with its own private IP inside my VPC - like its own little machine on the network. That makes security groups apply per task, which is exactly the isolation I want. (The VPC/subnet/security-group details are deep enough that they're my next post.)

Wiring up the load balancer

Tasks are ephemeral - they come and go with their own IPs - so I can't hardcode where they are. An Application Load Balancer (ALB) solves this:

  • The ECS service registers each task into a target group.
  • The ALB health-checks /health; only healthy tasks receive traffic.
  • During a deploy, the service spins up new tasks, waits for them to pass health checks, registers them, then drains the old ones - a rolling deploy with zero downtime.

This is the load-balancing theory from my scaling post, now concrete: the ALB is the stable front door, tasks scale behind it, and Service Auto Scaling adds/removes tasks based on CPU or request count.

What's clicking and what's still hard

Clicking: how cleanly ECS composes everything - my Docker image, IAM least-privilege via the task role, the load balancer, and rolling deploys all fit together.

Still hard: the networking underneath. Public vs. private subnets, why my task couldn't reach the internet to pull the image at first (it needed a NAT gateway / proper subnet routing), and security-group rules. That confusion is exactly what I'm tackling next.

Takeaways

  • Cluster → service → task → task definition is the whole mental model.
  • Two roles, two jobs: execution role to start the task, task role for the app's AWS access.
  • Fargate trades cost for not managing hosts - usually worth it early.
  • The ALB + health checks are what make ephemeral tasks safe to deploy continuously.

Next

ECS made it obvious I don't actually understand the network my containers live in. Next I'm going one layer down: AWS IAM, VPCs, subnets, and security groups - the security and networking foundation everything else sits on.