๐Ÿš€ Noahops enterprise beta is now open โ€” Get early access โ†’
Back to blog

ECS + ALB Networking: How Traffic Actually Gets to Your Container (Without the AWS Docs Pain)

ECS networking with an Application Load Balancer is one of those things that looks simple in the AWS Console but has enough hidden gotchas to ruin your afternoon. This guide explains how it actually works โ€” not just the CLI commands, but the why behind each piece โ€” so you can set it up right the first time and debug it when something breaks.

What an ALB actually does for your ECS service

Your ECS tasks are containers running in a private subnet. They don't have public IP addresses. Nothing on the internet can reach them directly. That's intentional โ€” you don't want random traffic hitting your containers.

The Application Load Balancer sits in front of everything. It lives in your public subnets, faces the internet, and accepts traffic on port 80 (HTTP) and 443 (HTTPS). When a request comes in, the ALB forwards it to one of your healthy containers. When a container is unhealthy or being replaced during a deployment, the ALB stops sending traffic to it.

That's the basic model:

Internet โ†’ ALB (public subnets) โ†’ ECS tasks (private subnets)

The ALB also handles SSL termination. Your containers only need to speak HTTP on their internal port โ€” the ALB takes care of HTTPS toward the internet.

The four components you need to understand

Before touching the console, get this mental model straight. There are four things:

  1. The ALB itself โ€” the load balancer resource, lives in public subnets, has a public DNS name
  2. A target group โ€” a list of ECS tasks the ALB sends traffic to; one per ECS service
  3. A listener โ€” tells the ALB which port to listen on and what to do with requests (forward, redirect, etc.)
  4. Listener rules โ€” path-based or host-based conditions that route traffic to different target groups

One ALB can serve multiple ECS services. You do that by creating multiple target groups and listener rules (e.g., /api/* โ†’ API service, / โ†’ frontend service). This is how you avoid paying for a separate ALB per service โ€” one ALB, multiple services, routing rules do the work.

Step 1: Security groups โ€” get these wrong and nothing works

You need two security groups. Most startup engineers create one and wonder why traffic isn't flowing.

ALB security group โ€” allows inbound traffic from the internet on ports 80 and 443:

# Create ALB security group
aws ec2 create-security-group \
  --group-name my-alb-sg \
  --description "ALB: accepts traffic from internet" \
  --vpc-id vpc-YOUR_VPC_ID

# Allow HTTP and HTTPS from anywhere
aws ec2 authorize-security-group-ingress \
  --group-id sg-ALB_SG_ID \
  --ip-permissions \
    IpProtocol=tcp,FromPort=80,ToPort=80,IpRanges=[{CidrIp=0.0.0.0/0}] \
    IpProtocol=tcp,FromPort=443,ToPort=443,IpRanges=[{CidrIp=0.0.0.0/0}]

ECS tasks security group โ€” allows inbound traffic only from the ALB security group, on your container's port:

# Create ECS tasks security group
aws ec2 create-security-group \
  --group-name my-ecs-tasks-sg \
  --description "ECS tasks: only accepts traffic from ALB" \
  --vpc-id vpc-YOUR_VPC_ID

# Allow traffic from ALB security group on your app port (e.g. 8080)
aws ec2 authorize-security-group-ingress \
  --group-id sg-ECS_TASKS_SG_ID \
  --protocol tcp \
  --port 8080 \
  --source-group sg-ALB_SG_ID

The key point: your ECS tasks never accept traffic directly from the internet. Only from the ALB. If you open your ECS tasks security group to 0.0.0.0/0, you've broken the security model. The ALB is your single entry point.

Step 2: Create the ALB in public subnets

aws elbv2 create-load-balancer \
  --name my-app-alb \
  --subnets subnet-PUBLIC_1 subnet-PUBLIC_2 \
  --security-groups sg-ALB_SG_ID \
  --scheme internet-facing \
  --type application \
  --ip-address-type ipv4

Two things to get right here:

  • Public subnets only โ€” the ALB needs internet access; don't put it in private subnets
  • At least two subnets in different AZs โ€” AWS requires multi-AZ for ALBs; single-subnet creation will fail

Save the ALB ARN from the output. You'll need it in the next two steps.

Step 3: Create a target group

A target group is where the ALB sends traffic. For Fargate, use --target-type ip (not instance โ€” this is one of the most common mistakes):

aws elbv2 create-target-group \
  --name api-service-tg \
  --protocol HTTP \
  --port 8080 \
  --vpc-id vpc-YOUR_VPC_ID \
  --target-type ip \
  --health-check-path /health \
  --health-check-interval-seconds 30 \
  --health-check-timeout-seconds 5 \
  --healthy-threshold-count 2 \
  --unhealthy-threshold-count 3

Why --target-type ip for Fargate? Fargate tasks use the awsvpc networking mode โ€” each task gets its own elastic network interface with its own IP. The ALB registers task IPs directly, not EC2 instances. If you use instance as the target type with Fargate, registration fails silently and no traffic reaches your containers.

The health check path (/health) should return HTTP 200 when your app is ready. This is how the ALB knows which tasks to send traffic to.

Step 4: Create listeners

A listener defines what the ALB does with incoming requests on a given port.

Standard setup: redirect HTTP to HTTPS, then forward HTTPS to your target group.

# HTTP listener: redirect all traffic to HTTPS
aws elbv2 create-listener \
  --load-balancer-arn arn:aws:elasticloadbalancing:REGION:ACCOUNT:loadbalancer/app/my-app-alb/ID \
  --protocol HTTP \
  --port 80 \
  --default-actions '[{
    "Type": "redirect",
    "RedirectConfig": {
      "Protocol": "HTTPS",
      "Port": "443",
      "StatusCode": "HTTP_301"
    }
  }]'

# HTTPS listener: forward to your target group
aws elbv2 create-listener \
  --load-balancer-arn arn:aws:elasticloadbalancing:REGION:ACCOUNT:loadbalancer/app/my-app-alb/ID \
  --protocol HTTPS \
  --port 443 \
  --ssl-policy ELBSecurityPolicy-TLS13-1-2-2021-06 \
  --certificates CertificateArn=arn:aws:acm:REGION:ACCOUNT:certificate/CERT_ID \
  --default-actions '[{
    "Type": "forward",
    "TargetGroupArn": "arn:aws:elasticloadbalancing:REGION:ACCOUNT:targetgroup/api-service-tg/ID"
  }]'

Your SSL certificate goes in AWS Certificate Manager (ACM) โ€” it's free for use with ALB. Request a certificate for your domain in ACM before creating the HTTPS listener.

Step 5: Routing multiple services from one ALB

If you have more than one ECS service (API + frontend, or API + admin), you can route them through the same ALB using listener rules. Create one target group per service, then add rules:

# Route /api/* to the API service
aws elbv2 create-rule \
  --listener-arn arn:aws:elasticloadbalancing:REGION:ACCOUNT:listener/app/my-app-alb/ALB_ID/LISTENER_ID \
  --priority 10 \
  --conditions '[{"Field": "path-pattern", "Values": ["/api/*"]}]' \
  --actions '[{"Type": "forward", "TargetGroupArn": "arn:...targetgroup/api-service-tg/ID"}]'

Rules are evaluated in priority order (lower number = higher priority). The listener's --default-actions is the catch-all for anything that doesn't match a rule.

Step 6: Create the ECS service with the ALB attached

aws ecs create-service \
  --cluster my-cluster \
  --service-name api-service \
  --task-definition api:1 \
  --desired-count 2 \
  --launch-type FARGATE \
  --network-configuration '{
    "awsvpcConfiguration": {
      "subnets": ["subnet-PRIVATE_1", "subnet-PRIVATE_2"],
      "securityGroups": ["sg-ECS_TASKS_SG_ID"],
      "assignPublicIp": "DISABLED"
    }
  }' \
  --load-balancers '[{
    "targetGroupArn": "arn:aws:elasticloadbalancing:REGION:ACCOUNT:targetgroup/api-service-tg/ID",
    "containerName": "api",
    "containerPort": 8080
  }]' \
  --health-check-grace-period-seconds 60

Two things to notice:

  • Private subnets for tasks โ€” your containers go in private subnets (assignPublicIp: DISABLED). The ALB handles all public traffic.
  • containerName and containerPort must match exactly what's in your task definition. If your task definition names the container app but you put api here, ECS silently fails to register with the ALB.

The three gotchas that break deployments

1. Health check grace period too short

If your app takes 30 seconds to start and your grace period is 10 seconds, the ALB marks your task unhealthy before it's ready, ECS kills it, starts a new one, and you're in an infinite loop. Set health-check-grace-period-seconds to at least 1.5ร— your app's startup time. A Java app that takes 45 seconds to boot needs a grace period of at least 60โ€“70 seconds.

2. target-type: instance instead of ip for Fargate

Fargate tasks register by IP, not EC2 instance. Using instance target type means zero tasks ever register with the target group, the ALB has no healthy targets, and you get 503s. Always use --target-type ip for Fargate.

3. ECS tasks in public subnets with assignPublicIp: DISABLED

If you put your tasks in public subnets but disable public IP assignment, your tasks can't pull images from ECR (no internet access) and they can't reach AWS services. Either put tasks in private subnets with a NAT Gateway, or put them in public subnets with assignPublicIp: ENABLED. For production, private subnets + NAT Gateway is the right answer.

Connection draining โ€” reduce deployment downtime

When a deployment replaces a task, the ALB deregisters the old task. By default, it waits 300 seconds for in-flight requests to complete. That's 5 minutes. For most APIs with short-lived requests, 30 seconds is enough:

aws elbv2 modify-target-group-attributes \
  --target-group-arn arn:aws:elasticloadbalancing:REGION:ACCOUNT:targetgroup/api-service-tg/ID \
  --attributes Key=deregistration_delay.timeout_seconds,Value=30

For WebSocket connections or long-running requests, keep it higher โ€” match or exceed your longest expected request duration.

Verification checklist

After setup, confirm:

  • ALB DNS name resolves and returns HTTP 200 (or your app's expected response)
  • Target group shows tasks in healthy state (not initial or unhealthy)
  • ECS service events don't show repeated task registration/deregistration
  • HTTPS works and redirects from HTTP

In the AWS Console: EC2 โ†’ Target Groups โ†’ select your target group โ†’ Targets tab. You should see your task IPs listed as healthy. If they're stuck in initial, your grace period or health check endpoint is the issue. If they flip to unhealthy immediately, the health check path isn't returning 200.

FAQ

Do I need one ALB per ECS service?

No. One ALB can serve multiple services via path-based or host-based routing rules. One ALB costs ~$18/month base. Running three services through one ALB is much cheaper than three separate ALBs.

Can I use an NLB instead of an ALB?

ALB is the right choice for HTTP/HTTPS services. NLB (Network Load Balancer) is for TCP/UDP traffic where you need static IPs or ultra-low latency. For typical web apps and APIs, use ALB.

Why does the ALB say "no healthy targets" right after I create the ECS service?

Tasks take time to start and pass health checks. Wait 1โ€“2 minutes after the service reaches RUNNING state. If it's still unhealthy after 3 minutes, check: (a) your health check path returns 200, (b) security group allows ALB โ†’ tasks on the right port, (c) grace period is long enough.

Does the ALB add latency?

Single-digit milliseconds โ€” negligible for application traffic. It's not a meaningful factor in startup infrastructure decisions.


Once your ALB is set up and your tasks are passing health checks, deployments become smooth: ECS registers new tasks, the ALB verifies they're healthy, drains traffic from old tasks, then deregisters them. Zero-downtime, handled automatically.

Request a free demo at noahops.com โ€” NoahOps sets up your ALB, target groups, and ECS service networking automatically, and Noah AI lets you describe what you want in plain English.

Ready to ship to AWS?

Try NoahOps free โ€” VPC, ECS, RDS, CI/CD in your own AWS account in 15 minutes.