9.5 Listener Rules: Path-Based and Host-Based Routing
Right, let’s talk about listener rules. This is where ELB stops being a simple traffic cop and starts acting like a concierge with a very specific, slightly obsessive set of instructions. You’ve already told your Application Load Balancer (ALB) to listen on port 443. Great. But when a request comes in, how does it know which target group to send it to? That’s the listener rule’s job. It’s a series of if statements that you get to define, and they are evaluated in a priority order until one matches. The two most powerful conditions you’ll use are based on the host (the Host header, like api.example.com) and the path (like /images/*). This is how you can host a dozen different microservices on a single load balancer, which is both elegant and a fantastic way to save money.
The Anatomy of a Rule (It’s Just if Statements, I Promise)
Think of a rule like this: IF these conditions are true, THEN forward the request to this target group. You can have multiple IF conditions in a single rule, which act as a logical AND. You want to send traffic for static.example.com AND for the path /assets/* to your static content servers? That’s one rule. The conditions are evaluated against the incoming HTTP request. The most important ones are:
host-header: Matches theHostheader. You can specify multiple values, and it’s not case-sensitive. A lifesaver for multi-tenant apps.path-pattern: Matches the URL path. This is where wildcards (*) come into play, and we’ll get into their quirks next.http-header: Matches any header and its value.http-request-method: Matches GET, POST, etc.
The THEN part is simple: forward to a target group, redirect to a URL, or return a fixed response (which is weirdly useful for maintenance pages or mocking endpoints during development).
Path Patterns: Where the Wild Things Are (And Where They Bite You)
Path-based routing seems simple until you actually have to design it. The pattern matching is prefix-based, meaning it looks for a match at the beginning of the path. The * is a wildcard that matches any sequence of characters after the specified prefix. This is not regex. Don’t try to make it regex. You’ll only upset yourself.
Let’s say you have two services: a main app (/app/*) and a reports service (/reports/*). Your rules would look like this, with the more specific path given a higher priority (lower number, like priority 1).
[
{
"Priority": 1,
"Conditions": [
{
"Field": "path-pattern",
"Values": ["/reports/*"]
}
],
"Actions": [
{
"Type": "forward",
"TargetGroupArn": "arn:aws:elasticloadbalancing:us-east-1:123456789012:targetgroup/reports-tg/abc123"
}
]
},
{
"Priority": 2,
"Conditions": [
{
"Field": "path-pattern",
"Values": ["/app/*"]
}
],
"Actions": [
{
"Type": "forward",
"TargetGroupArn": "arn:aws:elasticloadbalancing:us-east-1:123456789012:targetgroup/app-tg/def456"
}
]
}
]
Now, the pitfall: what happens to a request for /reports (without the trailing slash)? Nothing in the above rules matches it. The /reports/* pattern requires something after /reports/. The request will fall through to your default rule (the lowest priority one that usually catches everything), which might send it to your main app, causing a confusing 404. The solution? Be meticulous. You often need a separate, higher priority rule for the exact path /reports if that’s a valid endpoint. This is the kind of “it works on my whiteboard” oversight that gets you paged at 2 AM.
Host-Based Routing: Your Ticket to a Single-ALB Empire
Host-based routing is arguably cleaner. You just have different services living on different subdomains, all pointing to the same ALB. Your rules become beautifully simple.
[
{
"Priority": 10,
"Conditions": [
{
"Field": "host-header",
"Values": ["api.example.com"]
}
],
"Actions": [ { "Type": "forward", "TargetGroupArn": "arn:aws:.../api-tg" } ]
},
{
"Priority": 20,
"Conditions": [
{
"Field": "host-header",
"Values": ["static.example.com"]
}
],
"Actions": [ { "Type": "forward", "TargetGroupArn": "arn:aws:.../static-tg" } ]
}
]
The beauty here is that the path doesn’t matter. Everything for api.example.com goes to the API service, which can handle its own internal routing. It’s a much more robust separation of concerns. You can even use this with wildcards in the host condition, like *.example.com, but use that power carefully—it’s a great way to accidentally create a rule that hijacks a subdomain you forgot about.
The Default Rule: Your Catch-All Safety Net
Every listener has a default rule. You set it when you create the listener. This is your “if all else fails” rule. It’s crucial. It should point to a sensible default service, perhaps your main web application, or even a simple target group that serves a friendly 404 page. Never leave it unconfigured; that’s just asking for trouble. And remember, any request that doesn’t match your higher-priority rules will land here, so make sure your rule logic is airtight to avoid dumping errors into your user-facing app.
Pro-Tips from the Trenches
- Priority is a Number, Not a Ranking: You set priority as an integer (1, 2, 3…). Lower numbers win. When you add a new rule, you must choose a priority number that isn’t taken. The AWS console will often just assign the next available high number, which can break your carefully crafted order. Always check the priority list after making changes.
- The 100-Rule Limit is Real: A single listener can only have 100 rules. This sounds like a lot until you start building a huge multi-tenant platform. If you hit this, it’s a sign you need to consolidate rules or use host-based routing more aggressively.
- Health Checks are Per Target Group: This is the best part. If your
/reportsservice goes down and fails its health checks, the ALB will stop sending traffic to it without affecting your main app. This isolation is the entire point of this architecture. Use it. - Test Weird Paths: Don’t just test
/reports/monthly. Test/reports../etc/passwd(yes, path normalization is handled, but test it anyway). Test/reportsand/reports/. Your rules are the first line of defense; make sure they’re not doing something stupid.