Dans ce module de virtualisation, j’ai construit 18 pipelines GitLab CI/CD, du « Hello World » jusqu’aux tests de bout en bout. Chaque exercice est un dépôt public avec son historique et le résultat de son dernier pipeline.
Ce que j’y ai appris
Construire des images
Écrire des Dockerfile légers, ordonner les couches pour le cache et publier dans un registre avec des tags fiables.
Orchestrer un pipeline
Étapes, dépendances entre jobs, artefacts, cache, jobs manuels, modèles réutilisables et matrices de versions.
Tester à tous les niveaux
Lint, tests unitaires, intégration avec une vraie base, bout en bout dans un navigateur, charge et benchmark.
Sécuriser la chaîne
Analyse du code et des dépendances, variables protégées et masquées, identifiants jamais écrits dans les logs.
Appliqué à ce portfolio
Ce site utilise les mêmes principes : à chaque modification, GitHub Actions vérifie les traductions, le HTML, les liens et l’absence de secrets avant la mise en ligne.
La suite du module
Kubernetes arrive dans la prochaine partie du cours. Cette page sera complétée au fil des séances.
Detailed write-ups and revision notes are in French: they are my own study notes.
Les 18 exercices
Pour chaque exercice : ce qu’il fait, son pipeline et ses fichiers. Les pipelines affichent le statut et la durée réels de chaque job lors du dernier passage sur GitLab (données publiques de l’API GitLab) ; « Rejouer » les anime en accéléré. Les sorties de terminal ont été obtenues en relançant le code de l’exercice pour cette page, sur une autre machine que les runners GitLab.
1. Premiers pas et images Docker
ex1Premier pipelineUn job, une image, un scriptPipeline ✓
Le pipeline le plus simple possible : un seul job hello qui démarre un conteneur alpine sur un runner partagé de GitLab et y exécute echo "Hello World". Sans stages:, le job atterrit dans l’étape par défaut test.
ex2Construire et publier une imageDocker-in-Docker et registre GitLabPipeline ✓
Le job démarre le service docker:dind (un démon Docker dans un conteneur), se connecte au registre du projet avec --password-stdin pour ne pas exposer le mot de passe, puis construit et pousse l’image sous deux tags : le SHA court du commit (immuable) et latest (mobile).
ex3Installer Python dans une image Alpineapk add --no-cache et test de l’imagePipeline ✓
On part d’alpine et on installe Python avec apk add --no-cache (pas de cache d’index dans l’image, donc une couche plus légère). Le pipeline lance l’image avec docker run --rm avant de la pousser : si elle ne démarre pas, rien n’est publié.
ex4Vérifier la version de PythonLa commande par défaut CMDPipeline ✓
Même base que ex3, mais la commande par défaut de l’image (CMD) affiche la version installée : le log du job sert de preuve de ce que contient l’image.
ex5Application Python avec dépendancesImage officielle et cache des couchesPipeline ✓
Image python:3.13-alpine, dépendance rich épinglée dans requirements.txt. L’ordre compte : on copie requirements.txt et on installe avant de copier app.py, pour que modifier le code ne relance pas l’installation des dépendances.
ex6Séparer construction et exécutionDeux étapes, dont une manuellePipeline ✓
Le pipeline construit l’image dans build, puis un second job la tire du registre et l’exécute dans run. Ce job est en when: manual : il attend un clic, comme un déploiement qu’on déclenche volontairement.
2. Applications web conteneurisées
ex7Serveur PHP + ApacheImage php:8.3-apachePipeline ✓
Une page phpinfo() servie par Apache dans l’image officielle PHP. EXPOSE 80 documente le port écouté, sans l’ouvrir : c’est docker run -p qui publie un port.
ex8Pipeline complet : lint, build, test, releaseL’image testée avant d’être publiée en latestPipeline ✓
Le premier vrai pipeline de livraison :
- lint :
php -lvérifie la syntaxe (allow_failure: true: un échec ne bloque pas) ; - build : construit l’image taguée par SHA et transmet son nom aux jobs suivants par un artefact
image.txt; - test : lance le conteneur, interroge la page avec
curldans le conteneur et cherche « PHP Version » ; - release : manuel, seulement sur
main, retague l’image testée enlatest.
Le modèle .docker (job caché, préfixé par un point) factorise l’image, le service et la connexion au registre, réutilisés avec extends.
3. Publier un site avec GitLab Pages
ex9Mon portfolio sur GitLab PagesLe job spécial pagesPipeline ✓
Un job nommé pages qui publie le dossier public/ en artefact suffit à GitLab pour mettre le site en ligne. J’y ai déployé une copie de ce portfolio (non reproduite ici). Le site est en accès restreint aux membres du projet.
ex10Un sketch Processing en ligneGitLab Pages + Processing.jsPipeline ✓
Même principe avec un sketch Processing : un arbre fractal dont l’angle suit la souris. Le voir tourner plus bas ↓ · site GitLab Pages ↗
4. Qualité, tests et intégration
ex11Projet Node : lint, tests, buildESLint, Jest, cache npmPipeline ✓
Trois étapes sur node:22-alpine. npm ci installe exactement ce que fixe package-lock.json, et le cache (clé = le lockfile) évite de retélécharger les paquets à chaque job. Le build produit dist/, conservé une semaine en artefact.
node_modules/ et dist/ ont été commités (le .gitignore est vide). Ils sont régénérés par le pipeline : ils devraient être ignorés.
ex12Test d’intégration API + PostgreSQLDeux images, un réseau, une vraie basePipeline ✓
Deux images construites en parallèle : un backend Express qui parle à PostgreSQL et un frontend nginx. Le test d’intégration crée un réseau Docker, démarre postgres:16-alpine, attend qu’il soit prêt avec pg_isready dans une boucle, lance le backend sur le même réseau, puis appelle /api/health et /api/db-health depuis un conteneur curl. Sur le réseau, le nom du conteneur sert de nom d’hôte (http://backend:3000, DB_HOST=db).
ex16Matrices de versionsparallel:matrix : 7 jobs en parallèlePipeline ✓
Les mêmes tests sur Python 3.8, 3.9, 3.10, 3.11 et Node 16, 18, 20. parallel: matrix crée un job par valeur de la variable, et l’image est choisie avec cette variable (python:$PYTHON_VERSION-slim). Chaque job produit son rapport JSON en artefact. Lancer ces tests ici ↓
ex17Tests de bout en bout avec CypressPipeline en échec : cause trouvée et corrigée en localPipeline ✕
Cypress ouvre la page dans un vrai navigateur et vérifie le titre puis le message affiché au clic. Le pipeline a échoué : en relançant le job, l’erreur est Could not find a Cypress configuration file. La config est rangée dans cypress/e2e/ et le test à la racine, alors que Cypress attend l’inverse. Voir la correction et rejouer le scénario ↓
5. Performance et sécurité
ex13Benchmark Python contre Node.jsDeux mesures en parallèle, une comparaisonPipeline ✓
Deux serveurs HTTP minimalistes, mesurés en parallèle : pytest-benchmark côté Python, autocannon côté Node. Le job compare récupère les deux rapports (artefacts des jobs dont il a besoin, via needs) et affiche les latences.
Côté Python, le test chronomètre un lot de 10 requêtes concurrentes (5 répétitions) ; côté Node, autocannon donne la latence moyenne d’une requête sur 5 secondes de charge. Les deux chiffres ne mesurent pas la même chose : pour comparer, il faut le même outil et la même charge sur les deux serveurs.
ex15Test de charge avec k65 utilisateurs virtuels pendant 10 sPipeline ✓
k6 simule 5 utilisateurs qui enchaînent des requêtes pendant 10 secondes et vérifie chaque réponse (check : statut 200). L’image grafana/k6 a un point d’entrée qu’il faut neutraliser (entrypoint: [""]) pour que GitLab puisse lancer son propre shell. La cible est le site de démonstration public de k6.
ex14Analyse de sécurité automatiqueBandit, Safety, npm auditPipeline ✓
Trois analyses en parallèle dans une seule étape : Bandit lit le code Python à la recherche de motifs dangereux (analyse statique, SAST) ; Safety et npm audit comparent les dépendances aux bases de vulnérabilités connues (SCA). Les rapports JSON sont gardés en artefacts.
Chaque commande se termine par || true : le job est toujours vert, même si une faille est trouvée. Pratique pour collecter des rapports ; pour bloquer une livraison, il faudrait laisser le job échouer.
extraVariables protégées et branches protégéesUn secret disponible seulement là où il fautPipeline ✓
La variable DEPLOY_TOKEN est déclarée protégée dans les réglages CI/CD : GitLab ne l’injecte que dans les pipelines des branches ou tags protégés. Le job affiche donc soit « non disponible », soit la longueur du jeton. L’exercice l’affichait aussi en clair pour le constater ; en vrai, on coche aussi Masquer pour que GitLab le remplace par [MASKED] dans les logs, et on ne l’affiche jamais.
Démos dans le navigateur
Le code des exercices, exécuté pour de vrai dans cette page.
Tests unitaires (ex11 et ex16)
Les fichiers de test des dépôts, lancés tels quels avec un mini-lanceur compatible Jest pour le JavaScript, et avec Python compilé en WebAssembly (Pyodide) pour test_calc.py.
Pour comparaison, la sortie de pytest -v et de jest relancés en ligne de commande :
Tests de bout en bout (ex17, Cypress)
L’application testée tourne ci-dessous ; le bouton exécute les étapes de home.cy.js sur cette page.
Pourquoi le pipeline GitLab a échoué
Correction testée sur une copie locale (le dépôt n’a pas été modifié) : déplacer cypress.config.js à la racine, ranger le test dans cypress/e2e/home.cy.js et ajouter supportFile: false, car le projet n’a pas de fichier de support.
const { defineConfig } = require("cypress");
module.exports = defineConfig({
e2e: {
baseUrl: "http://localhost:8080",
supportFile: false,
setupNodeEvents(on, config) {},
},
});
Le sketch de ex10
sketch.pde traduit de Processing en JavaScript, ligne pour ligne : déplacez la souris ou le doigt pour changer l’angle des branches.
Detailed write-ups and revision notes are in French: they are my own study notes.
Fiche de révision
Tout ce qu’il faut savoir, illustré par les exercices. Ouvre une section, puis vérifie-toi avec le quiz et les cartes en bas de page.
1. Conteneur, image, machine virtuelle
| Machine virtuelle | Conteneur | |
|---|---|---|
| Isole | Un système complet, avec son noyau, sur un hyperviseur | Des processus, qui partagent le noyau de l’hôte |
| Démarrage | Dizaines de secondes | Moins d’une seconde |
| Taille | Plusieurs Go | Quelques Mo (alpine ≈ 8 Mo) |
| Usage | Faire tourner un autre OS, isolation forte | Livrer une appli avec ses dépendances, à l’identique partout |
- Image : modèle en lecture seule, empilement de couches (une par instruction du Dockerfile qui modifie le système de fichiers).
- Conteneur : instance en cours d’exécution d’une image, avec une couche inscriptible par-dessus. On peut lancer plusieurs conteneurs depuis la même image.
- Registre : serveur qui stocke les images (Docker Hub, registre GitLab). On y fait
pushetpull.
Image = recette figée ; conteneur = plat servi. Un conteneur est jetable : ce qui doit survivre va dans un volume ou une base.
2. Écrire un Dockerfile
| Instruction | Rôle | Vu dans |
|---|---|---|
FROM | Image de base (toujours en premier) | tous |
RUN | Exécute une commande à la construction et crée une couche | ex3, ex5 |
WORKDIR | Dossier courant pour la suite (créé s’il n’existe pas) | ex5, ex12 |
COPY | Copie des fichiers du contexte de build dans l’image | ex5, ex7 |
CMD | Commande par défaut au démarrage, remplaçable par docker run image autre-commande | ex2 à ex5 |
ENTRYPOINT | Programme toujours lancé ; CMD devient ses arguments | image k6 (ex15) |
EXPOSE | Documente un port ; ne le publie pas (-p le fait) | ex7, ex12 |
ENV / ARG | Variable d’environnement (exécution) / variable de build | — |
FROM python:3.13-alpine WORKDIR /app COPY requirements.txt . # change rarement RUN pip install --no-cache-dir -r requirements.txt COPY app.py . # change souvent : en dernier CMD ["python", "app.py"]
Docker réutilise une couche tant que l’instruction et ses fichiers n’ont pas changé. Dès qu’une couche change, toutes les suivantes sont reconstruites. On met donc en haut ce qui bouge peu (dépendances) et en bas ce qui bouge souvent (code).
- Images légères :
alpine, variantes-slim;apk add --no-cacheetpip install --no-cache-dirévitent de garder les caches dans l’image. - Forme « exec »
CMD ["python", "app.py"](tableau) plutôt que la forme shell : le programme reçoit directement les signaux d’arrêt.
3. Les commandes Docker à connaître
docker build -t monimage:1.0 . # construire depuis le Dockerfile du dossier courant docker run --rm monimage:1.0 # lancer, puis supprimer le conteneur à l'arrêt docker run -d --name web -p 8080:80 monimage # en arrière-plan, nommé, port publié docker exec web curl -sf http://localhost/ # commande dans un conteneur qui tourne (ex8) docker logs backend # journaux du conteneur (ex12) docker ps / docker ps -a # conteneurs actifs / tous docker pull image:tag && docker push image:tag docker tag image:sha image:latest # un même contenu, un autre nom (ex8, release) docker network create test-net # réseau où les conteneurs se trouvent par leur nom (ex12) echo "$PASS" | docker login -u "$USER" --password-stdin "$REGISTRY"
-d | détaché (arrière-plan) | --rm | supprimé à l’arrêt |
-p h:c | port hôte → conteneur | -e K=V | variable d’environnement |
-v | volume (données persistantes) | --network | rejoindre un réseau |
4. Registre et stratégie de tags
$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA: tag immuable, relié à un commit précis. On sait exactement ce qui tourne et on peut revenir en arrière.latest: tag mobile, qui pointe vers la dernière version publiée. Pratique, mais ambigu : à ne mettre à jour qu’après les tests (ex8, jobreleasemanuel).- Connexion au registre avec
--password-stdin: le mot de passe passe par l’entrée standard, il n’apparaît ni dans la commande, ni dans l’historique.
build (tag SHA) → test de cette image → release qui retague la même image en latest. On publie ce qu’on a testé, sans reconstruire.
5. Anatomie d’un pipeline GitLab CI
stages: [lint, build, test, release] # ordre des étapes
.docker: # job caché (point) = modèle réutilisable
image: docker:latest
services: [docker:dind] # démon Docker à côté du job
variables:
DOCKER_TLS_CERTDIR: "/certs"
build-image:
extends: .docker # hérite du modèle
stage: build
script:
- docker build -t "$IMAGE:$CI_COMMIT_SHORT_SHA" .
- echo "$IMAGE:$CI_COMMIT_SHORT_SHA" > image.txt
artifacts: # fichiers transmis aux jobs suivants
paths: [image.txt]
expire_in: 1 week
test-image:
extends: .docker
stage: test
needs: [build-image] # démarre dès que build-image a fini
release-image:
stage: release
when: manual # attend un clic
only: [main] # seulement sur main (rules: est la syntaxe moderne)
| Mot-clé | À quoi il sert |
|---|---|
stages | Ordre des étapes ; les jobs d’une même étape tournent en parallèle |
image / services | Conteneur du job / conteneurs annexes (base, docker:dind) |
before_script / script | Commandes de préparation / commandes du job (le job échoue au premier code de sortie non nul) |
artifacts | Fichiers produits par un job, téléchargeables et transmis aux jobs suivants |
cache | Fichiers réutilisés d’un pipeline à l’autre pour aller plus vite (ex : node_modules, clé = lockfile) |
needs | Dépendances explicites : le job démarre dès que ceux-ci sont finis, sans attendre toute l’étape |
dependencies | De quels jobs récupérer les artefacts |
extends | Hériter d’un job (souvent caché, préfixé par .) |
when: manual | Job déclenché à la main |
allow_failure | Un échec n’arrête pas le pipeline (affiché en orange) |
only / rules | Conditions d’exécution (branche, tag, variables…) |
parallel: matrix | Un job par combinaison de variables (ex16 : 7 jobs) |
pages | Job spécial : son artefact public/ devient un site GitLab Pages |
Artefact = un résultat qu’on veut garder ou passer au job suivant (rapport, binaire, image.txt). Cache = une accélération, qui peut disparaître sans casser le pipeline (dépendances téléchargées).
6. Variables prédéfinies utiles
CI_REGISTRY | Adresse du registre GitLab |
CI_REGISTRY_IMAGE | Chemin de l’image du projet dans le registre |
CI_REGISTRY_USER / CI_REGISTRY_PASSWORD | Identifiants temporaires du job pour le registre |
CI_COMMIT_SHORT_SHA | 8 premiers caractères du commit |
CI_COMMIT_BRANCH | Branche du pipeline |
CI_COMMIT_REF_PROTECTED | true si la branche ou le tag est protégé (exextra) |
7. Les types de tests automatisés
| Type | Question posée | Outil (exercice) |
|---|---|---|
| Lint | Le code respecte-t-il la syntaxe et les règles ? | ESLint (ex11), php -l (ex8) |
| Unitaire | Une fonction isolée fait-elle ce qu’elle doit ? | Jest (ex11, ex16), pytest (ex16) |
| Intégration | Les briques fonctionnent-elles ensemble ? | API + PostgreSQL sur un réseau Docker (ex12) |
| Smoke test | Le service démarre-t-il et répond-il ? | curl -sf /api/health (ex12), grep "PHP Version" (ex8) |
| Bout en bout | Le parcours utilisateur marche-t-il dans un vrai navigateur ? | Cypress (ex17) |
| Charge | Le service tient-il sous plusieurs utilisateurs ? | k6 (ex15) |
| Benchmark | Quelle solution est la plus rapide ? | pytest-benchmark, autocannon (ex13) |
| Sécurité | Le code ou ses dépendances ont-ils des failles connues ? | Bandit, Safety, npm audit (ex14) |
Beaucoup de tests unitaires (rapides, précis), moins de tests d’intégration, peu de tests de bout en bout (lents, fragiles, mais au plus près de l’utilisateur).
8. Sécurité de la chaîne CI/CD
- SAST (analyse statique du code) : Bandit cherche des motifs risqués (mot de passe en dur,
eval,subprocessavec shell…). - SCA (analyse des dépendances) : Safety et npm audit comparent les versions utilisées aux vulnérabilités publiées (CVE).
- Variable protégée : injectée seulement dans les pipelines des branches et tags protégés. Variable masquée : remplacée par
[MASKED]dans les logs. Pour un jeton de déploiement : les deux. - Ne jamais écrire un secret dans le dépôt ni l’afficher dans un log ;
docker login --password-stdinplutôt que-p. - Épingler les versions (
rich==13.9.4,postgres:16-alpine) rend les builds reproductibles ;latestpartout rend le résultat imprévisible.
9. Pièges rencontrés dans mes exercices
- Cypress (ex17) : fichier de config au mauvais endroit, donc pipeline rouge. La structure attendue :
cypress.config.jsà la racine, tests danscypress/e2e/*.cy.js. - Benchmark (ex13) : deux outils qui ne mesurent pas la même chose ne se comparent pas directement.
|| true(ex14) : le job ne peut plus échouer ; à réserver à la collecte de rapports.- Dépendances commitées (ex11) :
node_modules/etdist/doivent être dans.gitignore. - Attendre un service (ex12) : un conteneur « démarré » n’est pas forcément « prêt » ; on boucle sur
pg_isreadyavant de lancer le backend. - Point d’entrée d’une image (ex15) :
entrypoint: [""]pour que GitLab exécute ses propres commandes dans l’image k6.