Mise en place d'une infrastructure de démonstration

Introduction

La mise en place de mon infrastructure de démonstration s'est réalisée en plusieurs phases :

  • 2018-2019 : mise en place d’une première infrastructure de démonstration et de développement pour le portail UCLouvain
  • janvier 2022 : passage en production de l’infrastructure Docker Elastic Search des bibliothèques basée sur cette première infrastructure de démonstration
  • avril 2022 : mise en place d’une nouvelle infrastructure de démonstration afin de tenir compte des évolutions de Docker depuis 2019
  • juin 2022 : dérivation de l’infrastructure dckr.sisg pour les besoins de production du nouveau portail UCLouvain

L’objectif était d’expérimenter les possibilités de Docker et d’étudier son adéquation avec les besoins des futures infrastructures web, en profitant des opportunités offertes par le développement du nouveau portail UCLouvain.

Infrastructure à déployer

L’infrastructure à déployer mettra à disposition un swarm Docker pour héberger des applications web.

schema de l'infrastructure

Éléments constitutifs de l'infrastructure :

  • Pool de managers : machines qui assurent la gestion du swarm et servent les applications de gestion, de monitoring…
  • Pool de workers : machines qui servent les applications exécutées sur le swarm

À cela s'ajoute le load balancer/Proxy (géré par SIPR) qui permet la répartition de charge entre les machines du swarm, redirige les requêtes sur les bonnes machines en fonction du fqdn et assure également la liaison sécurisée https avec les machines clientes

Dimensionnement des machines virtuelles et points de montage

Il y a 3 machines virtuelles pour mon infrastructure.

Voici leur spécification technique.

Manager (1 machine)

  • 4 coeurs
  • RAM 16G
  • Disques :
    • / 20G
    • /home 20G
    • /dockerdata-ceph 40G
    • /var/lib/docker 40G
    • /var/lib/docker.bk 10G
  • OS : Debian 11
  • Doit pouvoir faire des requêtes vers l'extérieur sur les ports
    • 80, 443 (git vers github)
    • 22 (git via ssh sur gitlab.sisg.ucl.ac.be et forge.uclouvain.be)
    • 11371 (key server)

Notes :

  • Le point de montage /dockerdata-ceph est destiné à contenir les fichiers partagés entre les services déployés sur l'infrastructure et qui seront montés en tant que volumes. Ces données devront être disponibles sur tous les noeuds du cluster et seront dès lors exportées depuis le manager via NFS.
  • Le point de montage /var/lib/docker est destiné au stockage des données propres à Docker
  • L'usage a montré que le répertoire /var/lib/docker.bk pouvait grandir rapidement et consommer de l'espace sur le système de fichier racine de la machine. J'ai donc décidé de l'externaliser via un point de montage séparé.
  • Le choix de Debian 11 était le choix fait à l'origine, mais il n'est en rien obligatoire. Les étapes décrites dans ce chapitre fonctionnent sur n'importe qu'elle distribution basée sur Debian (par exemple Ubuntu) et, avec un peu d'adaptation, sur n'importe quelle distribution Linux. Pour des infrastructures plus récentes, j'ai tendance à lui préférer Ubuntu dans sa dernière version LTS (la 22.04 au moment d'écrire ce texte) qui offre une plus grande stabilité et des paquets plus régulièrement mis à jour que Debian.

Workers (2 machines)

  • 4 coeurs
  • RAM 16G
  • Disques :
    • / 20G
    • /home 20G
    • /var/lib/docker 40G
    • /var/lib/docker.bk 10G
  • OS : Debian 11
  • Doivent pouvoir faire des requêtes vers l'extérieur sur les ports
    • 80, 443

Setup inital

Voici les étapes préliminaires nécessaires avant de pouvoir installer Docker sur les machines virtuelles.

Génération de la locale fr_BE.UTF-8

Sur les machines Debian 11 fournies par SIPR, la locale par défaut fr_BE.UTF-8 n'est pas générée. Cela provoque l'affichage d'avertissements lors de l'exécution de nombreuses commandes.

Pour y remédier, il suffit de générer cette locale :

sudo dpkg-reconfigure locales
# Sélectionner fr_BE.UTF-8 dans la liste (avec flèches pour naviguer puis espace pour sélectionner)
# Sélectionner fr_BE.UTF-8 comme locale par défaut (avec flèches pour naviguer puis espace pour sélectionner)
# Choisir OK pour générer les locales

Note : l'idéal serait que la locale fr_BE.UTF-8 soit intégrée dans les templates des images Debian de SIPR.

Configuration du proxy (optionnel)

Si les machines ne sont pas sur le bon gateway, ou si elles ne sont pas encore autorisées à sortir sur les ports nécessaires sur le load balancer HAProxy de SIPR, il peut être nécessaire de configurer le passage par le proxy HTTP(S) des Data Center afin d'accéder à l'Internet et pouvoir installer les paquets requis. Ce proxy doit être configuré pour différentes applications.

Note : Attention toutefois que pour l'utilisation de Docker, les machines devront être capables de faire des requêtes vers l'extérieur sur les ports 22 et 11371 qui ne sont pas pris en charge par le proxy du Data Center. Configurer le proxy HTTP(S) permet toutefois l'installation des paquets nécessaires sur les machines en attendant que la configuration sur le load balancer HAProxy de SIPR soit terminée.

Apt (normalement pas nécessaire)

Normalement le proxy est déjà configuré pour le gestionnaire de paquets apt dans les images Debian 11 de SIPR.

Pour le vérifier :

cat /etc/apt/apt.conf.d/01proxy
# Doit contenir les lignes suivantes :
# Acquire::http::Proxy "http://proxy.sipr.ucl.ac.be:889/";
# Acquire::https::Proxy "http://proxy.sipr.ucl.ac.be:889/";

Si ce n'est pas le cas, il suffit de créer le fichier /etc/apt/apt.conf.d/01proxy :

# vim /etc/apt/apt.conf.d/01proxy
Acquire::http::Proxy "http://proxy.sipr.ucl.ac.be:889/";
Acquire::https::Proxy "http://proxy.sipr.ucl.ac.be:889/";

Wget

Contrairement à curl qui utilise les variables d’environnement pour le proxy, wget utilise sa propre configuration. Il faut ajouter le proxy au fichier /etc/wgetrc.

Les wildcards n'étant pas supportées par la configuratuion, il est nécessaire de scripter l'ajout des IP de machine du sous-réseau de mon infrastructure echo 10.1.4.{1..255},.

# vim /etc/wgetrc
https_proxy = http://proxy.sipr.ucl.ac.be:889/
http_proxy = http://proxy.sipr.ucl.ac.be:889/
no_proxy = `echo 10.1.4.{1..255},`localhost,127.0.0.1,.sipr-dc.ucl.ac.be

Bash

Les wildcards n'étant pas supportées par Bash, il est nécessaire de scripter l'ajout des IP de machine du sous-réseau de mon infrastructure echo 10.1.4.{1..255},.

vim ~/.bashrc
# ajouter les lignes suivantes à la fin du fichier :
export http_proxy='proxy.sipr.ucl.ac.be:889'
export https_proxy='proxy.sipr.ucl.ac.be:889'
export no_proxy=`echo 10.1.4.{1..255},`localhost,127.0.0.1,.sipr-dc.ucl.ac.be
# recharger le fichier .bashrc
. ~/.bashrc

Installation des paquets de base

Installation des paquets requis pour la suite des opérations.

# Mise à jour de la liste des paquets
sudo apt update
# Mise à jour de paquets déjà installés
sudo apt upgrade
# Installation des nouveaux paquets
sudo apt install \
    ca-certificates \
    gnupg \
    lsb-release \
    vim \
    htop \
    ansible \
    git

Configuration du proxy pour git

Il reste encore à configurer git pour utiliser le proxy

# Soit de manière globale
git config --global http.proxy http://proxy.sipr.ucl.ac.be:889
# Soit pour le projet courant uniquement
git config http.proxy http://proxy.sipr.ucl.ac.be:889

Ajouts des groupes "admin"

Par facilité, on peut ajouter l'utilisateur courant, ainsi que les autres utilisateurs amenés à gérer la machine, aux groupes adm et staff afin d'éviter de devoir utiliser sudo pour certaines actions. Le groupe adm donne accès aux logs, le groupe staff permet de modifier le contenu du répertoire /usr/local

# Accès aux logs
sudo usermod -aG adm $USER
# Accès à /usr/local
sudo usermod -aG staff $USER

Il faudra se déconnecter et se reconnecter à la session pour que cela soit actif ou utiliser la commande newgrp.

Installation et configuration de Docker

Cette section constitue un mode d’emploi pour la mise en place d’une infrastructure Docker, Certaines opérations sont à réaliser sur tous les types de noeuds du cluster alors que certaines sont spécifiques aux noeuds managers ou workers.

Installation de l'environnement Docker (tous les nœuds)

La procédure d'installation des paquets de Docker est simplement celle décrite sur le site officiel :

# Suppression des versions précédentes de Docker
sudo apt remove docker docker-engine docker.io containerd runc

# Obtention de la clé GPG du dépôt des paquets de Docker
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/debian/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg

# Ajout du dépôt
echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian \
  $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
  
# Mise à jour de la liste des paquets
sudo apt update
# Installation des paquets Docker
sudo apt install docker-ce docker-ce-cli containerd.io docker-compose-plugin

Configuration de Docker (tous les nœuds)

Une fois Docker installé, il reste à le configurer correctement.

Créer un fichier pour systemctl

Par défaut, Docker se lance avec une série d'options que je veux modifier dans mon infrastructure. De plus j'aimerais fournir une partie de ces options via le fichier /etc/docker/daemon.json. Or certaines de ces options risques d'entrer en conflit avec les options par défaut de Docker et le daemon refusera de se lancer.

Pour contourner ce problème, il suffit de modifier les options par défaut passées au service Docker par systemd.

Cela se fait relativement simplement en créant un fichier d'override de configuration pour le service docker.service dans lequel je vais retirer toutes les options passées au daemon Docker (puisqu'elles seront définies dans mon fichier daemon.json). Cela peut se faire soit à la main, soit en utilisant systemctl, l'outil de gestion de systemd.

À la main

sudo mkdir -p /etc/systemd/system/docker.service.d

Ajouter le fichier docker.conf :

# vim /etc/systemd/system/docker.service.d/docker.conf
[Service]
ExecStart=
ExecStart=/usr/bin/dockerd

Avec systemctl

sudo systemctl edit docker.service

Ajouter le code suivant dans la zone prévue en début de fichier :

[Service]
ExecStart=
ExecStart=/usr/bin/dockerd

Configurer Docker via daemon.json

Configuration de base dans daemon.json, sudo vim /etc/docker/daemon.json :

{
  "storage-driver": "overlay2",
  "log-driver": "local",
  "debug": false
}

Note : Les options du driver local pour les logs sont décrites dans la documentation de Docker

Activer l'accès à l'API du daemon Docker via tcp

Deux ports sont possibles : 2375 (HTTP uniquement) ou 2376 (HTTP avec ou sans TLS).

Dans /etc/docker/daemon.json ajouter la configuration des sockets pour dockerd :

{
  /* ... */
  hosts: ["unix:///var/run/docker.sock", "tcp://0.0.0.0:2376"]
}

Notes :

  • L'utilisation de tcp://0.0.0.0:2376 n'est en théorie pas sécurisée car elle permet à n'importe quelle machine de parler au daemon Docker. Néanmoins, dans le cadre de mon infrastructure, cela ne pose pas de problème, puisqu’il est facile de limiter les accès aux machines du cluster elles-mêmes via des règles de firewall. j’ajouterais également, que le port 2376 n'étant accessible que via le VLAN des noeuds Docker, les risques sont assez réduits surtout en comparaison de la simplification qu'ils permettent dans la gestion du cluster.
  • Je n'ai pas activé le TLS sur le port 2376 à ce stade et ce principalement pour 2 raisons : il n'est pas nécessaire pour les raisons citées plus haut (VLAN + firewall), il requiert un certificat pour lequel il me semble préférable d'attendre qu'une CA "interne" aux Data Center SIPR soit disponible (l'alternative étant un certificat auto-signé).
Configurer un registry non sécurisé

Par défaut, Docker refuse d'utiliser un registry qui ne serait pas accessible via HTTPS. Il est toutefois possible de définir des regitries non sécurisés directement dans la configuration du daemon.

Dans /etc/docker/daemon.json ajouter la configuration du registry (l'installation du registry est décrite plus loin) :

{
  /* ... */
  "insecure-registries" : [ "private-registry:5000" ]
}
Configurer les métriques pour le monitoring

Dans /etc/docker/daemon.json ajouter la configuration pour l'accès aux métriques via une API REST. Cet accès sera nécessaire pour la mise en place du monitoring :

{
  /* ... */
  "metrics-addr" : "127.0.0.1:9323",
  "experimental" : true,
  /* ... */
}

Note : la question du monitoring sera abordée dans le chapitre Perspectives

Mon fichier daemon.json complet

Voici le fichier daemon.json complet :

{
  "storage-driver": "overlay2",
  "log-driver": "local",
  "debug": false,
  "metrics-addr" : "127.0.0.1:9323",
  "experimental" : true,
  "insecure-registries" : [ "private-registry:5000" ],
  "hosts": ["unix:///var/run/docker.sock", "tcp://0.0.0.0:2376"]
}

Relancer le daemon Docker

Recharger la configuration de systemd sudo systemctl daemon-reload et relancer docker sudo systemctl restart docker.

Puis relancer Docker : sudo systemctl restart docker

Configurer le proxy (optionnel)

Note : ces étapes ne sont nécessaires que si les machines n'ont pas un accès direct vers l'extérieur sur les ports 80 et 443. Cela ne suffira toutefois pas pour pouvoir construire certaines images Docker qui nécessite soit un accès aux serveurs de clés publiques via le port 11371 ou un accès à git via ssh (port 22).

Options de Docker sudo vim /etc/default/docker :

# Docker Upstart and SysVinit configuration file

#
# THIS FILE DOES NOT APPLY TO SYSTEMD
#
#   Please see the documentation for "systemd drop-ins":
#   https://docs.docker.com/engine/admin/systemd/
#

# Customize location of Docker binary (especially for development testing).
#DOCKERD="/usr/local/bin/dockerd"

# Use DOCKER_OPTS to modify the daemon startup options.
#DOCKER_OPTS="--dns 8.8.8.8 --dns 8.8.4.4"
DOCKER_OPTS="--config-file=/etc/docker/daemon.json"

# If you need Docker to use an HTTP proxy, it can also be specified here.
export http_proxy="http://proxy.sipr.ucl.ac.be:889
export https_proxy="http://proxy.sipr.ucl.ac.be:889
export no_proxy=`echo 10.1.4.{1..255},`,localhost,127.0.0.1,.sipr-dc.ucl.ac.be

# This is also a handy place to tweak where Docker's temporary files go.
#export DOCKER_TMPDIR="/mnt/bigdrive/docker-tmp"

Créer le répertoire de configuration de Docker pour systemd

sudo mkdir /etc/systemd/system/docker.service.d

Créer le fichier de configuration du proxy /etc/systemd/system/docker.service.d/http_proxy.conf

[Service]
# NO_PROXY is optional and can be removed if not needed
Environment="HTTP_PROXY=http://proxy.sipr.ucl.ac.be:889" "HTTPS_PROXY=http://proxy.sipr.ucl.ac.be:889" "NO_PROXY=localhost,127.0.0.0/8,10.1.4.1/24,*.sipr-dc.ucl.ac.be"

Recharger les configurations de systemd via sudo systemctl daemon-reload

Vérifier que les options sont bien prises en compte sudo systemctl show docker --property Environment

Redémarrer docker : sudo systemctl restart docker

Ajout de l'utilisateur courant au groupe Docker (tous les nœuds)

Par facilité, on ajoute l'utilisateur courant au groupe docker afin d'éviter d'avoir à utiliser sudo pour exécuter les commandes Docker :

# Ajout de l'utilisateur au groupe docker
sudo usermod -aG docker $USER

Pour appliquer ce changement, il reste alors à recharger la session, soit en quittant et en se reconnectant, soit via la commande newgrp :

# Rechargement de la session pour tenir compte du changement
newgrp docker

Note : newgrp lance une nouvelle session du shell au sein de la session actuelle. Il faudra donc utiliser la commande exit deux fois pour quitter la session.

Installation de Docker Compose (managers uniquement)

L'outil Docker Compose nous permettra d'effectuer le build des images Docker sur le manager. Il permettra également de déployer des stacks localement sur les managers si nécessaire.

curl -L "https://github.com/docker/compose/releases/download/v2.5.0/docker-compose-$(uname -s)-$(uname -m)" -o ./docker-compose
# Installer docker-compose
sudo chmod +x docker-compose
sudo mv docker-compose /usr/local/bin/.
# Commande alternative pour 
sudo install -o root -g root -m 0755 docker-compose /usr/local/bin/docker-compose
# Vérifier que l'installation s'est bien passée
docker-compose -v

Si l'utilisateur est déjà dans le groupe staff, il suffit d'exécuter

curl -L "https://github.com/docker/compose/releases/download/v2.5.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
chmod +x /usr/local/bin/docker-compose
docker-compose -v

Architectures possibles :

  • Un swarm de N noeuds tenant à la fois le rôle d'hôte docker et de noeud glusterfs (c'est à dire /data/.../vol-1 et /mnt/vol-1 sur chacun des noeuds)
  • Un swarm de N noeuds couplé à un cluster gluster de M noeuds

Script pour automatiser l'installation et l'update

#!/bin/bash

# Get latest docker compose released tag
COMPOSE_VERSION=$(curl -s https://api.github.com/repos/docker/compose/releases/latest | grep 'tag_name' | cut -d\" -f4)
# Continue ?
echo "Will install docker-compose versin ${COMPOSE_VERSION}"
read -n 1 -s -r -p "Press any key to continue [ctrl-c to abort]"
# Get docker-compose binary
curl -L "https://github.com/docker/compose/releases/download/${COMPOSE_VERSION}/docker-compose-$(uname -s)-$(uname -m)" -o ./docker-compose.${COMPOSE_VERSION}
# Install docker-compose
sudo install -o root -g root -m 0755 docker-compose.${COMPOSE_VERSION} /usr/local/bin/docker-compose
# Cleanup
rm docker-compose.${COMPOSE_VERSION}
# Output compose version
docker-compose -v

exit 0

Installation en tant que plugin Docker

Dans les versions récentes de Docker, Docker Compose est disponible sous la forme d'un plugin pour l'outil en ligne de commande :

sudo apt install docker-compose-plugin

Son utilisation est identique à celle de Docker Compose en version standalone, seule la manière d'appeler la commende change : de docker-compose <command> <option> elle devient docker compose <command> <option>.

Note : L'installation standalone permet d'avoir une version plus à jour de Docker Compose

  • docker compose version : Docker Compose version v2.17.3
  • docker-compose version : Docker Compose version v2.18.1

De plus la version du plugin est maintenue par les mainteneurs des paquets Docker pour les différentes distributions Linux.

Déploiement du cluster Docker Swarm

Le déploiement du swarm se fait en plusieurs étapes :

  1. activation du swarm et création du premier manager
  2. (optionnel) ajout de managers supplémentaires
  3. ajout des workers
  4. (optionnel) promotion de workers en managers

L'exemple suivant est basé sur une infrastructure avec 5 noeuds afin de donner une vue plus complète de l'initialisation d'un Swarm Docker. Dans le cas de mon infrastructure de démonstration, je suis directement passé à l'ajout des workers après l'initialisation du Swarm et la création du premier manager.

Activation du swarm et création du premier manager

Pour activer le swarm et créer le premier manager, il suffit d'exécuter la commande suivante :

# Initialisation du swarm
docker swarm init --advertise-addr <IP_DU_MANAGER>
# affiche un résultat du type
# docker swarm join --token <UN_LONG_TOKEN> <IP_DU_MANAGER>:2377

Notes : Le token généré lors de cette commande sera nécessaire pour joindre les autres nœuds au swarm.

La commande docker swarm join-token worker permet d'afficher ce token par la suite.

Après cette opération, on obtient un swarm composé d'un seul nœud qui remplira à la fois le rôle de manager et de worker.

La commande docker nod ls permet de visualiser les neuds du swarm :

# Vérifier l'état du cluster
docker node ls
ID                            HOSTNAME   STATUS    AVAILABILITY   MANAGER STATUS   ENGINE VERSION
kuj58tn3l1ueyuyt4nnjtulip *   manager1   Ready     Active         Leader           20.10.13

Pour un swarm à un seul nœud , comme l'infrastructure de formation du portail, c'est tout ! Le swarm est prêt à être utilisé. Pour un swarm à plusieurs nœuds, il faut maintenant joindre les autres nœuds au cluster.

Ajout des autres noeuds

Une fois le swarm initialisé, il est possible de lui ajouter d'autres nœuds workers ou managers.

Ajout des autres managers

Dans un swarm avec plusieurs managers, il reste à promouvoir les autres managers.

Pour obtenir un consensus sur l'état du cluster, il faut un nombre impair de managers. Afin de combiner redondance et performances, 2 managers supplémentaires sont ajoutés. Ajouter des managers supplémentaires peut se faire de deux manières :

  • En spécifiant le rôle de manager dans la commande en exécutant la commande générée par docker swarm join-token manager sur chacun des managers supplémentaires (voir plus haut)
  • En promouvant les noeuds en manager depuis le premier manager. Cette méthode est décrite plus bas.

Comme ici on sait déjà quels nœuds seront les managers, ils sont ajoutés au swarm directement avec ce rôle en exécutant la commande affichée par docker swarm join-token manager sur chacun d'entre eux :

# Afficher la commande à exécuter sur les workers
docker swarm join-token manager
# Retourne une commande du type à exécuter sur chacun des noeuds manager supplémentaires
docker swarm join --token <UN_LONG_TOKEN> <IP_DU_MANAGER>:2377

On peut alors vérifier l'état du cluster et que les 3 managers sont bien disponibles

# Vérifier l'état du cluster
docker node ls
ID                            HOSTNAME   STATUS    AVAILABILITY   MANAGER STATUS   ENGINE VERSION
kuj58tn3l1ueyuyt4nnjtulip *   manager1   Ready     Active         Leader           20.10.13
0whu7cq053xvrnrrtylrqp52e     manager2   Ready     Active         Reachable        20.10.13
k6h1kd3pd8l5ja9o29jhqmnmf     manager3   Ready     Active         Reachable        20.10.13

Si on ne sait pas encore quels nœuds seront manager ou si on veut donner le rôle de manager à des workers, il est possible de promouvoir un worker. La procédure est décrite plus loin.

Ajout des workers

Il suffit joindre les autres nœuds au swarm en exécutant la commande affichée par docker swarm join-token worker sur chacun d'entre eux :

# Afficher la commande à exécuter sur les workers
docker swarm join-token worker
# Retourne une commande du type à exécuter sur chacun des noeuds worker
docker swarm join --token <UN_LONG_TOKEN> <IP_DU_MANAGER>:2377

On peut vérifier que les noeuds sont bien ajoutés en exécutant la commande docker node ls sur le manager (ici on a ajouté 4 noeuds)

# Vérifier l'état du cluster
docker node ls
ID                            HOSTNAME   STATUS    AVAILABILITY   MANAGER STATUS   ENGINE VERSION
kuj58tn3l1ueyuyt4nnjtulip *   manager1   Ready     Active         Leader           20.10.13
0whu7cq053xvrnrrtylrqp52e     manager2   Ready     Active         Reachable        20.10.13
k6h1kd3pd8l5ja9o29jhqmnmf     manager3   Ready     Active         Reachable        20.10.13
opnn5kj1839pq603m1fbh029i     worker1    Ready     Active                          20.10.13
zqnzuwfmk2argo5eobp4ikegt     worker2    Ready     Active                          20.10.13

A la fin de cette opération, on a donc obtenu un swarm constitué d'un manager et de plusieurs workers comme, par exemple, l'infrastructure de développement du portail. Pour une infrastructure à plusieurs managers, il reste à promouvoir les managers supplémentaires.

Promotion d'un worker en manager (optionnel)

Dans ce scénario, on ajoute les autres nœuds que le manager1 en tant que worker du swarm avec la commande affichée par docker swarm join-token worker :

# Afficher la commande à exécuter sur les workers
docker swarm join-token worker
# Retourne une commande du type à exécuter sur chacun des noeuds worker
docker swarm join --token <UN_LONG_TOKEN> <IP_DU_MANAGER>:2377

Note : ports Docker

  • 2376 : port de l'API Docker
  • 2377 : port de l'API Docker Swarm
  • 5000 : port du registry Docker (machine de build)

L'état du swarm est alors le suivant :

# Afficher l'état du cluster
docker node ls
ID                            HOSTNAME   STATUS    AVAILABILITY   MANAGER STATUS   ENGINE VERSION
kuj58tn3l1ueyuyt4nnjtulip *   manager1   Ready     Active         Leader           20.10.13
0whu7cq053xvrnrrtylrqp52e     manager2   Ready     Active                          20.10.13
k6h1kd3pd8l5ja9o29jhqmnmf     manager3   Ready     Active                          20.10.13
opnn5kj1839pq603m1fbh029i     worker1    Ready     Active                          20.10.13
zqnzuwfmk2argo5eobp4ikegt     worker2    Ready     Active                          20.10.13

Il faut donc promouvoir les nœuds manager2 et manager3 au rôle de manager du swarm. Il suffit pour cela d'exécuter la commande docker promote <NODE> pour chacun de ces nœuds sur le premier manager :

# Promouvoir les autres managers
docker node promote manager2
docker node promote manager3

Il suffit alors de vérifier l'état du swarm avec docker node ls :

# Vérifier l'état du cluster
docker node ls
ID                            HOSTNAME   STATUS    AVAILABILITY   MANAGER STATUS   ENGINE VERSION
kuj58tn3l1ueyuyt4nnjtulip *   manager1   Ready     Active         Leader           20.10.13
0whu7cq053xvrnrrtylrqp52e     manager2   Ready     Active         Reachable        20.10.13
k6h1kd3pd8l5ja9o29jhqmnmf     manager3   Ready     Active         Reachable        20.10.13
opnn5kj1839pq603m1fbh029i     worker1    Ready     Active                          20.10.13
zqnzuwfmk2argo5eobp4ikegt     worker2    Ready     Active                          20.10.13

Remarque sur le statut de Leader des managers

Le Leader est le nœud manager qui a été élu comme référence pour l'état du swarm. Ce Leader n'est pas nécessairement le premier manager ajouté. Cela dépend du consensus trouvé entre les managers.

Par exemple, après un redémarrage des daemon docker sur les hôtes, le statut de Leader a été attribué au manager2 :

# Vérifier l'état du cluster
docker node ls
ID                            HOSTNAME   STATUS    AVAILABILITY   MANAGER STATUS   ENGINE VERSION
kuj58tn3l1ueyuyt4nnjtulip *   manager1   Ready     Active         Reachable        20.10.13
0whu7cq053xvrnrrtylrqp52e     manager2   Ready     Active         Leader           20.10.13
k6h1kd3pd8l5ja9o29jhqmnmf     manager3   Ready     Active         Reachable        20.10.13
opnn5kj1839pq603m1fbh029i     worker1    Ready     Active                          20.10.13
zqnzuwfmk2argo5eobp4ikegt     worker2    Ready     Active                          20.10.13

Déploiement des applications nécessaires au fonctionnement et à la gestion du swarm

Déploiement des 3 applications suivantes :

  • Registry Docker
  • Portainer
  • Swarmpit

Registry

Le Registry Docker permet de stocker et de distribuer les images Docker "maison" au sein du swarm. Il se déploie sous la forme d'un simple conteneur :

# Déploiement du registry sur le manager1
docker run -d -p 5000:5000 --restart=always --name registry registry:2

Le Registry sera accessible via http://<ip du manager1>:5000.

Ne pas oublier le --restart=always sans quoi le registry ne redémarrera pas automatiquement avec sa machine hôte...

Configuration des hôtes dans /etc/hosts sur les noeuds du cluster afin d'utiliser le même hôte sur toutes les machines

# Ajouter
<IP DU REGISTRY> private-registry

Et dans le fichier /etc/docker/daemon.json ajouter une entrée pour le registry afin d'autoriser celui-ci sur les machines du swarm :

{
  /* ... */
  "insecure-registries" : [ "private-registry:5000" ]
}

Recharger la config via sudo systemctl reload docker

Et dans les fichiers docker-compose, utiliser images: private-registry:5000/path/to/image:tag pour les images custom tuilisant le registry.

Registry centralisé

Dans un second temps, un registry centralisé pour toutes les infrastructures sera mis en place afin d'éviter le build inutile d'images et de centraliser la gestion des images "approuvées".

Portainer

Il existe deux modes de fonctionnement pour Portainer : le mode Docker "pur" et le mode Docker Swarm. C'est le second qui est utilisé. Il déploie un service pour l'application portainer sur un des managers et un service agent sur chacun des noeuds.

# Récupération du fichier de déploiement de la stack Portainer
curl -L https://downloads.portainer.io/portainer-agent-stack.yml -o portainer-agent-stack.yml
# Déploiement de la stack Portainer (peut prendre du temps)
docker stack deploy -c portainer-agent-stack.yml portainer

Par défaut, le port publié par le service est le port 9000. Il peut être changé dans le fichier portainer-agent-stack.yml ou, temporairement, via docker service update [OPTIONS] <service>.

portainer

Pour compléter l'installation, il reste à ajouter le Registry dont l'url est http://<ip du manager1>:5000 via l'interface web de Portainer.

Note : l'agent portainer consomme 1 CPU complet lors de son exécution. Afin de limiter l'impact sur les performances des machines, j'ai décidé de limiter la quantité de CPU qu'il peut consommer :

docker service update --limit-cpu 0.5 --force portainer_agent

Afin de répercuter le changement lors d'un prochain déploiement de la stack, la limite est ajoutée également dans le fichier de déploiement de la stack portainer :

# portaine-agent-stack.yml
services:
  agent:
    # ...
    deploy:
      # ...
      resources:
        limits:
          cpus: '0.5'

Impacts :

  1. l'agent consomme moins de ressource système lors de son exécution (1/2 CPU)
  2. l'agent tourne 2 fois plus souvent (toutes les 30 minutes au lieu de toutes les 1h)

L'exécution de l'agent impacte moins les performances et la consommation de ressources des machines hôtes.

Fichier complet de déploiement de Portainer :

version: '3.2'

services:
  agent:
    image: portainer/agent:2.11.1
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - /var/lib/docker/volumes:/var/lib/docker/volumes
    networks:
      - agent_network
    deploy:
      resources:
        limits:
          cpus: '0.5'
      mode: global
      placement:
        constraints: [node.platform.os == linux]

  portainer:
    image: portainer/portainer-ce:2.11.1
    command: -H tcp://tasks.agent:9001 --tlsskipverify
    ports:
      - "9443:9443"
      - "9000:9000"
      - "8000:8000"
    volumes:
      - portainer_data:/data
    networks:
      - agent_network
    deploy:
      mode: replicated
      replicas: 1
      placement:
        constraints: [node.role == manager]

networks:
  agent_network:
    driver: overlay
    attachable: true

volumes:
  portainer_data:

Pour mettre à jour

# Mettre à jour portainer
docker service update --image portainer/portainer-ce:latest --publish-add 9443:9443 --force portainer_portainer
# Mettre à jour l'agent
docker service update --image portainer/agent:latest --force portainer_agent 

Swarmpit

Pour Swarmpit, un installeur est fourni mais requiert un accès illimité à Internet depuis les machines à l'intérieur du container de l'installateur. Comme cela n'est pas encore possible au moment de l'installation du cluster, il faut utiliser la méthode manuelle :

# Récupération des fichiers de déploiement de la stack Swarmpit
git clone https://github.com/swarmpit/swarmpit.git
# Déploiement de la stack Swarmpit (peut prendre du temps)
docker stack deploy -c swarmpit/docker-compose.yml swarmpit

Swarmpit installe plusieurs services : les 3 services de l'application elle-même et un agent répliqué sur chaque nœud du cluster.

Par défaut, le port publié par le service est le port 888. Ce port a été changé en 81 via Portainer de manière temporaire afin de tester l'application. Alternativement, ce changement de port peut-être effectué en ligne de commande via la commande docker service update [OPTIONS] <service> ou configuré de manière définitive dans le fichier swarmpit/docker-compose.yml.

![swarpit](./Images/Dashboard swarmpit.png)

Vérification de l'état des services

Après l'installation des différentes stacks, l'état des services sur le cluster est le suivant :

# Vérification de l'état des services
docker service ls
ID             NAME                  MODE         REPLICAS   IMAGE                           PORTS
iwq8kvzazmj2   portainer_agent       global       5/5        portainer/agent:2.11.1          
h18n3f8ym5v6   portainer_portainer   replicated   1/1        portainer/portainer-ce:2.11.1   *:8000->8000/tcp, *:9000->9000/tcp, *:9443->9443/tcp
pmkzydpr79bz   swarmpit_agent        global       5/5        swarmpit/agent:latest           
tdg75vh6w91c   swarmpit_app          replicated   1/1        swarmpit/swarmpit:latest        *:81->8080/tcp
jdq8z2hff3oy   swarmpit_db           replicated   1/1        couchdb:2.3.0                   
zlxjgbtoiwy6   swarmpit_influxdb     replicated   1/1        influxdb:1.7        

Le registry n’apparaît pas car il ne tourne pas au sein d'une stack :

# Sur le manager1 sur lequel le registry est déployé
docker ps
CONTAINER ID   IMAGE                      COMMAND                  CREATED        STATUS                  PORTS                                       NAMES
33794adc956f   swarmpit/swarmpit:latest   "java -jar swarmpit.…"   20 hours ago   Up 20 hours (healthy)   8080/tcp                                    swarmpit_app.1.0mix46fd7260lyinkmf47rmw4
4e9eca7df72f   swarmpit/agent:latest      "./agent"                20 hours ago   Up 20 hours             8080/tcp                                    swarmpit_agent.kuj58tn3l1ueyuyt4nnjtulip.gkzen5bg8bjzhqije1fs858cg
d3ef3c549d05   portainer/agent:2.11.1     "./agent"                20 hours ago   Up 20 hours                                                         portainer_agent.kuj58tn3l1ueyuyt4nnjtulip.8x0869dns4rbuui1zei7ypbp4
9b4d6c22da5d   registry:2                 "/entrypoint.sh /etc…"   25 hours ago   Up 25 hours             0.0.0.0:5000->5000/tcp, :::5000->5000/tcp   registry

Quelques commandes utiles

Voir https://docs.docker.com/reference/ pour la référence complète des commandes et options disponibles.

  • Commandes générales
    • docker swarm <COMMAND> [OPTIONS] : gérer le cluster Docker Swarm
    • docker node <COMMAND> [OPTIONS] [NODE] : gérer les noeuds
    • docker stack <COMMAND> [OPTIONS] [stack name] : gérer les stacks applicatives
    • docker service <COMMAND> [OPTIONS] [service] : gérer les services
  • Commande d'état
    • docker ps : liste les tâches en cours d'exécution sur un hôte
    • docker service ls : affiche la liste des services en cours d'exécutions sur le swarm
    • docker service inspect <SERVICE> : afficher les informations sur un service (ajouter --pretty pour un affichage "human-readable")
    • docker node ls : affiche la liste des nœuds du swarm et leur état
    • docker node inspect <NODE> : afficher les informations sur un nœud (ajouter --pretty pour un affichage "human-readable")
  • Commandes de modification
    • docker service update [OPTIONS] <SERVICE> : modifier un service en cours d'exécution
    • docker service scale <SERVICE>=<NUMBER_OF_TASKS>: modifier le nombre de répliques d'un service
    • docker node update --label-add <LABEL> <NODE> : ajouter une étiquette sur un noeud , les labels peuvent être de la forme etiquette ou NAME=valeur

Système de fichier

Partage via NFS

Configuration des ACL

# Installer le paquet acl
sudo apt install acl
# Trouver le chemin de /dockerdata-ceph
df -a
/dev/vdf1                                               40972492      24  38858988   1% /dockerdata-ceph
# Vérifier que le filesystem supporte acl (remplacer /dev/vdd1 par le chemin correct)
sudo tune2fs -l /dev/vdf1
# Changer le groupe et le rendre "sticky"
sudo chgrp docker /dockerdata-ceph
sudo chmod g+s /dockerdata-ceph
sudo chmod g+rwX /dockerdata-ceph
# Ajouter les droits du groupe via les acl
sudo setfacl -Rdm g:docker:rwx /dockerdata-ceph/

Export via NFS

Sur le manager1

Installation du paquest nfs-kernel-server

sudo apt install nfs-kernel-server

Ajout du partage vers les nœuds du swarm dans /etc/exports

/dockerdata-ceph 10.1.4.26(rw,no_subtree_check,no_root_squash,async) 10.1.4.27(rw,no_subtree_check,no_root_squash,async) 10.1.4.28(rw,no_subtree_check,no_root_squash,async) 10.1.4.29(rw,no_subtree_check,no_root_squash,async)

Rechargement du daemon nfs-kernel-server

sudo systemctl reload nfs-kernel-server
Sur les autres noeuds

Ajout du partage dans /etc/fstab

10.1.4.25:/dockerdata-ceph /dockerdata-ceph nfs defaults,noatime

Creation du répertoire cible et montage du partage nfs

sudo mkdir /dockerdata-ceph
sudo mount -a
ls -al
total 24
drwxrwsr-x  3 root docker  4096 11 mar 14:29 .
drwxr-xr-x 20 root root    4096  4 avr 15:27 ..
drwx------  2 root root   16384 11 mar 14:29 lost+found

Reverse proxy en entrée du cluster

En plus du répartiteur de charge/reverse proxy haproxy mis en place pour l'infrastructure SIPR, j'ai mis en place un service Docker de reverse proxy en entrée du cluster Docker Swarm.

La raison de ce choix est de répondre aux besoins suivants :

  1. centralisation de l'exposition des applications hébergées, sans cela, chaque application devrait être exposée à l'extérieur du cluster et rendue accessible dans le haproxy de SIPR
  2. certaines configurations telles que la récupération de l'IP du client, les logs... seront gérées au niveau du proxy et de manière transparente pour les applications qu'il expose
  3. seul le proxy est exposé à l'extérieur, ce qui réduit fortement le nombre de ports ouverts en entrée sur les hôtes du cluster (actuellement uniquement 80 et 443, mais d'autres pourraient l'être dans le futur pour exposer des web services, des websockets...)

Frontend proxy et Stacks docker

Mise en place du reverse proxy avec nginx

Afin de rencontrer les besoins exprimés plus haut, le reverse proxy nginx sera déployé globalement sur le cluster (c'est-à-dire qu'un répliqua sera créé sur chacun des hôtes). D'un point de vue technique, cela est mis en place à travers le mode global de la directive deploy du fichier de définition du service.

Afin de fournir les fichiers de configuration nécessaires au reverse proxy, 5 volumes sont montés en mode bind :

  • ./config/nginx/conf.d:/etc/nginx/conf.d : fichiers de configurations chargés automatiquement pour tous les sites
  • ./config/nginx/sites:/etc/nginx/sites : fichiers de définitions des virtual hosts
  • ./config/nginx/nginx.conf:/etc/nginx/nginx.conf : configuration de base de nginx
  • ./config/nginx/includes:/etc/nginx/includes : fichiers de configuration pouvant être inclus dans les définitions des virtual hosts
  • ./config/ssl:/etc/nginx/ssl : certificats et clés pour la connection sécurisée via SSL

Comme je l'ai expliqué dans le chapitre technique sur Docker et Docker Swarm, le mode Swarm intègre une répartition de charge à travers son réseau ingress. L'utilisation d'un reverse proxy interne au cluster va nous permettre de profiter de cette fonctionnalité pour diriger les requêtes vers le bon noeud du Swarm et pour répartir la charge entre les conteneurs d'un même services sans que le HAProxy ou le proxy interne du cluster n'aient à avoir connaissance de la localisation de ces conteneurs.

Requêtes à travers l'infarstructure

Mais ce qui est vrai pour un service en général l'est aussi par défaut pour le proxy interne : une requête arrivant sur HAProxy va être balancée sur un des noeuds du Swarm et, ensuite, via le routing mesh, sur l'un des noeuds exécutant le proxy interne. Le proxy interne lui-même passera par le routing mesh pour accéder au service de l'application demandée par le client. Nous avons donc deux niveaux de répartition de charge avant d'arriver au service demandé. L'idéal serait d'éviter d'avoir recours au routing mesh pour accéder au proxy interne.

Pour garantir cela, nous allons « exclure » le proxy interne du routing mesh de Docker Swarm en exposant les ports 80 et 443 du service directement sur les machines hôtes. Cela se fait via le mode host dans la définition des ports du service. Il s'agit d'une pratique conseillée pour la plupart des solutions de reverse proxy tournant en conteneur.

En tenant compte de tout cela, le fichier de définition du service est le suivant :

# proxy-compose.yml
version: "3.9"
services:

  proxy:
    image: nginx:latest

    deploy:
      mode: global
    
    ports:
      - target: 80
        published: 80
        mode: host
      - target: 443
        published: 443
        mode: host
    
    volumes:
      - ./config/nginx/conf.d:/etc/nginx/conf.d
      - ./config/nginx/sites:/etc/nginx/sites
      - ./config/nginx/nginx.conf:/etc/nginx/nginx.conf
      - ./config/nginx/includes:/etc/nginx/includes
      - ./config/ssl:/etc/nginx/ssl
    
    networks:
      - proxy_public

networks:
  proxy_public:
    external: true
    name: proxy_public

Le fichier de configuration de nginx a dû être adapté afin de permettre la récupération de l'adresse IP du client:

# config/nginx/nginx.conf
user  nginx;
worker_processes  auto;

error_log  /var/log/nginx/error.log notice;
pid        /var/run/nginx.pid;


events {
    worker_connections  1024;
}


http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    server_names_hash_bucket_size  256;

    # Retrieve real remote host ip address from Docker ingress network
    set_real_ip_from 10.0.0.0/16;
    # Retrieve real remote address from SIPR load balancer if ports exposed on host
    set_real_ip_from 10.1.4.0/24;

    real_ip_recursive on;
    real_ip_header X-Forwarded-For;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;

    sendfile        on;
    #tcp_nopush     on;

    keepalive_timeout  65;

    gzip  on;

    include /etc/nginx/conf.d/*.conf;

    # Automatically load sites definitions
    include /etc/nginx/sites/*.conf;
}

Un fichier de configuration SSL/TLS fournit la configuration commune à toutes les applications :

# config/nginx/includes/ssl.conf
ssl_session_timeout 1d;
ssl_session_cache shared:MozSSL:10m;  # about 40000 sessions
ssl_session_tickets off;

# modern configuration
ssl_protocols TLSv1.3;
ssl_prefer_server_ciphers off;

# HSTS (ngx_http_headers_module is required) (63072000 seconds)
add_header Strict-Transport-Security "max-age=63072000" always;

# OCSP stapling
ssl_stapling on;
ssl_stapling_verify on;

ssl_certificate /etc/ssl/server.pem;
ssl_certificate_key /etc/ssl/server.key;
ssl_trusted_certificate /etc/ssl/server_interm.cert;

Avant de lancer le service, il faut également créer le réseau overlay sur lequel les applications devront être exposées.

`docker network create --opt encrypted --driver overlay --attachable proxy_public`

Il reste alors à lancer le proxy via docker stack deploy -c proxy-compose.yml proxy

Les fichiers de configuration des différents sites n'ont plus qu'à être placés dans le répertoire ./config/nginx/sites et seront chargés soit au démarrage du service, soit lors de son rechargement via docker service update proxy_proxy.

Voici par exemple le fichier donnant accès à l'instance de Portainer sur mon infrastructure :

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;

    access_log  /var/log/nginx/portainer.log  main;
    
    server_name 
      portainer.dckr.sisg.ucl.ac.be;
    
    include /etc/nginx/includes/ssl.conf;
    
    location / {

      include /etc/nginx/includes/access.conf;
      
      proxy_pass http://portainer_portainer:9000;
      proxy_set_header X-Real-IP  $remote_addr;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header X-Forwarded-Proto https;
      proxy_set_header X-Forwarded-Port 443;
      proxy_set_header Host $host;
    }
    
    # replace with the IP address of your resolver
    # resolver 127.0.0.1;
}

Une commande pour générer les configurations des sites automatiquement

Afin de déployer plus rapidement les applications sur mon infrastructure, j'ai écrit une petite commande permettant la génération des fichiers de configuration de site sur base d'un template et de variables d'environnement.

Template de configuration de site :

# Basic site configuration template
server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;

    access_log  /var/log/nginx/{{VIRTUAL_HOST}}.log  main;
    
    server_name {{VIRTUAL_HOST}};
    
    include /etc/nginx/includes/{{SSL_FILE}};
    include /etc/nginx/includes/fileprotect.conf;
    
    location / {
      
      include /etc/nginx/includes/proxy.conf;
      include /etc/nginx/includes/fileprotect.conf;
      
      proxy_pass {{VIRTUAL_PROTO}}://{{VIRTUAL_SERVICE}}:{{VIRTUAL_PORT}};
      proxy_set_header X-Forwarded-Proto https;
      proxy_set_header X-Forwarded-Port 443;
    }
    
    # replace with the IP address of your resolver
    # resolver 127.0.0.1;
}

Commande bash pour la génération des configurations :

#!/bin/bash

SCRIPT_cmdname=${0##*/}
SCRIPT_DIR="$( cd "$( dirname "$0" )" && pwd )"

if [[ -f ${1} ]]; then
  echo "Loading variables from ${1}"
  source ${1}
fi

if [[ -z "${VIRTUAL_HOST}" || -z "{VIRTUAL_SERVICE}" ]]; then
  echo "At least VIRTUAL_HOST and VIRTUAL_SERVICE must be defined in the environment or the given variable file."
  exit 1
fi

render_template() {
  cp "${SCRIPT_DIR}/../config/nginx/site.tpl.txt" "${SCRIPT_DIR}/../config/nginx/sites/${VIRTUAL_HOST}.conf"
  sed -i s/\{\{VIRTUAL_HOST\}\}/"${VIRTUAL_HOST}"/ "${SCRIPT_DIR}/../config/nginx/sites/${VIRTUAL_HOST}.conf"
  sed -i s/\{\{SSL_FILE\}\}/"${SSL_FILE:-ssl.conf}"/g "${SCRIPT_DIR}/../config/nginx/sites/${VIRTUAL_HOST}.conf"
  sed -i s/\{\{VIRTUAL_SERVICE\}\}/"${VIRTUAL_SERVICE}"/g "${SCRIPT_DIR}/../config/nginx/sites/${VIRTUAL_HOST}.conf"
  sed -i s/\{\{VIRTUAL_PORT\}\}/"${VIRTUAL_PORT:-80}"/g "${SCRIPT_DIR}/../config/nginx/sites/${VIRTUAL_HOST}.conf"
  sed -i s/\{\{VIRTUAL_PROTO\}\}/"${VIRTUAL_PROTO:-http}"/g "${SCRIPT_DIR}/../config/nginx/sites/${VIRTUAL_HOST}.conf"
}

render_template

docker service update proxy_proxy

Exemple de variables d'envirronment à fournir via un fichier :

SITE_HOST=myapp.apps.sisg.ucl.ac.be
SSL_FILE=apps-ssl.conf
VIRTUAL_SERVICE=myapp_webserver
VIRTUAL_PORT=8080
VIRTUAL_PROTO=http

Exécution de la commande

newsite /path/to/site/envfile

Le nouveau site est alors accessible sur l'infrastructure.

Note : Cette commande est encore expérimentale et ne fonctionne pas dans 100% des cas, mais elle permet de simplifier les déploiements en attendant de passer à une solution plus solide comme nginx-proxy que j'aborderai dans le chapitre sur les Perspectives.