Skip to content

Phase 3 · CLOUD

ECR + ECS/Fargate — containers on AWS (Go Deeper: where Lambda fits)

Day 59 of 90 ~60 min 0/20 in phase Builds on Day 58

By the end of today

  • Push a Docker image to a private Amazon ECR repository
  • Run a container on ECS Fargate from a task definition
  • Reach a Fargate task over the internet, then tear it all down

ECR and Fargate: your own registry plus serverless containers

Section 1 of 5 · ~3 min

You can build images and run them on your laptop. To run them in the cloud without renting and patching servers yourself, AWS gives you two pieces that click together: a place to store images and a way to run them.

Amazon ECR (Elastic Container Registry) is a private Docker registry that lives in your AWS account. It plays Docker Hub’s role — docker push and docker pull — but the repository is yours, in your region, and access is controlled by IAM instead of a Docker Hub login. You authenticate with a short-lived token from aws ecr get-login-password, push your image, and it sits there ready for anything in AWS to pull.

Amazon ECS (Elastic Container Service) is the orchestrator that actually runs the container. Three nouns carry the whole model:

  • A task definition is the recipe: which image, how much CPU and memory, which ports, environment, and the IAM roles the container uses. It is versioned — every change makes a new revision.
  • A task is one running copy of that recipe — the container(s) actually up.
  • A service keeps a desired number of tasks running: if one dies, ECS starts a replacement, and it can register tasks behind a load balancer so traffic spreads across them.

Fargate is the launch type that makes this serverless. With the older EC2 launch type you run and patch a fleet of EC2 instances for your tasks to land on. With Fargate you run none — you declare “256 CPU, 512 MB” in the task definition and AWS finds the capacity, bills you per second the task runs, and you never see the host. No servers to size, patch, or scale.

The production shape is: a service runs N Fargate tasks, and an Application Load Balancer (ALB) sits in front, routing each request to a healthy task through a target group. Scale the service to more tasks and the ALB spreads load automatically.

A local image is pushed to a private ECR repository, an ECS service runs it as a Fargate task, and an Application Load Balancer routes user traffic to the running task. local image docker build ECR repo private registry ECS service Fargate task ALB to users push pull
Build locally, push to ECR, run on Fargate as an ECS service, reach it through an ALB.

Real world: ECR is your private warehouse of sealed shipping containers; Fargate is a fully-staffed loading dock you rent by the minute. You hand over a container and the dock runs it — you never buy the trucks, hire the crew, or maintain the yard. The service is the dock manager who guarantees three containers are always working, replacing any that fail; the ALB is the traffic marshal waving each incoming lorry to a free bay.

Amazon’s own Prime Video team gave a well-known example of choosing between these tools. Their audio/video quality-monitoring service first ran as many small serverless pieces (Step Functions and Lambda); as traffic grew, per-call overhead and cost pushed them to repackage it as a single service on ECS — a public reminder that Lambda suits short, spiky, event-driven work while a long-running container on Fargate fits steady traffic better. Knowing where each fits is today’s real lesson; you can see where Lambda belongs in Go Deeper.

Today you push one image to ECR, run it as a Fargate task, reach it over the internet, and — the step that keeps the bill near zero — tear every piece down.

Hands-On Lab

Section 2 of 5 · ~5 min

Budget about 30 minutes. You’ll drive this from the AWS CLI v2 as your IAM user (not root), in the us-east-1 region, with Docker running locally to build the image. Type every command and read the output before moving on. Account IDs, ARNs, image digests and IPs are unique to your account and run — yours will differ from the samples.

What this costs: Not free, but tiny if you tear down. A 0.25 vCPU / 0.5 GB Fargate task bills only while it runs — roughly $0.017/hour including the public IPv4 address AWS now charges ($0.005/hr) — so 15 minutes is well under a cent. ECR storage is $0.10/GB-month and this image is a few megabytes, so fractions of a cent. Fargate has no always-free tier, and account Free Tier terms vary (legacy accounts get 12-month allowances; accounts created since mid-2025 get credits instead), so don’t rely on “free” — rely on deleting. The real bill comes from walking away: a task left running, plus the ALB from Go Deeper (~$0.0225/hr), quietly adds up. Steps 11–12 stop the task and delete the cluster, repo and security group — finish them.

# 1. Confirm you're your IAM user (NOT root) and set a default region.
aws sts get-caller-identity
export AWS_DEFAULT_REGION=us-east-1
# Output (your Account and Arn will differ):
# {
#     "UserId": "AIDA...EXAMPLE",
#     "Account": "123456789012",
#     "Arn": "arn:aws:iam::123456789012:user/devops-you"
# }
# 2. Create a private ECR repository and capture its URI into a variable.
REPO=$(aws ecr create-repository --repository-name m90-web \
  --query 'repository.repositoryUri' --output text)
echo "$REPO"
# Output (your account ID and region make this URI unique — yours will differ):
# 123456789012.dkr.ecr.us-east-1.amazonaws.com/m90-web
# 3. Build a tiny image locally: nginx serving one custom page.
cat > Dockerfile <<'EOF'
FROM nginx:1.27-alpine
RUN echo "Hello from Fargate - Mission 90 Day 59" > /usr/share/nginx/html/index.html
EOF
docker build -t m90-web:latest .
# Output (image digest and layer IDs will differ):
# => => naming to docker.io/library/m90-web:latest
# 4. Log Docker in to ECR with a short-lived token, tag the image, and push it.
aws ecr get-login-password | docker login --username AWS --password-stdin "${REPO%/*}"
docker tag m90-web:latest "$REPO:latest"
docker push "$REPO:latest"
# Output (Login Succeeded, then each layer pushed; the sha256 digest is yours):
# Login Succeeded
# latest: digest: sha256:0abc123...def size: 1778
# 5. Create the ECS task execution role so Fargate can pull from ECR (skip if it exists).
cat > trust.json <<'EOF'
{ "Version": "2012-10-17",
  "Statement": [ { "Effect": "Allow",
    "Principal": { "Service": "ecs-tasks.amazonaws.com" },
    "Action": "sts:AssumeRole" } ] }
EOF
ROLE=$(aws iam create-role --role-name ecsTaskExecutionRole \
  --assume-role-policy-document file://trust.json \
  --query 'Role.Arn' --output text)
aws iam attach-role-policy --role-name ecsTaskExecutionRole \
  --policy-arn arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
echo "$ROLE"
# Output (your account ID will differ):
# arn:aws:iam::123456789012:role/ecsTaskExecutionRole
# 6. Create an ECS cluster (just a namespace for Fargate tasks — no servers are created).
aws ecs create-cluster --cluster-name m90 \
  --query 'cluster.clusterName' --output text
# Output:
# m90
# 7. Register a Fargate task definition (the recipe): image, CPU/memory, port, exec role.
cat > taskdef.json <<EOF
{ "family": "m90-web",
  "networkMode": "awsvpc",
  "requiresCompatibilities": ["FARGATE"],
  "cpu": "256", "memory": "512",
  "executionRoleArn": "$ROLE",
  "containerDefinitions": [
    { "name": "web", "image": "$REPO:latest", "essential": true,
      "portMappings": [ { "containerPort": 80 } ] } ] }
EOF
aws ecs register-task-definition --cli-input-json file://taskdef.json \
  --query 'taskDefinition.taskDefinitionArn' --output text
# Output (the ":1" is the revision — yours will differ):
# arn:aws:ecs:us-east-1:123456789012:task-definition/m90-web:1
# 8. Find a default public subnet and open port 80 from YOUR IP only.
SUBNET=$(aws ec2 describe-subnets --filters Name=default-for-az,Values=true \
  --query 'Subnets[0].SubnetId' --output text)
MY_IP=$(curl -s https://checkip.amazonaws.com)
SG=$(aws ec2 create-security-group --group-name m90-web-sg \
  --description "HTTP from my IP" --query 'GroupId' --output text)
aws ec2 authorize-security-group-ingress --group-id "$SG" \
  --protocol tcp --port 80 --cidr "${MY_IP}/32"
echo "$SUBNET $SG"
# Output (subnet and security-group IDs are yours):
# subnet-0abc123def456 sg-0def456abc789
# 9. Run ONE Fargate task from the task definition, giving it a public IP.
TASK=$(aws ecs run-task --cluster m90 --task-definition m90-web \
  --launch-type FARGATE \
  --network-configuration "awsvpcConfiguration={subnets=[$SUBNET],securityGroups=[$SG],assignPublicIp=ENABLED}" \
  --query 'tasks[0].taskArn' --output text)
echo "$TASK"
# Output (your task ID will differ):
# arn:aws:ecs:us-east-1:123456789012:task/m90/1a2b3c4d5e6f7890
# 10. Wait until it's running, read its public IP from the network interface, then curl it.
aws ecs wait tasks-running --cluster m90 --tasks "$TASK"
ENI=$(aws ecs describe-tasks --cluster m90 --tasks "$TASK" \
  --query "tasks[0].attachments[0].details[?name=='networkInterfaceId'].value" --output text)
IP=$(aws ec2 describe-network-interfaces --network-interface-ids "$ENI" \
  --query 'NetworkInterfaces[0].Association.PublicIp' --output text)
curl -s "http://$IP"
# Output (public IP yours; the page is the one you baked into the image):
# Hello from Fargate - Mission 90 Day 59
# 11. TEAR DOWN: stop the task, wait for it to stop, then delete the cluster.
aws ecs stop-task --cluster m90 --task "$TASK" \
  --query 'task.desiredStatus' --output text
aws ecs wait tasks-stopped --cluster m90 --tasks "$TASK"
aws ecs delete-cluster --cluster m90 --query 'cluster.status' --output text
# Output:
# STOPPED
# INACTIVE
# 12. Delete the rest so nothing is left billing: security group, ECR repo, local files.
aws ec2 delete-security-group --group-id "$SG"
aws ecr delete-repository --repository-name m90-web --force
rm -f Dockerfile taskdef.json trust.json
echo "cleaned up"
# Output:
# cleaned up

Read the last outputs back: the curl returning your baked-in page in step 10 proved a container you built ran on hardware you never touched or paid to keep idle. INACTIVE in step 11 is the one that matters — it, plus the deleted repo, is the difference between a fraction-of-a-cent lab and a Fargate task quietly billing all week. The execution role is free to keep and reused by every future ECS lab, so leave it.

Common Errors & Fixes

Section 3 of 5 · ~3 min

These three catch almost everyone the first time they push to ECR and run a task on Fargate. Read the error text slowly — parsing it is the actual skill.

Common error: The task starts, then immediately stops; describe-tasks shows a pull failure because the execution role lacks ECR pull permission:

CannotPullContainerError: pull image manifest has been retried ... failed to resolve ref ... 403 Forbidden

Why: A Fargate task pulls its image using the execution role, not your CLI credentials. If that role doesn’t have AmazonECSTaskExecutionRolePolicy, Fargate has no authorization to pull from your private ECR repo, so the container never starts. (A task definition with no executionRoleArn at all fails earlier, with a ResourceInitializationError instead.)

Fix: Create ecsTaskExecutionRole, attach AmazonECSTaskExecutionRolePolicy (lab step 5), and put its ARN in the task definition’s executionRoleArn. Re-register the task definition and run again. Read the exact stop reason with aws ecs describe-tasks --cluster m90 --tasks "$TASK" --query 'tasks[0].stoppedReason'.

How you’d spot it in prod: A task stuck cycling PENDING → STOPPED with CannotPullContainerError is almost always an IAM or registry-permission problem, not a bad image. The stoppedReason field names the cause; read it before touching the Dockerfile.

Common error: docker push fails because you tagged the wrong URI or never logged in / created the repo:

denied: requested access to the resource is denied
# or, when the repo doesn't exist:
name unknown: The repository with name 'm90-web' does not exist in the registry

Why: ECR pushes are IAM-authenticated to a repository that must already exist. denied means Docker isn’t holding a valid ECR token (the get-login-password step was skipped or its 12-hour token expired); name unknown means the repository was never created, or the image tag points at the wrong account/region host.

Fix: Re-run aws ecr get-login-password | docker login --username AWS --password-stdin "${REPO%/*}", confirm the repo exists with aws ecr describe-repositories, and tag exactly with the URI ECR returned (docker tag m90-web:latest "$REPO:latest").

How you’d spot it in prod: A CI job that pushed fine yesterday and fails with denied today usually has an expired ECR login step — the token lasts 12 hours. Put get-login-password immediately before the push in the pipeline, not once at the top.

Common error: The task reaches RUNNING but curl times out, or the task fails to pull with a network timeout, because it has no public IP / no inbound rule:

curl: (28) Failed to connect to 54.81.152.7 port 80: Connection timed out

Why: Fargate tasks in awsvpc mode get no public IP unless you set assignPublicIp=ENABLED, and a task in a public subnet without one can’t even reach ECR to pull. Separately, the security group must allow inbound TCP 80 from your address, just like an EC2 security group.

Fix: Pass assignPublicIp=ENABLED in the awsvpcConfiguration (lab step 9) and authorize port 80 from your /32 (step 8). A timeout points at the network path — public IP, security group, subnet route — not at nginx.

How you’d spot it in prod: Real services don’t expose tasks directly; they put them in private subnets behind an ALB and reach ECR through a NAT gateway or VPC endpoints. A task that can’t pull with a network timeout in a private subnet almost always means a missing NAT route or ECR VPC endpoint.

ECS and Fargate Interview Questions

Section 4 of 5 · ~1 min

Cover the answers below and say your own version out loud first — name what ECR stores, and what Fargate frees you from that the EC2 launch type doesn’t, before you reveal each answer. Recalling before revealing is what makes these stick when an interviewer asks them cold. The four questions and answers render right after this note.

Go Deeper

Section 5 of 5 · ~1 min

Optional extras if you have ~30 more minutes today:

  • 10 min — Turn today’s one-off task into a real service behind an ALB: create an ip target group, an Application Load Balancer and a listener, then aws ecs create-service --load-balancers … with --desired-count 2. Watch the ALB health-check both tasks. Read the ECS service + load balancing guide — and delete the ALB and service when done, they bill by the hour.
  • 10 min — Read AWS’s Lambda vs Fargate decision guidance and the Prime Video monolith write-up: internalise when short event-driven work belongs on Lambda and when steady traffic belongs on a Fargate service.
  • 5 min — Add an awslogs logConfiguration to the task definition so container stdout flows to a CloudWatch log group (the service you met on Day 57), then read the stream with aws logs tail.
  • 5 min — Set an ECR lifecycle policy that expires untagged images after a few days, so a busy CI pipeline doesn’t grow your registry storage bill unnoticed.
What is Amazon ECR, and how does it differ from Docker Hub? Both

ECR (Elastic Container Registry) is a private Docker registry that lives inside your AWS account. It does the same job as Docker Hub — you docker push images and docker pull them — but the repository is yours, in your region, and access is governed by IAM rather than a Docker Hub account. You authenticate with a short-lived token from aws ecr get-login-password piped into docker login. The reasons to use it over Docker Hub: images stay private by default, pulls from ECS or EKS in the same region are fast and free of egress, and you avoid Docker Hub's anonymous pull-rate limits. The trade-off is it's AWS-only, so a multi-cloud team might keep images somewhere neutral instead.

Explain the difference between the Fargate and EC2 launch types in ECS. Product

Both run your containers; the difference is who owns the servers. With the EC2 launch type you run a fleet of EC2 instances yourself — you size them, patch them, scale them, and bin-pack tasks onto them, paying for the instances whether or not tasks fill them. With Fargate there are no instances: you declare CPU and memory in the task definition, AWS finds capacity, and you pay per second only while the task runs. Fargate is simpler and has no idle servers to manage, so it's my default for spiky or low-volume work. EC2 can be cheaper at steady high scale, or when you need GPUs, custom kernels, or daemon-style access to the host.

What are a task definition, a task, and a service in ECS? Product

A task definition is the recipe: which image, how much CPU and memory, which ports, environment variables, and IAM roles. It's versioned — every edit creates a new revision. A task is one running instance of that recipe: the actual container (or containers) up and running. A service keeps a desired number of tasks running: if a task dies, ECS launches a replacement to hold the count, and a service can register its tasks behind a load balancer so traffic spreads across them. The mental model I use: the task definition is the class, a task is an object, and the service is the supervisor that guarantees N objects always exist and are reachable.

When would you choose Lambda over Fargate to run code on AWS? Both

I reach for Lambda when the work is short, event-driven, and spiky: responding to an S3 upload, an API Gateway request, a queue message. It scales to zero — you pay nothing when idle — and runs per-invocation up to 15 minutes, so bursty or infrequent workloads cost almost nothing. I choose Fargate when the work is long-running or steady: a web service that must stay up, anything over the 15-minute cap, or a process needing more memory, a full container image, and OS control. Amazon's Prime Video team moved a monitoring service off Step Functions and Lambda onto ECS for that reason — constant traffic made per-invocation overhead the wrong fit.

Mark Day 59 complete

Tomorrow you leave AWS billing behind and start observability — Prometheus and Grafana fundamentals, running locally on your own machine.

Stuck on today’s lab? Ask in Mission 90 Q&A