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.
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.
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