CaenHackYouAcademy
DevSecOps

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 pipelineUne bonne pipeline
des heures perdues à attendre des résultatsun 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 soirdes déploiements prévisibles et reproductibles
des développeurs qui la contournent parce qu'elle est trop lenteune é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:production

Entre 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.yaml

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

AspectReconstruction par environnementPromotion d'artefact
Reproductibilitéincertainegarantie
Confiance« ça marchait en staging… »« c'est le même artefact »
Temps de déploiementlong : tout est reconstruitrapide : il n'y a qu'à déployer
Retour arrièrereconstruire 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éationimpossible de savoir ce qui tourne vraiment
scripts qui corrigent directement en productionl'é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éserveBénéfice
tag par empreinte de commit (app:abc123def)traçabilité totale : du code à l'artefact
gestion sémantique des versions, figées à la publicationv1.2.3 pointe toujours vers le même contenu
infrastructure décrite en codela configuration est versionnée, pas modifiée à la main
registre protégeant les tagsimpossible 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.3

Idempotence : 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   identique

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

CauseExempleProblème
dépendances non épingléesnpm install sans fichier de verrouillageune dépendance est mise à jour entre deux constructions
horodatage dans l'artefactdate de construction dans le binairel'artefact change à chaque exécution
données externestests appelant une vraie APIl'API peut répondre différemment
état global modifiéscript modifiant une variable partagéela deuxième exécution voit un autre état
ordre non déterministetests parallèles mal isolésles 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èmeFichier 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
Gogo.sum
RustCargo.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.txt

Isoler 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"] == 20

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

ÉtapeObjectifPourquoi cette durée
Style et formatagemoins d'1 minvérifications syntaxiques, très rapides
Tests unitairesmoins de 3 mintests isolés, sans dépendance externe
Constructionmoins de 5 mincompilation, assemblage
Tests d'intégrationmoins de 10 mintests avec base de données et services
Tests de bout en boutmoins de 15 minscé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 cacheAvec cache
npm install : 3 minnpm install : 10 s
pip install : 2 minpip install : 5 s
téléchargement des images Docker : 1 mindé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-production

Ce 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
ÉtapeResponsabilitéCe qu'elle ne fait pas
Constructioncompiler, assembler, créer l'artefactinjecter des URL, des secrets, une configuration d'environnement
Testvalider que l'artefact fonctionnemodifier l'artefact
Déploiementinstaller l'artefact et injecter la configurationrecompiler, 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
      fi

Un 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 }}
AspectImpératifDéclaratif
Lisibilitésuivre la logique du scriptstructure visible immédiatement
Réutilisationcopier-colleractions et modèles partagés
Maintenancemodifier des scripts complexeschanger des paramètres
Optimisationmanuelleautomatique, par la plateforme
Visualisationaucun graphegraphe 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éSignificationExemple
needsce job dépend de celui-làneeds: [build, test]
ifn'exécuter que si…if: github.ref == 'refs/heads/main'
environmentdéployer dans cet environnementenvironment: production
matrixexécuter pour chaque combinaisonmatrix: \{ node: [18, 20] \}
usesemployer une action réutilisableuses: 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
OptionEffet
-earrêter le script dès qu'une commande retourne une erreur
-uarrêter si une variable non définie est utilisée
-o pipefailconsidé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èreMauvaise notificationBonne notification
Moment30 minutes après l'échecimmédiatement
Destinatairetoute l'équipe (bruit)l'auteur du commit
Contenu« Pipeline failed »« test user_login : attendu 200, obtenu 401 »
Actionchercher dans les logslien 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étriqueCe qu'elle révèleSeuil d'alerte typique
Durée totalerégression de performanceplus de 15 min, selon le contexte
Durée par jobgoulot d'étranglementun job qui double de durée
Taux de succèssanté globalemoins de 90 % sur 24 h
Tests instablestests non fiablesle même test échoue plus de 2 fois par semaine
Temps d'attentemanque de runnersplus de 5 min d'attente
Fréquence de déploiementvélocité de l'équipebaisse 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.

ConditionAction requise
pipeline principale au-delà de 20 mininvestiguer la régression de performance
taux de succès inférieur à 85 % sur 24 hproblème systémique, à prioriser
un test échoue plus de 3 fois dans la semainele marquer instable, ouvrir un ticket
temps d'attente supérieur à 10 minajouter des runners ou paralléliser
échec d'un déploiement en productionalerte 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.
VérificationQuestion 1 / 3

Réponse unique

Pourquoi ne pas reconstruire l'artefact à chaque environnement ?

Indice · Que teste-t-on vraiment, si l'objet change entre deux étapes ?

On this page