Aller au contenu

Lab : construire un serveur d'intégration continue

Prérequis

Testé avec compose 5.5.1 docker 29.8.1 git 2.43.0 postgresql 18.6 python 3.14.7 registry 3 , vérifié le 1 octobre 2026

Ce lab rassemble tout le cours CI/CD : les principes. Vous construisez vous-même le serveur d'intégration continue monté au fil des leçons, puis vous y faites passer l'application Signalements jusqu'à un déploiement en recette. Un script vérifie à la fin que votre serveur a les bonnes propriétés : il valide le niveau 200 des compétences « Pipelines CI/CD » et « Stratégies de déploiement ».

Comptez deux à trois heures. Les scripts complets figurent dans les leçons 2, 3, 4 et 5 ; essayez d'écrire chaque pièce vous-même avant d'ouvrir les solutions. Ce serveur n'est pas fait pour la production : il est fait pour que chaque mécanisme soit visible.

Ce qu'il faut

  • Git, Docker Engine et le greffon Compose (comme dans les cours précédents).
  • L'application Signalements telle qu'à la fin du cours Construire des images de conteneurs : app.py, test_app.py, requirements.txt, Dockerfile, versionnés avec Git.
  • Un registre local : docker run -d --name registre-ci -p 127.0.0.1:5000:5000 registry:3.
  • Environ 2 Go d'espace disque pour les images et les volumes.

Warning

Ce lab lance des conteneurs et crée des volumes sur votre machine. Travaillez dans un répertoire dédié, et nettoyez à la fin (dernière section). Ne lancez jamais de docker system prune global si la machine sert à d'autres projets.

L'architecture à atteindre

serveur-ci/
├── signalements.git/          dépôt nu + hooks/post-receive (déclencheur)
├── executer                   l'orchestrateur : un pipeline pour un commit
├── deployer                   déploie l'artefact d'une exécution dans un environnement
├── etat                       le statut des dernières exécutions
├── environnements/*.env       la configuration par environnement (mode 600)
├── secrets.env                les secrets, chargés seulement pour main (mode 600)
├── travaux/<n>/               un espace de travail et ses journaux par exécution
└── cache/                     le cache des dépendances

Les exigences

ExigenceLeçon
Une poussée déclenche le pipeline (crochet post-receive)2
Chaque étape s'exécute dans un conteneur jetable, sur un espace de travail neuf2
Le verdict repose sur le code de sortie ; la première étape en échec arrête tout2
Un service PostgreSQL éphémère pour les tests d'intégration, nettoyé quoi qu'il arrive3
L'artefact (image) est construit une seule fois et publié, son empreinte notée4
Le test de fumée vise l'artefact lui-même4
Un déploiement en recette, par empreinte, avec une configuration hors du dépôt4
Une branche construit et teste, mais ne déploie pas4
Les secrets ne sont chargés que pour main et masqués dans les journaux5

Étape 1 : le déclencheur et une première étape

Solution

Créez le dépôt nu et le crochet (leçon 2) :

$ mkdir serveur-ci && cd serveur-ci
$ git init --bare -b main signalements.git

signalements.git/hooks/post-receive (à rendre exécutable) appelle l'orchestrateur pour chaque branche mise à jour :

#!/usr/bin/env bash
while read -r ancien nouveau ref; do
  case "$nouveau" in 0000000000000000000000000000000000000000) continue ;; esac
  "$(dirname "$PWD")/executer" "$nouveau" "$ref"
done

L'orchestrateur minimal extrait le commit dans un espace de travail neuf et exécute les lignes de .pipeline dans des conteneurs jetables. La version complète, qui gère numéro d'exécution, journaux, cache et statut, est le script executer de la leçon 2.

Étape 2 : services, construction, déploiement

Solution

La version finale d'executer reconnaît quatre types de lignes dans .pipeline : une étape ordinaire (conteneur jetable), un service (conteneur démarré en arrière-plan sur un réseau privé de l'exécution, nettoyé par un trap), une étape image (construction, publication, empreinte notée dans travaux/<n>/artefact), et une étape deployer (seulement pour main). Le script complet est donné à la leçon 4, et le script deployer, qui déploie l'artefact d'une exécution avec le fichier d'environnement correspondant, y figure aussi.

Le .pipeline de l'application, à viser :

format        python:3.14-slim   pip install --user --quiet ruff==0.16.9 && python -m ruff format --check --no-cache .
lint          python:3.14-slim   pip install --user --quiet ruff==0.16.9 && python -m ruff check --no-cache .
unitaires     python:3.14-slim   pip install --user --quiet -r requirements-dev.txt && python -m pytest -p no:cacheprovider --cov=app --junitxml=rapport-unitaires.xml
image         signalements
service       db                 postgres:18.6   POSTGRES_PASSWORD=ci
integration   python:3.14-slim   export DATABASE_URL=postgresql://postgres:ci@db/postgres && pip install --user --quiet -r requirements-dev.txt && python ci/attendre_postgres.py && python -m pytest -q -p no:cacheprovider test_integration.py
service       app                @artefact       DATABASE_URL=postgresql://postgres:ci@db/postgres
fumee         python:3.14-slim   python ci/fumee.py http://app:8000/sante
deployer      recette

La configuration de recette vit hors du dépôt, dans serveur-ci/environnements/recette.env (mode 600) :

PORT=8091
POSTGRES_PASSWORD=<une valeur tirée au hasard>
ENVIRONNEMENT=recette

Étape 3 : les secrets, protégés

Solution

Ajoutez serveur-ci/secrets.env (mode 600) et la gestion des secrets dans executer (leçon 5) : les secrets ne sont chargés que si [ "$ref" = refs/heads/main ], et la fonction masquer remplace leurs valeurs par *** dans les journaux. Une branche n'y a donc pas accès : c'est la défense contre la demande de fusion empoisonnée.

Vérification

Récupérez le script verifier.sh ci-dessous, puis lancez-le en lui donnant le chemin de votre serveur et celui de votre dépôt applicatif. Il fait une poussée sur main, une poussée sur une branche, et contrôle les propriétés attendues.

$ ./verifier.sh serveur-ci app
Structure du serveur
  OK  l'orchestrateur executer existe et est exécutable
  OK  le crochet post-receive existe et est exécutable
  OK  le fichier de secrets est protégé (mode 600)
Une poussée sur main déclenche un pipeline vert et un déploiement
  OK  la dernière exécution a réussi
  OK  un artefact a été produit (empreinte notée)
  OK  le déploiement est tracé dans deploiements.log
Les secrets sont protégés
  OK  aucun secret en clair dans les journaux de la dernière exécution
  OK  une branche ne déploie pas (pas de nouveau déploiement recette)

Résultat : 8 vérification(s) réussie(s), 0 échouée(s).
#!/usr/bin/env bash
# Vérifie qu'un serveur d'intégration continue construit pendant le lab fonctionne.
# Usage : ./verifier.sh <chemin du serveur-ci> <chemin du dépôt applicatif>
set -uo pipefail
serveur=${1:?chemin du serveur-ci} app=${2:?chemin du dépôt applicatif}
serveur=$(cd "$serveur" && pwd)
ok=0; ko=0
verifier() { local d=$1; shift; if "$@" >/dev/null 2>&1; then echo "  OK  $d"; ok=$((ok+1)); else echo "  KO  $d"; ko=$((ko+1)); fi; }

echo "Structure du serveur"
verifier "l'orchestrateur executer existe et est exécutable" test -x "$serveur/executer"
verifier "le crochet post-receive existe et est exécutable" test -x "$serveur/signalements.git/hooks/post-receive"
verifier "le fichier de secrets est protégé (mode 600)" bash -c "[ \"\$(stat -c %a '$serveur/secrets.env')\" = 600 ]"

echo "Une poussée sur main déclenche un pipeline vert et un déploiement"
( cd "$app" && git commit -q --allow-empty -m "Vérification du lab" && LC_ALL=C.UTF-8 git push -q ci main ) >/dev/null 2>&1
dernier=$(cat "$serveur/compteur")
verifier "la dernière exécution a réussi" bash -c "grep -q '^succes' '$serveur/travaux/$dernier/statut'"
verifier "un artefact a été produit (empreinte notée)" test -s "$serveur/travaux/$dernier/artefact"
verifier "le déploiement est tracé dans deploiements.log" bash -c "tail -1 '$serveur/deploiements.log' | grep -q recette"

echo "Les secrets sont protégés"
jeton=$(grep -m1 . "$serveur/secrets.env" | cut -d= -f2-)
verifier "aucun secret en clair dans les journaux de la dernière exécution" \
  bash -c "! grep -rqF '$jeton' '$serveur/travaux/$dernier/journaux'"
( cd "$app" && git switch -q -c verif-branche 2>/dev/null || git switch -q verif-branche; git commit -q --allow-empty -m "Depuis une branche"; LC_ALL=C.UTF-8 git push -q ci verif-branche ) >/dev/null 2>&1
verifier "une branche ne déploie pas (pas de nouveau déploiement recette)" \
  bash -c "[ \"\$(tail -1 '$serveur/deploiements.log' | awk '{print \$3}')\" = n°$dernier ]"
( cd "$app" && git switch -q main; git branch -D verif-branche >/dev/null 2>&1 )

echo
echo "Résultat : $ok vérification(s) réussie(s), $ko échouée(s)."
[ "$ko" -eq 0 ]

Pour aller plus loin

  • Isolez le cache par branche (leçon 5) : un répertoire de cache par branche, pour qu'une branche ne puisse pas empoisonner le cache de main.
  • Rapprochez le service de l'artefact de la production (leçon 4, exercice 2) : démarrez-le avec les mêmes restrictions, ou mieux, avec le deploiement/compose.yaml lui-même.
  • Calculez vos indicateurs DORA (leçon 6) sur le deploiements.log que votre serveur a produit.

Nettoyage

$ docker compose -p signalements-recette down -v
$ docker rm -f registre-ci
$ docker rm -f $(docker ps -aq --filter "label=ci.execution") 2>/dev/null

Supprimez ensuite le répertoire de travail du lab.

Plan du cours