Mise en place d'une infrastructure de démonstration
- Introduction
- Infrastructure à déployer
- Setup inital
- Installation et configuration de Docker
- Déploiement du cluster Docker Swarm
- Système de fichier
- Reverse proxy en entrée du cluster
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.
É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 commandeexit
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.3docker-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 :
- activation du swarm et création du premier manager
- (optionnel) ajout de managers supplémentaires
- ajout des workers
- (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>
.
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 :
- l'agent consomme moins de ressource système lors de son exécution (1/2 CPU)
- 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 Swarmdocker node <COMMAND> [OPTIONS] [NODE]
: gérer les noeudsdocker stack <COMMAND> [OPTIONS] [stack name]
: gérer les stacks applicativesdocker 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ôtedocker service ls
: affiche la liste des services en cours d'exécutions sur le swarmdocker 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 étatdocker 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écutiondocker service scale <SERVICE>=<NUMBER_OF_TASKS>
: modifier le nombre de répliques d'un servicedocker node update --label-add <LABEL> <NODE>
: ajouter une étiquette sur un noeud , les labels peuvent être de la formeetiquette
ouNAME=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 :
- 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
- 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
- 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...)
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.
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.