# Dockerfiles de Picasoft

**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 ! :)**

<!-- TOC depthFrom:2 depthTo:6 withLinks:1 updateOnSave:1 orderedList:0 -->

- [Introduction](#introduction)
- [Contenu du dépôt](#contenu-du-dpt)
- [Principes de la CI](#principes-de-la-ci)
	- [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. 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.

**À noter : un travail est actuellement en cours pour que l'intégralité des images utilisées en production soient centraliséés 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 toutes les ressources permettant de déployer les services que nous maintenons sur n'importe quelle machine virtuelle de l'infrastructure de Picasoft, sans prérequis.

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 contiendra :

* Un `Dockerfile` permettant de construire l'image,
* Un fichier `docker-compose.yml` 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 est susceptible d'altérer un service, c'est-à-dire dès qu'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 nécessaires pour les autres étapes (dossier, 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 bloquer les étapes ultérieures. Il faudra régler les problèmes et mettre à jour le dépôt afin de relancer la CI le cas échéant.

### 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 des données critiques. 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 : statique ou dynamique.

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 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 possible. Il faut néanmoins tendre vers le respect de ces bonnes pratiques.

### Étapes manuelles ou automatiques ?

Le push sur le registre de production est **toujours** manuel, car il faut s'assurer au préalable que le service 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 `docker-compose.yml` 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

Pour 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 **recommandé** 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 testera 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 criticité `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 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 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 d'un mauvais 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`) et on se rend dans `/DATA/docker/dockerfiles`.

Pour que le service réponde à nos critères, 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 `$ ./docker_test.sh <nom du dossier, e.g. pica-mattermost>` 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.

Vérifiez que les logs ne produisent aucune erreur et que le service fonctionne bien sur l'infrastructure de test.

Lorsque tout est bon, on retourne sur la page [Pipelines](https://gitlab.utc.fr/picasoft/projets/dockerfiles/pipelines), et on lance le push de l'image sur le registre de production.

On se rend enfin 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.

Attention : un conteneur noté `Unhealthy` à cause d'un mauvais `HEALTHCHECK` sera **exclu** de Traefik, même s'il fonctionne bien!

## Formalisme du dépôt

Pour que la CI et le déploiement des services fonctionnent 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 final de l'image 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 les 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.

### Erreurs de connexion à la base de données

Si le conteneur `*-app` n'arrive pas à se connecter à la BDD du conteneur `*-db`, il est possible que `*-db` soit en train de tourner avec des anciens identifiants. Pour corriger ce problème **sur la VM de test**, il faudra:

* effacer les conteneurs `*-app` et `*-db`
* effacer leurs volumes
* créer à nouveau leurs volumes
* relancer le script `./docker_test.sh`.

**Note:** Le script s'occupe déjà d'effacer et créer à nouveau les volumes, mais ça ne fonctionne pas dans le cas de `pica-etherpad`, probablement dû à un bug de `docker-compose config --volumes` qui retourne `etherpad-db-volume` et non `etherpad-db`.

## 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.

## 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 voir le commit [472e0d72](https://gitlab.utc.fr/picasoft/projets/dockerfiles/commit/472e0d72534659e215270adade6746b65b6f65e3) pour l'exemple.

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é.

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
qduchemi@pica01-test:/DATA/docker/dockerfiles$ ./docker_test.sh pica-mattermost
Starting procedure for pica-mattermost/...

==== Create dumb secret files ====
	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, je se rend sur [team.test.picasoft.net](https://team.test.picasoft.net) et je constate que tout fonctionne.

Je me rends 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 :
```bash
$ ssh qduchemi@pica02.picasoft.net
qduchemi@pica02:~$ cd /DATA/docker/dockerfiles
qduchemi@pica02:/DATA/docker/dockerfiles$ ./docker_prod.sh pica-mattermost
Starting procedure for pica-mattermost...

==== Pull Dockerfiles repository ====
Using branch  master, is this correct ? [y/N]
y
Username for 'https://gitlab.utc.fr': qduchemi
Password for 'https://qduchemi@gitlab.utc.fr':
Déjà à jour.

==== Pull new versions of images ====
Pulling mattermost-db ... done
Pulling mattermost    ... done

==== Ensure named external volumes are created ====
mattermost-config
mattermost-data
mattermost-plugins
mattermost-db

==== Lauch pica-mattermost ====
Recreating mattermost-db ... done
Recreating mattermost-app ... done

==== Print logs (use Ctrl+C to stop) ====
Attaching to mattermost-app, mattermost-db
```

J'attends que l'application ait démarré, je vérifie que tout est ok sur [team.picasoft.net](https://team.picasoft.net).

La mise à jour est terminée ! :)