Qu'est-ce que CI/CD ?
CI (Intégration Continue) automatise la vérification du code à chaque push : compilation, tests, linting.
CD (Livraison/Déploiement Continu) automatise ensuite la mise en production.
GitLab CI/CD lit le fichier .gitlab-ci.yml à la racine de votre repo et exécute automatiquement un pipeline.
| Terme | Signification | Exemple concret |
|---|---|---|
| CI — Intégration Continue | Vérifier automatiquement chaque commit | Tests lancés à chaque push |
| CD — Livraison Continue | Code toujours prêt à être déployé | Build versionné disponible après chaque merge |
| CD — Déploiement Continu | Déploiement automatique en production | Chaque merge sur main → prod mise à jour |
| Pipeline | Ensemble des stages et jobs déclenchés | build → test → deploy |
| Stage | Groupe de jobs qui s'exécutent en parallèle | Tous les jobs de "test" tournent en même temps |
| Job | Unité d'exécution avec un script | unit-tests, lint, docker-build |
| Runner | Machine qui exécute les jobs | Serveur Linux avec gitlab-runner installé |
| Artifact | Fichier produit par un job, partagé dans le pipeline | dist/, rapport de tests |
Démarrage rapide — Premier pipeline en 5 min
.gitlab-ci.yml à la racine (même niveau que README.md).# Ce pipeline a 1 stage et 1 job
stages:
- hello
dire-bonjour:
stage: hello
script:
- echo "Bonjour depuis GitLab CI !"
- echo "Je tourne sur un runner"stages:
- install
- test
- build
default:
image: node:20-alpine # image Docker utilisée par tous les jobs
cache:
key: "$CI_COMMIT_REF_SLUG"
paths: [node_modules/] # mise en cache pour accélérer
installer-deps:
stage: install
script:
- npm ci # install propre depuis package-lock.json
lancer-tests:
stage: test
script:
- npm test
artifacts:
paths: [coverage/] # conserve le rapport de couverture
expire_in: 1 week
compiler-app:
stage: build
script:
- npm run build
artifacts:
paths: [dist/] # fichiers compilés disponibles pour téléchargementStructure .gitlab-ci.yml
Fichier YAML placé à la racine du repo. GitLab le lit à chaque push, MR, tag ou déclenchement manuel.
# ── 1. ORDRE DES STAGES ───────────────────────────────────────
stages:
- build # les jobs de ce stage tournent avant "test"
- test
- deploy
# ── 2. VARIABLES GLOBALES ────────────────────────────────────
variables:
APP_NAME: "my-app"
DOCKER_IMAGE: "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA"
# ── 3. DEFAULTS (appliqués à tous les jobs) ──────────────────
default:
image: node:20-alpine
before_script:
- npm ci --cache .npm
retry: 2 # retry automatique en cas d'échec runner
timeout: 10 minutes
# ── 4. WORKFLOW — contrôle quand un pipeline est créé ────────
workflow:
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
- if: $CI_COMMIT_TAG
- when: never
# ── 5. UN JOB ────────────────────────────────────────────────
build-app:
stage: build # dans quel stage s'exécute ce job
image: node:20 # surcharge l'image default
script: # commandes exécutées dans le conteneur
- npm run build
artifacts: # fichiers à conserver
paths: [dist/]| Variable | Exemple de valeur | Usage |
|---|---|---|
| CI_COMMIT_SHA | a1b2c3d4e5f6... | Hash complet du commit |
| CI_COMMIT_SHORT_SHA | a1b2c3d4 | Tag d'image Docker, nommage de build |
| CI_COMMIT_BRANCH | main, feature/login | Conditionner selon la branche |
| CI_COMMIT_TAG | v1.2.3 | Non défini si ce n'est pas un tag |
| CI_COMMIT_MESSAGE | "Fix bug #42" | Message du commit |
| CI_PIPELINE_ID | 12345 | Identifiant unique du pipeline |
| CI_PIPELINE_SOURCE | push, merge_request_event, schedule | Origine du pipeline |
| CI_JOB_NAME | build-app | Nom du job en cours |
| CI_PROJECT_DIR | /builds/group/project | Répertoire de travail |
| CI_PROJECT_NAME | my-app | Nom du projet GitLab |
| CI_PROJECT_PATH | my-group/my-app | Chemin complet namespace/projet |
| CI_REGISTRY | registry.gitlab.com | URL du registry Docker GitLab |
| CI_REGISTRY_IMAGE | registry.gitlab.com/group/project | Image de base du projet |
| CI_REGISTRY_USER / PASSWORD | (auto-générés) | Login registry CI |
| CI_ENVIRONMENT_NAME | staging, production | Nom de l'environnement actif |
| CI_DEFAULT_BRANCH | main | Branche principale du projet |
| GITLAB_USER_EMAIL | dev@example.com | Email de l'auteur du push |
- export dans le script pour les voir toutes.
Stages & Jobs
Les stages définissent l'ordre d'exécution. Tous les jobs d'un même stage tournent en parallèle. Le stage suivant ne démarre que si tous les jobs du stage précédent réussissent.
unit-tests:
stage: test
image: node:20-alpine # surcharge l'image default
tags: # cibler un runner spécifique
- docker
- linux
allow_failure: false # true = job rouge mais pipeline continue
timeout: 10 minutes
retry:
max: 2
when:
- runner_system_failure
- stuck_or_timeout_failure
before_script:
- npm ci
script: # ← OBLIGATOIRE
- npm test -- --coverage
after_script: # toujours exécuté, même si script échoue
- echo "Tests terminés"
coverage: '/Lines\s*:\s*(\d+\.?\d*)%/' # regex extraction % coverage
# Job déclenché manuellement
deploy-prod:
stage: deploy
script:
- ./deploy.sh
when: manual # on_success | on_failure | always | manual | delayed
start_in: "1 hour" # uniquement pour when: delayed| Valeur | Comportement | Cas d'usage |
|---|---|---|
| on_success | Défaut — s'exécute si tous les jobs précédents réussissent | Job normal |
| on_failure | S'exécute uniquement si un job précédent échoue | Notification d'erreur, rollback |
| always | S'exécute quoi qu'il arrive | Nettoyage de ressources |
| manual | Nécessite un clic humain dans l'UI GitLab | Deploy en production |
| delayed | Attend un délai configuré via start_in | Canary deploy progressif |
| never | Ne s'exécute jamais (utile avec rules) | Désactiver un job selon condition |
Variables
Les variables sont des paires clé=valeur disponibles dans les scripts via $NOM_VARIABLE. On les définit dans le YAML ou dans l'interface GitLab pour les secrets.
# Variables globales (tous les jobs)
variables:
ENV: "production"
PORT: "3000" # toujours une string en YAML
URL: "https://$CI_PROJECT_NAME.example.com" # interpolation OK
# Variable locale à un job (surcharge la globale)
my-job:
variables:
ENV: "staging"
GIT_STRATEGY: none # fetch | clone | none — "none" = pas de checkout
GIT_DEPTH: "10" # shallow clone, accélère le checkout
script:
- echo $ENV # → staging
- echo ${ENV:-default} # valeur par défaut bash si vide
- echo ${APP_NAME:?Variable obligatoire} # erreur si non définie
# Secrets — définis dans GitLab UI (Settings › CI/CD › Variables)
# ⚠ Ne jamais mettre de mot de passe dans le YAML !
deploy:
script:
- echo $DB_PASSWORD # variable masquée définie dans l'UI → *** dans les logs
- curl -H "Token: $API_KEY" https://api.example.com| Option | Effet | Quand l'utiliser |
|---|---|---|
| Masked | Valeur remplacée par *** dans les logs | Mots de passe, tokens API |
| Protected | Disponible uniquement sur branches/tags protégés | Credentials de production |
| Expand variable references | Interpole $OTHER_VAR dans la valeur | Variables composées |
| File | Crée un fichier temporaire avec le contenu | Clés SSH, certificates .pem |
| # | Source | Scope |
|---|---|---|
| 1 | Trigger variables (API) | Variables passées au déclenchement |
| 2 | Variables UI projet (protected) | Branches/tags protégés uniquement |
| 3 | Variables UI projet | Tous les pipelines du projet |
| 4 | Variables UI groupe | Tous les projets du groupe |
| 5 | Variables .gitlab-ci.yml (job) | Job courant seulement |
| 6 | Variables .gitlab-ci.yml (global) | Tous les jobs du fichier |
| 7 | Variables prédéfinies CI | CI_*, GITLAB_*, etc. |
Rules & conditions
rules remplace only/except (déprécié). Il permet de conditionner finement quand un job s'exécute, avec quel comportement (when) et quelles variables.
deploy-staging:
script: ./deploy.sh staging
rules:
# Si branche develop → exécute automatiquement
- if: '$CI_COMMIT_BRANCH == "develop"'
when: on_success
# Si MR vers main → exécute manuellement
- if: '$CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "main"'
when: manual
# Si un fichier spécifique a été modifié
- changes:
- src/**/*
- package.json
# Fallback : ne jamais exécuter autrement
- when: never
release:
script: ./release.sh
rules:
# Uniquement sur les tags semver (ex: v1.2.3)
- if: '$CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/'
# Injecter des variables différentes selon la règle
build:
script: npm run build
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
variables:
BUILD_ENV: production
MINIFY: "true"
- if: '$CI_COMMIT_BRANCH'
variables:
BUILD_ENV: staging
MINIFY: "false"
# Combiner if ET changes (les deux doivent être vrais)
test-api:
script: npm run test:api
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
changes:
- api/**/*
- tests/api/**/*| Condition | Cas d'usage |
|---|---|
| $CI_COMMIT_BRANCH == "main" | Uniquement sur main |
| $CI_COMMIT_BRANCH =~ /^feature/ | Toutes les branches feature/* |
| $CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH | Tout sauf la branche principale |
| $CI_COMMIT_TAG | Si un tag est poussé (non nul) |
| $CI_COMMIT_TAG =~ /^v\d+/ | Tags de type v1.x.x |
| $CI_PIPELINE_SOURCE == "merge_request_event" | Pipeline MR uniquement |
| $CI_PIPELINE_SOURCE == "schedule" | Pipeline planifié (cron GitLab) |
| $CI_PIPELINE_SOURCE == "web" | Déclenché manuellement depuis l'UI |
| $CI_PIPELINE_SOURCE == "api" | Déclenché via l'API GitLab |
| $MY_VAR == null | Variable non définie |
| $MY_VAR != null | Variable définie (quelle que soit la valeur) |
| $MY_VAR == "true" && $ENV == "prod" | Combinaison avec ET logique |
if doivent être entre guillemets simples : if: '$VAR == "valeur"'. Les guillemets internes utilisent des doubles guillemets.
Needs & DAG
needs transforme votre pipeline en graphe orienté acyclique (DAG). Un job peut démarrer dès que ses dépendances directes sont terminées, sans attendre tous les jobs du stage précédent.
stages:
- build
- test
- deploy
build-frontend:
stage: build
script: npm run build:frontend
build-backend:
stage: build
script: npm run build:backend
# Ne attend que build-frontend, pas build-backend
test-frontend:
stage: test
needs:
- job: build-frontend
artifacts: true # télécharge les artifacts du job parent
script: npm run test:frontend
test-backend:
stage: test
needs: [build-backend] # syntaxe courte
script: npm run test:backend
# Attend les deux tests
deploy-staging:
stage: deploy
needs: [test-frontend, test-backend]
script: ./deploy.sh
# needs: [] = démarre immédiatement (ignore les stages précédents)
notify-start:
stage: test
needs: []
script: curl -X POST $SLACK_WEBHOOK -d '{"text":"Pipeline démarré"}'Artifacts & Cache
Deux mécanismes pour partager des fichiers, souvent confondus. Comprendre la différence est essentiel.
build:
script: npm run build
artifacts:
paths:
- dist/
- coverage/
exclude:
- dist/**/*.map # exclure les sourcemaps
reports:
junit: test-results.xml # rapport visible dans l'UI MR
coverage_report:
coverage_format: cobertura
path: coverage/cobertura.xml
expire_in: 1 week # 30 mins | 1 day | 1 week | never
when: always # conserver même si le job échoue
untracked: false # inclure les fichiers non-gitinstall:
script: npm ci
cache:
key:
files: # clé basée sur le hash du fichier lock
- package-lock.json
paths:
- node_modules/
- .npm/
policy: pull-push # pull | push | pull-push (défaut)
# Job qui ne fait que lire le cache (pas d'écriture)
test:
cache:
key: "$CI_COMMIT_REF_SLUG-node"
paths: [node_modules/]
policy: pull # plus rapide : ne re-pousse pas
# Cache Python pip
test-python:
image: python:3.12
cache:
key:
files: [requirements.txt]
paths: [.pip-cache/]
variables:
PIP_CACHE_DIR: "$CI_PROJECT_DIR/.pip-cache"
script:
- pip install -r requirements.txt
- pytest| Critère | Artifacts | Cache |
|---|---|---|
| Portée | Jobs du même pipeline | Entre pipelines (même branche/runner) |
| Fiabilité | Garanti disponible | Best-effort (peut ne pas exister) |
| Stockage | Serveur GitLab | Runner local (ou S3 configurable) |
| Visible dans UI | Oui, téléchargeable | Non |
| Usage typique | dist/, rapports de tests, binaires | node_modules/, .pip-cache/, .m2/ |
Runners
Un Runner est un agent (programme) installé sur une machine qui reçoit les jobs de GitLab et les exécute. GitLab.com propose des runners partagés gratuits. Vous pouvez aussi enregistrer vos propres runners.
| Executor | Description | Recommandé pour |
|---|---|---|
| docker | Chaque job dans un conteneur Docker isolé (image: dans le YAML) | La plupart des projets ✅ |
| shell | Exécute directement sur le serveur hôte (pas de conteneur) | Legacy, accès hardware spécifique |
| kubernetes | Crée un Pod K8s éphémère par job | Infra cloud native |
| docker+machine | Auto-scale via Docker Machine | Pipelines à fort volume |
| virtualbox / parallels | VMs isolées | Tests iOS / cross-platform |
# ── Installation sur Ubuntu/Debian ────────────────────────────
curl -L https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh | sudo bash
sudo apt install gitlab-runner
# ── Enregistrement interactif (pose des questions) ────────────
sudo gitlab-runner register
# ── Enregistrement non-interactif (scripts CI) ────────────────
sudo gitlab-runner register \
--non-interactive \
--url "https://gitlab.com/" \
--registration-token "$RUNNER_TOKEN" \
--executor "docker" \
--docker-image "alpine:latest" \
--description "mon-runner-docker" \
--tag-list "docker,linux" \
--docker-privileged # nécessaire pour Docker-in-Docker
# ── Vérifier que le runner tourne ────────────────────────────
sudo gitlab-runner status
sudo gitlab-runner list# Job sur runner avec tag "docker"
build-docker:
tags:
- docker
- linux
# Job sur runner K8s
deploy-k8s:
tags:
- kubernetes
# Job sur runner Windows
build-windows:
tags:
- windows
script:
- msbuild solution.sln
# ⚠ Si aucun runner ne correspond aux tags → job bloqué "stuck"Services (Docker, BDD)
Les services lancent des conteneurs annexes accessibles depuis votre job via leur alias hostname. Parfait pour les tests d'intégration avec une vraie base de données.
alias (ou le nom de l'image par défaut). Les deux partagent le même réseau Docker.
tests-integration:
image: node:20-alpine
services:
- name: postgres:16-alpine
alias: db # accès via hostname "db"
- name: redis:7-alpine
alias: redis
- name: elasticsearch:8.11.0
alias: elastic
variables:
discovery.type: single-node
variables:
POSTGRES_DB: testdb
POSTGRES_USER: ci
POSTGRES_PASSWORD: ci
DATABASE_URL: "postgresql://ci:ci@db:5432/testdb"
REDIS_URL: "redis://redis:6379"
before_script:
- npm ci
- npx wait-on tcp:db:5432 # attendre que postgres soit prêt
script:
- npm run db:migrate
- npm run test:integration# Docker-in-Docker : exécuter Docker depuis un job Docker
build-image:
image: docker:26
services:
- docker:26-dind # Docker daemon dans un conteneur
variables:
DOCKER_TLS_CERTDIR: "/certs" # TLS activé (recommandé)
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
script:
- docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA .
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
# Taguer aussi "latest" sur main
- |
if [ "$CI_COMMIT_BRANCH" = "$CI_DEFAULT_BRANCH" ]; then
docker tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA $CI_REGISTRY_IMAGE:latest
docker push $CI_REGISTRY_IMAGE:latest
fi| Service | Image | Alias conseillé | Variable clé |
|---|---|---|---|
| PostgreSQL | postgres:16-alpine | db | POSTGRES_DB, POSTGRES_USER, POSTGRES_PASSWORD |
| MySQL | mysql:8.0 | mysql | MYSQL_DATABASE, MYSQL_ROOT_PASSWORD |
| MongoDB | mongo:7 | mongo | MONGO_INITDB_DATABASE |
| Redis | redis:7-alpine | redis | — |
| Elasticsearch | elasticsearch:8.11.0 | elastic | discovery.type=single-node |
| RabbitMQ | rabbitmq:3-management | rabbitmq | RABBITMQ_DEFAULT_USER/PASS |
Environments & Deploy
Les environments tracent vos déploiements dans GitLab (Operate → Environments). Chaque deploy est enregistré avec son SHA, sa date et son auteur. Vous pouvez déclencher un rollback depuis l'UI.
deploy-staging:
stage: deploy
script:
- ./deploy.sh $CI_ENVIRONMENT_URL
environment:
name: staging
url: https://staging.example.com
on_stop: stop-staging # job à appeler pour supprimer l'env
deployment_tier: staging
stop-staging:
stage: deploy
script:
- ./teardown.sh staging
environment:
name: staging
action: stop # stop | start | prepare | verify | access
when: manual
# ── Review Apps : environnement dynamique par MR ─────────────
deploy-review:
stage: deploy
script:
- ./deploy-review.sh $CI_COMMIT_REF_SLUG
environment:
name: review/$CI_COMMIT_REF_SLUG
url: https://$CI_COMMIT_REF_SLUG.review.example.com
on_stop: stop-review
auto_stop_in: 1 week # suppression automatique
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
stop-review:
stage: deploy
script: ./teardown.sh $CI_COMMIT_REF_SLUG
environment:
name: review/$CI_COMMIT_REF_SLUG
action: stop
when: manual
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
when: manual
# ── Production : déclenchement sur tag uniquement ─────────────
deploy-prod:
stage: deploy
script:
- ./deploy.sh production
environment:
name: production
url: https://example.com
deployment_tier: production
rules:
- if: '$CI_COMMIT_TAG =~ /^v\d+/'
when: manualExtends & Anchors YAML
Pour éviter la duplication dans le YAML. extends est la méthode GitLab recommandée, les YAML anchors (&, *) sont une alternative native YAML.
# Job de base — préfixe . = caché (non exécuté directement)
.base-node:
image: node:20-alpine
before_script:
- npm ci --cache .npm
cache:
key: "$CI_COMMIT_REF_SLUG"
paths: [.npm/]
# .rules-mr-only — conditions réutilisables
.rules-mr-only:
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
build:
extends: .base-node # hérite image, before_script, cache
stage: build
script: npm run build # seule différence
test:
extends: .base-node
stage: test
script: npm test
# extends multiple (héritage de plusieurs sources)
lint:
extends:
- .base-node
- .rules-mr-only # applique aussi les rules
script: npm run lint# Définir un ancre &
.before_npm: &before_npm
- npm ci
- echo "Dépendances prêtes"
# Réutiliser avec *
build:
before_script: *before_npm
script: npm run build
# Merge de maps avec <<:
.common: &common
image: node:20
tags: [docker]
retry: 2
my-job:
<<: *common # fusionne les clés de .common
script: echo "hello"extends : il fusionne récursivement les clés imbriquées, les anchors YAML non. extends est aussi plus lisible dans l'interface GitLab (onglet "Full config").
Include & Templates
include permet de découper votre CI en plusieurs fichiers et de réutiliser des templates officiels GitLab ou d'équipe.
include:
# Fichier local dans ce repo
- local: ".gitlab/ci/build.yml"
- local: ".gitlab/ci/deploy.yml"
# Template officiel GitLab (hébergé sur gitlab.com)
- template: Jobs/SAST.gitlab-ci.yml
- template: Workflows/MergeRequest-Pipelines.gitlab-ci.yml
# Depuis un autre projet GitLab (templates d'équipe)
- project: my-group/ci-templates
ref: main
file:
- templates/docker.yml
- templates/notify.yml
# URL externe (HTTPS)
- remote: https://example.com/ci/shared-template.yml
# Surcharger un job venant d'un template inclus
SAST:
variables:
SAST_EXCLUDED_PATHS: "spec,test,tests,tmp,node_modules"
rules:
- if: $CI_PIPELINE_SOURCE != "schedule"| Template | Contenu |
|---|---|
| Jobs/SAST.gitlab-ci.yml | Analyse statique de sécurité du code source |
| Jobs/Secret-Detection.gitlab-ci.yml | Détection de secrets (tokens, clés) dans le code |
| Jobs/Container-Scanning.gitlab-ci.yml | Scan des vulnérabilités dans les images Docker |
| Jobs/Dependency-Scanning.gitlab-ci.yml | Vulnérabilités dans les dépendances (npm, pip…) |
| Jobs/DAST.gitlab-ci.yml | Scan dynamique de l'application déployée |
| Workflows/MergeRequest-Pipelines.yml | Configure les pipelines pour les MR |
| Auto-DevOps.gitlab-ci.yml | Pipeline complet clés en main (build, test, deploy K8s) |
.gitlab/ci/ avec un fichier par domaine : build.yml, test.yml, deploy.yml, security.yml. Puis include-les depuis .gitlab-ci.yml.
Parallel matrix
parallel duplique un job. parallel: matrix crée une combinatoire de variables — idéal pour tester sur plusieurs versions ou déployer en multi-régions.
# N copies identiques d'un job (utile pour fragmenter une suite de tests)
rspec:
script: bundle exec rspec
parallel: 5 # 5 jobs identiques → $CI_NODE_INDEX (1..5)
# Matrix — combinatoire de variables
test-matrix:
image: "$RUNTIME:$VERSION"
script: npm test
parallel:
matrix:
- RUNTIME: node
VERSION: ["18", "20", "22"]
- RUNTIME: bun
VERSION: ["1.0", "1.1"]
# → 5 jobs : node:18, node:20, node:22, bun:1.0, bun:1.1
# Matrix pour déploiement multi-régions × multi-envs
deploy:
script:
- ./deploy.sh --region $REGION --env $ENV
environment:
name: "$ENV/$REGION"
parallel:
matrix:
- REGION: [eu-west-1, us-east-1, ap-southeast-1]
ENV: [staging, production]
# → 6 jobs (3 régions × 2 envs)Pipelines parent / enfant
Pour les monorepos ou projets complexes : le pipeline parent orchestre des sous-pipelines enfants, déclenchés selon les fichiers modifiés.
# .gitlab-ci.yml (pipeline parent)
stages:
- triggers
trigger-frontend:
stage: triggers
trigger:
include: frontend/.gitlab-ci.yml # pipeline enfant local
strategy: depend # parent attend la fin de l'enfant
rules:
- changes: [frontend/**/*] # déclenché seulement si frontend modifié
trigger-api:
trigger:
project: my-group/api-service # pipeline dans un AUTRE repo
branch: main
rules:
- changes: [api/**/*]# Job qui génère un fichier YAML dynamiquement (Python, bash, etc.)
generate-pipeline:
script:
- python3 scripts/gen_pipeline.py > generated-pipeline.yml
artifacts:
paths: [generated-pipeline.yml]
run-generated:
trigger:
include:
- artifact: generated-pipeline.yml
job: generate-pipeline
strategy: depend
needs: [generate-pipeline]Security scans (SAST, DAST…)
GitLab embarque des scanners de sécurité accessibles via des templates officiels. Les résultats s'affichent directement dans les MR.
# Inclure les scanners souhaités
include:
- template: Jobs/SAST.gitlab-ci.yml
- template: Jobs/Secret-Detection.gitlab-ci.yml
- template: Jobs/Dependency-Scanning.gitlab-ci.yml
- template: Jobs/Container-Scanning.gitlab-ci.yml
# Personnaliser SAST (exclure des dossiers)
semgrep-sast:
variables:
SAST_EXCLUDED_PATHS: "spec,test,tests,tmp,node_modules"
SAST_EXCLUDED_ANALYZERS: "bandit,flawfinder"
# DAST — scanner l'appli déployée (nécessite un env accessible)
include:
- template: DAST.gitlab-ci.yml
dast:
variables:
DAST_WEBSITE: https://staging.example.com
DAST_FULL_SCAN_ENABLED: "true"
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
# Container scanning — analyser une image Docker
container_scanning:
variables:
CS_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA| Scanner | Ce qu'il cherche | Plan requis |
|---|---|---|
| SAST | Vulnérabilités dans le code source (injection, XSS…) | Free (résultats basiques) |
| Secret Detection | Clés API, tokens, mots de passe dans le code | Free |
| Dependency Scanning | CVE dans les dépendances npm, pip, maven… | Ultimate complet |
| Container Scanning | CVE dans les layers de l'image Docker | Ultimate complet |
| DAST | Vulnérabilités sur l'appli en cours d'exécution | Ultimate complet |
| License Scanning | Licences des dépendances (GPL, MIT…) | Ultimate |
Bonnes pratiques
workflow:
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
- if: $CI_COMMIT_TAG
- if: $CI_PIPELINE_SOURCE == "schedule"
- when: never # tous les autres cas ignorés
# Optimisations de performance
.slim-defaults:
image: alpine:3.19 # préférer :alpine ou :slim
variables:
GIT_DEPTH: "1" # shallow clone
FF_USE_FASTZIP: "true" # compression artifacts plus rapide
TRANSFER_METER_FREQUENCY: "5s"✅ À FAIRE
// Utiliser rules: plutôt que only:/except: (déprécié)
// Versionner les images Docker (node:20.11 > node:latest)
// Séparer build / test / deploy en stages distincts
// Utiliser needs: pour paralléliser et accélérer le pipeline
// Stocker les secrets dans CI/CD Variables (masked + protected)
// Factoriser avec .hidden-jobs + extends:
// Activer Secret Detection au minimum (gratuit)
// Fixer expire_in sur les artifacts non essentiels
// Utiliser GIT_DEPTH: "1" si historique git non nécessaire
// Valider le YAML avec CI Lint avant de pusher
// Utiliser workflow: rules pour éviter les pipelines en double
❌ À ÉVITER
// Stocker des secrets en clair dans le YAML ou le code
// image: latest (non reproductible, peut casser)
// allow_failure: true sur des jobs critiques (sécurité, tests)
// Scripts inline trop longs — extraire dans des fichiers .sh
// Un seul job monolithique qui fait tout (illisible, lent)
// Cache avec policy: pull-push dans des jobs parallèles (conflits)
// Ignorer les rapports Security dans les MR
// Laisser des tokens/credentials dans les variables non-maskedExemple complet — Application Node.js + Docker
Un pipeline réel et complet pour une application Node.js/Express déployée avec Docker. Copiez-collez et adaptez les variables à votre projet.
# ═══════════════════════════════════════════════════════════════
# Pipeline complet : install → quality → build → docker → deploy
# Adapté pour : Node.js 20, Docker, déploiement SSH/Kubernetes
# ═══════════════════════════════════════════════════════════════
# Évite les pipelines en double (push + MR)
workflow:
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
- if: $CI_COMMIT_TAG
- when: never
stages:
- install
- quality
- build
- docker
- deploy-staging
- deploy-prod
variables:
NODE_VERSION: "20"
IMAGE_TAG: "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA"
IMAGE_LATEST: "$CI_REGISTRY_IMAGE:latest"
# ── Template de base partagé ──────────────────────────────────
.node-base:
image: node:$NODE_VERSION-alpine
cache:
key:
files: [package-lock.json]
paths: [node_modules/, .npm/]
policy: pull
# ── STAGE : install ───────────────────────────────────────────
install-dependencies:
extends: .node-base
stage: install
cache:
key:
files: [package-lock.json]
paths: [node_modules/, .npm/]
policy: pull-push # ce job écrit le cache
script:
- npm ci --cache .npm --prefer-offline
# ── STAGE : quality (parallèle) ───────────────────────────────
lint:
extends: .node-base
stage: quality
needs: [install-dependencies]
script:
- npm run lint
allow_failure: false
unit-tests:
extends: .node-base
stage: quality
needs: [install-dependencies]
script:
- npm test -- --coverage --ci
coverage: '/Statements\s*:\s*(\d+\.?\d*)%/'
artifacts:
when: always
reports:
junit: junit.xml
coverage_report:
coverage_format: cobertura
path: coverage/cobertura.xml
paths: [coverage/]
expire_in: 30 days
security-audit:
extends: .node-base
stage: quality
needs: [install-dependencies]
script:
- npm audit --audit-level=high
allow_failure: true # ne bloque pas si vulnerabilité modérée
# ── STAGE : build ─────────────────────────────────────────────
build-app:
extends: .node-base
stage: build
needs: [lint, unit-tests]
script:
- npm run build
artifacts:
paths: [dist/]
expire_in: 1 week
# ── STAGE : docker ────────────────────────────────────────────
build-image:
stage: docker
image: docker:26
services:
- docker:26-dind
variables:
DOCKER_TLS_CERTDIR: "/certs"
needs:
- job: build-app
artifacts: true
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
script:
- docker build --build-arg NODE_ENV=production -t $IMAGE_TAG .
- docker push $IMAGE_TAG
- |
if [ "$CI_COMMIT_BRANCH" = "$CI_DEFAULT_BRANCH" ]; then
docker tag $IMAGE_TAG $IMAGE_LATEST
docker push $IMAGE_LATEST
fi
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
- if: $CI_COMMIT_TAG
# ── STAGE : deploy-staging ────────────────────────────────────
deploy-to-staging:
stage: deploy-staging
image: alpine:3.19
needs: [build-image]
before_script:
- apk add --no-cache openssh-client
- eval $(ssh-agent -s)
- echo "$SSH_PRIVATE_KEY" | ssh-add - # clé SSH définie dans CI/CD Variables
- mkdir -p ~/.ssh && chmod 700 ~/.ssh
- ssh-keyscan $STAGING_HOST >> ~/.ssh/known_hosts
script:
- ssh deploy@$STAGING_HOST "docker pull $IMAGE_TAG && docker-compose up -d"
environment:
name: staging
url: https://staging.example.com
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
# ── STAGE : deploy-prod (manuel, sur tag uniquement) ──────────
deploy-to-production:
stage: deploy-prod
image: alpine:3.19
needs: [deploy-to-staging]
before_script:
- apk add --no-cache openssh-client
- eval $(ssh-agent -s)
- echo "$SSH_PRIVATE_KEY" | ssh-add -
- mkdir -p ~/.ssh && chmod 700 ~/.ssh
- ssh-keyscan $PROD_HOST >> ~/.ssh/known_hosts
script:
- ssh deploy@$PROD_HOST "docker pull $IMAGE_TAG && docker-compose up -d"
environment:
name: production
url: https://example.com
deployment_tier: production
rules:
- if: '$CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/'
when: manual# Étape 1 : build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY dist/ ./dist/
# Étape 2 : image finale (légère)
FROM node:20-alpine
WORKDIR /app
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
COPY --from=builder /app .
USER appuser
EXPOSE 3000
CMD ["node", "dist/index.js"]| Variable | Type | Valeur |
|---|---|---|
| SSH_PRIVATE_KEY | File / Masked | Clé SSH privée du compte deploy |
| STAGING_HOST | Variable | IP ou hostname du serveur staging |
| PROD_HOST | Variable / Protected | IP ou hostname du serveur prod |
Troubleshooting — Résoudre les problèmes courants
set -x en début de script pour voir chaque commande exécutée. Temporairement : command || true pour ignorer une erreur.artifacts.paths est incorrect.when: always pour conserver les artifacts même en cas d'échec.CI_DEBUG_TRACE: "true" (attention : affiche tous les secrets).- when: on_success à la fin des rules pour voir si le job apparaît. Vérifiez la valeur réelle de la variable avec - echo "$CI_PIPELINE_SOURCE".docker:dind n'est pas démarré ou l'executor n'est pas en mode privileged.docker:XX-dind bien déclaré, (2) variable DOCKER_TLS_CERTDIR: "/certs", (3) runner enregistré avec --docker-privileged. Alternative : utiliser kaniko (sans DinD).~/.ssh/authorized_keys du serveur cible.ssh-add -l dans le job liste la clé, (2) la variable CI contient bien la clé privée complète (avec -----BEGIN/END-----), (3) ssh-keyscan est fait avant le premier ssh, (4) permissions : chmod 700 ~/.ssh.yamllint .gitlab-ci.yml. Les tabulations sont interdites en YAML — utilisez toujours des espaces.# Activer le mode debug (affiche toutes les commandes + variables)
# ⚠️ Expose les secrets masqués dans les logs !
my-job:
variables:
CI_DEBUG_TRACE: "true" # ou: CI_DEBUG_SERVICES pour les services
script:
- set -x # trace bash (moins intrusif)
- env | sort # affiche toutes les variables d'env
- whoami && pwd # vérifier l'utilisateur et le répertoire
# Valider le YAML localement (installer gitlab-ci-local)
npm install -g gitlab-ci-local
gitlab-ci-local --job mon-job # exécute un job en local
# Valider via l'API GitLab (sans push)
curl --header "PRIVATE-TOKEN: $GITLAB_TOKEN" \
--form "content=@.gitlab-ci.yml" \
https://gitlab.com/api/v4/ci/lintGlossaire CI/CD
needs:, un job démarre dès que ses dépendances directes sont terminées, sans attendre tout le stage..). Fusion récursive des clés... Non exécuté directement, utilisé comme template pour extends.script, un stage, et éventuellement des rules, artifacts, cache.merge_request_event. Permet de valider le code avant de merger.registry.gitlab.com). Accessible avec CI_REGISTRY_USER/PASSWORD.only/except. Conditions évaluées dans l'ordre pour décider si un job est inclus dans le pipeline et avec quel comportement.schedule. Utile pour backups, rapports nocturnes.strategy: depend, le parent attend la fin de l'enfant.CI_COMMIT_SHA, CI_COMMIT_BRANCH, CI_REGISTRY, etc. Toujours disponibles.on_success, on_failure, always, manual, delayed, never.GitLab CI/CD Cheatsheet — GitLab 17.x — 18 sections — 2026