Aller au contenu
CI/CD & Automatisation medium

Lab 18, Fiabilité et robustesse

60 min de lecture

logo gitlab

Un pipeline peut être propre, modulaire et malgré tout fragile. Il suffit d'une défaillance runner temporaire, d'un timeout non maîtrisé ou de deux déploiements concurrents pour casser la chaîne. Ce lab vous fait traiter cette couche de résilience.

Cette page fait partie d'une série de labs progressifs sur Pipeline Craft. Pour comprendre le projet fil rouge, le workflow fork -> starter -> solution et la progression complète, commencez par Labs Pipeline Craft.

  • Diagnostiquer une erreur de stage volontairement injectée
  • Ajouter retry avec un ciblage des causes transitoires
  • Poser des timeout explicites sur les jobs critiques
  • Utiliser resource_group pour sérialiser les déploiements

Cette étape correspond au passage d'un pipeline "fonctionnel" à un pipeline "opérationnel". Dans la vraie vie, les échecs transitoires sont inévitables : runners indisponibles, congestion réseau, jobs longs qui s'accumulent, déploiements concurrents.

Ce lab est utile quand :

  • les pipelines échouent de manière intermittente ;
  • plusieurs déploiements se chevauchent ;
  • l'équipe veut réduire les faux négatifs CI.

La branche starter/lab-18 contient un pipeline volontairement invalide. L'objectif de cette première manipulation n'est pas de le corriger tout de suite, mais de constater comment GitLab signale le problème. Notez la différence entre les deux modes de glab ci lint : sans option, la commande fait une validation statique du fichier ; avec --dry-run --ref, elle demande au serveur de simuler la création du pipeline sur la branche indiquée, ce qui détecte en plus les incohérences liées aux règles et aux stages.

  1. Basculez sur la branche de départ

    Fenêtre de terminal
    cd pipeline-craft
    git checkout starter/lab-18
  2. Lancez un lint branch-aware

    Fenêtre de terminal
    glab ci lint --dry-run --ref starter/lab-18
  3. Observez l'erreur attendue

    Le starter est volontairement invalide avec un stage orchestrate absent.

Vous devez d'abord rétablir un pipeline valide, puis renforcer sa robustesse sur les jobs sensibles.

GitLab refuse un pipeline dont un job référence un stage absent de la liste stages:. L'erreur est facile à lire une fois qu'on sait où regarder : le message du lint nomme le job fautif et le stage inconnu. Attention à la position du nouveau stage dans la liste : l'ordre de stages: détermine l'ordre d'exécution, orchestrate doit donc se placer entre test et build.

  1. Ouvrez .gitlab-ci.yml

  2. Ajoutez le stage manquant orchestrate dans stages:

  3. Revalidez

    Fenêtre de terminal
    glab ci lint .gitlab-ci.yml
👉 Vérifier votre solution (Étape 1)

Le starter du lab 18 référence déjà des jobs d'orchestration, mais oublie le stage correspondant.

stages:
- lint
- test
- orchestrate
- build
- deploy

Le mot-clé retry accepte une liste when qui restreint les causes d'échec donnant droit à une relance. Deux valeurs comptent ici : runner_system_failure couvre un runner qui disparaît ou plante, stuck_or_timeout_failure couvre un job qui reste bloqué sans progresser. Un retry sans when relance sur toutes les causes, y compris un test qui échoue légitimement, ce qui transforme le pipeline en générateur de faux verts. Le timeout du job, lui, prime sur le timeout global du projet tant qu'il est inférieur au timeout du runner.

  1. Ajoutez un retry ciblé

    Indice : ciblez les causes transitoires infra, pas les erreurs applicatives.

  2. Ajoutez un timeout explicite

    Indice : le timeout doit rester réaliste pour un build Docker normal.

  3. Gardez interruptible: true sur les jobs non critiques de long run

👉 Vérifier votre solution (Étape 2)

1️⃣ Renforcement de docker-build dans ci/build.yml

Section intitulée « 1️⃣ Renforcement de docker-build dans ci/build.yml »

Trois ajouts seulement, mais tous placés au niveau du job et non du stage : retry restreint aux deux causes infrastructure, timeout calibré sur la durée réelle d'un build Docker, et interruptible: true conservé puisqu'un build annulé ne laisse aucun état à nettoyer.

docker-build:
stage: build
needs:
- pytest-matrix
- run-child-pipeline
image: docker:27@sha256:aa3df78ecf320f5fafdce71c659f1629e96e9de0968305fe1de670e0ca9176ce
services:
- docker:27-dind
variables:
<<: *docker_vars
retry:
max: 2
when:
- runner_system_failure
- stuck_or_timeout_failure
timeout: 20m
interruptible: true

Un déploiement ne se traite pas comme un build. resource_group crée un verrou nommé : deux jobs qui portent le même nom de groupe ne s'exécutent jamais en parallèle, le second attend la fin du premier. Le nom du groupe doit donc correspondre à la ressource réellement partagée, ici l'environnement cible, sinon vous sérialisez des déploiements qui n'ont rien à voir entre eux. Symétriquement, interruptible: false empêche qu'un nouveau commit annule un déploiement en cours et laisse l'environnement dans un état intermédiaire.

  1. Ajoutez resource_group pour éviter les déploiements concurrents

    Indice : utilisez un verrou distinct par environnement.

  2. Ajoutez timeout et retry raisonnables sur staging et production

  3. Conservez interruptible: false pour éviter une interruption de déploiement en cours

👉 Vérifier votre solution (Étape 3)

Comparez les deux jobs : les valeurs diffèrent là où l'environnement diffère. Le resource_group porte le nom de l'environnement, le timeout est plus généreux en production, et le retry reste à max: 1 des deux côtés car relancer un déploiement partiel est plus risqué qu'échouer proprement.

deploy-staging:
stage: deploy
image: alpine:3.20@sha256:d9e853e87e55526f6b2917df91a2115c36dd7c696a35be12163d44e6e2a4b6bc
script:
- echo "Deploying $CI_COMMIT_SHORT_SHA to staging..."
- ./scripts/deploy-demo.sh staging
resource_group: staging
retry:
max: 1
when:
- runner_system_failure
timeout: 10m
interruptible: false
rules:
- if: $CI_MERGE_REQUEST_EVENT_TYPE == "merge_train"
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
deploy-production:
stage: deploy
image: alpine:3.20@sha256:d9e853e87e55526f6b2917df91a2115c36dd7c696a35be12163d44e6e2a4b6bc
script:
- echo "Deploying $CI_COMMIT_SHORT_SHA to production..."
- ./scripts/deploy-demo.sh production
resource_group: production
retry:
max: 1
when:
- runner_system_failure
timeout: 15m
interruptible: false
rules:
- if: $CI_COMMIT_TAG
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
when: manual

Le lint ne valide que la syntaxe et la cohérence des stages : il ne peut pas vous dire si vos retry sont bien ciblés ni si le verrou de déploiement fonctionne. Ces comportements ne se constatent qu'à l'exécution, d'où le run complet demandé à la dernière étape. Poussez sur la branche starter/lab-18 plutôt que sur la branche par défaut, pour que le job deploy-production reste en déclenchement manuel.

  1. Validation CI

    Fenêtre de terminal
    glab ci lint .gitlab-ci.yml
  2. Commit et push

    Fenêtre de terminal
    git add .gitlab-ci.yml ci/build.yml ci/deploy.yml
    git commit -m "ci: improve reliability with retry timeout and resource locks"
    git push origin starter/lab-18
  3. Vérifiez un run complet

👉 Vérifier votre solution (Étape 4)

Les deux premiers points se vérifient sur le run lui-même, les deux derniers demandent de relire la configuration : ni le verrou de déploiement ni les timeouts ne se manifestent tant qu'aucun incident ne survient.

  • le lint ne remonte plus l'erreur de stage manquant ;
  • docker-build supporte mieux les pannes transitoires runner/réseau ;
  • les déploiements sont sérialisés (resource_group) et non interruptibles ;
  • les timeouts explicites empêchent les jobs bloqués trop longtemps.

Les trois fichiers ci-dessous montrent l'état final attendu. Comparez-les avec les vôtres plutôt que de les recopier : l'intérêt du lab est le raisonnement sur le placement des garde-fous. Un détail à ne pas manquer dans ci/build.yml : l'ancre YAML .docker-vars est définie en tête de fichier, l'extrait de l'étape 2 ne montre que sa référence <<: *docker_vars. Une ancre doit toujours être déclarée avant son utilisation dans le même document, sinon GitLab rejette le fichier.

📄 Voir le fichier .gitlab-ci.yml complet
stages:
- lint
- test
- orchestrate
- build
- deploy
workflow:
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS
when: never
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
- if: $CI_COMMIT_BRANCH
- if: $CI_COMMIT_TAG
default:
interruptible: true
variables:
PIP_CACHE_DIR: "$CI_PROJECT_DIR/.pip-cache"
include:
- local: ci/lint.yml
- local: ci/test.yml
- local: ci/orchestration.yml
- local: ci/build.yml
- local: ci/deploy.yml
📄 Voir le fichier ci/build.yml complet
.docker-vars: &docker_vars
DOCKER_TLS_CERTDIR: "/certs"
docker-build:
stage: build
needs:
- pytest-matrix
- run-child-pipeline
image: docker:27@sha256:aa3df78ecf320f5fafdce71c659f1629e96e9de0968305fe1de670e0ca9176ce
services:
- docker:27-dind
variables:
<<: *docker_vars
retry:
max: 2
when:
- runner_system_failure
- stuck_or_timeout_failure
timeout: 20m
interruptible: true
script:
- docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA .
- docker build -t $CI_REGISTRY_IMAGE:latest .
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
- docker push $CI_REGISTRY_IMAGE:latest
📄 Voir le fichier ci/deploy.yml complet
deploy-staging:
stage: deploy
image: alpine:3.20@sha256:d9e853e87e55526f6b2917df91a2115c36dd7c696a35be12163d44e6e2a4b6bc
script:
- echo "Deploying $CI_COMMIT_SHORT_SHA to staging..."
- ./scripts/deploy-demo.sh staging
resource_group: staging
retry:
max: 1
when:
- runner_system_failure
timeout: 10m
interruptible: false
rules:
- if: $CI_MERGE_REQUEST_EVENT_TYPE == "merge_train"
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
deploy-production:
stage: deploy
image: alpine:3.20@sha256:d9e853e87e55526f6b2917df91a2115c36dd7c696a35be12163d44e6e2a4b6bc
script:
- echo "Deploying $CI_COMMIT_SHORT_SHA to production..."
- ./scripts/deploy-demo.sh production
resource_group: production
retry:
max: 1
when:
- runner_system_failure
timeout: 15m
interruptible: false
rules:
- if: $CI_COMMIT_TAG
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
when: manual

Le lab est réussi quand les quatre points ci-dessous sont vrais simultanément. Le troisième est celui qu'on oublie le plus souvent : sans timeout explicite, un job hérite du timeout du projet, souvent fixé à une heure, pendant laquelle il monopolise un runner sans que personne ne s'en aperçoive.

  • Le lint ne renvoie plus l'erreur de stage
  • retry est ciblé sur des causes transitoires pertinentes
  • Les jobs critiques ont un timeout explicite
  • Les déploiements sont sérialisés via resource_group

Un retry trop large masque les vraies erreurs applicatives. Ciblez seulement les cas transitoires liés à l'infrastructure CI.

Ne mettez pas interruptible: true sur un job de production critique, sinon un nouveau pipeline peut interrompre un déploiement en cours.

  • Fiabiliser un pipeline, ce n'est pas seulement le faire passer une fois
  • retry, timeout et resource_group traitent des risques différents
  • Le starter cassé est un exercice de diagnostic, pas un accident
  • La résilience CI doit rester explicite et mesurable

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