Deploy HAProxy as Load Balancer for Docker Swarm Mode

By | March 14, 2021

Overview

  • In lab, we have 3 nodes – 1 for manager and 2 for worker node(s).
  • You can follow the Docker Swarm Mode setup from here if not yet.
  • In case, we will deploy haproxy service on worker-01 and worker-02 node.

Prerequisites

1. Rebuild HAProxy image

  • HAProxy official image has no required directory “/var/lib/haproxy” so we cannot start HAProxy container yet.
  • In case we do not have our own registry, so we need to rebuild haproxy image for each node(s) of Docker Swarm Cluster.
  • Connect via SSH to each worker node(s) and rebuild haproxy image as the following command:
cat <<'EOF' | sudo tee Dockerfile
FROM haproxy:alpine
RUN mkdir /var/lib/haproxy
EOF

docker build -t my_haproxy .
  • If we do not have public certificate, connect to manager node and we should start using with self-signed as the following:
mkdir -p /srv/haproxy-load-balancer/certs
cd /srv/haproxy-load-balancer/certs/

# Create DHPARAM file
openssl dhparam -out dhparam.pem 2048

# My domain name
mydomain=lab.local

# Generate a unique private key (KEY)
sudo openssl genrsa -out $mydomain.key 2048

# Generating a Certificate Signing Request (CSR)
sudo openssl req -new -key $mydomain.key -out $mydomain.csr

# Creating a Self-Signed Certificate (CRT)
openssl x509 -req -days 365 -in $mydomain.csr -signkey $mydomain.key -out $mydomain.crt

# Append KEY and CRT to mydomain.pem
cat $mydomain.key $mydomain.crt >> $mydomain.pem

chmod 400 *.{key,pem,csr,crt}
chmod 600 ../certs
  • Create docker secret to store those self-signed certificates (.key, .pem)
docker secret create dhparam-pem /srv/haproxy-load-balancer/certs/dhparam.pem
docker secret create haproxy-certificate-pem /srv/haproxy-load-balancer/certs/lab.local.pem
docker secret ls

Deploy HAProxy

  • Create HAProxy configuration file named “haproxy.cfg”
cd /srv/haproxy-load-balancer/
cat <<'EOF' | sudo tee haproxy.cfg
global
    log         fd@2 local2
    chroot      /var/lib/haproxy
    pidfile     /var/run/haproxy.pid
    maxconn     40000
    user        haproxy
    group       haproxy
    stats socket /var/lib/haproxy/stats expose-fd listeners
    master-worker
    nbproc       1
    nbthread     4
    cpu-map      auto:1/1-4 0-3

    ssl-default-bind-ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384
    ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256
    ssl-default-bind-options prefer-client-ciphers no-sslv3 no-tlsv10 no-tlsv11 no-tls-tickets

    ssl-default-server-ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384
    ssl-default-server-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256
    ssl-default-server-options no-sslv3 no-tlsv10 no-tlsv11 no-tls-tickets

    ssl-dh-param-file /etc/haproxy/dhparam.pem

resolvers docker
    nameserver dns1 127.0.0.11:53
    resolve_retries 3
    timeout resolve 1s
    timeout retry   1s
    hold other      10s
    hold refused    10s
    hold nx         10s
    hold timeout    10s
    hold valid      10s
    hold obsolete   10s

defaults
    timeout connect 10s
    log global
    mode http
    option httplog
    option http-server-close
    option forwardfor
    option dontlognull
    option redispatch
    option contstats

frontend default
    mode http
    bind :80
    redirect scheme https code 301 if !{ ssl_fc }

    bind :443 ssl crt /etc/haproxy/haproxy-certificate.pem alpn h2,http/1.1
    http-response set-header Strict-Transport-Security max-age=63072000

    acl url_is_stats req.hdr(Host) -i stats.lab.local

    use_backend haproxy if url_is_stats

backend haproxy
    stats enable
    stats uri /
    stats refresh 15s
    stats show-legends
    stats show-node
    stats auth admin:changeme
    stats admin if TRUE
EOF
  • Create HAProxy deployment YAML file named “docker-compose-haproxy.yml”
cd /srv/haproxy-load-balancer/
cat <<'EOF' | sudo tee docker-compose-haproxy.yml
version: '3.7'

networks:
  backend-net:
    external: true

configs:
  haproxy_cfg:
    file: ./haproxy.cfg

secrets:
  dhparam-pem:
    external: true
  haproxy-certificate-pem:
    external: true

services:
  proxy:
    image: my_haproxy
    networks:
      - backend-net
    ports:
      - target: 80
        published: 80
        protocol: tcp
        mode: host
      - target: 443
        published: 443
        protocol: tcp
        mode: host
    configs:
      - source: haproxy_cfg
        target: /usr/local/etc/haproxy/haproxy.cfg
        mode: 0440
    secrets:
      - source: dhparam-pem
        target: /etc/haproxy/dhparam.pem
        mode: 0400
      - source: haproxy-certificate-pem
        target: /etc/haproxy/haproxy-certificate.pem
        mode: 0400
    dns:
      - 127.0.0.11
    deploy:
      mode: replicated
      replicas: ${HAPROXY_REPLICA:-2}
      placement:
        constraints:
          - node.role == worker

EOF
  • Create overlay network backend application
docker network create --attachable --driver overlay backend-net
  • Start deploy with stack named “haproxy”
cd /srv/haproxy-load-balancer/
docker stack deploy -c docker-compose-haproxy.yml haproxy
  • To verify the deployment status
docker stack ls
docker service ls
docker service logs -ft haproxy_proxy
  • You should be able to access HAProxy stats by url: https://stats.lab.local that stats.lab.local point to worker-01 & worker-02 IP address

Demonstration with Whoami

  • Create deployment YMAL file named “docker-compose-whoami.yml”
mkdir /srv/whoami
cd /srv/whoami/
cat <<'EOF' | sudo tee docker-compose-whoami.yml
version: '3.7'

networks:
  backend-net:
    external: true

services:
  app:
    image: containous/whoami:latest
    networks:
      - backend-net
    deploy:
      mode: replicated
      replicas: 4
      endpoint_mode: dnsrr
      placement:
        constraints:
          - node.role == worker
EOF
  • Start deploying with stack named “whoami”
docker stack deploy -c docker-compose-whoami.yml whoami
  • Update “haproxy.cfg” configuration for Whoami
cd /srv/haproxy-load-balancer/
vim haproxy.cfg

--> Add new ACL, USE_BACKEND and BACKEND name
frontend default
-----
   acl url_is_whoami req.hdr(Host) -i whoammi.lab.local
   use_backend whoami if url_is_whoami
-----

backend whoami
   balance roundrobin
   server-template whoami_app- 4 whoami_app:80 check resolvers docker init-addr libc,none
  • After save and exit, then need to redeploy HAProxy again as the following:
cd /srv/haproxy-load-balancer/
docker stack rm haproxy
docker stack deploy -c docker-compose-haproxy.yml haproxy
  • Now you should be able to access whoami application by URL: https://whoami.lab.local which this name will resolve into worker-01 & worker-02 IP address. Here is the result
  • Here is the HAProxy stats look like:

Leave a Reply

Your email address will not be published. Required fields are marked *