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:
Arrancamos Consul para que nos genere él mismo el directorio de configuración:
service consul start
Realizamos la configuración base:
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:
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:
Procedemos con su compilación:
Podemos ver el binario:
-rwxr-xr-x 1 root wheel 36904680 Sep 7 19:23 build/dataplaneapi
Lo copiamos al sistema y asignamos los permisos correctos:
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.
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:
#!/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:
Arrancamos el servicio habilitando el log para debugear posibles problemas:
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:
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:
syslogd_flags: -c -ss
Lo reconfiguramos y reiniciamos el servicio:
service syslogd restart
Configuramos HAProxy:
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:
Comprobamos manualmente que la configuración sea correcta:
[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:
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:
{
"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:
O eliminar uno de los servidores Consul:
Si consultamos el fichero de configuración de DataPlaneAPI veremos que ha sufrido cambios:
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:
Arrancamos Nginx:
service nginx start
Arrancamos Consul para que nos genere él mismo el directorio de configuración:
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):
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:
Damos de alta el servidor como servidor web:
{
"service": {
"name": "web",
"port": 80
}
}
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:
{"_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:
{"_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:
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:
Arrancar HAProxy en modo debug desde el script de RC:
: ${haproxy_flags:="-d -V -f ${haproxy_config} -p ${pidfile}"}
Arrancar DataPlaneAPI en modo debug:
service dataplaneapi start
tail -f /var/log/dataplaneapi
Arrancar manualmente HAProxy:
Consultar información a través de la API de DataPlaneAPI:
{
"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
}
}
[
{
"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"
}
]