If you host dozens of web services that reside at various subdomains, TCP ports, and paths, then migrating them to live under a single address could simplify how clients access them and make your job of managing access easier. It would mean moving from a hodgepodge of address schemes, such as:
www.example.com/app-a/
www.example.com:8080/app-b/
app-c.example.com
to a single address wherein services are designated by the URL’s path:
www.example.com/app-a/
www.example.com/app-b/
www.example.com/app-c/
The good news is that you don’t need to rearrange your entire network to make this happen. You can deploy the HAProxy load balancer in front of your services and then use path-based routing to direct requests to the correct backend service. Here’s how to do it.
Configure Haproxy for Path-Based Routing
In the end, your HAProxy configuration will look like this:
frontend mysite | |
bind :80 | |
# route to a backend based on path's prefix | |
use_backend app-a if { path /a } || { path_beg /a/ } | |
use_backend app-b if { path /b } || { path_beg /b/ } | |
backend app-a | |
# strip the prefix '/a' off of the path | |
http-request replace-path /a(/)?(.*) /\2 | |
server server1 127.0.0.1:8080 check maxconn 30 | |
backend app-b | |
# strip the prefix '/b' off of the path | |
http-request replace-path /b(/)?(.*) /\2 | |
server server1 127.0.0.1:8081 check maxconn 30 |
Let’s dig into what it all means. Consider the frontend section, which receives requests. It contains a use_backend
directive that directs traffic for each application.
use_backend app-a if { path /a } || { path_beg /a/ } | |
use_backend app-b if { path /b } || { path_beg /b/ } |
The first line states that if the path is /a or if it begins with /a/, then route to the backend named app-a. If the path is /b or if it begins with /b/, then route to the backend named app-b. The path
fetch method compares the whole path, while path_beg
compares the beginning of the path. These lines route requests to a specified backend pool of servers when the given condition is true.
For example, the first line will match requests such as:
www.example.com/a
www.example.com/a/my-function
But it will not match:
www.example.com/aaa
www.example.com/abbb/my-function
If you would like to match on other prefixes, simply make a list of them:
use_backend app-a if { path /a /c /d } || { path_beg /a/ /c/ /d/ } |
This syntax can be a bit terse. So if you prefer a more verbose option, you can break out the conditional expression onto a separate line or lines. The syntax below has the same result but explicitly defines the condition using two acl directives. By having two such directives with the same name, they form a single expression separated by a logical or:
acl app-a-path path /a /c /d | |
acl app-a-path path_beg /a/ /c/ /d/ | |
use_backend app-a if app-a-path |
You can also use map files to store long lists of path-to-backend mappings.
Next, define the backend sections.
backend app-a | |
http-request replace-path /a(/)?(.*) /\2 | |
server server1 127.0.0.1:8080 check maxconn 30 | |
backend app-b | |
http-request replace-path /b(/)?(.*) /\2 | |
server server1 127.0.0.1:8081 check maxconn 30 |
In this example, the http-request replace-path
directives remove the prefixes /a and /b before relaying the request to the server. A request for the URL path /a/my-function would become just /my-function before reaching the server. It’s important that this directive goes into the backend and not the frontend because if it were in the frontend, it would replace the path, stripping off the prefix, before the use_backend
rules could try to match it, resulting in no match. In general, HAProxy applies http-request
rules before use_backend
rules. Therefore, it’s better to route the request first, then apply such transformations.
The http-request replace-path
directive expects a regular expression, such as /a(/)?(.*)
, which states that you want to match any path that begins with /a optionally followed by a forward slash, and then followed by anything else. The anything else part, enclosed in parentheses, is captured, meaning you can refer to it in the replacement value by number. In this instance, the anything else we captured is referenced with \2, since it is the second capture group (second set of parentheses).
Below are a few ways in which HAProxy will route requests and how the backend servers will see them:
Original requested path | Routes to server | Server sees |
/a/ | app-a | / |
/b | app-b | / |
/a?foo=bar | app-a | /?foo=bar |
/b/my-function?foo=bar | app-b | /my-function?foo=bar |
Conclusion
In this blog post, you learned how to configure path-based routing using HAProxy. Several key points to remember: define conditions for which application to route the request to by using the path
and path_beg
fetch methods to match the path, and you can strip off the prefix before the request is relayed to the server by using the http-request replace-path
directive.