Danielside

informática softwarelibre divagaciones música

Mi cloud con Haproxy y LXC

Me ha costado un poco pero finalmente he conseguido reestructurar mi vetusto servidor de DigitalOcean con Debian 7, donde tengo varias aplicaciones personales (nextcloud, gnusocial, prosody, etc.) a uno con Debian 10 en el que cada aplicación reside en un contenedor LXC 3 sin privilegios usando Alpine 3.10 (el mismo que usa Docker por defecto)

Y diréis ¿por qué no Docker, kubernetes y esas cosas tan molonas que todo el mundo debe usar ahora? Pues, en primer lugar, creo que una arquitectura basada en docker, k8s es difícil de configurar bien y además se excede de las pretensiones de un servidor personal. Lo que yo perseguía con esta migración es jugar con haproxy y con los contenedores LXC sin privilegios (que son aquéllos que se ejecutan como un usuario no root) además de Ansible para configurar tanto el host como las aplicaciones. Además, algo hay en Docker que no me gusta demasiado. He usado Docker + k8s en proyectos reales y me resulta incómodo. Está muy bien eso de separar por completo los procesos. Por ejemplo en nuestra arquitectura teníamos un nginx, un php-fpm con el drupal, un volumen con mysql y otro contenedor con memcached. Pero, por un lado, el hecho de no poder conectarte fácilmente «al servidor» para ver qué está pasando me molesta. Por otro lado, hay que hacer una inversión no trivial para saber como montarte bien el entorno de desarrollo, no digamos ya el de producción. Y finalmente, docker necesita un proceso «daemon» siempre ejecutándose, cuando la limpieza y belleza de los contenedores basados en namespaces es que son exactamente los mismos procesos de la máquina (y ninguno más) ejecutándose en un espacio aparte.

Seguramente sea cuestión de opiniones y esta «crítica» no esté muy bien fundamentada, el caso es que LXC me parecía perfecto para esta misión. Si se usan imágenes de Alpine, los contenedores tardan menos de 2 segundos en arrancar y ocupan muy poco espacio. Si además usamos Ansible para configurarlos y realizar los despliegues de aplicaciones dentro de ellos, podemos tener cubierto un enorme porcentaje de las típicas cosas que realizamos con Docker. Además ¿para qué quiero aprender otro DSL (Domain Specific Language) para ejecutar comandos dentro de un contenedor, tal como el de los Dockerfile (por cierto bastante feo y pobre) cuando tengo la potencia de un lenguaje declarativo como Ansible? Y por último para terminar de despotricar, luego todo lo que hagas en los Dockerfiles solo te sirve para desarrollo en muchos casos, porque si quieres desplegar en producción con kubernetes te hacen falta otros ficheros aparte de configuración y despliegue, pero que hacen exactamente lo mismo… doble trabajo.

Resultado de ejecución de lxc-ls -f

Dentro de cada contenedor tengo un sistema operativo Alpine completo, con todas las herramientas de depuración que pueda necesitar. Un contenedor Alpine 3.10 recién creado rondará los 100MiB de espacio en disco.

Vistos desde el host los contenedores no son más que procesos normales, pero se ejecutan en otros espacios de nombres, invisibles desde el espacio de nombres normal o desde otros espacios de otros contenedores. Además, al usar contenedores sin privilegios (se ejecutan como usuario normal), en el hipotético caso de que un atacante consiguiera escapar del contenedor, al menos no sería root en el host (como sí pasaría con los contenedores con privilegios)

Captura de algunos grupos de procesos que corresponden a contenedores. El que tiene systemd corresponde a un contenedor Debian Jessie y el resto son Alpine

Por necesidades de compatibilidad hacia atrás, necesitaba tener un Debian Jessie en mi nuevo servidor (Debian Buster). Necesitaba poner el cliente de consola de Telegram para que funcionara el puente que tengo creado Telegram <> XMPP. Lo bueno de usar contenedores es que puedo tener todos los sistemas operativos que necesite, en la versión que necesite, conviviendo entre ellos. Cada uno recibe su IP de una red privada interna como veíamos en la primera captura y la comunicación entre ellos y desde ellos hacia el host y viceversa es simplemente TCP/IP, nada más sencillo (o UDP/IP o cualquier otro protocolo que se nos pueda ocurrir sobre IP).

Si entramos en el contenedor MySQL con lxc-attach mysql y hacemos un pstree vemos que obtenemos exactamente los mismos procesos que se veían en la captura anterior desde el host

Cada contenedor de aplicación Alpine lleva, si se trata de PHP como la mayoría de los casos, un servicio PHP7 FPM y un servidor nginx sirviendo en el puerto 80. Eso no sería visible desde el mundo exterior y por tanto la aplicación no se podría usar. Es entonces cuando entra Haproxy en juego. Me ha costado un poco dejarlo fino pero cuando se sabe lo suficiente da mucho juego. Haproxy se utiliza como balanceador de carga, pero ahora mismo lo estoy usando simplemente para redirigir el tráfico https del host hacia cada contenedor.

De manera que si tengo la aplicación nexctloud.dominio.tld apuntando en DNS (necesito registro A o CNAME) hacia la IP que tiene el servidor VPS, estos serían los fragmentos interesantes de haproxy:

frontend https_frontend
bind IP:443 ssl crt /etc/letsencrypt/haproxy/
http-request set-header X-Forwarded-Proto https if { ssl_fc }
reqadd X-Forwarded-Proto:\ https

Se define un frontal que atenderá todo el tráfico por el puerto 443 y leerá los certificados SSL de cada aplicación (más sobre esto después) desde un directorio determinado. Ahora, para poder diferenciar cada aplicación que se está solicitando y dirigirla a su contenedor necesitamos ACLs (Access Control Lists). Esta entrada explica maravillosamente bien las ACLs de haproxy.

acl nextcloud_req hdr(host) -i nextcloud.dominio.tld

Haproxy «captura» en esa lista las peticiones HTTP por el puerto 443 que vayan con una cabecera Host: nextcloud.dominio.tld y ahora tenemos que hacer cosas con esa petición:

use_backend nextcloud if nextclod_req

Con esta línea instruimos a Haproxy para que use el backend nextcloud si la petición es para nextcloud, finalmente definimos el backend, que va a ser nuestro contenedor de Nextcloud con su IP correspondiente, IP del rango privado que decíamos antes:

backend nextcloud
balance leastconn
http-request set-header X-Client-IP %[src]
server nc1 10.0.3.122:80 check

En un posible escenario de escalado automático (como los que se persiguen con Kubernetes) la estrategia sería a grandes rasgos como sigue:

  • Un proceso detectaría que hay un aumento del número de peticiones sobre una petición o bien que esa aplicación no está respondiendo adecuadamente en tiempo
  • Se lanzaría un proceso que, con Ansible, creara un contenedor idéntico al existente, con el mismo nginx, misma configuración, misma versión de la aplicación, etc.
  • Se añadiría al backend de haproxy como servidor adicional, y ya está listo para dar servicio, tenemos la aplicación balanceada en dos contenedores.
  • La red privada 10.0.3 se puede extender con túneles entre distintos servidores físicos, con lo cual podríamos tener contenedores de la misma aplicación en otro servidor. Llevando el ejemplo a otro nivel, un proceso podría crear un servidor totalmente nuevo (por ejemplo en DigitalOcean usando su API), configurarlo y crear el contenedor, para desprovisionarlo más tarde cuando bajara la demanda.

En definitiva, sin añadir la complejidad de Docker + Kubernetes para grandes despliegues, he conseguido una arquitectura segura, donde cada aplicación reside en su propio espacio confinado, en la cual puede tener su propio sistema operativo (los contenedores solo comparten el kernel del host) con todas sus librerías y todas las herramientas a las que estamos acostumbrados (¡sí, puedo instalar emacs!). Si quiero actualizar el entorno de una aplicación determinada porque hay actualizaciones de seguridad para el sistema operativo, nada más fácil que hacer backup del contenedor por si algo va mal y actualizarlo por completo. Si algo falla solo tengo que recuperar el backup del contedor completo.

El punto de partida es muy simple, un balanceador redirigiendo el tráfico hacia unos contenedores que tienen una IP fija, pero se puede complicar todo lo que se necesite usando más servidores virtuales y extendiendo la red y algún sistema de métricas que indique cuando hay que añadir más copias de los contenedores al balanceo y cuando se pueden eliminar para ahorrar recursos. Una integración con Jenkins sería muy recomendable en este sentido.

Finalmente comentar algo también de los certificados con Let’s Encrypt. En mi anterior servidor tenía que parar Apache, ya que como parte de la verificación del dominio el script de certbot monta su propio servicio en el puerto 80. Pero ahora con Haproxy solo tengo que definir un frontal http en el puerto 80 que dirija el tráfico hacia el servicio de certbot (que previamente tengo que configurar para que use otro puerto). De manera que puedo programar las renovaciones de todos los certificados con cron sin interrupción del servicio, me basta con recargar la configuración de haproxy cada vez que renueve certificados (systemctl reload haproxy). Este es el fragmento:

frontend http_frontend
bind IP:80
acl letsencrypt-acl path_beg /.well-known/acme-challenge/
redirect scheme https code 301 if !{ path_beg /.well-known/acme-challenge/ } !{ ssl_fc }
use_backend letsencrypt-backend if letsencrypt-acl
backend letsencrypt-backend
server letsencrypt 127.0.0.1:8888

La renovación de certificados la configuro con certbot para que monte su servicio en el 8888 y haproxy se encarga de redirigir las peticiones a mi servidor desde el puerto 80 hacia Let’s Encrypt en el 8888.


Archivado en categoría(s) GNU/Linux, PHP, Software Libre, XMPP

Enlace permanente



Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.