Aller au contenu
CI/CD & Automatisation medium

Fiabilité des pipelines GitLab CI/CD

15 min de lecture

logo gitlab

Votre pipeline échoue une fois sur cinq sans raison apparente ? Timeout réseau, test instable, registry Docker indisponible — les erreurs transitoires sont le quotidien des pipelines CI/CD. Ce guide vous donne les mécanismes pour absorber les erreurs passagères sans masquer les vrais problèmes.

Un pipeline fiable ne veut pas dire un pipeline qui ne fail jamais. C’est un pipeline qui échoue pour les bonnes raisons : un vrai bug, un vrai problème de code, pas un glitch réseau à 3h du matin.

  • Configurer des retries ciblés par type d’erreur (retry:when)
  • Définir des timeouts adaptés à chaque job
  • Détecter et isoler les tests flaky
  • Rendre un job idempotent (relançable sans effet de bord)
  • Distinguer erreur transitoire, erreur de code et erreur d’infrastructure

Votre pipeline tourne 50 fois par jour. Sur ces 50 exécutions, 3 à 5 échouent de façon aléatoire :

  • Un npm install qui timeout parce que le registry est lent
  • Un test d’intégration qui échoue parce que le service PostgreSQL n’était pas prêt
  • Un docker pull qui retourne un 503 du Docker Hub
  • Un test unitaire qui passe en local mais échoue 1 fois sur 10 en CI

Chaque échec transitoire déclenche une investigation manuelle, un re-run, et de la frustration. Ce guide transforme ces échecs en récupérations automatiques.

Le mécanisme le plus basique : relancer un job en cas d’échec.

test:
script: npm test
retry: 2 # Jusqu'à 2 relances (3 exécutions max au total)

Problème : ce retry attrape toutes les erreurs. Un vrai bug dans les tests sera relancé 2 fois pour rien, gaspillant du temps.

La bonne pratique : cibler les erreurs transitoires uniquement.

build:docker:
script:
- docker build -t myapp .
- docker push registry.example.com/myapp:$CI_COMMIT_SHA
retry:
max: 2
when:
- runner_system_failure
- stuck_or_timeout_failure
- scheduler_failure
Valeur whenSignificationRetry recommandé ?
alwaysToute erreur (défaut)❌ Trop large
unknown_failureErreur non classifiée
script_failureLe script a retourné un code non-zéro❌ C’est souvent un vrai bug
api_failureErreur API GitLab
stuck_or_timeout_failureJob bloqué ou timeout
runner_system_failureLe runner a crashé
runner_unsupportedRunner incompatible❌ Pas transitoire
stale_scheduleSchedule périmé
job_execution_timeoutTimeout du job⚠️ Selon le contexte
archived_failureProjet archivé
unmet_prerequisitesPrérequis non satisfaits
scheduler_failureErreur de planification
data_integrity_failureIntégrité des données
build:docker:
script:
- docker build -t myapp:$CI_COMMIT_SHA .
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
retry:
max: 2
when:
- runner_system_failure
- stuck_or_timeout_failure
- unknown_failure

Les erreurs Docker sont souvent liées au réseau (pull d’images de base) ou au registry.

Chaque job peut avoir son propre timeout :

lint:
script: npm run lint
timeout: 5 minutes # Le lint ne devrait pas prendre plus de 5 min
test:unit:
script: npm test
timeout: 15 minutes
test:e2e:
script: npm run test:e2e
timeout: 30 minutes # Les tests end-to-end sont plus longs
build:docker:
script: docker build .
timeout: 20 minutes

Configurable dans Settings > CI/CD > General pipelines > Timeout (défaut : 60 minutes).

Le timeout du job ne peut pas dépasser le timeout du projet. Si le projet est à 30 minutes et le job à 45, le job sera coupé à 30.

Type de jobTimeout recommandéRaison
Lint3-5 minTrès rapide, un timeout long cache un problème
Tests unitaires10-15 minDépend du nombre de tests
Tests d’intégration15-30 minServices à démarrer + tests
Build Docker10-20 minDépend de l’image et du cache
Déploiement10-15 minSi ça prend plus, il y a un problème
Tests E2E20-45 minLes plus longs, selon la suite

Un test flaky (instable) passe parfois et échoue parfois, sans changement de code. C’est le poison des pipelines CI/CD : vous ne savez jamais si l’échec est réel ou transitoire.

GitLab détecte automatiquement les tests flaky si vous utilisez les rapports JUnit :

test:
script: npm test -- --reporters=jest-junit
artifacts:
reports:
junit: junit.xml

GitLab marque comme flaky un test qui échoue puis passe (ou inversement) sur la même MR sans changement de code. Les résultats sont visibles dans Analytics > CI/CD Analytics > Test cases.

Isolez les tests flaky dans une suite séparée avec allow_failure :

test:stable:
script: npm test -- --testPathIgnorePatterns='flaky'
# Ceux-là doivent passer — échec = vrai bug
test:flaky:
script: npm test -- --testPathPattern='flaky'
allow_failure: true
# Monitorer — ne bloque pas le pipeline
# Le pipeline passe même si ce job échoue
security:scan:
script: trivy image myapp:latest
allow_failure: true # Informatif, ne bloque pas
# Variante : allow_failure seulement pour les exit codes spécifiques
lint:strict:
script: npm run lint:strict
allow_failure:
exit_codes:
- 2 # Warnings = ok, Errors = bloquant

Un job idempotent peut être relancé sans créer d’effet de bord. C’est la base de la fiabilité : si le retry crée des doublons ou des états incohérents, le remède est pire que le mal.

PrincipeExemple
Pas de création si existe déjàVérifier si l’image Docker existe avant de rebuild
Pas d’état global modifiéUtiliser des noms uniques basés sur $CI_COMMIT_SHA
Nettoyage avant exécutionrm -rf dist/ && npm run build
Opérations atomiquesDéployer dans un nouveau répertoire puis switcher le lien symbolique
# ❌ Non idempotent : crée des doublons
deploy:
script:
- echo "$CI_COMMIT_SHA" >> /srv/deploy-history.log
- cp -r dist/ /srv/app/
# ✅ Idempotent : le résultat est le même si relancé
deploy:
script:
- rm -rf /srv/app-$CI_COMMIT_SHA/
- cp -r dist/ /srv/app-$CI_COMMIT_SHA/
- ln -sfn /srv/app-$CI_COMMIT_SHA /srv/app
# ❌ Non idempotent : helm upgrade peut échouer si le release est dans un état intermédiaire
deploy:k8s:
script:
- helm upgrade myapp ./chart --install
# ✅ Idempotent : force l'état souhaité
deploy:k8s:
script:
- helm upgrade myapp ./chart --install --atomic --timeout 5m
# --atomic : rollback automatique en cas d'échec

Quand vous poussez un nouveau commit, les jobs du commit précédent sont souvent inutiles. interruptible permet à GitLab de les annuler automatiquement :

default:
interruptible: true # Tous les jobs sont annulables par défaut
lint:
script: npm run lint
# interruptible: true (hérité de default)
test:
script: npm test
# interruptible: true (hérité de default)
deploy:production:
script: ./deploy.sh
interruptible: false # JAMAIS interrompre un déploiement en cours
resource_group: production

Pour activer l’annulation automatique : Settings > CI/CD > General pipelines > Auto-cancel redundant pipelines.

Voici un pipeline qui combine toutes les techniques :

workflow:
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
- if: $CI_COMMIT_TAG
default:
interruptible: true
retry:
max: 1
when:
- runner_system_failure
- stuck_or_timeout_failure
stages:
- lint
- test
- build
- deploy
lint:
stage: lint
timeout: 5 minutes
script: npm run lint
test:unit:
stage: test
timeout: 15 minutes
script: npm test -- --reporters=jest-junit --retries=1
artifacts:
reports:
junit: junit.xml
test:integration:
stage: test
timeout: 20 minutes
services:
- postgres:16-alpine
script: npm run test:integration
retry:
max: 2
when:
- stuck_or_timeout_failure
- runner_system_failure
build:
stage: build
timeout: 15 minutes
script:
- rm -rf dist/
- npm run build
artifacts:
paths: [dist/]
deploy:production:
stage: deploy
timeout: 10 minutes
interruptible: false
script: ./deploy.sh --atomic
retry:
max: 2
when:
- runner_system_failure
resource_group: production
rules:
- if: $CI_COMMIT_TAG =~ /^v\d+/
when: manual
SymptômeCause probableSolution
Job relancé mais même erreurscript_failure inclus dans retry (vrai bug)Utiliser retry: when: ciblé, exclure script_failure
Retry sans fin (3 essais, même timeout)Timeout trop court pour le jobAugmenter le timeout ou optimiser le job
Test flaky non détecté par GitLabPas de rapport JUnitAjouter artifacts: reports: junit:
Pipeline non annulé après un nouveau pushinterruptible non configuréAjouter default: interruptible: true
Déploiement interrompu à mi-chemininterruptible: true sur le job de deployMettre interruptible: false explicitement
Job qui crée des doublons au retryJob non idempotentUtiliser des noms uniques ($CI_COMMIT_SHA) et nettoyer avant
  • retry: when: cible les erreurs transitoires — ne retryez jamais les script_failure sauf cas spécifique
  • Les timeouts doivent être adaptés à chaque type de job (lint 5 min, deploy 10 min)
  • Les tests flaky sont le premier ennemi de la fiabilité — détectez-les avec JUnit, corrigez-les en priorité
  • Un job idempotent peut être relancé sans effet de bord — clé du retry fiable
  • interruptible: true annule les jobs obsolètes — sauf les déploiements
  • La fiabilité ne vient pas d’un seul mécanisme mais de la combinaison : retry ciblé + timeout adapté + idempotence + interruptible
  • Un pipeline fiable échoue pour les bonnes raisons : un vrai bug, pas un glitch réseau

Ce site vous est utile ?

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

Je maintiens +700 guides gratuits, sans pub ni tracing. Aujourd'hui, ce site ne couvre même pas mes frais d'hébergement, d'électricité, de matériel, de logiciels, mais surtout de cafés.

Un soutien régulier, même symbolique, m'aide à garder ces ressources gratuites et à continuer de produire des guides de qualité. Merci pour votre appui.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn