Aller au contenu
CI/CD & Automatisation medium

Anti-patterns CI/CD

24 min de lecture

Un anti-pattern, c'est une solution qui semble bonne sur le moment mais qui crée des problèmes à long terme. Le gain est immédiat et visible, le coût est différé et diffus : c'est cette asymétrie qui le rend piégeant. On l'adopte pour livrer plus vite aujourd'hui, on le paie en temps de maintenance et en incidents pendant des mois.

Dans le contexte des pipelines CI/CD (Continuous Integration / Continuous Delivery, le système automatisé qui teste et déploie votre code), les anti-patterns sont des pratiques qui :

  • Ralentissent le feedback aux développeurs
  • Rendent la maintenance difficile
  • Créent des bugs difficiles à reproduire
  • Accumulent de la dette technique (des problèmes qu'on repousse à plus tard)

Une pipeline monolithe enchaîne toutes les étapes du cycle dans une seule définition, du lint au déploiement en production. Chaque exécution paie le coût de l'ensemble, même quand la modification ne concerne qu'une étape, et une défaillance sur n'importe quel maillon bloque tout le reste.

Une seule pipeline fait tout : vérification du code (lint), tests, compilation (build), analyse de sécurité (scan), déploiement sur tous les environnements. Elle dure 45 minutes. Personne n'ose y toucher.

Pipeline monolithe

Les quatre lignes du tableau découlent d'une même cause : l'absence de découpage. Concentrez-vous sur la première, le feedback lent, car c'est elle qui dégrade le plus le travail au quotidien. Un développeur qui attend quarante-sept minutes pour apprendre qu'une virgule casse le build finit par ne plus attendre : il enchaîne autre chose, perd le contexte, et revient à froid sur son erreur. Les trois autres lignes sont les conséquences structurelles de ce même monolithe.

ProblèmeImpact concret
Feedback lent47 minutes pour savoir si une faute de frappe casse le build
FragilitéUn changement dans une étape peut casser toutes les autres
Pas de parallélisationLes étapes s'exécutent une par une, même si elles pourraient tourner en parallèle
Debugging difficileQuand ça échoue, il faut chercher dans 500 lignes de configuration

Découper en pipelines indépendantes avec des responsabilités claires. Chaque pipeline a un seul objectif et peut échouer sans bloquer les autres :

Pipeline CI (5 min) ← Vérifie que le code est correct
├── lint ← Vérifie le style du code
├── tests unitaires ← Teste les fonctions individuellement
└── build ← Compile l'application
Pipeline CD-staging (3 min) ← Déploie pour les tests
├── deploy staging ← Met en ligne sur l'environnement de test
└── tests e2e ← Exerce l'application via son interface
Pipeline CD-prod (2 min) ← Déploie pour les vrais utilisateurs
├── deploy production
└── smoke tests ← Vérifie que les fonctions essentielles marchent

Bénéfice immédiat : si le lint échoue (2 minutes), le développeur le sait tout de suite au lieu d'attendre 47 minutes.

Une pipeline déterministe donne toujours le même résultat avec les mêmes entrées. Une pipeline non-déterministe introduit de l'aléatoire, et donc de l'incertitude.

La même pipeline sur le même code donne des résultats différents. La phrase révélatrice : « Relance, ça va passer. »

Run 1: ✓ succès
Run 2: ✗ échec (test timeout) ← Pourquoi maintenant ?
Run 3: ✓ succès
Run 4: ✗ échec (dépendance introuvable)
Run 5: ✓ succès ← Rien n'a changé pourtant...

Ces quatre causes ont un point commun : elles introduisent une entrée non maîtrisée dans un processus censé être reproductible. Les dépendances non épinglées et les tests flaky représentent à eux deux la majorité des cas rencontrés, et ce sont aussi les plus faciles à corriger. Repérez d'abord laquelle vous frappe en lisant le message d'échec : un « module not found » pointe vers les dépendances, un « timeout » ou un « connection refused » intermittent vers les tests instables ou les ressources partagées.

CauseExplication simple
Dépendances non épingléesnpm install télécharge la dernière version, qui peut avoir changé depuis hier
Tests flakyTests instables qui dépendent de l'ordre d'exécution, du timing, ou de données externes
Ressources partagéesPlusieurs tests utilisent la même base de données et se marchent dessus
Rate limitingLes API externes (GitHub, npm) bloquent parfois les requêtes trop fréquentes

Chaque action ci-dessous supprime une source d'aléatoire identifiée juste avant. La première, épingler les dépendances, est celle qui rapporte le plus vite et se résume à un seul changement de commande, illustré sous le tableau : passer de npm install à npm ci. Ne cédez jamais sur la dernière ligne, corriger les tests flaky : un test instable qu'on relance en boucle n'est pas réparé, il est seulement caché, et il finira par masquer un vrai défaut.

ActionPourquoi ça aide
Épingler toutes les dépendancesLe lockfile garantit les mêmes versions à chaque exécution
Isoler les testsChaque test crée ses propres données, pas de conflit possible
Mocker les API externesSimuler les réponses au lieu d'appeler les vrais services
Corriger les tests flakyLes ignorer ne fait que repousser le problème
# ❌ Non-déterministe
- run: npm install
# Problème : télécharge les dernières versions, qui peuvent avoir changé
# ✓ Déterministe
- run: npm ci
# npm ci utilise exactement les versions du fichier package-lock.json

Une pipeline qui fonctionne se duplique de projet en projet, puis diverge au gré des ajustements locaux. Chaque copie devient un exemplaire unique, sans source commune : une correction ne se propage plus, et un défaut recopié se répand avec le modèle.

Chaque projet a sa propre pipeline, copiée depuis un autre projet, légèrement modifiée. 50 projets = 50 variations incompatibles.

projet-a/.github/workflows/ci.yml (version originale)
projet-b/.github/workflows/ci.yml (copié, modifié)
projet-c/.github/workflows/ci.yml (copié de B, re-modifié)
projet-d/.github/workflows/ci.yml (copié de C, avec les bugs de C !)
...

La ligne qui fait vraiment mal est la maintenance impossible. Tant que tout va bien, cinquante copies divergentes ne dérangent personne. Le jour où une faille est découverte dans le modèle d'origine, il faut la corriger à la main dans cinquante fichiers, en espérant n'en oublier aucun et ne rien casser au passage. Le drift est le mécanisme silencieux qui rend cette correction si risquée : chaque copie a dérivé de son point de départ, si bien qu'aucun correctif ne s'applique proprement partout.

ProblèmeConséquence concrète
IncohérenceUn projet scanne les vulnérabilités, un autre non, sans raison valable
Maintenance impossibleUne faille de sécurité dans la pipeline = 50 fichiers à corriger manuellement
DriftLes copies divergent au fil du temps, personne ne sait plus quelle version est « la bonne »
Pas de capitalisationUne équipe améliore sa pipeline, les autres n'en bénéficient pas

Centraliser la logique dans un workflow réutilisable (workflow_call sur GitHub Actions) ou un template partagé. Le principe : une seule définition fait autorité, et chaque projet l'appelle en la référençant par une version. Une correction apportée à cette définition se propage à tous les projets qui la référencent, sans copier-coller manuel.

# Avant : 200 lignes de configuration copiées-collées
# Après : 5 lignes qui appellent le template centralisé
jobs:
ci:
uses: org/shared-workflows/.github/workflows/ci.yml@v1
with:
language: python
python-version: "3.11"

Résultat :

  • Corriger un bug = modifier un seul fichier
  • Ajouter une vérification de sécurité = tous les projets en bénéficient
  • Les nouveaux projets démarrent avec les bonnes pratiques

Un artefact, c'est le produit fini de votre pipeline : une image Docker, un fichier exécutable, un package. C'est ce qui sera déployé.

L'artefact est reconstruit pour chaque environnement. La phrase révélatrice : « Ça marchait en staging ! »

staging:
npm run build ← Compilation n°1
docker push staging
production:
npm run build ← Compilation n°2 (potentiellement différente !)
docker push production

Tout part de la non-reproductibilité. Reconstruire signifie retélécharger des dépendances, réutiliser un cache différent, tourner sur un runner à l'état distinct : rien ne garantit que la deuxième compilation produise le même binaire que la première. Les bugs fantômes en sont la conséquence directe, et ce sont les plus coûteux à traquer, puisque le « ça marchait en staging » est littéralement vrai. Vous avez testé un artefact, vous en déployez un autre.

ProblèmeExemple concret
Non-reproductibilitéL'artefact testé en staging ≠ l'artefact déployé en production
Bugs fantômesUn bug apparaît en production alors que « ça marchait en staging »
Temps perduChaque déploiement recompile tout, même si rien n'a changé

Appliquer le principe « Build once, deploy many » (construire une fois, déployer partout) : un artefact unique, promu d'environnement en environnement.

build:
npm run build
docker push app:abc123 ← Artefact unique, identifié par son hash
staging:
docker pull app:abc123 ← Exactement le même artefact
ENV=staging deploy ← Seule la configuration change
production:
docker pull app:abc123 ← Toujours le même artefact
ENV=production deploy ← Seule la configuration change

Garantie : ce qui a été testé en staging est exactement ce qui tourne en production. Les seules différences sont les variables d'environnement (URLs, clés API, etc.).

Pour aller plus loin : Bonnes pratiques CI/CD

Placer les tests les plus coûteux à la toute fin retarde le signal d'échec jusqu'après le build et le déploiement. Le développeur apprend la régression longtemps après avoir changé de sujet, et la remise en contexte coûte souvent plus cher que la correction elle-même.

Les tests lourds (tests e2e, « end-to-end », qui exercent l'application de bout en bout à travers son interface) s'exécutent après le build et le déploiement. Le feedback arrive trop tard.

lint (1s) → build (3min) → deploy (2min) → tests e2e (15min)
└── Échec détecté
après 20 minutes !

Le fil rouge est le coût de l'attente. Placer les tests lents en dernier n'est pas gênant quand ils passent ; ça le devient quand ils échouent, car tout le travail en amont (build, déploiement) a été consommé pour rien. Le gaspillage de ressources et la perte de contexte se cumulent : vous brûlez des minutes de runner ET vous forcez le développeur à se replonger dans un code qu'il a quitté vingt minutes plus tôt. Un test qui aurait pu échouer en trente secondes échoue après vingt minutes.

ProblèmeImpact sur le développeur
Feedback tardif20 minutes pour découvrir qu'un simple bug casse tout
Gaspillage de ressourcesBuild et deploy exécutés pour rien si les tests échouent
Perte de contexteLe développeur a commencé autre chose, il doit se replonger dans le code

Appliquer la pyramide des tests : exécuter les tests rapides en premier, les tests lents à la fin.

lint (1s) ──┬── tests unitaires (30s) ──┬── build (3min)
│ │
└── Échec rapide ! └── tests e2e (seulement si tout passe)

La pyramide des tests visualise cette stratégie :

/\ Tests e2e (lents, après merge)
/ \ → Peu nombreux, vérifient les parcours critiques
/────\
/ \ Tests d'intégration (sur PR)
/────────\ → Vérifient que les composants fonctionnent ensemble
/ \
/────────────\ Tests unitaires (à chaque commit)
→ Nombreux, rapides, vérifient chaque fonction

Logique : les tests du bas (unitaires) sont rapides et nombreux. Si l'un échoue, on le sait en 30 secondes. Pas besoin de lancer les tests lourds.

Un runner, c'est la machine (physique ou virtuelle) qui exécute votre pipeline. Différents runners peuvent avoir des configurations différentes.

La pipeline fonctionne sur un runner spécifique mais échoue ailleurs. C'est le fameux « Ça marche sur ma machine », version CI.

Runner A (ubuntu-20.04): ✓ succès
Runner B (ubuntu-22.04): ✗ échec ← Même code, résultat différent
Runner C (macos): ✗ échec

Ces quatre causes partagent une racine : la pipeline suppose quelque chose de l'environnement au lieu de le déclarer. Les versions implicites sont les plus sournoises, parce qu'elles ne produisent aucune erreur franche : python lance simplement une version différente, et votre code s'exécute jusqu'au moment où une incompatibilité subtile fait dérailler un test. Le réflexe à prendre en lisant ce tableau : chaque ligne correspond à une chose que vous croyiez acquise et qui varie d'un runner à l'autre.

CauseExemple concret
Outils non installésLa pipeline utilise jq qui est installé sur un runner mais pas les autres
Versions implicitespython appelle Python 3.8 sur un runner, Python 3.11 sur un autre
Chemins en dur/home/runner/specific/path n'existe pas sur tous les runners
Variables d'environnementUne variable est configurée sur un runner, absente sur les autres
SolutionPourquoi ça fonctionne
ConteneuriserL'image Docker embarque tous les outils nécessaires, identiques partout
Expliciter les versionspython-version: "3.11" garantit la même version sur tous les runners
Éviter les chemins absolus$GITHUB_WORKSPACE s'adapte automatiquement au runner
Installer les prérequisSi un outil est nécessaire, l'installer explicitement dans la pipeline
# ✓ Explicite et portable
permissions: {} # ← Aucun droit par défaut au niveau workflow
jobs:
test:
runs-on: ubuntu-24.04
permissions:
contents: read # ← Le strict nécessaire pour ce job
container: python:3.11-slim # ← Environnement contrôlé
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- run: pip install -r requirements.txt

Règle d'or : si quelque chose n'est pas explicitement défini dans la pipeline, ça peut changer sans prévenir.

Une dérogation accordée pour débloquer une livraison n'a presque jamais de date de fin. Faute d'échéance et de réexamen, elle survit à l'urgence qui l'a justifiée, et le périmètre réellement contrôlé se réduit sans que personne ne l'ait décidé.

Des projets sont exemptés des contrôles de sécurité « temporairement ». Deux ans plus tard, les exceptions sont toujours là, et personne ne sait si elles sont encore justifiées.

Exceptions au scan de sécurité:
- projet-legacy (temporaire, ajouté en 2022) ← Toujours là
- projet-urgence (temporaire, ajouté en 2023) ← Toujours là
- projet-client-X (temporaire, ajouté en 2023) ← Toujours là
- ...

Le danger le plus perfide est la fausse sécurité. Un tableau de bord tout vert donne l'illusion que la chaîne est saine, alors que les projets exemptés, invisibles dans ces indicateurs, sont justement ceux qui échappent au contrôle. La dette invisible aggrave le tout : sans inventaire des exceptions ni de leur justification, plus personne ne sait quel est le périmètre réellement protégé. Vous croyez couvrir cent pour cent du parc, vous en couvrez peut-être soixante.

ProblèmeRisque concret
Fausse sécuritéLes tableaux de bord sont verts, mais des projets vulnérables passent sous le radar
Précédent dangereux« Eux ont une exception, pourquoi pas nous ? », les exceptions se multiplient
Dette invisiblePersonne ne sait combien de projets sont exemptés ni pourquoi

Le principe directeur : une exception doit avoir une fin programmée, sinon elle est permanente par défaut. La date d'expiration automatique est la mesure la plus efficace, parce qu'elle inverse la charge de la preuve. Au lieu d'exiger que quelqu'un pense à supprimer l'exception, on force son détenteur à la renouveler explicitement, justification à l'appui. Les trois autres mesures organisent la traçabilité autour de ce mécanisme et évitent le bypass total quand un simple filtrage ciblé suffit.

MesureComment l'appliquer
Date d'expirationChaque exception expire automatiquement après 90 jours
Justification documentée« Pourquoi cette exception ? Quel est le plan pour la supprimer ? »
Revue régulièreAudit trimestriel : les exceptions non renouvelées sont supprimées
Alternative au bypass totalPlutôt qu'ignorer le scan complet, ignorer uniquement la vulnérabilité spécifique

SPOF signifie « Single Point of Failure » (point de défaillance unique). C'est un élément dont la panne bloque tout le système.

Une seule pipeline fait tout, sur un seul runner, avec un seul maintainer. Si l'un de ces éléments tombe, tout s'arrête.

Pipeline SPOF

Un SPOF n'est pas seulement technique. Le tableau distingue volontairement trois natures de point unique, et la plus souvent oubliée est le maintainer unique : une pipeline que personne d'autre ne comprend tombe en panne dès que son auteur part en congé ou change d'équipe. La parade est symétrique pour les trois lignes, introduire de la redondance, qu'il s'agisse de machines, de personnes formées ou de canaux d'alerte.

SPOF identifiéSolution
Runner uniqueCréer un pool de runners (au moins 2-3)
Maintainer uniqueDocumenter la pipeline, former au moins une autre personne
Pas d'alertesConfigurer des notifications quand le runner est indisponible

Utilisez cette liste pour évaluer la santé de vos pipelines. Elle reprend un anti-pattern par ligne, dans l'ordre du guide. Ne cherchez pas à tout cocher du premier coup : traitez d'abord le feedback rapide et le déterminisme, ce sont les deux points qui débloquent le plus de temps pour l'équipe. Les autres se corrigent ensuite, une fois que les développeurs ont retrouvé un cycle de retour court et fiable.

  • Feedback rapide : Durée < 15 min pour le premier retour
  • Déterminisme : Aucun « relance, ça va passer »
  • Gouvernance : Templates centralisés (pas de copier-coller)
  • Build once : Artefact construit une seule fois
  • Pyramide des tests : Tests rapides exécutés en premier
  • Exceptions traçables : Pas d'exception sans date d'expiration
  • Pas de SPOF : Plus d'une personne connaît la pipeline
  • Un anti-pattern est une solution qui crée plus de problèmes qu'elle n'en résout
  • La pipeline monolithe ralentit le feedback, découpez en pipelines indépendantes
  • Une pipeline non-déterministe érode la confiance, épinglez les dépendances
  • Le copier-coller multiplie la maintenance, centralisez dans des templates
  • Build once, deploy many, un artefact unique pour tous les environnements
  • Les tests rapides d'abord, échec en 30 secondes plutôt qu'en 20 minutes
  • Les exceptions temporaires deviennent permanentes, ajoutez des dates d'expiration
  • Éliminez les SPOF, redondance des runners et des connaissances

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracking. Un soutien, même symbolique, m'aide à couvrir l'hébergement et à garder ces ressources gratuites. Merci pour votre appui.

Le formulaire ne s'affiche pas ? Ouvrir Ko-fi dans un onglet.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn