Example: HAProxy Load Balancer

HAProxy Load Balancer

Set up HAProxy as a reverse proxy and load balancer with health checks, SSL termination, and stats dashboard.

Directory Structure

cookbooks/
  haproxy/
    default.rb
    templates/
      haproxy.cfg.erb
nodes/
  lb01.json

Node Attributes

{
  "haproxy": {
    "stats_port": 8404,
    "stats_user": "admin",
    "stats_password": "haproxy-stats-pass",
    "frontend_port": 443,
    "frontend_http_port": 80,
    "ssl_cert_path": "/etc/ssl/private/app.pem",
    "backend_port": 3000,
    "backend_servers": [
      { "name": "app01", "address": "10.0.1.10" },
      { "name": "app02", "address": "10.0.1.11" },
      { "name": "app03", "address": "10.0.1.12" }
    ],
    "health_check_path": "/health",
    "health_check_interval": 5000,
    "max_connections": 4096,
    "timeout_connect": 5000,
    "timeout_client": 50000,
    "timeout_server": 50000
  }
}

Recipe

# cookbooks/haproxy/default.rb

ha = node['haproxy']

package 'haproxy' do
  action :install
end

directory '/etc/haproxy' do
  owner 'root'
  group 'root'
  mode '0755'
end

directory '/var/lib/haproxy' do
  owner 'haproxy'
  group 'haproxy'
  mode '0750'
end

directory '/var/log/haproxy' do
  owner 'haproxy'
  group 'haproxy'
  mode '0750'
end

# Kernel tuning for high connection counts
execute 'sysctl-haproxy-somaxconn' do
  command 'sysctl -w net.core.somaxconn=4096'
  not_if 'sysctl net.core.somaxconn | grep -q 4096'
end

file '/etc/sysctl.d/99-haproxy.conf' do
  content "net.core.somaxconn = #{ha['max_connections']}\nnet.ipv4.ip_nonlocal_bind = 1\n"
  owner 'root'
  group 'root'
  mode '0644'
end

template '/etc/haproxy/haproxy.cfg' do
  source 'templates/haproxy.cfg.erb'
  owner 'root'
  group 'root'
  mode '0644'
  variables(
    stats_port: ha['stats_port'],
    stats_user: ha['stats_user'],
    stats_password: ha['stats_password'],
    frontend_port: ha['frontend_port'],
    frontend_http_port: ha['frontend_http_port'],
    ssl_cert_path: ha['ssl_cert_path'],
    backend_port: ha['backend_port'],
    backend_servers: ha['backend_servers'],
    health_check_path: ha['health_check_path'],
    health_check_interval: ha['health_check_interval'],
    max_connections: ha['max_connections'],
    timeout_connect: ha['timeout_connect'],
    timeout_client: ha['timeout_client'],
    timeout_server: ha['timeout_server']
  )
  notifies :reload, 'service[haproxy]'
end

# Validate config before restart
execute 'haproxy-check-config' do
  command 'haproxy -c -f /etc/haproxy/haproxy.cfg'
  only_if 'test -f /etc/haproxy/haproxy.cfg'
end

service 'haproxy' do
  action [:enable, :start]
end

Templates

haproxy.cfg.erb

global
    maxconn <%= @max_connections %>
    log /dev/log local0
    log /dev/log local1 notice
    chroot /var/lib/haproxy
    stats socket /run/haproxy/admin.sock mode 660 level admin
    user haproxy
    group haproxy
    daemon

defaults
    log     global
    mode    http
    option  httplog
    option  dontlognull
    option  forwardfor
    timeout connect <%= @timeout_connect %>ms
    timeout client  <%= @timeout_client %>ms
    timeout server  <%= @timeout_server %>ms

# Stats dashboard
listen stats
    bind *:<%= @stats_port %>
    stats enable
    stats uri /stats
    stats auth <%= @stats_user %>:<%= @stats_password %>

# HTTP to HTTPS redirect
frontend http_front
    bind *:<%= @frontend_http_port %>
    redirect scheme https code 301

# HTTPS frontend
frontend https_front
    bind *:<%= @frontend_port %> ssl crt <%= @ssl_cert_path %>
    default_backend app_servers

# Application backend
backend app_servers
    balance roundrobin
    option httpchk GET <%= @health_check_path %>
<% @backend_servers.each do |server| %>
    server <%= server['name'] %> <%= server['address'] %>:<%= @backend_port %> check inter <%= @health_check_interval %>ms
<% end %>

Running

itamae ssh -j nodes/lb01.json -h lb01.example.com cookbooks/haproxy/default.rb