26.1 ECS Concepts: Clusters, Task Definitions, Tasks, and Services
Right, let’s get our hands dirty with the core concepts of ECS. Forget the fluffy marketing speak; this is the actual machinery you need to understand. If you get this, everything else—Fargate, service discovery, scaling—clicks into place. Think of it like this: ECS is the stage manager for your containerized play, and these are the key backstage roles.
First, the Cluster. This one’s simple. It’s a logical grouping of stuff that runs your tasks. That “stuff” can be a fleet of EC2 instances you manage yourself (the “EC2 launch type,” which feels a bit old-school these days) or, more elegantly, it can be just empty, abstract compute-space waiting for Fargate to fill it (the “Fargate launch type”). You don’t pay for the cluster itself; it’s just a namespacing boundary, a folder for your resources. Best practice? One cluster per environment (prod, staging) per AWS account. Keeps things tidy and your security boundaries clear.
Now, the star of the show: the Task Definition. This is not a running container. Say it with me: It. Is. A. Blueprint. It’s a JSON document that describes everything a running container or group of containers needs: which image to use, CPU/memory reservations, environment variables, logging configuration, storage mounts, and networking mode. It’s the recipe, not the cake. You version these things, and rolling back a deployment often means just pointing a service back to an older task definition revision. Here’s what a dead-simple one for Fargate looks like:
{
"family": "my-api",
"networkMode": "awsvpc",
"requiresCompatibilities": ["FARGATE"],
"cpu": "256",
"memory": "512",
"executionRoleArn": "arn:aws:iam::123456789012:role/ecsTaskExecutionRole",
"containerDefinitions": [
{
"name": "api-container",
"image": "123456789012.dkr.ecr.us-east-1.amazonaws.com/my-api:latest",
"portMappings": [
{
"containerPort": 8080,
"protocol": "tcp"
}
],
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/ecs/my-api",
"awslogs-region": "us-east-1",
"awslogs-stream-prefix": "ecs"
}
}
}
]
}
Why this matters: The executionRoleArn is crucial—it’s the IAM role the ECS agent uses to pull your image from ECR and write logs to CloudWatch. Forget to set this up correctly, and your tasks will fail spectacularly with a permissions error that’ll have you scratching your head for hours. Trust me, we’ve all been there.
What’s a Task, Then?
A Task is the running instance of a Task Definition. It’s the actual cake baked from the recipe. When ECS instantiates your blueprint, it becomes a task, which consists of one or more running containers (a “group” of containers that are deployed together on the same host). In Fargate, each task gets its own isolated kernel runtime, which is fantastic for security. A task has a lifecycle: it can be PROVISIONING, PENDING, RUNNING, or STOPPED. You can run tasks manually (great for one-off jobs or debugging) via the run-task command, but for long-running applications, you don’t want to manage tasks yourself. That’s where the Service comes in.
The Service: The Real Brain
The Service is the autopilot. Its job is relentless: “Ensure that N copies of a given Task Definition are running and healthy at all times within a cluster.” You define a service, tell it which task definition to use, how many tasks to run (desiredCount), and how to distribute them (placement strategies), and it handles the rest. It’s constantly checking. If a task crashes, the service scheduler launches a new one. If you update the task definition, it performs a rolling deployment. It integrates with Elastic Load Balancing to register new tasks and drain connections from old ones. This is the workhorse that makes your application reliable.
# Creating a service that hooks our task definition to a load balancer
aws ecs create-service \
--cluster my-cluster \
--service-name my-api-service \
--task-definition my-api:1 \
--desired-count 2 \
--launch-type FARGATE \
--network-configuration "awsvpcConfiguration={subnets=[subnet-12345ab],securityGroups=[sg-67890cd],assignPublicIp=ENABLED}" \
--load-balancers "targetGroupArn=arn:aws:elasticloadbalancing:us-east-1:123456789012:targetgroup/my-api-tg/1234567890abcdef,containerName=api-container,containerPort=8080"
The Pitfall: See that assignPublicIp=ENABLED? In a real production environment, your tasks should live in private subnets without public IPs, and you’d use a NAT Gateway for outbound traffic. Giving every task a public IP is a billable item and a security no-no. This example is for simplicity, and I’m calling myself out on it. Don’t do this in prod.
The beautiful part of this architecture is the separation of concerns. The task definition defines what to run. The service defines how many to run and how to keep them running. The cluster defines where to run them. Understand these pieces, and you can bend ECS to your will.