Aller au contenu
CI/CD & Automatisation medium

Pipelines dynamiques GitLab CI/CD

24 min de lecture

logo gitlab

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.

À 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: artifact pour 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

Avant de continuer, assurez-vous de maîtriser :

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

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 :

  1. Générer : Un job (shell, Python, etc.) analyse le contexte et écrit un fichier YAML valide
  2. Stocker : Ce fichier est sauvegardé comme artefact du job
  3. Exécuter : Un job trigger utilise include: artifact: pour lancer ce pipeline comme enfant

dynamic pipeline

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.

BesoinSolution statique (rules)Solution dynamique
3 clients fixesparallel:matrixOverkill
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

Un pipeline dynamique nécessite deux jobs :

  1. Le générateur : crée le fichier YAML
  2. Le trigger : exécute le fichier généré
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 pipeline
generate_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 enfant

Décortiquons le mécanisme :

  1. cat > child-pipeline.yml << 'EOF', Crée un fichier avec le contenu entre EOF et EOF
  2. artifacts: paths:, Sauvegarde le fichier pour qu'il survive au job
  3. include: artifact:, Différent de include: local: ! Ici on référence un artefact, pas un fichier du repo
  4. job: generate_pipeline, Indique quel job a produit l'artefact

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)

Idée : Un script lit la liste des clients et génère un job par client.

.gitlab-ci.yml
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: depend

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.

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.

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 pipeline
generate_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 :

  1. detect_changes trouve les dossiers modifiés (frontend backend)
  2. generate_pipeline crée un job Docker build pour chaque dossier qui a un Dockerfile
  3. trigger_builds exécute le pipeline généré

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.

Un fichier YAML lisible par les non-experts CI/CD :

config/environments.yml
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 obligatoire

Un script qui lit cette config et génère le pipeline :

scripts/generate-deploy-pipeline.py
#!/usr/bin/env python3
import yaml
# 📖 Lire la configuration
with open('config/environments.yml') as f:
config = yaml.safe_load(f)
# 🛠️ Construire le pipeline
pipeline = {
'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")

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: depend

Résultat : L'équipe peut ajouter un environnement en éditant config/environments.yml. Le pipeline s'adapte sans modifier .gitlab-ci.yml.

Pour des cas complexes, utilisez des templates Jinja2 avec Ansible :

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.

templates/test-pipeline.yml.j2
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 %}

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.

generate-pipeline.yml
---
- hosts: localhost
gather_facts: false
tasks:
- name: Generate pipeline from template
template:
src: templates/test-pipeline.yml.j2
dest: ./generated-pipeline.yml

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: depend

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

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

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
fi

Option 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
  1. Utilisez des templates réutilisables dans le pipeline généré.

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 :

  1. Ajoutez cat pipeline.yml pour voir le contenu
  2. Validez avec un parser : python -c "import yaml; yaml.safe_load(open('pipeline.yml'))"
  3. Attention aux variables avec : ou # (les entourer de guillemets)

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 :

  1. Vérifiez que generate est passé en vert
  2. Vérifiez que le nom de fichier correspond exactement
  3. Vérifiez artifacts: paths: dans le job générateur

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

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

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 :

Fenêtre de terminal
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
  1. Génération : un job crée un fichier YAML
  2. Artefact : le fichier est sauvegardé
  3. Trigger : include: artifact: exécute le pipeline
  4. strategy: depend : le parent attend la fin
  5. Validez toujours le YAML généré
  6. Limites : 5 Mo max pour le YAML généré, jusqu'à 3 fichiers include par child pipeline
  7. Piège exists : ne teste pas les artefacts, utilisez des variables dotenv
  8. Piège HEAD~1 : préférez CI_COMMIT_BEFORE_SHACI_COMMIT_SHA

Passez au Lab 16, Pipelines dynamiques pour générer et déclencher un child pipeline.

Contrôle de connaissances

Validez vos connaissances avec ce quiz interactif

10 questions
5 min.
70% requis

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

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