Skip to main content

Automating HAProxy – Configuration Management

Posted on

Config management from human-readable documentation

Recently, my home lab grew to the point where manually updating HAProxy ACL rules became a chore. I needed to ensure my infrastructure could scale with minimal friction. Since HAProxy configurations can become unwieldy and difficult to manage, I designed and implemented a tool that lets me maintain a single, living document defining the business logic for HAProxy routing.

To tackle this, I chose Python as my weapon of choice. I needed something that could parse semi-structured data from a human-readable file format—something I could write quickly and clearly—and convert it into a format HAProxy could use.

I wanted HAProxy to interpret lines like the following:

https://www.foobar.com     -> http://foobarcom-internal-server-1:80  
https://foobar.com         -> http://foobarcom-internal-server-1:80  
https://stage.foobar.com   -> http://stage-foobarcom-internal-server-1:80  

My goal was to offload SSL termination to HAProxy so internal services wouldn’t need to manage HTTPS at all. I wanted to spin up new servers quickly, define a route, and go.


First Attempt: Functional Overkill?

My first draft used a set of functions: one to extract the config block from the README.md, two more to generate HAProxy config sections, and a final multi-line f-string to build the file contents, with a context manager to handle writing.

This worked, but a good friend (and fellow co-potato-battery-piloting-a-meatsuit-doomed-to-inevitable-decay) pointed out that my approach felt more like software engineering than systems administration. For sysadmins, readability and linear structure often trump modularity, especially in one-off scripts. It was a valid critique, and one I took to heart.


Refining the Approach: Simpler, More Explicit

I restructured the script around two clearly separated tasks:

  1. Parse the config block from README.md
  2. Generate the HAProxy configuration

Step 1: Extracting HAProxy Rules

with open('README.md', 'r') as file_handle:
    configuration_data = re.match(
        r'(?is).*# HAProxy Configuration.*`+\w*\n+(?P<conf>.*\w+)\n+`',
        file_handle.read()
    )

Then, I split the block and parsed each line:

for line in configuration_data.groupdict().get('conf').strip().split("\n"):
    if match := re.match(
        r'^\s+https://(?P<ingress_host>\S+)/?\s+->\s+http://(?P<destination_service>\S+)\s*$',
        line
    ):
        destination_service = match.group('destination_service')
        ingress_host = match.group('ingress_host')
        safe_ingress_host = re.sub(r'\W', '_', ingress_host)
        destination_service += ':80' if not re.match(r'.*:\d+$', destination_service) else ''

        configured_hosts[ingress_host] = {
            'safe_ingress_host': safe_ingress_host,
            'destination_service': destination_service
        }

Step 2: Generating the HAProxy Config

We begin by printing the static HAProxy preamble:

print("""
global
    # intermediate configuration
    ...
defaults
    timeout connect 5000
    timeout client 50000
    timeout server 50000
""")
Note: The preamble here is an excerpt for brevity.

For the full version please see the referenced GitHub repo: haproxy_config_automation_example

Then generate the ACLs:

for ingress_host in sorted(configured_hosts.keys()):
    print(f"""
    acl host_{configured_hosts[ingress_host]['safe_ingress_host']} hdr(host) -i {ingress_host}
    use_backend {configured_hosts[ingress_host]['safe_ingress_host']}_backend if host_{configured_hosts[ingress_host]['safe_ingress_host']}
""")

Set the default backend:

print("""
    default_backend catchall_backend

backend catchall_backend
    mode http
    http-request deny deny_status 404
""")

Finally, define each backend:

for ingress_host in sorted(configured_hosts.keys()):
    print(f"""
backend {configured_hosts[ingress_host]['safe_ingress_host']}_backend
    mode http
    server only_server {configured_hosts[ingress_host]['destination_service']} check
""")

This creates a clean, maintainable HAProxy config generated directly from documentation. Future improvements (like supporting load balancing across multiple destinations) can easily build on this foundation—but that’s a topic for another post.