Il y a une étape dans la vie de tout projet où l'on se rend compte que le déploiement repose sur une checklist dans la tête de quelqu'un. Lancer les tests, construire l'image, la pousser, se connecter au serveur, tirer la nouvelle version, redémarrer. Six étapes. Le jour où on en saute une un vendredi soir, le bug part en production.
La CI/CD, c'est précisément ce qui supprime cette checklist mentale. Et GitLab CI a un gros avantage : tout est intégré dans la forge, sans outil externe à brancher. Dans cet article, je reprends mon cours vidéo de A à Z, en ajoutant les exemples de configuration complets que je ne pouvais pas tous montrer à l'écran.
CI, CD : de quoi parle-t-on exactement ?
L'intégration continue (CI) vérifie automatiquement chaque modification : le code compile, les tests passent, le linter ne hurle pas. Le but : détecter un problème quelques minutes après l'avoir introduit, pas trois semaines plus tard.
Le déploiement continu (CD) prend le relais : une fois le code validé, il l'emmène jusqu'au serveur, sans manipulation humaine (ou avec un simple clic de validation pour la production).
L'ensemble forme un pipeline : une chaîne d'étapes déclenchée à chaque push. Si une étape échoue, la chaîne s'arrête et le code défectueux ne va pas plus loin.
Un seul fichier : .gitlab-ci.yml
Tout le pipeline se décrit dans un fichier YAML à la racine du dépôt. Il est versionné avec le code, ce qui veut dire que le pipeline évolue en même temps que le projet, se relit en merge request, et peut être restauré comme n'importe quel fichier.
Les trois briques : jobs, stages, runners
Le job
C'est l'unité de base : un nom, et un script, c'est-à-dire les commandes à exécuter.
tests:
image: python:3.12-slim
script:
- pip install -r requirements.txt
- pytest
Les stages
Les jobs sont rangés dans des étapes qui s'exécutent dans l'ordre. Les jobs d'un même stage tournent en parallèle.
stages:
- build
- test
- deploy
Les runners
Ce ne sont pas les serveurs GitLab qui exécutent vos commandes, ce sont des runners : des machines qui récupèrent les jobs en attente et les lancent, le plus souvent dans un conteneur Docker. Sur gitlab.com, des runners partagés sont fournis. En auto-hébergé, vous installez gitlab-runner sur une machine et vous l'enregistrez sur votre projet ou votre groupe.
docker run -d --name gitlab-runner --restart always \
-v /srv/gitlab-runner/config:/etc/gitlab-runner \
-v /var/run/docker.sock:/var/run/docker.sock \
gitlab/gitlab-runner:v17.5.0
docker exec -it gitlab-runner gitlab-runner register
Le mot-clé image choisit le conteneur dans lequel tourne chaque job : node:20, python:3.12, etc.
Voir le pipeline tourner
Vous poussez, le pipeline démarre. Dans l'interface, chaque job passe au vert quand il réussit, au rouge quand il échoue. Un clic sur un job affiche son journal, ligne par ligne, exactement comme si vous aviez tapé les commandes dans un terminal. C'est ma première destination quand quelque chose casse.
Merge requests : le pipeline avant la fusion
Le vrai confort arrive avec les merge requests. Le pipeline tourne sur la branche avant la fusion, et le résultat s'affiche directement dans la MR. Dans les paramètres du projet, l'option « Pipelines must succeed » empêche de fusionner tant que ce n'est pas vert. C'est la règle que j'active en premier sur tout nouveau projet.
Variables et secrets
GitLab fournit des dizaines de variables prédéfinies : $CI_COMMIT_BRANCH, $CI_COMMIT_SHORT_SHA, $CI_REGISTRY_IMAGE, $CI_PIPELINE_SOURCE…
Pour vos propres valeurs, et surtout pour les secrets (mot de passe de registre, clé SSH de déploiement, token d'API), passez par Settings → CI/CD → Variables. Cochez :
- Masked : la valeur est remplacée par
[MASKED]dans les journaux ; - Protected : la variable n'est disponible que sur les branches et tags protégés. Une branche de feature ne peut donc pas l'exfiltrer.
Règle absolue : aucun secret dans le .gitlab-ci.yml. Il est dans Git, donc dans l'historique, pour toujours.
Artifacts et cache : ce qui passe d'un job à l'autre
Chaque job démarre dans un conteneur neuf. Par défaut, il ne partage rien avec les autres.
- Artifacts : les fichiers produits par un job (un
dist/compilé, un rapport de tests) et transmis aux jobs des stages suivants. Ils sont aussi téléchargeables depuis l'interface. - Cache : les dépendances (
node_modules,.pip-cache) conservées d'un pipeline à l'autre pour ne pas tout retélécharger à chaque fois.
build:
stage: build
image: node:20-alpine
cache:
key:
files: [package-lock.json]
paths: [node_modules/]
script:
- npm ci
- npm run build
artifacts:
paths: [dist/]
expire_in: 1 week
La clé de cache basée sur package-lock.json est une astuce que j'utilise partout : le cache est invalidé uniquement quand les dépendances changent vraiment.
Rules : décider quand un job s'exécute
deploy_prod:
stage: deploy
script: ./deploy.sh
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
tests_front:
stage: test
script: npm test
rules:
- changes:
- front/**/*
Le premier job ne tourne que sur la branche principale. Le second uniquement quand quelque chose a changé dans front/. Sur un monorepo, ça fait gagner un temps considérable.
Environnements et déploiement manuel
deploy_staging:
stage: deploy
script: ./deploy.sh staging
environment:
name: staging
url: https://staging.monsite.fr
deploy_production:
stage: deploy
script: ./deploy.sh production
environment:
name: production
url: https://monsite.fr
when: manual
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
GitLab garde l'historique de chaque déploiement par environnement, et permet de redéployer une version précédente en un clic. Le when: manual sur la production ajoute un bouton : le job attend qu'un humain le déclenche. C'est un bon compromis tant que l'équipe n'a pas une confiance totale dans ses tests.
Niveau expert
needs : du pipeline en étages au graphe
Par défaut, un stage attend que tous les jobs du stage précédent soient terminés. Avec needs, un job démarre dès que ses dépendances directes sont finies :
tests_back:
stage: test
needs: [build_back]
script: pytest
Le pipeline devient un graphe orienté (un DAG). Sur un projet avec un front et un back indépendants, les deux chaînes avancent chacune à leur rythme au lieu de s'attendre : le gain de temps est souvent spectaculaire.
extends et include : arrêter le copier-coller
.deploy_template:
image: alpine:3.20
before_script:
- apk add --no-cache openssh-client
script:
- ssh deploy@$HOST "cd /srv/app && docker compose pull && docker compose up -d"
deploy_staging:
extends: .deploy_template
variables: { HOST: staging.monsite.fr }
include:
- project: 'infra/ci-templates'
ref: v2.1.0
file: '/templates/security.yml'
Les jobs qui commencent par un point sont des modèles cachés, jamais exécutés directement. Et include importe une configuration partagée par tous les projets du groupe, versionnée avec un tag.
parallel:matrix et pipelines parent/enfant
tests:
image: python:$VERSION
script: pytest
parallel:
matrix:
- VERSION: ["3.10", "3.11", "3.12"]
Un seul job décrit, trois jobs lancés en parallèle. Et pour les très gros dépôts, un pipeline parent peut déclencher des pipelines enfants (trigger: include:), un par composant, chacun avec son propre fichier.
Un pipeline complet, prêt à adapter
stages: [build, test, deploy]
variables:
IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
build_image:
stage: build
image: docker:27
services: [docker:27-dind]
script:
- echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin $CI_REGISTRY
- docker build -t $IMAGE .
- docker push $IMAGE
unit_tests:
stage: test
image: $IMAGE
needs: [build_image]
script: pytest -q
deploy_production:
stage: deploy
image: alpine:3.20
needs: [unit_tests]
environment: { name: production, url: https://monsite.fr }
when: manual
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
script:
- ./scripts/deploy.sh $IMAGE
Les bonnes pratiques que j'applique
- Un pipeline rapide : sous les dix minutes. Au-delà, les développeurs arrêtent de l'attendre et fusionnent « en espérant ».
- Des images légères et versionnées :
python:3.12-slim, jamaislatest. Un pipeline qui casse tout seul un lundi matin parce que l'image a changé, c'est du temps perdu. - Zéro secret dans le fichier : variables masquées et protégées.
- De la sécurité dans chaque pipeline : GitLab fournit des templates prêts à inclure (SAST, détection de secrets, analyse des dépendances).
À retenir
Un fichier .gitlab-ci.yml. Des jobs, rangés en stages, exécutés par des runners. Des variables protégées pour les secrets, des artifacts pour passer les résultats, un cache pour aller vite. Et une production déployée seulement depuis main, avec un clic de validation.
Si vous organisez vos branches avec Git Flow, chaque merge request vers develop passe par ce pipeline : c'est là que les deux outils prennent tout leur sens ensemble.