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 (yum or apt-get)

  • Binary install (fallback): requires a pre-built haproxy binary provided via software_stage; the gem copies and chmod +xs it into install_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 — checks exists? first; skips if the binary is already executable at haproxy_binary

  • uninstall — checks exists? first; no-ops if not installed

  • configure — renders the config to a remote temp file, validates it, then does a diff against the deployed file; writes and backs up only if content differs

  • configure_and_reload — calls configure, then only calls reload if running? returns true

  • start — checks running? first; no-ops if already active

  • stop — checks running? 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_certificate must 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 calling configure: 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: true on a frontend, HAProxy decrypts the connection and forwards plain HTTP to backend servers. Backend BackendServer entries still use standard HTTP ports. For passthrough (no decryption), use mode: 'tcp' on both the frontend and backend and omit ssl:.

  • Cipher recommendation: the ssl_ciphers value is passed verbatim to the HAProxy ciphers directive. 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_config does not include a stats socket line. If the stats socket is needed (for runtime API access or the haproxy -sf graceful-reload path used in the non-systemd fallback), add it to global_config and ensure the RuntimeDirectory=haproxy systemd 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, or pkill -f haproxy if 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: stop then start

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