HAProxy: routing HTTP requests by domain name

Recently, I have been looking into HAProxy as an alternative load balancer to NGINX. NGINX' free version lacks features vital to a multi-container environment. The most important features we need are upstream health checks and sticky sessions.

So far, least_conn or ip_hash have worked well enough, but cracks are beginning to show.
Two reasons forced the cracks to show:

  • Bad session handlers built into applications, forcing us away from least_conn to ip_hash
  • Heavy use from one IP address killing one container, and leaving others idle.

Being frugal is a good thing, so I started playing with HAProxy, discovering its features.

HAProxy supports the features mentioned above, but getting up and running took some experimenting. The docs for HAProxy are vast, and I felt overwhelmed.
Finally, I figured out routing HTTP requests based on domain names, is possible in several ways, using ACLs.

Direct ACL in haproxy.cfg

First, you define an ACL with acl <name-of-acl> hdr(host) -i <fqdn>. The -i flag, tells hdr(host) to perform a case-insensitive check on hdr(host).
Then, you can route the requests to their backends, using the use_backend directive:

use_backend <backend-name> if <name-of-acl>

Here is a cleaned up snippet:

frontend fe-http-incoming
  bind :80

  # Decide which ACL to set by reading host header
  acl goto-app-ghost     hdr(host) -i eng.eelcowesemann.nl
  acl goto-app-wordpress hdr(host) -i www.eelcowesemann.nl

  # Use wanted backend, based on ALC
  use_backend app-ghost     if goto-app-ghost
  use_backend app-wordpress if goto-app-wordpress

Mapping file

Another way to route requests is to use a mapping file. In this file, you have two columns like so:

cat /etc/haproxy/appservers.map
# hostname              backend
eng.eelcowesemann.nl    app-ghost-eng
zelda.eelcowesemann.nl  app-ghost-zelda
www.eelcowesemann.nl    app-ghost-www
iddqd.nl                static-iddqd

You can then tell HAProxy, you use this file to decide which backend to use based on the Host header.

frontend fe-http-incoming
  mode         http
  bind         :80
  use_backend  %[req.hdr(host),lower,map_dom(/etc/haproxy/appservers.map,app-ghost-eng)]

The use_backend line explained:
req.hdr(host): use the Host request header
lower: translate the Host header to lowercase
map_dom(/etc/haproxy/appservers.map): the path to the mapping file
app-ghost-eng: the default backend to use if none provided.

Which method to use

You can choose whichever method you want, and whether you use a configuration management tool like Puppet. It is easier to generate a separate mapping file, and to point haproxy.cfg towards that.
When not using configuration management, I would choose use the mapping file when the amount of host names you want to route exceeds ten entries, this way the main configuration file stays nice and clean.