User Tools

Site Tools


guia_rapida_de_docker_swarm

Guía rápida y completa de Docker Swarm

Manual rápido y completo de Docker: guia_rapida_de_dockerfile
Manual rápido y completo de Dockerfile:guia_rapida_de_la_linea_de_comandos_de_docker

Docker Swarm viene de serie en las instalaciones Docker y no es necesario instalar nada extra para poder empezar a crear entornos swarm. Es un planificador y orquestador de contenedores de propósito general.

Conceptos básicos de Docker Swarm (service task y container)

Servicios es lo que se crea cuando se quiere desplegar una imagen que tiene una finalidad determinada dentro de un contexto mayor. Por ejemplo servidor HTTP, una base de datos, o cualquier otro tipo de programa ejecutable que desee ejecutar en un entorno distribuido. Con los servicios se especifica qué imagen de contenedor se va a utilizar y comandos que se ejecutarán dentro de los contenedores. Además se puede configurar el puerto, la red, limites de memoria y CPU, políticas de actualización y número de réplicas. Si la configuración asignada no puede ser cumplida por ningún nodo, el servicio quedará en estado pendiente (pending).

Los servicios pueden usar réplicas o definirse como globales. Un servicio que usa réplicas permite configurar su número mientras que en el global, todos los nodos tendrán una tarea asignada. De agregarse nuevos nodos, estos recibirán la tarea automáticamente.

Task / Tarea es lo que el planificador llena generando un contenedor. Es decir, el contenedor es la instancia de la tarea. Si una tarea falla (health check) o termina, por ejemplo escuchando peticiones HTTP, el orquestador crea una nueva tarea de réplica que engendra un nuevo contenedor.Una tarea es un mecanismo unidireccional. Progresa a través de una serie de estados: asignado, preparado, en ejecución, etc. Si la tarea falla, el orquestador elimina la tarea y su contenedor y luego crea una nueva tarea para sustituirla según el estado deseado especificado por el servicio.

Stacks en Docker Swarm son definiciones de servicios, volúmenes, redes, secretos, etc en un archivo de texto en formato YAML. Permite iniciar por tanto múltiples contenedores y elementos necesarios para su correcto funcionamiento. Viene a ser el docker compose de los entornos swarm, teniendo un formato casi idéntico. Los ficheros compose se deben desplegar en enjambres siempre mediante el comando docker “stack”

Características de un entorno Docker swarm

Este tipo de clúster ofrecen las siguientes características.

Diseño descentralizado: Ofrece nodos manager y workers. Los nodos gestores pueden funcionar también como trabajadores, siendo esta la configuración predeterminada. Se recomienda usar 3 o 5 nodos manager en cada enjambre.Hay que tener en cuenta que se entiende por nodo a un host (un servidor o VM). Siempre habrá un nodo maestro considerado líder y es el encargado de tomar las decisiones, aunque los otros nodos están siempre informados del estado y configuración del clúster por si deben tomar el rol de líder en algún momento. Cualquier nodo manager, aunque no sea el líder, puede ejecutar tareas administrativas, como agregar o desactivar algún nodo en el enjambre.

Modelo de servicio declarativo: El motor Docker utiliza un enfoque declarativo para permitirle definir el estado deseado de los diversos servicios en su pila de aplicaciones. Por ejemplo, puede describir una aplicación compuesta por un servicio de front-end web con servicios de cola de mensajes y un back-end de base de datos.

Escalado: Para cada servicio, puede declarar el número de tareas que desea ejecutar. Cuando se aumenta o disminuye la escala, el gestor del enjambre se adapta automáticamente añadiendo o eliminando tareas para mantener el estado deseado.

Reconciliación del estado deseado: El nodo gestor del enjambre supervisa constantemente el estado del clúster y reconcilia cualquier diferencia entre el estado real y el estado deseado expresado por usted. Por ejemplo, si configura un servicio para ejecutar 10 réplicas de un contenedor, y una máquina trabajadora que aloja dos de esas réplicas se bloquea, el gestor crea dos nuevas réplicas para reemplazar las réplicas que se han bloqueado. El gestor del enjambre asigna las nuevas réplicas a los trabajadores que están en funcionamiento y disponibles.

Red multi-host: Puede especificar una red para sus servicios al margen de la predeterminada, la cual es asignada automáticamente a los contenedores.

Descubrimiento de servicios: Los nodos del gestor de enjambre asignan a cada servicio del enjambre un nombre DNS único y equilibran la carga de los contenedores en ejecución. Puede consultar cada contenedor que se ejecuta en el enjambre a través de un servidor DNS integrado en el enjambre.

Balanceo de carga: Puede exponer los puertos de los servicios a un balanceador de carga externo. Internamente, el enjambre permite especificar cómo distribuir los contenedores de servicios entre los nodos.

Seguridad por defecto: Cada nodo del enjambre aplica la autenticación mutua TLS y el cifrado para asegurar las comunicaciones entre él y todos los demás nodos. Tiene la opción de utilizar certificados raíz autofirmados o certificados de una CA raíz personalizada.

Actualizaciones continuas: En el momento del despliegue, puede aplicar las actualizaciones de servicio a los nodos de forma incremental. El gestor de enjambres le permite controlar el retraso entre el despliegue de servicios a diferentes conjuntos de nodos. Si algo va mal, puede volver a una versión anterior del servicio.

Puertos de Docker Swarm

  • TCP Puerto 2376: Comunicación segura con clientes Docker. Únicamente requerido en caso de usar Docker Machine.
  • TCP Puerto 2377: Comunicaciones de gestión del clúster (Solo necesarios en los nodos gestores).
  • TCP/UDP Puerto 7946: Comunicación entre nodos.
  • UDP Puerto 4789: Tráfico de red.

Número de nodos manager recomendado en Docker Swarm (Raft/Quorum)

Los nodos gestores de un enjambre utilizan el algoritmo de consenso de “Raft” para gestionar el estado del enjambre. Raft requiere una mayoría de gestores, también llamada “quórum”, para acordar las propuestas de actualización del enjambre, como la adición o eliminación de nodos.

El número de nodos gestores es importante a la hora de determinar cuando nodos pueden fallar sin perder la gestión del clúster. La formula es (N-1)/2. Si se disponen de 3 Nodos gestores, el clúster funcionará sin problemas mientras solo un nodo manager desaparezca. Si son 7, entonces pueden caer 3 nodos y no habría mayor problema. Se recomienda usar números impares y un mínimo de 3. A más número de nodos gestores más tolerancia a fallos pero menos rendimiento debido a que todos los nodos deben de aprobar las decisiones y se genera también más tráfico de red.

Si no se mantiene el quórum, perdiendo un nodo más del que nuestro clúster necesita, por ejemplo perder 3 nodos gestores de 5, el clúster no podrá ser administrado. las tareas del enjambre en los nodos trabajadores existentes siguen ejecutándose. Sin embargo, los nodos del enjambre no pueden añadirse, actualizarse o eliminarse, y las tareas nuevas o existentes no pueden iniciarse, detenerse, moverse o actualizarse.

En entornos grandes donde haya varias zonas de red y desde todas ellas deba estar el clúster operativo, se recomienda dividir el número de nodos entre el nodo de zonas de manera equitativa. Si se dispone de 9 Nodos gestores y tres zonas, pues 3-3-3. Si fueron 5 gestores y tres zonas, 2-2-1, etc.

Utilizando una implementación de Raft, los gestores mantienen un estado interno coherente de todo el enjambre y de todos los servicios que se ejecutan en él. Además del quórum, Raft permite también la “replicación de registros”, permitiendo que todos los nodos tengan los mismos datos. Estos datos, llamados registros Raft, se usan para guardar información relevante, como los secretos usados en los contenedores, la llave privada raiz de la CA del clúster, información sobre la configuración de las redes, y cualquier cosa necesaria para garantizar el correcto funcionamiento del enjambre.

Crear un entorno swarm

Inicializar un entorno Docker swarm

Las IPs deben ser fijas siempre ya que tras un reinicio se usarán y de tener nuevas direcciones IPs el clúster swarm no funcionará.

Inicializa el entorno swarm creando el primer nodo manager, se puede indicar la interfaz que se usará. La ejecución de este comando dará las instrucciones para introducir más nodos maestros o nodos trabajadores (workers) al clúster. Esto lo hace mostrando qué comandos ejecutar en los otros hosts que queremos que se unan al entorno. Esa información es accesible siempre desde cualquier nodo maestro.

docker swarm init
# docker swarm init --advertise-addr X.X.X.X # Indicando interfaz por IP.
# Para obtener la información de nuevo y agregar más nodos se pueden usar los siguientes comandos desde los nodos manager.
docker swarm join-token worker  # Nodos worker.
docker swarm join-token manager # Nodos Manager.

Entendiendo el proceso de inicializado de un entorno Docker swarm

Cuando se crea un enjambre ejecutando “docker swarm init”, el nodo se designa a sí mismo como nodo gestor. En ese momento el nodo gestor genera una nueva Autoridad de Certificación (CA) raíz (llave privada y certificado autofirmado), esta CA firmará los certificados de cada nodo pero no se almacena nunca en disco, se encuentra en los registros raft y por tanto está disponible en todos los nodos manager. Además de la CA, se genera un fichero “/var/lib/docker/swarm/certificates/swarm-node.key” con dos llaves privadas (una llave Raft DEK para cifrar los secretos y otra llave TLS para cifrar y autenticar a los nodos mediante TLS). Con la llave TLS se genera un certificado el cual es firmado por la CA.

Todo esto se utiliza para asegurar las comunicaciones con otros nodos que se unen al enjambre. Si se prefiere, se puede especificar una propia CA raíz generada por el usuario, utilizando la bandera --external-ca del comando “docker swarm init”.

El nodo gestor también genera dos tokens para utilizar cuando se unen nodos adicionales al enjambre: un token de trabajador y un token de gestor. Cada token incluye el compendio del certificado de la CA raíz y un secreto generado aleatoriamente. Cuando un nodo se une al enjambre, el nodo que se une utiliza el compendio para validar el certificado de la CA raíz del gestor remoto. El gestor remoto utiliza el secreto para asegurarse de que el nodo que se une es un nodo legítimo.

Cada vez que un nuevo nodo se une al enjambre, el nodo el gestor firma el certificado, el certificado contiene un ID de nodo generado aleatoriamente para identificar el nodo bajo el nombre común (CN) del certificado y el rol bajo la unidad organizativa (OU). El ID de nodo sirve como identidad de nodo criptográficamente segura durante la vida del nodo en el enjambre actual. Por defecto, cada nodo del enjambre renueva su certificado cada tres meses. Puede configurar este intervalo ejecutando el siguiente comando.

# La fecha de caducidad de los certificados será de 9 días (216 horas)
docker swarm update --cert-expiry 2160h0m0s

NOTA: Cada tres meses se renuevan los certificados de los nodos, no el certificado de la CA. Si se desea también renovar cada cierto tiempo el certificado raiz, se puede configurar un cronjob que ejecute una rotación de la CA mediante “docker swarm ca --rotate”.

Certificados y llave privada en Docker Swarm.

  • /var/lib/docker/swarm/certificates/swarm-node.crt Certificado del nodo (Rota cada 3 meses).
  • /var/lib/docker/swarm/certificates/swarm-node.key Llaves privadas DEK y TLS del nodo (Cifra y autentica Nodos (TLS) / Cifra los secretos (DEK)).
  • /var/lib/docker/swarm/certificates/swarm-root-ca.crt Certificado de la CA del clúster (No rota).
  • Llave privada rait de la CA del clúster se encuentra en el registro Raft.

El certificado CA es el mismo en todos los nodos, mientras que los certificados individuales de cada nodo tienen la misma firma de la CA. Es decir, swarm-root-ca.crt y swarm-node.crt tienen la misma firma ya que han sido firmados por el nodo manager elegido en el momento de inicialización del swarm. Cuando se usa el bloqueo de un enjambre Docker, lo que se hace es cifrar el fichero swarm-node.key (Llave TLS y llave DEK), obligando por tanto a usar un arranque manual donde se introduzca la “llave/passphrase” en cada inicio del clúster para desbloquearlo y que pueda funcionar. Es una medida de seguridad orientada a impedir males mayores si alguien indeseable accede al sistema de ficheros de algún nodo (leer).

Desplegar un servicio (Replicas / Global)

Al crear un servicio se puede configurar el nombre del mismo, cantidad de contenedores, puertos expuestos, qué servicios deben ejecutarse al inicializar Docker, tareas al realizarse tras un reinicio (por ejemplo rolling restart), características de los nodos donde deben correr los servicios (por ejemplo un tamaño de disco concreto y que el nodo sea del tipo manager).

El modo predeterminado es el modo réplica, el cual permite definir las réplicas que se quiere tener. Si se quiere que haya un task en cada uno de los nodos activos del clúster que cumplan los requisitos de recursos y restricciones configuradas, debe usarse le modo global.

# Modo Replica: Despliega el servicio con nombre helloworld, indicando una sola réplica, qué imagen usar y el comando.
docker service create --replicas 1 --name helloworld alpine ping docker.com
 
# Modo Global: El contenedor estará presente en todos los nodos. De agregarse más nodos, automáticamente recibirán la task.
docker service create --mode global --name helloworld_global alpine ping fediverse.tv
# NOTA: Si se especifican restricciones ya sean de recursos o definidas mediante etiquetas (placement contraints) el modo global las respetará.

Estos comandos llaman a la API de Docker, crea las task (tareas), configura la red para las tasks, las asigna e indica qué nodo debe ejecutarla. Las task (contenedores) una vez son parados / eliminados, no son reiniciados, se crean nuevas tasks y por tanto tienen nuevos IDs.

NOTA: No debe confundirse el modo global de los servicios con el modo host disponible al exponer puertos. Aunque suelen verse juntos muchas veces son cosas diferentes. Si se utiliza mode=host al exponer un puerto y no se utiliza la flag “--mode=global” al crear el servicio mediante “docker service create”, es difícil saber qué nodos están ejecutando el servicio para dirigir el trabajo hacia ellos. Si los dos van de la mano, simplemente estarán accesibles desde cualquier nodo en el puerto especificado sin que haya problemas.

Información sobre el clúster Docker swarm

En los comandos que permitan salida formateada (especificar qué campos queremos obtener), es recomendable usar la opción json primeramente para obtener un listado completo de los campos disponibles. Esto es recomendable ya que no siempre la salida predeterminada tiene todos los campos disponibles.

--format='{{json .}}'

Has varios comandos para obtener información sobre lo que corre en un entorno docker swarm. En casi todos los comandos habrá IDs, pero estos son siempre únicos, es decir, los servicios mostrarán unos IDs distintos a los de sus procesos y también diferentes a los de los contenedores que los forman.

Para obtener más información sobre los contenedores, por ejemplo, qué comando están ejecutando, hay que ejecutar el típico comando docker ps sobre los host que contengan los contenedores, ya sean maestros o trabajadores.

docker info # El comando "info" mostrará entre otra información relativa a Docker, si el nodo está en modo swarm, su ip y la IP de los manager.
 
docker swarm ca                                                          # Muestra en formato PEM el certificado CA.
docker swarm ca | openssl x509 -inform PEM -in - -text -noout | md5sum   # Muestra en formato legible el certificado CA.
Fichero cel certificado CA: /var/lib/docker/swarm/certificates/swarm-root-ca.crt # Este es el certificado CA.
 
# Lista los nodos que forman el clúster, con su nombre, estado, disponibilidad,gestores y su líder y la versión del motor.
# "*" Indica el nodo donde se ejecutó el comando. Este comando solo se puede ejecutar en nodos gestores.
docker node ls
 
ID                            HOSTNAME   STATUS    AVAILABILITY   MANAGER STATUS   ENGINE VERSION
e4ez7bpk32or81vs9e8dikapl *   nodo1      Ready     Active         Reachable        20.10.5
js1a4yne5ua9ka7qjl1c57dkz     nodo2      Ready     Active         Reachable        20.10.5
p7ohl281wzuzdluvz9qdwxru9     nodo3      Ready     Active         Leader           20.10.5
lmp1uan3bt9po4ypir22gcoaj     nodo4      Ready     Active                          20.10.5
baudom8x96uy76vf84rwaz0sg     nodo5      Ready     Active                          20.10.5
 
 
# Permite listar los servicios creados y su estado (id, nombre, modo, réplicas,imagen y puertos).
docker service ls
 
ID             NAME         MODE         REPLICAS   IMAGE           PORTS
bamr5hojemk8   helloworld   replicated   1/1        alpine:latest   
j84a98m8lj9m   prueba1      replicated   1/1        alpine:latest
 
 
# Muestra información sobre un servicio (número de réplicas, monitorización, comando que ejecuta, etc.)
docker service inspect --pretty j84a98m8lj9m
 
ID:		j84a98m8lj9m6ueulnmyywmyl
Name:		prueba1
Service Mode:	Replicated
 Replicas:	1
Placement:
UpdateConfig:
 Parallelism:	1
 On failure:	pause
 Monitoring Period: 5s
 Max failure ratio: 0
 Update order:      stop-first
RollbackConfig:
 Parallelism:	1
 On failure:	pause
 Monitoring Period: 5s
 Max failure ratio: 0
 Rollback order:    stop-first
ContainerSpec:
 Image:		alpine:latest@sha256:69e70a79f2d41ab5d637de98c1e0b055206ba40a8145e7bddb55ccc04e13cf8f
 Args:		ping fediverse.tv 
 Init:		false
Resources:
Endpoint Mode:	vip
 
 
# Muestra el servicios y su estado (id, nombre, imagen, nodo en el que corre, estado deseado y actual, error y puertos). 
docker service ps bamr5hojemk8 j84a98m8lj9m
 
ID             NAME           IMAGE           NODE      DESIRED STATE   CURRENT STATE            ERROR     PORTS
6ffy3ie0ykxx   helloworld.1   alpine:latest   nodo2     Running         Running 43 minutes ago             
q5kssp0vvfjm   prueba1.1      alpine:latest   nodo5     Running         Running 45 minutes ago             
# NOTA: El nombre 1.1 hace referencia a una sola réplica, 1.2 por tanto serían 2 réplicas. Si el servicio se desplegó mediante una stack, el comando "docker stack ps nombre_stack" mostraría la misma salida.
 
# Para conocer el comando que ejecutan los contenedores de un servicio debe usarse el típico comando "docker ps" (no docker service ps).
docker ps 
 
CONTAINER ID   IMAGE           COMMAND               CREATED          STATUS          PORTS     NAMES
afd29d7406df   alpine:latest   "ping fediverse.tv"   50 minutes ago   Up 50 minutes             prueba1.1.q5kssp0vvfjmw7sivez08a9dg

Estados de las tareas / tasks / contenedores en un entorno docker swarm

  • NEW La tarea fue inicializada.
  • PENDING Se asignaron los recursos para la tarea.
  • ASSIGNED Docker asignó la tarea a los nodos.
  • ACCEPTED La tarea fue aceptada por un nodo trabajador. Si un nodo trabajador rechaza la tarea, el estado cambia a REJECTED.
  • PREPARING Docker está preparando la tarea.
  • STARTING Docker está iniciando la tarea.
  • RUNNING La tarea se está ejecutando.
  • COMPLETE La tarea ha salido sin un código de error.
  • FAILED La tarea ha salido con un código de error.
  • SHUTDOWN Docker ha solicitado el cierre de la tarea.
  • REJECTED El nodo trabajador rechazó la tarea.
  • ORPHANED El nodo ha estado inactivo durante demasiado tiempo.
  • REMOVE La tarea no es terminal pero el servicio asociado fue eliminado o reducido.

Escalar, desescalar y borrar un servicio en Docker swarm

Una task es un contenedor corriendo en un servicio. Si se quieren tener un determinado número de réplicas de un servicio se debe usar la opción scale, la cual sirve para tanto para aumentar el número de réplicas como para disminuirlo.

docker service scale prueba1=2 # Crear una réplica más del servicio prueba1, que si se ve en los ejemplos anteriores, ejecuta un comando ping.
docker service ps j84a98m8lj9m # Comprobamos ahora donde corre la nueva réplica.
ID             NAME        IMAGE           NODE      DESIRED STATE   CURRENT STATE               ERROR     PORTS
q5kssp0vvfjm   prueba1.1   alpine:latest   nodo5     Running         Running about an hour ago             
t0mq1fh4dynm   prueba1.2   alpine:latest   nodo1     Running         Running 2 minutes ago
 
# Borrar un servicio: Todas las réplicas del contenedor perteneciente a ese servicio serán eliminadas.
docker service rm j84a98m8lj9m

Actualización continua de contenedores (rolling updates)

Docker Swarm dispone de un mecanismo que nos permite actualizar las imágenes de los contenedores de una forma gradual o en paralelo según se requiera. Lo que se realiza es para el primer contenedor (contenedor = réplica = task = tarea), realiza la actualización y arranca el contenedor. Si la actualización de una tarea devuelve RUNNING, se espera el período de retraso especificado con “--update-delay” antes de iniciar la siguiente tarea. Si por el contrario en cualquier momento durante la actualización, una tarea devuelve FAILED, el proceso de actualización es pausado. El estado puede mostrarse siempre mediante “service inspect”.

Para el ejemplo se crea un servicio (prueba_rolling_update) con tres réplicas que ejecutan un comando ping y se indica un retardo de 10 segundos para su actualización. Es decir, cada 10 segundos, si no hay ningún error, se ejecutará una actualización de la imagen usada por los contenedores del servicio.

# Se crea un servicio con tres réplicas que usan la imagen alpine 3.12.7
docker service create --replicas 3 --name prueba_rolling_update --update-delay 10s  alpine:3.12.7 ping fediverse.tv
# docker service create --replicas 3 --name prueba_rolling_update --update-parallelism alpine:3.12.7 ping fediverse.tv # En paralelo.
 
# NOTA: La opción --update-failure-action ("docker service create" y/o "docker service update") establece la acción a realizar si alguna actualización falla.
# Las acciones posibles son "pause", "continue" y "rollback".
 
# Actualizamos todos las réplicas para que usen alpine:latest
docker service update --image alpine:latest prueba_rolling_update
 
 
# De haber algún problema y estar pausada la actualización, con el siguiente comando podría de nuevo inicializarse.
docker service update prueba_rolling_update
 
 
# El comando docker service ps nos mostrará el nuevo estado donde latest será la versión activa de nuestros contenedores.
docker service ps prueba_rolling_update
ID             NAME                          IMAGE           NODE      DESIRED STATE   CURRENT STATE             ERROR     PORTS 
kyu04uh4rgtg   prueba_rolling_update.1       alpine:latest   nodo5     Running         Running 14 minutes ago              
kokppxvpfmo5    \_ prueba_rolling_update.1   alpine:3.12.7   nodo5     Shutdown        Shutdown 14 minutes ago             
cd9rrdfii9dx   prueba_rolling_update.2       alpine:latest   nodo4     Running         Running 14 minutes ago              
n2kyni4rgplw    \_ prueba_rolling_update.2   alpine:3.12.7   nodo4     Shutdown        Shutdown 14 minutes ago             
r5rbtufv4utu   prueba_rolling_update.3       alpine:latest   nodo3     Running         Running 14 minutes ago              
shvkmdjbecdp    \_ prueba_rolling_update.3   alpine:3.12.7   nodo3     Shutdown        Shutdown 14 minutes ago

Realizar un rollback de una actualización de un servicio docker swarm.

docker service update --rollback prueba_rolling_update

Restricciones / Preferencias de distribución de tasks en nodos de Docker Swarm

En Docker Swarm es posible combinar varios tipos de restricciones para conseguir el comportamiento deseado y distribuir los contenedores entre los nodos de manera personalizada. Aquí se listan todas las formas de distribuir tasks entre los nodos, las cuales se basan en recursos, modo de servicio usados (réplica / global), tipo de exposición de puertos (tipo ingress/host), preferencias y restricciones mediante etiquetas. También se podría agregar el tipo de disponibilidad como forma primitiva de gestionar la distribución contenedores.

Restricciones por modo de servicio

El modo réplica y el modo global de los servicios influyen en la distribución de las tareas (contenedores) por los nodos que forman el enjambre. El modo global si es restringido por recursos (CPU/Memoria) y restricciones de colocación. Las preferencias de colocación no interfieren en el modo global, el cual ignora toda preferencia.

Restricciones por CPU / Memoria RAM

Las opciones --reserve-memory y --reserve-cpu permiten restringir donde deben ejecutarse las tareas de un determinado servicio.

docker service create --name nginx-01 --reserve-memory 900Mb --reserve-cpu 2.5 --replicas 3 nginx

Restricciones de colocación

Los nodos que forman parte de un enjambre Docker pueden ser configurados con determinados etiquetas para restringir el uso del nodo a determinados servicios.

# Agregar una etiqueta denominada region=norte al nodo "nodo1"
docker node update --label-add region=norte nodo1
 
# Definir un servicio para que use dicha restricción de nodo.
docker service create --name werserver --constraint node.labels.region==norte nginx:alpine
# Define el mismo servicio, pero para que sea asignado a cualquier nodo menos el que tenga la etiqueta norte.
docker service create --name werserver --constraint node.labels.region!=norte nginx:alpine

Preferencias de colocación

Las preferencias de colocación permiten mediante un algoritmo interno, repartir de manera equitativa las tareas entre nodos en base a etiquetas. Por norma al desplegar un servicio, las preferencias buscarán los nodos que tengan la etiqueta con el valor deseado, pero si la etiqueta no existe, estos serán usados también. Si se desea restringir el uso de determinados nodos y aplicar preferencias a los no restringidos, se deben combinar las preferencias de colocación con las restricciones (que es su uso habitual).

Se pueden especificar varias preferencias y serán interpretadas por orden, por ejemplo una primera preferencia puede ser datacenter, la segunda rac y una tercera servidor. La asignación se hace por medio de algoritmos internos y no es algo que se pueda definir desde el usuario.

Las preferencias de colocación no tienen validez cuando se usa el modo global de un servicio, el cual coloca una tarea en cada nodo activo del enjambre.

docker service create --replicas 15 --name redis_2 --placement-pref 'spread=node.labels.datacenter' --placement-pref 'spread=node.labels.rack' redis:3.0.6
# Actualización de un servicio agregando y/o borrando las preferencias de colocación.
docker service update --placement-pref-add 'spread=node.labels.datacenter' nombre_servicio # Agregando una preferencia.
docker service update --placement-pref-rm 'spread=node.labels.datacenter' nombre_servicio  # Borrando una preferencia.

Mantenimiento sobre nodos (Drain) / Nodos únicamente manager

Para configurar un nodo gestor que no funcione también como worker, se debe configurar el host en estado drain. También se utiliza la disponiblidad “drain” para realizar labores de mantenimiento en un nodo que funcione como worker. Al poner la disponibilidad en drain en un nodo trabajador (worker) todos sus contenedores (tasks) son eliminados y arrancadas en otro nodo del clúster que esté en disponible (estado activo). Esta opción unicamente funciona con las task generadas mediante docker swarm y no tiene ningún efecto sobre los contenedores creados mediante docker run, docker-compose up o Docker Engine API.

Una vez el nodo pase a estado drain y vuelva a estar activo, no recibirá ninguna task al no ser que se realice algún tipo de reordenación en el entornos swarm. Es decir, hasta que una task falle en otro host, se haga una actualización del servicio o de los contenedores (rolling update) o bien otro nodo pase a estado Drain.

docker node update --availability active worker1 # Modo drain.
docker node update --availability pause worker1  # Modo pause (No recibe nuevas tasks pero las que tienen se mantienen funcionales).
docker node update --availability drain worker1  # Modo activo.
docker node inspect --pretty  worker 1  # El estado del nodo está en el campo "Availability"

Sacar / Agregar / Reingresar nodos en clusters Docker swarm

Convertir un nodo worker en manager.

docker node promote <NODE>

Para solventar algún tipo de problema puntual puede requerirse sacar y volver a introducir un nodo en el enjambre. Veamos algunos fundamentos para hacer esta labor de manera limpia.

Para eliminar nodos maestros, estos deben ser primero convertidos en workers. Una vez esté el nodo configurado como worker, el nodo debe salir del entorno. Si el worker sale del entorno pero no es borrado del enjambre, su estado será Down pero disponibilidad “Active”. Una vez sea borrado el nodo desaparecerá del enjambre y no será visualizable mediante “docker node ls”. Si se fuerza el borrado de un nodo mediante docker node rm, los contenedores serán migrados a otro nodo y desaparecerán del nodo eliminado. Pero el nodo seguirá enrutado a otros nodos si se usa un servicio que publique puertos. El comando “docker node ls” no lo mostraría.

NOTA: Hasta que el nodo no haya salido del enjambre mediante “docker swarm leave”, pese que haya sido eliminado del enjambre desde un gestor con “docker node rm”, este seguirá enrutando peticiones si se usa un servicio con puertos publicados y el nodo recibe peticiones.

Ejemplo sacando e introduciendo d nuevo un nodo manager en un entorno docker swarm.

# Eliminar la función de manager pasando a worker, esto puede realizarse desde cualquier nodo gestor.
docker node demote <NODE>
 
# En el nodo que se quiere expulsar del swarm se debe ejecutar el siguiente comando para salir verdaderamente del enjambre.
docker swarm leave
 
# Eliminar el host del enjambre desde cualquier nodo manager.
docker node rm <NODE>
# Si se muestra el siguiente error, es que el nodo no está parado (no se ejecutó "docker swarm leave").
# Error response from daemon: rpc error: code = FailedPrecondition desc = node p7ohl281wzuzdluvz9qdwxru9 is not down and can't be removed
docker node rm --force <NODE> # Se recomienda parar el worker de manera limpia mediante "docker swarm leave" primeramente y no usar --force).
 
# Desde cualquier nodo maestro se ejecuta el siguiente comando para obtener el token necesario para agregar un nodo gestor.
docker swarm join-token manager
 
# Reincorporar de nuevo el nodo en el clúster con los datos obtenidos desde el nodo gestor
docker swarm join --token SWMTKN-1-45w656bxujopy3csdjffuubw34jxk0uw1n4207utjxneqmzh8q-28nt0r0jqerkzpyuve0e6rwxt 192.168.178.25:2377
 
# Es posible que se quiera configurar el nodo como unicamente manager y no como worker. Si fue expulsado anteriormente el nodo no recuerda su configuración anterior.
docker node update --availability drain <NODE>

Ejemplo sacando e introduciendo un nodo worker en un enjambre docker.

# En el nodo que se quiere expulsar del swarm se debe ejecutar el siguiente comando para salir verdaderamente del enjambre.
docker swarm leave
 
# Eliminar el host del enjambre desde el mismo worker que se quiere expulsar del entorno.
docker node rm <NODE>
 
# Eliminar el host del enjambre desde cualquier nodo manager.
docker node rm <NODE>
# Si se muestra el siguiente error, es que el nodo no está parado (no se ejecutó "docker swarm leave").
# Error response from daemon: rpc error: code = FailedPrecondition desc = node p7ohl281wzuzdluvz9qdwxru9 is not down and can't be removed
docker node rm --force <NODE> # Se recomienda parar el worker de manera limpia mediante "docker swarm leave" primeramente y no usar --force).
 
# Desde cualquier nodo maestro se ejecuta el siguiente comando para obtener el token necesario para agregar un nodo worker.
docker swarm join-token worker
 
# Reincorporar de nuevo el nodo en el clúster con los datos obtenidos desde el nodo gestor
docker swarm join --token SWMTKN-1-123656bxujopy3csdjffuubw34jxk0uw1n4207utjxneqmzh8q-28nt0r0jqerkzpyuve0e6r456 192.168.178.25:2377

Conceptos de redes docker para entornos Swam

Los contenedores en entornos swarm tienen sus puertos accesibles y con posibilidad de balanceo de carga desde cualquier host del enjambre, lo que se denomina Malla de enrutamiento. Se pueden tener varias redes separadas o unidas, aunque normalmente en el modo swarm se usan redes overlay (superpuestas), puede hacerse la combinación de redes que se quiera. Por debajo, como podremos ver de manera práctica, la magia de docker swarm se basa en interfaces virtuales, reglas de iptables (DNAT, aislar redes, marcar paquetes para el balanceo,etc), el uso de espacios de nombres de red namespaces de red y el uso de IPVS.

La red bridge predeterminada implementa docker0 como interfaz puente y está siempre presente en los hosts, se use el modo swarm o no. Es la predeterminada a usar cuando no se especifica ninguna red al crear un servicio o contenedor. Esta red permite únicamente conexión entre contenedores dentro del mismo host y por consiguiente, tanto las redes puente predeterminadas como las creadas de manera explicita no suelen ser usadas en entornos swarm. El controlador de puente de Docker instala automáticamente reglas en la máquina anfitriona (Host) para que los contenedores en diferentes redes de puente no puedan comunicarse directamente entre sí. En entornos swarm automáticamente se crea una red bridge denominada docker_gwbridge, que usa una interfaz tipo bridge para comunicar los contenedores con el exterior y el propio host.

Por lo tanto, la interfaz docker0 y la red predeterminada bridge es normal encontrarla al listar las redes. En entornos swarm, si se ejecuta un inspect de dicha red predeterminada, no deberían aparecer contenedores que la usen ya que estos no podrían conectar con los contenedores de otros hosts del enjambre. De manera inicial suele ser “172.16.0.0/16”, aunque el segundo octeto va subiendo según se vayan creando más redes bridge. Por el contrario en entornos swarm la red docker_gwbridge funciona como puerta de enlace predeterminada para los contenedores, por tanto sí lista como vinculados todos los contenedores que corren en el host al ejecutar “docker network inspect docker_gwbridge”.

Las redes overlay (superpuestas) permiten que los contenedores que pertenezcan a este tipo de redes estén interconectados incluso cuando los contenedores se encuentran en diferentes hosts. Es decir, gestionan las comunicaciones entre los demonios Docker de los hosts que participan en el enjambre. Es posible crear redes overlay para contenedores independientes y/o adjuntar un servicio a una o más redes superpuestas existentes, para permitir la comunicación de servicio a servicio. Al crear un servicio en una red overlay, los contenedores tendrán siempre una interfaz virtual conectada a esa red (y otra interfaz conectada a la red docker_gwbridge). Es por tanto la red más común usada en enjambres Docker.

Las redes superpuestas de Docker utilizan por debajo la tecnología vxlan, que es la tecnología subyacente que hace tan especiales esas redes que permiten conectar contenedores entre diferentes hosts de diferentes redes. Vxlan encapsula las tramas de capa 2 en paquetes de capa 4 (UDP/IP). Esto permite a Docker crear redes virtuales sobre conexiones existentes entre hosts que pueden o no estar en la misma subred, sin tener que preocuparse por la red física subyacente.

La red ingress (de entrada) es una red superpuesta especial que facilita el balanceo de carga entre los nodos de un servicio, es la encargada de exponer los servicios con el exterior. La red de entrada se crea automáticamente al inicializar o unirse a un enjambre. La mayoría de los usuarios no necesitan personalizar su configuración. Cuando cualquier host del enjambre recibe una solicitud en un puerto publicado, entrega esa solicitud a un módulo del kernel llamado IPVS (IP Virtual server) destinado a hacer balanceo a nivel de capa de transporte. El IPVS hace un seguimiento de todas las direcciones IP que participan en ese servicio, selecciona una de ellas y enrutando la petición hacia ella, a través de la red de entrada.

La red docker_gwbridge es una red puente que conecta las redes superpuestas (incluyendo la red de entrada ingress) a la red física de un demonio Docker individual, es decir, al host. Sería el equivalente a la red bridge predeterminada con su interfaz docker0. Por defecto, cada contenedor que ejecuta un servicio está conectado a la red docker_gwbridge de su demonio Docker local. La red docker_gwbridge se crea automáticamente cuando se inicializa o se une a un enjambre. La mayoría de los usuarios no necesitan personalizar su configuración. Si se ejecuta un “docker network inspect docker_gwbridge” deberían de mostrarse todos los contenedores del Host (no de todo el enjambre) que se encuentran funcionando en el swarm. Se usa cuando los contenedores necesitan recursos externos, por ejemplo salir a internet u otras redes fuera del entorno swarm. La red docker_gwbridge NO se usa cuando se comunican dos contenedores de una red superpuesta en diferentes hosts.

NOTA: AL inspeccionar la red ingress y docker_gwbridge se mostrarán unos contenedores ingress-sbox ocultos con nombres como “ingress-endpoint” (ingress) y “gateway_ingress-sbox” (docker_gwbridge). Estos contenedores se encargan de conectar las redes docker con la red del host.

Mostrar los contenedores activos junto con todas sus IPs y puertos en uso (estén publicados al exterior o no).

docker inspect --format "{{ .Name }} {{range .NetworkSettings.Networks}} {{.IPAddress}}{{end}}  {{ .NetworkSettings.Ports }}" $(docker ps -q) |  sed 's/\///' | column  -t
 
portainer_portainer.1.vui9xkz0hvufn2pqt4s64jfax      10.0.4.7    10.0.3.3  map[8000/tcp:[]  9000/tcp:[]]
traefik_traefik.1.1hincnq9hopqwxfl2dqoypf0a          10.0.0.4    10.0.3.4  map[80/tcp:[]]   
portainer_agent.e4ez7bpk32or81vs9e8dikapl.xc49v      10.0.4.5    map[]
helloworld.1.enapmy5n8d9kbcfrdahaf3teu               172.17.0.2  map[]

Malla de enrutamiento (balanceo de carga) en Docker Swarm

El modo de enjambre de Docker facilita la publicación de puertos para ponerlos a disposición fuera del enjambre. Hay dos opciones, el modo “ingress” y el modo “host”. Hay varias maneras de publicar puertos dependiendo del tipo de protocolo que se quieras usar, solo TCP, solo UDP o TCP/UDP.

# TCP
--publish published=53,target=53 
-p 53:53
 
# UDP
--publish published=53,target=53,protocol=udp
-p 53:53/udp
 
# TCP y UDP
--publish published=53,target=53 --publish published=53,target=53,protocol=udp
-p 53:53 -p 53:53/udp

Modo ingress

En este modo, todos los nodos participan en una malla de enrutamiento de entrada. La malla de enrutamiento permite a cada nodo del enjambre aceptar conexiones en los puertos publicados para cualquier servicio que se ejecute en el enjambre, incluso si no hay ninguna tarea que se ejecute en el nodo. La malla de enrutamiento dirige todas las solicitudes entrantes a los puertos publicados en los nodos disponibles a un contenedor activo. Para utilizar dicha funcionalidad los puertos 7946 TCP/UDP y 4789 UDP son necesarios.

Crear una malla de enrutamiento denominada Nginx usando 2 réplicas de contenedores nginx en todos los hosts (por defecto). En los hosts se publica el 8080 el cual balancea la carga al puerto 80 de todos los contenedores de todos los hosts, aunque estos no contengan contenedores nginx.

docker service create --name  nginx --publish published=8080,target=80 --replicas 2 nginx
 
# Si el servicio existe actualmente pero no publica puertos, se puede actualizar fácilmente mediante este comando.
docker service update --publish-add published=<PUBLISHED-PORT>,target=<CONTAINER-PORT> <SERVICE>
 
# Consultar qué puerto está expuesto en un determinado servicio y qué nodos corren los contenedores actualmente.
docker service inspect --format="{{json .Endpoint.Spec.Ports}}" nginx  # Muestra el puerto del host y el de los contenedores.
docker service ps nginx                                                # Muestra el nombre del servicio, imagen, nodo donde corre, estado, etc.

Modo host

A diferencia del modo “ingress”, en el modo “host” cada puerto definido hace referencia a un host y está ligado al contenedor de ese mismo host. Es decir, el servicio no estará disponible desde cualquier host, solo donde corra la task. Por lo tanto, si hay más host que contenedores, habrá hosts que no tengan ningún puerto publicado en referencia a ese servicio ya que no habrá ningún contenedor corriendo. Si se quiere que todos los nodos dispongan del puerto accesible, el número de réplicas no debe superar el nodo de hosts.

Si se utiliza “mode=host” al exponer un puerto y no se utiliza la flag “--mode=global” al crear el servicio mediante “docker service create”, es difícil saber qué nodos están ejecutando el servicio para dirigir el trabajo hacia ellos. Si los dos van de la mano, simplemente estarán accesibles desde cualquier nodo en el puerto especificado sin que haya problemas.

# Expone el puerto 8080 de 5 hosts y lo mapea a cada contenedor.
docker service create --name  apache --publish mode=host,published=8080,target=80 --replicas 5 httpd

Si en el modo host se crean más réplicas que hosts existen en el clúster, no se puede publicar un puerto fijo. Esto se debe a que en este modo hay un puerto por contenedor. La solución sería no especificar ningún puerto y esperar que Docker asigne aleatorios, esto lo hace de manera inteligente, es decir, si tenemos un clúster de 5 nodos y se especifican 10 réplicas en modo host, Docker creará dos puertos en cada host, que serán los mismos en todos los hosts. En este ejemplo se puede ver que se usa 49156 y 49155 (2 réplicas por 5xhost)

docker service create --name  apache --publish mode=host,target=80 --replicas 10 httpd
docker service ps apache
ID             NAME        IMAGE          NODE      DESIRED STATE   CURRENT STATE                ERROR     PORTS
whkgwtmh7h9o   apache.1    httpd:latest   nodo4     Running         Running about a minute ago             *:49156->80/tcp
ild6lozg44zf   apache.2    httpd:latest   nodo1     Running         Running about a minute ago             *:49156->80/tcp
bjkyizlgpeoi   apache.3    httpd:latest   nodo3     Running         Running about a minute ago             *:49156->80/tcp
nj1cbxg4q17q   apache.4    httpd:latest   nodo5     Running         Running about a minute ago             *:49155->80/tcp
sb6o8654jo1p   apache.5    httpd:latest   nodo2     Running         Running about a minute ago             *:49156->80/tcp
v1i1lnpwewnm   apache.6    httpd:latest   nodo1     Running         Running about a minute ago             *:49155->80/tcp
t7yqqk3lsqbb   apache.7    httpd:latest   nodo2     Running         Running about a minute ago             *:49155->80/tcp
cvoyevd92vqg   apache.8    httpd:latest   nodo4     Running         Running about a minute ago             *:49155->80/tcp
8isi7addel93   apache.9    httpd:latest   nodo3     Running         Running about a minute ago             *:49155->80/tcp
u55mgn4rhoze   apache.10   httpd:latest   nodo5     Running         Running about a minute ago             *:49156->80/tcp

Si se quiere saber como funcionan por debajo las redes más usadas de docker, leer Cómo funcionan las redes overlay, docker_gwbridge y la red ingress.

Secrets en Docker Swarm

Un secreto es un bloque de datos, como una contraseña, una clave privada SSH, GPG, certificado SSL u otra pieza de datos que no debe transmitirse a través de una red o almacenarse sin cifrar en un archivo Docker o en el código fuente de su aplicación. Puedes utilizar los secretos de Docker para gestionar de forma centralizada estos datos y transmitirlos de forma segura sólo a aquellos contenedores que necesiten acceder a ellos. Un secreto dado sólo es accesible para aquellos servicios a los que se les ha concedido acceso explícito a él, y sólo mientras esas tareas de servicio se están ejecutando. Es decir, no se almacenan y solo están presentes en tiempo de ejecución (en forma de solo lectura.

Cuando se añada un secreto al enjambre, Docker envía el secreto al administrador del enjambre a través de una conexión TLS mutua. El secreto se almacena en el registro de Raft, que está siempre cifrado en disco (solo nodos gestores). Todo el registro de Raft se replica en los demás nodos gestores, asegurando así la alta disponibilidad para los secretos.

NOTA: Los secretos siempre están accesibles en texto claro en el mismo nodo donde fueron montados mediante tmpfs. Cuando el contenedor no está activo, ese directorio desaparece.

df -h | grep secret
tmpfs           236M   12K  236M   1% /var/lib/docker/containers/b0fc4488a65/mounts/secrets

Cuando se concede el acceso a un secreto a un servicio (recién creado o en ejecución), el secreto descifrado se monta en el contenedor en un sistema de archivos en memoria /run/secrets/XXX, pero es posible personalizar la ruta.

Se puede actualizar un servicio para concederle o revocarle acceso a secretos en cualquier momento. Cuando una tarea de contenedor deja de ejecutarse, los secretos descifrados se desmontan del sistema de archivos en memoria para ese contenedor y se eliminan de la memoria del nodo. Si un nodo pierde la conectividad con el enjambre mientras está ejecutando un contenedor de tareas con acceso a un secreto, el contenedor de tareas sigue teniendo acceso a sus secretos, pero lógicamente no puede recibir actualizaciones hasta que el nodo vuelva a conectarse al enjambre.

No se puede eliminar un secreto que esté utilizando un servicio en ejecución. Un secreto debe ser siempre menor a 500Kb. Para actualizar o deshacer los secretos más fácilmente, a veces es útil asignarles fechas o nombres de ficheros cuando el secreto es un archivo. Rotar, actualizar o borrar un secreto en un servicio requiere del comando “docker service update” y eso implica un reinicio del servicio. No se pueden agregar secretos a variables. La mayoría de imágenes oficiales permiten el uso de secrets por medio de ficheros. Por ejemplo en Wordpress se puede usar la variable “WORDPRESS_DB_PASSWORD” para enviar la contraseña o bien mediante un fichero “WORDPRESS_DB_PASSWORD_FILE”, el cual está orientado a ser usado mediante secretos.

docker secret create certificado_www certificado.pem # Se crea un secreto llamado certificado_www que contiene el fichero certificado certificado.pem.
printf "Esto es un secreto" | docker secret create secreto1 - # Se crea un secreto secreto1 con el contenido pasado por stdin.
 
docker secret inspect secreto1  # Se muestra información sobre el secreto, ID, nombre, driver, fecha de creación, actualización y etiquetas.
docker secret ls                # Se lista el secreto ID, nombre, driver, fecha de creación y actualización.
docker secret rm secreto1       # Elimina el secreto llamado "secreto1" (NO debe estar en uso en ningún servicio).
 
docker service  create --name redis --secret my_secret_data redis:alpine # Crea un servicio basado en redis que pueda acceder al secreto my_secret_data
docker service  update --secret-add secreto1 nginx_nginx  # Actualiza el servicio nginx_nginx para que use el secreto secreto1.
docker service update --secret-rm secreto1 nginx_nginx    # Elimina en el servicio nginx_nginx el secreto secreto1
 
# Es factible agregar varios secretos a la vez a un servicio, definir donde deben ser montados (por defecto en /run/secrets/) y pueden usar el mimo nombre que el fichero, cuando se el secreto en un fichero (=< 500Kb).
docker service create \
     --name nginx \
     --secret source=site.key,target=/etc/nginx/ssl_keys/site.key \
     --secret source=site.crt,target=/etc/nginx/ssl_certs/site.pem \
     ...

NOTA: Recordar que para cada actualización del servicio, este es rearrancado de nuevo (los contenedores serán sustituidos por otros nuevos).

Rotación de secretos en Docker swarm.

La rotación de secrets que hace referencia la documentación no se refiere a ninguna funcionalidad concreta. Los secretos no pueden sobrescribirse, deben ser siempre eliminados y creados de nuevo. Si se debe rotar un secreto se debe atender a como funcionan los contenedores del servicio y actuar en consecuencia para generar tiempos de downtime mínimos o nulos. Se debe tener en cuenta que siempre que se actualiza un secreto en un servicio, se requiere ejecutar un “docker service update” y como dijimos anteriormente, esto implica siempre parar y arrancar nuevamente los contenedores del servicio.

Ejemplo asignando un nuevo valor al secreto “secreto1” usado por servicio nginx_nginx.

printf "Esto es el nuevo secreto" | docker secret create secreto1.2 -
y0l5h2nsy81ejw6prvqet57jw
 
docker secret ls
ID                          NAME         DRIVER    CREATED             UPDATED
iqlchiyiw28u9phcagnuo76qc   secreto1               About an hour ago   About an hour ago <---- Creado anteriormente.
y0l5h2nsy81ejw6prvqet57jw   secreto1.2             10 seconds ago      10 seconds ago    <---- Creado con el comando anterior.
 
# Se borra el actual secreto (implica un reinicio).
docker service update --secret-rm secreto1 nginx_nginx
 
# Se vuelve a agregar el antiguo secreto con nombre _old y el nuevo
docker service  update --secret-add source=secreto1,target=secreto1_old --secret-add source=secreto1.2,target=secreto1 nginx_nginx
 
# Dentro de cualquier contenedor del servicio nginx_nginx se visualiza el contenido de /run/secrets/secreto1.
cat /run/secrets/secreto1
Esto es el nuevo secreto

Configs en Docker Swarm (Configuraciones / plantillas para procesos en contenedores)

Las configuraciones de servicio permiten almacenar información no sensible, normalmente archivos de configuración, fuera de la imagen de un servicio o de los contenedores en ejecución. Esto permite mantener imágenes genérica, sin la necesidad de bind-mount o el uso de variables de entorno. Estos ficheros de configuración pueden usarse como plantillas para que una vez arranque el contenedor de un servicio, la configuración se genere con los valores deseados mediante variables.

Las configuraciones operan de manera similar a los secretos, excepto que no están cifradas cuando están en reposo y se montan directamente en el sistema de archivos del contenedor sin el uso de discos RAM. Las configuraciones pueden ser añadidas o eliminadas de un servicio en cualquier momento, y los servicios pueden compartir una configuración. Incluso se pueden usar “configs” con variables de entorno o etiquetas, para crear plantillas que ofrezcan más flexibilidad. Los valores de configuración pueden ser cadenas genéricas o contenido binario (de hasta 500 kb de tamaño).

Cómo gestiona Docker Swarm las configuraciones.

Cuando se añade una configuración al enjambre, Docker envía la configuración al nodo administrador del enjambre a través de una conexión TLS mutua. La configuración se almacena en el registro cifrado de Raft. Todo el registro de Raft se replica en los demás nodos gestores, asegurando las mismas garantías de alta disponibilidad para las configuraciones que para el resto de los datos de gestión del enjambre.

Cuando se vincula una configuración a un servicio recién creado o en ejecución, ésta se monta como un archivo en la raíz del contenedor “/configuración” de manera predeterminada si no se especifica una ruta.

Se puede establecer el uid y gid de la configuración por nombre o ID y especificar los permisos del archivo. Si no se establece, la “config” es propiedad del usuario y grupo que ejecuta el comando del contenedor (a menudo root). Los permisos predeterminados son de lectura para todo el mundo (0444), a menos que se establezca un umask dentro del contenedor, en cuyo caso el modo se ve afectado por ese valor de umask.

Un nodo sólo tiene acceso a las configuraciones si el nodo es un gestor de enjambre o si está ejecutando tareas de servicio a las que se ha concedido acceso a la configuración. Cuando una tarea de contenedor deja de ejecutarse, las configuraciones compartidas con ella se desmontan del sistema de archivos en memoria para ese contenedor y se vacían de la memoria del nodo.

Si un nodo pierde la conectividad con el enjambre mientras está ejecutando un contenedor de tareas con acceso a una configuración, el contenedor de tareas sigue teniendo acceso a sus configuraciones, pero no puede recibir actualizaciones hasta que el nodo vuelva a conectarse al enjambre.

No se puede eliminar una configuración que esté utilizando un servicio en ejecución. Como en los secretos y cualquier otra acción que requiera cambiar la configuración de un servicio activo, implica parar y volver a crear los contenedores (docker service update). Si la configuración que se vincula a un servicio con más de una réplica en funcionamiento tiene errores, por ejemplo errores al usar Placeholders y olvidar una “}”, el primer contenedor fallará al iniciar y no se continuará con la actualización hasta que se corrija el problema. El resto de réplicas se mantendrá con la configuración anterior ya que la actualización del servicio no pudo finalizar. Si esto sucede con un servicio que no tiene más que una réplica, el servicio dejará de funcionar.

Para actualizar una stask que use configuraciones, se hacen cambios en el archivo Compose y luego vuelva a ejecutar lo siguiente

docker stack deploy -c <nuevo-archivo-compose> <nombre-de-la-pila>

Como se comentó anteriormente, las configuraciones son inmutables, así que no se puede cambiar el archivo para un servicio existente. En su lugar, se debe crear una nueva configuración.

Todas las stacks eliminadas mediante “docker stack rm” borran cualquier configuración creada por “docker stack deploy” con el mismo nombre de pila. Esto elimina todas las configuraciones, incluyendo las que no son referenciadas por los servicios.

Plantilla para un fichero index.html que se integrará en un servicio Nginx.

index.html.tmpl
<html lang="es">
  <head><title>Hello Docker</title></head>
  <body>
   <p>Hola {{ env "HELLO" }}! Vamos a listar todos los "Placeholders" disponibles para los templates según la documentación oficial: https://docs.docker.com/engine/reference/commandline/service_create/#create-services-using-templates</p>
 
<table>
<tr><th>Placeholder</th><th>Valor</th></tr>
<tr><td>Service ID</td><td>{{.Service.ID}}</td></tr>
<tr><td>Service name</td><td>{{.Service.Name}}</td></tr>
<tr><td>Service labels </td><td>{{.Service.Labels}}</td></tr>
<tr><td>Node ID</td><td> {{.Node.ID}}</td></tr>
<tr><td>Node Hostname </td><td>{{.Node.Hostname}}</td></tr>
<tr><td>Task ID </td><td>{{.Task.ID}}</td></tr>
<tr><td>Task /container name </td><td>{{.Task.Name}}</td></tr>
<tr><td>Task slot </td><td>{{.Task.Slot}}</td></tr>
</table>
 
  </body>
</html>
# Se crea la configuración "homepage" con el fichero plantilla "index.html.tmpl". El fichero de plantilla no es obligatorio al crear configuraciones.
docker config create --template-driver golang homepage index.html.tmpl
 
# Crear un nuevo servicio usando la configuración "homepage".
docker service create \
     --name hello-template \
     --env HELLO="Docker" \
     --config source=homepage,target=/usr/share/nginx/html/index.html \
     --publish published=3000,target=80 \
     nginx:alpine
 
# Actualizar un servicio llamado nginx_nginx para asignarle una configuración homepage indicando la ruta y una variable de entorno.
docker service update --env-add HELLO="Docker" --config-add source=homepage,target=/usr/share/nginx/html/index.html nginx_nginx 
 
# Rotar una configuración (homepage > homepage2)
docker config create --template-driver golang homepage2 index2.html.tmpl
docker service update --env-add HELLO="Docker" --config-rm homepage --config-add source=homepage2,target=/usr/share/nginx/html/index.html nginx_nginx
 
docker config inspect # Muestra información sobre una configuración (fecha de creación, actualización, nombre, etiquetas, configuración en Base64, etc).
docker config inspect --pretty # Muestra información sobre una configuración decodificando la configuración en Base64.
 
docker config ls      # Lista las configuraciones.
docker config rm      # Borra una configuración (No puede estar en uso por ningún servicio).

Entendiendo como funcionan las redes overlay y docker_gwbridge en Docker Swarm

Las redes ingress, overlay y docker_gwbridge (tipo bridge) son redes docker diferentes pero van siempre de la mano en los entornos swarm. La primera se encarga de recibir las peticiones desde cualquier nodo y enrutarla a donde debe ir.Las redes overlay creadas por el usuario tienen la finalidad de comunicar unos contenedores con otros, incluso cuando se encuentran en diferentes hosts. Por último la red docker_gwbridge permite a los contenedores de entornos swarm comunicarse con el host al que pertenecen y poder salir a Internet (u otras redes), además de comunicar las redes ingress con las overlay creadas explícitamente. Esto se consigue por medio del uso de diferentes tipos de interfaces (bridge, veth y vxlan), espacios de nombres de red y reglas iptables.

La red overlay ingress es una red predeterminada en entornos swarm a la que los servicios se unen si no se define una red overlay especifica. La trataremos en el siguiente apartado. Ahora nos centraremos en redes overlay definidas por el usuario y docker_gwbridge

Imaginamos un entorno swarm donde existe una red overlay llamada traefik-public, que tiene un contenedor traefik (servicio traefik_traefik) en un host denominado nodo1 y dos contenedores Nginx en un host nodo4 pertenecientes al servicio “nginx_nginx”. A continuación se mostrará mediante comandos como se relacionan las diferentes tecnologías de red del kernel de linux que facilitan la implementación y uso de redes overlay y docker_gwbridge.

# Servicios Nginx y traefik
docker service ls
ID             NAME                  MODE         REPLICAS   IMAGE                           PORTS
5fsv19hkeums   nginx_nginx           replicated   2/2        nginx:latest                    
x0slfctucpr7   traefik_traefik       replicated   1/1        traefik:v2.2                    *:80->80/tcp, *:443->443/tcp
 
# El servicio Nginx (dos contenedores) se ejecuta en el host nodo4 (Nodo Worker) y traefik en el host nodo1 (Nodo Manager).
docker service ps traefik_traefik nginx_nginx
gefxei0ossbp   nginx_nginx.1           nginx:latest   nodo4     Running         Running 7 minutes ago                                       
qwxvql8mubsy   nginx_nginx.2           nginx:latest   nodo4     Running         Running 7 minutes ago                                       
ri6o68bz6n9b   traefik_traefik.1       traefik:v2.2   nodo1     Running         Running 8 minutes ago 

El servicio Nginx está vinculado a la red overlay de traefik, denominada traefik-public. Es decir, hay tres contenedores que pertenecen a esa misma red.

docker network ls
NETWORK ID     NAME                      DRIVER    SCOPE
7dac3e2a5e2f   bridge                    bridge    local # NO se usa en este Swarm.
1ed48a32a803   docker_gwbridge           bridge    local # SI se usa en swarm (Se crea de manera automática).
419d527b0a42   host                      host      local # NO se usa en este Swarm.
pdfaswed392n   ingress                   overlay   swarm # SI se usa en Swarm (Se crea de manera automática).
dab253e432c0   none                      null      local # NO se usa en este Swarm.
npsnqeyhadtb   traefik-public            overlay   swarm # SI se usa en Swarm (Se ha creado al configurar traefik).

NOTA: De usar el comando “docker network inspect traefik-public” en un host, solo se mostrarán los contenedores corriendo sobre ese mismo host que estén asociados a dicha red. En la sección Peers se mostrarán las IPs de los hosts que contienen otros contenedores asociados a esa misma red, pero NO los contenedores. Por ahora no hay forma de poder averiguar desde un solo host, cuántos contenedores contiene una determinada red si sus contenedores se encuentran distribuidos en varios hosts.

Si un contenedor nginx quiere salir a internet, usará la red docker_gwbridge, ya que es la red que usan como puerta de enlace predeterminada. Si por el contrario un contenedor nginx del nodo4 quiere conctactar con el contenedor traefik del nodo1, se usará la red traefik-public, la cual conecta con otros nodos del enjambre por medio de una interfaz vxlan en el espacio de nombres de red de traefik-public. Lógicamente para que eso funcione, todos los hosts del enjambre tienen ese espacio de red con una interfaz vxlan.

Relación entre espacios de nombre de red y redes docker

Mostrar los espacios de nombres de red del host nodo4, donde corre el servicio nginx_nginx. Se verán las aplicaciones usadas por los contenedores.

# Se visualizan los contenedores corriendo en el host nodo4.
docker ps
CONTAINER ID   IMAGE                    COMMAND                  CREATED          STATUS          PORTS     NAMES
0eea3d6df6c1   nginx:latest             "/docker-entrypoint.…"   22 minutes ago   Up 22 minutes   80/tcp    nginx_nginx.2.vs2mr8p1fy97w9clys6jnlihy
70ac71547eaa   nginx:latest             "/docker-entrypoint.…"   22 minutes ago   Up 22 minutes   80/tcp    nginx_nginx.1.l2i3xr4bf0f8mf6orv47cz93e
 
 
# El comando lsns recoge la información mostrada comprobando el contenido del directorio /proc/<PID>/ns para cada PID.
# Por lo tanto solo se listan el espacio de nombres de red que usen algun proceso.
 lsns -t net 
        NS TYPE NPROCS   PID USER    NETNSID NSFS                           COMMAND
4026531992 net      86     1 root unassigned                                /sbin/init
4026532697 net       2  1465 root          7 /run/docker/netns/fb2ebb9c1b5d nginx: master process nginx -g daemon off;
4026532770 net       2  1511 root          8 /run/docker/netns/552e78323f9f nginx: master process nginx -g daemon off;
 
 
# El comando "ip netns" sólo funciona con los enlaces simbólicos del espacio de nombres en /var/run/netns. Pero ese enlace no es creado por docker.
# Debe crearse primero el enlace para listar todos los espacios de nombres de red disponibles.
cd /var/run
ln -s /var/run/docker/netns netns
ip netns
 
fb2ebb9c1b5d (id: 8) # Visualizado en lsns
552e78323f9f (id: 7) # Visualizado en lsns
1-pdfaswed39 (id: 2) # pdfaswed392n   ingress                   overlay   swarm
1-npsnqeyhad (id: 0) # npsnqeyhadtb   traefik-public            overlay   swarm
lb_npsnqeyha (id: 3) # npsnqeyhadtb   traefik-public            overlay   swarm
lb_gp0pylzm0 (id: 4) # gp0pylzm0a4w   portainer_agent_network   overlay   swarm
ingress_sbox (id: 5)
 
 
# Conociendo los espacios de nombres de red creados para los contenedores, se puede obtener la misma configuración ejecutando ip addr sobre el contenedor o sobre el espacio de red.
 
ip netns exec fb2ebb9c1b5d ip addr
docker exec 0eea3d6df6c1 ip addr
 
ip netns exec 552e78323f9f ip addr
docker exec 70ac71547eaa ip addr

NOTA: Todos los namespaces de red que coinciden con las redes docker, tendrán el mismo identificador en todos los hosts del enjambre.

Los interfaces de red de los contenedores están vinculadas a diferentes espacios de nombres de red. Si se listan las interfaces del host nodo4 donde corren los contenedores nginx, se puede ver la relación con los identificadores de espacio de nombres de red en uso. Las interfaces virtuales veth se usan para unir espacios de red diferentes y se crean siempre en pares. Estos pares están siempre unidos por un cable virtual y cada uno de ellos puede pertenecer a un espacio de nombres de red diferente. Estas interfaces veth son usadas para comunicar por ejemplo la interfaz de un contenedor con una interfaz del host y así poder salir a internet. Mientras que otra interfaz del contenedor puede estar vinculada a un espacio de red perteneciente a una red superpuesta.

docker_gwbridge

Al ejecutar el comando ip addr en el host, se puede ver como las interfaces vethXXX están conectadas a su vez a “docker_gwbridge” (funciona como un switch) que interconecta las interfaces veth. Como dijimos las interfaces virtuales veth son creadas en forma de pares unidos, en este caso se listan las del host, pero cada una de ellas tiene su par correspondiente en un contenedor. Esto puede verse en la descripción del interfaz veth, la cual muestra en “link-netns” a qué espacio de nombres de red está unida.

Estas conexiones entre host y contenedor por medio de interfaces ethernet virtuales, tiene como objetivo que los contenedores puedan contactar con el host o bien salir por ejemplo a otras redes como internet (Pero NO se usan para conectar a otras redes overlay).

# Host nodo4
ip addr
 
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
   ...
2: enp0s3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
   ...
3: enp0s8: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
   ...
4: docker_gwbridge: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default 
    link/ether 02:42:a3:eb:7c:c9 brd ff:ff:ff:ff:ff:ff
    inet 172.18.0.1/16 brd 172.18.255.255 scope global docker_gwbridge
       valid_lft forever preferred_lft forever
    inet6 fe80::42:a3ff:feeb:7cc9/64 scope link 
       valid_lft forever preferred_lft forever
5: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default 
   ...
14: veth09fa560@if13: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker_gwbridge state UP group default 
    link/ether 7e:2b:8b:1e:a8:21 brd ff:ff:ff:ff:ff:ff link-netns ingress_sbox # Contenedor oculto.
    inet6 fe80::7c2b:8bff:fe1e:a821/64 scope link 
       valid_lft forever preferred_lft forever
28: veth808fb29@if27: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker_gwbridge state UP group default 
    link/ether 2e:8c:4b:ff:a0:80 brd ff:ff:ff:ff:ff:ff link-netns fb2ebb9c1b5d # /run/docker/netns/fb2ebb9c1b5d nginx: master process nginx -g daemon off;
    inet6 fe80::2c8c:4bff:feff:a080/64 scope link 
       valid_lft forever preferred_lft forever
32: veth8e0d606@if31: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker_gwbridge state UP group default 
    link/ether f6:24:82:93:fd:7d brd ff:ff:ff:ff:ff:ff link-netns 552e78323f9f # /run/docker/netns/552e78323f9f nginx: master process nginx -g daemon off;
    inet6 fe80::f424:82ff:fe93:fd7d/64 scope link 
       valid_lft forever preferred_lft forever     

Las interfaces relevantes por tanto serían docker_gwbridge con la ip 172.18.0.1, veth09fa560@if13 (contenedor oculto), veth808fb29@if27 (contenedor nginx) y veth8e0d606@if31 (contenedor nginx).

Visualización de la configuración puente: Conocer qué interfaces están conectadas a la interfaz virtual del host docker_gwbridge.

brctl show
bridge name        bridge id            STP enabled     interfaces
docker0            8000.024218d79977    no
docker_gwbridge    8000.0242a3eb7cc9    no              veth09fa560 (contenedor oculto ingress_sbox)
                                                        veth808fb29 (contenedor nginx)
                                                        veth8e0d606 (contenedor nginx)

Red superpuesta (traefik-public)

Cuando se crea una red superpuesta, Docker crea un espacio de nombres para la red en el host, en este caso 1-npsnqeyhad, la cual está disponible en otros hosts que forman parte del enjambre. Dentro de ese espacio de nombres de red se crea un interfaz vxlan y un interfaz puente br0. Este br0 funciona como un switch interconectando el resto de interfaces de la red. Por lo tanto una vez creada la red, el espacio de nombres de red generado ya dispone de esas dos interfaces, además vxlan está siempre vinculada a br0.

Cuando se arranca un contenedor conectado a la red overlay, se genera en dicho espacio de red de la red superpuesta una interfaz veth que automáticamente se vincula a su vez al puente br0. Esta interfaz veth del espacio de red tiene su otro par en el contenedor. Cuando se envía tráfico entre contenedores de una misma red en diferentes hosts, el interfaz veth correspondiente se comunica con su par en ese espacio de red y a traves de br0, vxlan recibe dicho tráfico y lo enruta al host que corresponda. El host utiliza la cabecera vxlan para enrutar el paquete al nodo de destino correcto de manera exitosa.

Visualizar las interfaces que se usan en el espacio de nombres de red de la red overlay traefik-public y como están vinculadas a br0.

# Interfaces del espacio de nombres de red de traefik-public (overlay).
ip netns exec 1-npsnqeyhad ip link
 
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
2: br0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UP group default 
    link/ether 5a:83:eb:59:62:c5 brd ff:ff:ff:ff:ff:ff
    inet 10.0.3.1/24 brd 10.0.3.255 scope global br0
       valid_lft forever preferred_lft forever
22: vxlan0@if22: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue master br0 state UNKNOWN group default 
    link/ether fe:17:2b:aa:95:fc brd ff:ff:ff:ff:ff:ff link-netnsid 0  # Comunica contenedores pertenecientes a una red overlay en diferentes hosts.
24: veth0@if23: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue master br0 state UP group default 
    link/ether 62:5c:b1:42:39:c9 brd ff:ff:ff:ff:ff:ff link-netns lb_npsnqeyha # Interfaz usada para hacer balanceo de carga mediante nombre de servicios desde los hosts.
26: veth1@if25: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue master br0 state UP group default 
    link/ether 5a:83:eb:59:62:c5 brd ff:ff:ff:ff:ff:ff link-netns fb2ebb9c1b5d # Contenedor Nginx
30: veth2@if29: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue master br0 state UP group default 
    link/ether f2:cf:42:4c:f8:5b brd ff:ff:ff:ff:ff:ff link-netns 552e78323f9f # Contenedor Nginx
  • La interfaz br0 Es el puente donde conectan todas las interfaces. Por tanto esta interfaz, que funciona como un swith, se usa tanto si el tráfico sale del host para contactar con otros nodos dentro de la red overlay, como para comunicar dos contenedores de la misma red dentro del mismo host. Esta interfaz NO se usa si los contenedores conectan por ejemplo a internet, ya que no se está haciendo uso de la red superpuesta.
  • la interfaz vxlan0 es la interfaz que permite en redes overlay comunicar contenedores entre diferentes hosts. La cual está logicamente unida también al puente y es a través de él que obtiene el tráfico de los contenedores. No se usa si la comunicación es dentro del mismo host o si el tráfico va dirigido a otra red, por ejemplo internet.
  • Cada interfaz “vethX” puede actuar como una interfaz o como un túnel entre espacios de nombres de red. En el caso del host cada interfaz veth listada conecta con su par correspondiente en un contenedor. Esa información está disponible si se lee con atención la salida del comando “ip addr”. El dispositivo de red (del tipo veth aunque tengan nombres ethX) dentro del contenedor siempre tiene un número menor que la interfaz a la cual está unida. También mediante el identificador de interfaz posterior a “@” se puede averiguar la vinculación con el interfaz del host.

Espacio de red lb_xxxxxxxxx y el balanceo de carga por nombre de servicio.

La “interfaz 24: veth0@if23” del espacio de red “1-npsnqeyhad” (traefik-public) tienen su par correspondiente “23: eth0@if24” en el espacio de red “lb_npsnqeyha” cuya configuración de red se muestra a continuación.

ip netns exec lb_npsnqeyha ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
23: eth0@if24: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UP group default 
    link/ether 02:42:0a:00:03:13 brd ff:ff:ff:ff:ff:ff link-netns 1-npsnqeyhad
    inet 10.0.3.19/24 brd 10.0.3.255 scope global eth0
       valid_lft forever preferred_lft forever
    inet 10.0.3.5/32 scope global eth0 # Servicio traefik_traefik.
       valid_lft forever preferred_lft forever
    inet 10.0.3.8/32 scope global eth0 # Servicio nginx_nginx.
       valid_lft forever preferred_lft forever

Estos espacios de red lb_xxxxxxxxx aunque a fecha de esta guía no han sido documentados, siempre están disponibles al crear un servicio sobre una red overlay. Estos espacios de red destinados al balanceo de carga por nombre de servicio, siempre contienen una interfaz “lo” y una interfaz eth0. La interfaz eth0 (tipo veth) usa una IP principal (/24) para contactar con otros hosts y varias direcciones IPs secundarias (/32) que identifican los servicios. Todas con direcciones IP pertenecientes a la red traefik-public.

La dirección IP principal variará entre los hosts de la red, pero las secundarias usarán un prefijo /32 y serán las mismas en todos los host que hagan uso de la red, en nuestro caso traefik-public. Cada IP secundaria se refiere a un servicio docker vinvulado a la red overlay. Es decir, cada servicio funciona como un dominio, el cual tiene asignada una de esas IPs secundarias del espacio de nombres de red “lb_npsnqeyha”. Los contenedores que pertezcan a esa red podrán usar los nombres de servicios como dominios y si los servicios contienen varias replicas, disfrutarán de balanceo de carga.

Esto se hace por medio de un contenedor oculto que puede ser visualizado con “docker network inspect traefik-public” y usa el prefijo “lb-”, para nuestro caso “lb-traefik-public”. La IP listada para ese contenedor es la que usa el prefijo /24 y tiene la finalidad de balancear la carga entre los contenedores de los servicios de la red, esten o no en el host local. Cuando desde un contenedor, que puede estar en cualquier host, se hace una petición por nombre de servicio, por ejemplo “curl nginx_nginx”. La dirección que registrará nginx será la IP /24 que dicho host usa en lb_npsnqeyha.

Red docker_gwbridge y overlay (traefik-public) en los contenedores

Relación entre interfaces del host y contenedores (prestar atención al número de interfaz y el nombre eth@ifXX).

Host "28: veth808fb29@if27" --> Contenedor Nginx 70ac71547eaa "27: eth1@if28"
Host "32: veth8e0d606@if31" --> Contenedor Nginx 0eea3d6df6c1 "31: eth1@if32"

Relación entre las interfaces del espacio de nombres de red de la red overlay y contenedores (prestar atención al número de interfaz y el nombre eth@ifXX)

Espacio de nombres de red 1-npsnqeyhad "26: veth1@if25" --> Contenedor Nginx 70ac71547eaa "25: eth0@if26"
Espacio de nombres de red 1-npsnqeyhad "30: veth2@if29" --> Contenedor Nginx 0eea3d6df6c1 "29: eth0@if30"

Se listan las interfaces de red de los dos contenedores nginx. Cada uno tiene dos interfaces, eth0 y eth1, ambas del tipo veth. la interfaz eth0 en el contenedor coincide con la interfaz del espacio de nombres de red de la red superpuesta traefic-public. La interfaz eth1 (también virtual) coincide con la interfaz veth del host y como se puede ver en las rutas, será la usada para contactar con todo lo que no sea la red superpuesta traefik-public.

# Primer contenedor docker.
docker exec -it 70ac71547eaa ip --details addr
 
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    ...
# interfaz "26: veth1@if25" del espacio de nombres de red perteneciente a la red  overlay traefik-public.
25: eth0@if26: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UP group default 
    link/ether 02:42:0a:00:03:0f brd ff:ff:ff:ff:ff:ff link-netnsid 0
    veth numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535 
    inet 10.0.3.15/24 brd 10.0.3.255 scope global eth0
       valid_lft forever preferred_lft forever
# Interfaz 28: veth808fb29@if27 del host, la cual está vinculada a docker_gwbridge para salir a Internet.
27: eth1@if28: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
    link/ether 02:42:ac:12:00:04 brd ff:ff:ff:ff:ff:ff link-netnsid 1
    veth numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535 
    inet 172.18.0.4/16 brd 172.18.255.255 scope global eth1
       valid_lft forever preferred_lft forever
 
ip route
default via 172.18.0.1 dev eth1                                # Usa docker_gwbridge como puerta de enlace predeterminada.
10.0.3.0/24 dev eth0 proto kernel scope link src 10.0.3.7      # Usa eth0 para comunicarse con otros hosts de la red overlay traefik-public.
172.18.0.0/16 dev eth1 proto kernel scope link src 172.18.0.4  # usa eth1 para comunicarse con el puente docker_gwbridge.
 
# Segundo contenedor Nginx.
docker exec -it 0eea3d6df6c1 ip --details addr
 
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    ...
# interfaz "30: veth2@if29" del espacio de nombres de red perteneciente a la red overlay traefik-public.
29: eth0@if30: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UP group default # veth2@if29 (overlay traefik-public) 
    link/ether 02:42:0a:00:03:0e brd ff:ff:ff:ff:ff:ff link-netnsid 0
    veth numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535 
    inet 10.0.3.14/24 brd 10.0.3.255 scope global eth0
       valid_lft forever preferred_lft forever
# Interfaz "32: veth8e0d606@if31"  del host, la cual está vinculada a docker_gwbridge para salir a Internet.
31: eth1@if32: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default # veth8e0d606 (docker_gwbridge)
    link/ether 02:42:ac:12:00:05 brd ff:ff:ff:ff:ff:ff link-netnsid 1
    veth numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535 
    inet 172.18.0.5/16 brd 172.18.255.255 scope global eth1
       valid_lft forever preferred_lft forever
 
ip route
default via 172.18.0.1 dev eth1                                # Usa docker_gwbridge como puerta de enlace predeterminada.
10.0.3.0/24 dev eth0 proto kernel scope link src 10.0.3.9      # Usa eth0 para comunicarse con otros hosts de la red overlay traefik-public.
172.18.0.0/16 dev eth1 proto kernel scope link src 172.18.0.5  # usa eth1 para comunicarse con el puente docker_gwbridge.

Artículos de interés:

Entendiendo como funciona las red ingress en Docker Swarm

Cuando se inicializa un enjambre o se une un nuevo host a un enjambre Docker existente, se crean dos nuevas redes en ese host Docker, la red ingress (overlay) y la red docker_gwbridge de la cual hablamos en el apartado anterior. Cuando se crea un servicio de enjambre y no se conecta a una red superpuesta definida por el usuario, se conecta a la red ingress de manera predeterminada.

La red supuesta ingress maneja el tráfico de control y de datos relacionado con los servicios del enjambre. Docker swarm utiliza la red ingress para exponer los servicios a la red externa y proporcionar la malla de enrutamiento. Como sabemos esa malla de enrutamiento ofrece balanceo de carga, esto es manejado a nivel de kernel (IPVS) por un contenedor que docker swarm lanza por defecto denominado “ingress_sbox”.

El contenedor oculto ingress_sbox (también denominado contenedor tipo sandbox) tiene un interfaz virtual conectado al espacio de nombres de red de la red superpuesta “ingress” y otra única al host para usar la red docker_gwbridge.

Como se comentaba al principio del apartado anterior, se dispone de un clúster docker swarm donde corre un servicio “traefik” únicamente en un nodo manager y un servicio “Nginx” compuesto de dos contenedores en el host nodo4. Traefik crea su propia red overlay llamada traefik-public, a la que otros servicios deben estar conectados si quieren usar la funcionalidad que traefik ofrece. La funcionalidad principal de Traefik es la de un proxy reverso, enruta las peticiones según dominio a un determinado servicio dentro del enjambre. En otras palabras, facilita usar un mismo puerto, por ejemplo 443, para un sin fin de servicios que corran en un clúster docker swarm, cada uno con su dominio, certificado SSL, etc. De no usar traefik u otro contenedor con similar finalidad, los entornos swarm siempre enrutarían las peticiones a los puertos 80 y 443 a los mismos servicios.

En dicho entorno, cuando un host del clúster recibe una petición, por ejemplo https a un determinado dominio, el clúster swarm lo enviará al host donde esté traefik, esté en el nodo que esté (nodo1 en nuestro entorno). Naturalmente Traefik ha sido previamente configurado en el enjambre como el servicio que responde a los puertos 80 y 443 del clúster. En cuanto el contenedor Traefik reciba la petición https, que puede venir desde cualquier host, según el dominio seleccionará el servicio correspondiente y lo reenviará al host que tiene el contenedor Nginx correspondiente. Traefik también haría balanceo de carga si hubiera más de un contenedor.

En el ejemplo práctico se usará el nodo4 para ejecutar los comandos. Este nodo4 no tiene el contenedor traefik, por lo que si este host nodo4 recibe una solicitud http/https, docker swarm lo que hará es enviar al contenedor traefik (que está en el host nodo1) y enviarle la petición. Posteriormente traefik enviará de nuevo al nodo4 en este caso la petición http a uno de los contenedores nginx que ahí corren.

Lo único que nos interesa para el artículo es como la red ingress recibe la petición en un nodo que no es donde se encuentra el servicio necesario (traefik) y como se termina encaminando al contenedor traefik en otro host. Para poder seguir mejor el ejemplo se debe consultar la salida de los comandos en la sección anterior.

Espacio de nombres de red perteneciente a la red ingress (1-pdfaswed39): Leer el apartado anterior donde se vinculan redes docker con nombres de espacio de red.

ip netns exec 1-pdfaswed39 ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
2: br0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UP group default 
    link/ether 0e:4a:86:9e:51:e9 brd ff:ff:ff:ff:ff:ff
    inet 10.0.0.1/24 brd 10.0.0.255 scope global br0
       valid_lft forever preferred_lft forever
10: vxlan0@if10: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue master br0 state UNKNOWN group default 
    link/ether 26:08:27:43:18:96 brd ff:ff:ff:ff:ff:ff link-netnsid 0
12: veth0@if11: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue master br0 state UP group default 
    link/ether 0e:4a:86:9e:51:e9 brd ff:ff:ff:ff:ff:ff link-netns ingress_sbox # Interfaz virtual cuyo par reside en una interfaz del contenedor ingress_sbox.
 

En nuestro caso como ya explicamos, todas las peticiones https/https deben ser enviadas al nodo1. Si el nodo4 recibe una petición http/https, mediante la interfaz vlanx será enviadas a nodo1. Es decir, primeramente se comunican nodo4 y nodo1 por medio de la red superpuesta ingress para que el contenedor traefik (nodo1) reciba la consulta http/https. recibida en el nodo4. Posteriormente el contenedor traefik en el nodo1 se comunicará por medio la red traefik-public con el host nodo4 para enviar la peticione https/https al servicio nginx.

Analizar la configuración de red del contenedor ingress_sbox por medio de su espacio de nombres de red.

ip netns exec ingress_sbox ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
11: eth0@if12: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UP group default # Unido al espacio de nombres de red de ingress "12: veth0@if11"
    link/ether 02:42:0a:00:00:10 brd ff:ff:ff:ff:ff:ff link-netns 1-pdfaswed39
    inet 10.0.0.16/24 brd 10.0.0.255 scope global eth0
       valid_lft forever preferred_lft forever
    inet 10.0.0.3/32 scope global eth0
       valid_lft forever preferred_lft forever
13: eth1@if14: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default # Unido al interfaz del host host "14: veth09fa560" para hacer uso de docker_gwbridge.
    link/ether 02:42:ac:12:00:02 brd ff:ff:ff:ff:ff:ff link-netnsid 1
    inet 172.18.0.2/16 brd 172.18.255.255 scope global eth1
       valid_lft forever preferred_lft forever

Como se comentó con anterioridad y puede deducirse actualmente, la red docker_gwbridge funciona como un switch que permite interconectar todas las redes del host. Cuando el host nodo4 recibe una petición http por su interfaz habitual, este la enruta al contenedor ingress_sbox mediante la interfaz conectada a docker_gwbridge 172.18.0.2. Esta interfaz será usada también si los contenedores de este host quieren salir a internet, pero NO es usada si los dos contenedores nginx de este host conectan entre ellos o con el contenedor traefik del host nodo1.

Listar la configuración de las tablas nat en el host (nodo4) y prestar atención a la tabla nat DOCKER-INGRESS. La IP 172.18.0.2 coincide con la interfaz eth1@if14 de ingress_sbox usada en la red docker_gwbridge. Es decir, el tráfico entrante a este host encaminado a los puertos 80 y 443 son enviados (mediante su interfaz en docker_gwbridge) al contenedor “ingress_sbox”.

iptables -t nat -nvL
....
 
Chain DOCKER-INGRESS (2 references)
 pkts bytes target     prot opt in     out     source               destination         
    0     0 DNAT       tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            tcp dpt:443 to:172.18.0.2:443
    0     0 DNAT       tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            tcp dpt:80 to:172.18.0.2:80
 1457  122K RETURN     all  --  *      *       0.0.0.0/0            0.0.0.0/0

Ahora se procede a listar las tablas NAT pero del espacio de nombres de red del contenedor ingress_sbox y nos fijamos en la tabla POSTROUTING. La IP 10.0.0.16 coincide con la interfaz virtual eth0@if12 vinculada a la red superpuesta overlay. ipvs se usa para el balanceo de carga.

ip netns exec ingress_sbox iptables -nvL -t nat                                                                                                                                     
...
Chain POSTROUTING (policy ACCEPT 0 packets, 0 bytes)
 pkts bytes target              prot opt in     out     source               destination         
    0     0 DOCKER_POSTROUTING  all  --  *      *       0.0.0.0/0            127.0.0.11          
    0     0 SNAT                all  --  *      *       0.0.0.0/0            10.0.0.0/24   ipvs to:10.0.0.16
...

Dentro del mismo espacio de nombres de red del contenedor ingress_sbox se visualiza la configuración de la tabla mangle. Esta tabla mangle se utiliza para manipular paquetes, especificando cabeceras para los paquetes IP que afectan a las decisiones de enrutamiento posteriores. Aquí se marcan los paquetes “MARK set 0x104”.

ip netns exec ingress_sbox iptables -nvL -t mangle
Chain PREROUTING (policy ACCEPT 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination         
    0     0 MARK       tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            tcp dpt:80 MARK set 0x104
    0     0 MARK       tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            tcp dpt:443 MARK set 0x104
 
Chain INPUT (policy ACCEPT 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination         
    0     0 MARK       all  --  *      *       0.0.0.0/0            10.0.0.3             MARK set 0x104

El valor hexadecimal 0x104 en decimal equivale a 260. Si ahora se visualliza la configuración IPVS del espacio de red usado por el contenedor ingress-sbox, se ve como el identificador adjudicado en la tabla mangle está presente, el cual indica que debe enviar la petición a la IP 10.0.0.4. Esa IP es donde se encuentra el contenedor traefik, que en este entorno de prueba es el encargado de gestionar todas las peticiones entrantes a los servicios.

ip netns exec ingress_sbox ipvsadm -ln
IP Virtual Server version 1.2.1 (size=32768)
Prot LocalAddress:Port Scheduler Flags
  -> RemoteAddress:Port           Forward Weight ActiveConn InActConn
FWM  260 rr
  -> 10.0.0.4:0                   Masq    1      0          0 

Como el contenedor traefik se encuentra en otro host (nodo1), se usa la red overlay traefik-public (interfaz vxlan) para conectar con el otro host como vimos anteriormente.

Diagrama simple del funcionamiento de redes docker_gwbridge, traefik-public y ingress en los hosts nodo1 y nodo4.

NODO1                                        .                                   NODO4  
                                             .
                                             .                                                
             Red docker_gwbridge             .               Red docker_gwbridge                              
             +-------------------+           .               +------------------+       
             |                   |           .               |                  |             
             |                   |           .               |                  |             
      +------+------------+------+-----------.--------+------+-----------+------+---- Red ingress 
      |      |            |      |           .        |      |           |      |             
      |      |            |      |           .        |      |           |      |             
   +--+------+-----+   +--+------+-----+     .     +--+------+-----+  +--+------+-----+       
   | ingress_sbox  |   |   traefik     |     .     |     NGX1      |  |     NGX2      |       
   +---------------+   +-------+-------+     .     +-------+-------+  +-------+-------+       
                               |             .             |                  |               
                               |             .             |                  |               
                               |             .             |                  |               
                               |             .             |                  |               
                               +-------------.-------------+------------------+------ Red traefik-public
                                             .                               

Bloquear el entorno Swarm (autolock: cifrado de llaves privada)

Si no se tiene claro qué es lo que pasa al inicializar un enjambre docker se recomienda leer la sección “Entendiendo qué pasa internamente al iniciallizar un enjambre Docker

El almacén completo de raft como sabemos es el mismo en todos los nodos gestores, se propaga a todos los gestores cifrado únicamente a través de mTLS. Cada nodo gestor debe tener una copia idéntica de los registros, que se encuentra en la siguiente ruta /var/lib/docker/swarm/raft/. Estos registros están cifrados, ya que almacenan datos sensibles como los secretos de Docker utilizados por los servicios que se ejecutan en el clúster de Swarm.

Las llaves TLS/DEK están en la ruta /var/lib/docker/swarm/certificates/swarm-node.key.

Como ya se comentó, este fichero swarm-node.key tiene dos llaves privadas, la llave DEK en forma de cabecera en formato PEM y la llave TLS.

Raft DEK (Data Encrypting Key)

  • Uso: Cifra el registro Raft.
  • Única por nodo gestor.
  • Se almacena como una cabecera PEM en la clave TLS que se almacena en /var/lib/docker/swarm/certificates/swarm-node.key.
  • Se genera cuando el gestor se inicializa por primera vez, o en la rotación.
  • Cifrado: No está cifrada por defecto pero se cifra al usar el bloqueo del enjambre.
  • Rotación: Esta lave siempre rota cuando el clúster pasa de no autobloqueado a autobloqueado.
  • Eliminación: Cuando un gestor es degradado o abandona el clúster

Llave TLS

  • Uso: Cifra y autentica la comunicación a través de mTLS.
  • Única por nodo gestor.
  • Se almacena en /var/lib/docker/swarm/certificates/swarm-node.key y actualmente tiene le formato pkcs8.
  • Se genera cuando el nodo se une al enjambre por primera vez, o al renovar el certificado.
  • Cifrado: No está cifrada por defecto pero se cifra al usar el bloqueo del enjambre.

Estas dos llaves son cifradas si se usa el bloqueo del clúster, esto se realiza mediante otra llave, la denominada “Manager unlock key”

Manager unlock key

  • Uso: Cifra las llaves DEK y TLS (actúa como una KEK, o clave de cifrado).
  • No es única por nodo gestor: la comparten todos los gestores del clúster.
  • Se almacena en el resgitro Raft (así se propaga a todos los gestores).
  • Se genera cuando el clúster se pone en autolock, o cuando la llave de desbloqueo se rota.
  • Cifrado: Como el resto del registro Raft, al enviarse se usa TLS para cifrar la comunicación y vía DEK cuando está en reposo sobre el sistema de ficheros.
  • Se puede rotar a través de la API.
  • Se elimina cuando se desactiva el bloqueo automático.

Fichero swarm-node.key sin autolock.

NO cifrado
-----BEGIN PRIVATE KEY-----
kek-version: 300855
raft-dek: EiC9cFBOPxkZDl8VqRI/QsJ1ydTtUkcyeO/1loOZD+Ty6w==
 
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgBuGLymWVcAdlsVR0
q8n+urqIPnFu+jscKNXoY7lkhhKhRANCAAT63Va7/TnEMZv4d2EPMnQsG61DKl1e
Heo+CCIobsR6srfxMGyzFTxA5kbxgD24dJL+1pS/c8iZxmPbPAuLdr6e
-----END PRIVATE KEY-----

Fichero swarm-node.key con autolock.

cifrado
-----BEGIN ENCRYPTED PRIVATE KEY-----
kek-version: 250979
raft-dek: CAESMHie2K4PvoYXxTHVO2nvduabxZg9cSEKQAn/pr8kGXe8pYKwM0RlVlBmjSRzLzYm4hoYEdkND+C+RbqXfoO5Nlqja80xyw6vNtDq
 
MIHeMEkGCSqGSIb3DQEFDTA8MBsGCSqGSIb3DQEFDDAOBAiLTl7MCLK0CAICCAAw
HQYJYIZIAWUDBAEqBBC3SVE2J6Hp/E5cUp31Q6Z8BIGQejzdNjlB6kGLWfeO+YOW
U4fOaEuwGyBbihnsG4qkbDAJmp5qLJa8VhPhBzlPsmtswVTFpSmV9zD0TOuPPXAG
DavgVl+k5y0nCCHUW/dokackUB2QNH+V/SbDrQEB8XA+uG7dxVzdI6M4x6Nz6X/s
pwX17y2X6+/BYw0ERv7AfsfEwrMus6LmHHxmi3pvDAAg
-----END ENCRYPTED PRIVATE KEY-----

Los registros de Raft utilizados por los gestores de enjambres están cifrados en el disco por defecto. Si la llave está accesible desde el sistema de ficheros, se puede acceder a los datos del registro Raft. El bloqueo lo que hace es cifrar dicha llave y por lo tanto exige descifrarla en cada inicio del enjambre o al reiniciar nodos manager. Esto significa que si uno de los nodos gestores ha sido comprometido a nivel de sistema de fichero y no uso autoblock, es decir, no tiene su fichero swarm-node.key cifrado. Entonces es posible descifrar y leer los registros de Raft para obtener los secretos de Docker, entre otra información sensible.

Si el atacante tiene acceso a root, lógicamente siempre podrá consultar lo que quiera, desde los secretos a las llaves privadas, independientemente de la configuración autoblock. El bloqueo tiene como finalidad defenderse de accesos al sistema de ficheros, por ejemplo mediante a un acceso a backups/snapshots del disco.

# Incializar un entorno swarm bloqueando el entorno. (cifrando las llaves privadas TLS y DEK de swarm-node.key). 
docker swarm init --autolock
 
# Inicializar el bloqueo a un entorno ya inicializado.
docker swarm update --autolock=true
 
Mostrar la Manager unlock key (lógicamente solo funciona cuando esté el bloqueo activado pero el entorno está desactivado).
# docker swarm unlock-key
 
# A partir del momento en que esté bloqueado, cualquier comando de gestión nos avisará de que es necesario desbloquear el enjambre.
systemctl restart docker
docker node ls
Error response from daemon: Swarm is encrypted and needs to be unlocked before it can be used. Please use "docker swarm unlock" to unlock it.
 
# Desbloquear.
docker swarm unlock
Please enter unlock key:
 
# Rota las llaves de bloqueo.
docker swarm unlock-key --rotate
# Rota la llave CA del clúster. No está directamente relacionado con el tema de unlock, pero obliga a cambiar los certificados, llaves de los nodos y por supuesto se generan nuevos tokens.
docker swarm ca --rotate
 
# Desactiva el bloqueo.
docker swarm update --autolock=false

Si se quiere realizar un volcado del registro Raft puede usarse swarm-rafttool (swamkit): https://github.com/docker/swarmkit

guia_rapida_de_docker_swarm.txt · Last modified: 2021/06/06 02:14 by busindre