Automating HAProxy configuration management with Ansible is less about writing playbooks and more about understanding how HAProxy’s dynamic nature clashes with traditional static configuration.

Let’s see HAProxy in action with a simple backend pool managed by Ansible. Imagine you have a web service running on three nodes: web1.example.com, web2.example.com, and web3.example.com, all listening on port 8080.

# haproxy.cfg snippet
frontend http_in
    bind *:80
    default_backend web_servers

backend web_servers
    balance roundrobin
    server web1 web1.example.com:8080 check
    server web2 web2.example.com:8080 check
    server web3 web3.example.com:8080 check

Now, let’s say web2.example.com goes down. A typical Ansible playbook might wait for the next scheduled run to update haproxy.cfg and reload HAProxy. But HAProxy can do better.

The core problem HAProxy solves is efficiently distributing traffic across a pool of backend servers while being able to dynamically add, remove, or reconfigure those servers without interrupting service. It achieves this through its "runtime API," a powerful, often underutilized feature. When HAProxy is started with the stats socket option, it exposes a Unix domain socket or TCP port that allows for real-time configuration changes.

Here’s how you’d typically configure the stats socket in haproxy.cfg:

# haproxy.cfg snippet
global
    stats socket /var/run/haproxy.sock mode 660 level admin

listen stats
    bind *:9000
    mode http
    stats enable
    stats uri /haproxy?stats
    stats auth admin:YourSecurePassword

This configuration does two main things:

  1. stats socket /var/run/haproxy.sock mode 660 level admin: This directive, placed in the global section, tells HAProxy to create a Unix domain socket at /var/run/haproxy.sock. The mode 660 ensures only the HAProxy user and group can access it, and level admin grants full control over the runtime API.
  2. listen stats ...: This section sets up a web interface for monitoring and management, accessible at http://your_haproxy_ip:9000/haproxy?stats. The stats auth line is crucial for securing this interface.

Ansible’s role here isn’t to rewrite haproxy.cfg for every minor change, but to manage the state of the backend servers and then use the HAProxy runtime API to instruct HAProxy about these state changes.

Consider an Ansible task that checks the health of your web servers. If web2.example.com is deemed unhealthy (e.g., a simple uri check fails), instead of waiting to modify the config file, you can directly tell HAProxy to disable it.

Here’s a simplified Ansible task using the command module to interact with the HAProxy stats socket:

- name: Disable unhealthy backend server
  ansible.builtin.command: "echo 'disable server web_servers/web2' | socat stdio /var/run/haproxy.sock"
  when: web2_server_health_check_failed
  become: yes # Ensure you have permissions to access the socket

  delegate_to: "{{ haproxy_server_ip }}" # Run this on the HAProxy server

This task sends the command disable server web_servers/web2 directly to the HAProxy stats socket. HAProxy receives this, marks the web2 server within the web_servers backend as disabled, and immediately stops sending it traffic. The beauty is that the haproxy.cfg file itself remains untouched. When web2.example.com becomes healthy again, another Ansible task can send enable server web_servers/web2 to bring it back into rotation.

The real power emerges when you combine this with dynamic inventory or service discovery. If your backend servers are managed by a cloud provider’s API or a service registry like Consul, Ansible can query that source of truth, build a list of currently available servers, and then use the runtime API to add or remove servers from HAProxy’s configuration on the fly.

This approach fundamentally changes how you think about HAProxy configuration. Instead of a static file that requires a reload (which can briefly interrupt connections), you have a dynamic, event-driven system where HAProxy is constantly updated via its API. Ansible acts as the orchestrator, querying your infrastructure state and issuing commands to the HAProxy runtime.

The one thing most people don’t realize is that the enable and disable commands aren’t just toggles; they accept a state argument which can be ready, maint, or drain. drain is particularly useful: disable server web_servers/web2 state drain tells HAProxy to stop sending new connections to web2 but to allow existing connections to complete gracefully.

This dynamic interaction with the HAProxy runtime API is the key to achieving true zero-downtime updates and seamless failover scenarios without relying solely on HAProxy’s built-in health checks for immediate action.

The next step after mastering dynamic server management is implementing sophisticated traffic shaping and routing rules using the same runtime API.

Want structured learning?

Take the full Haproxy course →