Aller au contenu
CI/CD & Automatisation medium

extends et anchors YAML GitLab CI/CD

17 min de lecture

logo gitlab

Vous répétez les mêmes lignes dans plusieurs jobs ? extends et les anchors YAML permettent de factoriser votre configuration. Définissez une fois, réutilisez partout.

À la fin de ce module, vous saurez :

  • Utiliser extends : faire hériter un job d'un template caché
  • Créer des jobs cachés : la convention . pour les templates
  • Surcharger les propriétés : personnaliser l'héritage sans tout réécrire
  • Comprendre la fusion : dictionnaires fusionnent, listes remplacent
  • Utiliser les anchors YAML : &, * et <<: pour les cas avancés
  • Choisir le bon outil : extends vs anchors vs !reference

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

Le schéma suivant montre ce que le job enfant récupère du parent, et ce qu'il peut redéfinir pour son propre compte.

Héritage avec extends : le job enfant hérite des propriétés du parent et peut les surcharger

Un job peut hériter d'un autre job avec extends. Le job parent est généralement caché (préfixé par .) pour ne pas s'exécuter lui-même :

# Job caché (ne s'exécute pas)
.base_job:
image: python:3.11@sha256:c7220863385ee39fb6d822da81f4469d0cd33ff893d92ce94105e5c3f4b95fe2
before_script:
- pip install -r requirements.txt
# Job qui hérite
test:
extends: .base_job
script:
- pytest

Que se passe-t-il concrètement ?

GitLab "fusionne" le job parent et le job enfant. C'est comme si vous aviez écrit :

test:
image: python:3.11 # Hérité de .base_job
before_script: # Hérité de .base_job
- pip install -r requirements.txt
script: # Défini par test
- pytest

Le job test hérite automatiquement de :

  • image: python:3.11
  • before_script: [pip install -r requirements.txt]

Et définit son propre script.

Les jobs dont le nom commence par . sont des templates, ils ne s'exécutent jamais. Leur seul rôle est de servir de modèle pour d'autres jobs.

.template: # ❌ Ne s'exécute pas (commence par .)
image: alpine
my_job: # ✅ S'exécute (nom normal)
extends: .template
script: echo "Hello"

Pourquoi utiliser des jobs cachés ?

  1. Éviter la pollution : Sans le ., GitLab essaierait d'exécuter le template comme un vrai job
  2. Clarté : En voyant .nodejs_base, on comprend immédiatement que c'est un template
  3. Convention : Tout le monde utilise cette notation, votre code est lisible par d'autres

Un job peut hériter de plusieurs templates à la fois. C'est utile pour composer des comportements :

.base:
image: node:18@sha256:c6ae79e38498325db67193d391e6ec1d224d96c693a8a4d943498556716d3783
variables:
ENV: production
.with_cache:
cache:
paths:
- node_modules/
build:
extends:
- .base # Premier : fournit image + variables
- .with_cache # Second : ajoute le cache
script: npm run build

build hérite de .base ET .with_cache. Le résultat final :

build:
image: node:18 # De .base
variables:
ENV: production # De .base
cache:
paths:
- node_modules/ # De .with_cache
script: npm run build # Défini localement

L'héritage serait limité si on ne pouvait pas personnaliser. Le job enfant peut surcharger (remplacer) n'importe quelle propriété héritée du parent :

.node_base:
image: node:18@sha256:c6ae79e38498325db67193d391e6ec1d224d96c693a8a4d943498556716d3783
script:
- npm test
variables:
NODE_ENV: development
# Surcharge plusieurs propriétés
build_prod:
extends: .node_base
image: node:20 # 🔄 Remplace node:18 par node:20
script:
- npm run build # 🔄 Remplace npm test par npm run build
variables:
NODE_ENV: production # 🔄 Remplace development par production
# Garde presque tout, change juste le script
test:
extends: .node_base
script:
- npm run lint # 🔄 Remplace le script
- npm test

Ce qui est surchargé vs ce qui est hérité :

Propriété dans build_prodValeurSource
imagenode:20Surchargée (était node:18)
scriptnpm run buildSurchargée (était npm test)
variables.NODE_ENVproductionSurchargée (était development)

Voici le piège le plus fréquent avec extends : les tableaux (listes) sont complètement remplacés, pas fusionnés.

.base:
script:
- echo "step 1"
- echo "step 2"
job:
extends: .base
script:
- echo "only this" # ⚠️ REMPLACE complètement, ne s'ajoute pas !

Résultat : job exécute uniquement echo "only this". Les étapes "step 1" et "step 2" ont disparu.

Comment ajouter des étapes sans remplacer ?

Utilisez before_script et after_script qui s'ajoutent autour de script :

.base:
before_script:
- echo "setup" # Toujours exécuté en premier
script:
- echo "main" # Le script principal
job:
extends: .base
after_script:
- echo "cleanup" # S'ajoute à la fin, ne remplace rien

Ordre d'exécution : setupmaincleanup

Les dictionnaires fusionnent en profondeur :

.base:
variables:
VAR1: "value1"
VAR2: "value2"
job:
extends: .base
variables:
VAR2: "overridden" # Surcharge VAR2
VAR3: "value3" # Ajoute VAR3
# Résultat : VAR1=value1, VAR2=overridden, VAR3=value3

Les anchors sont une fonctionnalité native de YAML, elles fonctionnent dans n'importe quel fichier YAML, pas seulement GitLab CI.

SymboleCe qu'il fait
&nameDéfinit une ancre (comme "copier")
*nameRéférence l'ancre (comme "coller")
<<:Fusionne le contenu dans un dictionnaire

Voici comment utiliser les anchors pour éviter la répétition :

# 1️⃣ Définir une ancre avec &
.job_template: &job_config # &job_config = "je nomme ce bloc"
image: ruby:3.0@sha256:dcb8a8a6ee981f949da505cb9f4f15cde696e7bcb088955bc1f92f23a4cd7a72
services:
- postgres:14
# 2️⃣ Utiliser l'ancre avec << et *
test1:
<<: *job_config # "Fusionne job_config ici"
script: ruby test1.rb
test2:
<<: *job_config # Même configuration, autre script
script: ruby test2.rb

Équivalent sans anchors (ce que vous auriez dû écrire sinon) :

test1:
image: ruby:3.0@sha256:dcb8a8a6ee981f949da505cb9f4f15cde696e7bcb088955bc1f92f23a4cd7a72
services:
- postgres:14
script: ruby test1.rb
test2:
image: ruby:3.0@sha256:dcb8a8a6ee981f949da505cb9f4f15cde696e7bcb088955bc1f92f23a4cd7a72
services:
- postgres:14
script: ruby test2.rb

Les anchors vous ont économisé 4 lignes de duplication.

Un anchor ne se limite pas aux blocs de job : il fonctionne sur n'importe quelle valeur scalaire, ce qui permet de déclarer une chaîne une seule fois et de la réutiliser partout où elle est attendue.

variables:
DATABASE_URL: &db_url "postgres://localhost:5432/app"
job1:
variables:
DB: *db_url
job2:
variables:
DATABASE: *db_url

Plusieurs anchors peuvent être fusionnés dans un même job. L'ordre compte : en cas de clé présente dans deux sources, la dernière déclarée l'emporte.

.base_config: &base
image: node:18@sha256:c6ae79e38498325db67193d391e6ec1d224d96c693a8a4d943498556716d3783
.cache_config: &cache
cache:
paths:
- node_modules/
build:
<<: [*base, *cache] # Fusionne les deux
script: npm run build

Le bloc variables se fusionne comme n'importe quel mapping. Un job peut donc hériter d'un jeu commun tout en ajoutant les siennes, sans avoir à recopier les valeurs héritées.

variables: &global_vars
VAR1: "value1"
VAR2: "value2"
job:
variables:
<<: *global_vars
VAR3: "value3" # Ajoute une variable
AspectextendsAnchors!reference
Lisibilité✅ Plus clair❌ Syntaxe cryptique✅ Explicite
Merge mappings✅ Oui⚠️ Shallow❌ Non (insertion)
Réutiliser des listes❌ Écrase❌ Écrase✅ Oui
Multi-fichiers✅ Avec include❌ Même fichier✅ Avec include
Spécifique GitLabOuiNon (YAML)Oui
Recommandé pourJobs completsValeurs simplesFragments (rules, script)

GitLab propose !reference pour réutiliser des fragments de configuration (listes, mappings) sans les limitations des anchors :

.security_rules:
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
.test_script:
script:
- npm test
- npm run coverage
build:
script:
- npm run build
rules:
- !reference [.security_rules, rules] # Réutilise les rules
test:
script:
- !reference [.test_script, script] # Réutilise le script
- npm run e2e # Ajoute une commande
rules:
- !reference [.security_rules, rules]

Ces deux configurations sont directement transposables. Elles montrent le cas le plus courant, un socle commun décliné en plusieurs variantes de la même tâche.

variables:
NODE_VERSION: "18" # Valeur par défaut, surchargeable
.node_base:
image: "node:${NODE_VERSION}"
cache:
key:
files:
- package-lock.json
paths:
- node_modules/
before_script:
- npm ci
build:
extends: .node_base
script: npm run build
artifacts:
paths:
- dist/
test:
extends: .node_base
script: npm test
coverage: '/Lines\s*:\s*(\d+\.?\d*)%/'
lint:
extends: .node_base
script: npm run lint
allow_failure: true
.deploy_base:
image: alpine:3.18@sha256:de0eb0b3f2a47ba1eb89389859a9bd88b28e82f5826b6969ad604979713c2d4f
before_script:
- apk add --no-cache curl
script:
- ./deploy.sh "$ENVIRONMENT"
deploy_staging:
extends: .deploy_base
variables:
ENVIRONMENT: staging
environment:
name: staging
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
when: manual
deploy_production:
extends: .deploy_base
variables:
ENVIRONMENT: production
environment:
name: production
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
when: manual

Les trois erreurs ci-dessous se ressemblent : le pipeline part sans message clair, ou un job attendu n'apparaît pas. Le symptôme visible ne désigne jamais directement la cause.

Symptôme : Votre job n'apparaît pas dans le pipeline.

Cause : Son nom commence par ., c'est un job caché (template).

Solution : Retirez le . du nom, ou créez un job qui extends ce template.

# ❌ Ne s'exécutera jamais
.build:
script: npm run build
# ✅ S'exécutera
build:
script: npm run build

Symptôme : Les commandes du parent ont disparu.

Cause : Les tableaux (listes) ne fusionnent pas, ils sont remplacés.

Solution : Utilisez before_script / after_script, ou redéfinissez tout le script.

Symptôme : GitLab refuse de créer le pipeline.

Cause : Le job parent n'existe pas (faute de frappe, fichier non inclus).

Solution : Vérifiez :

  • Le nom exact du job parent (avec le . si c'est un template)
  • Que le fichier contenant le parent est bien inclus (include:)

Symptôme : Erreur YAML à la création du pipeline.

Cause : Vous référencez une ancre (*name) avant de l'avoir définie (&name).

Solution : Définissez toujours l'ancre avant de l'utiliser dans le fichier.

# ❌ Erreur : ancre utilisée avant d'être définie
job:
<<: *config # Erreur ! config n'existe pas encore
.template: &config
image: alpine
# ✅ Correct : ancre définie d'abord
.template: &config
image: alpine
job:
<<: *config # OK, config existe
  1. extends permet l'héritage de jobs, préférez-le aux anchors
  2. Jobs cachés (.name) ne s'exécutent pas, servent de templates
  3. Mappings fusionnent, scalaires et tableaux sont écrasés
  4. Héritage multiple : le dernier gagne en cas de conflit
  5. Anchors (&, *, <<:) pour les cas avancés dans le même fichier
  6. !reference pour réutiliser des fragments (rules, script) entre jobs
  7. Combinez avec include pour partager entre projets

Mettez ces techniques en pratique dans le Lab 13, DRY : extends et anchors.

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