Fecha: 2020-02-18 Tiempo de lectura: 10 minutos Categoría: Sistemas Tags: docker / swarm / nginx / balanceador / healthcheck / https
Cuando trabajamos en un entorno de varias aplicaciones tipo web o API nos solemos encontrar con la necesidad casi absoluta de poner un balanceador o proxy reverso; a veces es para balancear, otras es para la terminación SSL, y otras es para forzar la redirección a HTTPS. Para todas ellas nos sirve nginx.
Si tenemos la suerte de poder trabajar en un cluster basado en docker swarm, podemos utilizar balanceadores que ya saben que ejecutan en docker y pueden reconfigurarse según sea necesario; de hecho, en este blog se ha intentado utilizar traefik en varias ocasiones, como por ejemplo esta, esta o esta otra.
Traefik es una gran herramienta una vez que ha sido configurada, pero su configuración es un poco difícil, con unas directivas cambiantes entre versiones, una documentación escasa y una cantidad de despliegues limitada. De hecho, sigo intentando hacer que Let’s Encrypt funcione correctamente en el swarm.
Al final, siempre me acabo decantando por soluciones más conocidas, siendo nginx mi favorita. Será por su configuración simple, la gran cantidad de recursos online con los que contamos, o simplemente por la gran familiaridad que le tengo; por eso decidí acabar usándolo en mis entorno swarm.
El truco es simple: solo hay que tener en cuenta que la configuración del balanceador puede cambiar, los certificados también, y que necesitamos tener plena seguridad de que, en caso de un despliegue, haya alguna de las replicas del servicio funcional. Y todo esto ya lo sabemos hacer:
Vamos a suponer que tenemos dos aplicaciones web en nuestro swarm, que nos van a servir para ver como configurar los virtualhosts, los certificados individuales y nos servirán para probar que todo funciona como es debido.
NOTA: En este punto se asume que el swarm cuenta con una red tipo overlay,
que llamaremos frontend
, y que sirve para ser compartida entre el balanceador y
los servicios (así tendrán conectividad de red y se podrán pasar las peticiones).
gerard@shangrila:~/swarmbalancer$ docker network create -d overlay frontend
iv7yaa90pb755ygbkl1h2yyh0
gerard@shangrila:~/swarmbalancer$
Ya hemos utilizado esta imagen antes, y no tiene complicación. Se trata de un servicio web que devuelve el nombre del servidor y sus direcciones IP (en docker las devuelve del contenedor que responda).
El fichero tipo compose es relativamente simple, e incluso nos permitimos el lujo de automatizar el despliegue en un script (así evitaremos cambiar el nombre del stack, que puede darnos problemas en el futuro).
gerard@shangrila:~/swarmbalancer/whoami$ cat whoami.yml
version: '3'
services:
whoami:
image: emilevauge/whoami
networks:
- frontend
networks:
frontend:
external: true
gerard@shangrila:~/swarmbalancer/whoami$
gerard@shangrila:~/swarmbalancer/whoami$ cat deploy.sh
#!/bin/bash
docker stack deploy -c whoami.yml whoami
gerard@shangrila:~/swarmbalancer/whoami$
gerard@shangrila:~/swarmbalancer/whoami$ ./deploy.sh
Creating service whoami_whoami
gerard@shangrila:~/swarmbalancer/whoami$
Otro servicio muy socorrido para hacer pruebas tipo HTTP es este; simplemente se limita a respondernos a cualquier petición con un texto especificado. Esto nos sirve para ver que las peticiones le llegan a través del balanceador.
Siguiendo la anterior metodología, declararemos el servicio en un fichero tipo compose, le daremos un script de deploy y pondremos el servicio en marcha.
gerard@shangrila:~/swarmbalancer/echo$ cat echo.yml
version: '3'
services:
echo:
image: hashicorp/http-echo
command: -text="hello world"
networks:
- frontend
networks:
frontend:
external: true
gerard@shangrila:~/swarmbalancer/echo$
gerard@shangrila:~/swarmbalancer/echo$ cat deploy.sh
#!/bin/bash
docker stack deploy -c echo.yml echo
gerard@shangrila:~/swarmbalancer/echo$
gerard@shangrila:~/swarmbalancer/echo$ ./deploy.sh
Creating service echo_echo
gerard@shangrila:~/swarmbalancer/echo$
ESTADO: En este momento tenemos una red overlay llamada frontend
, en la
que tenemos dos servicios ejecutando: el servicio whoami (puerto TCP 80) y el
servicio echo (puerto TCP 5678).
Vamos a exponer un servicio nginx en el swarm, pero también en la red
frontend
. De esta forma podremos lanzar las peticiones que correspondan desde
cualquier nodo del swarm, y a su vez, pasarlas al servicio adecuado. Por
supuesto, vamos a utilizar SSL para los mismos y vamos a forzar las peticiones
HTTP a ir por las equivalentes en HTTPS.
Vamos a utilizar un sistema similar a los anteriores: un fichero tipo compose para declarar el stack, y un script de deploy (en donde calcularemos las sumas MD5 de los ficheros auxiliares, como se explica aquí); sobre esta base solo nos quedará añadir la configuración del nginx y los certificados de los servicios que queramos proteger.
Los certificados SSL se generan aparte del propio swarm; podemos generar certificados autofirmados, pagar por unos certificados válidos, o utilizar una aproximación como la que explicamos en este artículo. Para acortar, voy a poner unos certificados autofirmados.
El servidor nginx nos permite trabajar con los certificados en 2 ficheros (clave y certificado), o especificar un único fichero que los contenga a ambos en las dos directivas relacionadas. Voy a optar por esta opción para reducir la cantidad de secretos en el fichero compose resultante.
gerard@shangrila:~/swarmbalancer/balancer$ grep ^ certs/*
certs/echo.local.pem:-----BEGIN RSA PRIVATE KEY-----
...
certs/echo.local.pem:-----END RSA PRIVATE KEY-----
certs/echo.local.pem:-----BEGIN CERTIFICATE-----
...
certs/echo.local.pem:-----END CERTIFICATE-----
certs/whoami.local.pem:-----BEGIN RSA PRIVATE KEY-----
...
certs/whoami.local.pem:-----END RSA PRIVATE KEY-----
certs/whoami.local.pem:-----BEGIN CERTIFICATE-----
...
certs/whoami.local.pem:-----END CERTIFICATE-----
gerard@shangrila:~/swarmbalancer/balancer$
Para configurar el nginx vamos a definir 3 virtualhosts:
Esta configuración quedará así:
gerard@shangrila:~/swarmbalancer/balancer$ cat conf/balancer.conf
resolver 127.0.0.11 valid=5s;
map $ssl_server_name $docker_service {
whoami.local whoami_whoami:80;
echo.local echo_echo:5678;
}
server {
listen 8080;
stub_status;
}
server {
listen 80;
return 308 https://$host$request_uri;
}
server {
listen 443 ssl;
ssl_certificate_key /run/secrets/$ssl_server_name.pem;
ssl_certificate /run/secrets/$ssl_server_name.pem;
location / { proxy_pass http://$docker_service; }
}
gerard@shangrila:~/swarmbalancer/balancer$
Hay que tener en cuenta algunos puntos para entender esta configuración:
/run/secrets/<dominio>.pem
, sino dará un error$docker_service
map
de $ssl_server_name
y $docker_service
ssl_server_name = whoami.local
→ $docker_service = whoami_whoami:80
ssl_server_name = echo.local
→ $docker_service = echo_echo:5678
resolver
sirve para indicar el DNS del contenedor, de donde obtendremos la VIP del servicioTRUCO: Todo contenedor en un swarm tiene un servidor DNS expuesto que sabe resolver los servicios que estén en sus mismas redes, así como de otros dominios; lo encontraremos en la IP 127.0.0.11 (esto no cambia nunca).
WARNING: Esta configuración es muy genérica, pero la petición fallará si llega
una petición que no sea para whoami.local
o echo.local
. Si váis a poner más
servicios, apuntad el registro DNS hacia el swarm cuando ya tengamos la configuración
activa y los certificados en su sitio.
Vamos a hacer un stack relativamente simple; es un nginx genérico con una configuración propia y unos certificados puestos como secretos. La única complicación es que vamos a utilizar este método (concretamente con sumas MD5) para poder modificar la configuración y los certificados y no recibir un error durante el subsiguiente redeploy.
Las otras dos curiosidades son el healthcheck y los parámetros de deploy; gracias al healthcheck podemos dar a conocer a docker si el contenedor está respondiendo (no vale con levantado solamente), y gracias al deploy tendremos 4 contenedores funcionando y los reemplazaremos de 1 en 1 (momento en el que podremos tener 5 en marcha, hasta que este dé healthcheck correcto y se pueda reemplazar uno de los antiguos).
gerard@shangrila:~/swarmbalancer/balancer$ cat balancer.yml
version: '3.5'
services:
nginx:
image: sirrtea/nginx:alpine
configs:
- source: balancer.conf
target: /etc/nginx/conf.d/balancer.conf
secrets:
- source: whoami.local.pem
- source: echo.local.pem
networks:
- frontend
deploy:
replicas: 4
update_config:
parallelism: 1
order: start-first
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://localhost:8080/"]
interval: 5s
timeout: 3s
retries: 3
start_period: 10s
ports:
- "80:80"
- "443:443"
configs:
balancer.conf:
name: balancer_balancer.conf-${BALANCER_CONF_DIGEST}
file: conf/balancer.conf
secrets:
whoami.local.pem:
name: balancer_whoami.local.pem-${WHOAMI_LOCAL_PEM_DIGEST}
file: certs/whoami.local.pem
echo.local.pem:
name: balancer_echo.local.pem-${ECHO_LOCAL_PEM_DIGEST}
file: certs/echo.local.pem
networks:
frontend:
external: true
gerard@shangrila:~/swarmbalancer/balancer$
gerard@shangrila:~/swarmbalancer/balancer$ cat deploy.sh
#!/bin/bash
function md5 { md5sum ${1} | cut -b 1-32; }
export BALANCER_CONF_DIGEST=$(md5 conf/balancer.conf)
export WHOAMI_LOCAL_PEM_DIGEST=$(md5 certs/whoami.local.pem)
export ECHO_LOCAL_PEM_DIGEST=$(md5 certs/echo.local.pem)
docker stack deploy -c balancer.yml balancer
gerard@shangrila:~/swarmbalancer/balancer$
Levantamos el stack de balanceador y esperamos a que sus servicios estén funcionando. Futuros deploys deberían hacerse sin downtime, reemplazándose los contenedores 1 a 1 según las políticas declaradas. Esto nos permite cambiar las configuraciones y los certificados sin parada, de forma gradual.
gerard@shangrila:~/swarmbalancer/balancer$ ./deploy.sh
Creating secret balancer_echo.local.pem-7fefb7759833a6a0fedd1208b724a065
Creating secret balancer_whoami.local.pem-3f3abec4ba2f29adc60c691f858c8f7f
Creating config balancer_balancer.conf-38919a938105c010a25ca26b7bfc823e
Creating service balancer_nginx
gerard@shangrila:~/swarmbalancer/balancer$
TRUCO: La VIP del servicio solo va a balancear las peticiones al nginx entre los contenedores que pasen el healthcheck, así que deberíamos tener respuesta desde el primer contenedor levantado, y nunca desde un nginx que se esté levantando (su healthcheck fallará hasta que esté listo para recibir peticiones).
Lo primero es ver que nuestros nginx están ejecutando y en estado healthy;
podemos verificarlo con un docker ps
o revisar que hay las replicas necesarias
en el servicio (en este caso 4/4).
gerard@shangrila:~/swarmbalancer$ docker service ls
ID NAME MODE REPLICAS IMAGE PORTS
wq4cr5ya4air balancer_nginx replicated 4/4 sirrtea/nginx:alpine *:80->80/tcp, *:443->443/tcp
u6cc79nd8nc8 echo_echo replicated 1/1 hashicorp/http-echo:latest
0dz5ad69e55h whoami_whoami replicated 1/1 emilevauge/whoami:latest
gerard@shangrila:~/swarmbalancer$
Solo nos queda ver que el comportamiento de la configuración del nginx:
Todas ellas son verificables con simples peticiones usando curl
. Voy a poner
la salida de los dos servicios configurados para probar, y un dominio no configurado
para poder ver el error que nos daría un posible despiste.
gerard@shangrila:~/swarmbalancer$ curl -i http://whoami.local/
HTTP/1.1 308 Permanent Redirect
...
Location: https://whoami.local/
...
gerard@shangrila:~/swarmbalancer$
gerard@shangrila:~/swarmbalancer$ curl -i http://echo.local/
HTTP/1.1 308 Permanent Redirect
...
Location: https://echo.local/
...
gerard@shangrila:~/swarmbalancer$
gerard@shangrila:~/swarmbalancer$ curl -i http://server.local/
HTTP/1.1 308 Permanent Redirect
...
Location: https://server.local/
...
gerard@shangrila:~/swarmbalancer$
NOTA: Añado el flag -k
porque el certificado es autofirmado y falla verificación.
gerard@shangrila:~/swarmbalancer$ curl -k https://whoami.local/
Hostname: a1370d06574a
IP: 127.0.0.1
IP: 10.0.0.3
IP: 172.17.0.3
GET / HTTP/1.1
Host: whoami_whoami
User-Agent: curl/7.52.1
Accept: */*
Connection: close
gerard@shangrila:~/swarmbalancer$
gerard@shangrila:~/swarmbalancer$ curl -k https://echo.local/
hello world
gerard@shangrila:~/swarmbalancer$
gerard@shangrila:~/swarmbalancer$ curl -k https://server.local/
curl: (35) error:14077438:SSL routines:SSL23_GET_SERVER_HELLO:tlsv1 alert internal error
gerard@shangrila:~/swarmbalancer$
Este último caso era esperable, porque el servicio no está configurado; de hecho,
no llega siquiera a intentar pasar la petición a nadie, porque el error salta antes,
concretamente cuando intenta obtener el certificado SSL, que debería estar en el
fichero /run/secrets/server.local.pem
(y no está):
gerard@shangrila:~/swarmbalancer$ docker service logs balancer_nginx
...
balancer_nginx.2.ezfu64cjr1dw@shangrila | 2020/01/14 11:39:37 [error] 6#6: *130 cannot load certificate "/run/secrets/server.local.pem": BIO_new_file() failed (SSL: error:02001002:system library:fopen:No such file or directory:fopen('/run/secrets/server.local.pem','r') error:2006D080:BIO routines:BIO_new_file:no such file) while SSL handshaking, client: 10.255.0.2, server: 0.0.0.0:443
...
gerard@shangrila:~/swarmbalancer$
Solo nos quedaría que las peticiones llegaran desde fuera del swarm a cualquier nodo del mismo; este sabría enrutar la peticiones a alguno de los 4 contenedores nginx. De hecho, podemos asegurar alta disponibilidad del servicio si balanceamos las peticiones entre los nodos saludables del swarm, o compartiendo una VIP con keepalived entre algunos de los nodos del swarm.