Hey,

This is a quick tip on how you can switch from using ACLs (access control lists) to use maps for selecting backends based on a request parameter.

The usefulness of using maps instead of acls is that sometimes it might be easier to update the mappings rather than the ACLs (as well as allowing us to perform mutations on the maps without needing to spawn a new process due to the possibility of using the runtime api via unix sockets).

Overview

As an example, consider the job of selecting a backend based on a destination host as advertised by the Host header.

That backend might be configured with a great number of servers that are meant to deal with those requests arriving for that host.

Overview of a client communicating with backends using HAProxy as the HTTP frontend

For instance, a client performs a request against our load-balancer (HAProxy) which lives under a known address (test.com):

GET /hello HTTP/1.1
Host: test.com
User-Agent: Chrome

Once that requests lands on HAProxy, that HTTP request is then parsed and we’re able to make use of the HTTP headers to determine which set of servers (i.e., which backend) should take care of that request (one possibility is not matching a backend and then sending the traffic to a no-op “nomatch” backend that just replies back with 404).

The usual way of selecting a backend is using ACLs (access control lists) - for a more in-depth documentation of ACLs, see ACL basics on the official HAProxy documentation.

Selecting a backend using ACLs

To get started, let’s create a minimal configuration that can handle our example:

# Apply the `mode http` to every frontend and backend that
# doesn't specify a mode.
# 
# This is useful for us just to don't repeat ourselves throughout
# the config.
defaults
                mode http



# This main http frontend will be responsible for handling every
# request that comes to our load-balancer on port 80 (the default
# http port).
frontend        http
                bind                        *:80 

# Simple ACL to send traffic to our custom tailored web server.
#
# It looks at the `Host` header of the request, and if it matches
# `test.com`, then checking that `acl` will evaluate to `true`. 
                acl                         host_matches hdr_dom(host) test.com
# Make use of the backend `desired_backend` if the acl `host_matches`
# evaluates to `true`.
                use_backend                 desired_backend if host_matches
# By default use the `not_found` no-op backend.
                default_backend             not_found



# A static backend that will just serve some files from a local server
backend         desired_backend
                server                      myserver localhost:8000



# A dumb "not-found" auto-responder using HAProxy's errorfile
# directive.
#
# As the ACL that send traffic to this backend is the default
# (and least prioritized), when we reach this backend, it means
# that we didn't find a desired backend, thus we should serve
# a 404 instead of the generic 503 that haproxy gives back to
# clients when something goes wrong.
backend         not_found
                errorfile                   503 /tmp/sample/errorfiles/404.http

With that configuration, you can take HAProxy up and start seeing the requests with Host set to test.com succeeding and those with a different Host (for instance, Host: localhost) failing (404).

Looking at what’s going on we can see that that’s a job that is very well suited for a map lookup.

Selecting a backend using Maps

Using maps, we can remove acl completely and just rely on use_backend with the map lookup syntax.

defaults
                mode http

frontend        http
                bind                        *:80 

# Use the backend found in the map whose key matches the
# `Host` header.
#
# If nothing is found, use the default `not_found` backend
# (which will 404).
                use_backend	%[req.hdr(host),lower,map_str(/tmp/sample/maps/map1,not_found)]


backend         desired_backend
                server                      myserver localhost:8000


backend         not_found
                errorfile                   503 /tmp/sample/errorfiles/404.http

As you can see, we can simplify the configuration by making use of the map, but we need to know in advance what are the available backends and also have a map file.

HAProxy performing a map lookup

The map file is far from complicated though - it’s a simple line-by-line key-value file where each key is separated from its value using a space:

test.com desired_backend
another-host.com another_backend
another-blabla.com haha_backend
...

Whenever you spin up HAProxy (or tell it to reload), it looks at this file and then puts that mapping into memory such that it can very quickly perform the lookups.

One way to make sure that your map has been properly loaded by issuing Unix Socket Commands against the HAProxy runtime API:

 # Make haproxy create a stats and runtime api socket
 # at `/tmp/haproxy.sock` with admin permissions
+ global
+                 stats socket /tmp/haproxy.sock mode 666 level admin
# Issuing the `show map` command to the running HAProxy
# instance.
#
# Here I pipe to a privileged instance of `socat` such
# that we can properly access the `/tmp/haproxy.sock`
# unix socket.
#
# If you didn't need extra privileges to raise HAProxy,
# then you don't need `sudo`.
#
# In the output you can see an internal address and
# the key-value pair.
echo "show map /tmp/sample/maps/map1" | \
        sudo socat stdio /tmp/haproxy.sock          
0x7fbb72d013d0 test.com desired_backend

One of the benefits of using maps is that we can use the Unix sockets API to modify the map without needing to restart the HAProxy process (there’s no easy way of changing the ACLs with the unix socket api):

# Send the `add map` command through the unix socket so that 
# internally, HAProxy will update the map.
#
# Note that if HAProxy restarts, it won't know about this
# newly added domain - it'll look for the mappings specified
# at the file and that'll be its source of truth.
#
# To keep consistency, make sure you also modify the map file.
echo "add map /tmp/sample/maps/map1 another-host.com another_backend" | \
        sudo socat stdio /tmp/haproxy.sock

# Check the newly added domain
echo "show map /tmp/sample/maps/map1" | \
        sudo socat stdio /tmp/haproxy.sock
0x7fbb72d013d0 test.com desired_backend
0x7fbb72d02a30 another-host.com another_backend

Closing thoughts

HAProxy maps might offer a great way of reducing (or simplifying) the usage of ACLs in your configuration.

Here I showed one use of it - one that’s actually very common where the name of the backends are well known and updating the map that matches a domain with a backend is needed - but there are certainly many more uses that one can think of.

Please let me know if you need any help or spotted an error - you can find me on Twitter (@cirowrc).

Have a good one!

finis