Concevoir une pipeline robuste
Les pratiques qui rendent une chaîne rapide et fiable.
Deux équipes peuvent employer le même outil d'intégration et obtenir des résultats opposés. Chez l'une, chaque exécution part du même état, un échec se localise en quelques secondes, et le résultat ne dépend pas de la machine qui a joué le job. Chez l'autre, les exécutions divergent sans raison apparente, les erreurs remontent des heures plus tard, et personne ne sait reproduire la panne. L'écart ne vient pas de l'outil — GitHub Actions, GitLab CI ou Jenkins — mais des principes de conception appliqués à la chaîne.
En bref
Une bonne pipeline est prévisible, rapide et autonome. Elle produit le même résultat à chaque exécution, donne un retour en minutes, et ne dépend d'aucun humain pour fonctionner.
Pourquoi ces principes comptent
Une pipeline n'est pas un script qu'on écrit une fois et qu'on oublie. C'est un système critique : il valide chaque modification avant qu'elle n'atteigne les utilisateurs, automatise des déploiements parfois quotidiens, détient des secrets — clés d'API, identifiants de production — et pèse sur la productivité de toute l'équipe dès qu'il dysfonctionne.
| Une mauvaise pipeline | Une bonne pipeline |
|---|---|
| des heures perdues à attendre des résultats | un retour en minutes, pas en heures |
| des bugs en production parce que « les tests étaient verts » | la certitude que ce qui est testé est ce qui est déployé |
| des déploiements qui échouent le vendredi soir | des déploiements prévisibles et reproductibles |
| des développeurs qui la contournent parce qu'elle est trop lente | une équipe qui fait confiance à son outillage |
Les symptômes sont visibles bien avant les causes.
Construire une fois, déployer partout
Le principe : l'artefact — image Docker, binaire, paquet — est construit une seule fois, puis déployé tel quel dans chaque environnement.
Le problème de la reconstruction
{À éviter — on reconstruit pour chaque environnement.}
staging:
script:
- npm install # telecharge les dependances
- npm run build # construction no 1
- docker build -t app:staging .
- docker push registry/app:staging
production:
script:
- npm install # re-telecharge : versions peut-etre differentes
- npm run build # construction no 2, potentiellement differente
- docker build -t app:production .
- docker push registry/app:productionEntre la construction de staging et celle de production — quelques jours plus tard —, une dépendance a pu être mise à jour, l'image de base Docker a pu changer, un outil de construction a pu évoluer, une variable d'environnement système peut différer. Résultat : l'artefact en production n'est pas celui que vous avez testé. Et quand ça casse, vous entendez « mais ça marchait en staging ! ».
La solution : la promotion d'artefact
{Une seule construction, promue d'environnement en environnement.}
build:
script:
- npm install
- npm run build
- docker build -t registry/app:COMMIT_SHA . # tag unique par commit
- docker push registry/app:COMMIT_SHA
staging:
script:
- docker pull registry/app:COMMIT_SHA # meme artefact
- helm upgrade app ./chart --set image.tag=COMMIT_SHA -f values-staging.yaml
production:
script:
- docker pull registry/app:COMMIT_SHA # toujours le meme artefact
- helm upgrade app ./chart --set image.tag=COMMIT_SHA -f values-production.yamlCe qui change d'un environnement à l'autre : uniquement la configuration — variables d'environnement, secrets, URL de base de données. Ce qui ne change jamais : l'artefact lui-même.
Un artefact, trois déploiements : seule la configuration est injectée au moment du déploiement.
Pourquoi c'est important
Le tableau oppose les deux approches sur les critères qui pèsent en incident. Lisez d'abord la ligne retour arrière : avec une reconstruction, revenir en arrière suppose de reconstruire une ancienne version, donc de rejouer une construction qui peut échouer au pire moment. Avec la promotion, le retour arrière se résume à redéployer un tag déjà présent dans le registre.
| Aspect | Reconstruction par environnement | Promotion d'artefact |
|---|---|---|
| Reproductibilité | incertaine | garantie |
| Confiance | « ça marchait en staging… » | « c'est le même artefact » |
| Temps de déploiement | long : tout est reconstruit | rapide : il n'y a qu'à déployer |
| Retour arrière | reconstruire l'ancienne version, risqué | redéployer l'ancien tag, instantané |
La ligne du retour arrière est celle qui se paie un soir d'incident.
Ce que cela exige de l'application
Pour que la promotion fonctionne, l'application doit séparer le code de
la configuration — pas de const API_URL = "https://prod.example.com"
en dur, mais une valeur lue au démarrage —, accepter sa configuration à
l'exécution — variables d'environnement, fichiers montés, secrets injectés par
l'orchestrateur — et porter un tag unique : app:abc123def
plutôt que app:staging, qui ne dit rien du code qu'il contient.
Immutabilité : ce qui est créé ne change jamais
Le principe : un artefact, une fois créé, n'est jamais modifié. Si un changement est nécessaire, on crée un nouvel artefact.
Le scénario qui coûte une journée
Lundi, vous déployez l'image app:latest en production : tout fonctionne.
Mardi, un collègue reconstruit app:latest avec une correction.
Mercredi, un autre service redémarre et récupère app:latest. Jeudi, ce
service ne fonctionne plus et personne ne comprend pourquoi. Le
app:latest de lundi n'est pas celui de mardi : le tag est le même, le
contenu a changé.
| Pratique qui casse l'immutabilité | Problème |
|---|---|
tags mutables (latest, stable, main) | le contenu change, le nom reste |
| modification d'un artefact après création | impossible de savoir ce qui tourne vraiment |
| scripts qui corrigent directement en production | l'état diffère de ce qui est versionné |
| déploiement sans tag explicite | « quelle version est en production ? » |
Quatre façons de laisser le contenu évoluer alors que l'identifiant reste figé.
À l'inverse, un identifiant immuable pointe pour toujours vers le même contenu : chaque empreinte de commit ouvre une image distincte, et une correction produit un nouvel artefact au lieu d'écraser l'ancien.
abc123 -----> image v1.2.3 (ne change jamais)
def456 -----> image v1.2.4 (nouvel artefact)
ghi789 -----> image v1.2.5 (nouvel artefact)| Pratique qui la préserve | Bénéfice |
|---|---|
tag par empreinte de commit (app:abc123def) | traçabilité totale : du code à l'artefact |
| gestion sémantique des versions, figées à la publication | v1.2.3 pointe toujours vers le même contenu |
| infrastructure décrite en code | la configuration est versionnée, pas modifiée à la main |
| registre protégeant les tags | impossible d'écraser un tag existant |
La question à se poser : si je rejoue cette pipeline dans six mois, obtiendrai-je le même artefact ?
{Un tag mobile répond non, une empreinte répond oui.}
# À ÉVITER : tag mutable
docker build -t app:latest .
docker push app:latest
# CORRECT : tag immuable
docker build -t app:CI_COMMIT_SHA .
docker push app:CI_COMMIT_SHA
# Pour la lisibilité, on peut ajouter un tag sémantique,
# MAIS l'empreinte reste la référence de vérité
docker tag app:CI_COMMIT_SHA app:v1.2.3
docker push app:v1.2.3Idempotence : même entrée, même sortie
Le principe : exécuter la même pipeline sur le même commit doit produire exactement le même résultat, que ce soit la première ou la dixième fois.
Exécution 1 (lundi) : commit abc123 -> artefact xyz789
Exécution 2 (mercredi) : commit abc123 -> artefact xyz789 identique
Exécution 3 (vendredi) : commit abc123 -> artefact xyz789 identiqueSans idempotence, le débogage devient impossible (« ça marchait hier »), le retour arrière incertain — reconstruire l'ancienne version ne donne pas le même résultat —, et les tests cessent d'être fiables.
| Cause | Exemple | Problème |
|---|---|---|
| dépendances non épinglées | npm install sans fichier de verrouillage | une dépendance est mise à jour entre deux constructions |
| horodatage dans l'artefact | date de construction dans le binaire | l'artefact change à chaque exécution |
| données externes | tests appelant une vraie API | l'API peut répondre différemment |
| état global modifié | script modifiant une variable partagée | la deuxième exécution voit un autre état |
| ordre non déterministe | tests parallèles mal isolés | les tests s'influencent mutuellement |
Cinq sources de non-déterminisme — la première est la plus fréquente et la plus vicieuse.
Épingler toutes les dépendances
{Installer des versions exactes, pas les dernières en date.}
# À ÉVITER : non déterministe, peut changer demain
npm install
pip install requests
# CORRECT : déterministe, toujours les mêmes versions
npm ci # utilise package-lock.json
pip install -r requirements.txt --no-deps # versions exactes| Écosystème | Fichier de verrouillage à versionner |
|---|---|
| Node.js (npm) | package-lock.json |
| Node.js (yarn) | yarn.lock |
| Python (pip) | requirements.txt avec versions exactes |
| Python (poetry) | poetry.lock |
| Go | go.sum |
| Rust | Cargo.lock |
Ces fichiers ne sont pas des artefacts de construction : ils font partie du code source.
Éviter les données variables
{La date change à chaque construction l'empreinte du commit, non.}
# À ÉVITER
RUN echo "Built on (date)" > /app/version.txt
# CORRECT
ARG COMMIT_SHA
RUN echo "Commit: COMMIT_SHA" > /app/version.txtIsoler les tests et simuler l'extérieur
{Un test qui crée son propre contexte ne dépend de personne.}
# À ÉVITER : dépend d'un état global
def test_user_count():
assert User.count() == 5 # et si un autre test a cree des utilisateurs ?
# CORRECT : le test construit son propre état
def test_user_count():
with fresh_database():
create_users(5)
assert User.count() == 5{Une vraie API peut changer, être lente, ou ne pas répondre.}
# À ÉVITER : appelle le service réel
def test_weather():
response = requests.get("https://api.weather.com/paris")
assert response.status_code == 200
# CORRECT : simule la réponse
def test_weather(mock_weather_api):
mock_weather_api.return_value = {"temp": 20}
result = get_weather("paris")
assert result["temp"] == 20Retour rapide : savoir en minutes
La valeur d'un test ne tient pas seulement à ce qu'il détecte, mais au délai au bout duquel il le signale. Un échec remonté après quarante minutes arrive alors que la personne a déjà ouvert un autre sujet : plus le retour est tardif, plus le contexte est perdu, et plus la correction coûte.
| Étape | Objectif | Pourquoi cette durée |
|---|---|---|
| Style et formatage | moins d'1 min | vérifications syntaxiques, très rapides |
| Tests unitaires | moins de 3 min | tests isolés, sans dépendance externe |
| Construction | moins de 5 min | compilation, assemblage |
| Tests d'intégration | moins de 10 min | tests avec base de données et services |
| Tests de bout en bout | moins de 15 min | scénarios navigateur complets |
Au-delà de 30 minutes au total, les développeurs contournent la pipeline.
Quatre leviers pour accélérer. Paralléliser les jobs indépendants — style, tests et analyse de sécurité n'ont aucune raison de s'attendre. Échouer vite : si le style échoue en trois secondes, inutile de lancer trente secondes de tests. Cacher agressivement les dépendances. Organiser les tests en pyramide.
style (3 s)
|
+-- échec -> STOP (30 s économisées)
|
+-- succès -> tests (30 s)
|
+-- construction (2 min)| Sans cache | Avec cache |
|---|---|
npm install : 3 min | npm install : 10 s |
pip install : 2 min | pip install : 5 s |
| téléchargement des images Docker : 1 min | déjà en cache : 0 s |
Retélécharger les mêmes dépendances à chaque exécution est un gaspillage pur.
La pyramide des tests : beaucoup de tests rapides en bas, peu de tests lents en haut.
Séparer construction et déploiement
Le principe : la construction crée un artefact générique le déploiement l'installe avec une configuration spécifique. Les deux ne doivent pas être mélangés.
{À éviter — la construction connaît sa destination.}
build-staging:
script:
- API_URL=https://staging.example.com npm run build
- docker build -t app:staging .
- deploy-to-staging
build-production:
script:
- API_URL=https://api.example.com npm run build
- docker build -t app:production .
- deploy-to-productionCe couplage duplique la construction, interdit toute promotion — l'artefact de staging n'est pas celui de production — et impose un nouveau job à chaque environnement ajouté.
{Construction générique, déploiement configuré.}
build:
script:
- npm run build # aucune config d'environnement
- docker build -t app:COMMIT_SHA .
- docker push registry/app:COMMIT_SHA
deploy-staging:
needs: [build]
script:
- docker pull registry/app:COMMIT_SHA
- helm upgrade app ./chart --set image.tag=COMMIT_SHA
--set apiUrl=https://staging.example.com -f values-staging.yaml
deploy-production:
needs: [build]
script:
- docker pull registry/app:COMMIT_SHA # meme artefact
- helm upgrade app ./chart --set image.tag=COMMIT_SHA
--set apiUrl=https://api.example.com -f values-production.yaml| Étape | Responsabilité | Ce qu'elle ne fait pas |
|---|---|---|
| Construction | compiler, assembler, créer l'artefact | injecter des URL, des secrets, une configuration d'environnement |
| Test | valider que l'artefact fonctionne | modifier l'artefact |
| Déploiement | installer l'artefact et injecter la configuration | recompiler, modifier le code |
La colonne de droite est celle qui protège la promotion : dès qu'une étape déborde, la garantie tombe.
{La configuration se lit à l'exécution, pas à la construction.}
// À ÉVITER : configuration figée dans le code
const API_URL = "https://api.example.com"
// CORRECT : configuration lue au démarrage
const API_URL = process.env.API_URL || "http://localhost:3000"Décrire quoi, pas comment
Le principe : le fichier de pipeline décrit ce que vous voulez obtenir, pas les détails d'implémentation. C'est la plateforme qui orchestre.
{À éviter — le script gère tout lui-même.}
steps:
- run: |
if [ "BRANCH" = "main" ] then
npm run build
docker build -t app .
docker push registry/app:latest
if [ "DEPLOY" = "true" ] then
kubectl config use-context production
kubectl apply -f k8s/
fi
elif [ "BRANCH" = "develop" ] then
npm run build
# ... encore du code
fiUn tel script est difficile à lire — il faut suivre sa logique —, difficile à maintenir, non réutilisable, et surtout non optimisable : la plateforme, qui ne voit qu'une grande commande, ne peut rien paralléliser.
{Intention déclarée, orchestration déléguée à la plateforme.}
permissions: {}
jobs:
build:
runs-on: ubuntu-24.04
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '20'
- run: npm ci
- run: npm run build
deploy-staging:
needs: build
if: github.ref == 'refs/heads/main'
environment: staging
steps:
- uses: azure/k8s-deploy@51ca02a8b7225fbd0924aac359c5b336a5f1e5b4 # v7.0.0
with:
manifests: k8s/staging/
images: registry/app:{{ github.sha }}
deploy-production:
needs: deploy-staging
environment: production # approbation requise
steps:
- uses: azure/k8s-deploy@51ca02a8b7225fbd0924aac359c5b336a5f1e5b4 # v7.0.0
with:
manifests: k8s/production/
images: registry/app:{{ github.sha }}| Aspect | Impératif | Déclaratif |
|---|---|---|
| Lisibilité | suivre la logique du script | structure visible immédiatement |
| Réutilisation | copier-coller | actions et modèles partagés |
| Maintenance | modifier des scripts complexes | changer des paramètres |
| Optimisation | manuelle | automatique, par la plateforme |
| Visualisation | aucun graphe | graphe de dépendances généré |
La ligne décisive est l'optimisation : un script impose sa séquence, une déclaration expose un graphe.
| Mot-clé | Signification | Exemple |
|---|---|---|
needs | ce job dépend de celui-là | needs: [build, test] |
if | n'exécuter que si… | if: github.ref == 'refs/heads/main' |
environment | déployer dans cet environnement | environment: production |
matrix | exécuter pour chaque combinaison | matrix: \{ node: [18, 20] \} |
uses | employer une action réutilisable | uses: actions/checkout@v4 |
Syntaxe GitHub Actions, transposable ailleurs. Retenez surtout needs : c'est lui qui construit le graphe.
Échouer vite, échouer fort
Une erreur silencieuse ou tardive est bien pire qu'une erreur bruyante et immédiate. Silencieuse, le bug part en production et ce sont les utilisateurs qui le découvrent. Tardive, dix autres commits ont été fusionnés entre-temps et plus personne ne sait lequel a cassé. Bruyante et immédiate, elle se corrige en cinq minutes sans impact.
S'arrêter immédiatement
{Sans `set -euo pipefail`, un script continue sur un état invalide.}
# À ÉVITER : les erreurs sont ignorées
result=(command_that_might_fail)
next_command
deploy # deploie du code potentiellement casse
# CORRECT : arrêt au premier problème
set -euo pipefail
result=(command_that_might_fail)
next_command # jamais atteint en cas d'echec| Option | Effet |
|---|---|
-e | arrêter le script dès qu'une commande retourne une erreur |
-u | arrêter si une variable non définie est utilisée |
-o pipefail | considérer un tube comme échoué si l'une de ses commandes échoue |
Trois options, une seule ligne — et la moitié des échecs silencieux disparaissent.
Notifier clairement
| Critère | Mauvaise notification | Bonne notification |
|---|---|---|
| Moment | 30 minutes après l'échec | immédiatement |
| Destinataire | toute l'équipe (bruit) | l'auteur du commit |
| Contenu | « Pipeline failed » | « test user_login : attendu 200, obtenu 401 » |
| Action | chercher dans les logs | lien direct vers la ligne en échec |
Une notification qui n'indique pas quoi faire est du bruit.
Faciliter le débogage
Quand une pipeline échoue, il faut pouvoir comprendre pourquoi sans lire cinq
cents lignes de logs : des logs structurés en sections claires, un
contexte préservé — variables et versions utilisées —, des
artefacts de débogage (captures d'écran des tests de bout en bout,
rapports de couverture) et une reproduction locale documentée
(« pour reproduire : make test-integration »).
Observabilité
La pipeline doit exposer son état de santé d'elle-même. Vous ne devriez pas avoir à fouiller les logs pour savoir si tout va bien.
| Métrique | Ce qu'elle révèle | Seuil d'alerte typique |
|---|---|---|
| Durée totale | régression de performance | plus de 15 min, selon le contexte |
| Durée par job | goulot d'étranglement | un job qui double de durée |
| Taux de succès | santé globale | moins de 90 % sur 24 h |
| Tests instables | tests non fiables | le même test échoue plus de 2 fois par semaine |
| Temps d'attente | manque de runners | plus de 5 min d'attente |
| Fréquence de déploiement | vélocité de l'équipe | baisse soudaine |
Des repères de départ, à ajuster — ce ne sont pas des valeurs universelles.
Les tests instables sont un mal silencieux
Un test flaky échoue parfois sans raison apparente, puis passe à la relance. L'équipe perd confiance (« ça a échoué, je relance »), les vrais problèmes se noient dans le bruit, et du temps se perd à rejouer des pipelines. Détectez-les — tests qui échouent puis passent sur le même commit, taux de succès inférieur à 99 % — marquez-les comme instables pour ne pas bloquer les autres, ouvrez un ticket prioritaire, et corrigez la cause racine : dépendance au minutage, état partagé, ressource externe.
| Condition | Action requise |
|---|---|
| pipeline principale au-delà de 20 min | investiguer la régression de performance |
| taux de succès inférieur à 85 % sur 24 h | problème systémique, à prioriser |
| un test échoue plus de 3 fois dans la semaine | le marquer instable, ouvrir un ticket |
| temps d'attente supérieur à 10 min | ajouter des runners ou paralléliser |
| échec d'un déploiement en production | alerte immédiate à l'astreinte |
Ne vous noyez pas dans les alertes : ne gardez que celles sur lesquelles on peut agir.
À retenir
Huit principes
Retour rapide — savoir en minutes paralléliser, cacher, échouer vite. Échouer vite et fort — une erreur silencieuse est pire qu'une erreur bruyante. Construire une fois, déployer partout — ce qui est testé est ce qui est déployé. Immutabilité — un artefact créé ne change jamais. Idempotence — même commit, même résultat. Séparation construction/déploiement — l'un produit, l'autre configure. Pipeline déclarative — décrire le quoi, laisser la plateforme orchestrer. Observabilité — mesurer la durée, le taux de succès, les tests instables.
Avant de fusionner une modification de pipeline
- Construction unique — l'artefact n'est construit qu'une fois.
- Immutabilité — les tags sont des empreintes, pas
latest. - Idempotence — les fichiers de verrouillage sont versionnés.
- Retour rapide — la pipeline complète tient en moins de 15 min.
- Échec immédiat — un échec arrête la chaîne sur-le-champ.
- Notifications — les alertes sont actionnables : lien, contexte.
- Séparation — construction et déploiement sont des jobs distincts.
- Déclaratif — la logique est dans la structure, pas dans un script.
Réponse unique
Pourquoi ne pas reconstruire l'artefact à chaque environnement ?
Indice · Que teste-t-on vraiment, si l'objet change entre deux étapes ?
[ METTRE EN PRATIQUE ]
Passer à la promotion d'artefact
3 étapes · 30 min · 130 XP
[ CETTE LEÇON DANS LES PARCOURS ]