04 — GitLab CI/CD#

GitLab CI/CD est une plateforme CI/CD intégrée directement dans GitLab, disponible depuis la version 8.0 (2015). Contrairement à GitHub Actions qui repose sur un écosystème de marketplace, GitLab CI/CD propose une approche plus intégrée avec son propre container registry, ses environnements de déploiement dynamiques (review apps), ses merge trains et une gestion fine des règles de déclenchement.

Structure du fichier .gitlab-ci.yml#

Le point d’entrée de toute configuration GitLab CI/CD est le fichier .gitlab-ci.yml à la racine du dépôt.

# Structure complète d'un .gitlab-ci.yml (non exécutable)

# Variables globales
variables:
  DOCKER_DRIVER: overlay2
  PYTHON_VERSION: "3.12"

# Définition des stages (phases)
stages:
  - lint
  - test
  - build
  - security
  - deploy

# Job de lint
lint:
  stage: lint
  image: python:3.12-slim
  script:
    - pip install ruff
    - ruff check .
    - ruff format --check .
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

# Job de tests unitaires
unit-tests:
  stage: test
  image: python:$PYTHON_VERSION-slim
  script:
    - pip install -r requirements.txt
    - pytest tests/unit/ --cov=src --cov-report=xml
  coverage: '/TOTAL.*\s+(\d+%)$/'
  artifacts:
    when: always               # Conserver même en cas d'échec
    reports:
      coverage_report:
        coverage_format: cobertura
        path: coverage.xml
    expire_in: 1 week

# Job de build de l'image Docker
docker-build:
  stage: build
  image: docker:24
  services:
    - docker:24-dind            # Docker-in-Docker
  script:
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
    - docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA .
    - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
  only:
    - main
    - tags

Stages#

Les stages définissent les phases du pipeline. Les jobs d’un même stage s’exécutent en parallèle ; les stages s’enchaînent séquentiellement. Si un job d’un stage échoue, les stages suivants sont bloqués (comportement configurable avec allow_failure).

only/rules#

GitLab offre deux mécanismes de déclenchement conditionnel :

  • only/except (ancienne syntaxe, encore largement utilisée) : liste de branches, tags, événements.

  • rules (nouvelle syntaxe, recommandée) : évaluée dans l’ordre, plus expressive, supporte les expressions régulières et les variables CI.

# Comparaison only/except vs rules (non exécutable)

# Ancienne syntaxe
deploy-prod:
  only:
    - main
  except:
    - schedules

# Nouvelle syntaxe équivalente (plus puissante)
deploy-prod:
  rules:
    - if: $CI_COMMIT_BRANCH == "main" && $CI_PIPELINE_SOURCE != "schedule"
      when: on_success
    - when: never

when#

La directive when contrôle le déclenchement d’un job :

  • on_success (défaut) : seulement si les jobs précédents ont réussi.

  • on_failure : seulement si un job précédent a échoué.

  • always : toujours, quel que soit l’état précédent.

  • manual : déclenchement manuel depuis l’interface.

  • delayed : avec un délai configurable (start_in: 30 minutes).

  • never : désactivé (utile dans les règles conditionnelles).

Includes et templates#

GitLab CI/CD permet de découper la configuration en plusieurs fichiers et de réutiliser des templates.

# Include avec templates locaux et distants (non exécutable)
include:
  # Fichier local au dépôt
  - local: '.gitlab/ci/test.yml'

  # Template officiel GitLab
  - template: 'Security/SAST.gitlab-ci.yml'
  - template: 'Security/Dependency-Scanning.gitlab-ci.yml'

  # Fichier d'un autre projet GitLab
  - project: 'org/shared-ci-templates'
    ref: main
    file: '/templates/python.yml'

  # URL distante
  - remote: 'https://example.com/ci/template.yml'

extends#

extends permet d’hériter la configuration d’un job template (prefixé par . pour être ignoré comme job réel) :

# Héritage avec extends (non exécutable)
.base-python:
  image: python:3.12-slim
  before_script:
    - pip install -r requirements.txt
  cache:
    key: pip-$CI_COMMIT_REF_SLUG
    paths:
      - .cache/pip/

unit-tests:
  extends: .base-python
  stage: test
  script:
    - pytest tests/unit/

integration-tests:
  extends: .base-python
  stage: test
  script:
    - pytest tests/integration/

!reference#

!reference permet de réutiliser des fragments de configuration YAML de manière plus granulaire qu”extends :

# Réutilisation de fragments avec !reference (non exécutable)
.setup-steps:
  before_script:
    - apt-get update -qq && apt-get install -y curl
    - curl -fsSL https://example.com/install.sh | bash

deploy-staging:
  before_script:
    - !reference [.setup-steps, before_script]
    - echo "Configuration staging supplémentaire"

Variables CI/CD#

GitLab propose un système de variables CI/CD à plusieurs niveaux de portée.

Scopes de variables#

  • Instance (admin) : disponibles pour tous les groupes et projets de l’instance.

  • Groupe : héritées par tous les sous-groupes et projets du groupe.

  • Projet : spécifiques au projet.

  • Pipeline : passées lors du déclenchement via API ou interface.

  • Job : définies directement dans le fichier .gitlab-ci.yml.

Variables prédéfinies#

GitLab expose automatiquement des dizaines de variables :

# Variables prédéfinies utiles (non exécutable)
script:
  - echo $CI_COMMIT_SHA          # SHA du commit
  - echo $CI_COMMIT_REF_NAME     # Nom de la branche ou du tag
  - echo $CI_PIPELINE_ID         # ID du pipeline
  - echo $CI_JOB_ID              # ID du job
  - echo $CI_REGISTRY_IMAGE      # URL du container registry
  - echo $CI_ENVIRONMENT_NAME    # Nom de l'environnement (si défini)
  - echo $CI_MERGE_REQUEST_IID   # Numéro de la MR (si pipeline MR)

Masquage et protection#

  • Variables masquées : valeur cachée dans les logs CI (uniquement si la valeur respecte les contraintes de masquage : pas de saut de ligne, longueur > 8 caractères).

  • Variables protégées : disponibles uniquement sur les branches et tags protégés — protège les credentials de production contre les branches non autorisées.

Variables de groupe vs projet

Les variables de groupe permettent de partager des credentials communs (registry, tokens de déploiement) entre tous les projets d’une organisation sans les dupliquer. Toujours préférer les variables de groupe pour les credentials partagés, et les variables de projet pour les configurations spécifiques.

Environnements et review apps#

Environnements#

Les environnements GitLab modélisent les cibles de déploiement avec historique, URL et statut en temps réel.

# Déploiement avec environnement (non exécutable)
deploy-staging:
  stage: deploy
  script:
    - ./deploy.sh staging
  environment:
    name: staging
    url: https://staging.example.com
  rules:
    - if: $CI_COMMIT_BRANCH == "develop"

deploy-production:
  stage: deploy
  script:
    - ./deploy.sh production
  environment:
    name: production
    url: https://app.example.com
  when: manual                   # Déclenchement manuel obligatoire
  rules:
    - if: $CI_COMMIT_BRANCH == "main"

Review Apps#

Les review apps sont des environnements de déploiement dynamiques créés automatiquement pour chaque branche de feature ou merge request. Elles permettent de visualiser les changements dans un environnement réel avant le merge.

# Review App dynamique (non exécutable)
deploy-review:
  stage: deploy
  script:
    - ./deploy.sh review-$CI_COMMIT_REF_SLUG
  environment:
    name: review/$CI_COMMIT_REF_SLUG
    url: https://$CI_COMMIT_REF_SLUG.review.example.com
    on_stop: stop-review          # Job de nettoyage
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"

stop-review:
  stage: deploy
  script:
    - ./teardown.sh review-$CI_COMMIT_REF_SLUG
  environment:
    name: review/$CI_COMMIT_REF_SLUG
    action: stop
  when: manual
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"

GitLab Container Registry et intégration Kubernetes#

GitLab intègre un container registry par projet, accessible sans configuration supplémentaire via les variables CI_REGISTRY_*.

# Build et push vers le registry GitLab (non exécutable)
build-image:
  stage: build
  image: docker:24
  services:
    - docker:24-dind
  variables:
    IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
  script:
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
    - docker build --cache-from $CI_REGISTRY_IMAGE:latest
                   --tag $IMAGE_TAG
                   --tag $CI_REGISTRY_IMAGE:latest .
    - docker push $IMAGE_TAG
    - docker push $CI_REGISTRY_IMAGE:latest

L’intégration Kubernetes (GitLab Agent for Kubernetes, anciennement GitLab Kubernetes integration) permet de déployer directement depuis le pipeline vers un cluster Kubernetes via l’agent agentk, sans exposer l’API Kubernetes publiquement.

GitLab Runners — types et executors#

Types de runners#

  • Shared runners : disponibles pour tous les projets de l’instance GitLab.com ou de l’instance self-hosted. Pool partagé géré par GitLab ou l’admin.

  • Group runners : disponibles pour tous les projets d’un groupe.

  • Project runners : dédiés à un projet spécifique.

Executors#

L’executor définit comment le runner exécute les jobs :

  • Docker : chaque job dans un conteneur éphémère — isolation forte, recommandé.

  • Kubernetes : chaque job dans un pod Kubernetes éphémère — scalabilité maximale.

  • Shell : exécution directe sur la machine hôte — rapide mais peu isolé.

  • Docker Machine (déprécié) : provisionnement dynamique de VMs.

  • VirtualBox / Parallels : isolation maximale pour les tests sur macOS/Windows.

Executor recommandé en production

L’executor Docker ou Kubernetes est recommandé pour les environnements de production. L’executor Shell expose le système hôte aux scripts CI et ne doit être utilisé que sur des runners dédiés à un projet de confiance, dans un environnement réseau isolé.

Merge trains et merge request pipelines#

Merge request pipelines#

Un merge request pipeline s’exécute sur le code de la branche source d’une MR, avant le merge. Il peut être configuré différemment du pipeline de branche classique.

# Job uniquement pour les MR pipelines (non exécutable)
mr-lint:
  stage: lint
  script:
    - echo "Validation spécifique MR"
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"

Merged results pipelines#

Exécute le pipeline sur le résultat du merge simulé (branche source + branche cible), pas uniquement sur la branche source. Détecte les régressions d’intégration avant le merge réel.

Merge trains#

Les merge trains fusionnent plusieurs MRs en attente de façon ordonnée et sécurisée. Chaque MR est testée dans une « queue » qui inclut les MRs précédentes déjà en attente. Si une MR échoue, elle est retirée du train sans impacter les autres.

Artefacts, cache et dépendances#

Artefacts GitLab#

# Configuration complète des artefacts (non exécutable)
build:
  script:
    - make build
  artifacts:
    name: "$CI_JOB_NAME-$CI_COMMIT_REF_SLUG"
    paths:
      - dist/
      - reports/
    exclude:
      - dist/**/*.map           # Exclure les source maps
    expire_in: 30 days
    when: on_success
    reports:
      junit: reports/junit.xml  # Rapport de tests dans l'interface MR
      coverage_report:
        coverage_format: cobertura
        path: coverage.xml

Cache GitLab#

# Cache avec clé dynamique (non exécutable)
.python-cache:
  cache:
    key:
      files:
        - requirements.txt       # Clé basée sur le contenu du fichier
    paths:
      - .cache/pip/
    policy: pull-push            # pull au début, push à la fin

unit-tests:
  extends: .python-cache
  script:
    - pytest

dependencies#

Par défaut, un job télécharge les artefacts de tous les jobs des stages précédents. dependencies: [] désactive ce comportement pour accélérer le job.

# Contrôle fin des dépendances d'artefacts (non exécutable)
deploy:
  stage: deploy
  dependencies:
    - docker-build               # Uniquement les artefacts de ce job
  script:
    - ./deploy.sh

Règles avancées#

workflow:rules#

Contrôle si un pipeline est créé ou non, avant même d’évaluer les jobs :

# Règles de workflow globales (non exécutable)
workflow:
  rules:
    # Ne pas créer de pipeline pour les commits WIP
    - if: $CI_COMMIT_TITLE =~ /^WIP:/
      when: never
    # Pipeline pour les MRs
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    # Pipeline pour les commits sur main et develop
    - if: $CI_COMMIT_BRANCH == "main" || $CI_COMMIT_BRANCH == "develop"
    # Pipeline pour les tags
    - if: $CI_COMMIT_TAG
    # Tout le reste est ignoré
    - when: never

parallel:matrix#

parallel:matrix est l’équivalent GitLab de la stratégie matrix de GitHub Actions :

# Matrice de test GitLab (non exécutable)
test:
  parallel:
    matrix:
      - PYTHON_VERSION: ["3.10", "3.11", "3.12"]
        OS: ["ubuntu", "alpine"]
  image: python:${PYTHON_VERSION}-${OS}
  script:
    - pytest

Visualisations#

import numpy as np
import pandas as pd
import matplotlib
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import seaborn as sns
import networkx as nx

sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)

# DAG de pipeline GitLab avec stages comme couches
G = nx.DiGraph()

# Stages et jobs associés
stage_jobs = {
    'lint':     ['lint-python', 'lint-yaml'],
    'test':     ['unit-tests', 'integration', 'contract-tests'],
    'build':    ['build-wheel', 'build-image'],
    'security': ['sast', 'dependency-scan', 'container-scan'],
    'deploy':   ['deploy-review', 'deploy-staging', 'deploy-prod'],
}

stage_colors = {
    'lint':     '#B3E5FC',
    'test':     '#C8E6C9',
    'build':    '#FFE0B2',
    'security': '#F8BBD0',
    'deploy':   '#E1BEE7',
}

# Positions : stage sur l'axe X, jobs distribués sur l'axe Y
pos = {}
stage_x = {'lint': 0, 'test': 2, 'build': 4, 'security': 6, 'deploy': 8}
node_colors_map = {}

for stage, jobs in stage_jobs.items():
    n = len(jobs)
    for i, job in enumerate(jobs):
        y = (i - (n - 1) / 2) * 1.5
        pos[job] = (stage_x[stage], y)
        node_colors_map[job] = stage_colors[stage]

# Arêtes : chaque job d'un stage pointe vers tous les jobs du stage suivant
stage_list = list(stage_jobs.keys())
for i in range(len(stage_list) - 1):
    for src in stage_jobs[stage_list[i]]:
        for dst in stage_jobs[stage_list[i + 1]]:
            # Heuristique : connecter les jobs logiquement liés
            G.add_edge(src, dst)

# Simplifier : uniquement les connexions logiques
G.clear_edges()
connexions = [
    ('lint-python',    'unit-tests'),
    ('lint-python',    'integration'),
    ('lint-yaml',      'unit-tests'),
    ('unit-tests',     'build-wheel'),
    ('unit-tests',     'build-image'),
    ('integration',    'build-wheel'),
    ('integration',    'build-image'),
    ('contract-tests', 'build-image'),
    ('build-wheel',    'sast'),
    ('build-wheel',    'dependency-scan'),
    ('build-image',    'container-scan'),
    ('sast',           'deploy-staging'),
    ('dependency-scan','deploy-staging'),
    ('container-scan', 'deploy-review'),
    ('container-scan', 'deploy-staging'),
    ('deploy-review',  'deploy-staging'),
    ('deploy-staging', 'deploy-prod'),
]
G.add_edges_from(connexions)

colors_list = [node_colors_map[n] for n in G.nodes()]

fig, ax = plt.subplots(figsize=(14, 8))
nx.draw_networkx_nodes(G, pos, node_color=colors_list, node_size=2400, ax=ax, alpha=0.9)
nx.draw_networkx_labels(G, pos, font_size=7.5, font_weight='bold', ax=ax)
nx.draw_networkx_edges(G, pos, ax=ax, arrows=True, arrowstyle='->',
                       arrowsize=18, edge_color='#546E7A', width=1.5,
                       connectionstyle='arc3,rad=0.03')

# Annotations de stage
for stage, x in stage_x.items():
    ax.text(x, -2.6, stage.upper(), ha='center', va='center', fontsize=10,
            fontweight='bold', color='#37474F',
            bbox=dict(boxstyle='round,pad=0.3', facecolor=stage_colors[stage], alpha=0.8))

legend_elements = [mpatches.Patch(color=c, label=s.capitalize())
                   for s, c in stage_colors.items()]
ax.legend(handles=legend_elements, loc='upper left', fontsize=9, frameon=True)
ax.set_title('DAG de pipeline GitLab CI/CD — stages et dépendances', fontsize=13, fontweight='bold', pad=15)
ax.set_ylim(-3.2, 3.2)
ax.axis('off')
plt.show()
_images/d68eb85e17fdad1b4d790de91bb463b738557985e0adbb72420710070cbdedc6.png
sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)

# Heatmap comparative GitHub Actions vs GitLab CI
features = [
    'Syntaxe YAML',
    'Marketplace / Templates',
    'Runners self-hosted',
    'Matrice de build',
    'Cache natif',
    'Artefacts',
    'Workflows réutilisables',
    'Environnements',
    'Review Apps',
    'Container Registry intégré',
    'Merge trains',
    'OIDC / Secrets gestion',
    'Intégration Kubernetes',
    'Interface utilisateur',
    'Coût (self-hosted)',
]

# Scores sur 5 (5 = meilleur)
scores_github = [4, 5, 4, 4, 4, 4, 4, 4, 2, 3, 2, 5, 3, 4, 5]
scores_gitlab = [4, 3, 5, 5, 4, 5, 4, 5, 5, 5, 5, 4, 5, 3, 4]

data = np.array([scores_github, scores_gitlab]).T
df = pd.DataFrame(data, index=features, columns=['GitHub Actions', 'GitLab CI/CD'])

fig, ax = plt.subplots(figsize=(9, 9))
im = ax.imshow(data, cmap='RdYlGn', aspect='auto', vmin=1, vmax=5)

ax.set_xticks([0, 1])
ax.set_xticklabels(['GitHub Actions', 'GitLab CI/CD'], fontsize=12, fontweight='bold')
ax.set_yticks(range(len(features)))
ax.set_yticklabels(features, fontsize=10)

for i in range(len(features)):
    for j in range(2):
        score = data[i, j]
        ax.text(j, i, str(score), ha='center', va='center', fontsize=11,
                fontweight='bold', color='white' if score <= 2 else 'black')

cbar = fig.colorbar(im, ax=ax, fraction=0.03, pad=0.04)
cbar.set_label('Score (1 = faible, 5 = excellent)', fontsize=10)
cbar.set_ticks([1, 2, 3, 4, 5])

ax.set_title('Comparaison GitHub Actions vs GitLab CI/CD', fontsize=13, fontweight='bold', pad=15)
plt.show()
_images/0c639878e86a576668f59d48e833eb3c2d30d1f340024ea4cb288247409fb588.png

Tableau de décision : GitHub Actions vs GitLab CI#

Critère

GitHub Actions

GitLab CI/CD

Hébergement du code

GitHub

GitLab (SaaS ou self-hosted)

Écosystème d’actions

Très riche (Marketplace)

Templates officiels limités

Review Apps

Non natif

Natif et intégré

Container Registry

GHCR (séparé)

Intégré au projet

Merge trains

Non

Oui

Coût runners SaaS

Minutes incluses par plan

Minutes incluses par plan

Self-hosted runner

Facile

Facile + plus d’executors

Secrets gestion

GitHub Secrets + OIDC

Variables CI/CD + Vault natif

Intégration Kubernetes

Via actions tierces

GitLab Agent natif

Pipeline as Code

YAML dans .github/

YAML .gitlab-ci.yml

Includes/templates

Actions composites

include:, extends:, !reference

Règles avancées

if: + expressions

rules:, workflow:rules

Recommandation : choisir GitHub Actions si le code est sur GitHub et que l’écosystème Marketplace est un avantage décisif. Choisir GitLab CI/CD pour une plateforme DevOps complète et intégrée (code, CI, registry, déploiement, monitoring), en particulier en self-hosted pour des raisons de conformité ou de coût.

Résumé#

  1. Le fichier .gitlab-ci.yml structure le pipeline en stages (séquentiels) et jobs (parallèles au sein d’un stage), avec une syntaxe expressive pour les conditions de déclenchement via rules.

  2. La directive rules remplace only/except et offre une granularité fine sur les conditions de déclenchement, avec support des expressions régulières et des variables CI.

  3. Les include, extends et !reference permettent de factoriser la configuration CI en templates réutilisables, partagés entre projets via un dépôt de templates centralisé.

  4. Les variables CI/CD à trois niveaux de portée (instance, groupe, projet) permettent de gérer les credentials de manière hiérarchique avec masquage et protection.

  5. Les review apps créent automatiquement des environnements de déploiement éphémères par branche, permettant de visualiser les changements avant le merge.

  6. Le container registry GitLab est intégré sans configuration supplémentaire, accessible via les variables CI_REGISTRY_* prédéfinies dans chaque pipeline.

  7. Les runners GitLab supportent plusieurs executors (Docker, Kubernetes, Shell) ; l’executor Docker ou Kubernetes est recommandé pour l’isolation et la reproductibilité.

  8. Les merge trains permettent de fusionner plusieurs MRs en file d’attente de façon ordonnée, en testant chaque MR dans le contexte des MRs précédentes.

  9. workflow:rules contrôle la création même du pipeline, avant l’évaluation des jobs — utile pour éviter des pipelines inutiles sur les commits WIP.

  10. GitLab CI/CD est préférable à GitHub Actions pour les déploiements nécessitant une plateforme DevOps intégrée (registry, Kubernetes, review apps, merge trains) en particulier dans des environnements self-hosted soumis à des contraintes de conformité.