Aller au contenu
CI/CD & Automatisation medium

Pipelines dynamiques GitLab CI/CD

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

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

┌──────────────┐ ┌──────────────┐ ┌──────────────────┐
│ generate │───▶│ child.yml │───▶│ pipeline enfant │
│ (script) │ │ (artefact) │ │ (exécution) │
└──────────────┘ └──────────────┘ └──────────────────┘
  1. Un job génère un fichier YAML de pipeline
  2. Ce fichier est sauvegardé comme artefact
  3. Un trigger exécute ce pipeline comme enfant

dynamic pipeline

stages:
- generate
- trigger
generate_pipeline:
stage: generate
image: alpine:3.18
script:
- |
cat > child-pipeline.yml << 'EOF'
stages:
- test
test_job:
stage: test
script:
- echo "Generated job!"
EOF
artifacts:
paths:
- child-pipeline.yml
trigger_child:
stage: trigger
trigger:
include:
- artifact: child-pipeline.yml
job: generate_pipeline
strategy: depend

Imaginons que vous devez tester votre application pour plusieurs clients, chacun avec sa configuration.

.gitlab-ci.yml
stages:
- generate
- test
generate:
stage: generate
image: alpine:3.18
script:
- |
# Liste des clients (pourrait venir d'un fichier ou API)
CLIENTS="client1 client2 client3"
echo "stages:" > test-pipeline.yml
echo " - test" >> test-pipeline.yml
echo "" >> test-pipeline.yml
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
cat test-pipeline.yml
artifacts:
paths:
- test-pipeline.yml
run_tests:
stage: test
trigger:
include:
- artifact: test-pipeline.yml
job: generate
strategy: depend
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"

Détectez les services modifiés dans un monorepo et générez leurs jobs :

stages:
- detect
- generate
- build
detect_changes:
stage: detect
script:
- |
# Gérer le cas du premier push (BEFORE_SHA = 0000...)
if [ "$CI_COMMIT_BEFORE_SHA" = "0000000000000000000000000000000000000000" ]; then
# Premier push : considérer tous les fichiers
CHANGED=$(git ls-tree -r --name-only HEAD | cut -d'/' -f1 | sort -u | tr '\n' ' ')
else
CHANGED=$(git diff --name-only "$CI_COMMIT_BEFORE_SHA" "$CI_COMMIT_SHA" | cut -d'/' -f1 | sort -u | tr '\n' ' ')
fi
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
generate_pipeline:
stage: generate
needs: ["detect_changes"]
rules:
- if: '$PIPELINE_EMPTY == "false"'
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
cat >> build-pipeline.yml << EOF
build_${service}:
stage: build
script:
- cd ${service}
- docker build -t ${service}:\$CI_COMMIT_SHA .
EOF
fi
done
artifacts:
paths:
- build-pipeline.yml
trigger_builds:
stage: build
needs: ["generate_pipeline"]
trigger:
include:
- artifact: build-pipeline.yml
job: generate_pipeline
strategy: depend
rules:
- if: '$PIPELINE_EMPTY == "false"'

Utilisez un fichier de configuration pour définir les jobs :

config/environments.yml
environments:
- name: staging
url: https://staging.example.com
auto_deploy: true
- name: production
url: https://example.com
auto_deploy: false
approval_required: true
scripts/generate-deploy-pipeline.py
#!/usr/bin/env python3
import yaml
with open('config/environments.yml') as f:
config = yaml.safe_load(f)
pipeline = {
'stages': ['deploy'],
}
for env in config['environments']:
job_name = f"deploy_{env['name']}"
job = {
'stage': 'deploy',
'script': [
f"./deploy.sh {env['name']}"
],
'environment': {
'name': env['name'],
'url': env['url']
}
}
if not env.get('auto_deploy', True):
job['when'] = 'manual'
if env.get('approval_required'):
job['allow_failure'] = False
pipeline[job_name] = job
with open('deploy-pipeline.yml', 'w') as f:
yaml.dump(pipeline, f, default_flow_style=False)
stages:
- generate
- deploy
generate_deploy:
stage: generate
image: python:3.11
script:
- pip install pyyaml
- python scripts/generate-deploy-pipeline.py
artifacts:
paths:
- deploy-pipeline.yml
deploy:
stage: deploy
trigger:
include:
- artifact: deploy-pipeline.yml
job: generate_deploy
strategy: depend

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

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 %}
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
generate:
stage: generate
image: ansible/ansible-runner
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
  1. Validez le YAML généré avant de le passer au trigger :
generate:
script:
- ./generate.sh > pipeline.yml
- python -c "import yaml; yaml.safe_load(open('pipeline.yml'))" # Valide
artifacts:
paths:
- pipeline.yml
  1. Loggez le pipeline généré pour le debug :
generate:
script:
- ./generate.sh > pipeline.yml
- echo "=== Generated pipeline ===" && cat pipeline.yml
  1. Gérez le cas “aucun job” :
# Option A : générer un pipeline minimal (recommandé)
generate:
script:
- |
if [ -z "$JOBS_TO_CREATE" ]; then
# Pipeline vide = job noop
echo 'noop: { script: ["echo No jobs to run"] }' > pipeline.yml
else
./generate.sh > pipeline.yml
fi
# Option B : conditionner avec une variable dotenv
trigger:
trigger:
include:
- artifact: pipeline.yml
job: generate
rules:
- if: '$PIPELINE_EMPTY == "false"'
  1. Utilisez des templates réutilisables dans le pipeline généré.
ErreurCauseSolution
invalid YAMLErreur de syntaxe dans le généréValider avec un parser YAML
artifact not foundJob generate a échouéVérifier les logs de génération
Pipeline videAucun job généréGénérer un job noop par défaut
circular dependencyLe généré référence le parentVérifier les dépendances
rules: exists ne fonctionne pasexists teste le repo, pas les artefactsUtiliser une variable dotenv
Diff vide sur schedule/webCI_COMMIT_BEFORE_SHA absent ou invalidePrévoir un fallback
  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 : 1 Mo max, 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