This Role lets you Setup, Configure and Maintain a HAProxy virtual LoadBalancer with Ansible.
- Situations where you want/need to customize every aspect of the HAProxy Configuration.
- Running a newer HAProxy than your Linux Distribution has available by Default on a System.
- Running HAProxy with Docker Compose.
- Building a customized Docker Image for the use in an orchestrated Environment like Kubernetes.
- When you want to learn about HAProxy Setups and Ansible Galaxy Roles.
- Very simple HAProxy Setups running directly on a Server. You should instead just install the HAProxy Package and template the Configuration yourself.
- Very simple HAProxy Kubernetes Setups. You should use the official HAProxy Docker Image instead.
- Features
- Supported Distributions
- Role Modes
- HAProxy Configuration
- Requirements
- Role Steering Variables
- Dependencies
- Example Playbooks
- Tips and Tricks
- License
- Authors
- All available Sections of the haproxy.cfg can be fully templated by using customizable Variables.
- Works on many Distributions.
- Supports building a customized Docker Image and running it.
- Customizable Error pages.
- HAProxy Configmap Support.
- LUA Script Support.
- Default Prometheus Metrics.
- Support for Firewalld, SELinux and Sysctl Settings.
- Debian 13 Trixie
- Debian 12 Bookworm
- Debian 11 Bullseye
- Debian 10 Buster
- Ubuntu 24.04 Noble Numbat
- Ubuntu 22.04 Jammy Jellyfish
- Ubuntu 20.04 Focal Fossa
- Ubuntu 18.04 Bionic Beaver
- Rocky Linux 9 Blue Onyx
- Alma Linux 9.3 Shamrock Pampas Cat
- Alma Linux 9.2 Turquoise Kodkod
- Alma Linux 9.1 Lime Lynx
- Alma Linux 9.0 Emerald Puma
Currently this Role Supports two Modes to run HAProxy:
- System: HAProxy runs as a process on a Server.
- Docker: Allows you to run/build HAProxy in/for a Docker/Kubernetes Environment.
The behavior is set by this Variable:
Name | Default | Description |
---|---|---|
haproxy_mode |
system | Can be one of: system, docker |
In System Mode this Role assumes
- ... that the Installation of the HAProxy Package will create a HAProxy User (normally haproxy) that the Process will run in.
- You can modify the
haproxy_user
andhaproxy_group
Variable if this is not the case. - This Role does not cover the Creation of this User.
- You can modify the
- ... that the Service on the System will be managed by Systemd.
- ... that you have SELinux set up already if you want to use it in this Role.
- ... that you have Firewalld set up already if you want to use it in this Role.
Look here for Installation Instructions for Debian & Ubuntu Distributions. For RedHat Compatible Distributions, there are only the Default Versions at the Moment.
You can overwrite the Version with the haproxy_version
Variable, but make sure that the haproxy_versions
Variable in defaults/main/versions.yml
also includes that Version on your Distribution.
If you want to modify the default Versions for multiple Distributions, you can do so with the haproxy_<DISTRIBUTION>_<RELEASE>_default_version
Variables.
Version | End of Life | Debian 12 | Debian 11 | Debian 10 |
---|---|---|---|---|
3.0 | 2029-Q2 (LTS) | - | - | - |
2.9 | 2025-Q1 (stable) | Supported | - | - |
2.8 | 2028-Q2 (LTS) | Role Default | Role Default | - |
2.6 | 2027-Q2 (LTS) | OS Default | Supported | Role Default |
2.4 | 2026-Q2 (LTS) | - | OS Default | Supported |
2.2 | 2025-Q2 (critical fixes only) | - | Supported | Supported |
2.0 | 2024-Q2 (critical fixes only) | - | - | Supported |
1.8 | 2022-Q4 (EOL!) | - | - | OS Default |
Version | End of Life | Ubuntu 24 | Ubuntu 22 | Ubuntu 20 | Ubuntu 18 |
---|---|---|---|---|---|
3.0 | 2029-Q2 (LTS) | - | - | - | - |
2.9 | 2025-Q1 (stable) | Supported | Supported | Supported | - |
2.8 | 2028-Q2 (LTS) | Role Default | Role Default | Role Default | - |
2.6 | 2027-Q2 (LTS) | - | Supported | Supported | Role Default PPA unreliable |
2.4 | 2026-Q2 (LTS) | - | Supported | Supported | PPA unreliable |
2.2 | 2025-Q2 (critical fixes only) | - | OS Default | Supported | PPA unreliable |
2.0 | 2024-Q2 (critical fixes only) | - | - | OS Default | Supported |
1.8 | 2022-Q4 (EOL!) | - | - | - | OS Default |
Version | End of Life | 9 |
---|---|---|
2.4 | 2026-Q2 (LTS) | OS Default |
Version | End of Life | 9.3 | 9.2 | 9.1 | 9.0 |
---|---|---|---|---|---|
2.4 | 2026-Q2 (LTS) | OS Default | OS Default | OS Default | OS Default |
The following Variables will give you Control over SELinux Settings:
Variable | Default | Description |
---|---|---|
haproxy_selinux_enabled |
false | Lets you enable SELinux Support |
haproxy_selinux_packages |
['python3-selinux', 'python3-sepolicy', 'selinux-utils', 'selinux-basics', 'selinux-policy-default'] |
Packages to install |
haproxy_selinux_ports |
[] | List of Ports to open with SELinux for HAProxy |
haproxy_selinux_flags |
[] | Experimental Dictionary of SELinux Flags |
haproxy_selinux_policy |
"default" | SELinux Policy to enforce |
haproxy_selinux_state |
"permissive" | SELinux State |
The SELinux Packages for the RedHat OS Family are overwritten in vars/RedHat.yml
.
These Variables give you some limited control over Firewalld Settings:
Variable | Default | Description |
---|---|---|
haproxy_firewalld_enabled |
false | Lets you enable Firewalld Support |
haproxy_firewalld_ports |
[] | Lists of Ports to be enabled in Firewalld Settings |
You can set Sysctl Settings with these Variables:
Variable | Default | Description |
---|---|---|
haproxy_sysctl_enabled |
false | Lets you enable Sysctl Support |
haproxy_sysctl_settings |
[] | List of Sysctl Settings |
Here is an Example on how to enable IP-Forwarding for IPv4:
haproxy_sysctl_enabled: true
haproxy_sysctl_settings:
- name: "net.ipv4.ip_forward"
value: "1"
sysctl_set: true
If enabled with haproxy_helper_scripts_enabled: true
this Role will add two Helper Scripts for you:
Name | Description |
---|---|
{{ haproxy_helper_scripts_dir }}/haproxy_status.sh |
Outputs a Status of the HAProxy (using the Prometheus Exporter) |
{{ haproxy_helper_scripts_dir }}/haproxy_nurse.sh |
Experimental: Self-Healing Script for ultra high usage Scenarios to keep Memory usage at bay |
These Variables let you configure the Behaviour of the Helper Scripts:
Name | Default | Description |
---|---|---|
haproxy_helper_scripts_dir |
"/opt/haproxy" | Path where the Helper Scripts live |
haproxy_helper_scripts_metrics_auth |
"" | Basic Auth Credentials for the Prometheus Exporter in the Format -u prometheus:foobar |
haproxy_helper_scripts_free_sys_mem |
1024 | Memory in Megabytes that should be free on the System |
haproxy_helper_scripts_max_idle_time |
300 | Amount of Seconds with no Log events after that we declare HAProxy unresponsive |
haproxy_nurse.sh
Script to work, rsyslog will be installed, to ensure that {{ haproxy_log_file }}
exists and is written to instead of journald. Maybe you have to restart the System in certain conditions in order for the Log to appear.
In Docker Mode this role assumes:
- ... that you already have
docker
installed. This Role does not cover the Installation of the Docker Runtime. - ... that you have the Docker-Compose-Plugin installed, if you want to run the Container on a Host
- We use the V2 Compose Version.
You may just Build and Push the created Image into a Docker Repository for use in different Scenarios like Kubernetes.
haproxy_mode
to "docker"
if you want to use this Mode.
This Role is tested for all Versions from 2.5.0 (23 Nov 2021 EOL) to 2.9.5 (15 Feb 2024). For Production use you should stick to the latest Stable Version.
This Variable sets the Version for HAProxy inside the Docker Image:
Name | Default | Description |
---|---|---|
haproxy_docker_patch_version |
2.8.6 | This must be an exact SemVer Version. |
haproxy_docker_src_download_url |
"" | Provide the Exact URL to a tar.gz to overwrite the default Download URL. You still have to set haproxy_docker_patch_version to a SemVer Version. |
By Default all Sources are Downloaded from https://www.haproxy.org and more specific from https://www.haproxy.org/download/{{ haproxy_docker_patch_version.split('.')[0] }}.{{ haproxy_docker_patch_version.split('.')[1] }}/src/haproxy-{{ haproxy_docker_patch_version }}.tar.gz
You can use this Role to build and push a customized Docker Image that is a little different than the Official Images. The little Difference is the not needed docker-entrypoint.sh Script.
Variable | Default | Description |
---|---|---|
haproxy_docker_build |
true | Build the Docker Image |
haproxy_docker_push |
false | Push the Docker Image, only works together with Building |
haproxy_docker_name |
"haproxy" | The Base name of the Image that will be build. If you plan to push it, prefix it with your Docker Registry Address |
haproxy_docker_image |
"{{ haproxy_docker_name }}:{{ haproxy_docker_patch_version }}" |
You should not overwrite this directly, if you plan to Build and/or Push. Only when you want to run an other Image. |
These are the possible Customizations:
Variable | Default | Description |
---|---|---|
haproxy_docker_base_image |
"debian:bookworm-slim" | The Base Image of the Container, if you change this you are on your own. |
haproxy_docker_config_dir |
"/etc/haproxy" | The Configuration Folder for HAProxy inside the Docker Container. |
haproxy_docker_user |
"haproxy" | The User and Group inside the Container. |
haproxy_docker_uid_gid |
99 | The GID and UID for that User. |
haproxy_docker_workdir |
"/var/lib/haproxy" | The Workdir and Homedir of the User in the Container, changing this is not supported and comes with Implications. |
haproxy_docker_makeopts |
TARGET=linux-glibc USE_GETADDRINFO=1 USE_LUA=1 LUA_INC=/usr/include/lua5.3 USE_OPENSSL=1 USE_PCRE2=1 USE_PCRE2_JIT=1 USE_PROMEX=1 EXTRA_OBJS="" |
Make Options for HAProxy, if you change these you are on your own. |
haproxy_docker_commands |
"-W", "-db", "-f", "{{ haproxy_docker_config_dir }}/{{ haproxy_config_file }}" |
The build in HAProxy Start Command, if you change these you are on your own. |
You can Run the Image locally with the Support of the docker-compose-plugin
like this:
Variable | Default | Description |
---|---|---|
haproxy_docker_compose |
false | If Enabled the Service will be started |
haproxy_docker_container_name |
"haproxy" | The Name of the started Docker Container |
haproxy_docker_mounts |
[source: "{{ haproxy_docker_dir }}/config", target: "{{ haproxy_docker_config_dir }}", type: "bind"] |
How the Configuration is mounted into the Container. |
haproxy_docker_restart_policy |
"always" | You should keep this at always or unless-stopped to have the Service start after Reboot. |
haproxy_docker_sysctls |
["net.ipv4.ip_unprivileged_port_start=0"] | You should keep this if you want HAProxy to serve Ports below 1024, if you add more you are on your own. |
haproxy_docker_log_options |
[driver: "json-file", options: [max-size: "10m", max-file: "1"]] | We do not want Docker to fill our Drives with Logs. |
haproxy_docker_extra_ports |
[] | Add a List of extra Ports in the Format "<host_port>:<container_port>" that are not in haproxy_frontend_vars or haproxy_listener_vars (added automatically). |
haproxy_docker_cpu_count |
"1" | The Containers cpus_count Setting. |
haproxy_docker_memory_reservation |
"0.5g" | The Containers --memory-reservation Setting. |
haproxy_docker_memory |
"1g" | The Containers --memory Setting. |
haproxy_docker_service_extra_settings |
[] | A List of services top-level variables not set in other settings above. In the Format <setting>: <value> |
haproxy_docker_healthcheck |
['test: ["CMD", "haproxy", "-c", "-f" "{{ haproxy_docker_config_dir }}/haproxy.cfg"]', 'interval: 10s', 'timeout: 10s', 'retries: 3'] | A Healthcheck for the Docker Container |
You can find required CPU and memory Settings for your Setup here. You should always set Limitations for your Containers, because they can impact the Host.
If you want to Reload the HAProxy inside the Container execute this command: docker kill -s USR2 {{ haproxy_docker_container_name }}
. Very useful after renewing TLS Certificates. On Configuration Change this Role also does this in the "Reload Docker HAProxy" Handler.
If you plan to use the Official Image or any other prebuild Image, you should tinker with these Variables:
Name | Default | Description |
---|---|---|
haproxy_docker_image |
"{{ haproxy_docker_name }}:{{ haproxy_docker_patch_version }}" |
Set this to the full Path of your Image |
haproxy_docker_mounts |
[source: "{{ haproxy_docker_dir }}/config", target: "{{ haproxy_docker_config_dir }}", type: "bind"] |
Here you can configure your Docker Mounts |
Look at the Playbook Docker official Image Section for an Example on how to do this.
The Configuration Sections are separated into different Variables.
Section | Variable | Type | Template |
---|---|---|---|
global | haproxy_global_vars |
List | templates/haproxy_global.j2 |
defaults | haproxy_defaults_vars |
List | templates/haproxy_defaults.j2 |
userlist | haproxy_userlist_vars |
List of name and vars | templates/haproxy_userlist.j2 |
peers | haproxy_peers_vars |
List of name and vars | templates/haproxy_peers.j2 |
mailers | haproxy_mailsers_vars |
List of name and vars | templates/haproxy_mailers.j2 |
programs | haproxy_programs_vars |
List of name and vars | templates/haproxy_programs.j2 |
http-errors | haproxy_httperrors_vars |
List of name and vars | templates/haproxy_httperrors.j2 |
rings | haproxy_rings_vars |
List of name and vars | templates/haproxy_rings.j2 |
log-forward | haproxy_logforward_vars |
List of name and vars | templates/haproxy_logforward.j2 |
modules | haproxy_modules_vars |
List | templates/haproxy_modules.j2 |
resolvers | haproxy_resolvers_vars |
List of name and vars | templates/haproxy_resolvers.j2 |
listen | haproxy_listen_vars |
List of name and vars | templates/haproxy_listen.j2 |
frontend | haproxy_frontend_vars |
List of name and vars | templates/haproxy_frontend.j2 |
backend | haproxy_backend_vars |
List of name and vars | templates/haproxy_backend.j2 |
The templates/haproxy_modules.j2
Template is a generic Template that you could use to create any section you want.
haproxy_global_vars:
- "log /dev/log local0"
- "log /dev/log local1 notice"
- "chroot /var/lib/haproxy"
- "user haproxy"
- "group haproxy"
- "daemon"
haproxy_defaults_vars:
- "option tcplog"
- "log global"
- "option dontlognull"
- "timeout connect 5000"
- "timeout client 500000"
- "timeout server 500000"
- "errorfiles default_error_files"
haproxy_userlist_vars: []
haproxy_peers_vars: []
haproxy_mailers_vars: []
haproxy_programs_vars: []
haproxy_httperrors_vars:
- name: "default_error_files"
vars:
- "errorfile 400 /etc/haproxy/errors/400.http"
- "errorfile 403 /etc/haproxy/errors/403.http"
- "errorfile 408 /etc/haproxy/errors/408.http"
- "errorfile 500 /etc/haproxy/errors/500.http"
- "errorfile 502 /etc/haproxy/errors/502.http"
- "errorfile 503 /etc/haproxy/errors/503.http"
- "errorfile 504 /etc/haproxy/errors/504.http"
haproxy_rings_vars: []
haproxy_logforward_vars: []
haproxy_modules_vars: []
haproxy_resolvers_vars: []
haproxy_listen_vars: []
haproxy_frontend_vars:
- name: "prometheus"
vars:
- "mode http"
- "bind localhost:{{ haproxy_prometheus_port }}"
- "http-request use-service prometheus-exporter if { path /metrics }"
- "stats enable"
- "stats uri /stats"
- "stats refresh 10s"
haproxy_backend_vars: []
This will produce this valid haproxy.cfg:
# Ansible managed: Do NOT edit this file manually!
global
log /dev/log local0
log /dev/log local1 notice
chroot /var/lib/haproxy
user haproxy
group haproxy
daemon
defaults
option tcplog
log global
mode tcp
option dontlognull
timeout connect 5000
timeout client 500000
timeout server 500000
errorfiles default_error_files
http-errors default_error_files
errorfile 400 /etc/haproxy/errors/400.http
errorfile 403 /etc/haproxy/errors/403.http
errorfile 408 /etc/haproxy/errors/408.http
errorfile 500 /etc/haproxy/errors/500.http
errorfile 502 /etc/haproxy/errors/502.http
errorfile 503 /etc/haproxy/errors/503.http
errorfile 504 /etc/haproxy/errors/504.http
frontend prometheus
mode http
bind localhost:8404
http-request use-service prometheus-exporter if { path /metrics }
stats enable
stats uri /stats
stats refresh 10s
A full Configuration Example can be found in test/haproxy_full.yml
This Role wants you to have the Creation and Management of your TLS Certificates in a separate Role.
The Certificates (or Symlink to them) should be present in the /etc/haproxy/certs
directory in the PEM combined Format that HAProxy understands. They should be imported in your HTTPS Frontend (as a directory) like this:
haproxy_global_vars:
- "ssl-default-bind-ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305"
- "ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256"
- "ssl-default-bind-options prefer-client-ciphers no-sslv3 no-tlsv10 no-tlsv11 no-tls-tickets"
- "ssl-default-server-ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305"
- "ssl-default-server-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256"
- "ssl-default-server-options no-sslv3 no-tlsv10 no-tlsv11 no-tls-tickets"
- "ssl-dh-param-file {{ haproxy_config_dir }}/dhparams.pem"
...
haproxy_frontend_vars:
- name: "http"
vars:
- "mode http"
- "bind *:443 ssl crt /etc/haproxy/certs/ alpn h2,http/1.1"
- "http-request set-header X-Real-IP %[src]"
- "http-request set-header X-Forwarded-Host %[req.hdr(host)]"
- "http-request set-header X-Forwarded-Port %[dst_port]"
- "http-request set-header X-Forwarded-For %[src]"
- "http-request set-header X-Forwarded-Proto https"
- "http-response set-header Strict-Transport-Security max-age=63072000"
...
Look at the TLS Configuration Generator from Mozilla to find suitable Options for you.
In order to help with the Integration of TLS Certificates, this Role has some Switches:
Variable | Default | Description |
---|---|---|
haproxy_dhparams_create |
false | Create a dhparams.pem File for you. |
haproxy_dhparams_speedup |
false | Install haveged for Kernels older than 5.6 to speed up Entropy Generation. |
haproxy_dhparams_bits |
2048 | The Size of the Diffie-Hellman Parameters for the dhparams File. |
haproxy_dhparams_file |
"{{ haproxy_config_dir }}/dhparams.pem" |
Full Path where to store the dhparams.pem File. |
Usually it is completely sufficient to reload HAProxy via systemctl reload haproxy.service
in order for all new Connections to get new Certificates.
There is a Solution on how to integrate acme.sh to work with HAProxy. Here the Admin Socket is used to put the Certificates directly into HAProxy's memory during execution.
Normally the HAProxy Package includes a file /etc/logrotate.d/haproxy
, that rotates the file /var/log/haproxy.log
. This Log file is written to via the Rsyslog Settings in /etc/rsyslog.d/49-haproxy.conf
and works from the more secure chrooted Environment too.
Ideally you just have to keep the default global Settings. Alternatively you should include these Settings in your individual haproxy_global_vars
for Logging to work:
haproxy_global_vars:
- "log /dev/log local0"
- "log /dev/log local1 notice"
- "chroot /var/lib/haproxy"
- "user haproxy"
- "group haproxy"
- "daemon"
...
You can fiddle with the Location of the Log file by using these Settings:
Variable | Default | Description |
---|---|---|
haproxy_config_logrotate |
false | Enables individual Log rotation configuration. |
haproxy_log_file |
"/var/log/haproxy.log" | The Log file Location. |
You may individualize the default Log Formats as described in the Documentation in your global Configuration Section.
More Information about Configuring HAProxy Logging.
If you are running HAProxy as the Entrypoint of a Docker Container, you want to log directly to stdout like this:
haproxy_global_vars:
- "log stdout format raw daemon info"
...
You may want to Encrypt certain sensitive Information in your Playbook/Inventory Variables.
For this you have to create a Secret variable like this:
haproxy_example_secret: !vault |
$ANSIBLE_VAULT;1.1;AES256
64336533393735353032633233653937313761343464626365346535303938376230316339663465
6631613937613534353663363035313034613537656634620a356563396136333936373466633761
65373639623164613930316131323638343334316238323335656666373434363762376332343564
3731396431396438380a633932383466656139313163633164313632353635616131663166313562
3039
You get this Secret by Invoking the ansible-vault
Tool that comes by Default with Ansible like this:
# To store your Vault Key in a File
echo "sup3rs3cr3t_not" > .ansible-vault
# For BasicAuth Credentials
echo -n "foo:bar" | base64 | ansible-vault encrypt --vault-password-file .ansible-vault
# For Plain Secrets
echo -n "foobar" | ansible-vault encrypt --vault-password-file .ansible-vault
Then use the Variable in one of your Configuration Settings like this (Yes, the Variables are filled in when used, this works fine):
haproxy_backend_vars:
- name: "be_api"
vars:
- "mode http"
- "server be_api 127.0.0.1:8081"
- "option httpchk GET /path_to_heath_check HTTP/1.1\r\nAuthorization:\ Basic\ {{ haproxy_example_secret }}"
haproxy_userlist_vars:
- name: "myusers"
vars:
- "user joe insecure-password {{ haproxy_example_secret }}"
You have to provide the .ansible-vault
File used in this example to the ansible-playbook Command like this:
ansible-playbook --vault-pass-file .ansible-vault -i your_inventory.yml your_playbook.yml
There are other good methods of providing a Vault Key for Ansible.
.ansible-vault
is included in the .gitignore
File and you should do the same if you consider using a different Filename.
By Default this Role comes with the Prometheus Metrics Plugin enabled on the localhost Port 8404 as defined in the haproxy_prometheus_port
Variable.
It is configured in the Default Prometheus Frontend:
haproxy_frontend_vars:
- name: "prometheus"
vars:
- "mode http"
- "bind localhost:{{ haproxy_prometheus_port }}"
- "http-request use-service prometheus-exporter if { path /metrics }"
- "stats enable"
- "stats uri /stats"
- "stats refresh 10s"
This Endpoint is used in the {{ haproxy_helper_scripts_dir }}/haproxy_status.sh
Helper Script to Query the Metrics with curl:
curl --silent {{ haproxy_helper_scripts_metrics_auth }} localhost:{{ haproxy_prometheus_port }}/metrics
If you want to expose the Metrics Endpoint to a Network, it is a good Idea to use BasicAuth to secure it:
haproxy_frontend_vars:
- name: "prometheus"
vars:
- "mode http"
- "bind *:{{ haproxy_prometheus_port }}"
- "stats enable"
- "stats uri /stats"
- "stats refresh 10s"
- "http-request auth unless { http_auth(mycredentials) }"
- "acl metrics_path path /metrics"
- "http-request use-service prometheus-exporter if { path /metrics }"
haproxy_userlist_vars:
- name: "mycredentials"
vars:
- "user prometheus insecure-password {{ haproxy_prometheus_password }}"
You have to create the Variable haproxy_prometheus_password
on your own as described in the Secrets Section.
If you use the haproxy_status.sh
Script, you want to encode a matching haproxy_helper_scripts_metrics_auth
Variable too.
Then you are able to Scrape the Metrics with Prometheus like this:
scrape_configs:
- job_name: haproxy
static_configs:
- targets: ['your_haproxy_ip:{{ haproxy_prometheus_port }}']
metrics_path: "/metrics"
basic_auth:
username: 'prometheus'
password: '{{ haproxy_prometheus_password }}'
...
If you have Grafana you may want to use Dashboard 12693 to get the most out of the Prometheus Metrics.
HAProxy supports Error Files that are pre rendered HTTP responses:
HTTP/1.1 400 Bad Request
Cache-Control: no-cache
Connection: close
Content-Type: text/html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>400 Bad Request</title>
</head>
<body>
<h1>400 Bad Request</h1>
<h3>You sent a malformed Request.</h3>
</body>
</html>
The Option errorfile can be defined in all proxy Sections (defaults, frontend, listen and backend) of the Configuration. Groups of Error Files can be defined in the http-errors section for later use with the errorfiles option.
These are the Default Error pages:
haproxy_error_files:
- state: 400
description: "Bad Request"
explanation: "You sent a malformed Request."
method: "close"
- state: 403
description: "Forbidden"
explanation: "You do not have Access to this Site."
method: "close"
- state: 408
description: "Request Timeout"
explanation: "Your Request timed out."
method: "close"
- state: 500
description: "Internal Server Error"
explanation: "There is an internal Server Error on our side."
method: "close"
- state: 502
description: "Bad Gateway"
explanation: "The Server could not redirect you."
method: "close"
- state: 503
description: "Service Unavailable"
explanation: "This Service is currently unavailable."
method: "close"
- state: 504
description: "Gateway Timeout"
explanation: "The Server could not redirect you in time."
method: "close"
They are rendered with this Default Template templates/errorfile.http.j2
:
HTTP/1.1 {{ item.state }} {{ item.description }}
Cache-Control: no-cache
Connection: {{ item.method }}
Content-Type: text/html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>{{ item.state }} {{ item.description }}</title>
</head>
<body>
<h1>{{ item.state }} {{ item.description }}</h1>
<h3>{{ item.explanation }}</h3>
</body>
</html>
All Error Files will be created in the Sub directory errors/ of the Config Directory having the Name {{ item.state }}.http
. By Default there is a http-errors Section in the Configuration that will be used in the default Section for all Proxy Sections.
Since Error Pages are highly customizable please feel free to modify the Template and the haproxy_error_files
and haproxy_httperrors_vars
Variables according to your needs.
HAProxy supports Configuration Maps that are Key Value Pairs separated by a white space:
example.com be_default
api.example.com be_api
You may use them in ACL's and other Configuration Settings:
haproxy_frontend_vars:
- name: "default"
vars:
- "mode http"
- "bind :80"
- "use_backend %[req.hdr(host),lower,map_dom(/etc/haproxy/maps/hosts.map,be_default)]"
haproxy_backend_vars:
- name: "be_default"
vars:
- "mode http"
- "server be_default 127.0.0.1:8080"
- name: "be_api"
vars:
- "mode http"
- "server be_api 127.0.0.1:8081"
You can define such maps in the haproxy_configmaps
Variable:
haproxy_config_maps:
- hosts:
example.com: "be_default"
api.example.com: "be_api"
All Configmaps will be created in the Sub directory maps/ of the Config Directory having the File Ending .map
You can extend the functionality of HAProxy with the use of LUA Scripts.
You have to put your .lua Files into the files/lua/
Directory and include the Filename in the haproxy_lua_files
List. Then modify the Configuration to load and use them.
Enable the Use of LUA Files by using these Variables:
Variable | Default | Description |
---|---|---|
haproxy_lua_dir |
"{{ haproxy_config_dir }}/lua" |
The Path where the LUA Files should be stored. |
haproxy_lua_files |
[] | A List of files that should be copied into the haproxy_lua_dir . |
All LUA Files will be created in the Sub directory lua/ of the Config Directory.
Name | Optional | Mode | Default | Description |
---|---|---|---|---|
haproxy_mode |
false | both | "system" | Can be one of system or docker |
haproxy_helper_scripts_enabled |
true | both | false | Install the Helper Scripts |
haproxy_dhparams_create |
true | both | false | Create a dhparams File |
haproxy_dhparams_speedup |
true | both | false | Speedup Entropy Generation on older Kernels |
haproxy_config_file |
true | both | "haproxy.cfg" | The Name of the HAProxy Configuration |
haproxy_docker_build |
true | docker | false | Select if you want to build a Docker Image |
haproxy_docker_push |
true | docker | false | Select if you want to push the Image to a Registry |
haproxy_docker_compose |
true | docker | false | Select if you want to run the Image with Docker Compose |
haproxy_service_state |
true | system | "started" | The Condition the HAProxy should be set to |
haproxy_config_logrotate |
true | system | false | Select if you want to change the Logging to rsyslogd and Configure Log rotation |
haproxy_selinux_enabled |
true | system | false | Select if you want to configure SELinux Settings |
haproxy_firewalld_enabled |
true | system | false | Select if you want to configure Firewalld Settings |
haproxy_sysctl_enabled |
true | system | false | Select if you want to configure Kernel Parameters |
haproxy_show_debug |
true | system | false | If you enable this some Debug Information is shown |
haproxy_show_template_diffs |
true | both | true | By default haproxy is showing the diffs of the templates |
haproxy_testing_mode |
true | both | false | If you enable this a fake TLS Certificate will be created and Apache will be installed |
haproxy_molecule_test |
false | system | true | Only used for Molecule Tests where this is true |
Mode | Collections | Executables | Plugins |
---|---|---|---|
System | - | - | - |
System with SELinux | ansible.posix community.general |
- | - |
System with haproxy_testing_mode |
community.crypto | - | - |
Docker | ansible.posix community.general community.docker |
docker | docker-compose |
This Role is not dependent on any other Role if used in System Mode. In Docker Mode you need to make sure you have Docker (with the docker-compose-plugin) Installed.
To use this Role install it from the Ansible Galaxy
Either from Shell on your local machine:
ansible-galaxy role install martin-micimo.haproxy
Or put it into a requirements.yml
that can be installed with ansible-galaxy install -r requirements.yml
roles:
- name: martin-micimo.haproxy
For all Requirements for this Project look at the requirements.yml
This will set up:
- A HAProxy Service will be installed on the System (managed by Systemd) with the Default Version for that System.
- A Prometheus Frontend for Metrics on the default Port 8404.
- A HTTP Frontend listening on Port 80, forwarding Traffic to the web_servers Backends.
- Three Backends in leastconn Balancing Mode.
- A Port Forward from 8080 to an SSH Port (22) on remote_backend in TCP Mode. Be extra careful with something like this.
---
- name: Simple Haproxy Setup.
hosts: debian-bookworm
gather_facts: true
become: true
roles:
- role: martin-micimo.haproxy
vars:
haproxy_mode: "system"
haproxy_frontend_vars:
- name: "prometheus"
vars:
- "mode http"
- "bind localhost:{{ haproxy_prometheus_port }}"
- "http-request use-service prometheus-exporter if { path /metrics }"
- "stats enable"
- "stats uri /stats"
- "stats refresh 10s"
- name: "http"
vars:
- "mode http"
- "bind *:80"
- "default_backend web_servers"
haproxy_backend_vars:
- name: "web_servers"
vars:
- "mode http"
- "balance leastconn"
- "server myfirstserver 192.168.1.25:80 check"
- "server mysecondserver 192.168.1.26:80 check"
- "server mythirdserver 192.168.1.27:80 check"
haproxy_listen_vars:
- name: "ssh_proxy"
vars:
- "mode tcp"
- "bind *:8080"
- "server server1 192.168.1.25:22 check"
This will set up:
- A HAProxy Service on the System (managed by Systemd) with the supported Version for that System.
- An exposed Prometheus Frontend for Metrics with Basic Authentication on port 9404.
- Two subdomain Configmaps to redirect different Subdomains to various Backends.
- A HTTP Frontend, that will redirect to various Backends using a Configmap.
- A HTTPS Frontend with TLS Termination, that will redirect to various Backends using a different Configmap.
- A pre generated dhparams File for the TLS Handshakes.
- It will install the Helper Scripts with the Correct
haproxy_helper_scripts_metrics_auth
in thehaproxy_status.sh
.
---
- name: Advanced Haproxy Setup.
hosts: debian_bookworm
gather_facts: true
become: true
roles:
- role: martin-micimo.haproxy
vars:
haproxy_mode: "system"
haproxy_version: "2.6"
haproxy_dhparams_create: true
haproxy_prometheus_port: 9404
haproxy_helper_scripts_enabled: true
# Vaulted String "-u prometheus:foobar"
haproxy_helper_scripts_metrics_auth: !vault |
$ANSIBLE_VAULT;1.1;AES256
33346434653732636364643334303839633432396634323261643463303737343239636431663032
3738303165623032353936373064383631393733366164390a366566303932303436373563303933
30313735363137643735353632383631383038346239383236646132336537666362346462333036
3236646434653435370a313934626231303664623837393664623064356365326361356535306434
34336464346566356332396239373866313763663162626237313265313363643438
# Vaulted String "foobar"
haproxy_prometheus_secret: !vault |
$ANSIBLE_VAULT;1.1;AES256
64336533393735353032633233653937313761343464626365346535303938376230316339663465
6631613937613534353663363035313034613537656634620a356563396136333936373466633761
65373639623164613930316131323638343334316238323335656666373434363762376332343564
3731396431396438380a633932383466656139313163633164313632353635616131663166313562
3039
haproxy_global_vars:
- "log /dev/log local0"
- "log /dev/log local1 debug"
- "chroot /var/lib/haproxy"
- "user haproxy"
- "group haproxy"
- "daemon"
- "ssl-default-bind-ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305"
- "ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256"
- "ssl-default-bind-options prefer-client-ciphers no-sslv3 no-tlsv10 no-tlsv11 no-tls-tickets"
- "ssl-default-server-ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305"
- "ssl-default-server-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256"
- "ssl-default-server-options no-sslv3 no-tlsv10 no-tlsv11 no-tls-tickets"
- "ssl-dh-param-file {{ haproxy_config_dir }}/dhparams.pem"
haproxy_userlist_vars:
- name: "mycredentials"
vars:
- "user prometheus insecure-password {{ haproxy_prometheus_secret }}"
haproxy_config_maps:
- http_hosts:
example.com: "be_default_http"
api.example.com: "be_api_http"
- https_hosts:
example.com: "be_default_https"
api.example.com: "be_api_https"
haproxy_frontend_vars:
- name: "prometheus"
vars:
- "mode http"
- "bind *:{{ haproxy_prometheus_port }}"
- "stats enable"
- "stats uri /stats"
- "stats refresh 10s"
- "http-request auth unless { http_auth(mycredentials) }"
- "acl metrics_path path /metrics"
- "http-request use-service prometheus-exporter if { path /metrics }"
- name: "http"
vars:
- "mode http"
- "bind *:80"
- "use_backend %[req.hdr(host),lower,map({{ haproxy_maps_dir }}/http_hosts.map,be_default_http)]"
- name: "https"
vars:
- "mode http"
- "bind *:443 ssl crt {{ haproxy_certs_dir }}/ alpn h2,http/1.1"
- "http-request set-header X-Real-IP %[src]"
- "http-request set-header X-Forwarded-Host %[req.hdr(host)]"
- "http-request set-header X-Forwarded-Port %[dst_port]"
- "http-request set-header X-Forwarded-For %[src]"
- "http-request set-header X-Forwarded-Proto https"
- "http-response set-header Strict-Transport-Security max-age=63072000"
- "use_backend %[req.hdr(host),lower,map({{ haproxy_maps_dir }}/https_hosts.map,be_default_https)]"
haproxy_backend_vars:
- name: "be_default_http"
vars:
- "mode http"
- "server myfirstserver 127.0.0.1:8080 check"
- name: "be_api_http"
vars:
- "mode http"
- "server mysecondserver 127.0.0.1:8080 check"
- name: "be_default_https"
vars:
- "mode http"
- "server myfirstserver 127.0.0.1:8443 check"
- name: "be_api_https"
vars:
- "mode http"
- "server mysecondserver 127.0.0.1:8443 check"
...
This will set up:
- A Docker Image with the Default Version
- A running Docker Container managed by the Compose Plugin.
- The exact same Configuration as the Playbook System Simple Example.
- We have to overwrite the default
haproxy_validation_command
, because we can not verify the Config without a local HAProxy. ⚠️ Be extra careful with the exposed unsecured Prometheus Metrics Port 8404 here
---
- name: Docker Compose Haproxy Setup.
hosts: local
gather_facts: true
become: true
roles:
- role: martin-micimo.haproxy
vars:
haproxy_mode: "docker"
haproxy_docker_build: true
haproxy_docker_compose: true
haproxy_helper_scripts_enabled: true
haproxy_validation_command: "/bin/true %s"
haproxy_global_vars:
- "log stdout local0 debug"
haproxy_frontend_vars:
- name: "prometheus"
vars:
- "mode http"
- "bind *:{{ haproxy_prometheus_port }}"
- "http-request use-service prometheus-exporter if { path /metrics }"
- "stats enable"
- "stats uri /stats"
- "stats refresh 10s"
- name: "http"
vars:
- "mode http"
- "bind *:80"
- "default_backend web_servers"
haproxy_backend_vars:
- name: "web_servers"
vars:
- "mode http"
- "balance leastconn"
- "server myfirstserver 192.168.1.25:80 check"
- "server mysecondserver 192.168.1.26:80 check"
- "server mythirdserver 192.168.1.27:80 check"
haproxy_listen_vars:
- name: "ssh_proxy"
vars:
- "mode tcp"
- "bind *:8080"
- "server server1 192.168.1.25:22 check"
...
This will set up:
- A Docker Image with a specific Version and Registry Path to push it to.
- The User in the Docker Image that will run the HAProxy is named
harharproxy
instead of the defaulthaproxy
- The smallest Configuration possible that HAProxy accept as valid. Totally useless, but valid.
- You must have a working Docker Registry Authentification configured on the System for this to work.
---
- name: Docker Compose Haproxy Setup.
hosts: local
gather_facts: true
become: true
roles:
- role: martin-micimo.haproxy
vars:
haproxy_mode: "docker"
haproxy_docker_build: true
haproxy_docker_name: "your-registry.domain.tld/docker/experiment:2.6.8"
haproxy_docker_user: "harharproxy"
haproxy_docker_push: true
haproxy_frontend_vars:
- name: "foo"
vars:
- "bind :9001"
- "timeout client 5000"
haproxy_global_vars: []
haproxy_defaults_vars: []
haproxy_httperrors_vars: []
haproxy_error_files: []
...
This will set up:
- A Docker Compose File with the official HAProxy Docker Image.
- The exact same Configuration as the Playbook System Advanced Example.
- We have to overwrite the default
haproxy_docker_config_dir
andhaproxy_httperrors_vars
, because the official Image Entrypoint Script expects the Configuration in/usr/local/etc/haproxy
. - We have to overwrite the default
haproxy_validation_command
, because we can not verify the Config without a local HAProxy and the correct Paths. - The Official Image is used, because
haproxy_docker_build
is false by default.
---
- name: Official Docker Haproxy Setup.
hosts: local
gather_facts: true
become: true
roles:
- role: martin-micimo.haproxy
vars:
haproxy_mode: "docker"
haproxy_docker_compose: true
haproxy_version: "2.8.6"
haproxy_testing_mode: true
haproxy_docker_config_dir: "/usr/local/etc/haproxy"
haproxy_validation_command: "/bin/true %s"
haproxy_dhparams_create: true
haproxy_prometheus_port: 9404
haproxy_helper_scripts_enabled: true
# Vaulted String "-u prometheus:foobar"
haproxy_helper_scripts_metrics_auth: !vault |
$ANSIBLE_VAULT;1.1;AES256
33346434653732636364643334303839633432396634323261643463303737343239636431663032
3738303165623032353936373064383631393733366164390a366566303932303436373563303933
30313735363137643735353632383631383038346239383236646132336537666362346462333036
3236646434653435370a313934626231303664623837393664623064356365326361356535306434
34336464346566356332396239373866313763663162626237313265313363643438
# Vaulted String "foobar"
haproxy_prometheus_secret: !vault |
$ANSIBLE_VAULT;1.1;AES256
64336533393735353032633233653937313761343464626365346535303938376230316339663465
6631613937613534353663363035313034613537656634620a356563396136333936373466633761
65373639623164613930316131323638343334316238323335656666373434363762376332343564
3731396431396438380a633932383466656139313163633164313632353635616131663166313562
3039
haproxy_global_vars:
- "log stdout local0 debug"
- "ssl-default-bind-ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305"
- "ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256"
- "ssl-default-bind-options prefer-client-ciphers no-sslv3 no-tlsv10 no-tlsv11 no-tls-tickets"
- "ssl-default-server-ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305"
- "ssl-default-server-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256"
- "ssl-default-server-options no-sslv3 no-tlsv10 no-tlsv11 no-tls-tickets"
- "ssl-dh-param-file {{ haproxy_docker_config_dir }}/dhparams.pem"
haproxy_httperrors_vars:
- name: "default_error_files"
vars:
- "errorfile 400 {{ haproxy_docker_config_dir }}/errors/400.http"
- "errorfile 403 {{ haproxy_docker_config_dir }}/errors/403.http"
- "errorfile 408 {{ haproxy_docker_config_dir }}/errors/408.http"
- "errorfile 500 {{ haproxy_docker_config_dir }}/errors/500.http"
- "errorfile 502 {{ haproxy_docker_config_dir }}/errors/502.http"
- "errorfile 503 {{ haproxy_docker_config_dir }}/errors/503.http"
- "errorfile 504 {{ haproxy_docker_config_dir }}/errors/504.http"
haproxy_userlist_vars:
- name: "mycredentials"
vars:
- "user prometheus insecure-password {{ haproxy_prometheus_secret }}"
haproxy_config_maps:
- http_hosts:
example.com: "be_default_http"
api.example.com: "be_api_http"
- https_hosts:
example.com: "be_default_https"
api.example.com: "be_api_https"
haproxy_frontend_vars:
- name: "prometheus"
vars:
- "mode http"
- "bind *:{{ haproxy_prometheus_port }}"
- "stats enable"
- "stats uri /stats"
- "stats refresh 10s"
- "http-request auth unless { http_auth(mycredentials) }"
- "acl metrics_path path /metrics"
- "http-request use-service prometheus-exporter if { path /metrics }"
- name: "http"
vars:
- "mode http"
- "bind *:80"
- "use_backend %[req.hdr(host),lower,map({{ haproxy_docker_config_dir }}/maps/http_hosts.map,be_default_http)]"
- name: "https"
vars:
- "mode http"
- "bind *:443 ssl crt {{ haproxy_docker_config_dir }}/certs/ alpn h2,http/1.1"
- "http-request set-header X-Real-IP %[src]"
- "http-request set-header X-Forwarded-Host %[req.hdr(host)]"
- "http-request set-header X-Forwarded-Port %[dst_port]"
- "http-request set-header X-Forwarded-For %[src]"
- "http-request set-header X-Forwarded-Proto https"
- "http-response set-header Strict-Transport-Security max-age=63072000"
- "use_backend %[req.hdr(host),lower,map({{ haproxy_docker_config_dir }}/maps/https_hosts.map,be_default_https)]"
haproxy_backend_vars:
- name: "be_default_http"
vars:
- "mode http"
- "server myfirstserver 172.17.0.1:8080 check"
- name: "be_api_http"
vars:
- "mode http"
- "server mysecondserver 172.17.0.1:8080 check"
- name: "be_default_https"
vars:
- "mode http"
- "server myfirstserver 172.17.0.1:8443 check"
- name: "be_api_https"
vars:
- "mode http"
- "server mysecondserver 172.17.0.1:8443 check"
...
- Find Proper TLS Certificate Settings in the Moz://a SSL Configuration Generator
- Test your TLS Configuration Strength with Qualys SSL Labs
- If you want to deploy a not functional haproxy.cfg overwrite the Variable
haproxy_validation_command
with "/bin/true %s". You also want to sethaproxy_service_state
orhaproxy_docker_compose
accordingly. - Always make sure to only connect HTTP Mode Frontends to HTTP Mode Backends and only TCP Frontends to TCP Backends. I write the
mode
at the beginning of these Definitions and set it explicitly everywhere. - Do not put your
dhparams.pem
File into a dynamically loaded certs Directory. HAProxy can currently not deal with that.
This Work is licensed under the GPLv3 License
Created in 2024 by Martin Meier