HAProxy Data Plane API: Dynamic Configuration Management

Share
HAProxy Data Plane API: Dynamic Configuration Management

Managing HAProxy configuration traditionally requires editing files and reloading the service. The Data Plane API transforms HAProxy into a dynamically configurable load balancer with a RESTful interface, enabling real-time configuration changes without service interruption.

Why Data Plane API?

The Data Plane API provides:

  • Runtime configuration - Add/remove backends and servers without reloads
  • RESTful interface - Standard HTTP methods for configuration management
  • Transaction support - Atomic changes with rollback capability
  • Integration friendly - Perfect for CI/CD pipelines and orchestration tools
  • Version control - Track configuration changes over time

Installing the Data Plane API

Download and Install

# Download the latest release
DATAPLANE_VERSION="2.9.0"
wget https://github.com/haproxytech/dataplaneapi/releases/download/v${DATAPLANEAPI_VERSION}/dataplaneapi_${DATAPLANEAPI_VERSION}_linux_amd64.tar.gz

# Extract and install
tar xzf dataplaneapi_${DATAPLANEAPI_VERSION}_linux_amd64.tar.gz
sudo mv dataplaneapi /usr/local/bin/
sudo chmod +x /usr/local/bin/dataplaneapi

Configuration File

Create /etc/haproxy/dataplaneapi.yaml:

config_version: 2
name: haproxy_dataplane
mode: single

dataplaneapi:
  host: 0.0.0.0
  port: 5555
  scheme:
    - http
  
  user:
    - name: admin
      password: $6$rounds=500000$...$...  # Use mkpasswd to generate
      insecure: false
  
  transaction:
    transaction_dir: /tmp/haproxy
  
  resources:
    maps_dir: /etc/haproxy/maps
    ssl_certs_dir: /etc/haproxy/certs
    spoe_dir: /etc/haproxy/spoe
    spoe_transaction_dir: /tmp/spoe-haproxy

haproxy:
  config_file: /etc/haproxy/haproxy.cfg
  haproxy_bin: /usr/sbin/haproxy
  reload:
    reload_delay: 5
    reload_cmd: systemctl reload haproxy
    restart_cmd: systemctl restart haproxy
    status_cmd: systemctl status haproxy

Generate Password Hash

# Install mkpasswd if needed
sudo apt install whois

# Generate SHA-512 password hash
mkpasswd -m sha-512 'your-secure-password'

HAProxy Configuration for API

Add to /etc/haproxy/haproxy.cfg:

global
    log /dev/log local0
    chroot /var/lib/haproxy
    stats socket /run/haproxy/admin.sock mode 660 level admin expose-fd listeners
    stats timeout 30s
    user haproxy
    group haproxy
    daemon
    
    # Required for Data Plane API
    master-worker

defaults
    log global
    mode http
    option httplog
    option dontlognull
    timeout connect 5000
    timeout client 50000
    timeout server 50000

# Data Plane API Frontend
frontend dataplane_api
    bind *:5556 ssl crt /etc/haproxy/certs/api.pem
    mode http
    http-request auth unless { http_auth(api_users) }
    default_backend dataplane_api_backend

backend dataplane_api_backend
    mode http
    server dataplane 127.0.0.1:5555

userlist api_users
    user admin password $6$rounds=500000$...$...

# Application frontends and backends
frontend http_front
    bind *:80
    bind *:443 ssl crt /etc/haproxy/certs/
    default_backend web_servers

backend web_servers
    balance roundrobin
    option httpchk GET /health
    server web1 192.168.1.10:8080 check
    server web2 192.168.1.11:8080 check

Starting the Data Plane API

Systemd Service

Create /etc/systemd/system/dataplaneapi.service:

[Unit]
Description=HAProxy Data Plane API
After=network.target haproxy.service
Requires=haproxy.service

[Service]
Type=simple
ExecStart=/usr/local/bin/dataplaneapi -f /etc/haproxy/dataplaneapi.yaml
Restart=on-failure
RestartSec=5
User=haproxy
Group=haproxy

[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable dataplaneapi
sudo systemctl start dataplaneapi

API Operations

Authentication

# Set credentials
export HAPROXY_API="https://haproxy.example.com:5556/v2"
export AUTH="admin:your-secure-password"

# Test connection
curl -s -u $AUTH $HAPROXY_API/info | jq .

Get Current Configuration

# Get HAProxy configuration
curl -s -u $AUTH $HAPROXY_API/services/haproxy/configuration/raw | jq .

# Get specific backend
curl -s -u $AUTH "$HAPROXY_API/services/haproxy/configuration/backends/web_servers" | jq .

# List all servers in a backend
curl -s -u $AUTH "$HAPROXY_API/services/haproxy/configuration/servers?backend=web_servers" | jq .

Working with Transactions

The API uses transactions to ensure atomic configuration changes:

# Get current configuration version
VERSION=$(curl -s -u $AUTH $HAPROXY_API/services/haproxy/configuration/version)
echo "Current version: $VERSION"

# Start a transaction
TRANSACTION=$(curl -s -u $AUTH -X POST \
  "$HAPROXY_API/services/haproxy/transactions?version=$VERSION" | jq -r '.id')
echo "Transaction ID: $TRANSACTION"

# Make changes within transaction (see examples below)

# Commit transaction
curl -s -u $AUTH -X PUT \
  "$HAPROXY_API/services/haproxy/transactions/$TRANSACTION"

# Or rollback if needed
curl -s -u $AUTH -X DELETE \
  "$HAPROXY_API/services/haproxy/transactions/$TRANSACTION"

Adding a Server

# Get version and start transaction
VERSION=$(curl -s -u $AUTH $HAPROXY_API/services/haproxy/configuration/version)
TRANSACTION=$(curl -s -u $AUTH -X POST \
  "$HAPROXY_API/services/haproxy/transactions?version=$VERSION" | jq -r '.id')

# Add new server
curl -s -u $AUTH -X POST \
  "$HAPROXY_API/services/haproxy/configuration/servers?backend=web_servers&transaction_id=$TRANSACTION" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "web3",
    "address": "192.168.1.12",
    "port": 8080,
    "check": "enabled",
    "weight": 100
  }'

# Commit changes
curl -s -u $AUTH -X PUT \
  "$HAPROXY_API/services/haproxy/transactions/$TRANSACTION"

Removing a Server

VERSION=$(curl -s -u $AUTH $HAPROXY_API/services/haproxy/configuration/version)
TRANSACTION=$(curl -s -u $AUTH -X POST \
  "$HAPROXY_API/services/haproxy/transactions?version=$VERSION" | jq -r '.id')

# Remove server
curl -s -u $AUTH -X DELETE \
  "$HAPROXY_API/services/haproxy/configuration/servers/web3?backend=web_servers&transaction_id=$TRANSACTION"

curl -s -u $AUTH -X PUT \
  "$HAPROXY_API/services/haproxy/transactions/$TRANSACTION"

Modifying Server Weight

VERSION=$(curl -s -u $AUTH $HAPROXY_API/services/haproxy/configuration/version)
TRANSACTION=$(curl -s -u $AUTH -X POST \
  "$HAPROXY_API/services/haproxy/transactions?version=$VERSION" | jq -r '.id')

# Update server weight
curl -s -u $AUTH -X PUT \
  "$HAPROXY_API/services/haproxy/configuration/servers/web1?backend=web_servers&transaction_id=$TRANSACTION" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "web1",
    "address": "192.168.1.10",
    "port": 8080,
    "check": "enabled",
    "weight": 50
  }'

curl -s -u $AUTH -X PUT \
  "$HAPROXY_API/services/haproxy/transactions/$TRANSACTION"

Creating a New Backend

VERSION=$(curl -s -u $AUTH $HAPROXY_API/services/haproxy/configuration/version)
TRANSACTION=$(curl -s -u $AUTH -X POST \
  "$HAPROXY_API/services/haproxy/transactions?version=$VERSION" | jq -r '.id')

# Create backend
curl -s -u $AUTH -X POST \
  "$HAPROXY_API/services/haproxy/configuration/backends?transaction_id=$TRANSACTION" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "api_servers",
    "mode": "http",
    "balance": {"algorithm": "leastconn"},
    "httpchk": {
      "method": "GET",
      "uri": "/health"
    }
  }'

# Add servers to new backend
curl -s -u $AUTH -X POST \
  "$HAPROXY_API/services/haproxy/configuration/servers?backend=api_servers&transaction_id=$TRANSACTION" \
  -H "Content-Type: application/json" \
  -d '{"name": "api1", "address": "192.168.1.20", "port": 3000, "check": "enabled"}'

curl -s -u $AUTH -X POST \
  "$HAPROXY_API/services/haproxy/configuration/servers?backend=api_servers&transaction_id=$TRANSACTION" \
  -H "Content-Type: application/json" \
  -d '{"name": "api2", "address": "192.168.1.21", "port": 3000, "check": "enabled"}'

curl -s -u $AUTH -X PUT \
  "$HAPROXY_API/services/haproxy/transactions/$TRANSACTION"

Runtime API Operations

Some changes can be made at runtime without configuration reload:

Server State Management

# Get server runtime state
curl -s -u $AUTH "$HAPROXY_API/services/haproxy/runtime/servers?backend=web_servers" | jq .

# Disable server (drain mode)
curl -s -u $AUTH -X PUT \
  "$HAPROXY_API/services/haproxy/runtime/servers/web1?backend=web_servers" \
  -H "Content-Type: application/json" \
  -d '{"admin_state": "drain"}'

# Set server to maintenance
curl -s -u $AUTH -X PUT \
  "$HAPROXY_API/services/haproxy/runtime/servers/web1?backend=web_servers" \
  -H "Content-Type: application/json" \
  -d '{"admin_state": "maint"}'

# Re-enable server
curl -s -u $AUTH -X PUT \
  "$HAPROXY_API/services/haproxy/runtime/servers/web1?backend=web_servers" \
  -H "Content-Type: application/json" \
  -d '{"admin_state": "ready"}'

Runtime Weight Adjustment

# Adjust weight without reload
curl -s -u $AUTH -X PUT \
  "$HAPROXY_API/services/haproxy/runtime/servers/web1?backend=web_servers" \
  -H "Content-Type: application/json" \
  -d '{"operational_state": "up", "weight": 150}'

Automation Scripts

Blue-Green Deployment Script

#!/bin/bash
# blue-green-deploy.sh - Switch traffic between blue and green environments

set -e

HAPROXY_API="https://haproxy.example.com:5556/v2"
AUTH="admin:your-secure-password"
BACKEND="app_servers"

BLUE_SERVERS=("blue1:192.168.1.10:8080" "blue2:192.168.1.11:8080")
GREEN_SERVERS=("green1:192.168.1.20:8080" "green2:192.168.1.21:8080")

get_version() {
  curl -s -u $AUTH $HAPROXY_API/services/haproxy/configuration/version
}

start_transaction() {
  curl -s -u $AUTH -X POST \
    "$HAPROXY_API/services/haproxy/transactions?version=$1" | jq -r '.id'
}

add_server() {
  local name=$1
  local addr=$2
  local port=$3
  local txn=$4
  
  curl -s -u $AUTH -X POST \
    "$HAPROXY_API/services/haproxy/configuration/servers?backend=$BACKEND&transaction_id=$txn" \
    -H "Content-Type: application/json" \
    -d "{\"name\": \"$name\", \"address\": \"$addr\", \"port\": $port, \"check\": \"enabled\"}"
}

remove_server() {
  local name=$1
  local txn=$2
  
  curl -s -u $AUTH -X DELETE \
    "$HAPROXY_API/services/haproxy/configuration/servers/${name}?backend=$BACKEND&transaction_id=$txn"
}

commit_transaction() {
  curl -s -u $AUTH -X PUT \
    "$HAPROXY_API/services/haproxy/transactions/$1"
}

deploy_environment() {
  local target=$1
  local servers_to_add
  local servers_to_remove
  
  if [ "$target" == "green" ]; then
    servers_to_add=("${GREEN_SERVERS[@]}")
    servers_to_remove=("${BLUE_SERVERS[@]}")
  else
    servers_to_add=("${BLUE_SERVERS[@]}")
    servers_to_remove=("${GREEN_SERVERS[@]}")
  fi
  
  echo "Switching to $target environment..."
  
  VERSION=$(get_version)
  TXN=$(start_transaction $VERSION)
  
  # Add new servers
  for server in "${servers_to_add[@]}"; do
    IFS=':' read -r name addr port <<< "$server"
    echo "Adding $name ($addr:$port)"
    add_server $name $addr $port $TXN
  done
  
  # Remove old servers
  for server in "${servers_to_remove[@]}"; do
    IFS=':' read -r name addr port <<< "$server"
    echo "Removing $name"
    remove_server $name $TXN 2>/dev/null || true
  done
  
  commit_transaction $TXN
  echo "Deployment complete!"
}

case "$1" in
  blue)  deploy_environment blue ;;
  green) deploy_environment green ;;
  *)     echo "Usage: $0 {blue|green}" ;;
esac

Canary Deployment Script

#!/bin/bash
# canary-deploy.sh - Gradual traffic shifting to new version

set -e

HAPROXY_API="https://haproxy.example.com:5556/v2"
AUTH="admin:your-secure-password"
BACKEND="web_servers"

CANARY_SERVER="web-canary"
CANARY_ADDRESS="192.168.1.50"
CANARY_PORT="8080"

shift_traffic() {
  local canary_weight=$1
  local production_weight=$((100 - canary_weight))
  
  echo "Shifting traffic: Canary=$canary_weight%, Production=$production_weight%"
  
  # Use runtime API for immediate weight changes
  curl -s -u $AUTH -X PUT \
    "$HAPROXY_API/services/haproxy/runtime/servers/${CANARY_SERVER}?backend=$BACKEND" \
    -H "Content-Type: application/json" \
    -d "{\"weight\": $canary_weight}"
}

add_canary() {
  VERSION=$(curl -s -u $AUTH $HAPROXY_API/services/haproxy/configuration/version)
  TXN=$(curl -s -u $AUTH -X POST \
    "$HAPROXY_API/services/haproxy/transactions?version=$VERSION" | jq -r '.id')
  
  curl -s -u $AUTH -X POST \
    "$HAPROXY_API/services/haproxy/configuration/servers?backend=$BACKEND&transaction_id=$TXN" \
    -H "Content-Type: application/json" \
    -d '{
      "name": "'$CANARY_SERVER'",
      "address": "'$CANARY_ADDRESS'",
      "port": '$CANARY_PORT',
      "check": "enabled",
      "weight": 0
    }'
  
  curl -s -u $AUTH -X PUT "$HAPROXY_API/services/haproxy/transactions/$TXN"
  echo "Canary server added with 0% traffic"
}

remove_canary() {
  VERSION=$(curl -s -u $AUTH $HAPROXY_API/services/haproxy/configuration/version)
  TXN=$(curl -s -u $AUTH -X POST \
    "$HAPROXY_API/services/haproxy/transactions?version=$VERSION" | jq -r '.id')
  
  curl -s -u $AUTH -X DELETE \
    "$HAPROXY_API/services/haproxy/configuration/servers/${CANARY_SERVER}?backend=$BACKEND&transaction_id=$TXN"
  
  curl -s -u $AUTH -X PUT "$HAPROXY_API/services/haproxy/transactions/$TXN"
  echo "Canary server removed"
}

case "$1" in
  add)    add_canary ;;
  remove) remove_canary ;;
  shift)  shift_traffic $2 ;;
  *)      echo "Usage: $0 {add|remove|shift <percentage>}" ;;
esac

Python Client Library

#!/usr/bin/env python3
import requests
from requests.auth import HTTPBasicAuth
import json

class HAProxyAPI:
    def __init__(self, base_url, username, password):
        self.base_url = base_url.rstrip('/')
        self.auth = HTTPBasicAuth(username, password)
        self.session = requests.Session()
        self.session.auth = self.auth
        self.session.verify = True  # Set to False for self-signed certs
    
    def get_version(self):
        """Get current configuration version"""
        r = self.session.get(f"{self.base_url}/services/haproxy/configuration/version")
        return r.text.strip()
    
    def start_transaction(self, version=None):
        """Start a new transaction"""
        if version is None:
            version = self.get_version()
        r = self.session.post(f"{self.base_url}/services/haproxy/transactions?version={version}")
        return r.json()['id']
    
    def commit_transaction(self, txn_id):
        """Commit a transaction"""
        r = self.session.put(f"{self.base_url}/services/haproxy/transactions/{txn_id}")
        return r.status_code == 202
    
    def rollback_transaction(self, txn_id):
        """Rollback a transaction"""
        r = self.session.delete(f"{self.base_url}/services/haproxy/transactions/{txn_id}")
        return r.status_code == 204
    
    def get_backends(self):
        """List all backends"""
        r = self.session.get(f"{self.base_url}/services/haproxy/configuration/backends")
        return r.json().get('data', [])
    
    def get_servers(self, backend):
        """List servers in a backend"""
        r = self.session.get(
            f"{self.base_url}/services/haproxy/configuration/servers",
            params={'backend': backend}
        )
        return r.json().get('data', [])
    
    def add_server(self, backend, name, address, port, weight=100, check=True):
        """Add a server to a backend"""
        txn = self.start_transaction()
        
        data = {
            'name': name,
            'address': address,
            'port': port,
            'weight': weight,
            'check': 'enabled' if check else 'disabled'
        }
        
        r = self.session.post(
            f"{self.base_url}/services/haproxy/configuration/servers",
            params={'backend': backend, 'transaction_id': txn},
            json=data
        )
        
        if r.status_code in [200, 201, 202]:
            self.commit_transaction(txn)
            return True
        else:
            self.rollback_transaction(txn)
            return False
    
    def remove_server(self, backend, name):
        """Remove a server from a backend"""
        txn = self.start_transaction()
        
        r = self.session.delete(
            f"{self.base_url}/services/haproxy/configuration/servers/{name}",
            params={'backend': backend, 'transaction_id': txn}
        )
        
        if r.status_code in [200, 202, 204]:
            self.commit_transaction(txn)
            return True
        else:
            self.rollback_transaction(txn)
            return False
    
    def set_server_state(self, backend, name, state):
        """Set server runtime state (ready/drain/maint)"""
        r = self.session.put(
            f"{self.base_url}/services/haproxy/runtime/servers/{name}",
            params={'backend': backend},
            json={'admin_state': state}
        )
        return r.status_code == 200
    
    def set_server_weight(self, backend, name, weight):
        """Set server weight at runtime"""
        r = self.session.put(
            f"{self.base_url}/services/haproxy/runtime/servers/{name}",
            params={'backend': backend},
            json={'weight': weight}
        )
        return r.status_code == 200


# Usage example
if __name__ == '__main__':
    api = HAProxyAPI(
        'https://haproxy.example.com:5556/v2',
        'admin',
        'your-secure-password'
    )
    
    # List backends
    print("Backends:")
    for backend in api.get_backends():
        print(f"  - {backend['name']}")
    
    # List servers
    print("\nServers in web_servers:")
    for server in api.get_servers('web_servers'):
        print(f"  - {server['name']}: {server['address']}:{server['port']}")
    
    # Add a new server
    print("\nAdding new server...")
    api.add_server('web_servers', 'web4', '192.168.1.14', 8080)
    
    # Put server in maintenance
    print("Setting web4 to maintenance...")
    api.set_server_state('web_servers', 'web4', 'maint')

Security Best Practices

TLS Configuration

# dataplaneapi.yaml with TLS
dataplaneapi:
  host: 0.0.0.0
  port: 5555
  scheme:
    - https
  
  tls:
    tls_certificate: /etc/haproxy/certs/api.pem
    tls_key: /etc/haproxy/certs/api.key
    tls_ca: /etc/haproxy/certs/ca.pem  # For mTLS
    tls_port: 5555

Network Restrictions

frontend dataplane_api
    bind *:5556 ssl crt /etc/haproxy/certs/api.pem
    
    # Restrict to management network
    acl allowed_networks src 10.0.0.0/8 192.168.0.0/16
    http-request deny unless allowed_networks
    
    # Rate limiting
    stick-table type ip size 100k expire 30s store http_req_rate(10s)
    http-request track-sc0 src
    http-request deny deny_status 429 if { sc_http_req_rate(0) gt 100 }
    
    http-request auth unless { http_auth(api_users) }
    default_backend dataplane_api_backend

Integration with CI/CD

GitLab CI Example

# .gitlab-ci.yml
stages:
  - deploy
  - traffic

variables:
  HAPROXY_API: "https://haproxy.example.com:5556/v2"

deploy-canary:
  stage: deploy
  script:
    - |
      # Add canary server
      VERSION=$(curl -s -u $HAPROXY_AUTH $HAPROXY_API/services/haproxy/configuration/version)
      TXN=$(curl -s -u $HAPROXY_AUTH -X POST \
        "$HAPROXY_API/services/haproxy/transactions?version=$VERSION" | jq -r '.id')
      
      curl -s -u $HAPROXY_AUTH -X POST \
        "$HAPROXY_API/services/haproxy/configuration/servers?backend=app_servers&transaction_id=$TXN" \
        -H "Content-Type: application/json" \
        -d '{"name": "canary", "address": "'$CANARY_IP'", "port": 8080, "weight": 10, "check": "enabled"}'
      
      curl -s -u $HAPROXY_AUTH -X PUT "$HAPROXY_API/services/haproxy/transactions/$TXN"
  only:
    - main

shift-traffic-50:
  stage: traffic
  script:
    - |
      curl -s -u $HAPROXY_AUTH -X PUT \
        "$HAPROXY_API/services/haproxy/runtime/servers/canary?backend=app_servers" \
        -H "Content-Type: application/json" \
        -d '{"weight": 50}'
  when: manual
  only:
    - main

promote-canary:
  stage: traffic
  script:
    - |
      # Shift all traffic to canary
      curl -s -u $HAPROXY_AUTH -X PUT \
        "$HAPROXY_API/services/haproxy/runtime/servers/canary?backend=app_servers" \
        -d '{"weight": 100}'
      
      # Set old servers to drain
      for server in web1 web2 web3; do
        curl -s -u $HAPROXY_AUTH -X PUT \
          "$HAPROXY_API/services/haproxy/runtime/servers/${server}?backend=app_servers" \
          -d '{"admin_state": "drain"}'
      done
  when: manual
  only:
    - main

Troubleshooting

Check API Status

# Test API connectivity
curl -v -u $AUTH $HAPROXY_API/info

# Check pending transactions
curl -s -u $AUTH $HAPROXY_API/services/haproxy/transactions | jq .

# View HAProxy stats
curl -s -u $AUTH $HAPROXY_API/services/haproxy/stats/native | jq .

Common Issues

  1. Transaction conflicts: Always use fresh version number
  2. Permission denied: Check user permissions in dataplaneapi.yaml
  3. Configuration invalid: Use GET endpoints to verify current state
  4. Reload failures: Check HAProxy configuration syntax

Next Steps

Now that you can dynamically manage HAProxy, consider:

  • Service Discovery Integration - Auto-register services from Consul or Kubernetes
  • Monitoring - Track configuration changes and performance metrics
  • GitOps - Version control your HAProxy configuration changes

The Data Plane API transforms HAProxy from a static configuration file into a dynamic, programmable load balancer that integrates seamlessly with modern infrastructure automation.

Read more