AWS Containers

Amazon ECS on Fargate: Deploy Your First Service Behind an ALB (Hands-On)

Quick take: Amazon ECS runs containers by holding a service at a desired count of tasks, each task launched from a task definition blueprint. Choose the Fargate launch type and there are no servers to patch, scale, or right-size — you declare CPU and memory per task, AWS runs it, and you pay per second of vCPU and GB. Put an Application Load Balancer in front with an IP target group, give each task its own ENI in a private subnet, and you have a production-shaped, self-healing web service. Get the execution-role-versus-task-role split or the ALB security-group chain wrong and you will watch tasks flap between PROVISIONING and STOPPED, or stare at 503 targets that never go healthy.

A team I worked with moved a small Node.js API off a single hand-built EC2 box and onto ECS Fargate on a Friday afternoon. The task definition validated. The service created. Then every task marched from PROVISIONING to STOPPED in about forty seconds, over and over, desired count never met. The engineer on point restarted the service (no change), bumped the CPU (no change), and suspected a bad image. The real cause took two minutes to find once someone ran describe-tasks and read the stoppedReason: CannotPullContainerError. They had followed best practice and placed the tasks in private subnets — but with no NAT gateway and no VPC endpoints, the task’s ENI had no route to Amazon ECR to pull the image. Not a code bug. Not a capacity bug. A networking bug, one layer below where they were looking.

This article is the hands-on, production-grade path to your first ECS Fargate service: the whole object model (cluster → service → task → container, with the task definition as the blueprint), the Fargate versus EC2 launch decision and Fargate Spot, every field of a real task definition (image, the valid Fargate CPU/memory combos, portMappings, secrets, the awslogs driver, and the crucial execution role versus task role split), the service and its ALB / target-group wiring, awsvpc networking with an ENI per task, a rolling deploy, and ECS Exec to shell into a running container. You will build the real thing with the aws CLI and Terraform, verify healthy targets, deploy a new image, and tear it all down. Because you will return to this mid-incident, the options, limits, error strings, and the playbook are laid out as scannable tables. Read the prose once; keep the tables open when the pager fires.

By the end you will stop guessing. When a task will not start you will localise it to the exact hop — image pull, secrets init, health check, security group, or the missing route to ECR — and fix it because you understand what each object is for, not by restarting the service and hoping.

What problem this solves

You have a container that runs fine on your laptop. Now it has to run in the cloud: always on, across at least two Availability Zones, replaced automatically when it crashes, reachable behind one stable DNS name, and deployable without dropping traffic. Doing that by hand — provision instances, install a runtime, wire a load balancer, write health checks, script rolling replacement, patch the OS forever — is weeks of undifferentiated work you then own on call.

Amazon Elastic Container Service (ECS) is AWS’s answer: a control plane that runs and maintains containers for you. You describe the desired state — “keep two copies of this task running, behind this load balancer, in these subnets” — and ECS makes reality match, restarting failed tasks, registering healthy ones with the ALB, and rolling out new versions. Pick the Fargate launch type and you do not manage a single server: no AMIs to bake, no cluster of EC2 instances to patch or scale, no bin-packing to worry about. You pay for the vCPU and memory your tasks actually request, per second.

What breaks without it, or with it misconfigured: you run one container on one instance and it is a single point of failure; the box dies at 3 a.m. and so does the service. You deploy a new image by SSHing in and docker pull-ing, and users get connection resets mid-request. You forget which subnet the container is in and it silently cannot reach the internet. You confuse the two IAM roles and the app gets AccessDenied calling DynamoDB even though “the role has permission.” You point the ALB health check at the wrong path and every target goes unhealthy, so the ALB returns 503 while the app is perfectly fine. Every one of these is an ECS/Fargate configuration mistake, three layers down from the symptom — and every one is in this guide.

Who hits this: essentially every team putting a web app, API, or worker into containers on AWS without wanting to run Kubernetes. It bites hardest on teams who treat ECS as a black box — they click through the console wizard, it “works,” and then they have no mental model when a task will not start or a target will not go healthy. The fix is almost never “restart.” It is “find the object that is lying — the task definition, the role, the health check, or the route to ECR — and make it tell the truth.”

To frame the field, here is the ECS object model — the objects you configure, from the outside in. Internalise this hierarchy and every setting later has an obvious home.

Object What it is Belongs to You configure Failure it owns
Cluster A logical grouping / namespace for services and tasks A region + account Name, capacity providers Wrong cluster, capacity-provider mismatch
Capacity provider Where tasks run: FARGATE, FARGATE_SPOT, or an EC2 ASG A cluster Strategy (base/weight) Spot interruption, no capacity
Task definition Immutable, versioned blueprint (family + revision) A region + account CPU/mem, containers, roles, network mode Bad image, invalid CPU/mem combo, wrong role
Container definition One container inside a task def A task definition Image, ports, env, secrets, logs, essential Pull failure, port mismatch, OOM
Service Keeps N tasks running + ALB registration + deploys A cluster Desired count, LB, subnets/SG, deploy config Stuck deploy, unhealthy targets, flapping
Task One running instance of a task definition (1+ containers) A service (or standalone) (launched from the def) Task won’t start, exits, killed by health check
ENI The elastic network interface Fargate attaches per task A task (awsvpc) Subnets, security groups, public IP No route to ECR, SG blocks ALB, no egress

Learning objectives

By the end of this article you can:

Prerequisites & where this fits

You should already have a container image that listens on a known port and exposes a health endpoint (this lab uses a tiny public nginx/httpd-style image so you can follow along even before you have your own in ECR). You should understand VPC basics — that a public ALB lives in public subnets while its targets usually sit in private subnets, and how security groups work as stateful allow-lists — covered in VPC, Subnets, Route Tables & Security Groups Explained. You should know how private subnets reach the internet, from NAT Gateway & Private-Subnet Egress Hands-On. You should be comfortable with the aws CLI v2 and a configured profile — see AWS CLI Profiles, SSO & Config Troubleshooting — and able to apply a small Terraform config. A working grasp of IAM roles and policies, from IAM Users, Groups, Roles & Policies Hands-On, is assumed.

This sits in the Containers track. For the decision of which container platform to use at all — ECS versus EKS, Fargate versus EC2 — read ECS vs EKS vs Fargate: Choosing Your Container Path; this article is the hands-on build of the ECS-on-Fargate option that guide often points you to. For where a single service grows into a system of services with per-service data and async workers, see Microservices on ECS Fargate: A Reference Architecture. For the load-balancer layer in depth — listeners, rules, target-group attributes, health-check reason codes — keep Application Load Balancer & Target Groups Hands-On nearby; this article wires an ALB to ECS but that one is the ALB reference. For the broader compute menu, see AWS Compute Compared: EC2, Lambda, ECS & EKS. Autoscaling the service, pushing images to ECR, and a focused “task fails to start” playbook are companion topics in this same wave — referenced by name where they matter below.

Here is where a first ECS Fargate service fits in the build order, so you know what must exist before each step.

Building block Must exist first Provided by This article
VPC + 2 public + 2 private subnets (2 AZs) Account, region Your networking baseline Assumed; created in lab
NAT gateway or VPC endpoints Private subnets Egress design Explained; both shown
A container image A Dockerfile ECR push (companion) Uses a public image
Execution role + task role IAM This + IAM article Created in lab
ALB + IP target group + listener VPC, subnets, SG ALB article Created in lab
ECS cluster Region This article Created in lab
Task definition Roles, log group, image This article Created in lab
Service All of the above This article Created in lab

Core concepts

ECS has a small vocabulary, but the words are precise and people mix them up constantly. Nail these and everything else clicks.

A cluster is just a namespace — a logical boundary that holds services and tasks. On Fargate the cluster has no servers in it; it is a label plus a set of capacity providers (where tasks may run). A task definition is an immutable, versioned document (family:revision, e.g. kv-web:3) describing one or more containers, the task-level CPU and memory, the network mode, the two IAM roles, and volumes. You never “edit” a task definition — you register a new revision. A task is one running realisation of a task definition: on Fargate that means a micro-VM with an ENI and one or more containers. A service is a controller that keeps a desired count of tasks running, replaces failed ones, registers healthy task IPs with an ALB target group, and orchestrates deployments when you change the task definition.

The single most-confused pair is the two IAM roles. The execution role is assumed by the ECS/Fargate agent — the infrastructure — to do things on your behalf before and around your container: pull the image from ECR, fetch secrets, and write to CloudWatch Logs. The task role is assumed by your application code inside the container to call AWS APIs at runtime — read an S3 object, write to DynamoDB, publish to SNS. Wrong role, wrong failure: an execution-role problem stops the task from starting; a task-role problem shows up as AccessDenied while the app runs.

Here is the core vocabulary in one place.

Term One-line definition Mutable? Scope You touch it when
Cluster Namespace for services/tasks + capacity providers Yes Region/account Grouping workloads, setting Spot strategy
Task definition Versioned blueprint (family:revision) No (new revision) Region/account Changing image, CPU/mem, env, roles
Container definition One container inside a task def No (via new revision) Task definition Ports, image, essential, logs, secrets
Task A running instance of a task def Runtime Cluster Debugging, ECS Exec, reading stoppedReason
Service Keeps desired count + LB + deploys Yes Cluster Scaling, deploys, ALB wiring
Desired count How many tasks the service holds Yes Service Manual scale / autoscaling target
Execution role Agent role: pull image, secrets, logs Yes Task def Pull/secrets/log failures at start
Task role App role: runtime AWS API calls Yes Task def App AccessDenied at runtime
Capacity provider Where tasks run (FARGATE / FARGATE_SPOT / EC2) Yes Cluster/service Spot mix, guaranteed base
ENI Per-task network interface (awsvpc) Runtime Task Routing, SG, public IP, endpoints

Fargate supports exactly one network modeawsvpc — so every Fargate task is a first-class network citizen with its own private IP. The other modes exist only on the EC2 launch type; know them so you recognise them in EC2 task definitions.

networkMode What each task gets Launch types Notes
awsvpc Own ENI + private IP + own SGs Fargate and EC2 Required on Fargate; target group must be ip
bridge Docker bridge, dynamic host ports EC2 only Classic; needs dynamic port mapping to ALB
host Shares the host’s network stack EC2 only Fast, but port conflicts; no SG per task
none No external networking EC2 only Batch/utility tasks with no inbound/outbound

And the launch decision that everything else hangs off — where do your tasks actually run?

Launch option What you manage Billing Best for
Fargate Nothing below the task Per-second vCPU + GB Web/API/workers, small ops teams, spiky load
Fargate Spot Nothing; tolerate interruption ~70% off, interruptible Stateless, fault-tolerant, batch, dev/test
EC2 The instances (patch, scale, AMI) EC2 price (RI/SP/Spot possible) High density, GPU, daemonsets, cost at scale
External (ECS Anywhere) Your own on-prem/other-cloud hosts Per managed instance/hour Hybrid, run ECS control on your hardware

Fargate vs EC2 launch type (and Fargate Spot)

This is the first real decision, and for a first service the answer is almost always Fargate. The value proposition is that there is no host: nothing to patch, no CVE scramble on the node OS, no Auto Scaling group of instances to size, no bin-packing tasks onto instances. You request cpu and memory at the task level, Fargate finds capacity, and you are billed per second (one-minute minimum) for exactly that vCPU and memory from image-pull start to task stop. The trade is cost-at-scale and control: at steady high utilisation, a well-packed EC2 fleet with Savings Plans is cheaper per vCPU, and only EC2 gives you GPUs, privileged containers, host networking, DaemonSet-style scheduling, and per-instance tuning.

Decide per dimension, not by vibe.

Dimension Fargate EC2 launch type Who wins
Server patching AWS patches the runtime You patch the AMI/OS Fargate
Capacity/scaling No nodes; task scales directly Scale ASG and service Fargate
Billing model Per-second vCPU + GB requested Per-instance (pay for idle) Fargate (spiky), EC2 (steady)
Cost at high steady load Pay per task, no packing Bin-pack many tasks/instance EC2
GPU / accelerators Not supported Supported (g/p families) EC2
DaemonSet (one/host) Not supported (no hosts) DAEMON scheduling strategy EC2
Privileged / host network Not supported Supported EC2
Task density / very small tasks Min 0.25 vCPU/task Many tiny tasks per instance EC2
Ops burden Lowest Highest Fargate
Startup granularity Per task Per task (+ instance if scaling out) Fargate
Local NVMe / instance store Ephemeral 20–200 GB only Full instance store / EBS EC2

Fargate Spot runs the same Fargate task on spare capacity for roughly a 70% discount, with the catch that AWS can reclaim it. On interruption, ECS emits a task state-change event, sends SIGTERM to your containers, waits stopTimeout (default 30s, up to 120s), then SIGKILL. Use it where a task disappearing is a non-event: stateless web tiers with enough replicas, background/batch workers, CI, and non-prod. Do not put your only replica, a stateful singleton, or a latency-critical path solely on Spot.

Aspect Fargate (On-Demand) Fargate Spot
Price Full per-second rate ~70% cheaper (varies)
Interruption None (you stop it) AWS may reclaim capacity
Warning n/a SIGTERM → wait stopTimeoutSIGKILL
Handle it with n/a Graceful shutdown, ≥2 replicas, retries
Safe for Anything Stateless, fault-tolerant, batch, dev
Avoid for Stateful singletons, sole replica, hard-latency
Windows containers Supported Not supported on Spot

You blend the two with a capacity provider strategy on the cluster or service: a base guarantees a floor of On-Demand tasks, and weight sets the ratio for everything above the base.

Field Meaning Example Effect
capacityProvider FARGATE or FARGATE_SPOT FARGATE Names the pool
base Minimum tasks placed here first base=1 on FARGATE 1 task always On-Demand
weight Relative share above the base FARGATE=1, FARGATE_SPOT=3 3 Spot : 1 On-Demand thereafter
(result) Blended placement desired=5 1 guaranteed + 4 split 3:1

Fargate also has a platform version; LATEST currently resolves to 1.4.0, which you want.

Platform version Notable capabilities Use it for
1.4.0 / LATEST Configurable ephemeral storage (20–200 GB), ECS Exec (SSM agent), 8/16 vCPU sizes, network metrics Everything new
1.3.0 Legacy; 20 GB fixed ephemeral, no ECS Exec Only if pinned by an old service

The task definition — your container blueprint

The task definition is where most of the real configuration lives. It is a JSON document you register; ECS stores it as family:revision. Get the task-level cpu/memory, the container’s image/portMappings/logConfiguration, and the two role ARNs right, and the rest is refinement.

Task-level fields

Field What it sets Fargate requirement / note
family The name; revisions increment under it Required; e.g. kv-web
requiresCompatibilities Launch types this def supports ["FARGATE"]
networkMode Network mode Must be awsvpc
cpu Task-level vCPU (in CPU units) Required on Fargate; valid combo only
memory Task-level memory (MiB) Required on Fargate; valid combo only
executionRoleArn Agent role (pull/secrets/logs) Needed if using ECR private/secrets/awslogs
taskRoleArn App role (runtime AWS calls) Optional but usually set
runtimePlatform OS + CPU arch (LINUX, X86_64/ARM64) Set ARM64 for Graviton savings
ephemeralStorage.sizeInGiB Scratch disk (21–200) Default 20 GB; 1.4.0+ to raise
containerDefinitions The array of containers ≥1; exactly one usually essential
volumes EFS / ephemeral volume defs No host bind mounts on Fargate

The number that trips up every newcomer: on Fargate, cpu and memory are not free-form — they must be one of a fixed set of valid combinations. Pick the CPU size, then memory must fall in that CPU’s allowed range.

Task cpu vCPU Allowed memory (MiB)
256 0.25 512, 1024, 2048
512 0.5 1024 – 4096 (1 GB steps)
1024 1 2048 – 8192 (1 GB steps)
2048 2 4096 – 16384 (1 GB steps)
4096 4 8192 – 30720 (1 GB steps)
8192 8 16384 – 61440 (4 GB steps) · needs PV 1.4.0
16384 16 32768 – 122880 (8 GB steps) · needs PV 1.4.0

Register a memory value outside the range for its CPU and register-task-definition fails with ClientException: No Fargate configuration exists for given values.

Container definition fields

Inside containerDefinitions, each container is its own object. Enumerate the ones you will actually set.

Field What it does Gotcha
name Container name (used by LB mapping, Exec) Must match --load-balancers containerName
image Image URI (ECR or public) Use an immutable tag/digest, not :latest
essential If true, its exit stops the whole task Exactly one primary should be essential
portMappings Ports the container exposes containerPort must match health check + LB
cpu / memory / memoryReservation Optional container-level limits Sum ≤ task-level; memory is a hard cap (OOM)
environment Plain-text env vars Never put secrets here (visible in describe)
secrets Env vars pulled from Secrets Mgr / SSM Needs execution-role perms + endpoint/NAT
environmentFiles Env from an S3 .env file Execution role needs s3:GetObject
logConfiguration Log driver + options awslogs for CloudWatch (below)
healthCheck Container-level health command Different from ALB health check
command / entryPoint Override image CMD/ENTRYPOINT Bad value → CannotStartContainerError
dependsOn Ordering vs other containers For sidecars (START, HEALTHY)
ulimits / linuxParameters nofile, initProcessEnabled, caps Limited capability set on Fargate
readonlyRootFilesystem Immutable root FS Good hardening; needs writable tmpfs volume
stopTimeout Grace before SIGKILL on stop Default 30s, max 120s (Fargate)

portMappings is small but load-bearing — the containerPort has to line up with your health check and your load-balancer mapping or the target will never go healthy.

portMappings field Meaning Fargate note
containerPort Port the app listens on The one the ALB targets (e.g. 8080)
hostPort Host port On awsvpc, equals containerPort
protocol tcp / udp tcp for HTTP apps
name Named port (Service Connect) Optional; enables service discovery
appProtocol http/http2/grpc Hints for Service Connect

environment vs secrets vs environmentFiles

Configuration comes in three shapes; the difference is who can read the value and where it lives.

Mechanism Source Visible in describe-task-definition? Use for
environment Inline plaintext Yes (anyone with describe) Non-secret config: LOG_LEVEL, PORT
secrets Secrets Manager / SSM Parameter Store No (only ARN shown) Passwords, API keys, DB creds
environmentFiles S3 .env object Bucket/key shown, not values Bulk non-secret config

Both secrets and private ECR images are fetched by the execution role before your app runs, so a permission or routing gap there fails the task at start with ResourceInitializationError or CannotPullContainerError, not at runtime.

The awslogs driver

To see your container’s stdout/stderr in CloudWatch, set logConfiguration.logDriver to awslogs. The execution role needs logs:CreateLogStream and logs:PutLogEvents (plus logs:CreateLogGroup if you auto-create).

"logConfiguration": {
  "logDriver": "awslogs",
  "options": {
    "awslogs-group": "/ecs/kv-web",
    "awslogs-region": "ap-south-1",
    "awslogs-stream-prefix": "web",
    "awslogs-create-group": "true"
  }
}
awslogs option Purpose Note
awslogs-group Target log group Create it or set awslogs-create-group
awslogs-region Region of the log group Usually the task’s region
awslogs-stream-prefix Prefix; stream = prefix/container/taskID Omit and streams are unnamed/confusing — always set
awslogs-create-group Auto-create the group Needs logs:CreateLogGroup on exec role
mode blocking (default) or non-blocking non-blocking drops logs under pressure, never blocks app
max-buffer-size Buffer for non-blocking Default 1 MB

Fargate supports a subset of log drivers; know which are available.

Log driver On Fargate? Use for
awslogs Yes CloudWatch Logs (default choice)
awsfirelens Yes Route to Fluent Bit → Kinesis/OpenSearch/3rd-party
splunk Yes Direct to Splunk HEC
json-file / syslog / journald No EC2 launch type only

Execution role vs task role — the #1 confusion

Two roles, two very different jobs. Memorise this table; it prevents more wasted hours than any other in the article.

Aspect Execution role Task role
Assumed by ECS/Fargate agent (infrastructure) Your application code in the container
When Before/around container start While the app runs
Typical actions Pull ECR image, fetch secrets, write logs s3:GetObject, dynamodb:PutItem, sns:Publish
Managed policy AmazonECSTaskExecutionRolePolicy None — you write least-privilege
Failure looks like Task won’t start: pull/secrets/log error App AccessDenied at runtime
Set via executionRoleArn taskRoleArn
Needed if Private ECR, secrets, or awslogs App calls any AWS API

The managed AmazonECSTaskExecutionRolePolicy covers the common execution-role needs; you extend it when you use secrets (add secretsmanager:GetSecretValue and the KMS kms:Decrypt on the key) or environmentFiles (add s3:GetObject).

Execution-role permission Why From
ecr:GetAuthorizationToken Auth to ECR Managed policy
ecr:BatchGetImage, ecr:GetDownloadUrlForLayer Pull image layers Managed policy
logs:CreateLogStream, logs:PutLogEvents awslogs driver Managed policy
logs:CreateLogGroup Auto-create log group Add if awslogs-create-group=true
secretsmanager:GetSecretValue Fetch secrets Add per secret ARN
ssm:GetParameters Fetch SSM secrets Add per parameter ARN
kms:Decrypt Decrypt a CMK-encrypted secret Add for the KMS key
s3:GetObject environmentFiles Add per object

The service — desired state, ALB integration, deployments

A service is the controller that turns “I want two of these” into reality and keeps it that way. It owns the desired count, the network configuration (subnets + security group for the task ENIs), the load-balancer registration, and the deployment behaviour when the task definition changes.

Service parameters

Parameter What it controls Typical value
desiredCount How many tasks to keep running 2 (spread over 2 AZs)
launchType / capacityProviderStrategy Where tasks run FARGATE or a Spot mix
platformVersion Fargate platform version LATEST
networkConfiguration.awsvpcConfiguration Subnets, SGs, public IP 2 private subnets, task SG
loadBalancers Target group + container + port TG ARN, web, 8080
healthCheckGracePeriodSeconds Ignore ELB health checks after start ≥ app cold-start (e.g. 60)
deploymentConfiguration Rolling knobs + circuit breaker min 100 / max 200 + breaker
deploymentController ECS / CODE_DEPLOY / EXTERNAL ECS (rolling)
enableExecuteCommand Turns on ECS Exec true for debuggable services
schedulingStrategy REPLICA / DAEMON REPLICA (DAEMON is EC2-only)
propagateTags Copy tags to tasks SERVICE or TASK_DEFINITION
enableECSManagedTags AWS-managed cost tags true

Rolling deployments: minimum-healthy and maximum-percent

With the default ECS deployment controller, changing the task definition triggers a rolling update governed by two percentages of desiredCount. minimumHealthyPercent is the floor of healthy tasks ECS must keep during the deploy; maximumPercent is the ceiling of total tasks it may run. For a service behind a load balancer the defaults are 100% / 200% — ECS spins up replacements before draining the old ones, so capacity never dips.

desired min% max% Behaviour during deploy
4 100 200 Up to 8 tasks; never <4 healthy; add-then-drain (zero-downtime)
4 50 100 Drain 2 first, then add 2; capacity dips to 50%
2 100 200 Up to 4; safest for small services
1 0 100 Stop the 1, then start 1 — downtime on a single-task service

The deployment circuit breaker

Turn this on. Without it, a bad revision whose tasks never go healthy leaves the service stuck deploying forever, quietly retrying while the old tasks keep serving. The deployment circuit breaker watches a new deployment, and after enough consecutive failed tasks it declares the deployment failed — and, if rollback: true, automatically rolls back to the last good revision.

Setting Values Effect
deploymentCircuitBreaker.enable true / false Detect a failing deployment and stop it
deploymentCircuitBreaker.rollback true / false On failure, auto-revert to last good task def
Rollout state IN_PROGRESSCOMPLETED / FAILED Read via describe-services

Deployment controller types

Controller How it deploys Use when
ECS Rolling update (min/max %) Default; simplest
CODE_DEPLOY Blue/green (shift traffic, then cut) Need instant rollback, canary/linear traffic shift
EXTERNAL You drive deployments via API Custom tooling / third-party controllers

Wiring the ALB target group

Because Fargate uses awsvpc, each task has its own IP, so the target group must be ip type — ECS registers and deregisters the task ENIs’ IPs automatically as it launches and drains tasks. An instance-type target group cannot work with Fargate.

Target-group setting Value for ECS Fargate Why
TargetType ip awsvpc tasks register by IP, not instance
Protocol / Port HTTP / 8080 Must match containerPort
HealthCheckPath /healthz (your endpoint) Wrong path → all unhealthy
Matcher 200 (or your codes) 404/301 from app fails the check
HealthCheckIntervalSeconds 1530 Balance speed vs noise
HealthyThresholdCount 23 Consecutive OKs to mark healthy
DeregistrationDelay 3060s Drain in-flight requests on deploy

awsvpc networking — an ENI per task

On Fargate, every task gets its own ENI with a private IP in one of the subnets you list, and the security groups you attach apply to that task, not to a shared host. This is powerful — per-task IPs and per-task security groups — but it means the task’s connectivity is entirely determined by the subnet’s route table, the assignPublicIp flag, and the security groups. Two things must work: the ALB must be able to reach the task on the container port (inbound), and the task must be able to reach ECR, CloudWatch Logs, and Secrets Manager to start (outbound).

awsvpcConfiguration

Field Values Meaning
subnets 2+ subnet IDs Where task ENIs are placed (span 2 AZs)
securityGroups 1+ SG IDs SGs applied to the task ENI
assignPublicIp ENABLED / DISABLED Give the ENI a public IP (public subnets only)

Where does the task get internet? assignPublicIp and egress

The most common first-service failure is a task in a private subnet that cannot pull its image because it has no route out. There are three valid egress designs; pick one deliberately.

Placement assignPublicIp Egress path to ECR/logs Notes
Public subnet ENABLED Public IP → Internet Gateway Simplest; task is publicly addressable (still gate with SG)
Public subnet DISABLED None → pull fails Broken combo: IGW needs a public IP; don’t do this
Private subnet DISABLED NAT gateway → IGW Standard prod; costs NAT + per-GB
Private subnet DISABLED VPC endpoints (no NAT) Cheapest at scale; needs the right endpoints

If you go private-without-NAT (recommended for cost and isolation), you need these VPC endpoints so AWS-service traffic never touches the internet.

Endpoint Type Why the task needs it
com.amazonaws.<region>.ecr.api Interface ECR auth / API
com.amazonaws.<region>.ecr.dkr Interface Pull image manifest/config
com.amazonaws.<region>.s3 Gateway Image layers live in S3
com.amazonaws.<region>.logs Interface awslogs → CloudWatch Logs
com.amazonaws.<region>.secretsmanager Interface secrets (if used)
com.amazonaws.<region>.ssmmessages Interface ECS Exec control channel

The security-group chain

Two security groups, one referencing the other. The ALB’s SG allows the world on 80/443; the task’s SG allows only the ALB SG on the container port, and allows egress on 443 for the pull.

SG Inbound Outbound
ALB SG (sg-alb) 80/443 from 0.0.0.0/0 All (to targets)
Task SG (sg-task) 8080 from sg-alb (source = SG, not CIDR) 443 to 0.0.0.0/0 or endpoints

Referencing sg-alb as the source on the task SG is the correct pattern — it keeps working as ALB nodes move IPs across AZs, and it means only the load balancer can reach your tasks.

ENI / density fact Value Note
ENIs per Fargate task 1 One ENI, one private IP
Network mode awsvpc only No bridge/host on Fargate
Public IP Optional per task assignPublicIp
SG scope Per task Not shared with a host
Cross-AZ spread Via multiple subnets List 1 subnet per AZ

Scaling and ECS Exec

Scaling, in brief

A first service usually runs a fixed desiredCount. When you outgrow that, Application Auto Scaling adjusts desiredCount on a signal — target-tracking on CPU, memory, or (best for web) ALBRequestCountPerTarget. That is a topic in its own right and has a dedicated companion deep-dive on ECS Service Auto Scaling in this wave; the pointer table below is enough to know what exists.

Scaling signal Metric Good for Watch out
CPU target tracking ECSServiceAverageCPUUtilization CPU-bound apps Lags spiky traffic
Memory target tracking ECSServiceAverageMemoryUtilization Memory-bound apps Leaks look like load
Requests per target ALBRequestCountPerTarget Web/API front ends Best proxy for user load
Scheduled scaling Cron Predictable peaks Not reactive
Step scaling on custom Any CloudWatch metric Queue depth, custom KPIs More tuning

ECS Exec — shell into a running task

ECS Exec lets you open an interactive shell in a running Fargate container without SSH, a bastion, or a public IP — it tunnels through AWS Systems Manager. It is the single best debugging tool for “the task is running but behaving oddly.”

Requirement Detail
Service/run flag enableExecuteCommand (service) or --enable-execute-command (run-task)
Platform version 1.4.0+ (SSM agent bundled)
Task role perms ssmmessages:CreateControlChannel, CreateDataChannel, OpenControlChannel, OpenDataChannel
Networking (private) ssmmessages VPC endpoint (or NAT)
Local tooling AWS CLI v2 + Session Manager plugin
Container A shell present (/bin/sh or /bin/bash)
aws ecs execute-command \
  --cluster kv-demo --task <task-id> \
  --container web --interactive --command "/bin/sh"

Note that ECS Exec permissions live on the task role, not the execution role — one more reason to keep the two straight.

Architecture at a glance

The diagram below is the exact system you build in the lab, read left to right along the request path. A client hits the ALB on :443 in the public subnets; the ALB’s listener forwards to an IP target group whose members are the private IPs of your Fargate tasks. Each task runs in a private subnet with its own ENI and the task security group, which allows inbound only from the ALB’s security group on :8080. To start, each task’s execution role pulls the image from Amazon ECR (via NAT or VPC endpoints) and streams stdout to CloudWatch Logs through the awslogs driver. The numbered badges mark the six hops where a first service most often breaks — image pull, secrets/logs init, the ALB→task security group, the health check, egress, and ECS Exec — each narrated in the legend as symptom · confirm · fix.

Client to ALB in public subnets, forwarding over an IP target group to ECS Fargate tasks each with its own ENI in two private subnets, pulling an image from Amazon ECR and streaming logs to CloudWatch, with numbered failure badges on the pull, secrets, security-group, health-check, egress and ECS Exec hops

Real-world scenario

GroceRun, a Bengaluru grocery-delivery startup, ran its order API as a single hand-managed t3.medium EC2 instance: one docker run, deploys by SSH-and-pull, and a health check that was really just “is the box pingable.” It survived until a Diwali promo. The instance’s disk filled with old images, the container was OOM-killed under load, and because there was exactly one of it, the API went dark for eleven minutes during peak checkout. The post-incident action was blunt: no more single boxes, no more manual deploys.

The platform engineer chose ECS on Fargate for the rebuild — the team was four people and had no appetite to run Kubernetes or patch nodes. The target design was two tasks at 0.5 vCPU / 1 GB across two AZs, behind an internet-facing ALB, tasks in private subnets, images in ECR, logs in CloudWatch. The build went smoothly until the service came up and every task cycled PROVISIONING → STOPPED. describe-tasks showed CannotPullContainerError: the private subnets had no NAT gateway yet and no VPC endpoints, so the task ENIs had no route to ECR. Rather than pay for a NAT gateway for what was almost entirely AWS-internal traffic, they added the four endpoints — ecr.api, ecr.dkr, the S3 gateway endpoint (for the image layers), and logs — and the tasks pulled and started within a minute.

The next wall was 503 from the ALB with all targets “unhealthy.” The app booted in about 25 seconds (JIT warm-up plus a schema check), but the target group started failing it immediately and the service kept killing “unhealthy” tasks before they finished booting. The fix was two settings: healthCheckGracePeriodSeconds = 60 so ECS ignored ELB health checks during boot, and a real /healthz endpoint returning 200 (the default matcher had been hitting /, which returned a 302 redirect). Targets went healthy and stayed.

They turned on the deployment circuit breaker with rollback: true before the first real deploy — and it earned its keep a week later when a config typo shipped an image that crash-looped: ECS detected the failing deployment, stopped it, and rolled back to the previous revision automatically, with the old tasks never having stopped serving. For the promo weekend they added a Fargate Spot capacity provider with base: 2 On-Demand (their steady floor) and Spot for burst, cutting the peak compute bill by roughly 55% while keeping two guaranteed replicas. The eleven-minute outage did not recur; the next promo scaled from 2 to 9 tasks on ALBRequestCountPerTarget and back down, untouched by a human.

Advantages and disadvantages

Advantages Disadvantages
No servers to patch, scale, or bin-pack Per-vCPU cost higher than a packed EC2 fleet at steady high load
Per-second billing for exactly what you request No GPU, privileged mode, host networking, or DaemonSet
Per-task ENI + per-task security group (strong isolation) Each task needs a route to ECR/logs (NAT or endpoints)
Self-healing: failed tasks replaced automatically Cold-start of a task is slower than reusing a warm EC2 host
Zero-downtime rolling deploys + circuit-breaker rollback Ephemeral storage only (20–200 GB); no instance store
ECS Exec debugging without SSH/bastions Min task size 0.25 vCPU — many tiny tasks are cheaper on EC2
Simplest managed-container option on AWS (no K8s) Less low-level control than EC2 or EKS

Fargate wins decisively for a first service and for most web/API/worker workloads run by small teams: the operational savings dwarf the per-vCPU premium until you are running a large, steadily-utilised fleet. Reach for the EC2 launch type when you need GPUs, run at high steady utilisation where bin-packing and Savings Plans make instances cheaper, or need daemonset-style agents on every host. The disadvantages that bite first-service teams are almost always the networking ones — the route to ECR and the ALB→task security group — which is exactly why the lab and playbook below dwell on them.

Hands-on lab

You will register a task definition, create an ALB with an IP target group, run a Fargate service of two tasks across two private subnets behind the ALB, verify healthy targets and hit the ALB, do a rolling deploy, and shell in with ECS Exec — then tear it all down. Both the aws CLI and Terraform are shown. ⚠️ Cost note: Fargate has no perpetual free tier; two 0.5 vCPU / 1 GB tasks plus an ALB cost a few rupees per hour. A NAT gateway (if you choose it over endpoints) adds ~₹4/hour plus per-GB. Tear down when done.

Lab variables

Variable Example Notes
Region ap-south-1 Mumbai
VPC vpc-0abc... Has 2 public + 2 private subnets
Public subnets subnet-pub-a, subnet-pub-b For the ALB
Private subnets subnet-prv-a, subnet-prv-b For the tasks
Image public.ecr.aws/nginx/nginx:stable Public; no ECR auth needed
Container port 80 nginx default
Health path / nginx returns 200 at /

This lab uses a public image and container port 80 so you can run it without first pushing to ECR. A companion hands-on in this wave walks through creating an ECR repository and pushing your own image; when you switch to a private ECR image, the execution role needs the ECR permissions in the table above and the tasks need a route to ECR (NAT or endpoints).

Step 1 — Create the cluster and log group

aws ecs create-cluster --cluster-name kv-demo \
  --capacity-providers FARGATE FARGATE_SPOT \
  --region ap-south-1

aws logs create-log-group --log-group-name /ecs/kv-web --region ap-south-1

Expected: JSON with "status": "ACTIVE" for the cluster.

Step 2 — Create the two IAM roles

# Trust policy so ECS tasks can assume the roles
cat > trust.json <<'EOF'
{ "Version": "2012-10-17", "Statement": [
  { "Effect": "Allow", "Principal": { "Service": "ecs-tasks.amazonaws.com" }, "Action": "sts:AssumeRole" } ] }
EOF

aws iam create-role --role-name kvEcsExecutionRole \
  --assume-role-policy-document file://trust.json
aws iam attach-role-policy --role-name kvEcsExecutionRole \
  --policy-arn arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy

aws iam create-role --role-name kvEcsTaskRole \
  --assume-role-policy-document file://trust.json
# Task role: add app permissions + ECS Exec (ssmmessages) as needed

Step 3 — Register the task definition

cat > taskdef.json <<'EOF'
{
  "family": "kv-web",
  "requiresCompatibilities": ["FARGATE"],
  "networkMode": "awsvpc",
  "cpu": "512",
  "memory": "1024",
  "executionRoleArn": "arn:aws:iam::123456789012:role/kvEcsExecutionRole",
  "taskRoleArn": "arn:aws:iam::123456789012:role/kvEcsTaskRole",
  "runtimePlatform": { "operatingSystemFamily": "LINUX", "cpuArchitecture": "X86_64" },
  "containerDefinitions": [
    {
      "name": "web",
      "image": "public.ecr.aws/nginx/nginx:stable",
      "essential": true,
      "portMappings": [ { "containerPort": 80, "protocol": "tcp" } ],
      "environment": [ { "name": "LOG_LEVEL", "value": "info" } ],
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-group": "/ecs/kv-web",
          "awslogs-region": "ap-south-1",
          "awslogs-stream-prefix": "web"
        }
      }
    }
  ]
}
EOF

aws ecs register-task-definition --cli-input-json file://taskdef.json

Expected: "status": "ACTIVE" and a "revision": 1 — your blueprint is now kv-web:1.

Step 4 — Security groups

SG_ALB=$(aws ec2 create-security-group --group-name kv-alb-sg \
  --description "ALB" --vpc-id vpc-0abc --query GroupId --output text)
SG_TASK=$(aws ec2 create-security-group --group-name kv-task-sg \
  --description "ECS tasks" --vpc-id vpc-0abc --query GroupId --output text)

aws ec2 authorize-security-group-ingress --group-id $SG_ALB \
  --protocol tcp --port 80 --cidr 0.0.0.0/0
# Task SG: allow ONLY the ALB SG on the container port
aws ec2 authorize-security-group-ingress --group-id $SG_TASK \
  --protocol tcp --port 80 --source-group $SG_ALB

Step 5 — ALB, IP target group, listener

ALB_ARN=$(aws elbv2 create-load-balancer --name kv-demo-alb --type application \
  --scheme internet-facing --subnets subnet-pub-a subnet-pub-b \
  --security-groups $SG_ALB --query 'LoadBalancers[0].LoadBalancerArn' --output text)

TG_ARN=$(aws elbv2 create-target-group --name kv-demo-tg \
  --protocol HTTP --port 80 --vpc-id vpc-0abc \
  --target-type ip --health-check-path / --matcher HttpCode=200 \
  --query 'TargetGroups[0].TargetGroupArn' --output text)

aws elbv2 create-listener --load-balancer-arn $ALB_ARN \
  --protocol HTTP --port 80 \
  --default-actions Type=forward,TargetGroupArn=$TG_ARN

The --target-type ip is the critical flag — an instance target group cannot register Fargate tasks.

Step 6 — Create the Fargate service

aws ecs create-service \
  --cluster kv-demo \
  --service-name kv-web \
  --task-definition kv-web:1 \
  --desired-count 2 \
  --launch-type FARGATE \
  --platform-version LATEST \
  --network-configuration "awsvpcConfiguration={subnets=[subnet-prv-a,subnet-prv-b],securityGroups=[$SG_TASK],assignPublicIp=DISABLED}" \
  --load-balancers "targetGroupArn=$TG_ARN,containerName=web,containerPort=80" \
  --health-check-grace-period-seconds 60 \
  --deployment-configuration "deploymentCircuitBreaker={enable=true,rollback=true},minimumHealthyPercent=100,maximumPercent=200" \
  --enable-execute-command

assignPublicIp=DISABLED in private subnets means the tasks need a route to ECR/logs. The public nginx image is pulled from a public ECR registry, so you still need egress — either a NAT gateway on the private subnets or the VPC endpoints listed earlier. If tasks stick at PROVISIONING, that route is missing (see the playbook).

Step 7 — Verify

# Wait until the service reaches steady state
aws ecs wait services-stable --cluster kv-demo --services kv-web

# Targets should be "healthy"
aws elbv2 describe-target-health --target-group-arn $TG_ARN \
  --query 'TargetHealthDescriptions[].TargetHealth.State'

# Hit the ALB
ALB_DNS=$(aws elbv2 describe-load-balancers --load-balancer-arns $ALB_ARN \
  --query 'LoadBalancers[0].DNSName' --output text)
curl -s -o /dev/null -w "%{http_code}\n" http://$ALB_DNS/

Expected: two "healthy" states and 200 from curl.

Step 8 — Rolling deploy of a new image

Register a new revision (e.g. bump to nginx:mainline or your own tag), then force a rolling deployment:

# after editing taskdef.json image → registers kv-web:2
aws ecs register-task-definition --cli-input-json file://taskdef.json

aws ecs update-service --cluster kv-demo --service kv-web \
  --task-definition kv-web:2

# Watch the rollout
aws ecs describe-services --cluster kv-demo --services kv-web \
  --query 'services[0].deployments[].{status:status,rollout:rolloutState,running:runningCount,desired:desiredCount}'

Expected: a PRIMARY deployment moving to rolloutState: COMPLETED, old tasks draining after new ones go healthy — no dip below two healthy targets.

Step 9 — ECS Exec into a task

TASK=$(aws ecs list-tasks --cluster kv-demo --service-name kv-web \
  --query 'taskArns[0]' --output text)

aws ecs execute-command --cluster kv-demo --task $TASK \
  --container web --interactive --command "/bin/sh"
# then, inside the container:  wget -qO- localhost:80 | head

If this errors, the task role lacks ssmmessages:*, enableExecuteCommand was off, or (private subnets) the ssmmessages endpoint is missing.

Step 10 — Teardown

# Resource Command Billable if left?
1 Service (scale to 0 first) aws ecs update-service --cluster kv-demo --service kv-web --desired-count 0 then delete-service --force Yes (Fargate)
2 Listener + target group aws elbv2 delete-listener / delete-target-group Small
3 Load balancer aws elbv2 delete-load-balancer --load-balancer-arn $ALB_ARN Yes (hourly + LCU)
4 NAT gateway (if created) aws ec2 delete-nat-gateway Yes (hourly + per-GB)
5 VPC endpoints (if created) aws ec2 delete-vpc-endpoints Interface endpoints hourly
6 Cluster aws ecs delete-cluster --cluster kv-demo No
7 Log group aws logs delete-log-group --log-group-name /ecs/kv-web Storage only
8 Security groups / roles delete-security-group / delete-role No

The same thing in Terraform

resource "aws_ecs_cluster" "demo" {
  name = "kv-demo"
}

resource "aws_cloudwatch_log_group" "web" {
  name              = "/ecs/kv-web"
  retention_in_days = 30
}

resource "aws_iam_role" "exec" {
  name               = "kvEcsExecutionRole"
  assume_role_policy = data.aws_iam_policy_document.ecs_assume.json
}
resource "aws_iam_role_policy_attachment" "exec" {
  role       = aws_iam_role.exec.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
}
resource "aws_iam_role" "task" {
  name               = "kvEcsTaskRole"
  assume_role_policy = data.aws_iam_policy_document.ecs_assume.json
}

data "aws_iam_policy_document" "ecs_assume" {
  statement {
    actions = ["sts:AssumeRole"]
    principals {
      type        = "Service"
      identifiers = ["ecs-tasks.amazonaws.com"]
    }
  }
}

resource "aws_ecs_task_definition" "web" {
  family                   = "kv-web"
  requires_compatibilities = ["FARGATE"]
  network_mode             = "awsvpc"
  cpu                      = "512"
  memory                   = "1024"
  execution_role_arn       = aws_iam_role.exec.arn
  task_role_arn            = aws_iam_role.task.arn

  container_definitions = jsonencode([{
    name      = "web"
    image     = "public.ecr.aws/nginx/nginx:stable"
    essential = true
    portMappings = [{ containerPort = 80, protocol = "tcp" }]
    logConfiguration = {
      logDriver = "awslogs"
      options = {
        "awslogs-group"         = aws_cloudwatch_log_group.web.name
        "awslogs-region"        = "ap-south-1"
        "awslogs-stream-prefix" = "web"
      }
    }
  }])
}

resource "aws_lb" "alb" {
  name               = "kv-demo-alb"
  load_balancer_type = "application"
  subnets            = [var.public_a, var.public_b]
  security_groups    = [aws_security_group.alb.id]
}

resource "aws_lb_target_group" "tg" {
  name        = "kv-demo-tg"
  port        = 80
  protocol    = "HTTP"
  vpc_id      = var.vpc_id
  target_type = "ip"                # REQUIRED for Fargate/awsvpc
  health_check {
    path    = "/"
    matcher = "200"
  }
}

resource "aws_lb_listener" "http" {
  load_balancer_arn = aws_lb.alb.arn
  port              = 80
  protocol          = "HTTP"
  default_action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.tg.arn
  }
}

resource "aws_ecs_service" "web" {
  name                               = "kv-web"
  cluster                            = aws_ecs_cluster.demo.id
  task_definition                    = aws_ecs_task_definition.web.arn
  desired_count                      = 2
  launch_type                        = "FARGATE"
  health_check_grace_period_seconds  = 60
  enable_execute_command             = true

  network_configuration {
    subnets          = [var.private_a, var.private_b]
    security_groups  = [aws_security_group.task.id]
    assign_public_ip = false
  }

  load_balancer {
    target_group_arn = aws_lb_target_group.tg.arn
    container_name   = "web"
    container_port   = 80
  }

  deployment_circuit_breaker {
    enable   = true
    rollback = true
  }

  depends_on = [aws_lb_listener.http]
}

The depends_on the listener matters: ECS registration fails if the listener/target group is not ready. terraform destroy tears everything down in reverse.

Common mistakes & troubleshooting

This is the section you will come back to. The playbook maps each real failure to the exact command that confirms it and the fix. A focused companion in this wave — the “ECS task fails to start” troubleshooting guide — goes deeper on the start-time failures (rows 1–2, 6); this is the first-service-wide view.

# Symptom Root cause Confirm (exact command) Fix
1 Tasks loop PROVISIONING → STOPPED; CannotPullContainerError No route to ECR (private subnet, no NAT/endpoints), bad tag, or exec-role missing ECR perms aws ecs describe-tasks --cluster kv-demo --tasks <arn> --query 'tasks[0].stoppedReason' Add NAT or ecr.api+ecr.dkr+s3(gw)+logs endpoints; fix image URI; attach AmazonECSTaskExecutionRolePolicy
2 Task STOPPED; ResourceInitializationError: unable to pull secrets or registry auth Exec role lacks secretsmanager:GetSecretValue/kms:Decrypt, or no secretsmanager endpoint Same stoppedReason query Grant the secret + KMS key to the execution role; add the endpoint/NAT
3 ALB returns 503; targets unhealthy Health path/matcher wrong, or task SG doesn’t allow the ALB SG on the port aws elbv2 describe-target-health --target-group-arn $TG --query 'TargetHealthDescriptions[].TargetHealth' Point health check at a real 200 path; allow sg-alb → sg-task on containerPort
4 Targets go unhealthy right after deploy, tasks get killed Health-check grace shorter than app boot time describe-services … --query 'services[0].events' shows “unhealthy … health checks” Set healthCheckGracePeriodSeconds ≥ cold-start (e.g. 60)
5 App can’t reach an external API; connections time out Private subnet with no NAT and no endpoint for that service ECS Exec in, curl https://api.example.com hangs Add a NAT gateway, or a VPC endpoint if it’s an AWS service
6 Public subnet, assignPublicIp=DISABLED, can’t pull IGW needs a public IP; without NAT/endpoints there’s no egress stoppedReason = CannotPullContainerError Set assignPublicIp=ENABLED (public) or move to private + NAT/endpoints
7 No logs in CloudWatch Log group missing, exec role lacks logs:*, or wrong awslogs-region Check group exists; describe-task-definition … logConfiguration Create group (or awslogs-create-group=true + logs:CreateLogGroup); fix region
8 App gets AccessDenied calling S3/DynamoDB, though “the role has it” Permission is on the execution role, not the task role describe-task-definition --query 'taskDefinition.{exec:executionRoleArn,task:taskRoleArn}' Add the app permissions to the task role
9 Service stuck “deployment in progress” forever New tasks never go healthy; circuit breaker off describe-services … 'services[0].deployments[].rolloutState' = IN_PROGRESS Enable circuit breaker; fix the health check / image
10 Deployment auto-rolled back Circuit breaker fired on a bad revision Service event: “… circuit breaker …”; failed task stoppedReason Debug the new image/config locally, then redeploy
11 ECS Exec: “execute-command failed” / can’t connect enableExecuteCommand off, task role lacks ssmmessages, no ssmmessages endpoint, PV <1.4.0 describe-tasks --query 'tasks[0].enableExecuteCommand'; run the amazon-ecs-exec-checker Enable exec, grant ssmmessages:* on task role, add endpoint, install Session Manager plugin
12 Task killed; exit code 137, OutOfMemoryError Container exceeded its memory hard limit describe-tasks --query 'tasks[0].containers[].{name:name,reason:reason,exit:exitCode}' Raise task/container memory; fix the leak
13 New version won’t take over; old tasks keep serving New tasks never pass health checks, so ECS keeps the old ones describe-target-health shows new IPs unhealthy Fix health path/port; only healthy new targets get traffic
14 create-service fails: target type not supported / registration errors Target group is instance type, not ip aws elbv2 describe-target-groups --query 'TargetGroups[].TargetType' Recreate the target group with --target-type ip

stoppedReason reference

When a task will not stay up, the truth is in stoppedReason on the task and reason on each container. Know the common strings.

stoppedReason / container reason Meaning Usual fix
CannotPullContainerError Image pull failed Route to ECR + exec-role ECR perms + valid tag
ResourceInitializationError: unable to pull secrets… Secrets/registry init failed Exec-role GetSecretValue/kms:Decrypt + endpoint
ResourceInitializationError: failed to configure logging Log group/driver init failed Create group; exec-role logs:*; region
CannotStartContainerError Bad command/entryPoint or missing binary Fix the command; verify the image locally
OutOfMemoryError: Container killed due to memory usage Exceeded memory hard limit Raise memory; fix leak
Essential container in task exited An essential container stopped Read its exitCode; fix the app
Task failed ELB health checks in (target-group …) Killed for failing health checks Grace period + correct health path/SG
Scaling activity initiated by (deployment …) Normal drain during deploy/scale-in None — expected

Target-health reason codes

Reason code Meaning Fix
Target.FailedHealthChecks Failed the configured check Wrong path/port/SG or app down
Target.ResponseCodeMismatch Got a code not in the matcher (e.g. 302) Fix the path or widen the matcher
Target.Timeout No response within timeout App slow/blocked, or SG blocks the probe
Target.NotRegistered Not yet registered Wait; ECS registers on task start
Target.DeregistrationInProgress Draining after deploy/scale-in Expected during rollout
Elb.InternalError ALB-side error Retry; check ALB config/limits

Exit-code cheat sheet

Exit code Signal Typical cause
0 Clean exit (an essential container exiting 0 still stops the task)
1 / 2 App error / config error at startup
137 128+9 SIGKILL OOM kill, or stopTimeout exceeded
139 128+11 SIGSEGV Native crash / segfault
143 128+15 SIGTERM Graceful stop (deploy, scale-in, Spot reclaim)

The two nastiest real failures for a first service are worth spelling out. The private-subnet pull failure (rows 1, 6) is the most common: everyone rightly puts tasks in private subnets, then forgets those subnets need an explicit route to ECR. The tell is CannotPullContainerError in stoppedReason within ~40 seconds of the task starting; the fix is either a NAT gateway or the endpoint quartet (ecr.api, ecr.dkr, the S3 gateway endpoint for layers, and logs) — people frequently add the two ECR interface endpoints and still fail because they forgot the S3 gateway endpoint where the image layers actually live. The role mix-up (row 8) is the sneakiest because nothing fails at start: the task runs, then throws AccessDenied the first time the app calls DynamoDB, and the engineer swears “the role has that permission” — because they put it on the execution role. The permission your code needs always goes on the task role.

Best practices

Security notes

Container security on Fargate is mostly about identity, network isolation, and image hygiene — the host is AWS’s problem.

Control What to do Why
Task role least-privilege Scope to the exact ARNs/actions the app calls Blast radius if the container is compromised
Execution-role scoping Limit secrets/KMS to the specific secret ARNs An over-broad exec role can read every secret
Secrets via secrets Pull from Secrets Manager/SSM, not environment Plaintext env is visible in describe/console
Private subnets + SG source-by-SG Only the ALB SG reaches tasks No direct internet path to containers
readonlyRootFilesystem Set true; mount a writable tmpfs where needed Blocks tamper/persistence in the container
Image scanning Enable ECR scan-on-push; fail builds on criticals Catch CVEs before they reach production
Non-root user Run as a non-root UID in the image Limits container-escape impact
ECS Exec auditing Log sessions to CloudWatch/S3; gate the SSM perms Shell access is powerful; record it
Encryption in transit Terminate TLS at the ALB (ACM cert) Protects client↔ALB traffic
VPC endpoints with policies Restrict which principals/repos an endpoint allows Tightens the AWS-service egress path

Cost & sizing

You pay for Fargate compute (vCPU-hours + GB-hours, per second, one-minute minimum), plus the ALB, plus NAT/endpoints and data transfer. The ECS control plane itself is free. There is no perpetual free tier for Fargate.

Cost component Driver Approx (ap-south-1 / Mumbai) Lever
Fargate vCPU vCPU-hours requested ~$0.05088 / vCPU-hr Smaller cpu, Spot, Graviton (ARM64)
Fargate memory GB-hours requested ~$0.005584 / GB-hr Smaller memory, right-size
Fargate Spot Same, discounted ~70% off Stateless/burst tasks
Ephemeral storage >20 GB GB-hours above 20 ~$0.000123 / GB-hr Keep default 20 GB
ALB Hours + LCUs ~$0.0225/hr + LCU Share one ALB across services (path rules)
NAT gateway Hours + per-GB ~$0.056/hr + per-GB Prefer VPC endpoints for AWS traffic
Interface endpoints Per endpoint-hour + per-GB ~$0.013/endpoint-hr Consolidate; still cheaper than NAT at scale

(Prices are approximate and region-specific; check the Fargate pricing page. us-east-1 runs roughly 20% cheaper — ~$0.04048/vCPU-hr, ~$0.004445/GB-hr.)

Worked example — the lab’s steady service, two 0.5 vCPU / 1 GB tasks running 24×7 (~730 hr/mo) in Mumbai:

Line Calculation Monthly
vCPU 1.0 vCPU × 730 hr × $0.05088 ≈ $37.1
Memory 2.0 GB × 730 hr × $0.005584 ≈ $8.2
Fargate subtotal ≈ $45 (~₹3,750)
ALB ~$0.0225 × 730 + light LCU ≈ $18–22
Egress 2× endpoints or 1 NAT ~$19 (endpoints) / ~$41+ (NAT)
Rough total small prod-shaped service ≈ $82–108 / mo

The biggest levers: run fewer/smaller tasks and autoscale rather than over-provisioning; put burst tasks on Fargate Spot; use ARM64/Graviton for ~20% off compute at equal size; schedule non-prod to desiredCount=0 overnight; and prefer VPC endpoints over a NAT gateway when egress is mostly AWS services. On us-east-1 the same service lands closer to ~$65–90/mo.

Interview & exam questions

Q1. What is the difference between the ECS execution role and the task role? The execution role is assumed by the Fargate agent to pull the image, fetch secrets, and write logs — its failures stop the task from starting. The task role is assumed by your application to call AWS APIs at runtime — its failures show up as AccessDenied while the app runs. (DVA-C02, SAA-C03)

Q2. Why must an ECS Fargate target group be ip type? Fargate requires the awsvpc network mode, so each task has its own ENI and private IP; the target group registers those IPs. An instance-type target group registers EC2 instance IDs and cannot represent a Fargate task. (SAA-C03, SOA-C02)

Q3. A Fargate task in a private subnet loops PROVISIONING → STOPPED with CannotPullContainerError. Why? The task ENI has no route to ECR — no NAT gateway and no VPC endpoints. Add a NAT gateway, or the ecr.api, ecr.dkr, S3 gateway, and logs endpoints. (DVA-C02, SOA-C02)

Q4. What does the deployment circuit breaker do? It watches a new deployment and, after enough tasks fail to reach steady state, marks the deployment FAILED and stops it; with rollback: true it automatically reverts to the last healthy task-definition revision. (DVA-C02)

Q5. What are minimumHealthyPercent and maximumPercent? They bound a rolling deploy relative to desiredCount: the minimum healthy tasks ECS keeps and the maximum total it may run. For a load-balanced service the defaults are 100%/200%, giving zero-downtime add-then-drain. (DVA-C02)

Q6. When would you choose the EC2 launch type over Fargate? For GPUs, privileged containers, host networking, DaemonSet-style agents, very high steady utilisation where bin-packing plus Savings Plans is cheaper, or very small tasks packed densely. (SAA-C03)

Q7. How does healthCheckGracePeriodSeconds help? It tells ECS to ignore ELB/target-group health checks for that many seconds after a task starts, so a slow-booting app is not marked unhealthy and killed before it is ready. (SOA-C02)

Q8. What is Fargate Spot and what must the app tolerate? Fargate on spare capacity at ~70% off; AWS can reclaim it, sending SIGTERM then SIGKILL after stopTimeout. The workload must be stateless/fault-tolerant with graceful shutdown and enough replicas. (SAA-C03)

Q9. Which valid CPU/memory combination would you pick for a small API, and what happens if you pick an invalid one? For example cpu=512 (0.5 vCPU) with memory=1024. An invalid pair fails register-task-definition with “No Fargate configuration exists for given values.” (DVA-C02)

Q10. How do you get a shell in a running Fargate task without SSH? ECS Exec: enable enableExecuteCommand, grant the task role ssmmessages:*, ensure platform ≥1.4.0 and an ssmmessages route, then aws ecs execute-command … --interactive --command "/bin/sh". (DVA-C02, SOA-C02)

Q11. Why put secrets in secrets rather than environment? environment values are plaintext and visible in describe-task-definition/console; secrets shows only the ARN and pulls the value at start via the execution role. (SCS-C02, DVA-C02)

Q12. Your service is “stuck deploying” and old tasks keep serving. What’s happening and how do you fix it? The new tasks never pass health checks, so ECS never shifts traffic; with the circuit breaker off it retries indefinitely. Enable the circuit breaker and fix the health check (path/port/grace) or the image. (DVA-C02)

Quick check

  1. Which network mode does Fargate require, and what does each task get because of it?
  2. You need your app to write to DynamoDB. Which role gets the dynamodb:PutItem permission — execution or task?
  3. Your ALB returns 503 with all targets unhealthy, but the app runs fine. Name two likely causes.
  4. What target-group TargetType must you use for a Fargate service, and why?
  5. Tasks in a private subnet fail with CannotPullContainerError. What are the two egress designs that fix it?

Answers

  1. awsvpc — each task gets its own ENI with a private IP and its own security groups.
  2. The task role — it is assumed by your application code at runtime.
  3. The health-check path/matcher is wrong (app returns a non-200 at that path), or the task security group does not allow inbound from the ALB security group on the container port. (Also possible: grace period too short.)
  4. ip — because awsvpc tasks register by private IP, not by EC2 instance ID.
  5. A NAT gateway on the private subnets, or VPC endpoints (ecr.api, ecr.dkr, the S3 gateway endpoint, and logs).

Glossary

Term Definition
Cluster A logical namespace grouping ECS services, tasks, and capacity providers.
Task definition An immutable, versioned (family:revision) blueprint describing containers, CPU/memory, roles, and network mode.
Container definition One container’s config inside a task definition: image, ports, env, secrets, logs, essential.
Task A running instance of a task definition; on Fargate, a micro-VM with an ENI and one or more containers.
Service A controller that maintains a desired count of tasks, registers them with a load balancer, and manages deployments.
Fargate The serverless ECS launch type: no hosts to manage; pay per vCPU-second and GB-second.
Fargate Spot Fargate on spare capacity at a large discount, subject to interruption.
Launch type Where tasks run: FARGATE (serverless) or EC2 (your instances).
Execution role IAM role the Fargate agent assumes to pull images, fetch secrets, and write logs.
Task role IAM role the application inside the container assumes to call AWS APIs.
awsvpc Network mode giving each task its own ENI, private IP, and security groups (required on Fargate).
ENI Elastic Network Interface — the virtual NIC attached per task in awsvpc mode.
Target group (ip) An ALB pool that registers targets by IP address — required for Fargate tasks.
Health-check grace period Seconds ECS ignores ELB health checks after a task starts, so slow boots aren’t killed.
Deployment circuit breaker A service feature that stops (and optionally rolls back) a deployment whose tasks keep failing.
Capacity provider The abstraction for where tasks run (FARGATE, FARGATE_SPOT, or an EC2 ASG), with base/weight strategy.
ECS Exec A feature to open an interactive shell in a running task via Systems Manager, without SSH.

Next steps

AWSECSFargateContainersALBTask DefinitionawsvpcECS Exec
Need this built for real?

Vinod is a Senior Cloud Architect (22+ yrs) — available for Azure / AWS / GCP architecture, landing zones, and migrations.

Work with me

Comments

Keep Reading