Adam Innes · Blog

AWS's New Application Load Balancer and How Path-Based Routing Works

· 7 min · aws, load balancing, ecs, architecture

Last Thursday, August 11, AWS added a second kind of load balancer to Elastic Load Balancing. The new one is the Application Load Balancer, and the load balancer we’ve been using all along now has a name of its own: the Classic Load Balancer. The headline feature is that the new option can look at the URL path of a request and decide where to send it, so several services can share one load balancer instead of each getting their own.

Here’s what the launch post and the new docs say, how the routing works, and some advice for setting it up without surprises.

What AWS launched

Jeff Barr’s launch post on the AWS News Blog frames it in OSI terms. A layer 4 load balancer doesn’t know much about HTTP, while a layer 7 one can see HTTP and HTTPS headers and make smarter choices. The Application Load Balancer runs at layer 7. The Classic Load Balancer is still available and, per the post, keeps offering both layer 4 and layer 7 functionality.

The post highlights content-based routing, support for applications running in containers, WebSocket and HTTP/2, and more detail about the health of targets. It also mentions that the new load balancer is VPC only and available in all commercial regions. The What’s New entry from the same day lists integrations including Auto Scaling, CloudFormation, EC2 Container Service and Certificate Manager, with Elastic Beanstalk support marked as coming soon.

From the launch week docs: listeners speak HTTP or HTTPS, and WebSockets work on both. HTTP/2 only works on HTTPS listeners, allows up to 128 parallel requests on one connection, and doesn’t do server push. An HTTPS listener takes exactly one certificate.

Listeners, rules and target groups

The Application Load Balancer guide describes the pieces. The load balancer is the single point of contact for clients. It has one or more listeners, each checking for connections on a protocol and port you choose. Each listener has rules, and each rule points at a target group. A target group is a set of registered targets (such as EC2 instances) plus the protocol and port used to reach them. Health checks are configured per target group, not per load balancer.

Every listener needs a default rule, which has no condition and catches whatever the other rules don’t. On top of that you add rules that match a path pattern and forward to a different target group. A typical setup sends /api/* to the API servers, /app/* to containers, and everything else to the web tier.

A target can be registered with more than one target group, and one instance can be registered in the same target group several times on different ports. That second detail is what makes the container story work.

Path patterns and priority

Rules are evaluated in priority order, from the lowest value to the highest, and the default rule always comes last. A path pattern is case sensitive and supports two wildcards, where * matches zero or more characters and ? matches exactly one. The launch week docs cap a pattern at 255 characters, and the API reference lists path-pattern as the only kind of condition and forward as the only kind of action. For now, a rule really is just “this path goes to that target group.”

The wildcard rules have a couple of consequences that are easy to trip over. /api/* matches /api/orders, but it doesn’t match a bare /api, because there’s no slash after it for the pattern to consume. It doesn’t match /API/orders either. Both fall through to the default rule, where a friendly 404 page can hide the bug for a while. There’s also no rewrite step, so your API servers see the full /api/orders path and need to serve under that prefix.

If you script this, the AWS CLI has a new elbv2 command set. The CLI tutorial in the docs creates a target group, registers instances, and adds a rule along these lines (I’ve swapped in an API example and a health check path):

aws elbv2 create-target-group --name api-targets \
  --protocol HTTP --port 8080 --vpc-id vpc-12345678 \
  --health-check-path /api/health

aws elbv2 create-rule --listener-arn listener-arn --priority 10 \
  --conditions Field=path-pattern,Values='/api/*' \
  --actions Type=forward,TargetGroupArn=targetgroup-arn

Watch the limits. The launch post says each Application Load Balancer allows up to 10 URL-based rules, and the limits page counts that per load balancer (not counting default rules), not per listener. Each rule gets one condition, and a target group can be attached to only one load balancer.

Containers and dynamic ports

The ECS What’s New entry explains the old pain: you had to define a fixed host port for a container to use ELB, which kept multiple copies of the same task off a single host, and ECS services mapped one to one with load balancers. Now a container can use a dynamic host port. ECS gives it an unused port when the task is scheduled and registers the task with the load balancer on that port, and several services can share one load balancer, each on its own path.

How it compares with the Classic Load Balancer

The feature comparison in the Elastic Load Balancing User Guide makes the split clear. Classic supports HTTP, HTTPS, TCP and SSL listeners and runs in both EC2-Classic and VPC. The Application Load Balancer is HTTP and HTTPS only, VPC only. Classic has back-end server authentication, which the new one doesn’t. The Application Load Balancer adds path-based routing, routing to multiple ports on one instance, HTTP/2, WebSockets and deletion protection, and it always has cross-zone load balancing on. Its health checks let you choose which HTTP codes count as success and report more detail when a target fails. Sticky sessions exist on both, though the new one only uses cookies it generates itself.

My read is that the Classic Load Balancer is still the right tool if you need TCP or SSL listeners, you’re still on EC2-Classic, or you depend on back-end authentication. For HTTP services in a VPC, the new one simply does more. AWS also released a Python Load Balancer Copy Utility that builds an Application Load Balancer from an existing Classic configuration.

Deciding what shares a load balancer

Sharing works best when services live under one domain and are already split by path, like an API and a front end. Each gets its own target group, its own health check and metrics reported at the target group level, and attaching a target group to an Auto Scaling group lets each service scale on its own. With a 10 rule cap, think a handful of services, not dozens.

I’d keep things on separate load balancers when they need different exposure (the internet-facing or internal scheme is set per load balancer), different domains (rules only look at the path), or TCP. A shared load balancer is also a shared blast radius, since one bad rule change affects every service behind it.

Health checks per service

The health check settings default to HTTP on the traffic port, path /, and a 200 response. Give each target group a path that actually exercises that service, like /api/health, instead of letting everything check / on a server that might answer happily while its database is down. A new target group only gets health checked once a listener rule points at it, and until then its targets show as unused, which can be confusing during setup. Health checks don’t support WebSockets, so a WebSocket service still needs a plain HTTP endpoint to check. And if no Availability Zone has a healthy target, the load balancer routes to all targets anyway.

Security groups

The security group docs say the load balancer must reach targets on both the listener port and the health check port. For an internet-facing load balancer, the recommended rules allow inbound traffic from anywhere on the listener port and outbound traffic to the instance security group on the instance port and health check port. My advice is to have target security groups allow traffic from the load balancer’s security group, not from the world. With ECS dynamic ports you won’t know the port ahead of time, so that rule has to cover a port range. If a target group checks health on a different port, open that one too.

Pricing changed shape

Classic pricing is an hourly charge plus a per GB charge for data transferred through it, which the pricing page before launch listed as $0.025 per hour and $0.008 per GB in US East. The launch post says the Application Load Balancer’s hourly rate is 10 percent lower than Classic, plus Load Balancer Capacity Units, or LCUs, at $0.008 per LCU hour, billed fractionally. An LCU measures new connections per second, active connections and data transfer, and you’re billed on whichever dimension is highest. One LCU covers 25 new connections per second with a 2 KB certificate (5 with a 4 KB one), 3,000 active connections and 2.22 Mbps. AWS says it believes virtually all customers can get a net reduction in load balancer costs by switching.

By my arithmetic, 2.22 Mbps for an hour is roughly 1 GB, so bandwidth-heavy traffic costs about what Classic’s per GB charge would. Lots of new TLS connections or long-lived WebSocket connections are where the numbers can move, so check your traffic before assuming a discount.

Where I’d start

If you run several HTTP services in a VPC, especially on ECS, try the next one behind an Application Load Balancer with a rule per path, a real health check per target group, and security groups that only trust the load balancer. Test the bare paths and the casing, and let Classic keep the TCP work.

← all posts