Phase 3 · CLOUD
ECR + ECS/Fargate — containers on AWS (Go Deeper: where Lambda fits)
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
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.
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
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
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-tasksshows a pull failure because the execution role lacks ECR pull permission:CannotPullContainerError: pull image manifest has been retried ... failed to resolve ref ... 403 ForbiddenWhy: 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 noexecutionRoleArnat all fails earlier, with aResourceInitializationErrorinstead.)Fix: Create
ecsTaskExecutionRole, attachAmazonECSTaskExecutionRolePolicy(lab step 5), and put its ARN in the task definition’sexecutionRoleArn. Re-register the task definition and run again. Read the exact stop reason withaws 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
CannotPullContainerErroris almost always an IAM or registry-permission problem, not a bad image. ThestoppedReasonfield names the cause; read it before touching the Dockerfile.
Common error:
docker pushfails 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 registryWhy: ECR pushes are IAM-authenticated to a repository that must already exist.
deniedmeans Docker isn’t holding a valid ECR token (theget-login-passwordstep was skipped or its 12-hour token expired);name unknownmeans 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 withaws 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
deniedtoday usually has an expired ECR login step — the token lasts 12 hours. Putget-login-passwordimmediately before the push in the pipeline, not once at the top.
Common error: The task reaches RUNNING but
curltimes 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 outWhy: Fargate tasks in
awsvpcmode get no public IP unless you setassignPublicIp=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=ENABLEDin theawsvpcConfiguration(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
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
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
iptarget group, an Application Load Balancer and a listener, thenaws 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
awslogslogConfigurationto the task definition so container stdout flows to a CloudWatch log group (the service you met on Day 57), then read the stream withaws 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