El bog de pepramon

Mis redes en Docker

Categorías: docker
Tags: docker , redes

En este post se explicarán las redes que se están utilizando en Docker, su configuración y las razones que me han llevado a esta configuración.

Introducción

Cuando se trabaja con Docker, y con Docker compose en particular, se crea una red por defecto para cada stack que se esté configurando. Esto puede provocar una gran cantidad de redes que realmente no se utilizan ni sirven para nada.

Al mismo tiempo, mi fontend principal es un proxy con Nginx. Esto tiene el problema que es necesario que el servicio que se quiera servir debe estar en la misma red de Nginx o accesible por este de alguna manera.

La manera usual en que todo el mundo hace esto es colocar todos los servicios en el mismo docker-compose.yml, pero esto implica que cualquier cambio de configuración en cualquier contenedor, requerirá el reinicio de TODO el stack.

El problema

Con servicios sencillos, colocarlos todos en el mismo docker-compose.yml es relativamente rápido rearrancar, pero ahora uno se podría imaginar que se tienen como 15 servicios y que a más, hay algunos que tardan en arrancar por las comprobaciones que tienen que hacer. Al mismo tiempo, como el proxy dependerá de TODOS los servicios, este no arrancará hasta que todos estén levantados.

Para mi, no tiene sentido que si toco la configuración del servicio A el servicio B tenga que parar y arrancar, igual que tampoco tendría sentido que tuviera que arrancar y parar el servidor que está haciendo de proxy si no se ha tocado la configuración del mismo.

También puede existir el problema que uno de los servicios no se levante correctamente, en ese caso, todos los servicios dejarían de estar operativos, ya que al no poder levantarse ese servicio, al depender Nginx del mismo, este no arrancaría, dejando todo no accesible.

Problema con Nginx

Al mismo tiempo, Nginx tiene otro problema a la hora de hacer de servidor proxy. Comprueba el host del que hace proxy en el arranque. Esto que podría parecer trivial, tiene el problema de que es necesario que todos los servicios estén escuchando cuando arranque Nginx.

Como se puede ver, la diferencia entre colocar todos los servicios en el mismo docker-compose.yml y el expuesto, es poca, ya que para poder arranar Nginx como servidor proxy es necesario que todos los servicios estén escuchando cuando este arranque. Por tanto, no deja de ser el mismo problema, con la dificultad de que habría que levantarlos manualmente.

No obstante, el propio Docker ofrece una solución para este problema.

Generación de redes para contenedores únicos

También existe el problema que para cada stack que se levante, Docker automáticamente genera una red que en la realidad no es necesaria.

Por ejemplo, al utilizar whatchtower se creará una red /16 para ese contenedor. Eso son 65536 direcciones IP, de las cuales solo 2 estarán ocupadas, la del propio whatchtower y la del gateway.

Al mismo tiempo, en el sistema operativo anfitrión se creará un dispositivo bridge, una interfaz para representar al host y otra interfaz para el contenedor, un total de 3 dispositivos, cosa que se cree muy ineficiente.

La solución

Como se ha indicado, existen dos problemas principales, el primer es como hacer proxy de la manera más flexible, y al mismo tiempo, dar acceso o juntar contenedores únicos en una sola red para evitar multitud de redes y dispositivos en el host.

La red de proxy

Docker tiene un par de características que en este caso serán de vital importancia:

  • Si se crea una red manualmente, esta se mantiene entre reincios
  • Se puede poner a escuchar a un contenedor a una IP en concreto

Sabiendo esto, se puede crear una red con el siguiente comando:

docker network create \
      --driver bridge \
      --subnet 10.81.1.0/29 \
      --gateway 10.81.1.1 \
      proxy

donde:

  • docker network create –> Comando de docker para crear una red
  • --driver bridge –> Red a crear tipo bridge
  • --subnet 10.81.1.0/29 –> Rango de la red
  • --gateway 10.81.1.1 –> Dirección IP de gateway de la red
  • proxy –> Nombre de la red

Esta red dispondrá de un total de 8 direcciones IP asignables, de las que habrá que descontar la 10.81.1.0 que es la dirección de red, la 10.81.1.7 que es la dirección de Broadcast y la 10.81.1.1 que se ha definido como gateway de la red.

En el caso de mi servidor hay una previsión de ocupar solo 2 IP (una para Nginx como proxy y por comodidad otra para CertBoot), y aunque un rango /30 habría valido si se movía CertBoot a otra red, por comodidad lo he dejado en un /29

Con esto, se tiene una red que aguanta entre reinicios, que se crea antes de levantar cualquier contenedor, y por tanto, está disponible para los mismos.

Configuración del proxy con Nginx

Ahora es cuando viene la configuración que permite desacoplar el arranque y parada de Nginx del resto de servicios. Pero vayamos por partes.

Se configurará Nginx para que se haga proxy SOLO con la IP de gateway. Por ejemplo (se verá en otro post), cuando se accede a este blog mediante https://pepramon.duckdns.org/blog/, el servidor Nginx que hace de proxy redirige la conexión a la IP y puerto al servidor que está escuchando enhttp://10.81.1.1:8101/.

Cuando arranca Nginx, comprobará que la IP 10.81.1.1 es accesible, y como es la que se ha definido como gateway de la red proxy que es donde está Nginx, la verá y arrancará sin problemas al poder resolver la ip i/o el nombre del host.

Con esto, se permite el arranque de Nginx, aun a cuentas que el resto de servicios no estén funcionando, ya que Nginx comprobará la IP, pero no el puerto donde se redirige la conexión.

Como poner a escuchar un contenedor a una IP particular

Ahora viene el momento de levantar los servicios. Por poner un ejemplo, tenemos un servidor de algo que está escuchando en el puerto 3000 del contenedor.

Lo habitual es hacer una configuración tipo:

services:
  app:
    image: imagen_del_servicio
    ports:
      - "80:3000"

Esto hará que cualquier conexión al puerto 80 del host se redirija al puerto 3000 del contenedor. Pero, ¿no sería interesante poner a escuchar en una IP en concreto, y a poder ser en la IP del gateway de la red proxy donde está el servidor proxy Nginx?

Pues esto último se puede hacer fácilmente si se define el servicio como (ya se pone la IP del gateway de la red proxy creada anteriormente):

services:
  app:
    image: imagen_del_servicio
    ports:
      - "10.81.1.1:8101:3000"

Como se puede ver, solo es necesario añadir la IP antes del puerto que estaría escuchando.

Con lo anterior se podría configurar Nginx como proxy inverso haciendo un proxy_pass http://10.81.1.1:8101; para una localización dada, y el resultado sería que se accedería al puerto 3000 del contenedor.

Esto permite arrancar y parar contenedores, sustituirlos, etc… sin tener que tocar la configuración de Nginx, arrancar Nginx sin ningún servicio arrancado, etc…

Red de contenedores únicos

Se da la situación en que hay veces que es necesario tener un stack con un solo contenedor, y por defecto Docker crea una red para ese stack. En el caso de mi servidor, tengo a watchtower y un servidor Nginx que sirve el contenido estático generado por Jekyll.

Pues, no es necesario malgastar recursos de red, a que como en el caso anterior, para cada stack se crearía en el host un bridge, una interfaz para hacer de gateway y la del propio contenedor.

Para evitarlo, se ha creado una red llamada acceso a internet, donde se conectan los contenedores que están en esa situación. Como se verá es muy similar a la que se ha creado para proxy, pero con más direcciones de red y aislando un contenedor de otro.

El hecho que que los contenedores que están en esa red estén aislados el uno del otro aporta seguridad. Si uno de ellos se viera comprometido, no seria capaz de comunicarse con el resto de contenedores. Al final, cada pequeña dificultad para romper el sistema que se pueda poner, aporta seguridad.

Para definir esa red se ha hecho:

docker network create \
      --driver bridge \
      --opt com.docker.network.bridge.enable_icc=false \
      --subnet 10.81.2.0/24 \
      acceso_internet

donde:

  • docker network create –> Comando de docker para crear una red
  • --driver bridge –> Red a crear tipo bridge
  • --opt com.docker.network.bridge.enable_icc=false –> Aislar los contenedores entre si (Inter Container Comunication)
  • --subnet 10.81.1.0/24 –> Rango de la red
  • --gateway 10.81.1.1 –> Dirección IP de gateway de la red
  • proxy –> Nombre de la red

Resumen

Con todo lo expuesto, quedaría solucionado el tema de poder levantar los servicios individualmente, que levantar el proxy no dependa de que los servicios estén activos, y se evita crear interfaces innecesarias que consuman recursos (aunque estos sean pocos)