Maximize Page
Tech & DevOps HubEspace Tech & DevOps: Explorez le monde du Dev, du Cloud et des outils DevOps à travers nos articles et discussions Explore the world of development, the cloud and DevOps tools

Refactoring CI/CD: optimisation des pipelines avec les variables de template

Date de l'article:25-07-2026
CI/CD Gitlab
Dans GitLab, plusieurs mécanismes permettent de limiter la duplication. En centralisant les versions d'images Docker, nous rendons les pipelines plus souples ; nous créons une configuration CI/CD dynamique, réutilisable et plus simple à maintenir.
Dans cet article, nous allons appliquer "le principe DRY" (Don't Repeat Yourself) à nos pipelines.
En utilisant les variables de template pour centraliser les versions d'images Docker et rendre nos pipelines dynamiques, fluides et simples à maintenir.
Les variables sont particulièrement utiles lorsqu'une valeur doit être disponible comme variable d'environnement pendant l'exécution d'un job,
lorsqu'elle doit pouvoir être modifiée selon le contexte de l'environement ou, lorsqu'il s'agit d'une valeur sensible stockée dans les paramètres CI/CD.
Avec les variables centralisée, une seule modification permet de changer la valeur de l'image utilisée pour plusieurs jobs, plusieurs pipelines.


Répartition du travail en deux étapes:
1. On va utiliser la méthode traditionnelle en rassemblant dans un premier temps, les versions des images dans un template classique nommé global.yml
2. Avec les templates variables, on créera des fichiers templates que l'on introduira ensuite dans les pipelines avec le mot-clé "extends"

Mise en place des variables du projet
Pour cette première partie, nous travaillerons sur ces fichiers:
.gitlab/
├── jobs/
│   ├── develop.yml
│   ├── staging.yml
│   └── main.yml
├── variables/
│   └── global.yml
Processus:
  1. Créer la structure de fichiers ".gitlab/variables/global.yml"
  2. Depuis les pipelines, lister les images
  3. Les ajouter dans le fichier global
  4. Mettre à jour les pipelines avec les variables "image" du fichier global

Les images sont utilisées partout dans le projet et, mis à part au début du projet, on a toujours plus facile à réutiliser les blocs de code.
Les rassembler dans un fichier unique nous permettra de ne plus devoir les modifier dans chaque pipeline en cas de mise à jour de version
1. Lister les images trouvées
[develop]
image: maven:3.9.11-eclipse-temurin-17
name: aquasec/trivy:0.72.0

[staging]
image: maven:3.9.11-eclipse-temurin-17
name: aquasec/trivy:0.72.0
image: docker:29 + dind
image: postgres:16
grafana/k6:latest (voir les logs)

[main]
image: maven:3.9.11-eclipse-temurin-17
image: maven:3.9.11-eclipse-temurin-21
name: aquasec/trivy:0.72.0
image: docker:29  + dind

[auto-promote]
image: alpine:3.22
2. Ajouter les variables dans le fichier "variables\global.yml", précédé du mot-clé "variables:"
variables:
  JAVA_VERSION: "17"
  MAVEN_USER_HOME: "$CI_PROJECT_DIR/.m2"
  MAVEN_OPTS: "-Dmaven.repo.local=$CI_PROJECT_DIR/.m2/repository"

  MAVEN_IMAGE: "maven:3.9.11-eclipse-temurin-17"
  MAVEN_IMAGE_JAVA21: "maven:3.9.11-eclipse-temurin-21"

  ALPINE_IMAGE: "alpine:3.22"
  POSTGRES_IMAGE: "postgres:16.14-alpine3.23"
  TRIVY_IMAGE: "aquasec/trivy:0.72.0"

  DOCKER_IMAGE: "docker:29.6.1"
  DOCKER_DIND_IMAGE: "docker:29.6.1-dind"

  K6_IMAGE: "grafana/k6:2.1.0"
La variable JAVA_VERSION indique sous quelle version de Maven le projet tourne
Les variables: MAVEN_USER_HOME modifie le répertoire personnel de Maven pour l'utilisateur.
et MAVEN_OPTS est automatiquement lue par les scripts de démarrage et force Maven à modifier l'emplacement de son dépôt local
Conseil: Récupérer les images directement depuis les registres officiels (Docker Hub ou le registre GitLab selon l'image)
Et gardez-en une trace, un fichier txt par exemple qui servira retrouver les repository facilement et d'y receuillir d'autres informations tels que la récupération des digests. Ici j'explique juste la procédure par étapes, l'ajout des "sha" se fera dans l'article suivant.

3. La dernière étape de ce premier process est de modifier les pipelines un à un:

Remplacer simplement les images:
image: maven:3.9.11-eclipse-temurin-17
ou
image: 
  name: maven:3.9.11-eclipse-temurin-17
Par:
image: $MAVEN_IMAGE
ou
image: 
  name: $MAVEN_IMAGE

Faites de même pour toutes les images présentes dans le fichier global.
Consultez les logs ou le registre pour les jobs contenant "latest"
Après ça:
toutes les images seront centralisées dans "global.yml"
nous n'aurons plus de tags flottants (latest, 27, etc.)

Les variables template
On continue dans la factorisation des jobs avec les templates de variables et un deuxième raz-de-marée sur les pipelines.
La mise en place des variables de template simplifiera considérablement le code et la lecture des pipelines tout en bénéficiant d'un endroit centralisé pour les blocs réutilisables. Documentation officielle: docs.gitlab.com/ci/variables/
Processus de mise en place
  1. Examiner les pipelines et repérer les duplications de code
  2. Exporter les blocs de code dupliqués dans des fichiers template
  3. Modifier les pipelines pour intégrer les templates
  4. Ajouter les templates dans le fichier .gitlab-ci.yml
L'utilisation de variables au sein des templates (modèles de configuration CI/CD ou modèles de tickets/merge requests) permet de créer des architectures réutilisables, dynamiques et sécurisées.
Conseil: créer les fichiers au fur et à mesure des blocs (dupliqués) trouvés.
Nous travaillerons sur ces fichiers:
.gitlab/
├── jobs/
│   ├── develop.yml
│   ├── staging.yml
│   ├── auto-mr.yml
│   └── main.yml
├── templates/
│   ├── artifacts.yml
│   ├── docker.yml
│   ├── maven.yml
│   ├── postgres-it.yml
│   ├── rules.yml
│   └── security.yml

Examiner et repérer les duplications de code
Ouvrez les pipelines un à un et repérez les bouts de code répétitifs.
Par exemple, on constate immédiatement que l'on a des "rules" pour chaque branche (et dans chaque job):
#develop
 rules:
    - if: '$CI_COMMIT_BRANCH == "develop"'
    - if: '$CI_COMMIT_BRANCH =~ /^(feature|fix|hotfix|refactor|chore)\/.+$/'
    - when: never
#staging
 - if: '$CI_COMMIT_BRANCH == "staging"'
#main
 - if: '$CI_COMMIT_BRANCH == "main"'
#main: push Docker hub
- if: '$CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/'
Regoupons-les dans un seul fichier: .gitlab/templates/rules.yml
# develop & feature 
.develop_rules:
  rules:
    - if: '$CI_COMMIT_BRANCH == "develop"' 
    - if: '$CI_COMMIT_BRANCH =~ /^(feature|fix|hotfix|refactor|chore)\/.+$/'
    - when: never

.staging_rules:
  rules:
    - if: '$CI_COMMIT_BRANCH == "staging"'

.main_rules:
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'

# publish in docker hub
.push_rules:
  rules:
    - if: '$CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/'
En entreprise, on aurait peut-être mis les règles dans un fichier par branche, ici, je choisis de les mettre dans un seul fichier, il n'y a pas beaucoup de lignes et je ne veux rien d'inutile. Libre à vous de créer les fichiers et noms de templates appropriés à votre projet.

Processus (suite)
Voici un exemple de la manière de procéder (exemple avec Docker)
Le pipeline staging.yml contient 3 jobs avec ce bloc à l'identique.
Le pipeline main.yml contient 2 jobs avec ce même bloc.
image: $DOCKER_IMAGE
   services: ...
.. variables: DOCKER_TLS_CERTDIR: "/certs"
Ma manière de faire:
Je commente le bloc de code. Je crée ensuite le fichier, je copie le bloc à l'intérieur et le nomme en fonction de ce à quoi il sert et/ou de ce qu'il fait.
Mon conseil est, de ne pas aller trop vite, de bien vérifier que le bloc de code vaille la peine d'en faire un template et que, là où il est intégré, il soit exactement le même utilisé partout
#staging
staging-docker-build:
  stage: docker
  needs:
    - staging-semgrep
    - staging-trivy
    - job: staging-package
      artifacts: true
  # image: $DOCKER_IMAGE
  # services:
  #   - $DOCKER_DIND_IMAGE
  #     alias: docker
  # variables:
  #   DOCKER_HOST: tcp://docker:2376
  #   DOCKER_TLS_CERTDIR: "/certs"
  #   DOCKER_TLS_VERIFY: "1"
  #   DOCKER_TLS_CERTDIR: "/certs"
  script:
    - docker build -t flashcards:staging .
    - docker save flashcards:staging -o flashcards-staging.tar
  artifacts:
    paths:
      - flashcards-staging.tar
  # rules:
  #   - if: '$CI_COMMIT_BRANCH == "staging"'  
Depuis la partie commentées, on peut créer le fichier .gitlab/templates/docker.yml:
.docker:
  image: $DOCKER_IMAGE
  services:
    - name: $DOCKER_DIND_IMAGE
      alias: docker
  variables:
    DOCKER_HOST: tcp://docker:2376
    DOCKER_TLS_CERTDIR: "/certs"
    DOCKER_TLS_VERIFY: "1"
    DOCKER_CERT_PATH: "/certs/client"
 
Les lignes sont commentées (#) pour montrer ce qui est copié dans le template. Vous supprimez évidemment cette partie, une fois la portion de code exportée (et testée) dans le fichier template

La suite du processus consiste à utiliser le mot-clé "extends" pour intégrer les blocs aux pipelines: exemple avec les deux templates déja fait: .rule et .docker, et bien sûr de continuer les autres.

Intégration des templates dans les jobs des pipelines
Quelques règles à respecter
  • Le nom du template doit commencer par un point si on veut l'étendre
  • Un seul extends par job, qui peut contenir un ou plusieurs templates
  • Lorsque plusieurs templates sont étendus, GitLab fusionne leurs configurations selon ses règles de merge.
  • Il faut éviter de définir la même propriété dans plusieurs templates.
Suivons une convention simple:
.docker, .maven, .trivy → environnement d'exécution
.staging_rules, .main_rules, .push_rules → règles
On garde la cohérence dans tous les pipelines et c'est plus facile à relire.
Ajout de "extends:"
La meilleure place où le mettre est, le + en haut possible:
donc, après le needs:
En suivant les règles et conventions, l'écriture des jobs sera presque toujours:
#main
extends:
  - .docker
  - .main_rules
#staging
extends:
  - .docker
  - .staging_rules
Et tous les jobs deviennent immédiatement plus courts et plus lisibles

Les autres fichiers template:
.gitlab/templates/artifacts.yml develop staging main
.artifacts:
  artifacts:
    expire_in: 1 week
Définis une règle de nettoyage automatique
Les fichiers sauvegardés sont supprimés des serveurs après 1 semaine, ce qui évite de saturer l'espace de stockage
.gitlab/templates/maven.yml develop staging main
.maven:
  cache:
    key:
      files:
        - pom.xml
        - .mvn/wrapper/maven-wrapper.properties
    paths:
      - .m2/repository/
    policy: pull-push
L'inclusion du fichier maven-wrapper.properties dans la clé, garantit que le cache sera également validé, en cas de changement de version de Maven par exemple
.gitlab/templates/postgres-it.yml staging main
.postgres-it:
  services:
    - name: $POSTGRES_IMAGE
      alias: postgres
  variables:
    POSTGRES_DB: flashcardsdb
    POSTGRES_USER: postgres
    POSTGRES_PASSWORD: $POSTGRES_PASSWORD

    DB_HOST: postgres
    DB_PORT: "5432"

    DB_USER: postgres
    DB_PASSWORD: $POSTGRES_PASSWORD
Permet de lancer une base de données PostgreSQL en tant que service secondaire, durant l'exécution d'un job
Le suffixe "-it" fait référence aux Tests d'Intégration (staging/main), le profil "it" est également ajouté aux variables du job, permettant d'agir avec une vraie base de donnée Postgres.
.gitlab/templates/security.yml develop staging main
.trivy:
  image:
    name: $TRIVY_IMAGE
    entrypoint: [""]
  cache:
    - key: trivy-db
      paths:
        - .trivycache/
      policy: pull-push
  variables:
    TRIVY_CACHE_DIR: "$CI_PROJECT_DIR/.trivycache"

.trivy-with-maven:
  extends: .trivy
Définis une stratégie de sécurité optimisée pour scanner les vulnérabilités du projet avec l'outil Trivy, tout en gérant l'intégration avec Maven
L'extension pour Maven (.trivy-with-maven:) hérite de toute la configuration précédente (image, cache, variables). Il sert de point d'ancrage pour ajouter des configurations spécifiques à l'analyse Trivy des projets Maven (comme la mise en cache du dossier ".m2" et/ou des variables spécifiques à Maven).

Pourquoi certains jobs ont il besoin du cache Maven?
Pour détecter les vulnérabilités applicatives, Trivy doit analyser les bibliothèques Java
Sans accès au dossier du cache Maven (.m2/repository/), Trivy ne verrait que le fichier pom.xml vide et ne pourrait pas scanner les dépendances réelles
Job Cache Maven Cache Trivy
develop-trivy
staging-trivy
staging-trivy-image
prod-trivy-fs
prod-trivy-image
Exemples de liens "trivy" et "trivy-with-maven" entre job (exemple staging)
staging-it ➔ staging-trivy (les dépendances): Le job "staging-it" télécharge les bibliothèques Java et les transmet de manière 100% garantie à "staging-trivy" sous forme d'artefacts (.m2/repository/), évitant ainsi les risques de cache vide lors du scan de code.
staging-trivy ➔ Cache Trivy (la base de données): Ce job utilise le modèle ".trivy-with-maven" pour interroger le cache global (trivy-db), cela lui évite de télécharger à nouveau la base des vulnérabilités sur Internet et réduit le scan du code à quelques secondes.
staging-docker-build ➔ staging-trivy-image (le conteneur): Le job de build emballe l'application dans un artefact (.tar) et le passe au job "staging-trivy-image", qui réutilise instantanément le même cache global Trivy pour valider la sécurité de l'image finale.
La commande "- key: trivy-db" mets la base Trivy en cache, ce qui évite de télécharger la base de données à chaque exécution
Ainsi, le cache .m2/repository sera partagé entre tous les jobs Maven du pipeline. GitLab restaurera le cache Maven avant que Trivy ne lance son analyse.

Le diagramme ci-dessous met en lumière une stratégie essentielle de GitLab CI/CD
La distinction fondamentale entre le cycle de vie d'un cache et celui d'un artefact
Bien que les deux systèmes stockent des fichiers, ils répondent à deux besoins diamétralement opposés dans le pipeline de sécurité.

          [CACHE GLOBAL GITLAB] <--- (Persistant entre pipelines)
      └──────┬───────────┬───────┘
             │           │
          ( Cache trivy-db )
             │           │
             ▼           ▼
┌───────────────┐   ┌───────────────────┐   ┌──────────────────────────┐
│  staging-it   │   │   staging-trivy   │   │   staging-trivy-image    │
└───────┬───────┘   └───────────────────┘   └────────────▲─────────────┘
        │                                                │
        │ (Artifacts .m2/repository/ & .jar)             │
        ├──────────────────────────────────────┐         │
        │                                      │         │
        ▼                                      ▼         │
┌───────────────────────────┐            ┌───────────────┴────────┐
│   staging-docker-build    │───────────>│ flashcards-staging.tar │
└───────────────────────────┘            └────────────────────────┘
                                          (Artifact de l'image Docker)
Le canal du haut (Cache Global)
gère la base de données des vulnérabilités de Trivy (.trivycache/).
Il est partagé d'un pipeline à un autre pour éviter les téléchargements réseau redondant.
Le canal du bas (Artifacts)
gère le code et les dépendances du projet (.m2/repository/ et fichiers .tar)
Contrairement au cache, les artefacts sont transférés (via needs) de manière 100% garantie d'un job à un autre au sein du même pipeline, éliminant tout risque de dossier manquant pendant le scan ou le build.

Ce que l'on ne factorisera PAS
Par exemple, en staging et production, nous avons presque les mêmes blocks utilisant plus ou moins les mêmes commandes Docker:
docker network create
docker run postgres
docker run flashcards
Oui c'est dupliqué. Mais c'est:
  • différent entre "staging" et "prod"
  • différent entre le job "smoke" et le job "functional"
  • différent entre le job "functional" et "load-test"
Essayer de factoriser ça avec des anchors YAML ou des scripts Bash rendrait vite le pipeline difficile à suivre. Je laisserais donc ces scripts tels quels.

Intégrer les templates dans la CI
Dernière phase du processus, ajouter les templates et fichier de variables/global dans le fichier .gitlab-ci.yml, sans cela, les pipelines ne prendrons rien en compte.
#.gitlab-ci.yml
include:
  - local: '.gitlab/jobs/...

  - local: .gitlab/templates/artifacts.yml
  - local: .gitlab/templates/docker.yml
  - local: .gitlab/templates/maven.yml
  - local: .gitlab/templates/postgres-it.yml
  - local: .gitlab/templates/rules.yml
  - local: .gitlab/templates/security.yml
  
  - local: .gitlab/variables/global.yml
La suite logique est de faire tourner le cycle complet, pour vérifications.
À ce stade:
Les jobs Docker partagent un template commun
Semgrep et Trivy utilisent des images figées
Les règles sont mutualisées.
Ces templates permettent de réduire fortement la duplication tout en gardant les jobs très lisibles. C'est un bon équilibre entre factorisation et simplicité de maintenance.
Dans l'article suivant, nous optimiserons davantage en remplaçant les versions d'images par leur digest SHA et mettrons en place la configuration et l'ajout d'un pipeline "schedule" pour faire jouer Renovate et automatiser ainsi les mises à jour des versions d'image.

Laissez-moi un commentaire

En postant un commentaire anonyme, vous adhérez automatiquement aux conditions d'utilisation du site.