Skip to content

ECS on Fargate

2 min read · main part only; folded sections are optional

After this you can answer: How do a cluster, a service, a task definition and a task relate to each other?

flowchart TB
CL["cluster minitickets-dev"] --> SVC["service minitickets-dev-api<br/>desiredCount 1"]
TD["task definition<br/>minitickets-dev-api:revision"] --> SVC
SVC --> T["task: one api container<br/>+ Service Connect proxy"]
SVC -->|"registers task IP"| TG["api target group"]

The web side has the same shape: minitickets-dev-web.

  1. The task definition says which image, how much CPU and memory, which env vars and secrets, and which roles. Every change makes a new numbered revision.
  2. The service starts tasks from the newest revision and keeps desiredCount of them running. On a new revision it starts new tasks first, then stops the old ones.
  3. Each task gets its own network card (awsvpc mode), a private IP, a security group and, here, a public IP.
  4. Health decides who lives. A task that fails its health checks is stopped and replaced. If new tasks never get healthy, the circuit breaker rolls back to the last working revision.
apiweb
Size (Fargate, ARM64)512 CPU, 1024 MB512 CPU, 1024 MB
Port40003000
Rolesexecution role + task roleexecution role only
Healthcontainer check on /health, ALB check on /healthALB check on /

Both services run one task on FARGATE_SPOT: spare AWS capacity, about 70% cheaper, which AWS may take back with a 2-minute warning. Each has a 60 s health check grace period and the circuit breaker with rollback (infra/src/compute/service.ts → createApiService).

Service Connect gives the two services private names in the namespace minitickets.internal: the web server calls http://api:4000 to render /events, and the api calls http://web:3000 to clear the web cache. A small proxy in each task listens on 14000 (api) and 13000 (web).

From → toWhat happensWhat allows it
API ECS service → Public subnetsonly when computeEnabled = trueapi tasks get a private and a public IP in a public subnet
  • config networkConfiguration: subnets = public subnets, assignPublicIp: truecreateApiService
API ECS service → ECS Service Connectonly when entryEnabled = trueapi offers the name api and may call web
  • service-connect serviceConnectConfiguration: namespace minitickets.internal, discoveryName api, clientAlias port 4000, proxy port 14000createApiService
API ECS service → API security grouponly when computeEnabled = trueevery API task (and the migration task) uses the api security group
  • config networkConfiguration.securityGroups: [apiSgId]; run-migrations.sh uses the same groupcreateApiService
API target group → API ECS serviceonly when entryEnabled = truethe ALB forwards to a healthy api task on port 4000 and checks GET /health
  • sg-rule api-sg inbound rule minitickets-dev-api-in-4000: TCP 4000 from alb-sgcreateSecurityGroups
  • registration the api service registers each task IP: loadBalancers container api, port 4000; health check grace 60 screateApiService
Web ECS service → API ECS serviceonly when entryEnabled = trueserver-side rendering: the web server calls http://api:4000
  • service-connect API_INTERNAL_URL http://api:4000 → web's proxy → the api's proxy on 14000 → the api containercreateServiceConnect
  • sg-rule api-sg inbound rule minitickets-dev-api-in-14000: TCP 14000 from web-sgcreateServiceConnect
API ECS service → Web ECS serviceonly when entryEnabled = truerevalidate: POST http://web:3000/api/revalidate after an event or seat count changes
  • service-connect WEB_INTERNAL_URL http://web:3000 → the web proxy on 13000 → the web containercreateServiceConnect
  • sg-rule web-sg inbound rule minitickets-dev-web-in-13000: TCP 13000 from api-sgcreateServiceConnect
  • secret the call carries REVALIDATE_SECRET; both tasks read it from minitickets/dev/revalidate-secretrevalidateEventsPages
API ECS service → RDS Postgresonly when computeEnabled = trueSQL over TLS on port 5432
  • sg-rule db-sg inbound rule minitickets-dev-db-in-5432: TCP 5432 from api-sgcreateSecurityGroups
  • route local route 10.0.0.0/16: a public subnet reaches a private subnet without leaving the VPCcreateNetwork
  • secret DB_USER and DB_PASSWORD from the RDS master secret (<arn>:username::, <arn>:password::)createApiTask
API ECS service → ElastiCache Valkeyonly when computeEnabled = truethe Redis protocol over TLS (rediss://) on port 6379
  • sg-rule redis-sg inbound rule minitickets-dev-redis-in-6379: TCP 6379 from api-sgcreateSecurityGroups
  • route local route 10.0.0.0/16: a public subnet reaches a private subnet without leaving the VPCcreateNetwork
  • secret REDIS_URL from minitickets/dev/redis-url, which holds the Valkey auth tokencreateSecrets
API ECS service → API task definitiononly when computeEnabled = truethe service starts tasks from the newest revision
  • config taskDefinition = minitickets-dev-api:<revision>; a new revision rolls the servicecreateApiService
API ECS service → ECS clusteronly when computeEnabled = truethe api service runs in cluster minitickets-dev on FARGATE_SPOT
  • config capacityProviderStrategies: FARGATE_SPOT weight 1 (attached to the cluster)createCluster
API ECS service → API apponly when computeEnabled = trueeach api task runs the Fastify server on port 4000
API ECS service → ECR repositoriesonly when computeEnabled = trueECS pulls the api image when a task starts
  • iam api execution role: AmazonECSTaskExecutionRolePolicy (ECR pull, CloudWatch Logs write)createIamRoles
  • route public route table: 0.0.0.0/0 → minitickets-dev-igw. The task has a public IP; there is no NAT gateway and no VPC endpoint.createNetwork
  • sg-rule api-sg outbound rule minitickets-dev-api-out-all: all traffic to 0.0.0.0/0createSecurityGroups
API ECS service → CloudWatch log groupsonly when computeEnabled = truethe awslogs driver sends every api line to /minitickets/dev/api
  • iam api execution role managed policy: logs:CreateLogStream, logs:PutLogEventscreateIamRoles
  • route public route table: 0.0.0.0/0 → minitickets-dev-igw. The task has a public IP; there is no NAT gateway and no VPC endpoint.createNetwork
API ECS service → Booking-events SNS topiconly when computeEnabled = truethe outbox relay publishes each booking-confirmed event
  • iam api task role, policy send-jobs: sns:Publish on minitickets-dev-booking-events onlycreateIamRoles
  • route public route table: 0.0.0.0/0 → minitickets-dev-igw. The task has a public IP; there is no NAT gateway and no VPC endpoint.createNetwork
API ECS service → Tickets tableonly when computeEnabled = truereads one user's tickets for My tickets
  • iam api task role ReadTickets: dynamodb:Query on minitickets-dev-tickets (no Scan, no writes)createIamRoles
  • route public route table: 0.0.0.0/0 → minitickets-dev-igw. The task has a public IP; there is no NAT gateway and no VPC endpoint.createNetwork
API ECS service → Ticket assets bucketonly when computeEnabled = truesigns GET and PUT URLs, and copies a checked upload to posters/
  • iam api task role: s3:GetObject on tickets/*, posters/*, poster-uploads/*; s3:PutObject on posters/* and poster-uploads/* only with x-amz-server-side-encryption AES256createIamRoles
  • resource-policy bucket policy: Deny every s3:* request when aws:SecureTransport is falsecreateTicketStorage
CD deploy job → API ECS serviceonly when computeEnabled = truephase B pulumi up rolls the services; aws ecs wait services-stable
  • iam deploy role StackServices grants ecs:* on Resource *; PassOurRolesToEcsTasksAndLambda grants iam:PassRole on minitickets-dev-* to ECS tasks; DenyOtherRegions blocks calls outside us-east-1createCicd
API ECS service → RDS master password secretonly when computeEnabled = trueECS reads the database user and password when a task starts
  • iam api execution role, policy read-secrets: secretsmanager:GetSecretValue on this secret's ARNcreateIamRoles
  • route public route table: 0.0.0.0/0 → minitickets-dev-igw. The task has a public IP; there is no NAT gateway and no VPC endpoint.createNetwork
API ECS service → Redis URL secretonly when computeEnabled = trueECS reads REDIS_URL when a task starts
  • iam api execution role, policy read-secrets: secretsmanager:GetSecretValue on minitickets/dev/redis-urlcreateIamRoles
  • route public route table: 0.0.0.0/0 → minitickets-dev-igw. The task has a public IP; there is no NAT gateway and no VPC endpoint.createNetwork
API ECS service → Revalidate secretonly when entryEnabled = trueECS reads REVALIDATE_SECRET for the api when a task starts
  • iam api execution role, policy read-secrets: secretsmanager:GetSecretValue on minitickets/dev/revalidate-secretcreateIamRoles
  • route public route table: 0.0.0.0/0 → minitickets-dev-igw. The task has a public IP; there is no NAT gateway and no VPC endpoint.createNetwork
See the api service on the map →

ECS → Clusters → minitickets-dev → Services → minitickets-dev-api → Deployments, Events, Tasks.

SymptomCauseWhere to look
“circuit breaker triggered”, old version still runsNew tasks never got healthyService → Events
The api is down a minute, then back by itselfAWS took back the Spot capacityStopped task’s reason
SSR times out calling http://api:4000No api-sg rule for TCP 14000 from web-sgapi-sg → Inbound rules
What is the trade-off of Fargate Spot?

It is about 70% cheaper, but AWS can reclaim the capacity with two minutes’ warning. With one task, that means a short outage until the replacement is healthy. Production would run at least two tasks, partly on regular FARGATE.

Fargate or EC2 launch type?

Fargate: no servers to patch or scale, pay per task. EC2: cheaper at steady high load and more control, but we would manage the instances. For one small task each, Fargate wins.

Why do the web and api not call each other through the ALB?

In CloudFront mode the ALB accepts only requests with CloudFront’s secret header, and going out and back in would be slower. Service Connect keeps the call inside the VPC.

What does the 60-second grace period protect?

A task that is still starting fails its first health checks. Without the grace period, the service would kill it before it is ready, forever.

Advanced: Sizes, Exec and Container Insights

The api is 256 CPU / 512 MB when entry is off (no Service Connect proxy). Service Connect’s proxy shares the task’s CPU and memory, which is why both tasks moved to 512 / 1024. ECS Exec is on for the api, so ECS Exec can open a debug shell in the container; the task role carries the four ssmmessages actions it needs. Container Insights is off on the cluster to avoid extra metric costs (infra/src/compute/cluster.ts → createCluster).

Answer these without scrolling up. If one feels hard, reread that section.

  1. What is the difference between a task definition and a task?
  2. What does a service do that a task started by hand does not?
  3. What happens when new tasks never become healthy?
  4. What are the costs and risks of FARGATE_SPOT?
  5. On which ports do the Service Connect proxies listen, and who may reach each?

Redraw it from memory: cluster, task definition, service, task and target group, with the arrow for each relation

Try this: on the map, select ECS Service Connect and find the two security group rules that make it work

Read this book online: https://book.theversehub.in/