**Toute modification du fonctionnement de la chaîne d'intégration (`.gitlab-ci.yml`) doit être documentée dans ce README.**
*Disclaimer* : cette doc est assez longue et peut faire peur ; elle essaye de prévoir tous les cas d'utilisations et d'expliquer à quelqu'un qui débarque. Mais tl;dr : mettez à jour le Dockerfile, poussez sur le dépôt, attendez que tout soit construit et analysé, testez sur l'instance de test, poussez en production, et lancez le service. C'est pas plus compliqué que ça dans la majorité des cas! **Je vous conseille la section [Exemple](#exemple) pour vous donner une idée ! :)**
-[Des analyses de sécurité ?](#des-analyses-de-scurit-)
-[Étapes manuelles ou automatiques ?](#tapes-manuelles-ou-automatiques-)
-[Mettre à jour un service existant](#mettre-jour-un-service-existant)
-[Procédure standard](#procdure-standard)
-[En cas d'erreur](#en-cas-derreur)
-[Lors de la construction de l'image](#lors-de-la-construction-de-limage)
-[Lors de l'analyse de sécurité statique](#lors-de-lanalyse-de-scurit-statique)
-[Lors de l'analyse de sécurité dynamique](#lors-de-lanalyse-de-scurit-dynamique)
-[Déployer un service](#dployer-un-service)
-[Formalisme du dépôt](#formalisme-du-dpt)
-[Migrer un service à la chaîne d'intégration](#migrer-un-service-la-chane-dintgration)
-[Troubleshooting](#troubleshooting)
-[Impossibilité de pull une image](#impossibilit-de-pull-une-image)
-[Astuces](#astuces)
-[Exemple](#exemple)
<!-- /TOC -->
## Introduction
Ce dépôt centralise les Dockerfiles et autre ressources utilisées pour construire **et** déployer les images Docker tournant en production sur l'infrastructure de Picasoft.
Ce README fait office de documentation pour le fonctionnement et le formalisme de ce dépôt, les concepts ainsi que l'historique ne sont pas expliqués en détail. Pour une plongée plus complète dans les différents concepts abordés ici, on pourra se référer aux documentations officielles le cas échéant. Il n'est pas nécessaire de les lire en entier pour travailler sur ce dépôt : ils seront surtout utiles pour comprendre l'existant.
*[Une introduction à Docker : pourquoi et comment](https://docs.docker.com/engine/docker-overview/)
*[Référence pour l'écriture d'un Dockerfile](https://docs.docker.com/engine/reference/builder/)
*[Bonnes pratiques pour l'écriture des Dockerfile](https://docs.docker.com/develop/develop-images/dockerfile_best-practices/)
*[Orchestrer le lancement de conteneurs avec Docker Compose](https://docs.docker.com/compose/)
*[Référence pour l'écriture de fichiers Docker Compose](https://docs.docker.com/compose/compose-file/)
Ce dépôt utilise la [chaîne d'intégration de Gitlab](https://docs.gitlab.com/ee/ci/), ou **CI**, pour automatiser certaines tâches dès lors que des changements ont lieu sur le dépôt. L'idée est d'automatiser certaines tâches, ce qui permet d'améliorer la sécurité de l'infrastructure, d'unifier les procédures et de garder un historique clair des modifications, comme nous le verrons ci-après.
**Disclaimer : un travail est actuellement en cours pour que l'intégralité des images utilisées en production soit centralisé sur ce dépôt. Il sera rendu public dès que ce sera le cas.**
## Contenu du dépôt
Ce dépôt contient (autant que faire se peut, il existe des exceptions) toutes les ressources permettant de déployer un service sur n'importe quelle machine virtuelle de l'infrastructure de Picasoft.
Cela signifie que le bon fonctionnement d'un service n'est pas dépendant de la machine virtuelle sur laquelle on essaye de le lancer (en adéquation avec la philosophie Docker).
Ainsi, chaque service versionné sur ce dépôt présentera :
* Un Dockerfile permettant de construire l'image,
* Un Docker Compose permettant de lancer le service,
* Si possible, un ou des fichiers de configuration permettant de personnaliser le service selon nos besoins,
* Un fichier d'exemple de secrets (mot de passe...) nécessaire au lancement du service,
* Un `README` résumant les paramètres modifiables sur le dépôt, les mécanismes pour en rajouter, etc.
Il n'y a pas de Docker Compose global : chaque service a son propre Docker Compose.
## Principes de la CI
La CI est déclenchée dès lors qu'une modification susceptible d'altérer un service (c'est-à-dire, une modification conforme au [Formalisme du dépôt](#formalisme-du-dpt) est détectée.
Elle va effectuer les opérations suivantes, automatiquement ou manuellement suivant le contexte (voir [Étapes manuelles ou automatiques ?](#tapes-manuelles-ou-automatiques-)) :
* Initialisation des variables (nom et version de l'image modifiée),
* Construction de l'image Docker et push vers le registre de test,
* Analyses de sécurité,
* Push vers le registre de production.
Chaque étape peut échouer et bloque les étapes ultérieures. Il faudra régler les problèmes et mettre à jour le dépôt afin de relancer la CI.
### Des analyses de sécurité ?
Les analyses de sécurité permettent d'auditer les images Docker que nous construisons, afin de prévenir un maximum de failles de sécurité sur nos services. En effet, bien que les conteneurs Docker soient en théorie isolés du système lui-même, ils contiennent tout de même des données critiques, utilisateur ou techniques. Il faut donc s'assurer que l'image est suffisamment propre avant de la lancer, d'autant qu'on se base parfois sur des images pré-construites.
Les analyses sont de deux types : statiques ou dynamiques.
L'analyse statique est effectuée par [Clair Scanner](https://github.com/arminc/clair-scanner) : son objectif est d'analyser l'intégralité des paquets installés dans une image Docker et de reporter les vulnérabilités présentes sur ces paquets ([CVE, ou Common Vulnerabilities and Exposures](https://cve.mitre.org/)). Cette analyse ne dit rien sur la sécurité du **service** en lui-même, mais plutôt sur la sécurité des bibliothèques et logiciels dont il dépend.
L'analyse dynamique est effectuée par [Docker Bench for Security](https://github.com/docker/docker-bench-security), qui lance le service en interne et vérifie que le ou les conteneurs lancés respectent une liste de bonnes pratiques. Docker Bench for Security ne fait jamais échouer la CI, même lorsque les bonnes pratiques ne sont pas respectées, car ce n'est pas toujours le cas. Il faut néanmoins tendre vers le respect de ces bonnes pratiques lorsque cela est possible.
### Étapes manuelles ou automatiques ?
Le déploiement sur le registre de production est **toujours** manuel, pour s'assurer que le service fonctionne comme prévu et a été testé par un humain avant de le pousser.
La mise à jour d'un `Dockerfile` entraîne le lancement de l'ensemble de la CI.
La mise à jour d'une liste blanche de CVE (voir [Formalisme du dépôt](#formalisme-du-dpt)) déclenche uniquement l'analyse de sécurité statique sur la dernière image construite (pas de nécessité de reconstruire l'image).
La mise à jour d'un fichier Docker Compose déclenche toutes les analyses de sécurité sur la dernière image construite (pas de nécessité de reconstruire l'image).
La mise à jour d'un fichier autre donne la possibilité de déclencher manuellement les étapes de la CI : on appréciera au cas par cas s'il est nécessaire de reconstruire l'image. Par exemple, mettre à jour le README ou un fichier de secrets d'exemple ne devrait pas déclencher la CI, tandis que mettre à jour un fichier de configuration devrait déclencher la CI.
## Mettre à jour un service existant
Cette section suppose que l'on veut modifier un service qui passe déjà la CI (exemples : Mattermost, Etherpad, Dokuwiki, Backup des BDD...).
Modifier peut vouloir dire :
* Mettre à jour le service (*e.g.* changer un numéro de version dans le Dockerfile),
* Mettre à jour la configuration (éditer un fichier quelconque),
* Mettre à jour la configuration des volumes (*e.g.* changer le point de montage du Docker Compose),
* etc.
### Procédure standard
Il suffit de récupérer ce dépôt, de faire les mises à jour, de commit les changements et de les pousser. Attention, la CI se déclenche en regardant les modifications apportées lors du **dernier commit**. Si vous modifiez le Dockerfile dans un commit, puis le README dans un autre, et que vous poussez le tout, la construction ne se déclenchera pas automatiquement.
Vous pouvez suivre les différentes étapes de la CI dans la section [Pipelines](https://gitlab.utc.fr/picasoft/projets/dockerfiles/pipelines), et il est **indispensable** de lire les logs des différentes étapes en cliquant sur chacune d'entre elles. Si la construction et les analyses de sécurité se passent bien, tous les voyants sont au vert (sauf la dernière étape, qui doit être déclenchée manuellement). Sinon, référez-vous à la section [En cas d'erreur](#en-cas-derreur).
On peut ensuite [Déployer un service](#dployer-un-service).
### En cas d'erreur
Des erreurs peuvent survenir à plusieurs étapes de la CI.
Les logs de chaque étape donnent des informations sur la nature de l'erreur.
#### Lors de la construction de l'image
Il est fort probable que l'erreur viennent d'un Dockerfile mal écrit. En général, on préférera tester le Dockerfile en local avant de le pousser sur le dépôt (`docker build`...).
L'erreur peut aussi venir d'un nom d'image mal formaté dans le Docker Compose (voir [Formalisme du dépôt](#formalisme-du-dpt)).
#### Lors de l'analyse de sécurité statique
Cette analyse échoue lorsque des CVE avec une classification `High` ou plus sont détectées. Les CVE de niveau plus faible étant inévitables, la CI n'échoue pas et se contente de les afficher.
Clair vous indique alors quels paquets sont vulnérables. Plusieurs choix s'offrent à vous : mettre la CVE en liste blanche ou mitiger la CVE.
Une CVE est mise en liste blanche en l'ajoutant au fichier `clair-whitelist.yml`, dont on trouvera un exemple [ici](./pica-mattermost/clair-whitelist.yml). Il faut spécifier le nom de la CVE, le paquet affecté et la raison de la mise en liste blanche. Une mise en liste blanche est acceptable si :
* Clair détecte des vulnérabilités présentes dans des paquets installés dans des conteneurs intermédiaires mais ne faisant pas partie de l'image finale (cas des [multi-stage builds](https://docs.docker.com/develop/develop-images/multistage-build/)). On utilisera le motif *Paquet non présent*,
* Clair détecte des vulnérabilités sur le paquet `linux`. En effet, le noyau utilisé par le conteneur est celui de l'hôte. On utilisera le motif *Vulnérabilité Linux*,
* Le paquet est à jour et ne peut pas être installé dans une version différente à cause de dépendances d'autres paquets,
* Le paquet ne peut pas être supprimé,
* Il n'existe pas de contre-mesure à la vulnérabilité,
* La vulnérabilité, même si elle est classée en criticité `High`, a *peu* de conséquences pour Picasoft. Cela peut être le cas pour une attaque par déni de service causant un usage de 100% du processeur, qui n'aura d'impact que sur Etherpad en raison des limitations de ressources.
Sinon, on règlera la CVE en prenant les mesures nécessaires.
Ensuite, on pousse les modifications et la CI ré-effectuera les tests de vulnérabilités, en reconstruisant l'image si nécessaire.
#### Lors de l'analyse de sécurité dynamique
Il est fort probable que l'erreur provienne du Docker Compose, et que Docker Bench for Security n'arrive pas à lancer votre ou vos conteneur(s), par exemple à cause d'un fichier manquant ou d'un mauvais formattage.
## Déployer un service
Une fois les premières étapes de la CI passées, il est temps de déployer le service.
À ce stade, l'image est construite, saine, et poussée sur le registre de test. Avant de la mettre en production, il faut vérifier que le service se lance bien comme prévu. Pour ce faire, on se rend sur une machine virtuelle de test (exemple : `pica01-test.picasoft.net`), on clone le dépôt ou on se rend dans `/DATA/docker/dockerfiles` s'il existe.
Pour s'assurer que le service est fonctionnel, il faut s'assurer qu'il démarre indépendamment de ce qui existe sur la machine. Le script [`docker_test.sh`](./docker_test.sh) s'occupe de tout cela pour vous. Il suffit de lancer la commande suivante `$ ./docker_test.sh <nom du dossier>` pour effectuer les opérations suivantes :
* Mise à jour du dépôt dans sa dernière version,
* Remplacement des URL en `picasoft.net` par `test.picasoft.net`,
* Création d'un fichier de secrets avec les valeurs d'exemple du dépôt,
* Remise à zéro des volumes Docker existants,
* Suppression des anciennes images,
* Pull de la nouvelle version de(s) l'image(s),
* Lancement de Docker Compose,
* Affichage des logs.
Vous pouvez effectuer ces opérations à la main si vous le souhaitez.
Regardez que les logs ne produisent aucune erreur, et testez si le service fonctionne bien sur l'infrastructure de test. Si ce n'est pas le cas, il faut revoir le Dockerfile ou la configuration.
Lorsque tout est bon, on retourne sur la page [Pipelines](https://gitlab.utc.fr/picasoft/projets/dockerfiles/pipelines), on se rend dans la dernière pipeline et on lance le push de l'image sur le registre de production.
On se rend ensuite sur la machine de production, dans le dossier `/DATA/docker/dockerfiles`. On s'assure que les fichiers de secrets existent et contiennent bien des valeurs **secrètes**, pas celles du fichier d'exemple !
On peut alors utiliser le script [`docker_prod.sh`](./docker_prod.sh), qui automatise quelques étapes. Il suffit de lancer la commande `$ ./docker_prod.sh <nom du dossier>` pour effectuer les opérations suivantes :
* Mise à jour du dépôt dans sa dernière version,
* Création des volumes Docker non-existants,
* Pull de la nouvelle version de(s) l'image(s),
* Lancement de Docker Compose,
* Affichage des logs.
À ce stade, si quelque chose ne fonctionne pas, il y a un sérieux problème.
## Formalisme du dépôt
Pour que la CI et le déploiement des services fonctionne correctement, il faut respecter plusieurs contraintes sur la nomenclature, l'arborescence et le contenu des fichiers. Il ne s'agit pas ici de bonnes pratiques - voir le rapport Docker Bench for Security pour cela.
* Chaque service géré par la chaîne d'intégration est dans un sous dossier `pica-*`,
* Chaque sous dossier contient au moins un `Dockerfile`, un `docker-compose.yml` et un `clair-whitelist.yml`,
* Le nom de l'image à construire est spécifiée dans le `docker-compose.yml`, au format `registry.picasoft.net/<nom image>:<version image>`,
* Les secrets sont répertoriés dans des fichiers `<nom>.secrets.example` dans un sous-dossier `secrets`, avec des valeurs d'exemple,
* Les fichiers de secrets sont injectés dans le conteneur via la directive `env_file`, sans l'extension `.example`,
* Tous les volumes du Docker Compose sont déclarés comme `external` (pour éviter leur suppression lors d'un `docker-compose down`, ce qui serait dramatique),
* Le Docker Compose déclare un réseau Docker externe nommé `docker_default`, pour pouvoir rejoindre le réseau de Traefik.
Un exemple concret peut être trouvé au niveau de [pica-mattermost](./pica-mattermost) ou [pica-etherpad](./pica-etherpad).
## Migrer un service à la chaîne d'intégration
Tous les services répertoriés sur ce dépôt ne passent pas encore la chaîne d'intégration. Pour les migrer dessus, il suffit de modifier les sous-dossiers des services pour qu'ils respectent le [Formalisme du dépôt](#formalisme-du-dpt).
Ensuite, il faudra les supprimer du `docker-compose.yml` global présent dans `/DATA/docker` sur les machines, et les déployer en utilisant des Docker Compose individuels.
## Troubleshooting
Cette section répertorie les problèmes anecdotiques indépendants des modifications des fichiers.
### Impossibilité de pull une image
Parfois, un timeout empêche de pull l'image depuis le registre de test entre deux étapes, avec des messages de timeout ou de type `Unknown blob`. Il faudra dans ce cas redémarrer le registre de test (sur `pica01-test`, probablement) et relancer la chaîne d'intégration.
## Astuces
Si pour une quelconque raison, on souhaite pousser un commit sans déclencher la chaîne d'intégration, on pourra ajouter `[skip ci]` au message du commit.
Il est aussi possible de relancer une pipeline plus ancienne, pour reconstruire une ancienne version de l'image, quand cela est nécessaire, et revenir exactement dans l'état indiqué par le dépôt. C'est l'avantage d'avoir des builds *reproductibles*.
## Exemple
On veut mettre à jour Mattermost de la version 5.18.0 à la version 5.19.0. On fait les modifications dans le Dockerfile et on pousse le commit. Les modifications dépendent évidemment du service, on peut les voir [ici pour l'exemple](https://gitlab.utc.fr/picasoft/projets/dockerfiles/commit/472e0d72534659e215270adade6746b65b6f65e3).
Le [pipeline](https://gitlab.utc.fr/picasoft/projets/dockerfiles/pipelines/54707) se lance automatiquement. Je suis les logs des jobs un par un pour vérifier que tout se passe bien.
La construction de l'image et les analyses de vulnérabilité se passent bien : aucun paquet n'introduit de nouvelle vulnérabilités. L'image est maintenant poussée sur le registre de test.
Je me rends sur `pica01-test` et je teste la nouvelle version de l'image :
```bash
$ ssh qduchemi@pica01-test.picasoft.net
qduchemi@pica01-test:~$ cd /DATA/docker/dockerfiles
File pica-mattermost//secrets/mattermost-db.secrets created
==== Stop and remove existing containers ====
Network docker_default is external, skipping
==== RESET HARD and pull Dockerfiles repository ====
Using branch master, is this correct ? [y/N]
y
HEAD is now at 472e0d7 [Mattermost] Bump to version 5.19.0
Username for'https://gitlab.utc.fr': qduchemi
Password for'https://qduchemi@gitlab.utc.fr':
Already up-to-date.
==== Replace production URL with testing URL in all files ====
Found in ./entrypoint.sh
Found in ./docker-compose.yml
==== Remove and re-create named external volumes ====
mattermost-config
mattermost-config
mattermost-data
mattermost-data
mattermost-plugins
mattermost-plugins
mattermost-db
mattermost-db
==== Remove old images ====
Error: No such image: registry.test.picasoft.net/pica-mattermost:5.19.0
Error: No such image: postgres:9.4-alpine
==== Pull new versions of images ====
Pulling mattermost-db ... done
Pulling mattermost ... done
==== Lauch pica-mattermost/ and restore repository ====
Creating mattermost-db ... done
Creating mattermost-app ... done
HEAD is now at 472e0d7 [Mattermost] Bump to version 5.19.0
==== Print logs (use Ctrl+C to stop)====
Attaching to mattermost-app, mattermost-db
[...]
```
Je vérifie que les logs sont ok, on se rend sur [team.test.picasoft.net](https://team.test.picasoft.net) et on constate que tout fonctionne.
Je se rend de nouveau sur le [pipeline](https://gitlab.utc.fr/picasoft/projets/dockerfiles/pipelines/54707) et je lance manuellement l'étape `push-prod` pour pousser l'image sur le registre de production.
Je me rends ensuite sur la machine de production (`pica02` à ce jour) et je lance la nouvelle version du service :