haproxy
MintPress controller gem for installing, configuring, and managing HAProxy on remote hosts via a transport abstraction.
Overview
This gem provides a Ruby API for automating the full lifecycle of HAProxy — installation, configuration rendering, validation, and service management — against any host reachable via a MintPress transport (SSH/RPC). Configuration is expressed as a composable object model (Config, Frontend, Backend, BackendServer, Stats) that is rendered to a validated haproxy.cfg before being deployed. The gem lives in the MintPress monorepo alongside the Oracle FMW controller gems and follows the same transport-based, idempotent automation patterns used throughout the platform.
Requirements
-
Ruby compatible with the MintPress 399.x gem family
-
MintPress infrastructure gems:
mintpress-infrastructure,mintpress-logger,mintpress-common,mintpress-resources(all~> 399, sourced from the LimePoint Artifactory instance) -
HAProxy on the target host:
-
Package install (default): targets whatever version the OS package manager provides (
yumorapt-get) -
Binary install (fallback): requires a pre-built
haproxybinary provided viasoftware_stage; the gem copies andchmod +xs it intoinstall_dir
Installation
From within the gem’s subdirectory:
bundle install
Dependencies are pulled from https://artifactory.limepoint.engineering/artifactory/api/gems/mintpress-gems-master.
Quick Start
Minimal example: install HAProxy, configure one HTTP backend, and start the service.
require 'haproxy'
host = MintPress::Infrastructure::Host.new(
name: 'proxy01.example.com',
connect_user: 'root',
keys: ['~/.ssh/id_rsa']
)
haproxy = MintPress::HAProxy::Installation.new(
host: host,
config: MintPress::HAProxy::Config.new(
frontends: {
'http_in' => MintPress::HAProxy::Frontend.new(
bind: '*:80',
default_backend: 'app_servers'
)
},
backends: {
'app_servers' => MintPress::HAProxy::Backend.new(
servers: {
'app1' => MintPress::HAProxy::BackendServer.new(
name: 'app1', address: '10.0.1.11', port: 8080
)
}
)
}
)
)
haproxy.install
haproxy.configure
haproxy.start
Host Connection
MintPress::Infrastructure::Host is the standard MintPress connectivity object. Pass it as host: to Installation.new; the transport is derived automatically.
# SSH key authentication
host = MintPress::Infrastructure::Host.new(
name: 'proxy01.example.com',
connect_user: 'oracle',
keys: ['~/.ssh/id_rsa']
)
# Password authentication
host = MintPress::Infrastructure::Host.new(
name: 'proxy01.example.com',
connect_user: 'oracle',
password: 'secret'
)
To use a custom transport directly (e.g. when the transport is shared with other controllers), pass it as transport: and omit host:.
Configuration Model
The object hierarchy mirrors the HAProxy configuration file structure:
Installation
└── config: Config
├── global_config: Array # verbatim global stanza lines
├── defaults_config: Array # verbatim defaults stanza lines
├── frontends: Hash # frontend_name => Frontend
│ └── Frontend
│ └── use_backends: Hash # acl_name => backend_name
├── backends: Hash # backend_name => Backend
│ └── Backend
│ └── servers: Hash # server_name => BackendServer
└── stats: Stats
Config, Frontend, Backend, BackendServer, and Stats all use MintPress::Mixins::Properties for attribute declaration. The Config object is passed to an ERB template at render time; no intermediate string manipulation is required in calling code.
Scenarios
HTTP load balancing
Roundrobin across three app servers with X-Forwarded-For injection and an HTTP health check endpoint.
require 'haproxy'
host = MintPress::Infrastructure::Host.new(
name: 'proxy01.example.com', connect_user: 'root', keys: ['~/.ssh/id_rsa']
)
config = MintPress::HAProxy::Config.new(
frontends: {
'http_in' => MintPress::HAProxy::Frontend.new(
bind: '*:80',
mode: 'http',
options: ['forwardfor', 'http-server-close'],
extra: ['monitor-uri /health'],
default_backend: 'app_servers'
)
},
backends: {
'app_servers' => MintPress::HAProxy::Backend.new(
mode: 'http',
balance: 'roundrobin',
options: ['httpchk GET /health'],
servers: {
'app1' => MintPress::HAProxy::BackendServer.new(
name: 'app1', address: '10.0.1.11', port: 8080,
check: true, inter: 2000, rise: 3, fall: 2
),
'app2' => MintPress::HAProxy::BackendServer.new(
name: 'app2', address: '10.0.1.12', port: 8080,
check: true, inter: 2000, rise: 3, fall: 2
),
'app3' => MintPress::HAProxy::BackendServer.new(
name: 'app3', address: '10.0.1.13', port: 8080,
check: true, inter: 2000, rise: 3, fall: 2, weight: 2
)
}
)
},
stats: MintPress::HAProxy::Stats.new(enabled: true, bind: '*:8404')
)
haproxy = MintPress::HAProxy::Installation.new(host: host, config: config)
haproxy.install
haproxy.configure
haproxy.start
SSL/TLS termination
Port 80 issues a 301 redirect; port 443 terminates TLS and forwards plain HTTP to backends.
require 'haproxy'
host = MintPress::Infrastructure::Host.new(
name: 'proxy01.example.com', connect_user: 'root', keys: ['~/.ssh/id_rsa']
)
config = MintPress::HAProxy::Config.new(
frontends: {
'http_redirect' => MintPress::HAProxy::Frontend.new(
bind: '*:80',
mode: 'http',
extra: ['redirect scheme https code 301']
),
'https_in' => MintPress::HAProxy::Frontend.new(
bind: '*:443',
mode: 'http',
ssl: true,
ssl_certificate: '/etc/ssl/private/example.com.pem',
ssl_ciphers: 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256',
options: ['forwardfor', 'http-server-close'],
extra: ['http-response set-header Strict-Transport-Security "max-age=31536000"'],
default_backend: 'app_servers'
)
},
backends: {
'app_servers' => MintPress::HAProxy::Backend.new(
mode: 'http',
balance: 'roundrobin',
options: ['httpchk GET /health'],
servers: {
'app1' => MintPress::HAProxy::BackendServer.new(
name: 'app1', address: '10.0.1.11', port: 8080, check: true
),
'app2' => MintPress::HAProxy::BackendServer.new(
name: 'app2', address: '10.0.1.12', port: 8080, check: true
)
}
)
},
stats: MintPress::HAProxy::Stats.new(
enabled: true, bind: '*:8404',
auth_user: 'admin', auth_password: 'changeme'
)
)
haproxy = MintPress::HAProxy::Installation.new(host: host, config: config)
haproxy.validate # dry-run validation before deploying
haproxy.configure
haproxy.start
TCP load balancing
PostgreSQL with leastconn and TCP health checks. Redis with health checks disabled (see note in Health Check Tuning).
require 'haproxy'
host = MintPress::Infrastructure::Host.new(
name: 'proxy01.example.com', connect_user: 'root', keys: ['~/.ssh/id_rsa']
)
config = MintPress::HAProxy::Config.new(
frontends: {
'pg_frontend' => MintPress::HAProxy::Frontend.new(
bind: '*:5432', mode: 'tcp', default_backend: 'pg_cluster'
),
'redis_frontend' => MintPress::HAProxy::Frontend.new(
bind: '*:6379', mode: 'tcp', default_backend: 'redis_nodes'
)
},
backends: {
'pg_cluster' => MintPress::HAProxy::Backend.new(
mode: 'tcp', balance: 'leastconn', maxconn: 200,
servers: {
'pg1' => MintPress::HAProxy::BackendServer.new(
name: 'pg1', address: '10.0.2.11', port: 5432,
check: true, inter: 3000, rise: 2, fall: 3
),
'pg2' => MintPress::HAProxy::BackendServer.new(
name: 'pg2', address: '10.0.2.12', port: 5432,
check: true, inter: 3000, rise: 2, fall: 3
)
}
),
'redis_nodes' => MintPress::HAProxy::Backend.new(
mode: 'tcp', balance: 'roundrobin',
servers: {
'redis1' => MintPress::HAProxy::BackendServer.new(
name: 'redis1', address: '10.0.3.11', port: 6379, check: false
),
'redis2' => MintPress::HAProxy::BackendServer.new(
name: 'redis2', address: '10.0.3.12', port: 6379, check: false
)
}
)
}
)
haproxy = MintPress::HAProxy::Installation.new(host: host, config: config)
haproxy.configure_and_reload
Enabling the stats page
config = MintPress::HAProxy::Config.new(
# ... frontends and backends ...
stats: MintPress::HAProxy::Stats.new(
enabled: true,
bind: '*:8404',
uri: '/stats',
refresh: 10,
auth_user: 'admin',
auth_password: 'changeme'
)
)
Stats are rendered as a dedicated frontend stats stanza. When enabled is false (the default) the stanza is omitted entirely.
Using a pre-built config file
When config_source is set, the Config object and ERB template are bypassed. The specified local file is copied to the target, validated with haproxy -c, and deployed.
haproxy = MintPress::HAProxy::Installation.new(
host: host,
config_source: '/path/to/my/haproxy.cfg'
)
haproxy.configure
haproxy.start
Health Check Tuning
Health check behaviour is configured per BackendServer:
| Property | Description |
|---|---|
| ‘check` | ‘true` to enable health checks (default). Set `false` to disable entirely — useful for backends that cannot handle bare TCP probes (e.g. password-protected Redis). |
| ‘inter` | Check interval in milliseconds. Lower values detect failures faster but increase probe traffic. |
| ‘rise` | Consecutive successful checks before marking the server UP. Higher values reduce flapping. |
| ‘fall` | Consecutive failed checks before marking the server DOWN. Higher values tolerate transient failures. |
| ‘maxconn` | Per-server connection cap. New connections queue at the backend when the limit is reached. |
Example with conservative thresholds suitable for a database backend:
MintPress::HAProxy::BackendServer.new(
name: 'db1',
address: '10.0.2.11',
port: 5432,
check: true,
inter: 3000, # check every 3 s
rise: 2, # 2 successes → UP
fall: 3, # 3 failures → DOWN
maxconn: 100
)
TCP health checks for protocols with a connection handshake (Redis with requirepass, MySQL, etc.) produce spurious error log entries on the backend because HAProxy opens a bare TCP connection without authenticating. Disable check for those servers and monitor availability at the application layer instead.
Idempotency
All mutating methods are safe to call repeatedly:
-
install— checksexists?first; skips if the binary is already executable athaproxy_binary -
uninstall— checksexists?first; no-ops if not installed -
configure— renders the config to a remote temp file, validates it, then does adiffagainst the deployed file; writes and backs up only if content differs -
configure_and_reload— callsconfigure, then only callsreloadifrunning?returnstrue -
start— checksrunning?first; no-ops if already active -
stop— checksrunning?first; no-ops if already stopped
Config Validation
validate is a standalone dry-run that renders the config (or uses config_source) to a remote temp file and runs haproxy -c -f <tmpfile>. It raises on any validation error. The temp file is always discarded after validation; nothing is deployed.
configure runs the same validation step internally before writing. If validation fails and keep_failed_config is true (the default), the rendered file is copied to /tmp/haproxy_failed_<timestamp>.cfg on the target host for inspection. Set keep_failed_config: false to suppress this.
haproxy = MintPress::HAProxy::Installation.new(
host: host,
config: config,
keep_failed_config: true # default; set false to discard on failure
)
# Validate without deploying
haproxy.validate
# Validate and deploy (keeps failed config on target if invalid)
haproxy.configure
SSL/TLS Notes
-
PEM format:
ssl_certificatemust point to a single PEM file on the target host containing both the certificate and the private key concatenated in that order. Prepare it before callingconfigure:bash cat server.crt server.key > /etc/ssl/private/example.com.pem chmod 600 /etc/ssl/private/example.com.pem -
HAProxy terminates TLS: with
ssl: trueon a frontend, HAProxy decrypts the connection and forwards plain HTTP to backend servers. BackendBackendServerentries still use standard HTTP ports. For passthrough (no decryption), usemode: 'tcp'on both the frontend and backend and omitssl:. -
Cipher recommendation: the
ssl_ciphersvalue is passed verbatim to the HAProxyciphersdirective. Use a modern restricted cipher string, e.g.ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384. -
Stats socket: the default
global_configdoes not include astats socketline. If the stats socket is needed (for runtime API access or thehaproxy -sfgraceful-reload path used in the non-systemd fallback), add it toglobal_configand ensure theRuntimeDirectory=haproxysystemd drop-in is present so the socket directory exists.
Service Management
On start, stop, reload, and restart, the controller first checks for systemctl on the target host. If found, all lifecycle operations delegate to systemd:
systemctl start <service_name>
systemctl stop <service_name>
systemctl reload <service_name> # graceful reload
systemctl restart <service_name>
When systemctl is absent (non-systemd hosts), the controller falls back to direct process management:
-
start:
haproxy -f <config_file> -D -p <pid_file> -
stop:
kill <pid>via the PID file, orpkill -f haproxyif the PID file is missing -
reload:
haproxy -f <config_file> -D -p <pid_file> -sf <old_pids>— starts new workers and signals old workers to finish existing connections before exiting; no connections are dropped -
restart:
stopthenstart
running? uses systemctl is-active when systemd is available; otherwise it reads the PID file and sends signal 0 to test liveness.
API Reference
MintPress::HAProxy::Installation
| Property | Type | Default | Description |
|---|---|---|---|
| ‘host` | ‘Infrastructure::Host` | — | Target host |
| ‘transport` | Object | ‘host.transport` | Remote execution transport; derived from ‘host` by default |
| ‘package_name` | String | ‘’haproxy’‘ | Package name as it appears in the OS package manager |
| ‘service_name` | String | ‘’haproxy’‘ | systemd unit name |
| ‘haproxy_binary` | String | ‘’/usr/sbin/haproxy’‘ | Path to the HAProxy executable on the target |
| ‘config_file` | String | ‘’/etc/haproxy/haproxy.cfg’‘ | Deployment path for the rendered config |
| ‘pid_file` | String | ‘’/var/run/haproxy/haproxy.pid’‘ | PID file path (used when systemd is unavailable) |
| ‘software_stage` | String | — | Local path to a pre-built binary; used when no package manager is detected |
| ‘install_dir` | String | ‘’/usr/sbin’‘ | Target directory for binary installs from ‘software_stage` |
| ‘config` | ‘Config` | ‘Config.new` | Configuration object |
| ‘config_source` | String | — | Local path to a pre-built ‘haproxy.cfg`; when set, `config` and the ERB template are ignored |
| ‘keep_failed_config` | Boolean | ‘true` | Copy failed config to ‘/tmp` on the target for debugging |
| Method | Description |
|---|---|
| ‘install` | Install via ‘yum`/`apt-get`; fall back to `software_stage` binary if no package manager found; idempotent |
| ‘uninstall` | Remove via package manager or delete binary directly; idempotent |
| ‘exists?` | Returns ‘true` if the HAProxy binary is executable at `haproxy_binary` |
| ‘validate` | Render config to remote temp file and run ‘haproxy -c`; raises on invalid config; does not deploy |
| ‘configure` | Validate, diff, and deploy config; backs up existing config to ‘config_file.bak`; idempotent |
| ‘configure_and_reload` | ‘configure` then `reload` if currently running |
| ‘start` | Start service; no-op if already running |
| ‘stop` | Stop service; no-op if already stopped |
| ‘reload` | Graceful reload: new workers replace old without dropping connections |
| ‘restart` | Full stop and start |
| ‘running?` | Returns ‘true` if HAProxy is active |
| ‘status` | Returns ‘systemctl status` output, or `’running’‘/`’stopped’‘ on non-systemd hosts |
MintPress::HAProxy::Config
| Property | Type | Default | Description |
|---|---|---|---|
| ‘global_config` | Array | (see below) | Verbatim lines for the ‘global` stanza |
| ‘defaults_config` | Array | (see below) | Verbatim lines for the ‘defaults` stanza |
| ‘frontends` | Hash | ‘{}` | ‘frontend_name => Frontend` |
| ‘backends` | Hash | ‘{}` | ‘backend_name => Backend` |
| ‘stats` | ‘Stats` | ‘Stats.new` | Stats endpoint; set ‘stats.enabled = true` to activate |
Default global_config:
log /dev/log local0
log /dev/log local1 notice
chroot /var/lib/haproxy
user haproxy
group haproxy
daemon
Default defaults_config:
log global
mode http
option httplog
option dontlognull
timeout connect 5000
timeout client 50000
timeout server 50000
MintPress::HAProxy::Frontend
| Property | Type | Default | Description |
|---|---|---|---|
| ‘bind` | String | required | Listen address and port, e.g. ‘’*:80’‘ |
| ‘mode` | String | ‘’http’‘ | ‘’http’‘ or `’tcp’‘ |
| ‘maxconn` | Integer | — | Max concurrent connections on this frontend |
| ‘ssl` | Boolean | ‘false` | Enable SSL/TLS termination |
| ‘ssl_certificate` | String | — | Path to PEM file on target host |
| ‘ssl_ciphers` | String | — | OpenSSL cipher string |
| ‘default_backend` | String | — | Fallback backend name |
| ‘acls` | Array | ‘[]` | Full ACL declaration lines, e.g. ‘[’is_api path_beg /api’]‘ |
| ‘use_backends` | Hash | ‘{}` | ACL name => backend name; rendered as ‘use_backend <b> if <acl>` |
| ‘options` | Array | ‘[]` | HAProxy ‘option` directives |
| ‘extra` | Array | ‘[]` | Verbatim extra lines appended to the stanza |
MintPress::HAProxy::Backend
| Property | Type | Default | Description |
|---|---|---|---|
| ‘mode` | String | ‘’http’‘ | ‘’http’‘ or `’tcp’‘ |
| ‘balance` | String | ‘’roundrobin’‘ | Balance algorithm, e.g. ‘’roundrobin’‘, `’leastconn’‘, `’source’‘ |
| ‘maxconn` | Integer | — | Max concurrent connections queued to this backend |
| ‘servers` | Hash | ‘{}` | ‘server_name => BackendServer` |
| ‘options` | Array | ‘[]` | HAProxy ‘option` directives |
| ‘extra` | Array | ‘[]` | Verbatim extra lines appended to the stanza |
MintPress::HAProxy::BackendServer
| Property | Type | Default | Description |
|---|---|---|---|
| ‘name` | String | required | Server label in the generated config |
| ‘address` | String | required | IP or hostname |
| ‘port` | Integer | required | TCP port |
| ‘check` | Boolean | ‘true` | Enable health checks |
| ‘weight` | Integer | — | Load balancing weight; higher values receive proportionally more connections |
| ‘inter` | Integer | — | Health check interval (ms) |
| ‘rise` | Integer | — | Consecutive successes before marking UP |
| ‘fall` | Integer | — | Consecutive failures before marking DOWN |
| ‘maxconn` | Integer | — | Max concurrent connections to this server |
| ‘options` | Array | ‘[]` | Extra verbatim server-line flags |
MintPress::HAProxy::Stats
| Property | Type | Default | Description |
|---|---|---|---|
| ‘enabled` | Boolean | ‘false` | Render the stats frontend stanza |
| ‘bind` | String | ‘’*:8404’‘ | Listen address and port |
| ‘uri` | String | ‘’/stats’‘ | Stats page URI path |
| ‘refresh` | Integer | ‘10` | Auto-refresh interval (seconds) |
| ‘auth_user` | String | — | HTTP basic auth username |
| ‘auth_password` | String | — | HTTP basic auth password |
| ‘extra` | Array | ‘[]` | Verbatim extra lines appended to the stats stanza |
Development
bundle install # install dependencies
rake spec # run RSpec tests
rake style # run RuboCop
rake # run spec and style (default)
To run a single spec file:
bundle exec rspec test/unit/test-installation.rb