Esta pagina se ve mejor con JavaScript habilitado

Configuración dinámica HAProxy mediante Consul

 ·  🎃 kr0m

Consul es un software que nos ofrece funcionalidades muy interesantes sobretodo en entornos dinámicos con muchos cambios, dichas funcionalidades son:

  • Registro de servicios: Grupos de servidores agrupados por el servicio ofrecido.
  • Base de datos K/V: Centralización de parámetros de nuestras aplicaciones.
  • Control de acceso: Definición de los accesos a los servicios registrados.

En este artículo tan solo vamos a utilizar el registro de servicios para reconfigurar de forma dinámica un HAProxy y así añadir o eliminar servidores de los backends según su estado. La reescritura de la configuración del HAProxy será posible gracias a una herramienta llamada DataPlaneAPI .

El artículo se compone de varias partes:


INTRODUCCIóN:

El escenario se compone de un servidor Consul, un HAProxy y algunos servidores Nginx , todos ellos jails creadas mediante IOCage .

CONSUL: NYNEX-King
HAPROXY: BlindSnipper
NGINX: PeanutBrain00/01/02/03/04/05/06/07/09/10/11
+-----+---------------+-------+--------------+---------------+
| JID |     NAME      | STATE |   RELEASE    |      IP4      |
+=====+===============+=======+==============+===============+
| 39  | BlindSnipper  | up    | 13.1-RELEASE | 192.168.69.51 |
+-----+---------------+-------+--------------+---------------+
| 36  | NYNEX-King    | up    | 13.1-RELEASE | 192.168.69.50 |
+-----+---------------+-------+--------------+---------------+
| 42  | PeanutBrain00 | up    | 13.1-RELEASE | 192.168.69.52 |
+-----+---------------+-------+--------------+---------------+
| 45  | PeanutBrain01 | up    | 13.1-RELEASE | 192.168.69.53 |
+-----+---------------+-------+--------------+---------------+
| 50  | PeanutBrain02 | up    | 13.1-RELEASE | 192.168.69.54 |
+-----+---------------+-------+--------------+---------------+
| 53  | PeanutBrain03 | up    | 13.1-RELEASE | 192.168.69.55 |
+-----+---------------+-------+--------------+---------------+
| 56  | PeanutBrain04 | up    | 13.1-RELEASE | 192.168.69.56 |
+-----+---------------+-------+--------------+---------------+
| 59  | PeanutBrain05 | up    | 13.1-RELEASE | 192.168.69.57 |
+-----+---------------+-------+--------------+---------------+
| 63  | PeanutBrain06 | up    | 13.1-RELEASE | 192.168.69.58 |
+-----+---------------+-------+--------------+---------------+
| 66  | PeanutBrain07 | up    | 13.1-RELEASE | 192.168.69.59 |
+-----+---------------+-------+--------------+---------------+
| 69  | PeanutBrain08 | up    | 13.1-RELEASE | 192.168.69.60 |
+-----+---------------+-------+--------------+---------------+
| 72  | PeanutBrain09 | up    | 13.1-RELEASE | 192.168.69.61 |
+-----+---------------+-------+--------------+---------------+
| 75  | PeanutBrain10 | up    | 13.1-RELEASE | 192.168.69.62 |
+-----+---------------+-------+--------------+---------------+
| 78  | PeanutBrain11 | up    | 13.1-RELEASE | 192.168.69.63 |
+-----+---------------+-------+--------------+---------------+

CONSUL SERVER:

Instalamos Consul:

pkg install consul

Arrancamos Consul para que nos genere él mismo el directorio de configuración:

sysrc consul_enable=YES
service consul start

Realizamos la configuración base:

vi /usr/local/etc/consul.d/consul.hcl

datacenter = "AlfaExploitDC01"
server = true
data_dir = "/var/db/consul"
bind_addr = "192.168.69.50"
client_addr = "192.168.69.50"
bootstrap = true
bootstrap_expect = 1

ui_config {
    enabled = true
}

enable_syslog = true
log_level = "INFO"

Reiniciamos Consul:

service consul restart

Accedemos a la interfaz web, tan solo veremos un servicio registrado, el del propio Consul:
http://192.168.69.50:8500


DATAPLANEAPI:

Para poder reconfigurar HAProxy de forma dinámica utilizaremos Data Plane API este permite dos backends de descubrimiento de servicios , AWS y Consul.

Para poder utilizar Data Plane API es necesaria una versión de HAProxy >= 1.9.0 lo que no supone ningún problema en instalaciones nuevas ya que FreeBSD instala 2.6.1

Instalamos el software necesario para la compilación de DataPlaneAPI ya que no se distribuye de forma binaria, también instalamos jq para poder visualizar la salida json de forma mas cómoda:

pkg install git go jq

Procedemos con su compilación:

git clone https://github.com/haproxytech/dataplaneapi.git
cd dataplaneapi
make build

Podemos ver el binario:

ls -la build/dataplaneapi

-rwxr-xr-x  1 root  wheel  36904680 Sep  7 19:23 build/dataplaneapi

Lo copiamos al sistema y asignamos los permisos correctos:

cp build/dataplaneapi /usr/local/bin/
chmod +x /usr/local/bin/dataplaneapi

En la configuración de Data Plane API definimos la IP/puerto de bindeo además de algunos parámetros como el usuario/password de acceso a la API de Data Plane API.

vi /usr/local/etc/dataplaneapi.yaml

config_version: 2
name: haproxy0
dataplaneapi:
  host: 192.168.69.51
  port: 5555
  user:
  - name: kr0m
    password: PASSWORD
    insecure: true

En la mayoría de configuraciones veremos que el HAProxy ha sido configurado con la opción program , esto se hace para que sea el propio HAProxy el que gestione DataPlaneAPI, pero esto en realidad no es necesario ya que cada proceso puede funcionar de forma independiente, el HAProxy sirviendo peticiones y el DataPlaeAPI reescribiendo la configuración. Además en FreeBSD es problemático ya que el script de RC de HAProxy se trastorna perdiendo el rastro del PID del HAProxy dando error al parar/reiniciar el servicio, así que gestionaremos cada servicio de forma independiente.

Generamos el script RC de DataPlaneAPI:

vi /usr/local/etc/rc.d/dataplaneapi

#!/bin/sh
#
# $FreeBSD$
#

# PROVIDE: dataplaneapi

. /etc/rc.subr

name="dataplaneapi"
command="/usr/local/bin/dataplaneapi"
desc="Data Plane API: HAProxy reconfigurator"
rcvar="dataplaneapi_enable"

start_cmd="dataplaneapi_start"
stop_cmd="dataplaneapi_stop"
status_cmd="dataplaneapi_status"

extra_commands="status"

: ${dataplaneapi_config:="/usr/local/etc/dataplaneapi.yaml"}
: ${dataplaneapi_haproxyconfig:="/usr/local/etc/haproxy.conf"}
: ${dataplaneapi_haproxybin:="/usr/local/sbin/haproxy"}
: ${dataplaneapi_haproxyreloadcmd:="/usr/sbin/service haproxy reload"}
: ${dataplaneapi_haproxyrestartcmd:="/usr/sbin/service haproxy restart"}
: ${dataplaneapi_logfile:="/var/log/dataplaneapi.log"}
: ${dataplaneapi_loglevel:="warning"}

dataplaneapi_start()
{
	if [ -f /var/run/${name}.pid ]; then
		echo "${name} running with PID: $(cat /var/run/${name}.pid)"
		echo "Stop it before starting it"
		exit
	else
		echo "Starting service: ${name}"
		/usr/sbin/daemon -S -p /var/run/${name}.pid -u root ${command} -f ${dataplaneapi_config} --config-file ${dataplaneapi_haproxyconfig} --haproxy-bin ${dataplaneapi_haproxybin} --reload-cmd "${dataplaneapi_haproxyreloadcmd}" --restart-cmd "${dataplaneapi_haproxyrestartcmd}" --log-to file --log-file ${dataplaneapi_logfile} --log-level ${dataplaneapi_loglevel} --show-system-info
	fi
}

dataplaneapi_stop()
{
	if [ -f /var/run/${name}.pid ]; then
        	echo "Stopping service: ${name}"
	        kill -s INT $(cat /var/run/${name}.pid)
	        sleep 3
	else
        	echo "It appears ${name} is not running."
	fi
}

dataplaneapi_status()
{
	if [ -f /var/run/${name}.pid ]; then
        	echo "${name} running with PID: $(cat /var/run/${name}.pid)"
	else
        	echo "It appears ${name} is not running."
	fi
}

load_rc_config $name
run_rc_command $*

Le asignamos los permisos necesarios:

chmod 755 /usr/local/etc/rc.d/dataplaneapi

Arrancamos el servicio habilitando el log para debugear posibles problemas:

sysrc dataplaneapi_enable=YES
sysrc dataplaneapi_loglevel=debug
service dataplaneapi start

Data Plane API tiene su interfaz, accedemos a ella para comprobar que funcione correctamente:
http://192.168.69.51:5555/v2/docs

kr0m/PASSWORD

HAPROXY:

Instalamos el software necesario:

pkg install haproxy

Nos aseguramos de que el servicio syslod esté bindeado por red, consultamos las flags de configuración, si aparece el parámetro -ss debemos reconfigurarlo:

sysrc syslogd_flags

syslogd_flags: -c -ss

Lo reconfiguramos y reiniciamos el servicio:

sysrc syslogd_flags="-c"
service syslogd restart

Configuramos HAProxy:

vi /usr/local/etc/haproxy.conf

global
    daemon
    maxconn 5000
    log 192.168.69.51:514 local0
    user nobody
    group nobody
    stats socket /var/run/haproxy.sock user nobody group nobody mode 660 level admin

    ssl-default-bind-ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256
    ssl-default-bind-options ssl-min-ver TLSv1.2 no-tls-tickets
    
defaults
    timeout connect 10s
    timeout client 30s
    timeout server 30s
    log global
    mode http
    option httplog

listen stats
    bind *:8404
    stats enable
    stats uri /stats
    stats refresh 5s
    stats auth kr0m:PASSWORD

Asignamos los permisos correctos del fichero:

chmod 660 /usr/local/etc/haproxy.conf

Comprobamos manualmente que la configuración sea correcta:

haproxy -c -f /usr/local/etc/haproxy.conf

[NOTICE]   (83382) : haproxy version is 2.6.1-f6ca66d
[NOTICE]   (83382) : path to executable is /usr/local/sbin/haproxy
[WARNING]  (83382) : config : ca-file: 0 CA were loaded from '@system-ca'
Warnings were found.
Configuration file is valid

Arrancamos el servicio:

sysrc haproxy_enable=YES
service haproxy start

Le indicamos a DataPlaneAPI que debe consultar el servicio llamado web de Consul para obtener los backend servers:
https://github.com/haproxytech/dataplaneapi/blob/master/discovery/CONSUL.md
http://192.168.69.51:5555/v2/docs#tag/ServiceDiscovery/operation/createConsul

curl -u kr0m:PASSWORD \
       -H 'Content-Type: application/json' \
       -d '{
             "address": "192.168.69.50",
             "port": 8500,
             "enabled": true,
             "retry_timeout": 10,
             "server_slots_growth_increment": 1,
             "server_slots_growth_type": "linear",
             "service_allowlist": ["web"]
           }' http://192.168.69.51:5555/v2/service_discovery/consul | jq
{
  "address": "192.168.69.50",
  "enabled": true,
  "id": "d36476d7-3bea-474e-a4b6-0f39c3878759",
  "port": 8500,
  "retry_timeout": 10,
  "server_slots_base": 10,
  "server_slots_growth_increment": 10,
  "server_slots_growth_type": "linear",
  "service-blacklist": null,
  "service-whitelist": null,
  "service_allowlist": [
    "web"
  ],
  "service_denylist": null
}

Consultamos los servidores Consul dados de alta:

curl -u kr0m:PASSWORD -H ‘Content-Type: application/json’ http://192.168.69.51:5555/v2/service_discovery/consul | jq

{
  "data": [
    {
      "address": "192.168.69.50",
      "enabled": true,
      "id": "d36476d7-3bea-474e-a4b6-0f39c3878759",
      "port": 8500,
      "retry_timeout": 10,
      "server_slots_base": 10,
      "server_slots_growth_increment": 10,
      "server_slots_growth_type": "linear",
      "service-blacklist": null,
      "service-whitelist": null,
      "service_allowlist": [
        "web"
      ],
      "service_denylist": null
    }
  ]
}

También podemos consultar los datos de uno de los servidores Consul:

curl -u kr0m:PASSWORD -H ‘Content-Type: application/json’ http://192.168.69.51:5555/v2/service_discovery/consul/{id} | jq

O eliminar uno de los servidores Consul:

curl -u kr0m:PASSWORD -H ‘Content-Type: application/json’ -X DELETE http://192.168.69.51:5555/v2/service_discovery/consul/{id}

Si consultamos el fichero de configuración de DataPlaneAPI veremos que ha sufrido cambios:

cat /usr/local/etc/dataplaneapi.yaml

config_version: 2
name: haproxy0
mode: single
status: ""
dataplaneapi:
  host: 192.168.69.51
  port: 5555
  user:
  - name: kr0m
    insecure: true
    password: PASSWORD
  advertised:
    api_address: ""
    api_port: 0
service_discovery:
  consuls:
  - address: 192.168.69.50
    description: ""
    enabled: true
    id: d36476d7-3bea-474e-a4b6-0f39c3878759
    name: ""
    namespace: ""
    port: 8500
    retrytimeout: 10
    serverslotsbase: 10
    serverslotsgrowthincrement: 10
    serverslotsgrowthtype: linear
    serviceblacklist: []
    servicewhitelist: []
    serviceallowlist:
    - web
    servicedenylist: []
    token: ""

NGINX:

Instalamos Consul y Nginx en los servidores web:

pkg install consul nginx

Arrancamos Nginx:

sysrc nginx_enable=YES
service nginx start

Arrancamos Consul para que nos genere él mismo el directorio de configuración:

sysrc consul_enable=YES
service consul start

En la configuración de Consul definimos la ip a la que bindearse y la ip del consul-server(192.168.69.50):

vi /usr/local/etc/consul.d/consul.hcl

datacenter = "AlfaExploitDC01"
server = false
data_dir = "/var/db/consul"
bind_addr = "192.168.69.XX"
retry_join = ["192.168.69.50"]
enable_syslog = true
log_level = "INFO"

Reiniciamos el servicio:

service consul restart

Damos de alta el servidor como servidor web:

vi web.json

{
  "service": {
    "name": "web",
    "port": 80
  }
}
consul services register ./web.json
Node name "PeanutBrain00.alfaexploit.com" will not be discoverable via DNS due to invalid characters. Valid characters include all alpha-numerics and dashes.
Registered service: web

NOTA: Por ahora solo vamos a dar de alta del nodo 00-08, 9 en total.

Si accedemos de nuevo a la interfaz del servidor de Consul podremos ver las intancias dadas de alta en el servicio web:
http://192.168.69.50:8500/ui/alfaexploitdc01/services

Podemos consultar el estado de cada una de las instancias:

DataPlaneAPI habrá detectado los servidores dados de alta y veremos que HAProxy ha sido reconfigurado con un backend con los nodos web:
http://192.168.69.51:8404/stats

Podemos ver como el backend dispone de servidores en mantenimiento, esto es debido a un limitación de DataPlaneAPI, el mínimo de servidores en el backend siempre será 10, si tenemos menos de 10 instancias dadas de alta en Consul el restante hasta 10 serán servidores en mantenimiento, recordad que hemos registrado en Consul los nodos web 00-08, 9 en total.

Según la documentación de DataPlaneAPI permite indicar el parámetro server_slots_base que es el número de servidores con el que generar el backend:
http://192.168.69.51:5555/v2/docs#tag/ServiceDiscovery/operation/createConsul

Pero el software impone un mínimo de 10 servidores para evitar reinicios del servicio innecesarios, en este issue abierto al desarrollador podéis ver como muy amablemente responde a mi duda:
https://github.com/haproxytech/dataplaneapi/issues/265

Damos de alta un frontend con el backend generado por DataPlaneAPI:

consul-backend-<service-ip>-<service-port>-<service-name>
consul-backend-192.168.69.50-8500-web

Podemos ver la documentación en la URL:
http://192.168.69.51:5555/v2/docs#tag/Frontend/operation/createFrontend

Obtenemos la versión actual de la configuración:

curl -u kr0m:PASSWORD http://192.168.69.51:5555/v2/services/haproxy/configuration/frontends

{"_version":2,"data":[]}

Damos de alta el frontend:

curl -u kr0m:PASSWORD \
       -H 'Content-Type: application/json' \
       -d '{
            "default_backend": "consul-backend-192.168.69.50-8500-web",
            "mode": "http",
            "name": "consul-frontend"
            }' 'http://192.168.69.51:5555/v2/services/haproxy/configuration/frontends?version=2' | jq
{
  "default_backend": "consul-backend-192.168.69.50-8500-web",
  "mode": "http",
  "name": "consul-frontend"
}

Comprobamos que se haya resgistrado correctamente:

curl -u kr0m:PASSWORD http://192.168.69.51:5555/v2/services/haproxy/configuration/frontends

{"_version":3,"data":[{"default_backend":"consul-backend-192.168.69.50-8500-web","mode":"http","name":"consul-frontend"}]}

Bindeamos el frontend al puerto 80:

curl -u kr0m:PASSWORD \
       -H 'Content-Type: application/json' \
       -d '{
            "address": "192.168.69.51",
            "port": 80
       }' 'http://192.168.69.51:5555/v2/services/haproxy/configuration/binds?version=3&frontend=consul-frontend' | jq
{
  "address": "192.168.69.51",
  "port": 80
}

Comprobamos que efectivamente el HAProxy se ha bindeado al puerto 80 en la ip indicada:

sockstat -46 -l -s

USER     COMMAND    PID   FD PROTO  LOCAL ADDRESS         FOREIGN ADDRESS       PATH STATE   CONN STATE  
nobody   haproxy    29646 5  tcp4   192.168.69.51:80      *:*                                LISTEN

Ahora damos de alta en Consul un servidor web nuevo y podemos ver como se ha llegado a los 10 servidores del backend y se han añadido 10 mas:

Si fuesemos añadiendo servidores se irían ocupando los slots hasta los 10 próximos ampliando el pool en 10 mas, por otro lado si vamos desregistrándolos de Consul al llegar a 9 servidores el backend quedaría con 10 servidores uno de ellos en mantenimiento.


TROUBLESHOOTING:

Podemos consultar los logs de HAProxy:

tail -f /var/log/messages

Arrancar HAProxy en modo debug desde el script de RC:

cp /usr/local/etc/rc.d/haproxy /usr/local/etc/rc.d/haproxy.ori

vi /usr/local/etc/rc.d/haproxy
: ${haproxy_flags:="-d -V -f ${haproxy_config} -p ${pidfile}"}

Arrancar DataPlaneAPI en modo debug:

sysrc dataplaneapi_loglevel=debug
service dataplaneapi start
tail -f /var/log/dataplaneapi

Arrancar manualmente HAProxy:

haproxy -d -V -f /usr/local/etc/haproxy.conf

Consultar información a través de la API de DataPlaneAPI:

curl -u kr0m:PASSWORD -H ‘Content-Type: application/json’ http://192.168.69.51:5555/v2/info|jq

{
  "api": {
    "build_date": "0001-01-01T00:00:00.000Z",
    "version": " "
  },
  "system": {
    "cpu_info": {
      "num_cpus": 2
    },
    "hostname": "BlindSnipper.alfaexploit.com",
    "mem_info": {
      "dataplaneapi_memory": 53365520,
      "free_memory": 408793088,
      "total_memory": 8299794432
    },
    "os_string": "FreeBSD 13.1-RELEASE-p2 FreeBSD 13.1-RELEASE-p2 #19 releng/13.1-n250155-514a191356c-dirty: Thu Sep  1 13:01:03 CEST 2022     root@MightyMax.alfaexploit.com:/usr/obj/usr/src/amd64.amd64/sys/KR0M-MINIMAL ",
    "time": 1662710775,
    "uptime": 539432
  }
}
curl -u kr0m:PASSWORD -H ‘Content-Type: application/json’ http://192.168.69.51:5555/v2/services/haproxy/runtime/info|jq
[
  {
    "info": {
      "active_peers": 0,
      "busy_polling": 0,
      "bytes_out_rate": 0,
      "compress_bps_in": 0,
      "compress_bps_out": 0,
      "compress_bps_rate_lim": 0,
      "conn_rate": 0,
      "conn_rate_limit": 0,
      "connected_peers": 0,
      "cum_conns": 1997,
      "cum_req": 463,
      "cum_ssl_conns": 0,
      "curr_conns": 1,
      "curr_ssl_conns": 0,
      "dropped_logs": 0,
      "failed_resolutions": 0,
      "hard_max_conn": 5000,
      "idle_pct": 100,
      "jobs": 6,
      "listeners": 4,
      "max_conn": 5000,
      "max_conn_rate": 1,
      "max_pipes": 0,
      "max_sess_rate": 1,
      "max_sock": 10032,
      "max_ssl_conns": 0,
      "max_ssl_rate": 0,
      "max_zlib_mem_usage": 0,
      "mem_max_mb": 0,
      "nbthread": 2,
      "node": "BlindSnipper.alfaexploit.com",
      "pid": 31170,
      "pipes_free": 0,
      "pipes_used": 0,
      "pool_alloc_mb": 0,
      "pool_failed": 0,
      "pool_used_mb": 0,
      "process_num": 1,
      "processes": 1,
      "release_date": "2022-06-21",
      "run_queue": 0,
      "sess_rate": 0,
      "sess_rate_limit": 0,
      "ssl_backend_key_rate": 0,
      "ssl_backend_max_key_rate": 0,
      "ssl_cache_lookups": 0,
      "ssl_cache_misses": 0,
      "ssl_frontend_key_rate": 0,
      "ssl_frontend_max_key_rate": 0,
      "ssl_frontend_session_reuse": 0,
      "ssl_rate": 0,
      "ssl_rate_limit": 0,
      "stopping": 0,
      "tasks": 17,
      "total_bytes_out": 17103080,
      "ulimit_n": 10032,
      "unstoppable": 1,
      "uptime": 1957,
      "version": "2.6.1-f6ca66d",
      "zlib_mem_usage": 0
    },
    "runtimeAPI": "/var/run/haproxy.sock"
  }
]
Si te ha gustado el artículo puedes invitarme a un RedBull aquí