Getting started

Introduction to HAProxy Lua programming

HAProxy and HAProxy Enterprise include the Lua interpreter, which allows you to extend the load balancer’s functionality with custom Lua scripts. This guide is a quick introduction.

You may also find the following links helpful:

The basics Jump to heading

In the global section of your configuration file, define the Lua files that you want to load. HAProxy and HAProxy Enterprise read these files before processing any chroot lines in the configuration, so they can be placed outside of your chrooted directory if needed.

HAProxy and HAProxy Enterprise have a non-blocking architecture and, because of that, Lua scripts must be written in a way that avoids reading files, making network calls, or performing other actions that might block the main thread of the load balancer process during runtime. The best place to read files and perform other blocking IO calls is within a core.register_init function. Then store that data in a global variable so that it can be accessed during runtime from a core.register_service block.

Inside your Lua script you will add blocks that start with core.register_service. These define functions that the load balancer will execute during runtime whenever one of the following configuration directives fires:

  • tcp-request content use-service lua.<name used in register>
  • tcp-response content use-service lua.<name used in register>
  • http-request use-service lua.<name used in register>
  • http-response use-service lua.<name used in register>

Hello world Jump to heading

First, let’s consider a basic hello world example. This will echo back (i.e. print to the screen) the the URL that the client requested.

  1. Create a file named /tmp/hello_world.lua with the following content:

    hello_world.lua
    lua
    core.register_service("hello_world_tcp", "tcp", function(applet)
    applet:send("hello world\n")
    end)
    core.register_service("hello_world_http", "http", function(applet)
    local response = "The path which was requested is: '" .. applet.path .. "'\n"
    applet:set_status(200)
    applet:add_header("content-length", string.len(response))
    applet:add_header("content-type", "text/plain")
    applet:start_response()
    applet:send(response)
    end)
    hello_world.lua
    lua
    core.register_service("hello_world_tcp", "tcp", function(applet)
    applet:send("hello world\n")
    end)
    core.register_service("hello_world_http", "http", function(applet)
    local response = "The path which was requested is: '" .. applet.path .. "'\n"
    applet:set_status(200)
    applet:add_header("content-length", string.len(response))
    applet:add_header("content-type", "text/plain")
    applet:start_response()
    applet:send(response)
    end)

    This defines two services:

    • a simple hello_world_tcp service that will blindly respond to any TCP connection (so no headers/etc) with hello world.
    • a more complicated HTTP example that will send a response with headers/etc.
  2. Add the following lines to your load balancer configuration:

    haproxy
    global
    lua-load /tmp/hello_world.lua
    frontend http_test
    bind 127.0.0.1:81
    mode http
    tcp-request inspect-delay 1s
    http-request use-service lua.hello_world_http
    frontend tcp_test
    bind 127.0.0.1:82
    mode tcp
    tcp-request inspect-delay 1s
    tcp-request content use-service lua.hello_world_tcp
    haproxy
    global
    lua-load /tmp/hello_world.lua
    frontend http_test
    bind 127.0.0.1:81
    mode http
    tcp-request inspect-delay 1s
    http-request use-service lua.hello_world_http
    frontend tcp_test
    bind 127.0.0.1:82
    mode tcp
    tcp-request inspect-delay 1s
    tcp-request content use-service lua.hello_world_tcp
  3. Restart the load balancer.

  4. Make a request to the load balancer at 127.0.0.1:81 (i.e. view it in a web browser). It will show the URL that you requested.

Common Lua tasks Jump to heading

In this section, we demonstrate common ways to use Lua.

Verify a request with another service Jump to heading

Using Lua, you can have the load balancer validate some requests before servicing them.

  1. Create a Lua script called verify_request.lua:

    verify_request.lua
    lua
    core.register_action("verify_request", { "http-req" }, function(txn)
    -- Verify that the request is authorized
    -- Obviously stupid in this case without additional information being sent
    local s = core.tcp()
    -- Should be pointing to a frontend with balancing/health checks/etc.
    s:connect("127.0.0.1:8080")
    -- We use HTTP 1.0 because we don't support keepalive or any other advanced features in this script.
    s:send("GET /verify.php?url=" .. txn.sf:path() .. " HTTP/1.1\r\nHost: veriy.example.com\r\n\r\n")
    local msg = s:receive("*l")
    -- Indicates a connection failure
    if msg == nil then
    -- This leave txn.request_verified unset for potentially different handling
    return
    end
    msg = tonumber(string.sub(msg, 9, 12)) -- Read code from 'HTTP/1.0 XXX'
    -- Makes it easy to test by making any file to be denied.
    if msg == 404 then
    txn.set_var(txn,"txn.request_verified",true)
    else
    txn.set_var(txn,"txn.request_verified",false)
    end
    -- Read the response body, though in this example we aren't using it.
    msg = s:receive("*l")
    end)
    verify_request.lua
    lua
    core.register_action("verify_request", { "http-req" }, function(txn)
    -- Verify that the request is authorized
    -- Obviously stupid in this case without additional information being sent
    local s = core.tcp()
    -- Should be pointing to a frontend with balancing/health checks/etc.
    s:connect("127.0.0.1:8080")
    -- We use HTTP 1.0 because we don't support keepalive or any other advanced features in this script.
    s:send("GET /verify.php?url=" .. txn.sf:path() .. " HTTP/1.1\r\nHost: veriy.example.com\r\n\r\n")
    local msg = s:receive("*l")
    -- Indicates a connection failure
    if msg == nil then
    -- This leave txn.request_verified unset for potentially different handling
    return
    end
    msg = tonumber(string.sub(msg, 9, 12)) -- Read code from 'HTTP/1.0 XXX'
    -- Makes it easy to test by making any file to be denied.
    if msg == 404 then
    txn.set_var(txn,"txn.request_verified",true)
    else
    txn.set_var(txn,"txn.request_verified",false)
    end
    -- Read the response body, though in this example we aren't using it.
    msg = s:receive("*l")
    end)
  2. Add a lua-load directive to the global section of your configuration to load the Lua script:

    haproxy
    global
    lua-load /tmp/verify_request.lua
    haproxy
    global
    lua-load /tmp/verify_request.lua
  3. Add the following to the configuration inside the frontend where you wish to validate requests:

    haproxy
    frontend example
    http-request lua.verify_request
    http-request deny if !{ var(txn.request_verified) -m bool }
    haproxy
    frontend example
    http-request lua.verify_request
    http-request deny if !{ var(txn.request_verified) -m bool }

The Lua code sets a variable named txn.request_verified that the load balancer can read. It then denies the request based on the value. You might also use the variable’s value to add a header, reduce a rate limit, or log the request.

Do you have any suggestions on how we can improve the content of this page?