Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/wikioasis/salt/llms.txt

Use this file to discover all available pages before exploring further.

The haproxy state installs HAProxy and socat on all proxy* servers and manages the complete proxy configuration from pillar. All load balancer behaviour — global tunables, timeouts, frontend bind addresses, backend server pools, and HTTP health checks — is declared in the haproxy: pillar map and rendered into /etc/haproxy/haproxy.cfg via a Jinja template. A companion sub-state, haproxy.route, manages the dynamic /etc/haproxy/routes.map file and can sync routing changes into the HAProxy runtime via the admin socket without requiring a full reload.

What gets installed

ResourceDetails
haproxy packageInstalled via apt with refresh: True
socat packageUsed by haproxy.route to communicate with the admin socket
/etc/haproxy/haproxy.cfgRendered from Jinja; owned by root:haproxy, mode 0640
/etc/haproxy/routes.mapWritten only when haproxy:routes is non-empty
haproxy serviceEnabled and running with reload: True on config changes

Pillar keys

All keys live under the top-level haproxy: map.

global

Controls HAProxy’s global stanza:
KeyDefaultDescription
haproxy:global:maxconn4096Maximum simultaneous connections
haproxy:global:log/dev/log local0 infoSyslog target and level
haproxy:stats_socket/run/haproxy/admin.sockAdmin socket path (mode 660, level admin)

defaults

Controls HAProxy’s defaults stanza:
KeyDefaultDescription
haproxy:defaults:modehttpProtocol mode (http or tcp)
haproxy:defaults:timeout_connect5sBackend connection timeout
haproxy:defaults:timeout_client50sClient inactivity timeout
haproxy:defaults:timeout_server50sServer response timeout

frontends

A map of frontend name → frontend definition. Each frontend can bind a port, declare persistent host ACLs, and optionally use map-based routing.
KeyDescription
bindAddress and port to listen on (e.g. *:80)
modeProtocol mode; inherits from defaults if absent
optionsList of option directives (e.g. forwardfor, http-server-close)
use_routesIf true, enables map-based routing via routes.map
persistent_hostsList of {hostname, backend} entries compiled to static ACLs
default_backendFallback backend when no ACL matches

backends

A map of backend name → backend definition.
KeyDescription
balanceLoad balancing algorithm (default: roundrobin)
modeProtocol mode
optionsList of option directives
http_checksList of http-check directives for active health checks
serversList of server entries (see below)
Each server entry:
KeyDescription
nameHAProxy server label
hostFQDN or IP
portTCP port
checkEnable active health check (true/false)
weightServer weight for load balancing
depooledIf true, the server is disabled in the pool

routes

A list of dynamic hostname → backend routing entries used by routes.map:
KeyDescription
hostnameRequest Host header value
backendHAProxy backend name to forward to
activeIf false, the entry is removed from the map

haproxy.cfg template

The full configuration is rendered by salt://haproxy/files/haproxy.cfg.jinja. Here is the structure:
global
    log {{ global_cfg.get('log', '/dev/log local0 info') }}
    maxconn {{ global_cfg.get('maxconn', 4096) }}
    stats socket {{ stats_socket }} mode 660 level admin expose-fd listeners
    user haproxy
    group haproxy
    daemon

defaults
    mode {{ defaults_cfg.get('mode', 'http') }}
    log global
    option dontlognull
    timeout connect {{ defaults_cfg.get('timeout_connect', '5s') }}
    timeout client  {{ defaults_cfg.get('timeout_client', '50s') }}
    timeout server  {{ defaults_cfg.get('timeout_server', '50s') }}
Frontends with use_routes: true emit an ACL that looks up the Host header in routes.map and forwards to the matched backend:
{%- if frontend.get('use_routes') and haproxy.get('routes') %}
    acl has_route req.hdr(host),lower,map(/etc/haproxy/routes.map) -m found
    use_backend %[req.hdr(host),lower,map(/etc/haproxy/routes.map)] if has_route
{%- endif %}
persistent_hosts entries are compiled to static ACLs that always override the map, useful for hosts that must never be affected by route updates:
{%- for ph in exact_hosts %}
    acl fe_{{ name }}_h{{ loop.index }} hdr(host) -i {{ ph.hostname }}
    use_backend {{ ph.backend }} if fe_{{ name }}_h{{ loop.index }}
{%- endfor %}
Wildcard entries (starting with *.) use hdr_end to match subdomains.

Production backend example

The mediawiki backend demonstrates active HTTP health checking across all four application servers:
haproxy:
  backends:
    mediawiki:
      balance: roundrobin
      options:
        - forwardfor
        - httpchk
      http_checks:
        - send meth GET uri /wiki/Main_Page ver HTTP/1.1 hdr Host wikioasis.org
        - expect str wikioasis
      servers:
        - name: mw-us-east-011
          host: mw-us-east-011.ovvin.wonet
          port: 80
          check: true
          weight: 1
          depooled: false
        - name: mw-us-east-012
          host: mw-us-east-012.ovvin.wonet
          port: 80
          check: true
          weight: 1
          depooled: false
        - name: mw-us-east-021
          host: mw-us-east-021.ovvin.wonet
          port: 80
          check: true
          weight: 1
          depooled: false
        - name: mw-us-east-022
          host: mw-us-east-022.ovvin.wonet
          port: 80
          check: true
          weight: 1
          depooled: false
HAProxy performs a GET /wiki/Main_Page health check against each backend and marks a server down if the response doesn’t contain the string wikioasis.

routes.map and persistent_hosts

WikiOasis uses two complementary routing mechanisms in the same frontend:

persistent_hosts

Static ACLs compiled directly into haproxy.cfg. Changing a persistent host requires a config change and a full haproxy reload (state.apply haproxy). Used for long-lived, critical routes (e.g. test.wikioasis.org → staging, phorge.wikioasis.org → apps).

routes (map)

Dynamic entries in /etc/haproxy/routes.map. Can be updated at runtime via the admin socket without a reload. Used for service-level routes (e.g. grafana.wikioasis.org → grafana, icinga.wikioasis.org → icinga) that may change more frequently.

routes.map template

{%- set routes = salt['pillar.get']('haproxy:routes', []) %}
{%- for route in routes %}
{%- if route.get('active', true) %}
{{ route.hostname }} {{ route.backend }}
{%- endif %}
{%- endfor %}
Only entries with active: true (the default) appear in the file. Setting active: false removes the entry on the next state apply and runtime sync.

haproxy.route sub-state

The haproxy.route state is a lightweight alternative to a full state.apply haproxy for route-only changes. It:
  1. Writes /etc/haproxy/routes.map from pillar (active entries only)
  2. On file change, syncs the in-memory map via the admin socket — no reload required
  3. On every apply, idempotently re-adds any active routes that may be missing from the live map (e.g. after an unexpected haproxy restart)
  4. Removes entries with active: false from the live map immediately

Runtime sync logic

echo "clear map /etc/haproxy/routes.map" | socat stdio /run/haproxy/admin.sock
while IFS=' ' read -r hostname backend remainder; do
  [ -n "$hostname" ] || continue
  echo "add map /etc/haproxy/routes.map $hostname $backend" | socat stdio /run/haproxy/admin.sock
done < /etc/haproxy/routes.map
This clear-and-re-add approach ensures stale entries from a previous configuration are always purged.

Applying route changes

salt 'proxy*' state.apply haproxy.route
Use haproxy.route for day-to-day route additions and removals. Use the full state.apply haproxy only when changing globals, defaults, frontend binds, or backend server pools — changes that require a config file update and service reload.
The runtime sync (haproxy_sync_routes) only runs onchanges — when the routes.map file content actually changes. If you need to force a sync after a haproxy restart lost in-memory routes without a pillar change, use state.apply haproxy.route which also runs per-route idempotent add map commands on every apply.

Applying the full state

salt 'proxy*' state.apply haproxy

Build docs developers (and LLMs) love