AWS: Zero to Hero — Week 8: Containers on AWS — ECS, EKS, and Fargate
Week 8 of learning AWS from zero, with Claude as my research partner. ECS (Elastic Container Service), EKS (Elastic Kubernetes Service), and Fargate explained, plus the real reason to default to ECS: an EKS cluster costs $0.10/hour with zero free tier, jumping to $0.60/hour if a Kubernetes version lapses into extended support. A full ECS-on-Fargate lab (build, push, run, ECS Exec), plus an optional, tightly-scoped EKS create-and-delete.
Series: AWS: Zero to Hero (9 articles)
- …
- 8AWS: Zero to Hero — Week 8: Containers on AWS — ECS, EKS, and Fargate This article
Week 4 was a virtual machine. Week 7 was a function that isn't allowed to run longer than 15 minutes. This week sits deliberately in between: containers give you a full, controllable runtime like a VM, but packaged light enough to start in seconds and schedule by the thousand — and AWS gives you two genuinely different ways to orchestrate them, with a cost difference between them that's worth understanding before touching either.
The Deep Dive
Why containers exist between "VM" and "function"
A container packages an application together with everything it needs to run — but unlike a VM, it doesn't bundle its own OS kernel; it shares the host's. That makes containers dramatically lighter and faster to start than a full EC2 instance, while still giving you the thing Lambda deliberately doesn't: a long-running process, full control over the runtime, and no 15-minute ceiling. It's the middle layer of last two weeks' spectrum, on purpose.
ECS: AWS's own container orchestrator
ECS (Elastic Container Service) is AWS's native way to run containers at scale. Four concepts, and they map onto ideas from earlier weeks almost exactly:
- Cluster — a logical grouping of resources your containers run in. Unlike EKS below, an ECS cluster itself costs nothing — you only ever pay for the compute underneath it.
- Task definition — a blueprint: which container image, how much CPU/memory, which ports, which IAM role. Think of it like Week 4's launch template, but for a container instead of a VM.
- Task — one running instance of a task definition.
- Service — keeps a specified number of tasks running, replacing any that fail. This is directly Week 4's Auto Scaling Group concept, just orchestrating containers instead of EC2 instances.
ECS offers two launch types, and the difference is a direct continuation of Week 3's sliding shared-responsibility line:
- EC2 launch type — you manage the underlying EC2 instances yourself; ECS just schedules containers onto them.
- Fargate launch type — fully serverless. You specify CPU and memory per task, and AWS handles the actual servers entirely — you never see or patch an instance at all.
EKS: the same idea, running actual Kubernetes
EKS (Elastic Kubernetes Service) solves the same orchestration problem, but through the open-source Kubernetes API instead of AWS's own proprietary model. The appeal is real: Kubernetes manifests are portable across clouds and on-prem, and there's a massive existing ecosystem (Helm charts, operators, and a huge pool of engineers who already know it).
EKS also offers multiple ways to run the actual compute:
- EC2 node groups (self-managed or AWS-managed) — you still own the underlying instances.
- Fargate on EKS — serverless pods, the same idea as ECS Fargate, just running inside a Kubernetes cluster.
- EKS Auto Mode — the newest option: AWS provisions and manages the worker nodes automatically, for a management fee on top of the EC2 instance price (roughly 10–12%, varying by instance type) — Kubernetes with a meaningfully smaller operational burden, at a real cost premium.
The cost difference that actually decides this for most people
Here's the part that genuinely should drive which one you pick, not just feature comparisons: an ECS cluster is free. An EKS cluster is not, ever, no matter how small.
EKS charges a flat $0.10 per hour, per cluster, for the control plane alone — about $73/month — before a single container runs, with no free tier at all, on any account. And there's a trap worth knowing about specifically because it's current and easy to miss: a cluster gets 14 months of standard support on a given Kubernetes version at that $0.10/hour rate; if you don't upgrade before then, it automatically enters extended support at $0.60/hour — roughly $438/month — for up to 12 more months, with no warning banner forcing the issue. Letting a cluster's Kubernetes version quietly lapse is one of the most common real-world EKS billing surprises that exists.
So which one, when
- Default to ECS on Fargate for most new, AWS-only projects. No control plane fee, no servers to manage, genuinely the simplest path from "I have a container image" to "it's running."
- Reach for EKS specifically when you need Kubernetes itself — multi-cloud portability, an existing Kubernetes-skilled team, or tooling from the Kubernetes ecosystem that doesn't have an ECS equivalent. Not because it's "the more real option" — plenty of serious production systems run entirely on ECS.
- Choose EC2 launch type (on either) when you need control Fargate doesn't give you — specific instance types, GPU workloads, or fine-grained control over the underlying host.
Cost Reality Check for This Week — read this one carefully
This week has the widest cost range of the whole series so far. ECS itself costs nothing, and Fargate bills only for the CPU/memory a task actually consumes while it's running (roughly $0.04048 per vCPU-hour and $0.004445 per GB-hour) — stop the task, and billing stops immediately, the same instant. That makes the ECS lab below genuinely safe: a short test costs a few cents at most.
EKS is a different situation entirely. The $0.10/hour control plane fee starts the moment
create-cluster succeeds and keeps running until you explicitly delete it — completely independent of whether any workload is actually deployed. There is no free tier, no grace period, nothing. For that reason, the EKS section below is marked optional, kept deliberately minimal, and paired with an immediate teardown — if you're being careful with your account's $200 credit, it's completely fine to just read that part rather than run it.Hands-On, Part 1: ECS on Fargate (the safe one)
Step 1 — Prerequisites: Docker installed, and an ECR repository
1
2
3
aws ecr create-repository --repository-name zero-to-hero-app
REPO_URI=$(aws ecr describe-repositories --repository-names zero-to-hero-app \
--query 'repositories[0].repositoryUri' --output text)
Step 2 — A tiny containerized app
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
mkdir zth-container && cd zth-container
cat > app.py <<'PY'
from http.server import HTTPServer, BaseHTTPRequestHandler
import socket
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(200)
self.end_headers()
self.wfile.write(f"Hello from ECS task on {socket.gethostname()}\n".encode())
HTTPServer(("0.0.0.0", 80), Handler).serve_forever()
PY
cat > Dockerfile <<'DOCKER'
FROM python:3.13-slim
COPY app.py /app.py
EXPOSE 80
CMD ["python3", "/app.py"]
DOCKER
Step 3 — Build, tag, and push to ECR
1
2
3
4
5
aws ecr get-login-password | docker login --username AWS --password-stdin $(echo $REPO_URI | cut -d/ -f1)
docker build -t zero-to-hero-app .
docker tag zero-to-hero-app:latest $REPO_URI:latest
docker push $REPO_URI:latest
Step 4 — An ECS cluster (free — this is the whole point of the comparison above)
1
aws ecs create-cluster --cluster-name zero-to-hero-cluster
Step 5 — A task execution role, so the task can actually pull the image and write logs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
cat > ecs-trust-policy.json <<'JSON'
{
"Version": "2012-10-17",
"Statement": [{"Effect": "Allow", "Principal": {"Service": "ecs-tasks.amazonaws.com"}, "Action": "sts:AssumeRole"}]
}
JSON
aws iam create-role --role-name ZeroToHeroECSTaskExecutionRole \
--assume-role-policy-document file://ecs-trust-policy.json
aws iam attach-role-policy --role-name ZeroToHeroECSTaskExecutionRole \
--policy-arn arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
EXEC_ROLE_ARN=$(aws iam get-role --role-name ZeroToHeroECSTaskExecutionRole --query 'Role.Arn' --output text)
sleep 10
Step 6 — Register the task definition
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
cat > task-def.json <<JSON
{
"family": "zero-to-hero-task",
"networkMode": "awsvpc",
"requiresCompatibilities": ["FARGATE"],
"cpu": "256",
"memory": "512",
"executionRoleArn": "$EXEC_ROLE_ARN",
"containerDefinitions": [{
"name": "zero-to-hero-app",
"image": "$REPO_URI:latest",
"portMappings": [{"containerPort": 80, "protocol": "tcp"}]
}]
}
JSON
aws ecs register-task-definition --cli-input-json file://task-def.json
Step 7 — A service, running in public subnets with public IPs (no NAT Gateway needed for this lab)
Unlike Week 6's private-subnet pattern, this lab deliberately uses public subnets with a public IP per task — purely to avoid paying for a NAT Gateway just to let Fargate pull the image from ECR. For anything production-bound, route this through a private subnet with a VPC endpoint instead, the way Week 6 covered.
1
2
3
4
5
6
7
8
9
10
11
VPC_ID=$(aws ec2 describe-vpcs --filters Name=isDefault,Values=true --query 'Vpcs[0].VpcId' --output text)
SUBNET_IDS=$(aws ec2 describe-subnets --filters Name=vpc-id,Values=$VPC_ID --query 'Subnets[].SubnetId' --output text | tr '\t' ',')
SG_ID=$(aws ec2 describe-security-groups --filters Name=vpc-id,Values=$VPC_ID Name=group-name,Values=default --query 'SecurityGroups[0].GroupId' --output text)
aws ecs create-service \
--cluster zero-to-hero-cluster \
--service-name zero-to-hero-service \
--task-definition zero-to-hero-task \
--desired-count 2 \
--launch-type FARGATE \
--network-configuration "awsvpcConfiguration={subnets=[$SUBNET_IDS],securityGroups=[$SG_ID],assignPublicIp=ENABLED}"
Step 8 — Find a running task and curl it
1
2
3
4
5
6
7
8
9
TASK_ARN=$(aws ecs list-tasks --cluster zero-to-hero-cluster --query 'taskArns[0]' --output text)
ENI_ID=$(aws ecs describe-tasks --cluster zero-to-hero-cluster --tasks $TASK_ARN \
--query 'tasks[0].attachments[0].details[?name==`networkInterfaceId`].value' --output text)
PUBLIC_IP=$(aws ec2 describe-network-interfaces --network-interface-ids $ENI_ID \
--query 'NetworkInterfaces[0].Association.PublicIp' --output text)
curl http://$PUBLIC_IP
You should get back
Hello from ECS task on <container-id> — a real container, actually running on Fargate, reachable over the internet.Step 9 — Shell into the running container (ECS Exec — Session Manager's container equivalent)
1
2
3
4
aws ecs update-service --cluster zero-to-hero-cluster --service zero-to-hero-service --enable-execute-command --force-new-deployment
# wait for the new tasks to be running, then:
aws ecs execute-command --cluster zero-to-hero-cluster --task $TASK_ARN \
--container zero-to-hero-app --interactive --command "/bin/sh"
Same underlying idea as Week 4's Session Manager — no open port, no SSH key, IAM-controlled — just for a container instead of an instance.
Cleanup
1
2
3
4
5
6
7
aws ecs update-service --cluster zero-to-hero-cluster --service zero-to-hero-service --desired-count 0
aws ecs delete-service --cluster zero-to-hero-cluster --service zero-to-hero-service
aws ecs deregister-task-definition --task-definition zero-to-hero-task:1
aws ecs delete-cluster --cluster zero-to-hero-cluster
aws ecr delete-repository --repository-name zero-to-hero-app --force
aws iam detach-role-policy --role-name ZeroToHeroECSTaskExecutionRole --policy-arn arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
aws iam delete-role --role-name ZeroToHeroECSTaskExecutionRole
Hands-On, Part 2: EKS (optional — read the cost box above before running this)
This is deliberately minimal: enough to see a real cluster exist and respond, torn down immediately after.
1
2
3
4
5
# Costs $0.10/hr the instant this succeeds — no free tier. ~15-20 min to provision.
eksctl create cluster --name zero-to-hero-eks --region <your-region> \
--nodes 1 --node-type t3.small --managed
kubectl get nodes
That single command proves the whole point of EKS in one line: a real Kubernetes cluster, with a real node registered, controllable through the standard
kubectl anyone with existing Kubernetes experience already knows — no AWS-specific tooling required to actually operate it.Delete it immediately after you've seen that output:
1
eksctl delete cluster --name zero-to-hero-eks --region <your-region>
A clean 20-minute create-verify-delete cycle costs a few cents in control plane time plus the node's own on-demand rate for that window — genuinely small, but only if the delete step actually happens. If you'd rather not spend any of your credit on this right now, skipping this part entirely and coming back to it once a real project needs Kubernetes specifically is a perfectly reasonable call.
My Take
ECS with Fargate is the simplest, cost-effective choice for most AWS projects, while EKS is worth the extra cost when Kubernetes is essential. Always choose simplicity and clean up resources to avoid unexpected bills.
Next week — Part 9: CI/CD & Infrastructure as Code — CodePipeline, CloudFormation, and CDK, and how "integrating cloud into an app" stops being manual clicking and becomes something version-controlled and repeatable.
Series: AWS: Zero to Hero (9 articles)
- …
- 8AWS: Zero to Hero — Week 8: Containers on AWS — ECS, EKS, and Fargate This article
Enjoyed reading this content? Let the author know!
Your likes, comments, shares, and saves help creators reach more builders.
Loading recommendations
Loading article