MSII : Orchestration de conteneurs et intégration continue
Parcours en quatre modules autour du projet FestiPass : conteneuriser des services avec Docker, orchestrer une architecture microservices derrière un reverse proxy, livrer les images en continu depuis un dépôt GitHub, puis livrer l'ensemble en autonomie lors d'un TP de synthèse. Prérequis : pratique courante de Linux, aucune connaissance de Docker.
S'inscrireProgramme complet
Les textes des cours sont en accès libre. Les vidéos et exercices (🔒) sont réservés aux abonnés.
Conteneuriser les services de FestiPass avec Docker
- FestiPass, le point de départ et le but du parcours
Ce parcours part d'un dépôt et d'une contrainte. Le dépôt est celui de FestiPass, la plateforme de billetterie d'un festival de musique : trois services applicatifs déjà écrits, une base de données, un cache, une interface web statique. La contrainte est qu'aucune ligne de code applicatif ne sera modifiée — le code fonctionne, il est fourni tel quel. Ce qui change d'un module à l'autre, c'est uniquement la façon de l'exécuter, de l'exposer et de l'exploiter.
Ce cours suppose une pratique courante de Linux : se déplacer dans une arborescence, lire un fichier de configuration, installer un paquet, consulter les processus, comprendre ce qu'est un port en écoute. Il ne suppose **aucune** connaissance de Docker. Chaque notion nouvelle sera systématiquement rattachée à son équivalent Linux, parce que c'est exactement ce qu'est un [[conteneur|conteneur-docker]] : un assemblage de mécanismes du noyau que vous connaissez déjà, rendus commodes par un outil.
Deux outils suffisent sur la machine pour ce premier module : Git et Docker Engine en version 27 ou supérieure. Sur une distribution Debian ou Ubuntu, on installe depuis le dépôt officiel de Docker et non depuis les paquets de la distribution, dont la version est presque toujours en retard de plusieurs années.
Une précision de sécurité qui compte, et qu'on préfère dire au premier jour : appartenir au groupe `docker` équivaut à disposer des droits root sur la machine. N'importe quel membre de ce groupe peut monter le système de fichiers de l'hôte dans un [[conteneur|conteneur-docker]] privilégié. Ce n'est pas une faille, c'est la conséquence directe de ce que fait le démon Docker — mais cela signifie que l'ajout au groupe `docker` se traite avec la même prudence qu'une entrée dans `sudoers`.
Zéro fichier Docker : c'est bien le point de départ annoncé. À la fin de ce module, le dépôt contiendra `services/catalogue/Dockerfile` et `services/catalogue/.dockerignore`, la base PostgreSQL tournera dans un [[conteneur|conteneur-docker]] dont les données survivent à sa suppression, et le service catalogue joindra cette base par son nom sur un réseau Docker dédié.
- Ce qu'est réellement un conteneur, vu depuis Linux
Une confusion tenace mérite d'être levée immédiatement, car elle fausse tout le raisonnement qui suit : **un [[conteneur|conteneur-docker]] n'est pas une machine virtuelle légère**. Il n'y a pas de second noyau, pas d'hyperviseur, pas de matériel émulé. Un [[conteneur|conteneur-docker]] est un processus ordinaire de votre machine Linux, lancé avec une vue restreinte du système.
Cette vue restreinte repose sur trois mécanismes du noyau Linux, tous antérieurs à Docker et tous utilisables sans lui. Les **[[namespaces|namespace]]** limitent ce que le processus voit : ses propres processus, ses propres interfaces réseau, son propre nom de machine, sa propre arborescence de fichiers. Les **cgroups** limitent ce qu'il consomme : processeur, mémoire, entrées-sorties. Le **système de fichiers en couches** lui fournit une racine construite par empilement d'images en lecture seule.
Plutôt que de croire cette affirmation sur parole, vérifions-la. On lance un [[conteneur|conteneur-docker]] qui ne fait rien d'autre que dormir, puis on l'observe depuis l'hôte avec les outils Linux habituels — `ps`, `/proc`, `lsns`. Ce que l'on voit tranche définitivement la question de la machine virtuelle.
Le processus `sleep` apparaît dans le `ps` de l'hôte avec le PID 48213. Vu de l'intérieur du [[conteneur|conteneur-docker]], ce même processus porte le PID 1 : c'est tout l'effet du [[namespace|namespace]] `pid`. Deux numéros pour un seul et même processus, une seule table de processus dans un seul noyau — la démonstration est faite.
Même version de noyau de part et d'autre : la preuve définitive. Elle a une conséquence pratique importante — un [[conteneur|conteneur-docker]] Linux ne peut pas s'exécuter directement sur un noyau Windows ou macOS. Sur ces systèmes, Docker Desktop démarre discrètement une machine virtuelle Linux, et les conteneurs y tournent. Sur une machine Linux, il n'y a rien de tel : c'est votre noyau qui exécute tout.
Dernier élément de vocabulaire avant de passer à la pratique : la commande `docker` que vous tapez n'exécute rien elle-même. C'est un client qui envoie une requête HTTP au **démon** `dockerd` via le socket Unix `/var/run/docker.sock`. Le démon, lui, parle à un runtime de plus bas niveau (`containerd`, puis `runc`) qui crée effectivement les [[namespaces|namespace]] et lance le processus. C'est pourquoi arrêter le service `docker` n'arrête pas nécessairement les conteneurs en cours.
- Premier conteneur : la base PostgreSQL de FestiPass
FestiPass a besoin de PostgreSQL 16. L'approche classique consisterait à installer le paquet sur la machine, éditer `postgresql.conf` et `pg_hba.conf`, créer l'utilisateur et la base. Nous allons faire autrement : lancer PostgreSQL dans un [[conteneur|conteneur-docker]], à partir d'une image officielle déjà configurée pour ce scénario.
Deux mots sont à distinguer dès maintenant, car tout le reste du module repose sur cette distinction. Une **image** est un modèle figé, en lecture seule, versionné : `postgres:16-alpine`. Un **[[conteneur|conteneur-docker]]** est une exécution de ce modèle, avec sa propre couche inscriptible et son propre cycle de vie. Le rapport est celui du fichier exécutable au processus : une image donne autant de conteneurs qu'on veut.
Chaque option de cette commande mérite d'être comprise plutôt que recopiée. `-d` détache le [[conteneur|conteneur-docker]] du terminal, comme un `&` suivi d'un `disown`. `--name` lui donne un nom stable, sans quoi Docker en génère un aléatoire. `-e` définit une variable d'environnement lue par le script de démarrage de l'image. Et `-p` publie un port : c'est celle qui surprend le plus souvent.
Sans publication de port, le [[conteneur|conteneur-docker]] n'est joignable par aucun client de l'hôte : il vit dans son propre [[namespace|namespace]] réseau, avec ses propres interfaces. C'est le comportement par défaut, et c'est une bonne chose — on n'expose que ce que l'on décide d'exposer. Vérifions que la base répond.
La base tourne, mais elle est vide. Le dépôt fournit `db/init.sql`, qui crée les tables et insère le jeu d'essai du festival. Les images officielles de PostgreSQL exécutent automatiquement tout fichier `.sql` déposé dans `/docker-entrypoint-initdb.d` — mais uniquement lors de la toute première initialisation, quand le répertoire de données est encore vide. Nous allons donc recréer le [[conteneur|conteneur-docker]] en montant ce fichier.
L'option `-v` réalise un **montage lié** (`bind mount`) : un fichier ou un répertoire de l'hôte est rendu visible à un chemin donné dans le [[conteneur|conteneur-docker]]. Le suffixe `:ro` le monte en lecture seule. C'est exactement un `mount --bind` de Linux, appliqué à l'intérieur du [[namespace|namespace]] `mnt` du [[conteneur|conteneur-docker]] — encore une brique que vous connaissiez déjà sous un autre nom.
Un dernier réflexe à prendre dès maintenant : le mot de passe est passé en clair dans la ligne de commande, donc visible dans l'historique du shell et dans `docker inspect`. C'est acceptable pour un poste de développement local et pour cette première prise en main uniquement. Le module suivant traite de la manière correcte de fournir un secret à un [[conteneur|conteneur-docker]].
- Cycle de vie, journaux et intervention dans un conteneur
Administrer un service systemd repose sur une poignée de commandes : `systemctl status`, `journalctl -u`, `systemctl restart`. Administrer un [[conteneur|conteneur-docker]] repose sur un jeu équivalent, qu'il faut acquérir maintenant car toute la suite du cours s'appuie dessus. La correspondance est presque terme à terme.
Une différence structurelle change les habitudes de journalisation : Docker ne collecte que ce que le processus principal écrit sur sa sortie standard et sa sortie d'erreur. Une application qui journalise dans `/var/log/monapp.log` à l'intérieur du [[conteneur|conteneur-docker]] écrit dans un fichier que personne ne lira et qui disparaîtra avec lui. **Journaliser sur la sortie standard n'est pas une préférence de style : c'est le contrat.**
Autre point de vigilance : un [[conteneur|conteneur-docker]] s'arrête dès que son processus principal se termine. Il n'y a pas de notion de service en arrière-plan à l'intérieur. Un [[conteneur|conteneur-docker]] qui lance un démon en mode « fork » et rend la main s'arrête aussitôt — d'où l'option `-g 'daemon off;'` de Nginx ou `-F` de nombreux services. Le processus principal doit rester au premier plan.
`stop` puis `start` conserve tout. `rm` détruit. C'est la nuance la plus coûteuse à apprendre sur le tas, et le chapitre 8 en fait la démonstration en supprimant réellement les données du festival. En attendant, notez que `docker rm -f` combine les deux : il arrête puis supprime, sans confirmation.
L'absence de limite par défaut est un piège classique en production : un [[conteneur|conteneur-docker]] qui fuit emporte la machine entière, y compris les autres conteneurs. Déclarer `--memory` et `--cpus` est le minimum vital, et c'est exactement ce que Compose formalisera dans un fichier versionné plutôt que dans une ligne de commande.
- Images, couches, tags et registre
L'image `postgres:16-alpine` est arrivée sur la machine sans qu'on l'ait explicitement demandée. Comprendre d'où elle vient, comment elle est structurée et comment on la désigne sans ambiguïté est indispensable avant d'en construire une soi-même au chapitre suivant.
Une image est un empilement de **couches** en lecture seule, plus un [[manifeste|manifeste]] décrivant comment les assembler et quelle commande lancer. Chaque couche est un ensemble de différences de fichiers par rapport à la précédente : ajouts, modifications, suppressions. Au démarrage d'un [[conteneur|conteneur-docker]], ces couches sont montées en superposition (`overlayfs`) et une couche inscriptible fine est ajoutée par-dessus.
Le nom complet d'une image comporte quatre parties, dont trois sont presque toujours omises : [[registre|registre-images]], espace de noms, dépôt et référence. Écrire `postgres:16-alpine` revient exactement à écrire `docker.io/library/postgres:16-alpine`. Cette omission est commode mais dangereuse en production : elle rend implicite la dépendance à un [[registre|registre-images]] public qui peut être indisponible, limiter les téléchargements, ou voir un tag réécrit.
La règle professionnelle qui en découle : un tag lisible pour les humains dans les fichiers de développement, un digest épinglé dans les déploiements de production. C'est le seul moyen de garantir que l'image testée en recette est **bit pour bit** celle qui part en production.
Deux réflexes de choix d'image de base, utiles dès le module suivant. Les variantes `-alpine` reposent sur une bibliothèque C différente (musl au lieu de glibc) : très légères, mais quelques logiciels s'y comportent différemment. Les variantes `-slim` restent sur Debian avec glibc, pour un poids intermédiaire. Et l'étiquette `latest` n'a aucune signification particulière : ce n'est qu'un tag par défaut, qui ne désigne ni la version la plus récente ni la plus stable.
- Écrire le Dockerfile du service catalogue
Le service catalogue de FestiPass est une application [[Node|node-runtime?]] 22 qui expose les concerts, les tarifs et les quotas. Elle écoute sur le port 3000 et lit sa configuration dans trois variables d'environnement. Notre travail est de la transformer en image, c'est-à-dire d'écrire la recette de sa construction dans un fichier versionné.
Un détail de ce code a une importance capitale pour la suite : `host: '0.0.0.0'`. Une application qui écoute sur `127.0.0.1` à l'intérieur d'un [[conteneur|conteneur-docker]] n'est joignable par personne — pas même par l'hôte, puisque le [[namespace|namespace]] réseau du [[conteneur|conteneur-docker]] possède sa propre interface de bouclage. C'est la cause numéro un des « ça marche en local mais pas dans le [[conteneur|conteneur-docker]] ».
Un `Dockerfile` est une suite d'instructions exécutées de haut en bas, chacune produisant une nouvelle couche. Cinq instructions suffisent pour cette première version : `FROM` choisit l'image de base, `WORKDIR` fixe le [[répertoire de travail|working-directory]], `COPY` fait entrer des fichiers, `RUN` exécute une commande au moment de la construction, et `CMD` déclare ce qui sera lancé au démarrage du [[conteneur|conteneur-docker]].
Deux précisions sur ce fichier. `EXPOSE 3000` ne publie aucun port — c'est une déclaration d'intention, lue par les outils et par les humains ; seule l'option `-p` de `docker run` publie réellement. Et `CMD` s'écrit sous forme de tableau (`["node", "src/serveur.js"]`) plutôt que de chaîne : la forme tableau lance directement le programme, sans passer par un shell intermédiaire qui intercepterait les signaux d'arrêt.
1,12 Go pour une application de quelques centaines de lignes, et 214 Mo de contexte transférés au démon avant même le début de la construction : les deux chiffres sont anormaux et seront corrigés au chapitre suivant. Vérifions d'abord que l'image fonctionne — ou plutôt, constatons qu'elle ne fonctionne pas encore.
Cette erreur est la plus instructive du module, et elle mérite qu'on s'y arrête plutôt que de la corriger mécaniquement. `PGHOST=localhost` désigne, à l'intérieur du [[conteneur|conteneur-docker]] du catalogue, **le [[conteneur|conteneur-docker]] du catalogue lui-même** — qui n'héberge évidemment aucune base de données. Chaque [[conteneur|conteneur-docker]] possède son propre [[namespace|namespace]] réseau, donc sa propre interface de bouclage.
On pourrait corriger en passant l'adresse `172.17.0.2` relevée plus haut avec `docker inspect`. Cela fonctionnerait — jusqu'au prochain redémarrage, où Docker attribuerait une autre adresse. Ce n'est donc pas une solution, et le chapitre 8 apportera la bonne : un réseau bridge créé explicitement, doté d'un résolveur de noms interne.
- Cache de build, .dockerignore et build multi-étages
Avant de résoudre le problème réseau, réglons les deux anomalies mesurées au chapitre précédent : 214 Mo de contexte et 1,12 Go d'image finale. Les deux ont la même origine — nous avons tout copié sans discernement — et deux remèdes distincts.
Commençons par le contexte. La commande `docker build .` envoie au démon **l'intégralité** du répertoire indiqué, avant d'exécuter la moindre instruction. Le répertoire du catalogue contient un `node_modules/` local hérité d'un développement antérieur : 210 Mo de fichiers inutiles à la construction, transférés puis recopiés par le `COPY . .` dans l'image finale.
Exclure `.env` n'est pas une optimisation mais une mesure de sécurité : un fichier de secrets copié dans une couche d'image y reste **définitivement**, même si une instruction ultérieure le supprime — la couche précédente est toujours là, et quiconque récupère l'image peut l'extraire. C'est la fuite de secret la plus banale du monde des conteneurs.
Passons au cache. Docker réutilise une couche déjà construite tant que l'instruction et les fichiers dont elle dépend sont inchangés. Dès qu'une couche est invalidée, **toutes les suivantes le sont aussi**. Avec `COPY . .` placé avant `RUN npm ci`, la moindre modification d'une ligne de code invalide la copie, donc l'installation des dépendances : vingt secondes perdues à chaque itération.
Reste le poids de l'image. Elle contient tout ce qui a servi à la construire — chaîne de compilation de `node-gyp`, dépendances de développement, sources — alors que l'exécution n'a besoin que du code et des dépendances de production. Le **build multi-étages** résout cela : plusieurs `FROM` dans un même fichier, et seul le dernier étage devient l'image publiée. Les précédents servent d'ateliers, dont on ne retient que quelques fichiers copiés explicitement.
Trois ajouts méritent un commentaire. `USER node` fait tourner l'application sans les droits root : par défaut un [[conteneur|conteneur-docker]] s'exécute en root, et une évasion de [[conteneur|conteneur-docker]] donne alors root sur l'hôte. `--chown=node:node` évite une couche de correction de permissions supplémentaire. Et `HEALTHCHECK` déclare comment savoir si le service va bien — information dont Compose et le reverse proxy se serviront pour retirer une instance défaillante du trafic.
Quarante et une secondes au premier essai, moins de trois secondes après réordonnancement : c'est ce gain, répété des dizaines de fois par jour, qui rend un cycle de développement conteneurisé supportable. Et l'image a été divisée par huit, ce qui se traduira directement en temps de transfert vers le [[registre|registre-images]].
- Durcir l'image : surface d'attaque, privilèges et vulnérabilités
L'image de 141 Mo fait tourner le catalogue, et c'est tout ce qu'on lui a demandé jusqu'ici. Personne ne lui a encore posé la question qui décide de sa mise en production : que se passe-t-il si quelqu'un obtient l'exécution de code à l'intérieur ? Trois propriétés distinctes y répondent — ce que l'image **contient**, sous quelle **identité** elle s'exécute, et ce que le [[conteneur|conteneur-docker]] a le **droit de faire**. Le chapitre précédent en a effleuré une avec `USER node` ; celui-ci traite les trois.
Trois constats, donc. L'image embarque un shell, `apk` et npm — de quoi installer un outil, ouvrir une connexion sortante ou récupérer une charge utile, alors que l'exécution du catalogue n'a besoin d'aucun des trois. Elle démarre sous un utilisateur nommé, ce qui vaut mieux que root, mais `node` n'est qu'une étiquette locale à cette image-là. Et le répertoire du code est inscriptible : un processus compromis peut y réécrire l'application qu'il exécute.
Trivy dresse l'inventaire des paquets présents dans l'image et le confronte aux bases publiques de vulnérabilités. Deux options font toute la différence entre un contrôle tenable et un contrôle qu'on finira par contourner : `--severity CRITICAL,HIGH` fixe le seuil — descendre à `MEDIUM` rendrait le rapport rouge en permanence — et `--ignore-unfixed` écarte les failles sans correctif publié, parce que bloquer sur ce que personne ne peut corriger n'apprend rien.
Les trois failles viennent toutes de la distribution de base ; le second bloc du rapport, celui des dépendances npm, est vide. Le correctif n'est donc pas dans le code de FestiPass : il consiste à reprendre l'image de base à jour, puis à figer ce choix pour qu'il ne rebouge plus tout seul.
Un tag est une étiquette mouvante : `node:22.11-alpine3.21` désignera une autre image au prochain correctif publié. L'empreinte, elle, ne désignera jamais que celle-ci. L'inscrire au `Dockerfile` ne fige pas la sécurité de l'image — elle fige la **date à laquelle on l'a choisie**, ce qui transforme une dérive silencieuse en décision datée, relue au même titre que le reste du code.
`USER 1000:1000` dit la même chose que `USER node`, sous une forme qui ne dépend plus de cette image. C'est aussi la seule que sache lire un contrôle automatique : le module d'[[intégration continue|cicd]] s'en servira pour refuser de publier une image qui démarrerait en root. Un `USER` absent, et l'image démarre sous l'identité root — la valeur par défaut n'est jamais un choix, seulement l'absence de choix.
Le contenu et l'identité sont réglés. Reste la troisième question, qui ne se joue plus dans le `Dockerfile` mais au lancement : ce que le [[conteneur|conteneur-docker]] a le droit de faire. Par défaut, il peut écrire partout dans son système de fichiers, il conserve une quinzaine de capacités du noyau dont il ne se sert pas, et un binaire `setuid` peut y élever ses privilèges. Trois options le referment.
`--read-only` monte tout le système de fichiers en lecture seule ; `--tmpfs /tmp` rend au processus le seul répertoire inscriptible dont il ait besoin, en mémoire, perdu à l'arrêt et monté `noexec` pour qu'on ne puisse rien y lancer. `--cap-drop=ALL` retire les capacités du noyau que Docker accorde par défaut. Et `no-new-privileges` interdit à tout processus d'en gagner après coup, ce qui neutralise les binaires `setuid` de l'image.
Ces trois options se tapent ici à la main, sur une ligne de commande déjà trop longue — c'est la limite de l'exercice, pas celle de la méthode : le module suivant les déclarera dans le fichier qui décrit la stack, une fois pour toutes. Retenez surtout que rien de ce chapitre n'est un réglage définitif : une image n'est pas durcie, elle l'est **à une date**, et c'est la chaîne d'[[intégration continue|cicd]] du module 3 qui se chargera de le revérifier à chaque poussée.
- Volumes et réseaux : persister et faire dialoguer
L'image est petite, à jour et sans privilèges superflus. Restent les deux problèmes ouverts depuis le chapitre 6, et ils se règlent ensemble : les données de la base disparaissent avec le [[conteneur|conteneur-docker]], et le catalogue ne sait toujours pas joindre cette base. Commençons par la démonstration de la perte de données, parce que la constater soi-même vaut mieux que la lire.
Tout a disparu, y compris le schéma. L'explication tient au chapitre 5 : PostgreSQL écrit dans `/var/lib/postgresql/data`, qui appartient à la **couche inscriptible** du [[conteneur|conteneur-docker]] — détruite avec lui. Un [[conteneur|conteneur-docker]] est par nature éphémère, et c'est une propriété voulue : c'est ce qui rend un déploiement reproductible. Il faut donc désigner explicitement ce qui doit survivre.
Les douze concerts ont survécu à la destruction du [[conteneur|conteneur-docker]] : c'est exactement le comportement recherché. Notez au passage que le script d'initialisation n'a pas été rejoué au second démarrage — les images officielles ne l'exécutent que lorsque le répertoire de données est vide, ce qui évite d'écraser une base existante à chaque redémarrage.
Le point décisif est ailleurs : un réseau bridge **créé explicitement** dispose d'un résolveur DNS interne, contrairement au bridge par défaut. Chaque [[conteneur|conteneur-docker]] y est joignable par son nom, sans jamais connaître son adresse. C'est ce qui rendra le fichier de configuration du module suivant indépendant des adresses IP attribuées.
Le but du module est atteint : `{"statut":"ok","base":"up"}`. Le seul changement par rapport à la tentative ratée du chapitre 6 est `PGHOST=festipass-db` au lieu de `localhost`, plus le rattachement au réseau dédié. Le nom du [[conteneur|conteneur-docker]] sert de nom d'hôte, résolu par le serveur DNS interne que Docker place à l'adresse `127.0.0.11` dans chaque [[conteneur|conteneur-docker]] du réseau.
Une dernière remarque pour clore ce module. Tout ce qui vient d'être fait tient dans huit commandes très longues, tapées à la main, dont rien n'est versionné : la mémoire de cette infrastructure est dans l'historique de votre shell. Reproduire ce résultat sur une autre machine relève de la transmission orale. C'est précisément le problème que règle le module suivant, en décrivant la stack entière dans un fichier versionné avec le code — et en y ajoutant les deux autres services, le cache et le reverse proxy.
Orchestrer FestiPass en microservices derrière un reverse proxy
- Lire l'architecture de FestiPass avant de l'orchestrer
Le module précédent s'est terminé sur un constat : la stack FestiPass existe, mais uniquement dans l'historique de votre shell. Huit commandes longues, un ordre de lancement à retenir, aucune trace versionnée. Avant de corriger cela, prenons dix minutes pour regarder ce que nous orchestrons — parce qu'orchestrer une architecture qu'on n'a pas lue produit un fichier illisible.
Le découpage suit les capacités métier du festival, pas les couches techniques. Gérer une programmation et gérer des ventes sont deux métiers distincts, avec des rythmes d'évolution et des profils de charge très différents : le catalogue est lu massivement pendant toute la campagne, la billetterie est sollicitée par rafales à l'ouverture des ventes. Ce sont ces deux caractéristiques, et non la taille du code, qui justifient la séparation.
Une nuance importante sur la base de données. FestiPass n'utilise **qu'une seule instance PostgreSQL**, mais avec deux schémas cloisonnés et deux utilisateurs sans droits croisés : le catalogue ne peut pas lire les commandes, la billetterie ne peut pas écrire dans les concerts. La règle « une base par service » porte sur l'accès, pas sur la colocation d'infrastructure — mutualiser l'instance en développement est parfaitement légitime.
Ce choix de mode est une décision d'architecture, pas une préférence technique. La règle appliquée dans FestiPass est simple : **ce qui conditionne la réponse à l'utilisateur est synchrone, tout le reste est asynchrone**. Le quota conditionne l'acceptation de la commande, donc il est vérifié en synchrone. Le courriel de confirmation ne conditionne rien, donc il part en file — et l'utilisateur n'attend pas le serveur de messagerie pour voir sa commande validée.
- Du docker run au compose.yaml
Docker Compose décrit dans un fichier [[YAML|yaml]] ce que l'on tapait en ligne de commande : les services, leurs images, leurs variables, leurs volumes, leurs réseaux. Le gain n'est pas seulement le confort de frappe — c'est que l'infrastructure devient un artefact versionné, relu en revue de code, et reproductible à l'identique sur n'importe quelle machine.
Le fichier s'appelle `compose.yaml` et se place à la racine du dépôt. Deux remarques de vocabulaire : l'ancienne clé `version:` en tête de fichier est obsolète depuis la spécification Compose et provoque un avertissement ; et la commande moderne est `docker compose` en deux mots, `docker-compose` étant l'ancien binaire Python.
Chaque clé correspond exactement à une option du module précédent. `image` remplace le dernier argument de `docker run`, `environment` remplace les `-e`, `volumes` les `-v`, `ports` le `-p`, `networks` le `--network`. La clé `build` est nouvelle : elle indique le répertoire contenant le `Dockerfile`, ce qui permet à Compose de construire l'image si elle n'existe pas encore.
Le point le plus important est invisible dans le fichier : **le nom du service devient le nom d'hôte**. Le catalogue joint la base par `PGHOST: db` parce que le service s'appelle `db`, et Compose crée automatiquement le réseau et l'entrée DNS correspondante. Plus besoin de `docker network create` ni de `--name` : le fichier est la seule source de vérité.
Une commande a remplacé huit. Notez le préfixe `festipass-` sur les conteneurs et `festipass_` sur les réseaux et volumes : il vient de la clé `name:` en tête de fichier et isole ce projet de tout autre projet Compose sur la même machine. Sans cette clé, Compose utiliserait le nom du répertoire courant, ce qui rend le comportement dépendant de l'endroit où le dépôt a été cloné.
Attention à une nuance de `down` qui coûte cher quand on l'apprend en production : par défaut, la commande supprime les conteneurs et les réseaux mais **conserve les volumes nommés**. C'est `down -v` qui détruit aussi les données. Inversement, `down --rmi local` supprime les images construites. Ces deux options ne se tapent jamais par réflexe.
- Sondes de santé et ordre de démarrage
Le fichier précédent contient un défaut que la chance a masqué : rien ne garantit que la base soit prête quand le catalogue démarre. Sur une machine chargée, PostgreSQL met plusieurs secondes à initialiser son répertoire de données, et le catalogue échoue au premier accès. Reproduisons le problème plutôt que d'attendre qu'il survienne en démonstration.
Le réflexe naturel est d'ajouter `depends_on: [db]`. Il est insuffisant, et c'est un des malentendus les plus répandus : sous sa forme courte, `depends_on` garantit uniquement que le [[conteneur|conteneur-docker]] `db` a été **démarré**, pas que PostgreSQL à l'intérieur accepte des connexions. Entre les deux, il s'écoule plusieurs secondes.
Compose a bien attendu 18 secondes que la base soit `healthy` avant de lancer le catalogue. Trois paramètres de la [[sonde|sonde-sante]] méritent d'être compris. `start_period` accorde un délai de grâce au démarrage pendant lequel un échec ne compte pas — sans lui, un service lent à démarrer est déclaré malade puis redémarré en boucle. `interval` fixe la fréquence des vérifications. `retries` le nombre d'échecs consécutifs avant bascule en `unhealthy`.
Enfin, `restart: unless-stopped` demande à Docker de relancer le [[conteneur|conteneur-docker]] après un plantage ou un redémarrage de la machine, sauf s'il a été arrêté explicitement. C'est l'équivalent du `Restart=` d'une unité systemd, et il devrait figurer sur tout service destiné à tourner en continu.
- Configuration et secrets : sortir les mots de passe du fichier
Le mot de passe de la base est écrit en clair dans un fichier destiné à être versionné : c'est le défaut le plus grave du fichier actuel. Avant de le corriger, posons la distinction qui structure tout le sujet. Une **configuration** est une valeur qui change selon l'environnement mais dont la divulgation est sans conséquence — un nom d'hôte, un port, un niveau de journalisation. Un **secret** est une valeur dont la divulgation compromet le système.
La conséquence pratique est nette : la configuration peut vivre dans le dépôt, le secret jamais. Et un secret ne doit pas non plus se retrouver dans une image — le module précédent a montré qu'un fichier copié dans une couche y reste définitivement, même supprimé ensuite. Trois mécanismes sont disponibles, du plus simple au plus rigoureux.
Le secret est monté en fichier, lisible uniquement par l'utilisateur du [[conteneur|conteneur-docker]], absent de l'environnement et absent de `docker inspect`. C'est la différence majeure avec une variable d'environnement : celle-ci est visible par tout processus du [[conteneur|conteneur-docker]], apparaît dans les traces d'erreur de nombreux langages, et se retrouve régulièrement recopiée telle quelle dans les journaux.
- La billetterie : appel synchrone entre deux services
Le service billetterie enregistre les commandes. Avant d'accepter une vente, il doit vérifier auprès du catalogue que le concert existe et que son quota n'est pas épuisé. C'est le premier appel de service à service du système, et il illustre exactement le couplage temporel décrit dans le magistral : la billetterie ne peut pas répondre si le catalogue ne répond pas.
Le `Dockerfile` de la billetterie est structurellement identique à celui du catalogue — même langage, même structure de projet. C'est un cas fréquent et il ne faut pas y voir de la duplication inutile : chaque service reste maître de sa recette de construction, ce qui lui permet d'évoluer sans négocier avec les autres.
Observons maintenant la conséquence du couplage temporel — c'est l'expérience la plus formatrice du module. Arrêtons le catalogue et regardons ce que devient la billetterie.
Deux enseignements. Le premier : le délai d'attente a transformé un blocage indéfini en erreur nette et rapide — c'est la protection la moins chère et la plus efficace contre la cascade d'effondrement. Le second : la panne est restée **confinée à la fonctionnalité concernée**. Consulter ses commandes fonctionne encore. C'est exactement le bénéfice qu'on attend d'un découpage réussi.
- La notification : communication asynchrone par file Redis
Après un paiement accepté, FestiPass envoie un courriel de confirmation. Le faire de façon synchrone serait une faute d'architecture : le festivalier attendrait le serveur de messagerie pour voir sa commande validée, et une indisponibilité du fournisseur de courriel ferait échouer des ventes déjà payées.
Le travailleur de notification est écrit en Python. C'est l'occasion de vérifier une promesse du style microservices : chaque service choisit sa technologie. Pour l'[[orchestrateur|orchestrateur]], un [[conteneur|conteneur-docker]] Python et un [[conteneur|conteneur-docker]] [[Node|node-runtime?]] se traitent exactement de la même façon — même cycle de vie, même journalisation, même [[sonde|sonde-sante]].
`PYTHONUNBUFFERED=1` n'est pas un détail : sans cette variable, Python tamponne sa sortie standard, et les journaux du [[conteneur|conteneur-docker]] n'apparaissent qu'après plusieurs kilo-octets accumulés — ce qui donne l'impression d'un service muet, puis d'un service bavard par à-coups. C'est un piège classique de la conteneurisation d'applications Python.
Ce comportement est la contrepartie directe du couplage rompu : le travailleur peut être arrêté, redéployé, mis à l'échelle, sans qu'aucune vente n'échoue. En échange, l'émetteur ne sait pas si le courriel est parti — c'est la cohérence éventuelle, et elle doit être assumée dans l'interface plutôt que masquée.
- Le reverse proxy : un point d'entrée unique avec Traefik
L'état actuel fonctionne, mais il n'est pas exposable. Le catalogue publie le port 3001, la billetterie 3002, l'interface web devrait publier le 8080 : trois adresses différentes, trois configurations à mémoriser côté navigateur, et une politique de partage de ressources entre origines à écrire pour chacune. Multipliez par le nombre de services et le modèle s'effondre.
Un **reverse proxy** est un serveur placé devant les services : il reçoit toutes les requêtes venues de l'extérieur et les redirige vers le service concerné, selon le nom d'hôte demandé, le chemin de l'URL ou tout autre critère. « Reverse » — inverse — parce qu'un proxy classique agit pour le compte du client, tandis que celui-ci agit pour le compte des serveurs.
Plusieurs logiciels remplissent ce rôle. Nginx et HAProxy sont les valeurs sûres, éprouvées et très performantes, mais leur configuration vit dans un fichier qu'il faut recharger à la main. Traefik est né avec les conteneurs : il interroge en continu le démon Docker et se reconfigure seul quand un [[conteneur|conteneur-docker]] apparaît ou disparaît. C'est ce qui en fait le meilleur choix pédagogique ici — et le principe que reprennent les orchestrateurs de [[cluster|cluster]].
La mise en place introduit un second réseau. Jusqu'ici tous les services partageaient `dorsale`. Nous allons créer `frontal`, auquel seuls le proxy et les services exposés au public appartiennent — la base, le cache et le travailleur de notification resteront sur `dorsale` uniquement, donc hors d'atteinte depuis l'extérieur, quoi qu'il arrive au proxy.
Deux lignes de ce bloc portent une décision de sécurité. `exposedByDefault=false` impose de déclarer explicitement chaque service à router : sans elle, Traefik exposerait aussi la base de données. Et `internal: true` sur `dorsale` interdit toute route sortante vers l'extérieur depuis ce réseau — un [[conteneur|conteneur-docker]] compromis ne pourra pas exfiltrer de données vers Internet.
Le montage de `/var/run/docker.sock` mérite un avertissement. Donner accès au socket Docker, même en lecture seule, revient à donner un pouvoir considérable au [[conteneur|conteneur-docker]] qui le reçoit — c'est pour cette raison qu'en production on interpose un mandataire de socket qui n'autorise que les appels de lecture strictement nécessaires. En développement local, le montage direct est acceptable ; en production, il ne l'est pas.
- HTTPS, middlewares, mise à l'échelle et dégradation
Le trafic circule en clair. Terminer le TLS au proxy est le geste le plus rentable du module : un seul certificat, un seul point de renouvellement, et les services internes continuent de parler HTTP en clair sur un réseau isolé — ce qui simplifie leur code et leur diagnostic.
En production, Traefik obtient et renouvelle seul un certificat auprès de Let's Encrypt. En local, le domaine `festipass.localhost` n'est pas validable publiquement : nous générons donc un certificat signé par une autorité locale, que nous déclarons au proxy par un fichier de configuration dynamique.
Ce fichier illustre la distinction centrale de Traefik : la configuration **statique** (options de démarrage : points d'entrée, fournisseurs) ne peut changer sans redémarrage, tandis que la configuration **dynamique** (routeurs, services, middlewares, certificats) est rechargée à chaud. Confondre les deux est la source d'erreur la plus fréquente chez qui découvre l'outil.
Passons à la mise à l'échelle. Le catalogue est le service le plus lu du système : c'est celui qu'il faudra multiplier quand la campagne de vente s'ouvrira. La question intéressante n'est pas de savoir lancer trois instances, mais de savoir **ce que le proxy en fait sans qu'on le lui dise**.
Deux requêtes par instance : la répartition s'est faite seule. Aucune ligne de configuration n'a été ajoutée au proxy — Traefik a détecté les nouveaux conteneurs par leurs labels et les a ajoutés à la rotation. C'est exactement ce que la découverte côté serveur promet, et c'est ce qu'un [[orchestrateur|orchestrateur]] de [[cluster|cluster]] appelle un `Service`.
Vérifions maintenant le retrait automatique d'une instance défaillante — c'est la vraie valeur de la [[sonde|sonde-sante]] déclarée plus haut. Nous arrêtons brutalement une des trois instances et observons ce qui arrive aux requêtes suivantes.
Les deux dernières lignes sont volontairement rouges : elles nomment les points de défaillance uniques qui subsistent. Les nommer explicitement vaut mieux que de les découvrir en incident — et c'est le seul traitement honnête tant que tout tient sur une seule machine.
Une limite de fond de Compose pour terminer : tout ceci tourne sur **une seule machine**. Compose ne sait ni répartir des conteneurs sur plusieurs serveurs, ni redémarrer ailleurs ce qui tombe avec une machine — c'est le périmètre d'un [[orchestrateur|orchestrateur]] de [[cluster|cluster]], hors du programme de cette formation. Un point reste intact avant le TP : les images de ce `compose.yaml` sont toujours construites à la main. C'est l'objet du module suivant.
Livrer les images de FestiPass en continu
- Le dépôt GitHub, et la fin de l'image construite à la main
Le module précédent s'achève sur un `compose.yaml` qui décrit huit services et quatre `Dockerfile` qui savent les construire. Tout cela est versionné, relu, reproductible — sauf l'essentiel : les images. Elles n'existent que dans le cache Docker de votre poste, parce que c'est vous qui avez tapé `docker build`.
Trois conséquences qu'aucune relecture de `compose.yaml` ne rattrape. Rien ne garantit que l'image exécutée corresponde au code du dépôt. Personne d'autre ne peut la reproduire à l'identique. Et rien n'a jamais vérifié qu'elle démarre ailleurs que chez vous.
L'objectif du module tient en une phrase : à la fin, aucune image de FestiPass ne sera plus construite à la main. Une poussée déclenche une chaîne qui valide, éprouve, construit et publie les cinq images sur le [[registre|registre-images]] de GitHub, chacune étiquetée par l'empreinte du [[commit|commit]] qui l'a produite.
Le dépôt vit désormais sur GitHub, et ce choix n'est pas affaire de confort. L'automatisation y est un service du dépôt lui-même — aucun serveur à installer ni à maintenir — et le [[registre d'images|registre-images]] `ghcr.io` est adossé au même compte, avec les mêmes droits. Une seule autorisation à accorder au lieu de deux.
Le dépôt est en place, mais l'y déposer ne suffit pas : à partir de maintenant, **ce qui n'est pas versionné n'existe pas**. La machine qui construira les images ne verra rien d'autre que le contenu du dépôt. Vérifions donc que tout ce qui sert à la construction s'y trouve, et que ce qui n'a rien à y faire en reste dehors.
Reste un détail qui n'en est pas un : le message de [[commit|commit]]. Il ne sera pas décoratif ici — au dernier chapitre, c'est lui qui décidera de ce qui mérite une version. Nous adoptons donc dès la première poussée la convention `type(portée): sujet`, avec un type pris dans une liste fermée.
Le dépôt est prêt, la branche de travail est ouverte, la convention est posée. Il n'y manque que le fichier qui dit à GitHub quoi faire de tout cela.
- Un premier workflow : ce que GitHub exécute à votre place
Le vocabulaire tient en quatre mots, et il vaut mieux les fixer maintenant. Un **workflow** est un fichier [[YAML|yaml]] posé dans `.github/workflows/` ; un **déclencheur** dit à quelle occasion il s'exécute ; un **job** est une suite d'**étapes** exécutées sur une machine neuve, l'**exécuteur**. Deux jobs sont indépendants sauf mention contraire, et chacun démarre sur sa propre machine.
Trois décisions sont déjà prises dans ces vingt lignes. Le workflow ne se déclenche pas sur n'importe quelle branche, sans quoi chaque expérimentation consommerait du temps de calcul. Il déclare ses droits au minimum, `contents: read`, et non le jeton complet accordé par défaut. Et sa première étape récupère le dépôt : sans elle, la machine est vide.
La version de l'exécuteur est fixée à `ubuntu-24.04` plutôt que `ubuntu-latest` : l'étiquette mouvante changera de système sous vos pieds le jour où GitHub la fera basculer, et la chaîne se mettra à échouer sans qu'une seule ligne du dépôt ait bougé.
Premier échec, et il est instructif : la machine ne trouve pas `secrets/pg_motdepasse.txt`. C'est exact, et c'est voulu — ce fichier est ignoré par Git depuis le module précédent. La leçon est celle annoncée : **la chaîne ne voit que le dépôt**. Ce qui n'est pas versionné doit être fourni autrement, ou ne pas être requis à cette étape.
Un dernier réglage avant de passer aux vraies étapes. Sans précaution, trois poussées rapprochées lancent trois exécutions concurrentes de la même branche, dont seule la dernière a un sens. Le bloc `concurrency` annule les précédentes.
La chaîne existe et elle est verte. Elle ne vérifie encore qu'une chose, et la moins coûteuse — c'est précisément par là qu'il fallait commencer.
- Refuser tôt : contrôle des Dockerfile et tests des services
Une chaîne d'intégration se conçoit dans l'ordre du coût. Analyser un `Dockerfile` prend deux secondes, exécuter la suite de tests d'un service en prend trente, construire cinq images en prend plusieurs minutes. Placer le contrôle le plus rapide en tête, c'est rendre son verdict avant d'avoir dépensé quoi que ce soit.
Le premier étage pose un problème pratique : cinq `Dockerfile` à analyser, et personne n'a envie d'écrire cinq fois la même étape. Une **matrice** décline un job en autant d'exécutions parallèles qu'il y a de valeurs — même définition, une machine par service.
Le linter reproche la même chose partout : des versions non fixées. `apk add curl` installe la version du jour, qui ne sera pas celle de demain — l'image cesse d'être reproductible sans qu'une ligne du dépôt ait changé. C'est exactement la règle posée pour un TD : les versions se fixent, toujours.
Second étage : les tests. Chaque service porte les siens, écrits dans son propre langage, et le dépôt ne demande qu'à les exécuter. Le job reprend la même matrice, avec une commande par technologie — c'est le seul endroit du module où les cinq services ne se traitent pas rigoureusement de la même façon.
La clé `needs: lint` est ce qui met les deux étages dans l'ordre : sans elle, GitHub lancerait tout en parallèle et paierait les tests d'un service dont le `Dockerfile` est déjà refusé. Le reste de la chaîne s'écrira de la même manière — un `needs` par dépendance réelle, jamais un de plus.
- Construire les images dans un exécuteur vierge
Vient l'étage coûteux. Sur votre poste, une reconstruction du catalogue prend trois secondes : les couches de dépendances sont dans le cache local, seule la dernière est refaite. L'exécuteur, lui, démarre vierge à chaque exécution. Il n'a aucun cache, et il le paie.
Cinq services à ce régime, et la chaîne dépasse les cinq minutes pour une modification d'une ligne. Le remède n'est pas de construire moins, mais de **rendre le cache persistant entre deux exécutions** : Buildx sait l'exporter vers un stockage externe, et GitHub en fournit un, adossé au dépôt.
La clé `scope` mérite un mot : sans elle, les cinq services partageraient un même espace de cache et s'évinceraient mutuellement. Un espace par service, et chacun retrouve ses couches. C'est le pendant exact du réordonnancement des instructions vu au premier module — le cache ne se décrète pas, il se dessine.
Deux minutes quarante-et-une la première fois, vingt-deux secondes ensuite. La chaîne est redevenue utilisable, et surtout : ce gain n'est pas le vôtre, il est celui de tout le monde. Les images existent maintenant — mais uniquement dans un exécuteur détruit à la fin du job. Il faut les publier.
- Publier sur GHCR : jeton, nommage et digest
Le [[registre|registre-images]] retenu est `ghcr.io`, celui de GitHub. Ce n'est pas une préférence : il est adossé au dépôt, ce qui règle d'un coup la question de l'authentification. Le workflow dispose déjà d'un jeton propre à l'exécution ; il suffit de lui accorder le droit d'écrire des paquets.
Il n'y a donc **aucun secret à créer** pour publier. C'est une propriété rare, et elle vaut d'être notée : un mot de passe de [[registre|registre-images]] stocké dans les réglages du dépôt serait un secret de plus à protéger, à faire tourner, et à retirer le jour où quelqu'un quitte l'équipe.
Quatre étiquettes pour une même image, et elles ne se valent pas. `latest` est mouvante par construction : elle désignera autre chose demain. `sha-9f4d2c7` est stable tant que l'historique ne l'est pas. Et sous les deux se trouve le **digest**, l'empreinte du contenu de l'image — qui, lui, ne peut pas mentir.
Retenez ce digest : c'est le seul identifiant que le TP de synthèse acceptera dans le `compose.yaml` de la plateforme. Une étiquette y désignerait une image susceptible de changer sous la stack, ce qui rendrait un incident impossible à rejouer — on ne saurait plus quelle image tournait au moment de la panne.
- Ce que la chaîne doit refuser de publier
Une chaîne qui ne refuse jamais rien n'est pas un contrôle, c'est une décoration. Trois refus vont donc être ajoutés avant la publication, et chacun répond à une question différente : l'image contient-elle une faille connue, démarre-t-elle en root, et contient-elle bien ce qu'elle prétend contenir.
Le premier contrôle s'insère entre la construction et la publication, ce qui impose de scinder l'étape précédente : construire, charger l'image dans l'exécuteur, l'éprouver, puis seulement la pousser. Une image refusée ne doit jamais avoir existé sur le [[registre|registre-images]].
Deux failles, un correctif disponible pour chacune, et la publication est bloquée. La correction ne se fait pas dans le workflow mais dans le `Dockerfile` : l'image de base est remontée d'un cran, ce qui embarque les paquets corrigés. C'est le geste courant, et il vaut mieux qu'il devienne réflexe.
Le seuil retenu — `CRITICAL,HIGH`, correctif disponible uniquement — est une décision, pas une évidence. Le descendre à `MEDIUM` rendrait la chaîne rouge en permanence ; le monter à `CRITICAL` seul laisserait passer l'essentiel. Ce genre d'arbitrage se documente, faute de quoi il se défait au premier incident.
L'image publiée est désormais celle-là même qui a passé les contrôles — pas une reconstruction faite juste après, qui pourrait différer. Ce détail est la moitié de l'intérêt de la manœuvre : contrôler une image et en publier une autre serait un contrôle pour rien.
- Du commit au digest déployable
La chaîne est écrite et elle passe. Il lui manque l'essentiel : être **obligatoire**. Tant qu'une poussée directe sur la branche principale reste possible, tout ce qui précède est facultatif, et ce qui est facultatif finit par être contourné un vendredi soir.
La poussée directe est refusée par le dépôt lui-même, et non par une convention d'équipe qu'il faudrait rappeler. Le travail passe désormais par une demande de fusion, dont la chaîne conditionne l'acceptation.
Reste une faiblesse dans tout ce qui précède : le workflow emploie sept actions écrites par d'autres, désignées par une étiquette. `actions/checkout@v4` désigne aujourd'hui un certain code, et rien n'empêche qu'il en désigne un autre demain — c'est le mécanisme même de plusieurs compromissions récentes.
Dernier ajout : la version. Jusqu'ici toutes les images portent l'empreinte de leur [[commit|commit]], ce qui suffit à les tracer mais ne dit rien de leur maturité. Une étiquette Git annotée, posée à la main, déclenche un second workflow qui republie les mêmes images sous un numéro de version.
Ce que ce module ne fait **pas**, et qu'il faut savoir nommer : rien n'est déployé automatiquement. La chaîne s'arrête au [[registre|registre-images]], volontairement. Le [[déploiement continu|cicd]] — un dépôt qui décrit l'[[état voulu|etat-voulu]] d'une plateforme et un agent qui l'y applique — suppose une cible stable et un [[orchestrateur|orchestrateur]], et sort du périmètre de cette formation.
Vous entrez donc dans le TP de synthèse avec quelque chose que vous n'aviez pas jusqu'ici : cinq images dont vous pouvez dire, pour chacune, de quel [[commit|commit]] elle sort, ce qui a été vérifié avant sa publication, et quel digest la désigne sans ambiguïté. C'est le minimum exigible avant de mettre un système en production.
TP de synthèse : livrer FestiPass de bout en bout
- Cahier des charges, livrables et règles du jeu
Les trois modules précédents vous ont fait dérouler un pas-à-pas. Celui-ci change de posture : vous recevez un cahier des charges, un dépôt, une échéance et une grille d'évaluation. Aucune commande n'est donnée. Ce que vous ne retrouvez pas de mémoire, vous le cherchez dans les modules précédents, dans les cours magistraux référencés ou dans la documentation officielle — c'est exactement la situation professionnelle que ce TP simule.
Le contexte : le festival ouvre sa billetterie dans trois semaines. La direction technique attend une plateforme capable de survivre à la perte d'une instance, de livrer une correction sans interrompre les ventes et d'absorber la ruée du premier jour. Le dépôt est remis à l'état initial, et il contient un service de plus qu'aux jours précédents.
Le service nouveau s'appelle `controle-acces`. À l'entrée du festival, les agents scannent le code QR d'un billet ; le service vérifie qu'il est valide et qu'il n'a pas déjà été utilisé, puis le marque comme consommé. Il est écrit en Go, ce qui n'a jamais été traité dans les modules précédents — et c'est délibéré : vous devez transposer la méthode, pas rejouer un fichier existant.
Trois règles pour ce TP. **Aucune ligne de code applicatif ne doit être modifiée** — si un service ne démarre pas, la cause est dans votre configuration, jamais dans le code fourni. **Tout ce que vous produisez est versionné** dans le dépôt, y compris les fichiers de recette. Et **le travail se fait en binôme**, avec une répartition explicite annoncée en soutenance.
Le formateur passe en revue chaque poste toutes les heures. Vous pouvez poser des questions, mais elles seront systématiquement renvoyées vers le chapitre du cours qui y répond : l'objectif du jour est de savoir retrouver l'information, pas de la recevoir. La seule exception concerne les blocages d'environnement — un démon Docker qui refuse de démarrer, un [[registre|registre-images]] injoignable — pour lesquels l'aide est immédiate.
- Lot 1 — Images : conteneuriser les cinq services et publier
Le premier lot produit les images des quatre services applicatifs, plus l'interface statique. Trois d'entre elles reprennent des structures que vous avez déjà écrites ; la quatrième — le contrôle d'accès en Go — demande une adaptation. Chaque image doit satisfaire six contraintes, toutes vérifiables par une commande.
Le cas Go mérite une indication de cadrage, sans plus. Un programme Go se compile en un **binaire unique**, qui peut être lié statiquement — c'est-à-dire sans dépendance à aucune bibliothèque système. Une image finale peut donc ne contenir que ce binaire, et rien d'autre : ni interpréteur, ni gestionnaire de paquets, ni shell. La limite de 25 Mo n'est pas atteignable autrement.
Trois questions à trancher vous-même sur ce service. Une image sans shell rend le `HEALTHCHECK` du premier module inapplicable — comment déclarer une [[sonde|sonde-sante]] dans ces conditions ? Une image sans `/etc/passwd` ne connaît aucun utilisateur — comment tourner en non-root malgré tout ? Et une image sans autorités de certification ne peut établir aucune connexion TLS sortante — est-ce un problème ici, et si oui comment le résoudre ?
Le lot se termine par la publication, et elle ne se fait pas à la main. Un workflow `.github/workflows/ci.yml` doit construire les cinq images et les publier sur `ghcr.io`, étiquetées par l'empreinte du [[commit|commit]]. Une image poussée depuis votre poste ne vaut aucun point : ce qui est évalué, c'est que la chaîne sache la produire sans vous.
- Lot 2 — Stack Compose complète derrière le reverse proxy
Le deuxième lot assemble les sept composants — quatre services applicatifs, l'interface web, la base, le cache — plus le reverse proxy, dans un `compose.yaml` unique versionné à la racine. C'est le lot le plus proche de ce que vous avez fait avec Compose, et le plus lourd du TP. Trois écarts avec le module 2 : l'ajout du contrôle d'accès, l'exigence de cloisonnement, et le fait que les services publics tournent en plusieurs exemplaires — condition sans laquelle aucune des preuves du lot 3 ne pourra être produite.
Le point 2 introduit une exigence nouvelle : le contrôle d'accès n'est pas public. Deux mécanismes vus avec le reverse proxy permettent de le protéger, et ils se combinent — un middleware d'authentification, et une restriction par plage d'adresses correspondant au réseau du festival. Le choix vous appartient, mais il devra être justifié en soutenance.
Une difficulté vous attend au point 6, et il vaut mieux la connaître : le contrôle d'accès utilise Redis comme **verrou distribué** pour empêcher qu'un même billet soit validé simultanément à deux portes. Si Redis est indisponible, la question n'est plus technique mais métier — refuser toutes les entrées, ou accepter au risque d'un double passage ? Votre configuration doit refléter une décision consciente.
Les points 7 et 8 sont ceux qui coûtent le plus cher à rattraper. Le nombre d'exemplaires doit être **déclaré dans le fichier**, pas obtenu par un `--scale` tapé à la main : ce qui n'est pas versionné n'existe pas au redémarrage suivant. Quant au référencement par digest, il a une conséquence pratique à anticiper — l'empreinte change à chaque reconstruction, et la recopier dans cinq services est une source d'erreur garantie. Une variable par image, définie dans le fichier d'environnement produit par la chaîne, règle le problème.
- Lot 3 — Recette de résilience et panne injectée
Un déploiement qui fonctionne au moment où on le regarde ne prouve rien. Le troisième lot demande d'**établir par l'expérience** quatre propriétés de résilience, chacune avec un protocole reproductible et une trace consignée dans un fichier de recette. C'est le lot qui distingue un système déployé d'un système livrable.
Une mise en garde honnête sur la preuve 1 : elle peut produire une interruption réelle de quelques secondes, le temps que le proxy constate la disparition de l'instance. Ce résultat n'est pas un échec — c'est une mesure. Ce qui est évalué, c'est votre capacité à **expliquer** ce que vous observez, pas à obtenir un résultat parfait.
À 16h00, le formateur intervient sur votre stack et y introduit **une panne unique**, choisie parmi celles que la formation a couvertes. Vous avez trente minutes pour la diagnostiquer et la corriger. La correction compte, mais la moitié des points porte sur le **compte rendu** : le symptôme observé, les commandes exécutées dans l'ordre, ce que chacune a permis d'éliminer, et la cause finale.
La colonne « ce qu'elle a éliminé » est celle qui compte le plus, et c'est la moins spontanée. Un diagnostic efficace n'est pas une suite de commandes lancées au hasard jusqu'à ce que quelque chose apparaisse : c'est une réduction progressive de l'espace des causes possibles, où chaque commande est choisie pour éliminer une hypothèse.
- Rendu, grille d'évaluation et soutenance
Le rendu est le [[dépôt Git|depot-git]] lui-même, sur une branche nommée `tp-<binome>`, avec un historique de commits lisible — un [[commit|commit]] par lot au minimum. Ce n'est pas une exigence cosmétique : un dépôt d'infrastructure dont l'historique ne raconte rien est inexploitable en cas d'incident, quand il faut répondre à « qu'est-ce qui a changé, et quand ? ».
Le fichier `docs/decisions.md` est le seul livrable purement rédactionnel, et c'est celui que les recruteurs lisent en premier dans un dossier technique. Cinq décisions à documenter, en dix lignes chacune : le choix de protection du contrôle d'accès, le comportement retenu en cas d'indisponibilité de Redis, la base conteneurisée ou confiée à un service géré, le nombre d'exemplaires par service, et la stratégie de gestion des secrets. Pour chacune : le contexte, les options envisagées, le choix, et ce qu'il coûte.
La dernière ligne mérite d'être prise au mot. Un binôme qui annonce « le lot 3 est complet sauf la montée en charge, que nous n'avons pas eu le temps de mesurer » obtient plus de points qu'un binôme qui le passe sous silence et se fait démentir par une commande. Savoir dire ce qui n'a pas été testé est une compétence professionnelle, et c'est celle qui distingue le plus nettement les profils en exploitation.
Pour prolonger ce parcours, trois directions valent l'investissement, dans cet ordre. Un **[[orchestrateur|orchestrateur]]** d'abord — Swarm ou [[Kubernetes|kubernetes]] : ce qui manque à Compose est nommé depuis le module 2, à savoir répartir des conteneurs sur plusieurs machines et relever ailleurs ce qui tombe avec l'une d'elles. L'**observabilité** ensuite : collecte centralisée des journaux, métriques et traces, sans laquelle un système distribué reste opaque. La **sécurité de la chaîne d'approvisionnement** enfin : inventaire logiciel, signature des images et vérification avant déploiement.
Tarifs
Prix mensuel
29 € / mois
Durée estimée
1 mois
au rythme standard (29 chapitres)
Coût total estimé
29 €
prix mensuel × durée estimée
