
Votre pipeline doit créer des jobs différents selon le contexte ? Les pipelines dynamiques permettent de générer la configuration CI/CD à la volée. Un job crée le YAML, un autre l'exécute.
Ce guide est fait pour vous si...
Section intitulée « Ce guide est fait pour vous si... »Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »À la fin de ce module, vous saurez :
- Générer un pipeline YAML : script qui crée le fichier de config
- Déclencher avec un artefact :
include: artifactpour l'enfant - Détecter les changements : git diff pour générer les jobs nécessaires
- Utiliser une config externe : YAML, JSON ou API comme source
- Valider le YAML généré : éviter les erreurs de syntaxe
- Gérer le cas vide : que faire quand aucun job n'est nécessaire
Prérequis
Section intitulée « Prérequis »Avant de continuer, assurez-vous de maîtriser :
Principe des pipelines dynamiques
Section intitulée « Principe des pipelines dynamiques »La différence avec les pipelines classiques
Section intitulée « La différence avec les pipelines classiques »Pipeline classique : tout est écrit à l'avance dans .gitlab-ci.yml. GitLab lit le fichier et exécute les jobs définis.
Pipeline dynamique : un job génère un fichier YAML, puis GitLab exécute ce fichier généré.
Le flux en 3 étapes
Section intitulée « Le flux en 3 étapes »Un pipeline dynamique repose sur une séparation stricte entre le job qui
produit la configuration et celui qui la consomme. Le fichier YAML
transite de l'un à l'autre par le mécanisme d'artefact, ce qui explique
pourquoi la déclaration artifacts: est obligatoire sur le générateur. Retenez
ce découpage : il structure tous les exemples de cette page.
┌──────────────┐ ┌──────────────┐ ┌──────────────────┐│ generate │───▶│ child.yml │───▶│ pipeline enfant ││ (script) │ │ (artefact) │ │ (exécution) │└──────────────┘ └──────────────┘ └──────────────────┘ ① GÉNÈRE ② STOCKE ③ EXÉCUTEÉtape par étape :
- Générer : Un job (shell, Python, etc.) analyse le contexte et écrit un fichier YAML valide
- Stocker : Ce fichier est sauvegardé comme artefact du job
- Exécuter : Un job trigger utilise
include: artifact:pour lancer ce pipeline comme enfant

Pourquoi c'est puissant ?
Section intitulée « Pourquoi c'est puissant ? »La génération dynamique n'a d'intérêt que face à un besoin qu'une configuration
statique ne couvre pas. Lisez ce tableau par sa dernière colonne : dès que la
liste des jobs dépend d'une source externe (base de données, API, fichiers
réellement modifiés), les mots-clés rules et parallel:matrix atteignent
leur limite, car ils exigent que tout soit connu à l'écriture du YAML.
| Besoin | Solution statique (rules) | Solution dynamique |
|---|---|---|
| 3 clients fixes | ✅ parallel:matrix | Overkill |
| Clients lus depuis une base | ❌ Impossible | ✅ Script qui lit la DB |
| Jobs selon fichiers modifiés | ⚠️ changes: (limité) | ✅ git diff + génération |
| 1 job par ligne d'un fichier | ❌ Impossible | ✅ Script qui parse le fichier |
Configuration de base
Section intitulée « Configuration de base »Comprendre la structure
Section intitulée « Comprendre la structure »Un pipeline dynamique nécessite deux jobs :
- Le générateur : crée le fichier YAML
- Le trigger : exécute le fichier généré
Pipeline parent : exemple commenté
Section intitulée « Pipeline parent : exemple commenté »stages: - generate # 1️⃣ Étape : générer le YAML - trigger # 2️⃣ Étape : exécuter le YAML généré
# 🛠️ Job qui GÉNÈRE le pipelinegenerate_pipeline: stage: generate image: alpine:3.18 # Image légère pour le scripting script: - | # Script multi-lignes cat > child-pipeline.yml << 'EOF' # Écrit dans un fichier stages: - test
test_job: stage: test script: - echo "Generated job!" EOF artifacts: paths: - child-pipeline.yml # 👈 CRUCIAL : sauvegarder le fichier généré
# 🚀 Job qui EXÉCUTE le pipeline générétrigger_child: stage: trigger trigger: # Mot-clé spécial : ce n'est pas un job normal include: - artifact: child-pipeline.yml # 👈 Le fichier généré job: generate_pipeline # 👈 Le job qui l'a créé strategy: depend # Attend la fin du pipeline enfantDécortiquons le mécanisme :
cat > child-pipeline.yml << 'EOF', Crée un fichier avec le contenu entreEOFetEOFartifacts: paths:, Sauvegarde le fichier pour qu'il survive au jobinclude: artifact:, Différent deinclude: local:! Ici on référence un artefact, pas un fichier du repojob: generate_pipeline, Indique quel job a produit l'artefact
Cas d'usage : tests multi-clients
Section intitulée « Cas d'usage : tests multi-clients »Le problème
Section intitulée « Le problème »Vous avez une application SaaS avec plusieurs clients, chacun ayant sa propre configuration. Vous devez tester l'application pour chaque client avant de déployer.
Pourquoi pas parallel:matrix ?
- La liste des clients peut changer (nouveaux clients, clients supprimés)
- La liste vient d'un fichier de config ou d'une API, pas du YAML
- Vous avez besoin de logique conditionnelle (certains clients ont des tests spéciaux)
Solution : génération dynamique
Section intitulée « Solution : génération dynamique »Idée : Un script lit la liste des clients et génère un job par client.
stages: - generate - test
generate: stage: generate image: alpine:3.18@sha256:de0eb0b3f2a47ba1eb89389859a9bd88b28e82f5826b6969ad604979713c2d4f script: - | # 📋 Liste des clients (pourrait venir d'un fichier, d'une API...) CLIENTS="client1 client2 client3"
# 🛠️ Création du fichier YAML echo "stages:" > test-pipeline.yml echo " - test" >> test-pipeline.yml echo "" >> test-pipeline.yml
# 🔁 Boucle : un job par client for client in $CLIENTS; do cat >> test-pipeline.yml << EOF test_${client}: stage: test script: - echo "Testing for ${client}" - ./run-tests.sh --client ${client} variables: CLIENT_NAME: "${client}"
EOF done
# 👀 Affiche le résultat (utile pour debug) echo "=== Pipeline généré ===" cat test-pipeline.yml artifacts: paths: - test-pipeline.yml
run_tests: stage: test trigger: include: - artifact: test-pipeline.yml job: generate strategy: dependCe que le script génère
Section intitulée « Ce que le script génère »Le fichier test-pipeline.yml contient :
stages: - test
test_client1: stage: test script: - echo "Testing for client1" - ./run-tests.sh --client client1 variables: CLIENT_NAME: "client1"
test_client2: stage: test script: - echo "Testing for client2" - ./run-tests.sh --client client2 variables: CLIENT_NAME: "client2"
test_client3: stage: test script: - echo "Testing for client3" - ./run-tests.sh --client client3 variables: CLIENT_NAME: "client3"Résultat dans GitLab : 3 jobs s'exécutent en parallèle, chacun testant un client différent. Si vous ajoutez client4 à la liste, un 4ème job apparaît automatiquement.
Générer selon les fichiers modifiés
Section intitulée « Générer selon les fichiers modifiés »Le cas d'usage : monorepo
Section intitulée « Le cas d'usage : monorepo »Vous avez un monorepo avec plusieurs services :
monorepo/├── frontend/│ └── Dockerfile├── backend/│ └── Dockerfile├── api/│ └── Dockerfile└── docs/ └── (pas de Dockerfile)Objectif : Ne builder que les services modifiés. Si seul frontend/ change, ne pas rebuilder backend/ ni api/.
Pourquoi pas rules: changes: ?
changes: fonctionne, mais vous devez lister tous les services manuellement. Si vous ajoutez un service, vous devez modifier le YAML.
Avec la génération dynamique, le script découvre automatiquement les services modifiés.
Le piège de HEAD~1
Section intitulée « Le piège de HEAD~1 »Solution complète
Section intitulée « Solution complète »Détectez les services modifiés et générez leurs jobs :
stages: - detect - generate - build
# 🔍 Étape 1 : Détecter ce qui a changédetect_changes: stage: detect image: alpine:3.18@sha256:de0eb0b3f2a47ba1eb89389859a9bd88b28e82f5826b6969ad604979713c2d4f before_script: - apk add --no-cache git # Git n'est pas installé dans alpine script: - | # Cas spécial : premier push (BEFORE_SHA = 0000...) if [ "$CI_COMMIT_BEFORE_SHA" = "0000000000000000000000000000000000000000" ]; then echo "🆕 Premier push : considérer tous les dossiers" CHANGED=$(git ls-tree -r --name-only HEAD | cut -d'/' -f1 | sort -u | tr '\n' ' ') else echo "🔄 Comparaison $CI_COMMIT_BEFORE_SHA → $CI_COMMIT_SHA" CHANGED=$(git diff --name-only "$CI_COMMIT_BEFORE_SHA" "$CI_COMMIT_SHA" | cut -d'/' -f1 | sort -u | tr '\n' ' ') fi
echo "📁 Dossiers modifiés : $CHANGED"
# Stocker le résultat pour les jobs suivants if [ -z "$CHANGED" ]; then echo "PIPELINE_EMPTY=true" >> detect.env else echo "PIPELINE_EMPTY=false" >> detect.env echo "SERVICES=$CHANGED" >> detect.env fi artifacts: reports: dotenv: detect.env # 👈 Variables disponibles pour les jobs suivants
# 🛠️ Étape 2 : Générer le pipelinegenerate_pipeline: stage: generate image: alpine:3.18@sha256:de0eb0b3f2a47ba1eb89389859a9bd88b28e82f5826b6969ad604979713c2d4f needs: ["detect_changes"] rules: - if: '$PIPELINE_EMPTY == "false"' # Ne génère que si quelque chose a changé script: - | echo "stages:" > build-pipeline.yml echo " - build" >> build-pipeline.yml
for service in $SERVICES; do # Vérifier que c'est un service avec Dockerfile if [ -f "${service}/Dockerfile" ]; then echo "✅ Génération du job pour ${service}" cat >> build-pipeline.yml << EOF
build_${service}: stage: build image: docker:24@sha256:9b17a9f25adf17b88d0a013b4f00160754adf4b07ccbe9986664a49886c2c98e services: - docker:24-dind script: - cd ${service} - docker build -t ${service}:\$CI_COMMIT_SHA .EOF else echo "⏭️ Ignoré ${service} (pas de Dockerfile)" fi done
echo "=== Pipeline généré ===" cat build-pipeline.yml artifacts: paths: - build-pipeline.yml
# 🚀 Étape 3 : Exécuter le pipeline générétrigger_builds: stage: build needs: ["generate_pipeline"] trigger: include: - artifact: build-pipeline.yml job: generate_pipeline strategy: depend rules: - if: '$PIPELINE_EMPTY == "false"'Comment ça marche :
detect_changestrouve les dossiers modifiés (frontend backend)generate_pipelinecrée un job Docker build pour chaque dossier qui a un Dockerfiletrigger_buildsexécute le pipeline généré
Générer depuis une configuration
Section intitulée « Générer depuis une configuration »Le cas d'usage
Section intitulée « Le cas d'usage »Votre équipe veut définir les environnements de déploiement sans toucher au CI/CD. Ils éditent un fichier YAML simple, et le pipeline s'adapte automatiquement.
Fichier de configuration
Section intitulée « Fichier de configuration »Un fichier YAML lisible par les non-experts CI/CD :
environments: - name: staging url: https://staging.example.com auto_deploy: true # Déploiement automatique - name: production url: https://example.com auto_deploy: false # Manuel seulement approval_required: true # Validation obligatoireGénérateur Python
Section intitulée « Générateur Python »Un script qui lit cette config et génère le pipeline :
#!/usr/bin/env python3import yaml
# 📖 Lire la configurationwith open('config/environments.yml') as f: config = yaml.safe_load(f)
# 🛠️ Construire le pipelinepipeline = { 'stages': ['deploy'],}
for env in config['environments']: job_name = f"deploy_{env['name']}"
# Job de base job = { 'stage': 'deploy', 'script': [ f"./deploy.sh {env['name']}" ], 'environment': { 'name': env['name'], 'url': env['url'] } }
# 🔒 Si pas auto_deploy : job manuel if not env.get('auto_deploy', True): job['when'] = 'manual'
# ✅ Si approval_required : échec interdit if env.get('approval_required'): job['allow_failure'] = False
pipeline[job_name] = job
# 💾 Écrire le pipeline généréwith open('deploy-pipeline.yml', 'w') as f: yaml.dump(pipeline, f, default_flow_style=False)
print("✅ Pipeline généré : deploy-pipeline.yml")Pipeline parent
Section intitulée « Pipeline parent »Le pipeline parent reste volontairement minimal : il installe PyYAML, lance
le script Python, puis déclenche le pipeline enfant à partir de l'artefact
produit. Toute la logique métier vit dans le script, jamais dans ce YAML. C'est
ce qui permet à une équipe non spécialiste du CI/CD de modifier le
comportement du déploiement sans jamais toucher à .gitlab-ci.yml.
stages: - generate - deploy
generate_deploy: stage: generate image: python:3.11@sha256:c7220863385ee39fb6d822da81f4469d0cd33ff893d92ce94105e5c3f4b95fe2 script: - pip install pyyaml - python scripts/generate-deploy-pipeline.py - cat deploy-pipeline.yml # Debug : voir le résultat artifacts: paths: - deploy-pipeline.yml
deploy: stage: deploy trigger: include: - artifact: deploy-pipeline.yml job: generate_deploy strategy: dependRésultat : L'équipe peut ajouter un environnement en éditant config/environments.yml. Le pipeline s'adapte sans modifier .gitlab-ci.yml.
Génération avec Ansible
Section intitulée « Génération avec Ansible »Pour des cas complexes, utilisez des templates Jinja2 avec Ansible :
Template Jinja2
Section intitulée « Template Jinja2 »Le template décrit la forme d'un job, et la boucle {% for %} le répète pour
chaque hôte de l'inventaire Ansible. L'intérêt par rapport à un script shell
apparaît dès que le job devient complexe : l'indentation et les variables sont
gérées par le moteur de template, ce qui évite les erreurs de YAML générées à la
main avec des echo successifs.
stages: - test
{% for host in groups['all'] %}test_{{ host }}: stage: test script: - pytest --host {{ host }} variables: TARGET_HOST: "{{ host }}" HOST_NAME: "{{ hostvars[host]['name'] }}"
{% endfor %}Playbook de génération
Section intitulée « Playbook de génération »Le playbook se limite à un seul module, template, qui rend le fichier .j2 en
un YAML concret. Le paramètre gather_facts: false évite la collecte inutile
d'informations système, puisque la génération ne dépend que de l'inventaire.
C'est ce fichier generated-pipeline.yml qui sera ensuite publié comme
artefact.
---- hosts: localhost gather_facts: false tasks: - name: Generate pipeline from template template: src: templates/test-pipeline.yml.j2 dest: ./generated-pipeline.ymlPipeline GitLab
Section intitulée « Pipeline GitLab »Côté GitLab, le schéma reste identique aux exemples précédents : un job
generate exécute le playbook et publie l'artefact, un job run_tests le
déclenche. La seule spécificité est l'image utilisée, qui doit embarquer
Ansible ; l'image ansible/ansible-runner remplit ce rôle sans installation
supplémentaire.
generate: stage: generate # depot sans version publiee : seul le digest fige l'image image: quay.io/ansible/ansible-runner@sha256:001a4bde411be863d54c1d293f3d2e7b0ff0e67ef5d7b2f9f7fb56b61694f4e8 # stable-2.12 script: - ansible-playbook -i inventory generate-pipeline.yml artifacts: paths: - generated-pipeline.yml
run_tests: stage: test trigger: include: - artifact: generated-pipeline.yml job: generate strategy: dependBonnes pratiques
Section intitulée « Bonnes pratiques »1. Toujours valider le YAML généré
Section intitulée « 1. Toujours valider le YAML généré »Une erreur de syntaxe dans le YAML généré = pipeline qui échoue. Validez avant de sauvegarder :
generate: script: - ./generate.sh > pipeline.yml # ✅ Valide la syntaxe YAML - python -c "import yaml; yaml.safe_load(open('pipeline.yml'))" artifacts: paths: - pipeline.yml2. Toujours afficher le pipeline généré
Section intitulée « 2. Toujours afficher le pipeline généré »Quand quelque chose ne marche pas, la première question est "qu'est-ce qui a été généré ?" :
generate: script: - ./generate.sh > pipeline.yml - echo "=== Pipeline généré ===" && cat pipeline.yml3. Gérer le cas "aucun job à exécuter"
Section intitulée « 3. Gérer le cas "aucun job à exécuter" »Si votre script ne génère aucun job (rien n'a changé), GitLab refuse le pipeline vide. Deux solutions :
Option A : Générer un job "noop" (recommandé)
generate: script: - | if [ -z "$JOBS_TO_CREATE" ]; then # Pas de jobs → pipeline minimal echo 'noop: { stage: test, script: ["echo Rien \u00e0 faire"] }' > pipeline.yml else ./generate.sh > pipeline.yml fiOption B : Conditionner le trigger avec dotenv
generate: script: - | if ./generate.sh > pipeline.yml 2>/dev/null; then echo "HAS_JOBS=true" >> build.env else echo "HAS_JOBS=false" >> build.env fi artifacts: reports: dotenv: build.env paths: - pipeline.yml
trigger: trigger: include: - artifact: pipeline.yml job: generate rules: - if: '$HAS_JOBS == "true"' # Ne trigger que s'il y a des jobs- Utilisez des templates réutilisables dans le pipeline généré.
Erreurs fréquentes
Section intitulée « Erreurs fréquentes »1. "invalid YAML"
Section intitulée « 1. "invalid YAML" »Symptôme : Le trigger échoue avec une erreur de syntaxe.
Cause : Le script a généré du YAML invalide (indentation, caractères spéciaux...).
Solution :
- Ajoutez
cat pipeline.ymlpour voir le contenu - Validez avec un parser :
python -c "import yaml; yaml.safe_load(open('pipeline.yml'))" - Attention aux variables avec
:ou#(les entourer de guillemets)
2. "artifact not found"
Section intitulée « 2. "artifact not found" »Symptôme : Le trigger ne trouve pas le fichier.
Cause : Le job generate a échoué, ou le fichier n'est pas dans artifacts: paths:.
Solution :
- Vérifiez que
generateest passé en vert - Vérifiez que le nom de fichier correspond exactement
- Vérifiez
artifacts: paths:dans le job générateur
3. Pipeline vide
Section intitulée « 3. Pipeline vide »Symptôme : Le pipeline enfant refuse de se créer ("pipeline has no jobs").
Cause : Le YAML généré ne contient aucun job.
Solution : Générez un job "noop" par défaut (voir Bonnes pratiques).
4. rules: exists ne fonctionne pas
Section intitulée « 4. rules: exists ne fonctionne pas »Symptôme : exists: ignore le fichier généré.
Cause : exists: vérifie les fichiers dans le repo, pas les artefacts.
Solution : Utilisez une variable dotenv (HAS_JOBS, PIPELINE_EMPTY).
5. Diff vide sur schedule/web
Section intitulée « 5. Diff vide sur schedule/web »Symptôme : git diff retourne rien sur un pipeline déclenché manuellement.
Cause : CI_COMMIT_BEFORE_SHA n'existe pas ou vaut 0000....
Solution : Prévoyez un fallback :
if [ "$CI_COMMIT_BEFORE_SHA" = "0000000000000000000000000000000000000000" ] || [ -z "$CI_COMMIT_BEFORE_SHA" ]; then # Pas de commit précédent : tout considérer comme modifié CHANGED=$(git ls-tree -r --name-only HEAD | cut -d'/' -f1 | sort -u)else CHANGED=$(git diff --name-only "$CI_COMMIT_BEFORE_SHA" "$CI_COMMIT_SHA" | cut -d'/' -f1 | sort -u)fiÀ retenir
Section intitulée « À retenir »- Génération : un job crée un fichier YAML
- Artefact : le fichier est sauvegardé
- Trigger :
include: artifact:exécute le pipeline strategy: depend: le parent attend la fin- Validez toujours le YAML généré
- Limites : 5 Mo max pour le YAML généré, jusqu'à 3 fichiers include par child pipeline
- Piège
exists: ne teste pas les artefacts, utilisez des variables dotenv - Piège
HEAD~1: préférezCI_COMMIT_BEFORE_SHA→CI_COMMIT_SHA
On passe à la pratique
Section intitulée « On passe à la pratique »Passez au Lab 16, Pipelines dynamiques pour générer et déclencher un child pipeline.
Testez vos connaissances
Section intitulée « Testez vos connaissances »Contrôle de connaissances
Validez vos connaissances avec ce quiz interactif
Informations
- Le chronomètre démarre au clic sur Démarrer
- Questions à choix multiples, vrai/faux et réponses courtes
- Vous pouvez naviguer entre les questions
- Les résultats détaillés sont affichés à la fin
Lance le quiz et démarre le chronomètre
Vérification
(0/0)Profil de compétences
Quoi faire maintenant
Ressources pour progresser
Des indices pour retenter votre chance ?
Nouveau quiz complet avec des questions aléatoires
Retravailler uniquement les questions ratées
Retour à la liste des certifications