ECS on Fargate
Running containers with no servers to manage.
Part 1: The problem
QuickCart’s order service runs in a container on one EC2 server. Leo started it with docker run, and it worked.
At 3 a.m. on a Saturday, the container ran out of memory and stopped. Nothing started it again. Orders failed until Noor checked her phone at 8.
On Monday, releasing the fix meant Leo logging in to the server, stopping the old container and starting the new one. For about a minute, there was no container at all. Customers saw errors.
QuickCart needs something that keeps a set number of copies running, spreads them across zones, replaces any that die, and connects them to the load balancer. That’s a container orchestrator. On AWS, the simplest one is ECS.
Part 2: The idea
ECS, Elastic Container Service, runs containers for you. You describe the container once, say how many copies you want, and ECS keeps that many running.
What an orchestrator does
| Job | Without one | With ECS |
|---|---|---|
| Put a container somewhere to run | You pick a server and run docker run | ECS finds room for it |
| Keep it running | Nobody notices when it stops | A stopped copy is replaced automatically |
| Run several copies across zones | You start each one by hand | You set a number; ECS spreads them |
| Connect copies to the load balancer | You register each server | ECS registers and removes them |
| Release a new version | Stop old, start new, by hand | ECS replaces copies one batch at a time |
The four ECS words
| Word | What it is | The EC2 version you already know |
|---|---|---|
| Cluster | A named group for your services. On Fargate it holds no servers | — |
| Task definition | The recipe: which image, how much CPU and memory, which port, where logs go | A launch template |
| Task | One running copy of a task definition | One EC2 instance |
| Service | Keeps a set number of tasks running, and connects them to a load balancer | An Auto Scaling group |
Every change to a task definition makes a new numbered revision, like order-service:1, order-service:2. Old revisions stay exactly as they were, so going back is just choosing an earlier number.
Fargate: no servers of your own
ECS can run tasks in two ways:
| Fargate | EC2 | |
|---|---|---|
| Servers | None that you see. AWS runs them | EC2 instances in your account |
| Patching servers | AWS does it | You do it |
| You pay for | The CPU and memory each task asks for, while it runs | Every instance, busy or not |
| Cheapest when | Load is small, or goes up and down | Load is large and steady |
QuickCart uses Fargate. It costs a little more per hour of computing, but there is no server to patch, resize or replace. Auto scaling showed how much work servers are.
The task definition, every field explained
This is QuickCart’s task definition as JSON. The console builds exactly this from its form, and you can also paste it in directly, which you’ll do in the lab.
{
"family": "order-service",
"requiresCompatibilities": ["FARGATE"],
"networkMode": "awsvpc",
"cpu": "256",
"memory": "512",
"runtimePlatform": {
"operatingSystemFamily": "LINUX",
"cpuArchitecture": "X86_64"
},
"executionRoleArn": "arn:aws:iam::111122223333:role/ecsTaskExecutionRole",
"containerDefinitions": [
{
"name": "app",
"image": "111122223333.dkr.ecr.us-east-1.amazonaws.com/quickcart-orders:v1",
"essential": true,
"portMappings": [
{ "containerPort": 8080, "protocol": "tcp" }
],
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/ecs/order-service",
"awslogs-region": "us-east-1",
"awslogs-stream-prefix": "ecs"
}
}
}
]
}| Field | What it does | If you leave it out | When you’d change it |
|---|---|---|---|
family | The task definition’s name. Each save adds a revision to this family | Required | Never; a new name is a new task definition |
requiresCompatibilities | Check that this recipe can run on Fargate | ECS doesn’t check Fargate’s rules until you try to run it | To EC2 if you run on your own instances |
networkMode | awsvpc: each task gets its own network card and private IP in your subnet | Required on Fargate — it’s the only mode Fargate supports | Only on the EC2 launch type |
cpu | CPU for the whole task, in units where 1024 is one vCPU. 256 is a quarter | Required on Fargate | When the app needs more. Load tests tell you |
memory | Memory for the whole task, in MB. Must be a size Fargate allows for that CPU | Required on Fargate | With cpu. Too little and the task is killed for running out of memory |
runtimePlatform | Linux on Intel/AMD processors — matches --platform linux/amd64 from the build | Defaults to Linux on X86_64 | To ARM64 if you build ARM images. Fargate on ARM costs less |
executionRoleArn | The IAM role ECS uses before your app starts: pull the image from ECR, send logs | ECS can’t pull from a private ECR repository or send logs, so the task fails to start | Only to point at a different role |
taskRoleArn (not used here) | The IAM role your code uses for its own AWS calls | Your code has no AWS permissions. Fine here — the order service doesn’t call AWS | When the app reads S3, a secret or a queue |
containerDefinitions | The containers in the task. Usually one | Required | To add a helper container, such as a log shipper |
name | The container’s name. The service uses it to know which container gets load balancer traffic | Required | Rarely |
image | The full image URI to pull, with its tag | Required | For every release: v1, then v2 |
essential | If this container stops, stop the whole task | Defaults to true | Set false only for optional helper containers |
portMappings | The port the app listens on inside the container, and its protocol | The load balancer has no port to send traffic to | If the app’s port changes |
logConfiguration → logDriver | awslogs: send everything the app prints to CloudWatch Logs | Output is thrown away. You can’t see why a task crashed | Rarely |
awslogs-group | The CloudWatch log group to write to. It must exist already | Required with awslogs | One group per service |
awslogs-region | The region of that log group | Required with awslogs | If logs go to another region |
awslogs-stream-prefix | Start of each log stream’s name. Streams are named ecs/app/TASK_ID, so you can find one task’s output | Required on Fargate | Rarely |
Fargate only accepts certain CPU and memory pairs. For the small sizes:
cpu | Allowed memory |
|---|---|
256 (0.25 vCPU) | 512, 1024, 2048 |
512 (0.5 vCPU) | 1024 to 4096, in steps of 1024 |
1024 (1 vCPU) | 2048 to 8192, in steps of 1024 |
Every task gets its own IP address
With awsvpc, a task is a full member of your VPC, like a small server:
- Its security group applies to the task itself.
- The load balancer’s target group must use IP addresses as targets, not instances. There’s no instance of yours to register.
- Each task uses one IP in its subnet. A busy service can run a small subnet out of addresses.
Two roles, easy to mix up
| Role | Used by | When | For | If it’s wrong |
|---|---|---|---|---|
| Execution role | ECS itself | Before your app starts | Pull the image, send logs, fetch secrets | The task never starts. No app logs. The stopped reason says why |
| Task role | Your code | While your app runs | Anything your code does in AWS | The task runs fine, then your app logs AccessDenied errors |
The service and the load balancer
A service holds a desired count — how many tasks you want. It keeps comparing that number with what’s running and closes the gap. A task stops, a zone has a problem, a release starts: it’s always the same loop.
When a task starts, the service registers its IP with the target group. The load balancer only sends it traffic once it passes the health check on /health. When a task stops, the service removes it from the target group first.
Part 3: See it
Here is QuickCart’s service with a desired count of 2. Stop a task and watch the service replace it. Change the desired count. Then break the execution role and stop a task again.
Task definition
order-service:1
image quickcart-orders:v1, 0.25 vCPU, 0.5 GB, port 8080
Service order-service
Desired 2Running 2
Desired and running match. Nothing to do.
Subnet in us-east-1a
- task 3f9a1cRUNNING172.31.8.41
Subnet in us-east-1b
- task b27e4dRUNNING172.31.20.17
Stopped tasks (newest first)
- None yet
What to notice: with the execution role broken, the running tasks carried on — they had pulled their image long before. Only new tasks failed, and they left no app logs at all. The only clue was the stopped reason. That’s why it’s the first place to look when a task won’t start.
Part 4: Do it
Costs money — about $2 a day while it runs: two small Fargate tasks (about $0.60 a day together), the load balancer (about $0.55 a day plus a little per request), and public IPv4 addresses (about $0.12 a day each). Prices are for us-east-1 and change, so check the pricing pages. Delete everything at the end.
You’ll run QuickCart’s order service on Fargate: two tasks in two zones, behind a load balancer, with logs in CloudWatch. Then you’ll stop a task and watch it come back. About 45 minutes.
You need the quickcart-orders repository with the v1 image from Storing images: ECR. Check the region selector says N. Virginia (us-east-1).
To save money, this lab uses the default VPC that every account has, and gives each task a public IP, so it can pull its image from ECR without a NAT gateway. That’s safe because the task’s security group only lets the load balancer in. The routing lesson shows the stricter version, with private subnets and a NAT gateway.
Check the image is in ECR
In the AWS console: Amazon ECRPrivate registryRepositoriesquickcart-orders
You should seeAn image tagged v1. Copy the repository URI — you’ll need it for the task definition.
Create the execution role
Many accounts already have a role called
ecsTaskExecutionRole. Look for it in IAM → Roles first. If it’s there, skip to the next step.In the AWS console: IAMRolesCreate role
Field Enter Trusted entity type AWS service Service or use case Elastic Container Service Use case Elastic Container Service Task Choose Next. Search for and tick
AmazonECSTaskExecutionRolePolicy, then Next.Field Enter Role name ecsTaskExecutionRoleChoose Create role.
You should seeThe role in the list. Open it and copy its ARN. It looks like
arn:aws:iam::111122223333:role/ecsTaskExecutionRole, with your account ID.That managed policy allows exactly the execution role’s job: pulling images from ECR and writing to CloudWatch Logs.
Create the log group
In the AWS console: CloudWatchLogsLog groupsCreate log group
Field Enter Log group name /ecs/order-serviceRetention setting 1 week Choose Create. Without a retention setting, logs are kept, and billed, forever.
Create the two security groups
In the AWS console: EC2Security GroupsCreate security group
First, the load balancer’s:
Field Enter Security group name quickcart-alb-sgDescription QuickCart load balancerVPC the default VPC Inbound rule type Port Source HTTP 80 Anywhere-IPv4 Choose Create security group. Then create a second one, for the tasks:
Field Enter Security group name quickcart-app-sgDescription QuickCart order service tasksVPC the default VPC Inbound rule type Port range Source Custom TCP 8080Custom, then choose quickcart-alb-sgLeave outbound rules as they are on both. The tasks need to reach ECR on the way out.
You should seeTwo security groups.
quickcart-app-sghas one inbound rule, naming the load balancer’s group rather than any address. Tasks can come and go; the rule still fits.Create the target group
In the AWS console: EC2Target groupsCreate target group
Field Enter Choose a target type IP addresses Target group name quickcart-orders-tgProtocol : Port HTTP : 8080VPC the default VPC Health check path /healthOpen Advanced health check settings:
Field Enter Healthy threshold 2Unhealthy threshold 2Interval 10secondsChoose Next. On the Register targets page, don’t register anything — remove any IP the page filled in. The ECS service adds its tasks itself. Choose Create target group.
Then make it drain faster:
In the AWS console: EC2Target groupsquickcart-orders-tgAttributesEdit
Field Enter Deregistration delay 30secondsChoose Save changes. The default of 300 seconds makes every stop and every release wait five minutes. Releases without downtime explains how to choose the number.
Create the load balancer
In the AWS console: EC2Load balancersCreate load balancerApplication Load BalancerCreate
Field Enter Load balancer name quickcart-albScheme Internet-facing Load balancer IP address type IPv4 VPC the default VPC Availability Zones and subnets tick us-east-1a and us-east-1b Security groups quickcart-alb-sgonly — removedefaultListener HTTP : 80, default action Forward toquickcart-orders-tgChoose Create load balancer. It takes a few minutes to become Active. Carry on meanwhile.
Create the cluster
In the AWS console: Amazon Elastic Container ServiceClustersCreate cluster
Field Enter Cluster name quickcartInfrastructure AWS Fargate (serverless) only Choose Create.
You should seeThe cluster quickcart, active, with 0 services and 0 tasks. Look at EC2 → Instances: nothing new. On Fargate a cluster is just a name.
Create the task definition
In the AWS console: Amazon Elastic Container ServiceTask definitionsCreate new task definitionCreate new task definition with JSON
Replace everything in the editor with the task definition from the idea, above. Put your account ID in both highlighted places: the role ARN and the image URI.
Choose Create.
You should seeorder-service:1 — revision 1 of the
order-servicefamily. The JSON tab shows your definition, with a few extra fields ECS filled in with their defaults.Create the service
In the AWS console: Amazon Elastic Container ServiceClustersquickcartServicesCreate
The form is long, and AWS moves its sections around from time to time. Find each field by its name.
Field Enter Task definition family order-serviceTask definition revision the latest, 1 Service name order-serviceCompute options Launch type Launch type FARGATE Platform version LATEST Desired tasks 2Health check grace period 30secondsLeave Deployment configuration on its defaults. Releases without downtime goes through every setting in it.
Under Networking:
Field Enter VPC the default VPC Subnets only the us-east-1a and us-east-1b subnets — remove the others Security group Use an existing security group, then quickcart-app-sgonly — removedefaultPublic IP Turned on Under Load balancing:
Field Enter Use load balancing ticked Load balancer type Application Load Balancer Container app 8080:8080Application Load Balancer Use an existing load balancer, then quickcart-albListener Use an existing listener, then 80:HTTPTarget group Use an existing target group, then quickcart-orders-tgChoose Create.
Setting In plain words Desired tasks 2 Always keep two copies running Health check grace period 30 Give a new task 30 seconds to start before its failed health checks count against it Public IP on Lets the task reach ECR to pull its image, without a NAT gateway Security group quickcart-app-sgOnly the load balancer may reach the task You should seeAfter one to three minutes, the service shows 2/2 tasks running. On the Tasks tab, two tasks, one in each zone, each with its own private IP. In EC2 → Target groups → quickcart-orders-tg → Targets, two IP addresses, both healthy.
Reach it through the load balancer
In the AWS console: EC2Load balancersquickcart-alb
Copy the DNS name. It looks like
quickcart-alb-1234567890.us-east-1.elb.amazonaws.com.Terminalcurl ALB_DNS_NAME/Part What it means curlfetch a web page and print it ALB_DNS_NAMEreplace this with the DNS name you copied. With no http://in front, curl uses plain HTTP/the home page, which says which task answered You should see{"served_by":"ip-172-31-8-41.ec2.internal","service":"order-service","version":"v1"}Run it a few more times.
served_byis the name of the task that answered — yours will differ — and it switches between your two tasks. The load balancer is sharing the traffic.Stop a task and watch it come back
In the AWS console: Amazon Elastic Container ServiceClustersquickcartServicesorder-serviceTasks
Tick one task, then choose Stop and Stop selected.
You should seeThe service shows 1 running and 2 desired. Within about a minute a new task appears, goes from Provisioning to Pending to Running, and the count is back to 2. Nobody started it.
Change the filter from running tasks to stopped tasks, and open the one you stopped. Its Stopped reason says it was stopped by a user. When a task fails for any other reason — a missing image, a missing permission — this is where ECS writes why.
Read the logs
In the AWS console: CloudWatchLogsLog groups/ecs/order-service
Open the newest log stream. Its name is
ecs/app/followed by the task’s ID.The stream shows gunicorn starting[INFO] Starting gunicorn 23.0.0 [INFO] Listening at: http://0.0.0.0:8080 [INFO] Using worker: sync [INFO] Booting worker with pid: 7 [INFO] Booting worker with pid: 8Each real line starts with a time and a process number, and yours differ. Everything the app prints ends up here, and it stays after the task is gone.
Change the desired count
In the AWS console: Amazon Elastic Container ServiceClustersquickcartServicesorder-serviceUpdate service
Field Enter Desired tasks 3Choose Update.
You should seeA third task starts, and a third target appears in the target group. Change it back to
2the same way: the service stops one task, draining it from the load balancer first.Clean-up step: Clean up
If you’re going straight on to Releases without downtime, keep everything and clean up at the end of that lesson. Otherwise, delete it all now, in this order.
1. The service.
In the AWS console: Amazon Elastic Container ServiceClustersquickcartServicesorder-serviceDelete service
If the console asks, tick Force delete (or first update the service to 0 desired tasks), then confirm. Wait until the service and its tasks are gone.
2. The cluster.
In the AWS console: Amazon Elastic Container ServiceClustersquickcartDelete cluster
Type the confirmation text it asks for, and delete.
3. The task definition. Open Task definitions → order-service, tick every revision, and choose Actions → Deregister. Then, with them still ticked, choose Actions → Delete.
4. The load balancer. In EC2 → Load balancers, tick
quickcart-alb, choose Actions → Delete load balancer, and typeconfirm.5. The target group. In EC2 → Target groups, tick
quickcart-orders-tgand choose Actions → Delete.6. The security groups. In EC2 → Security Groups, delete
quickcart-app-sgfirst, thenquickcart-alb-sg. The app’s group refers to the load balancer’s, so it has to go first. If one won’t delete, wait a minute — a network card from a stopped task is still letting go of it.7. The log group. In CloudWatch → Log groups, tick
/ecs/order-service, then Actions → Delete log group(s).8. The image repository, unless a later lesson needs it: Amazon ECR → Repositories, tick
quickcart-orders, Delete, and typedelete.The
ecsTaskExecutionRolerole is free to keep. Later lessons use it.You should seeECS → Clusters is empty. EC2 → Load balancers and EC2 → Target groups are empty. EC2 → Network interfaces shows nothing that mentions ECS. Nothing from this lab is still billing.
Part 5: Check yourself
A new task stops within a minute of starting, and its log stream in CloudWatch is empty. Where do you look first?
The order service needs to read a file from S3 while it runs. Which role gets that permission?
Why must the target group's target type be "IP addresses" for Fargate?
The service's desired count is 2. You stop one of its tasks by hand. What happens?
QuickCart's tasks run in public subnets with a public IP, but no NAT gateway. What stops anyone on the internet reaching port 8080 on a task directly?
In the task definition, what does cpu set to 256 mean?
Part 6: Cheat sheet
| Term or setting | In one line |
|---|---|
| Cluster | A named group for services. Holds no servers on Fargate |
| Task definition | The recipe: image, CPU, memory, port, logs, roles. Every change is a new revision |
| Task | One running copy of a task definition |
| Service | Keeps the desired count of tasks running and registered with the load balancer |
| Fargate | AWS runs the servers. You pay per task, for what it asks for |
awsvpc | Each task gets its own network card, private IP and security group |
| Target type | IP addresses for Fargate |
cpu 256 / memory 512 | A quarter of a vCPU and 512 MB |
| Execution role | ECS’s role: pull the image, send logs, before the app starts |
| Task role | Your code’s role, while it runs |
| Task stops with no logs | Read the stopped reason |
| Targets say “Request timed out” | Security groups or port, not code |
exec format error | Image built for the wrong processor. Build with --platform linux/amd64 |