Annexe 2: Première infrastructure de démonstration (2018)

Note : Ce chapitre décrit la première infrastructure de démonstration et de développement basée sur Docker Swarm que j'ai mise en place. Cette démarche a depuis été remplacée par celle décrite au chapitre 3 lors de l'instanciation de la nouvelle infrastructure de démonstration de ce projet, et suite aux améliorations du moteur Docker et de mes compétences dans son utilisation.

Ce projet a énormément changer depuis la première version de l'infrastructure de démonstration mise en place en 2018-2019 détaillée en annexe :

  1. Intégration les nouvelles fonctionnalités offertes par les versions récente de Docker et en particulier au niveau du mode Swarm. Ces nouveautés simplifient énormément la gestion et la mise en place du cluster.
  2. Utilisation d'un registre docker privé non sécurisé afin d'éviter les difficultés liées à la mise en place du certificat SSL sur le cluster. Ce registre sera de plus complété par celui fourni par Gitlab afin d'assurer la centralisation des images.
  3. Abandon des configurations des services via systemd au profit des options de redémarrage des services intégrées à Docker Swarm et qui permettent plus de souplesse
  4. Abandon du proxy frontal nginx mis en place sur une machine virtuelle séparée au profit du load balancer Haproxy déployé par SIPR

Ancienne infrastructure "LVS" de 2018-2019

Ces infrastructures utilisent l'ancien load balancer de SIPR et nécessite des machines proxy supplémentaires.

Par exemple : infrastructure de démonstration de 2018-2019 et infrastructure de développement du portail de 2019

lvs

Nouvelle infrastructure "HAProxy"

Par exemple : seconde infrastructure de démonstration dckr.sisg et nouvelles infrastructures de développement, qa et production du portail

haproxy

Description de l’infrastructure

Aperçu de l’infrastructure

L’infrastructure générale mise en œuvre est composée des éléments suivants :

  • Proxy frontend : gère la connexion sécurisée aux applications depuis l’extérieur pour donner accès aux applications et services hébergées sur le cluster. Il joue également le rôle de répartiteur de charge et de firewall du cluster en limitant l’accès à des machines ou réseaux clients et l'accès à certains ports (80 et 443).
  • Le cluster Docker Swarm proprement dit, composé de
    • 1 ou plusieurs « workers » qui exécutent les conteneurs des différents services du cluster, l'infrastructure de démonstration en possède 2, mais d’autres peuvent être ajoutés « à chaud »
    • 1 « manager » qui fournit les fonctionnalités suivantes
      • L'orchestrateur des stacks et services du cluster (Docker Swarm)
      • Le registre et dépôt des images Docker custom utilisées par les services et conteneurs (Docker Registry)
      • Le partage de fichiers entre le « manager » et les « workers » via un serveur NFS
      • Une application web de gestion du cluster (Portainer)
      • L'exécution de tâches de gestion sur les différentes machines du cluster avec l’outil Ansible
      • La construction des images Docker avec l’outil Docker Compose
      • D'autres applications web de gestion ou de monitoring selon les besoins qui apparaîtront lors de l'utilisation du cluster

D'autres applications externes au cluster peuvent lui être connectées, par exemple

  • l'authentification SSO via l'IDP Shibboleth UCLouvain
  • un cluster de base de données (MySQL, Percona, PostgreSQL…)
  • un serveur Gitlab pour la récupération du code des applications…

schéma technique de l’infrastructure

Les machines du cluster

L'infrastructure de démonstration est mise en place dans le cloud Open Nebula géré par SIPR et est composée des éléments suivants :

  • 1 machine virtuelle Debian GNU/Linux1 pour le manager du cluster – qui hébergent les conteneurs des services des applications ;
  • 2 machines virtuelles Debian GNU/Linux pour les workers.

Schéma de l’infrastructure

Load balancing et proxys

La répartition de charge et le reverse proxy se fait à deux niveaux dans l’infrastructure :

  1. Un niveau « externe » situé devant le cluster qui permet de gérer les certificats SSL, les règles spéciales de droits d’accès et les noms de domaine des applications. Il est chargé de répartir la charge entre les différentes machines du cluster. Il permet également d’accéder aux applications web de gestion et de monitoring du cluster.
  2. Un niveau « interne » situé dans le cluster qui permet de joindre les applications déployées sur ce dernier. Celui-ci utilise le DNS interne de Docker pour joindre les services des stacks applicatives. Il permet également de limiter les ports ouverts sur les machines du cluster et évite d’exposer tous les conteneurs à l’extérieur du cluster. Ce proxy est exposé au proxy extérieur via le port 80 et aux services du cluster via un réseau interne à Docker. Pour l’utiliser, les services devant être accédés depuis l’extérieur du cluster devront s’y connecter.

Les solutions de proxy et load balancing qui ont été envisagées dans le cadre de ce brevet sont :

  • NGINX : solution de serveur web, reverse proxy et répartiteur de charge open source très légère et très performante
  • Caddy : un serveur web open source très léger utilisant HTTPS par défaut
  • Traefik : un reverse proxy / répartiteur de charge open source conçu pour faciliter le déploiement d’application en microservices
  • HAProxy : une solution de proxy et répartiteur de charge performante
SolutionExterneInterne
NGINXouioui
HAProxyouinon
Traefik(oui)oui
Caddyouioui

Ayant déjà une expérience de NGINX, j’ai préféré utiliser ce dernier pour l'infrastructure de démonstration, mais les deux autres solutions pourraient être envisagées pour une future infrastructure.

Remarque :

  1. En production, HAProxy (en cours de mise en place pour l’infrastructure SIPR) pourrait remplacer NGINX
  2. Docker Swarm effectue lui-même un routage interne ainsi qu’une répartition de charge entre les conteneurs (tâches) d’un service

SCHEMA PROXY FRONT END

Utilisation de l’infrastructure de démonstration

L’infrastructure de démonstration mise en place dans le cadre de ce brevet est utilisée activement depuis 2019 pour le déploiement de stacks applicatives dans le cadre du développement du nouveau portail UCLouvain en Drupal 9.

Voici par exemple les stacks déployées afin de fournir un site Drupal ainsi que l’authentification SAML (via Shibboleth) associée :

Schéma des stacks Docker utilisée pour le portail en Drupal 9

Une stack Drupal peut-être déployée pour chaque développement ou plateforme de démonstration spécifique. La stack SAML SP est commune à l’ensemble des stacks Drupal déployées.

En plus de ces stacks destinées au développement du portail, l’infrastructure héberge aussi d’autres stacks applicatives : blog, outils de développement, gestionnaire de base de données…

Cette infrastructure a également servi de modèle à une infrastructure similaire utilisée pour la mise en place d’un cluster ElasticSearch. Ce cluster est au cœur de la nouvelle application du service bibliothèques de l'UCLouvain et qui doit être mise en production début 2022 afin de succéder à VIRTUA.

Mise en place de l’infrastructure

1. Infrastructure de machines virtuelles dans Open Nebula

Mise en place de 4 machines virtuelles Debian dans OpenNebula par SIPR : 3 pour le cluster Docker Swarm, une pour le proxy frontal.

Configuration des machines :

  • manager du Swarm : 4 cœurs, 16 Go RAM, 20Go pour le système, 40 Go pour /var/lib/docker 2, 20 Go pour le partage avec les nœuds /dockerdata-ceph
  • workers du cluster : 4 cœurs, 16 Go RAM, 20 Go pour le système 3
  • proxy Nginx : 4 cœurs, 8 Go RAM, 20 Go pour le système

2. Le stockage partagé

Pour le partage des fichiers des applications, un volume Ceph est monté sur le nœud manager à l’emplacement /dockerdata-ceph.

Ce point de montage est partagé avec les hôtes depuis le manager via le protocole NFS.

Remarques :

  • Le partage du point de montage via NFS depuis le manager plutôt que depuis l’infrastructure NFS fournie par SIPR a été préférée pour des raisons de performances.
  • La gestion des permissions sur les fichiers est mise en place en utilisant les groupes POSIX et le système d'ACL4 de Debian.
  • Le nombre de nœuds hôtes du cluster pourra être doublé en production si la charge le nécessite ou pour assurer une redondance des services en cas de perte d’un data center.

2.1. Configuration de la gestion via les ACL

sudo apt install acl
#check if acl supported by filesystem
sudo tune2fs -l /dev/vdd1
# set group as "sticky"
sudo chmod g+s docker /dockerdata-ceph
# set group default acls
sudo setfacl -Rdm g:docker:rwx /dockerdata-ceph/

Dans cette configuration, le groupe docker est appliqué automatiquement à tous les fichiers et répertoires et reçoit les droits rwx sur ceux-ci afin que les conteneurs puissent lire et écrire sans problème sur le système de fichier partagé.

2.2. Configuration du partage NFS

Export du répertoire /dockerdata-cephvia NFS depuis le manager /etc/exports

/dockerdata-ceph 10.1.4.64(rw,no_subtree_check,no_root_squash,async) 10.1.4.65(rw,no_subtree_check,no_root_squash,async)

Le choix du flag async sur le serveur a été fait afin de fournir les performances nécessaires sur l'infrastructure. Cette option peut poser des problèmes lors de l’écriture des fichiers, mais ceux-ci sont mitigés par le fait qu’un seul hôte écrit à la fois sur le montage.

Montage sur les workers /etc/fstab

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

sync and async have different meanings for the two different situations.

sync in the client context makes all writes to the file be committed to the server. async causes all writes to the file to not be transmitted to the server immediately, usually only when the file is closed. So another host opening the same file is not going to see the changes made by the first host.

Note that NFS offers "close to open" consistency meaning other clients cannot anyway assume that data in a file open by others is consistent (or frankly, that it is consistent at all if you do not use nfslock to add some form of synchronization between clients).

sync in the server context (the default) causes the server to only reply to say the data was written when the storage backend actually informs it the data was written. async in the server context gets the server to merely respond as if the file was written on the server irrespective of if it actually has written it. This is a lot faster but also very dangerous as data may have a problem being committed!

In most cases, async on the client side and sync on the server side. This offers a pretty consistent behaviour with regards to how NFS is supposed to work.

3. Installation et configuration de Docker et du Docker Swarm

L'installation de Docker sous Debian se fait en suivant la documentation officielle5.

Configuration de Docker dans le fichier /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"

# If you need Docker to use an HTTP proxy, it can also be specified here.
#export http_proxy="http://127.0.0.1:3128/"

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

DOCKER_OPTS="--config-file=/etc/docker/daemon.json"

Options du daemon Docker dans /etc/docker/daemon.json

{
  "storage-driver": "overlay2",
  "graph": "/var/lib/docker",
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "2"
  },
  "debug": false
}

Installation de docker-py sur les hôtes Docker

apt-get install python-pip
pip install docker-py

Initialisation du mode swarm sur le manager6 :

docker swarm init --advertise-addr <internal_server_ip>

Exemple :

$ docker swarm init --advertise-addr 192.168.99.100
Swarm initialized: current node (dxn1zf6l61qsb1josjja83ngz) is now a manager.

To add a worker to this swarm, run the following command:

    docker swarm join \
    --token SWMTKN-1-49nj1cmql0jkz5s954yi3oex3nedyz0fb0xx14ie39trti4wxv-8vxv8rssmk743ojnwacrr2e7c \
    192.168.99.100:2377

To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions.

Le nom des nœuds peuvent être obtenus via :

docker node ls

Sur les workers, la commande suivante permet de rejoindre le cluster.

$ docker swarm join \
  --token  SWMTKN-1-49nj1cmql0jkz5s954yi3oex3nedyz0fb0xx14ie39trti4wxv-8vxv8rssmk743ojnwacrr2e7c \
  192.168.99.100:2377

This node joined a swarm as a worker.

4. Portainer, Registry, Swarmpit et Docker-Compose

4.1. Installation de Portainer sur le manager

1ʳᵉ étape : instanciation du conteneur Portainer
docker volume create portainer_data
docker run -d -p 9000:9000 -v /var/run/docker.sock:/var/run/docker.sock -v /opt/portainer-data:/data portainer/portainer
2ᵉ étape : lancement de Portainer via Systemd7

Configuration /etc/systemd/system/portainer.service

[Unit]
Description=Portainer
After=dkm_registry.service
Requires=docker.service

[Service]
ExecStartPre=/usr/bin/docker stop dkm_portainer
ExecStartPre=/usr/bin/docker rm dkm_portainer
ExecStart=/usr/bin/docker run -p 9000:9000 -v /var/run/docker.sock:/var/run/docker.sock -v /opt/portainer-data:/data --name dkm_portainer portainer/portainer
;ExecStart=/usr/bin/docker start dkm_portainer
ExecStop=/usr/bin/docker stop dkm_portainer

[Install]
WantedBy = multi-user.target

Activation du service

systemctl enable portainer.service
systemctl start portainer
systemctl status portainer

Remarque : cette configuration n’est plus optimale aujourd’hui, le lancement automatique du conteneur pouvant être géré directement par Docker.

Mise à jour de portainer
docker image pull portainer/portainer
docker kill dkm_portainer
docker rm dkm_portainer
docker run -d -p 9000:9000 -v /var/run/docker.sock:/var/run/docker.sock -v /opt/portainer-data:/data --name dkm_portainer portainer/portainer
Configuration du proxy frontal

Une fois Portainer installé, il restera à définir un site dans le proxy frontal (voir plus loin) afin d'y avoir accès depuis l'extérieur des data center :

upstream portainer_backend {

  server dkm-webapps.sipr-dc.ucl.ac.be:9000;
}

server {

  listen 80;
  listen [::]:80;

  server_name portainer.apps.sisg.ucl.ac.be;

  # Redirect all HTTP requests to HTTPS with a 301 Moved Permanently response.
  return 301 https://$host$request_uri;

}

server {

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

  include /etc/nginx/uclinclude/ssl/apps-ssl.conf;

  server_name portainer.apps.sisg.ucl.ac.be;

  location / {

    include /etc/nginx/uclinclude/access/apps.access.conf;

    proxy_pass http://portainer_backend;
    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 X-NGINX-UPSTREAM dkm;
    proxy_set_header Host $host;
  }
}

4.2. Installation du registry

Sources :

  • https://www.server-world.info/en/note?os=Debian_9&p=docker&f=6
  • https://www.server-world.info/en/note?os=Debian_9&p=ssl
  • https://docs.docker.com/registry/deploying/
1ʳᵉ étape : Création d’un certificat auto-signé pour les communications entre les nœuds du cluster et le registry.

Génération de la clé

root@dkm-webapps:/etc/ssl/private# openssl genrsa -aes128 -out dkm-webapps.key 2048
Generating RSA private key, 2048 bit long modulus
......................+++
........................................+++
e is 65537 (0x010001)
Enter pass phrase for dkm-webapps.key:
Verifying - Enter pass phrase for dkm-webapps.key:

Il faut ensuite retirer la pass phrase de la clé :

root@dkm-webapps:/etc/ssl/private# openssl rsa -in dkm-webapps.key -out dkm-webapps-nopass.key
Enter pass phrase for dkm-webapps.key:
writing RSA key

Génération du fichier de demande de certificat (CSR) :

root@dkm-webapps:/etc/ssl/private# openssl req -new -days 3650 -key dkm-webapps-nopass.key -out dkm-webapps-selfsigned.csr
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:BE
State or Province Name (full name) [Some-State]:BW
Locality Name (eg, city) []:LLN
Organization Name (eg, company) [Internet Widgits Pty Ltd]:SISG
Organizational Unit Name (eg, section) []:^C
root@dkm-webapps:/etc/ssl/private# openssl req -new -days 3650 -key dkm-webapps-nopass.key -out dkm-webapps-selfsigned.csr
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:BE
State or Province Name (full name) [Some-State]:BW
Locality Name (eg, city) []:LLN
Organization Name (eg, company) [Internet Widgits Pty Ltd]:UCLouvain
Organizational Unit Name (eg, section) []:SISG
Common Name (e.g. server FQDN or YOUR name) []:dkm-webapps.sipr-dc.ucl.ac.be
Email Address []:info-portail@uclouvain.be

Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:
An optional company name []:

Création du certificat auto-signé :

root@dkm-webapps:/etc/ssl/private# openssl x509 -in dkm-webapps-selfsigned.csr -out dkm-webapps-selfsigned.crt -req -signkey dkm-webapps-nopass.key -days 3650
Signature ok
subject=C = BE, ST = BW, L = LLN, O = UCLouvain, OU = SISG, CN = dkm-webapps.sipr-dc.ucl.ac.be, emailAddress = info-portail@uclouvain.be
Getting Private key

Vérification :

root@dkm-webapps:/etc/ssl/private# ls
dkm-webapps.key  dkm-webapps-nopass.key  dkm-webapps-selfsigned.crt  dkm-webapps-selfsigned.csr

2ᵉ étape : Installation du certificat

Sur le manager :

root@dkm-webapps:/etc/ssl/private# mkdir -p /etc/docker/certs.d/dkm-webapps.sipr-dc.ucl.ac.be:5000
root@dkm-webapps:/etc/ssl/private# cp /etc/ssl/private/dkm-webapps-selfsigned.crt /etc/docker/certs.d/dkm-webapps.sipr-dc.ucl.ac.be\:5000/ca.crt

Sur chacun des workers :

root@dkh-webapps[1-2]:/root# mkdir -p /etc/docker/certs.d/dkm-webapps.sipr-dc.ucl.ac.be:5000
root@dkh-webapps[1-2]:/root# cp dkm-webapps-selfsigned.crt /etc/docker/certs.d/dkm-webapps.sipr-dc.ucl.ac.be\:5000/ca.crt
3ᵉ étape : Installation du registry

Récupération de l’image :

root@dkm-webapps:/etc/ssl/private# docker pull registry:2
2: Pulling from library/registry
Digest: sha256:5a156ff125e5a12ac7fdec2b90b7e2ae5120fa249cf62248337b6d04abc574c8
Status: Image is up to date for registry:2

Création du répertoire pour le stockage des données du registry :

mkdir /var/lib/docker/registry

Lancement du registry :

root@dkm-webapps:/etc/ssl/private# docker run -d -p 5000:5000 -e REGISTRY_HTTP_TLS_CERTIFICATE=/certs/dkm-webapps-selfsigned.crt -e REGISTRY_HTTP_TLS_KEY=/certs/dkm-webapps-nopass.key -v /etc/ssl/private:/certs -v /var/lib/docker/registry:/var/lib/registry registry:2
4ᵉ étape : Test du registry

Sur la machine manager :

root@dkm-webapps:/etc/ssl/private# docker pull debian:latest

root@dkm-webapps:/etc/ssl/private# docker tag debian dkm-webapps.sipr-dc.ucl.ac.be:5000/debian_reg

root@dkm-webapps:/etc/ssl/private# docker push dkm-webapps.sipr-dc.ucl.ac.be:5000/debian_reg
The push refers to repository [dkm-webapps.sipr-dc.ucl.ac.be:5000/debian_reg]
f715ed19c28b: Pushed
latest: digest: sha256:bbb3345ed2e7548dc7a53385b724374ecfb166489a1066cc31b345d0d767df78 size: 529

root@dkm-webapps:/etc/ssl/private# docker images
REPOSITORY                                      TAG                 IMAGE ID            CREATED             SIZE
drupal                                          latest              e4207c5bbdec        11 days ago         446MB
mariadb                                         latest              67238b4c1da0        5 weeks ago         365MB
nginx                                           latest              dbfc48660aeb        6 weeks ago         109MB
dkm-webapps.sipr-dc.ucl.ac.be:5000/debian_reg   latest              be2868bebaba        6 weeks ago         101MB
debian                                          latest              be2868bebaba        6 weeks ago         101MB
portainer/portainer                             latest              00ead811e8ae        2 months ago        58.7MB
registry                                        2                   2e2f252f3c88        2 months ago        33.3MB
registry                                        latest              2e2f252f3c88        2 months ago        33.3MB
root@dkm-webapps:/etc/ssl/private# curl -k https://localhost:5000/v2/_catalog
{"repositories":["debian_reg"]}

Sur les workers :

root@dkh-webapps1:~# docker pull dkm-webapps.sipr-dc.ucl.ac.be:5000/debian_reg
Using default tag: latest
latest: Pulling from debian_reg
Digest: sha256:bbb3345ed2e7548dc7a53385b724374ecfb166489a1066cc31b345d0d767df78
Status: Downloaded newer image for dkm-webapps.sipr-dc.ucl.ac.be:5000/debian_reg:latest

root@dkh-webapps1:~# docker images
REPOSITORY                                      TAG                 IMAGE ID            CREATED             SIZE
mariadb                                         <none>              67238b4c1da0        5 weeks ago         365MB
nginx                                           <none>              dbfc48660aeb        6 weeks ago         109MB
dkm-webapps.sipr-dc.ucl.ac.be:5000/debian_reg   latest              be2868bebaba        6 weeks ago         101MB
debian                                          <none>              be2868bebaba        6 weeks ago         101MB
registry                                        <none>              2e2f252f3c88        2 months ago        33.3MB

5ᵉ étape : Lancement du registry comme service Systemd
[Unit]
Description=Docker Registry
After=docker.service
Requires=docker.service

[Service]
Type=simple
ExecStart=/usr/bin/docker run -p 5000:5000 -e REGISTRY_HTTP_TLS_CERTIFICATE=/certs/dkm-webapps-selfsigned.crt -e REGISTRY_HTTP_TLS_KEY=/certs/dkm-webapps-nopass.key -v /etc/ssl/private:/certs -v /var/lib/docker/registry:/var/lib/registry  --name dkm_registry --rm registry:2
[Install]
WantedBy = multi-user.target

Remarque : cette configuration n'est plus optimale aujourd’hui, le lancement automatique du conteneur pouvant être géré directement par Docker.

4.3. Installation du registry

Swarpit est un outil de gestion de cluster Docker Swarm fournissant un monitoring des performance du cluster. Son installation est plutôt simple et rapide grâce à un installateur :

docker run -it --rm \
  --name swarmpit-installer \
  --volume /var/run/docker.sock:/var/run/docker.sock \
  swarmpit/install:1.9

L'installeur pose alors quelques questions permettant la personnalisation de l'outil (nom de la stack Docker Swarm, port sur lequel exposer le service, volume de stockage...). L’installation déploie également un agent sur chacun des noeuds du cluster.

Cet installeur ne permet pas de configurer le redémarrage automatique de la stack en cas de crash. Il faudra donc modifier cela via les outils en ligne de commande ou l'interface web de Portainer.

Il est également possible d'installer Swarpit manuellement en clonant son dépôt git et en adaptant le fichier yaml de description de la stack.

Une fois Swarmpit installé, il est nécessaire de configurer un site dans le proxy :

upstream swarmpit_backend {

  server dkm-webapps.sipr-dc.ucl.ac.be:8088;
}

server {

  listen 80;
  listen [::]:80;

  server_name swarmpit.apps.sisg.ucl.ac.be;

  # Redirect all HTTP requests to HTTPS with a 301 Moved Permanently response.
  return 301 https://$host$request_uri;

}

server {

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

  include /etc/nginx/uclinclude/ssl/apps-ssl.conf;

  server_name swarmpit.apps.sisg.ucl.ac.be;

  location / {

    include /etc/nginx/uclinclude/access/apps.access.conf;

    proxy_pass http://swarmpit_backend;
    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 X-NGINX-UPSTREAM dkm;
    proxy_set_header Host $host;
  }
}

4.4. Installation de docker-compose

Deux options :

  • installation via Python-Pip
  • installation manuelle :
sudo curl -L "https://github.com/docker/compose/releases/download/1.27.4/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
sudo ln -s /usr/local/bin/docker-compose /usr/bin/docker-compose

5. Proxy NGINX, SSL et templates

L’infrastructure possède deux niveaux de proxy. Tout d’abord, un proxy frontend est mis en place afin de gérer les connexions SSL, de diriger certaines requêtes vers une application ou ports spécifiques (par exemple applications de gestion sur le manager), de limiter l’accès à certaines applications à des plages d’IP spécifiques, de répartir la charge des applications déployées sur le Swarm entre les workers

5.1. Proxy frontal

Le proxy frontal (frontend) est un serveur nginx qui a été mis en place en dehors du cluster afin de servir de répartiteur de charge et de point d’entrée des applications hébergées sur l'infrastructure.

Il remplit les rôles suivants :

  • Répartition de charge entre les 2 workers du cluster
  • Proxy vers les applications hébergées sur le manager du cluster
  • Point d’entrée pour les connexions HTTPS pour les domaines *.apps.sisg.ucl.ac.be et *.portail.sisg.ucl.ac.be
  • Limitation d’accès aux applications de gestion du cluster
  • Isolation des machines du cluster afin d’éviter d’exposer les ports des applications directement au monde extérieur
  • Gestion des noms de domaine spécifiques pour certaines applications

Ce service est hébergé sur une machine virtuelle Debian dédiée dans OpenNebula.

La répartition de charge et le failover de Nginx se base sur des pools de machines appelés upstream. Voici la définition de l'upstream permettant l’accès aux applications via les proxies nginx des 2 workers :

# Upstream vers les 2 proxies des workers
upstream dkh_backend {

  server dkh-webapps1.sipr-dc.ucl.ac.be:8080;
  server dkh-webapps2.sipr-dc.ucl.ac.be:8080;
}

Voici un upstream permettant d’accéder à l’application Portainer sur le port 9000 du manager du cluster :

#
upstream portainer_backend {

  server dkm-webapps.sipr-dc.ucl.ac.be:9000;
}

Certaines configurations ont été placées dans des fichiers inclus dans les définitions des sites :

  • /etc/nginx/uclinclude/ssl/apps-ssl.conf : définit la configuration de la connection sécurisée SSL/TLS commune à tous les sites
  • /etc/nginx/uclinclude/access/apps.access.conf : définit les droits d'accès pour les applications de management (principalement du filtrage sur l'IP)

Exemples de configuration : domaine *.apps.sisg.ucl.ac.be

# generated 2020-12-14, Mozilla Guideline v5.6, nginx 1.10.3, OpenSSL 1.1.0l, intermediate configuration
# https://ssl-config.mozilla.org/#server=nginx&version=1.10.3&config=intermediate&openssl=1.1.0l&guideline=5.6
# Default server configuration
#
upstream dkh_backend {

  server dkh-webapps1.sipr-dc.ucl.ac.be:8080;
  server dkh-webapps2.sipr-dc.ucl.ac.be:8080;
}

server {

  listen 80;
  listen [::]:80;

  server_name *.apps.sisg.ucl.ac.be apps.sisg.ucl.ac.be;

  client_max_body_size 128M;

  # Redirect all HTTP requests to HTTPS with a 301 Moved Permanently response.
  return 301 https://$host$request_uri;

}

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

    include /etc/nginx/uclinclude/ssl/apps-ssl.conf;

    server_name *.apps.sisg.ucl.ac.be apps.sisg.ucl.ac.be;

    client_max_body_size 128M;

    location / {

      include /etc/nginx/uclinclude/access/apps.access.conf;
      include /etc/nginx/uclinclude/html/error_pages.conf;

      proxy_pass http://dkh_backend;
      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 X-NGINX-UPSTREAM dkh;
      proxy_set_header Host $host;
      proxy_buffer_size 128k;
      proxy_buffers 4 256k;
      proxy_busy_buffers_size 256k;
    }
}

Exemple de configuration : accès à Portainer

upstream portainer_backend {

  server dkm-webapps.sipr-dc.ucl.ac.be:9000;
}

server {

  listen 80;
  listen [::]:80;

  server_name portainer.apps.sisg.ucl.ac.be;

  # Redirect all HTTP requests to HTTPS with a 301 Moved Permanently response.
  return 301 https://$host$request_uri;

}

server {

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

  include /etc/nginx/uclinclude/ssl/apps-ssl.conf;

  server_name portainer.apps.sisg.ucl.ac.be;

  location / {

    include /etc/nginx/uclinclude/access/apps.access.conf;

    proxy_pass http://portainer_backend;
    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 X-NGINX-UPSTREAM dkm;
    proxy_set_header Host $host;
  }
}

Notes :

  • Pour ce proxy, il a fallu obtenir des certificats SSL avec wildcard. La méthode à utiliser pour créer le fichier CSR (Certificate Signing Request) se trouve dans les documents annexes.
  • Les configurations SSL peuvent être générées facilement en se rendant sur le site https://ssl-config.mozilla.org/
  • Les modèles et fichiers de configurationpour le proxy frontal se trouvent dans les documents techniques.

5.2. Proxy sur les workers

En plus du proxy frontal discuté plus haut, sur chaque nœud worker, un conteneur NGINX est déployé afin de diriger les requêtes vers le bon service et la bonne stack du Swarm.

Les services devant être accessibles depuis l’extérieur sont connectés sur un réseau docker de type overlay spécifique : UCLouvainProxy.

Cela ajoute une couche d’isolation en n’exposant vers l’extérieur que les ports nécessaires pour l'accès aux applications et pas ceux de l’ensemble des services d’une stack.

Le proxy des workers est un conteneur déployé via les commandes de gestion de Docker Swarm. Il est défini dans le fichier docker-compose-stack.yml :

version: "3.5"
services:
  proxy:
    image: dkm-webapps.sipr-dc.ucl.ac.be:5000/dkhproxy:latest
    deploy:
      replicas: 2
      placement:
        constraints:
          - node.role!=manager
    build:
      context: .
      cache_from:
        - dkm-webapps.sipr-dc.ucl.ac.be:5000/dkhproxy:latest
    env_file:
      - dkh_proxy.env
    ports:
      - target: 80
        published: 8080
        protocol: tcp
        mode: host
      - target: 443
        published: 8443
        protocol: tcp
        mode: host
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - type: bind
        source: ./entrypoint.sh
        target: /usr/local/bin/start_container.sh
      - type: bind
        source: ./uclouvain.d/
        target: /etc/nginx/sites-enabled/
      - type: bind
        source: ./tpl
        target: /templates
    networks:
      - UCLouvainProxy
networks:
  UCLouvainProxy:
    external: true
    name: UCLouvainProxy

Afin de pouvoir automatiser le déploiement des applications, les configurations des sites accessibles via le proxy sont générés grâce à un système de templates simple basé sur la commande sed.

Template de configuration de site :

# tpl/site.tpl
upstream ${site_name} {

  server ${service_name};
}

server {

    listen       80;
    server_name  ${site_name}.${host_name};

    client_max_body_size 128M;

    location / {

        proxy_pass http://${site_name};
        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 X-NGINX-UPSTREAM ${stack_name};
        proxy_buffer_size 128k;
        proxy_buffers 4 256k;
        proxy_busy_buffers_size 256k;
        proxy_set_header Host $host;
    }
}

Script de génération des configurations avec sed :

#generate sites config
serverconf() {

  NETWORK_NAME=UCLouvainProxy
  PROXY_NETWORK_ID=$(docker network inspect -f {{.Id}} $NETWORK_NAME )
  SERVICES=$(docker service ls --format {{.Name}})
  EXCLUDE_SERVICE=frontend_proxy
  HOST_NAME="apps.sisg.ucl.ac.be"

  for SERVICE in $SERVICES
  do
    if [ $SERVICE != $EXCLUDE_SERVICE ]
    then
      NETWORK_INSPECT=$(docker service inspect $SERVICE)

      if [ -n "$NETWORK_INSPECT" ]
      then

        NETWORKS_IDS=$( echo $NETWORK_INSPECT | jq -r '.[] | {Spec} | .[] | {TaskTemplate} | .[] | {Networks} | .[] | .[] | {Target} | .[]')

        for NETWORK_ID in $NETWORKS_IDS
        do
          if [ $NETWORK_ID = $PROXY_NETWORK_ID ]
          then
            STACK_NAME=${SERVICE%"_webserver"}
            sed -e "s/\${service_name}/$SERVICE/; s/\${site_name}/$STACK_NAME/; s/\${host_name}/$HOST_NAME/" ./tpl/site.tpl > ./apps.d/$SERVICE.conf
          fi
        done
      fi
    fi
  done

  reload
}

Les configurations de site sont générées dans le répertoire uclouvain.d monté comme volume dans le conteneur du proxy.

Ces configurations sont chargées automatiquement par Nginx lors de son démarrage ou via la commande reload fournie par le script d’automatisation :

reload () {
  echo "-- Reload Nginx Server"
  stack-exec frontend_proxy service nginx reload
}

La commande stack-exec permet d’exécuter une tâche sur un conteneur sans savoir sur quelle machine du cluster il se trouve. Elle est décrite dans la partie « Évolution de l’infrastructure de démonstration »

Notes :

6. Installation des outils d’automatisation (Ansible et Jenkins)

  • installer Ansible sur le manager
  • créer l'utilisateur Ansible (ajouter note sur l’échange de clé ssh pas encore fait)
  • installer Jenkins + plugin Ansible puis dire qu’on ne l’utilise pas encore

Outils de monitoring

  • Swarmpit : outil de management et de monitoring pour Docker Swarm
  • Agrégation des logs des conteneurs avec Graylog
  • Monitoring avec cadvisor, prometheus et grafana

Évolution de l’infrastructure au cours du temps

L’infrastructure a beaucoup changé depuis le développement initial du projet.

Ce qui est resté constant

  • Cluster Docker Swarm de 3 hôtes
  • Portainer pour la gestion via une interface web
  • Un Registry docker

Ce qui a changé

L'architecture décrite ici a été testée et modifiée pour l’infrastructure de développement du futur portail en Drupal 8.

Au départ, elle se basait sur le build d’images Docker qui intégraient tous les fichiers sources des applications dans l’image et le chargement des dépendances (ici dépendances PHP via le gestionnaire de paquets Composer) devaient être intégrées soit durant le build, soit au démarrage de l’image. Le registry était utilisé pour sauvegarder les modifications successives faites dans les conteneurs et les images. Ces images étaient alors déployées via l’interface web de Portainer.

Dans un second temps, de l’automatisation a été ajoutée via l’utilisation de Ansible et de docker-py pour déployer les conteneurs. Dans cette configuration, toute modification des fichiers sources nécessitait un build des images et un redéploiement sur l’infrastructure. Cette procédure était complexe, lente, lourde et non-standard et pas tenable sur le long terme !

La solution que j’ai proposée a été de passer à l’usage de docker-compose et de monter le répertoire des applications via un volume à l’intérieur du conteneur. Cela se justifiait par le fait que c’est le standard de facto pour le déploiement des applications disponibles en ligne (sur Github et autres plateformes concurrentes).

Pour permettre le déploiement sur la stack Docker Swarm, deux fichiers de configuration docker-compose sont fournis pour chaque stack de services à déployer :

  • le premier, docker-compose.yml, pour l’exécution en local, la préparation des images et leur publication sur le registry via les sous-commandes dedocker-compose
  • le second, docker-compose-stack.yml, contient en plus les paramètres nécessaires (contraintes de déploiement, réseaux overlay, utilisation du registry privé…) au déploiement dans Docker Swarm via les sous-commandes de docker stack

Cela permet de conserver la possibilité d’un déploiement local sans nécessiter, de cluster Docker Swarm, pour le test et le développement…

Une série de scripts bash sont utilisées afin de faciliter encore le déploiement et, surtout, l’exécution de commandes sur un service de la stack déployée via ssh. En effet, il n’est pas possible de savoir sur quel worker du cluster se trouve un service a priori.

Le cœur de la commande est le code ci-dessous :

stack-exec()
{

  if [ -z $SERVICENAME ]; then
    echo "Missing service name. Abort"
    usage
    exit 1
  fi

  SERVICE="${STACKNAME}_${SERVICENAME}"

  set -e

  echo "-- Execute task on service $SERVICE"

  SERVICE_IDS=$(docker service ps $SERVICE --format "{{.Name}}.{{.ID}}" -q --no-trunc -f "DESIRED-STATE=running")

  for SERVICE_ID in $SERVICE_IDS
  do
    NODE_ID=$(docker inspect -f "{{.NodeID}}" ${SERVICE_ID})

    NODE_IP=$(docker inspect -f {{.Status.Addr}} ${NODE_ID})
    echo "-- Execute task ${@} on container $SERVICE_ID deployed on $NODE_IP "

# here document
ssh -T ${NODE_IP} << EOSSH
docker exec $SERVICE_ID ${@}
EOSSH

  done

}

Problèmes et solutions

En cas de redémarrage de l’infrastructure

Si le système de fichiers NFS n’est pas monté, le cluster n’est pas capable de relancer les services.

Après un crash de l’infrastructure :

Il faut relancer le service NFS sur la manager

sudo systemctl restart nfs-kernel-server nfs-server

Ensuite sur chaque worker, il faut remonter le système de fichiers partagé :

sudo mount /dockerdata-ceph

Puis relancer Docker :

sudo systemctl restart docker

De plus, Portainer perd les informations des workers sur son dashboard, il faut donc mettre à jour les Endpoint via l’interface web.

Il faudra automatiser cette procédure.

Configuration des conteneurs

  • timezone

    • via DockerFile : ENV TZ="Europe/Brussels"
  • via docker-compose :

    environment:
      - TZ="Europe/Brussels"
    
  • logs de nginx et php-fpm

  • config buffer nginx :

    proxy_buffer_size 128k;
    proxy_buffers 4 256k;
    proxy_busy_buffers_size 256k;
    

Dépendance entre conteneurs

via docker-compose :

services:
  web:
    # ...
    depends_on:
      - "php-fpm"
      - "database"

utilisation d’un script pour attendre la disponibilité d’un service :

services:
  web:
    # ...
    command: ["./wait-for-it.sh", "php-fpm:9000", "--", "nginx", "-g", "daemon off;"]

Configurations diverses

Par défaut, certaines configurations de nginx ne sont pas suffisantes. C’est par exemple le cas de la taille maximum des requêtes acceptées par nginx via la directive client_body_max_sizeet dont la valeur par défaut est de 2Mo. Cela signifie que quelles que soient les limites de taille d'upload de fichiers fixés par les applications, les proxies nginx (frontend ou dans les stacks) ainsi que les services nginx des stacks elles-mêmes, empêcheront tout upload d’un fichier de taille supérieure à 2Mo.

Il est très simple de résoudre le problème en ajoutant la directive client_body_max_size dans les configurations des sites. Pour l'infrastructure, elle a été fixée à 128Mo pour les sites Drupal ou hébergeant un autre CMS, mais elle pourra être modifiée si nécessaire en production.

Ce qu’il reste à faire

Automatiser l’instanciation du cluster et l’ajout de nœud workers supplémentaires avec Ansible. Cette question sera traitée dans la section « Automatisation de l’instanciation du cluster avec Ansible ».

Automatiser le déploiement d’application :

  • Utiliser la composition de fichiers docker-compose pour alléger la configuration des stacks
  • Automatiser le déploiement des stacks avec ansible afin de pouvoir l’exécuter depuis une application distante (par exemple Jenkins, Gitlab, ou via des webhooks…)

Ces questions seront traitées dans le chapitre « Livraison continue et déploiement automatique d’applications sur le cluster ».

Gérer la perte du manager.

La perte du manager provoque actuellement un downtime de l’ensemble de l’infrastructure car

  1. On perd la machine qui assure la gestion du cluster.
  2. On perd le partage du système de fichiers NFS entre les nœuds du cluster. Si le manager tombe, les workers perdront donc l’accès à leurs données.

Cette question sera abordée plus en détail dans le chapitre des perspectives consacré à la mise en place d'une infrastructure de production.

Monter les volumes NFS directement dans les conteneneurs

Mettre en place des healthchecks spécifiques pour certains conteneurs (Drupal...).

Automatiser l'activation des applications dans les proxies avec Ansible et docker service plutôt que sed et la commande stack-exec, ce qui permettrait de mettre à jour les services nginx des proxies depuis le manager et sans avoir à se connecter sur les conteneurs.

Résolution de problèmes sur l’infrastructure

En cas de problème sur l’infrastructure.

Tous les conteneurs sont instanciés sur un seul des 2 workers

Vérifier le montage NFS sur le worker qui n’a reçu aucun conteneur, si absent sudo mount -a

Un seul replica du proxy des workers a été instancié

Idem que pour le problème précédent

Aucun conteneur ne se lance à part les conteneurs système (registry, portainer...)

C’est probablement un problème avec le serveur NFS sur le manager

  • relancer le serveur NFS sur dkm-webapps systemctl restart nfs-server
  • remonter système de fichiers sur dkh-webapps1-2 sudo mount -a

Le service du proxy des workers ne démarre pas

Cela est généralement dû à des configurations de site qui pointent vers des services absents et empêchent nginx de démarrer

  • supprimer toutes les configurations dans /dockerdata-ceph/proxy/uclouvain.d sauf apps.sisg.conf
  • relancer la stack frontend :
    • soit faire un update du service via Portainer
    • soit faire un update du service via le client ligne de commande Docker docker service update --force frontend_proxy
    • soit ré-instancier la stack (cd /dockerdata-ceph/proxy; docker stack rm frontend; docker stack deploy -c docker-compose.yml frontend)
  • une fois le service redémarré, recharger la config des sites via (cd /dockerdata-ceph/proxy/; sudo -u ansible bin/app serverconf)

Erreur 502 persistante

Généralement due à un problème avec un des niveaux de proxy

  • recharger la config nginx sur le proxy frontal via systemctl reload nginx
  • si ça ne suffit pas, recharger la config des sites configurés sur le proxy des workers via (cd /dockerdata-ceph/proxy/; sudo -u ansible bin/app serverconf)

Désactiver et réactiver un service

# désactiver un service
docker service scale <service_name>=0
# activer le service
docker service scale <service_name>=<number_of_replicas>
# si un seul replica
docker service scale <service_name>=1

1

Le choix s’était au départ porté sur une distribution Ubuntu 18.04 LTS présentée comme préférable à Debian pour le déploiement de Docker. Néanmoins, une incompatibilité entre la gestion du DNS dans Ubuntu 18.04 et la version du Docker Engine disponible au moment de la mise en place nous à fait choisir Debian GNU/Linux à la place.

2

Il faudra prévoir un stockage pour /var/lib/docker plus important en production.

3

Il faudra prévoir un stockage supplémentaire pour /var/lib/docker en production.

5

« Installation de docker sous debian et ubuntu » https://docs.docker.com/install/linux/docker-ce/debian/

4

ACL (Access Control List) est un système de gestion fin des droits d’accès aux fichiers. Les droits d’accès y sont définis par des ACE (Access Control Entry) pour un utilisateur ou un groupe. Ce système vient compléter la gestion des permissions POSIX sur les systèmes de type UNIX.

6

Voir la documentation officielle pour plus de détails https://docs.docker.com/engine/swarm/

7

Voir https://container-solutions.com/running-docker-containers-with-systemd/