DEV Community

Cover image for How to Put an ALB in Front of ECS Fargate
Matt
Matt

Posted on • Originally published at fortem.dev

How to Put an ALB in Front of ECS Fargate

How to Put an ALB in Front of ECS Fargate

Originally published at https://fortem.dev/blog/ecs-load-balancer-guide
Attach an Application Load Balancer to ECS Fargate: the awsvpc ip target-type rule, health checks that pass, the Terraform to wire it, and shared-vs-per-service ALB cost at fleet scale.


Guide

The task is RUNNING, but the target group says unhealthy and the URL 503s. Attaching an Application Load Balancer to a Fargate service has three real traps: the awsvpc "ip target type" rule, a health check that never passes, and — at 10+ services — one ALB per service quietly costing more than the compute. Here's the Terraform-native version, with the numbers to debug the 503 and to decide whether to share one ALB.

TL;DR

  • Fargate uses the awsvpc network mode, so the target group MUST use target type "ip", not "instance" — tasks are ENIs, not EC2 instances. This is the #1 first-time error.
  • ECS registers and deregisters tasks with the target group automatically through its service-linked role. You attach the target group to the service; you don't script registration.
  • An unhealthy target is 90% of the time a security-group gap or a health-check path that doesn't return 200. Fix the SG from the ALB to the task port first.
  • One ALB per service is simplest and costs ~$16/month each. At 20 services that's ~$330/month — a shared ALB with host/path routing collapses it to one base charge.
  • Tune deregistration delay (300s→5s) and stop timeout (30s→2s), and trap SIGTERM, for fast zero-downtime deploys.

Ready to use — ALB + target group + listener for a Fargate service

# The load balancer and its security group
resource "aws_lb" "app" {
  name               = "app-alb"
  load_balancer_type = "application"
  subnets            = var.public_subnet_ids
  security_groups    = [aws_security_group.alb.id]
}

resource "aws_security_group" "alb" {
  name   = "app-alb-sg"
  vpc_id = var.vpc_id
  ingress { from_port = 443, to_port = 443, protocol = "tcp", cidr_blocks = ["0.0.0.0/0"] }
  egress  { from_port = 0,   to_port = 0,   protocol = "-1",  cidr_blocks = ["0.0.0.0/0"] }
}

# Target group — MUST be target_type = "ip" for Fargate (awsvpc)
resource "aws_lb_target_group" "app" {
  name        = "app-tg"
  port        = 8080
  protocol    = "HTTP"
  vpc_id      = var.vpc_id
  target_type = "ip"

  health_check {
    path                = "/health"
    matcher             = "200"
    interval            = 15
    healthy_threshold   = 2
    unhealthy_threshold = 3
  }

  # Drain fast for short-lived requests (default is 300s)
  deregistration_delay = 5
}

resource "aws_lb_listener" "https" {
  load_balancer_arn = aws_lb.app.arn
  port              = 443
  protocol          = "HTTPS"
  certificate_arn   = var.acm_certificate_arn
  default_action { type = "forward", target_group_arn = aws_lb_target_group.app.arn }
}

# The task's SG must allow the ALB's SG on the container port
resource "aws_security_group_rule" "alb_to_task" {
  type                     = "ingress"
  from_port                = 8080
  to_port                  = 8080
  protocol                 = "tcp"
  security_group_id        = aws_security_group.task.id
  source_security_group_id = aws_security_group.alb.id
}

# Attach the target group to the service — ECS registers tasks for you
resource "aws_ecs_service" "app" {
  name            = "app"
  cluster         = var.cluster_arn
  task_definition = aws_ecs_task_definition.app.arn
  desired_count   = 3
  launch_type     = "FARGATE"

  health_check_grace_period_seconds = 60

  load_balancer {
    target_group_arn = aws_lb_target_group.app.arn
    container_name   = "app"
    container_port   = 8080
  }

  network_configuration {
    subnets         = var.private_subnet_ids
    security_groups = [aws_security_group.task.id]
  }
}
Enter fullscreen mode Exit fullscreen mode

The two lines that break most first attempts: target_type = "ip" and the alb_to_task security-group rule.

Why Fargate changes how you attach a load balancer

Fargate tasks use the awsvpc network mode, so each task is an elastic network interface with its own IP — not an EC2 instance. That's why the target group must use target type ip.

On EC2-backed ECS, a task maps to a host and a port, so the classic target type of instance works — the load balancer routes to an instance ID and a dynamically-mapped port. Fargate has no host you own. Every task gets its own ENI and private IP, and the load balancer has to route to that IP directly. Register a Fargate service against an instance target group and nothing registers at all — no error you'd expect, just zero targets and a 503.

"For services with tasks using the awsvpc network mode, when you create a target group for your service, you must choose ip as the target type, not instance. This is because tasks that use the awsvpc network mode are associated with an elastic network interface, not an Amazon EC2 instance."

AWS ECS documentation, verified July 2026

One upside of the ip target type: it supports cross-VPC connectivity, where the instance type requires the load balancer and tasks in the same VPC.

The Terraform to wire it up (target type ip)

One ALB, one target group with target_type = ip, one listener, and a security-group rule from the ALB to the task port. ECS registers tasks automatically through its service-linked role.

The Ready-to-use block above is the whole thing. The part people miss isn't the ALB — it's the wiring between it and the task. Two security groups: one on the ALB that lets the public in on 443, and a rule on the task's security group that lets the ALB's security group reach the container port. Reference the ALB's SG as the source, not a CIDR — the task should only accept traffic from the load balancer, never the open internet.

You never write registration logic. Amazon ECS holds a service-linked IAM role whose whole job is to register a task with the target group when it starts and deregister it when it stops. You declare the load_balancer block on the service, and ECS keeps the target group in sync with the running tasks as they cycle through deploys and scaling.

Why your target is unhealthy (the 503 debug)

An unhealthy target is almost always one of three things: the security group blocks the ALB on the task port, the health-check path returns non-200, or the app starts slower than the grace period.

AWS maintains an entire knowledge-center article on this one failure, which tells you how common it is. Work the causes in order — security group first, because it's both the most frequent and the easiest to overlook. If the task's SG doesn't allow the ALB's SG on the container port, the health check can't even reach the app, and the target sits unhealthy from the moment it launches.

Root cause Symptom Fix
Security group gap Target stuck unhealthy from launch Allow the ALB's SG inbound on the container port
Health path returns non-200 Target flaps or never passes Point the check at a path that returns 200 (e.g. /health)
Grace period too short Task killed and restarted in a loop Set healthCheckGracePeriodSeconds above real startup time
Wrong target type No targets register at all Use target_type = "ip" for awsvpc / Fargate
Check port mismatch Unhealthy despite app being up Match the target-group port to the container port

A recurring "unhealthy" that clears and comes back is different from one that never passes — it's usually load or a slow dependency, and that's where metrics earn their keep. Pairing target health with task-level signals is exactly what monitoring ECS Fargate across a fleet is for: a flapping target with a memory alarm behind it tells you the health check isn't the bug, the app is.

KEY INSIGHT: A newly-registered target only needs one passing health check to go healthy — the healthy-threshold count only applies when a target recovers from unhealthy back to healthy. So a target that never goes healthy on first launch is almost never a threshold problem; it's a reachability or path problem. Chase the security group and the path, not the thresholds.

ALB health checks vs container health checks

Different systems. The ALB health check decides if a target gets traffic; the container health check decides if ECS restarts the task. Run both — keep the grace period longer than startup.

This is a genuinely confusing overlap — practitioners ask on AWS re:Post whether they should use one, the other, or both. The answer is both, because they answer different questions. The target group's health check governs traffic: fail it and the ALB stops routing to that task. The container health check in the task definition governs lifecycle: fail it and ECS kills and replaces the container. A task can be healthy to ECS but pulled from the ALB, or the reverse.

The one setting that ties them together is health_check_grace_period_seconds on the service. It tells ECS to ignore failing ALB health checks for the first N seconds after a task starts, so a slow-booting app isn't killed before it's ready. Set it above your real cold-start time — a JVM service that takes 45 seconds to warm up needs a grace period well past that, or every deploy turns into a restart loop.

One ALB per service, or a shared ALB?

One ALB per service is simplest but costs about $16/month each. A shared ALB with host- or path-based listener rules serves many services on one base charge — the default past a handful of services.

Every ALB carries an hourly base charge whether it's serving one request or a million. At roughly $16.43/month per load balancer, the per-service pattern looks free at three services and turns into a line item at twenty. A single ALB can front dozens of services through listener rules — route api.example.com to one target group and /admin to another — so you pay one base charge for the whole fleet.

Services One ALB each (base) One shared ALB (base)
5 services $82/mo $16/mo
10 services $164/mo $16/mo
20 services $329/mo $16/mo

Base ALB charge only ($0.0225/hr, us-east-1, verified July 2026). LCU charges ($0.008/LCU-hr) add on top for both and scale with traffic.

KEY INSIGHT: The shared ALB isn't free savings — it trades dollars for coupling. One listener config now affects every service behind it, and a bad rule change can 503 the whole fleet instead of one app. The rule of thumb: per-service ALBs while you have a handful and want blast-radius isolation; a shared ALB once the base charges outweigh the isolation, with each service still on its own unique target group.

Zero-downtime deploys: deregistration delay and SIGTERM

The default 300-second deregistration delay makes every deploy drain slowly. For sub-second services, drop it to 5s, set ECS_CONTAINER_STOP_TIMEOUT to 2s, and trap SIGTERM to drain in-flight requests.

When ECS replaces a task, the ALB stops sending it new requests and waits out the deregistration delay before cutting existing connections. The default is 300 seconds — five minutes of a draining task lingering per deploy. AWS's own guidance: if your responses finish in under a second, set deregistration_delay.timeout_seconds to 5. Don't do this for long-lived requests like slow uploads or streaming — those need the room.

The other half is graceful shutdown. ECS sends SIGTERM, waits ECS_CONTAINER_STOP_TIMEOUT seconds (default 30), then SIGKILLs. If your app ignores SIGTERM, it eats the full wait on every deploy. Trap SIGTERM to stop accepting new connections and finish in-flight ones, and a task that drains in 500ms exits cleanly instead of waiting out the timeout.

javascriptCopy

// Trap SIGTERM so in-flight requests finish before the task exits
process.on("SIGTERM", () => {
  server.close(() => process.exit(0)); // stop new conns, drain, then exit
});
Enter fullscreen mode Exit fullscreen mode

The traps that bite at fleet scale

Load balancer config can't be changed in the console after the service is created — only via CLI or CloudFormation. Use a unique target group per service; sharing one breaks deployments.

Two AWS-documented rules cause most of the fleet-scale pain. First: once a service exists, you cannot change its load-balancer configuration from the console — you have to go through the CLI, CloudFormation, or the SDK, and the change forces a new deployment that re-registers every task. Discover this mid-incident and you'll waste time hunting for a console button that isn't there.

Second: give every service its own target group. Sharing one target group across services "might lead to issues during service deployments," per AWS — the two services fight over the same registration set and deploys go sideways. This is the ingress side of ECS networking; for service-to-service traffic inside the fleet, the internal-ALB and DNS options are a separate decision covered in which ECS service discovery you should use — Cloud Map, Service Connect, or an internal ALB for East-West traffic, versus the public ingress ALB here.

If you read this, you might also want to know

Do I need an NLB instead of an ALB for gRPC or raw TCP?

For HTTP/HTTPS and gRPC, the ALB is right — it speaks layer 7 and supports gRPC target groups. For raw TCP/UDP, non-HTTP protocols, or when you need a static IP and ultra-low latency, use a Network Load Balancer. The Fargate wiring is nearly identical: target_type = ip, and ECS registers tasks the same way.

How do I front multiple Fargate services on one domain?

Use one ALB with path- or host-based listener rules. /api forwards to the api target group, /billing to the billing target group; or api.example.com and app.example.com split by host. Each service keeps its own unique target group — you're only sharing the load balancer and its listeners, not the target group.

Can the ALB and the Fargate tasks live in different VPCs?

Yes, if the target group uses target type ip (which Fargate requires anyway). The ip target type supports cross-VPC connectivity; the instance target type does not. You still need routing and security-group rules that let the ALB reach the task IPs across the VPC boundary.

Frequently asked questions

Why is my ECS Fargate target unhealthy in the ALB?

Almost always one of three things: the task's security group doesn't allow inbound traffic from the load balancer's security group on the container port, the health-check path returns a non-200 status, or the application takes longer to start than the health-check grace period allows. Check the security group rule first — it's the most common cause.

Should I use ALB health checks, container health checks, or both?

They do different jobs, so run both. The ALB (target group) health check decides whether a task receives traffic. The container health check in the task definition decides whether ECS restarts the container. Keep the ALB health-check grace period longer than your container's startup time so a slow boot doesn't get killed before it's ready.

Why must Fargate use ip target type instead of instance?

Fargate tasks run in the awsvpc network mode, so each task gets its own elastic network interface and IP address rather than sharing an EC2 instance. The target group must use target type "ip" to register those task IPs. "instance" target type only works for EC2-backed tasks that map to an instance ID.

How much does an Application Load Balancer cost?

In us-east-1, $0.0225 per hour (about $16.43/month) plus $0.008 per LCU-hour for capacity used (verified July 2026). The hourly base charge is per ALB, so running one ALB per service at 20 services is roughly $330/month in base charges alone — a shared ALB with host/path routing collapses that to one base charge.

Can one ALB serve multiple ECS services?

Yes. A single ALB routes to many services using host-based or path-based listener rules — api.example.com to one target group, /admin to another. Give each service its own unique target group; sharing one target group across services causes problems during deployments.

Every ALB, target group, and service — mapped

At 10+ environments, nobody can say which ALB fronts which service, or which target groups are draining money on dead tasks. Fortem maps the whole ECS fleet — load balancers, target groups, and the services behind them — on one screen. Book a 20-minute call and we'll walk yours.

Book a 20-min call

Worth reading

GuideECS Service Discovery: Cloud Map vs Service ConnectThe other half of ECS networking — East-West service-to-service traffic. Cloud Map, Service Connect, or an internal ALB, with a decision table.LandingAWS ECS Fargate: What It Is, How It Works, What It CostsThe head-term reference for ECS Fargate — where networking, the awsvpc mode, and load balancing sit in the bigger picture.


Map every ALB and target group across your fleet: fortem.dev/book

Top comments (0)