An HAProxy map file stores key-value pairs and is the starting point for some inventive behavior including dynamic rate limiting and blue-green deployments.
Dictionaries. Maps. Hashes. Associative arrays. Can you imagine life without these wonderful key-value data structures? The mad, dystopian world it would be? They’re just sort of there when needed: a trusty tool, never too far out of reach.
So maybe you’re not entirely shocked that they’re counted among the HAProxy load balancer’s extensive feature set. They’re called maps and are built-in and ready to roll. What you’re probably not expecting are the imaginative tasks that you can tackle with them.
Want to set up blue-green deployments? Maybe you’d like to set up rate limits by URL path? How about just dynamically switching which backend servers are used for a domain? It’s all done with maps!
In this post, you’ll learn how to create a map file, store it on your system, reference it in your HAProxy configuration, and update it in real time. You’ll also see some useful scenarios in which to put your new knowledge to good use.
Getting Started
Before considering the fascinating things you can do with a map file, let’s wrap our minds around what a map file is.
The map file
Everything starts by creating a map file. Fire up your favorite text editor and create a file named hosts.map. Then add the following lines to it:
# A comment begins with a hash sign | |
static.example.com be_static | |
www.example.com be_static | |
# You can add additional comments, but they must be on a new line | |
example.com be_static | |
api.example.com be_api |
A few things to note about the structure of this file:
It’s plain text
A key begins each line (e.g. static.example.com)
A value comes after a key, separated by at least one space (e.g. be_static)
Empty lines and extra whitespace between words are ignored
Comments must begin with a hash sign and must be on their own line
A map file stores key-value pairs. HAProxy uses them as a lookup table, such as to find out which backend to route a client to based on the value of the Host header. The benefit of storing this association in a file rather than in the HAProxy configuration itself is the ability to change those values dynamically.
Next, transfer this file to a server where you have an instance of HAProxy that you don’t mind experimenting with and place it into a directory of your choice. In these examples, since we’re using HAProxy Enterprise, we’ll store it under /etc/hapee-1.8/maps. Map files are loaded by HAProxy when it starts, although, as you’ll see, they can be modified during runtime without a reload.
Map files are loaded into an Elastic Binary Tree format so you can look up a value from a map file containing millions of items without a noticeable performance impact.
Map converters
To give you an idea of what you can do with map files, let’s look at using one to find the correct backend pool of servers where users should be sent. You will use the hosts.map file that you created previously to look up which backend should be used based on a given domain name.
Begin by editing your haproxy.cfg file. As you will see, you will add a map converter that reads the map file and returns a backend
name.
A converter is a directive placed into your HAProxy configuration that takes in an input and returns an associated output. There are various types of converters. For example, you might use the lower
converter to change a given string to lowercase or url_dec
to URL decode it.
In the following example, the input is a string literal example.com and the map
converter looks up that key in the map file, hosts.map.
frontend fe_main | |
bind :80 | |
use_backend %[str(example.com),map(/etc/hapee-1.8/maps/hosts.map)] |
The first row in hosts.map that has example.com as a key will have its value returned. Notice how the input, str(example.com)
, begins the expression and is separated from the converter with a comma.
When this expression is evaluated at runtime, it will be converted to the line use_backend be_static
, which directs requests to the be_static pool of servers. Of course, rather than passing in a hardcoded string like example.com, you can send in the value of an HTTP header or a URL parameter. The next example uses the value of the Host header as the input.
use_backend %[req.hdr(host),lower,map(/etc/hapee-1.8/maps/hosts.map,be_static)] |
The map
converter takes up to two arguments. The first is the path to your map file. The second, optional argument declares a default value that will be used if no matching key is found. So, in this case, if there’s no match, be_static will be used. If the input matches multiple items in the map file, HAProxy will return the first one.
The map
converter looks for an exact match in the file, but there are a few variants that provide opportunities for a partial match. The most commonly used are summarized here:
| Looks for entries in the map file that match the beginning of the input (e.g. an input of “abcd” would match “a” in the file). |
| Looks for entries in the map file that match the end of the input (e.g. an input of “abcd” would match “d” in the file). Unlike the other match modes, this doesn’t perform ebtree lookups and instead checks each line. |
| Looks for entries in the map file that make up a substring of the sample (e.g. an input of “abcd” would match “ab” or “c” in the file). Unlike the other match modes, this doesn’t perform ebtree lookups and instead checks each line. |
| This takes the input as an IP address and looks it up in the map. If the map has masks (such as 192.168.0.0/16) in it then any IP in the range will match it.This is the default if the input type is an IP address. |
| This reads the samples in the map as regular expressions and will match if the regular expression matches.Unlike the other match modes, this doesn’t perform ebtree lookups and instead checks each line. |
| An alias for |
Modifying the Values
Much of the value of map files comes from your ability to modify them dynamically. This allows you to, for example, change the flow of traffic from one backend to another, such as for maintenance.
There are four ways to change the value that we get back from a map file. First, you can change the values by editing the file directly. This is a simple way to accomplish the task but does require a reload of HAProxy. This is a good choice if you’re using a configuration management tool like Puppet or Ansible.
The second way is provided with HAProxy Enterprise via the lb-update module, which you’ll really appreciate if you’re running a cluster of load balancers. It allows you to update maps within multiple instances of HAProxy at once by watching a map file hosted at a URL at a defined interval.
A third way to edit the file’s contents is by using the Runtime API. The API provides all of the necessary CRUD operations for creating, removing, updating, and deleting rows from the map in memory, without needing to reload HAProxy. There’s also a simple technique for saving your changes to disk, which you’ll see later in this post.
A fourth way is with the http-request set-map
directive in your HAProxy configuration file. This gives you the opportunity to update map entries based on URL parameters in the request. It’s easy to turn this into a convenient HTTP-based interface for making map file changes from a remote client.
In the next few sections, you’ll get some guidance on how to use these techniques.
Editing the file directly
A straightforward way to change the values you get back from your map file is to change the file itself. Open the file and make any modifications you need: adding rows, removing others, changing the values of existing rows. However, know that HAProxy only reads the file when it’s starting up and then loads it into memory. Refreshing the file, then, means reloading HAProxy.
Thanks to hitless reloads introduced in HAProxy Enterprise 1.8r1 and HAProxy 1.8, you can trigger a reload without dropping active connections. Read our blog post Hitless Reloads with HAProxy – HOWTO for an explanation on how to use this feature.
This approach will work with configuration management tools like Puppet, which allow you to distribute changes to your servers at a set interval. Be sure to reload HAProxy to pick up the changes.
Editing with the lb-update module
Although configuration management tools allow you to update the servers in your cluster, they can be a heavy solution that requires administration. An alternative is using the lb-update module to keep each replica of HAProxy within your cluster in sync. The lb-update module instructs HAProxy Enterprise to retrieve the contents of the map file from a URL at a defined interval. The module will automatically check for updates as frequently as configured. This is especially useful when there are a lot of processes and/or servers in a cluster that need the updated files.
The lb-update module can also be used to synchronize TLS ticket keys.
Below is a sample of a dynamic-update
section that manages updating the hosts.map file from a URL. You’d add an update
directive for each map file that you want to watch.
dynamic-update | |
update id /etc/hapee-1.8/maps/sample.map url http://10.0.0.1/sample.map delay 300s |
See the HAProxy Enterprise documentation for detailed usage instructions or contact us to learn more.
Editing with the Runtime API
Looking back at our previous blog post, Dynamic Configuration with the HAProxy Runtime API, you’ll see that there are several API methods available for updating an existing map file.
The table below summarizes them.
API method | Description |
---|---|
| Lists available map files or displays a map file’s contents. |
| Reports the keys and values matching a given input. |
| Modifies a map entry. |
| Adds a map entry. |
| Deletes a map entry. |
| Deletes all entries from a map file. |
Without any parameters, show map
lists the map files that are loaded into memory. If you give it the path to a particular file, it will display its contents. In the following example, we use it to display the key-value pairs inside hosts.map.
root@server1:~$ echo "show map /etc/hapee-1.8/maps/hosts.map" | socat stdio /var/run/hapee-1.8/hapee-lb.sock | |
0x1605c10 static.example.com be_static | |
0x1605c50 www.example.com be_static | |
0x1605c90 example.com be_static | |
0x1605cd0 api.example.com be_api |
The first column is the location of the entry and is typically ignored. The second column is the key to be matched and the third is the value. We can easily add and remove entries via the Runtime API. To remove an entry from the map file, use del map
. Note that this only removes it from memory and not from the actual file.
root@server1:~$ echo "del map /etc/hapee-1.8/hosts.map static.example.com" | socat stdio /var/run/hapee-1.8/hapee-lb.sock |
You can also delete all entries with clear map
:
root@server1:~$ echo "clear map /etc/hapee-1.8/maps/hosts.map" | socat stdio /var/run/hapee-1.8/hapee-lb.sock |
Add a new key and value with add map
:
root@server1:~$ echo "add map /etc/hapee-1.8/maps/hosts.map foo.example.com be_bar" | socat stdio /var/run/hapee-1.8/hapee-lb.sock |
Change an existing entry with set map
:
root@server1:~$ echo "set map /etc/hapee-1.8/maps/hosts.map foo.example.com be_baz" | socat stdio /var/run/hapee-1.8/hapee-lb.sock |
Using show map
, we can get the contents of the file, filter it to only the second and third columns with awk, and then save the in-memory representation back to disk:
root@server1:~$ echo "show map /etc/hapee-1.8/maps/hosts.map" | socat stdio /var/run/hapee-1.8/hapee-lb.sock | awk '{print $2" "$3}' > /etc/hapee-1.8/maps/hosts.map |
Actions can also be chained together with semicolons, which makes it easy to script changes and save the result:
root@server1:~$ echo "clear map /etc/hapee-1.8/maps/hosts.map; add map /etc/hapee-1.8/maps/hosts.map bar.example.com be_foo; add map /etc/hapee-1.8/maps/hosts.map foo.example.com be_baz" | socat stdio /var/run/hapee-1.8/hapee-lb.sock |
If you are forking HAProxy with multiple processes via nbproc
, you’ll want to configure one socket per process and then run a loop to update each process individually. This is not an issue when using multithreading.
Editing with http-request set-map
Suppose you didn’t want to go about editing files by hand or using the Runtime API. Instead, you wanted to be able to make an HTTP request with a certain URL parameter and have that update your map file. In that case, http-request set-map
is your go-to.
This allows the use of fetches, converters, and ACLs to decide when and how to change a map during runtime. In addition to set-map
, there’s also del-map
, which allows you to remove map entries in the same way. As with the runtime API, these changes also only apply to the process that the request ends up on.
Pass the map file’s path to set-map
and follow it with a key and value, separated by spaces, that you want to add or update. Both the key and value support the log-format notation, so you can specify them as plain strings or use fetches and converters. For example, to add a new entry to the hosts.map file, but only if the source address falls within the 192.168.122.0/24 range, you can use a configuration like this:
frontend fe_main | |
bind :80 | |
acl in_network src 192.168.122.0/24 | |
acl is_map_add path_beg /map/add | |
http-request set-map(/etc/hapee-1.8/maps/hosts.map) %[url_param(domain)] %[url_param(backend)] if is_map_add in_network | |
http-request deny deny_status 200 if { path_beg /map/ } | |
use_backend %[req.hdr(host),lower,map(/etc/hapee-1.8/maps/hosts.map)] |
This will allow you to make web requests such as http://192.168.122.64/map/add?domain=example.com&backend=be_static for a quick and easy way to update your maps. If the entry already exists, it will be updated. Notice that you can use http-request deny deny_status 200
to prevent the request from going to your backend servers.
The http-request del-map
command is followed by the key to remove from the map file.
acl is_map_del path_beg /map/delete | |
http-request del-map(/etc/hapee-1.8/maps/hosts.map) %[url_param(domain)] if is_map_del in_network |
Using the show map
technique you saw earlier, you might schedule a cron job to save your map files every few minutes. However, if you need to replicate these changes across multiple instances of HAProxy, using one of the other approaches will be a better bet.
DID YOU KNOWAnother way to control when to set or delete an entry is to check the method of the request and then set an entry if it’s POST or PUT. If it’s DELETE, delete an entry.
Putting it into Practice
We’ve seen how to use the Host header to look up a key in a map file and choose a backend to use. Let’s see some other ways to use maps.
A blue-green deployment
Suppose you wanted to implement a blue-green deployment wherein you’re able to deploy a new release of your web application onto a set of staging servers and then swap them with a set of production servers. You could create a file called bluegreen.map and add a single entry:
active be_blue |
In this scenario, the be_blue backend
contains your set of currently active, production servers. Here is your HAProxy configuration file:
frontend fe_main | |
bind :80 | |
use_backend %[str(active),map(/etc/hapee-1.8/maps/bluegreen.map)] | |
backend be_blue | |
server server1 10.0.0.3:80 check | |
server server2 10.0.0.4:80 check | |
backend be_green | |
server server1 10.0.0.5:80 check | |
server server2 10.0.0.6:80 check |
After you deploy a new version of your application to the be_green servers and test it, you can use the Runtime API to swap the active be_blue servers with the be_green servers, causing your be_green servers to become active in production.
root@server1:~$ echo "set map /etc/hapee-1.8/maps/bluegreen.map active be_green" | socat stdio /var/run/hapee-1.8/hapee-lb.sock |
Now your traffic will be directed away from your be_blue servers and to your be_green servers. This, unlike a rolling deployment, ensures that all of your users are migrated to the new version of your application at the same time.
Rate limiting by URL path
For this example, you will set rate limits for your website. Using a map file lets you set different limits for different URLs. For example, URLs that begin with /api/routeA may allow a higher request rate than those that begin with /api/routeB.
Add a map file called rates.map and add the following entries:
/api/routeA 40 | |
/api/routeB 20 |
Consider the following frontend
, wherein the current request rate for each client is measured over 10 seconds. A URL path like /api/routeA/someFunction would allow up to four requests per second (40 requests / 10 seconds = 4 rps).
frontend api_gateway | |
bind :80 | |
default_backend api_servers | |
# Set up stick table to track request rates | |
stick-table type binary len 8 size 1m expire 10s store http_req_rate(10s) | |
# Track client by base32+src (Host header + URL path + src IP) | |
http-request track-sc0 base32+src | |
# Check map file to get rate limit for path | |
http-request set-var(req.rate_limit) path,map_beg(/etc/hapee-1.8/maps/rates.map) | |
# Client's request rate is tracked | |
http-request set-var(req.request_rate) base32+src,table_http_req_rate(api_gateway) | |
# Subtract the current request rate from the limit | |
# If less than zero, set rate_abuse to true | |
acl rate_abuse var(req.rate_limit),sub(req.request_rate) lt 0 | |
# Deny if rate abuse | |
http-request deny deny_status 429 if rate_abuse |
Here, the stick-table
definition records client request rates over ten seconds. Note that we are tracking clients using the base32+src
fetch method, which is a combination of the Host header, URL path, and source IP address. This allows us to track each client’s request rate on a per-path basis. The base32+src
value is stored in the stick table as binary data.
Then, two variables are set with http-request set-var
. The first, req.rate_limit, is set to the predefined rate limit for the current path from the rates.map file. The second, req.request_rate, is set to the client’s current request rate.
The ACL rate_abuse does a calculation to see whether the client’s request rate is higher than the limit for this path. It does this by subtracting the request rate from the request limit and checking whether the difference is less than zero. If it is, the http-request deny
directive responds with 429 Too Many Requests.
Conclusion
Now that you’ve seen a few of the possibilities, consider reaching for your trusty tool, maps, the next time you run into a problem where it can help. What would you use maps for? Leave us a comment here or start a conversation with us on Twitter!
Planning to update maps across a cluster of HAProxy load balancers? Try out a free trial of HAProxy Enterprise so that you can take advantage of the lb-update module. Being an HAProxy Enterprise customer also includes expert technical support, so we can help you plan the map rules that will solve your specific problems.