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:
stats socket /var/run/haproxy.sock mode 660 level admin: This directive, placed in theglobalsection, tells HAProxy to create a Unix domain socket at/var/run/haproxy.sock. Themode 660ensures only the HAProxy user and group can access it, andlevel admingrants full control over the runtime API.listen stats ...: This section sets up a web interface for monitoring and management, accessible athttp://your_haproxy_ip:9000/haproxy?stats. Thestats authline 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.