Lab : construire un serveur d'intégration continue
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épendancesLes exigences
| Exigence | Leç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 neuf | 2 |
| Le verdict repose sur le code de sortie ; la première étape en échec arrête tout | 2 |
| Un service PostgreSQL éphémère pour les tests d'intégration, nettoyé quoi qu'il arrive | 3 |
| L'artefact (image) est construit une seule fois et publié, son empreinte notée | 4 |
| Le test de fumée vise l'artefact lui-même | 4 |
| Un déploiement en recette, par empreinte, avec une configuration hors du dépôt | 4 |
| Une branche construit et teste, mais ne déploie pas | 4 |
Les secrets ne sont chargés que pour main et masqués dans les journaux | 5 |
É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"
doneL'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 recetteLa 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.yamllui-même. - Calculez vos indicateurs DORA (leçon 6) sur le
deploiements.logque 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.