In this article, we’ll take you on a quick tour of the new Lua event framework shipped with HAProxy 2.8—plus demonstrate how Lua helps you further customize HAProxy’s behavior.
We’ll start with a brief, high-level overview of Lua and how HAProxy uses it. Next, we’ll introduce you to the Lua event framework, show you what you can do with it, and familiarize you with its mechanics.
Finally, we’ll describe the steps required to use the event framework in a Lua script by solving a practical use case.
What is Lua?
According to Lua’s developers, here’s what sets Lua apart:
Lua is a powerful, efficient, lightweight, embeddable scripting language. It supports procedural programming, object-oriented programming, functional programming, data-driven programming, and data description.
Lua is an interpreted language implemented in C, making it easily portable and embeddable. As a result, you’ll often find it used for scripted add-ons and applications—especially for game development.
You can find more info on Lua’s website, which is a fundamental tool while learning to use Lua. You’ll quickly find examples, documentation, and links to useful resources from there.
How Does HAProxy Support Lua?
We integrated Lua support with HAProxy 1.6 thanks to the work of Thierry Fournier. HAProxy now features a custom Lua interpreter on top of the Lua library. This interpreter works closely with HAProxy core to offer HAProxy-dedicated features directly from user-loaded Lua scripts.
After years of API functionality and stability gains, Lua has made HAProxy extendable with the following add-ons:
Custom background tasks
Custom fetches and converters
Sidecar TCP or HTTP services
But that’s not all! The best thing about Lua scripting—compared to patching the core C code—is that Lua script modifications don’t require you to recompile HAProxy. Therefore, it’s very easy to test and maintain custom features.
How Can You Benefit From Lua Support in HAProxy?
For HAProxy Enterprise and HAProxy ALOHA customers, Lua is supported out of the box. If you’re using HAProxy Community edition, here are some things to note:
If you’re getting HAProxy from your usual package manager, chances are that HAProxy already has Lua support and you have nothing to worry about.
However, if you’re manually compiling it from the sources, you should first install the Lua library. This is the only required dependency for Lua to work on HAProxy.
Currently, we recommend using Lua 5.4. While Lua 5.3 users can continue to use it with HAProxy, note that Lua no longer maintains this version. Newer builds will keep receiving security patches and bug fixes.
You’ll find the latest manual compilation source within Lua’s Download area. Alternatively, you can install it from your package manager if it’s available (often named after liblua.5.X.Y
package).
After installing the Lua library on your system, you simply need to enable Lua support when compiling HAProxy. You can do this by appending USE_LUA=1
on your usual compilation line:
$ make TARGET=linux-glibc USE_LUA=1 -j$(nproc) |
The makefile will automatically detect the Lua library path and use it to compile HAProxy.
When working with multiple Lua library versions on your system, you may need to provide explicit paths for both the include and library paths. You can do this by appending LUA_LIB
and LUA_INC
compiling hints. This ensures that HAProxy is exclusively built and linked with the proper version!
$ make TARGET=linux-glibc USE_LUA=1 LUA_LIB=/path/to/lua/lib LUA_INC=/path/to/lua/lib/include -j$(nproc) |
Checking for Lua support in HAProxy:
You can check for Lua support using this quick command:
$ ./haproxy -vv | grep Lua | |
Built with Lua version : Lua 5.4.6 |
What Can You Do With Lua in HAProxy?
Lua extends HAProxy to help you customize the behavior of your load balancer. Plus, Lua’s feature set is constantly growing as the Lua API evolves. Here’s a snapshot:
Extract HAProxy metrics/data
Fetch and set HAProxy dynamic variables, some of which may be used for ACL or routing logic
Implement custom sample fetches or converters as Lua functions and use them as native ones from the HAProxy configuration file.
Execute background tasks (perform periodic checks, cleanups, etc.)
Make TCP/HTTP requests (trigger external actions, fetch content, etc.)
Answer TCP/HTTP requests (implement custom web services, authenticators, etc.)
Capture, edit and filter HTTP requests and responses on the fly
Track server events (Since 2.8)
For more information, we encourage you to browse our HAProxy Lua documentation. You can also find our Lua API markup for HAProxy within HAProxy sources at doc/lua-api/index.rst.
Alternatively, you can view the web version on Thierry Fournier’s website. Here’s the base URL:
https://www.arpalert.org/src/haproxy-lua-api/<haproxy-version>/index.html
This URL requires a quick change so it can reach the right resource. Simply replace the <haproxy-version>
portion of the URL with your preferred HAProxy version, following MAJOR.MINOR formatting.
Here’s an example using HAProxy 2.8:
https://www.arpalert.org/src/haproxy-lua-api/2.8/index.html
Why Should a Load Balancer be Programmable?
Although core HAProxy offers a wide range of configurable features, it’s actually impossible to meet everyone’s needs—which greatly differ from one environment to the next.
Using HAProxy with Lua makes your load balancer programmable. This lets you better adapt and modulate your load balancer beyond its standard configurations and integrate it with your target environment. Many see programmability as a way to overcome the technical challenges specific to one user or a group of users.
Programming HAProxy may form a bridge between HAProxy and your existing applications without requiring additional tools.
Moreover, programming greatly reduces the extra development costs needed to integrate HAProxy into unique environments and use cases. Unlike custom C-patching of the source code, it also reduces maintenance costs since the HAProxy Lua API is relatively mature and stable.
Our developers take great care to support Lua API functionality between HAProxy versions. If a Lua feature will be deprecated, we’ll preserve it for at least one extra version. This gives you time to switch to the new recommended method in the API and ensure that your scripts keep working. On the other hand, custom patching the core code can cause unexpected breakages due to internal code refactors—which do happen often between major HAProxy versions.
And because Lua is easier to learn and maintain than pure C, an experienced system administrator may comfortably use it for common tasks. Plus, they can directly leverage HAProxy features from the script itself.
Finally, relying on an open-source programming language like Lua for programming HAProxy has a lot of advantages. Both Lua and HAProxy have great communities. Users can ping the Lua community to help solve their problems, and HAProxy users may also share custom-made scripts to help others with similar constraints. We cover this later in our ”Can the HAProxy community help?” section.
What is the Lua Event Framework in HAProxy?
HAProxy 2.8 includes some interesting changes to Lua engine:
We first added support for dynamic servers in HAProxy 2.5. But prior to version 2.8, the integrated Lua API didn't cope well with dynamic servers. The Lua API didn’t account for any servers added or removed after startup.
We reworked the Lua engine to properly handle dynamic server changes. This is completely transparent to the user since API usage is backward-compatible with previous versions. Very few attributes like Server.name
were superseded by their method equivalent (like Server.get_name()
) so while they’ll eventually be removed, they’ll work for the time needed to migrate to the new recommended methods.
This may seem like a minor improvement. However, it’s an important change to mention because, without this internal update, Lua couldn’t properly handle server events.
In addition to that, we started to develop the event handler API in HAProxy. This internal API lets developers publish and subscribe to various process events.
The first real use case for this new API was to enable the long-awaited re-implementation of HAProxy mailers in Lua. We previously implemented mailers in plain C, which had some drawbacks: users couldn't customize them at all and they relied on old, hard-to-maintain HAProxy code.
To implement HAProxy mailers in Lua, we introduced the Lua event framework, and then we introduced the very first event family: SERVER events, which propagates server events from HAProxy core to Lua.
Server events—being the first use case—simply mark the beginning of the event framework feature. New event families should be added over time.
Accordingly, the event framework aims to be generic in the first place. It works by registering custom Lua functions that HAProxy calls asynchronously when the selected events occur.
What Can You Do With the Lua Event Framework?
The event framework is currently limited to server events (though this may change in the future). Yet despite this, the SERVER family already supports multiple event types!
Here’s a quick summary:
event_type | meaning |
---|---|
| server was added |
| server was removed |
| server state changed from UP to DOWN |
| server state changed from DOWN to UP |
| server state changed |
| server administrative state changed |
| server's check status update |
Each server event type provides general server information as well as per-type contextual information. The SERVER_CHECK event, for example, provides check-related information.
Thanks to the supported events above, here are some examples of what you can already do with Lua:
Send an email or SMS alert, or trigger webhooks, when a server goes DOWN
Trigger automated scenarios when a server is added or removed from HAProxy
Log and/or report server state changes into a custom log or monitoring solution
Preemptively adjust the server weight when checks start failing
Customize email alerts sent from HAProxy mailers (see Email alerts with Lua to try it and customize the included mailers.lua file as needed)
However, this list isn’t exhaustive, and the Lua event framework supports many more possible scenarios.
How to Use the Lua Event Framework
To get you up and running quickly, we’ll cover the basics of Lua events with a practical use case. In this example, you’ll learn how to do the following things in Lua:
Subscribe to events
Handle event notifications
Perform HTTP requests
Now, let’s talk about our test case: automatically triggering a webhook request from Lua when all servers from a specific backend are DOWN.
This would typically generate a backend 'name' has no server available!
message from HAProxy.
For the sake of the example, we’ll set up a fake webhook endpoint using https://webhook.site. But, this applies to any imaginable kind of web service—from an SMTP gateway to a Slack bot. For instance, you could send HTTP requests to an online SMS or email API service to inform SysOps that something unexpected happened.
Opening https://webhook.site from a web browser grants us access to a temporary private webhook URL, which waits for any pending HTTP requests:
From there, we can start writing our sample Lua script!
Writing the Lua script
The first thing to do is to create a file named backend_down_webhook.lua
. This lays the groundwork for our upcoming steps.
Performing an HTTP request
From there, we need to define a function that makes an HTTP request to the webhook URL. To make the HTTP call, we’ll use the httpclient class, which we can obtain with the core.httpclient()
function.
-- Make an http post request to a webhook endpoint to notify about a backend | |
-- going down at a given time. For the sake of the example, contextual data | |
-- is formatted in JSON, webhook handlers often rely on a combination of | |
-- headers and plain POST or JSON formatted POST payloads to extract arguments | |
-- from the request. | |
local function backend_is_down(backend, when) | |
local httpclient = core.httpclient() | |
local api_base_url = os.getenv("DEMO_WEBHOOK_URL") | |
print(api_base_url) | |
local request_body = string.format("{\ | |
\"type\":\"%s\",\ | |
\"name\":\"%s\",\ | |
\"when\":\"%d\"\ | |
}", | |
"backend_down", | |
backend, | |
when) | |
local response = httpclient:post{url = api_base_url, body=request_body} | |
if (response.status ~= 200) then | |
core.Alert("Error when making request to http endpoint") | |
end | |
end |
Webhook.site acts as a dummy webhook that intercepts HTTP requests and reports their content on a webpage. It doesn't wait for specific inputs, but real webhook services often wait for POST requests with contextual data in JSON. We’re pretending that’s also the case here by embedding type
, name
, and when
variables in the request.
We’ll pass the HTTP URL to the Lua script through an environment variable named DEMO_WEBHOOK_URL
. The os.getenv()
Lua function makes this possible.
Handling events from a callback function
Next, we’ll create the Lua function that HAProxy will ultimately call. This occurs when a server being watched for the SERVER_DOWN event actually goes down.
-- this function will be called by event framework each time a server | |
-- from the chosen backend will be transitioning from UP to DOWN | |
local function server_down(event, data, mgmt, when) | |
local server = data.reference -- get a reference to the server | |
if server == nil then | |
-- the server has been removed, we cannot fetch | |
-- (should not happen in our testcase) | |
return | |
end | |
if server:get_proxy():get_srv_act() == 0 then | |
-- no more active servers within the backend, trigger an alert | |
backend_is_down(server:get_proxy():get_name(), when) | |
end | |
end |
You’ll see in the callback function above that we’re not using the event framework’s event
or mgmt
variables, since we don’t need them.
Under normal circumstances, the event
variable lets you differentiate between multiple event types when the callback function is used for multiple subscriptions. Meanwhile, mgmt
lets you manage the subscription from the callback function This includes canceling the subscription at any time.
Subscribing to events through a callback function
Finally, let’s create a subscription that will call the server_down
function on the SERVER_DOWN event for all servers within the "test" backend.
We’ll do this once when HAProxy is fully initialized (thanks to register_init
). This guarantees that all backends and servers from the configuration are available from the Lua context:
core.register_init(function() | |
-- register server_down function for DOWN event for every server | |
-- within "test" backend | |
for srv_name, srv in pairs(core.backends["test"].servers) do | |
srv:event_sub({"SERVER_DOWN"}, server_down) | |
end | |
end) |
Testing the script
For this demo project, we’ll use the full script that we’ve written thus far, which we’ve hosted on our HAProxy GitHub profile.
You can either test the script using the Docker reproducer by following the instructions from the above link, or test it under your usual test environment using the config file and directives below:
haproxy.cfg
configuration file:
global | |
stats socket ipv4@127.0.0.1:9999 level admin | |
lua-load backend_down_webhook.lua | |
# by default, the httpclient DNS resolver will use the IPv6 address, | |
# this changes it to prefer IPv4 | |
httpclient.resolvers.prefer ipv4 | |
defaults | |
timeout connect 5s | |
timeout server 5s | |
timeout client 5s | |
resolvers default | |
# we need to define a default resolver section | |
# for the httpclient in order to resolve hostnames in the URL | |
nameserver ns1 8.8.8.8:53 | |
backend test | |
server webserver1 127.0.0.1:8080 | |
server webserver2 127.0.0.1:8081 |
Complete these steps to begin testing:
Start HAProxy 2.8 or later using
DEMO_WEBHOOK_URL="personal url from webhook.site" haproxy -f haproxy.cfg
.Watch the webhook request catcher from your web browser. No requests should appear yet.
Set your webserver1 server to DOWN by forcing its health to down with the
echo "set server test/webserver1 health down" | socat stdio tcp4-connect:127.0.0.1:9999
command.Watch the webhook request catcher. No requests should appear.
Set your webserver2 server to DOWN by forcing maintenance mode with the
echo "set server test/webserver2 state maint" | socat stdio tcp4-connect:127.0.0.1:9999
command.
A new request should now show up in the webhook request catcher:
What’s next?
We can further improve our example script with a few changes and additions.
First, we can make the backend name configurable from our HAProxy configuration, instead of hardcoding it in the Lua script. For that, we’ll need to use optional arguments that we can pass to the lua-load
config directive.
Here’s a sample lua-load
directive:
global | |
lua-load backend_down_webhook.lua backend_name |
In our example, we should replace backend_name
with “test.”
Then we need to edit our init function in the backend_down_webhook.lua
script to use those optional arguments:
local arguments = table.pack(...) -- fetch global arguments from lua-load | |
core.register_init(function() | |
-- foreach backend name in arguments | |
for index,name in ipairs(arguments) do | |
-- register server_down function for DOWN event for every server | |
-- within current backend | |
for srv_name, srv in pairs(core.backends[name].servers) do | |
srv:event_sub({"SERVER_DOWN"}, server_down) | |
end | |
end | |
end) |
You can pass multiple arguments to lua-load and lua-load-per-thread directives.
Otherwise, we can add another event listener on the SERVER_ADD event. This will ensure that new servers added dynamically to tracked backends are also considered so that we don't miss any DOWN events!
-- watch for new server addition | |
core.event_sub({"SERVER_ADD"}, function(event, data) | |
local server = data.reference -- get a reference to the server | |
if server == nil then | |
return | |
end | |
-- as new server has been added dynamically, check if it belongs to | |
-- one of the tracked backends passed as optional lua-load arguments | |
for index,name in ipairs(arguments) do | |
if name == server:get_proxy():get_name() then | |
-- track the server for DOWN event | |
server:event_sub({"SERVER_DOWN"}, server_down) | |
break | |
end | |
end | |
end) |
For reference, here’s the "finished" script.
Since this is just a starting point, you’re free to improve or customize your scripts even further. We hope this example helped you see the Lua event framework’s growing potential.
If you want to learn more about this feature, take a look at the mailer implementation in Lua which heavily relies on it. We also encourage you to read the related Lua documentation for a full API overview complete with examples and technical considerations.
Can the HAProxy Community Help?
As always, we highly encourage HAProxy community members like you to contribute to the project however possible. Feel free to share your custom scripts and use cases to inspire other Lua enthusiasts.
Do you already use the new Lua event framework, or do you plan to use it? Let us know! We’d be happy to share awesome, community-driven content.
Conclusion
The Lua event framework brings new automation capabilities that once required third party or in-house, log-parsing middlewares. We expect this feature to evolve with users' needs and unlock deeper integrations with other services. As always, stay tuned for upcoming updates!
Subscribe to our blog. Get the latest release updates, tutorials, and deep-dives from HAProxy experts.