TP10 : CI/CD, Observabilité et Pipeline de production
Objectif du TP
Construire un workflow complet pour une application Node.js :
- CI/CD : Automatiser les tests et le build via Docker.
- Multi-environnement : Gérer des configurations distinctes pour le développement et la production.
- Observabilité : Rendre l'application capable d'exposer son état interne (logs, métriques applicatives, métriques VPS et métriques conteneurs).
- Pipeline de production : Sécuriser et automatiser la publication d'images avec SBOM et GitHub Actions.
Rendu attendu
- Le fichier
docker-compose.yamlgérant les multi-environnements. - Le fichier
Dockerfile(ou plusieurs si nécessaire pour le CI). - Le fichier
docker-compose.prod.yamlenrichi avecprometheus,grafana,node-exporteretcadvisor. - Le fichier
prometheus.ymlscrappant l'application, le VPS et les conteneurs. - Le fichier
.github/workflows/docker.ymlcomplété avec scan Trivy et génération SBOM. - Vos notes de carnet avec les réponses aux questions.
1. Pour débuter : L'application enrichie
Nous reprenons notre application Node.js du TP9, mais nous lui ajoutons quelques fonctionnalités pour suivre son usage.
app.js
const express = require('express');
const client = require('prom-client');
const app = express();
const env = process.env.APP_ENV || "dev";
let counter = 0;
let errors = 0;
// Registre Prometheus
const register = new client.Registry();
client.collectDefaultMetrics({ register });
const requestCounter = new client.Counter({
name: 'http_requests_total',
help: 'Total number of HTTP requests',
labelNames: ['method', 'route', 'status_code'],
});
const errorCounter = new client.Counter({
name: 'http_errors_total',
help: 'Total number of HTTP errors',
});
register.registerMetric(requestCounter);
register.registerMetric(errorCounter);
// Middleware de logging simple
app.use((req, res, next) => {
res.on('finish', () => {
if (req.path !== '/metrics') {
requestCounter.inc({
method: req.method,
route: req.path,
status_code: String(res.statusCode),
});
}
});
console.log(`[${new Date().toISOString()}] ${req.method} ${req.url} - Env: ${env}`);
next();
});
app.get('/', (req, res) => {
counter++;
res.json({
message: "Bienvenue sur l'API",
env: env,
counter: counter
});
});
app.get('/error', (req, res) => {
errors++;
errorCounter.inc();
res.status(500).json({ error: "Une erreur simulée s'est produite" });
});
// Endpoint Prometheus
app.get('/metrics', (req, res) => {
res.set('Content-Type', register.contentType);
register.metrics().then(metrics => res.end(metrics));
});
module.exports = app;
server.js
const app = require('./app');
const port = process.env.PORT || 3000;
const env = process.env.APP_ENV || 'dev';
app.listen(port, () => {
console.log(`Serveur demarre sur le port ${port} en mode ${env}`);
});
package.json (scripts)
Cette separation entre app.js (application Express) et server.js (demarrage) rend les tests HTTP plus simples et evite de lancer un serveur reseau complet dans chaque test.
Ajoutez aussi la dependance prom-client pour exposer des metriques compatibles Prometheus.
2. CI/CD avec Docker
L'idée de la CI (Intégration Continue) est de valider chaque changement avant qu'il ne soit déployé. Si un test échoue, le pipeline doit s'arrêter immédiatement.
- Ajouter des tests : Ajoutez un test simple avec
jestouvitestpour vérifier que la route/répond avec un code HTTP 200. Vous pouvez utiliser une IA pour vous aider, mais vous devez être capables d'expliquer le test obtenu. - Linting : Configurez un linter genre
eslintpour vérifier la qualité du code. - Service de test dans Docker :
Créez un service
testdans votredocker-compose.yaml. Ce service ne doit pas lancer l'app, mais uniquement les tests.
Le mécanisme d'échec : Docker (et les outils de CI) se basent sur l'exit code du processus.
exit 0: Tout va bien, le pipeline continue.exit 1(ou autre) : Erreur détectée, le pipeline s'arrête.
Exercice :
Question : Pourquoi est-il préférable de lancer les tests dans Docker plutôt que sur votre machine directement lors d'un pipeline CI ?
3. Gestion Multi-environnement (App vs Infra)
Plutôt que d'avoir des fichiers indépendants, Docker permet de "surcharger" (override) une configuration. On sépare donc :
- L'Application : Décrite dans le fichier de base.
- L'Infrastructure : Décrite dans le fichier de production (overrides).
Fichier de base : docker-compose.yaml
Ce fichier contient la structure commune à tous vos environnements.
Fichier de production : docker-compose.prod.yaml
Ce fichier vient modifier ou ajouter des éléments à la base. Complétez les trous ci-dessous pour mapper l'application sur le port 80 et ajouter une politique de redémarrage automatique.
services:
app:
# À COMPLÉTER : Mapper le port 80 de l'hôte sur le 3000 du conteneur
ports:
- "___:___"
# À COMPLÉTER : Ajouter la politique pour redémarrer automatiquement en cas de crash
restart: _________
environment:
- APP_ENV=production
- PORT=3000
Lancement en fusionnant les fichiers
Pour tester votre configuration de production :
Pour verifier le resultat de la fusion (et detecter les mauvaises surprises), utilisez :
Quel est l'avantage de cette méthode "d'override" plutôt que d'avoir deux fichiers Compose totalement indépendants si vous décidez de rajouter un nouveau réseau ou un nouveau volume demain ?
4. Observabilité, Prometheus & Grafana
L'observabilité consiste à pouvoir comprendre l'état interne de votre système (logs et métriques) uniquement à partir des données qu'il expose.
Les Logs
Les logs sont des événements textuels (ex: "Échec de connexion de l'utilisateur X"). - Pourquoi les structurer ? Si vos logs sont en JSON, une machine pourra les lire et créer des alertes automatiquement ("Alerte : 50 erreurs en 2 min !").
Les Métriques : Le tableau de bord
À l'inverse des logs, les métriques sont des chiffres montrant l'état actuel (ex: "42 utilisateurs connectés").
- Simulation d'usage : Lancez un petit script ou utilisez
curlen boucle pour appeler la racine/et faire monter vos compteurs. Provoquez manuellement des erreurs via/error. - Vérification** : Consultez
/metricspour voir l'évolution.
Aide :
for i in {1..30}; do curl -s http://localhost/ > /dev/null; done
for i in {1..5}; do curl -s http://localhost/error > /dev/null; done
Intégration Prometheus
Nous allons maintenant ajouter Prometheus pour qu'il vienne récupérer vos métriques automatiquement. Pour garder l'environnement de base léger, Prometheus sera ajouté uniquement dans docker-compose.prod.yaml
Ajoutez le service prometheus uniquement dans votre fichier docker-compose.prod.yaml.
NB: Cette configuration node-exporter est prévue pour un hôte Linux. Sur Windows Server, utilisez windows_exporter à la place.
# Dans docker-compose.prod.yaml
services:
prometheus:
image: prom/prometheus
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
ports:
- "9090:9090"
depends_on:
- app
- node-exporter
- cadvisor
grafana:
image: grafana/grafana:11.0.0
ports:
- "3001:3000"
volumes:
- grafana-data:/var/lib/grafana
restart: unless-stopped
depends_on:
- prometheus
node-exporter:
image: prom/node-exporter:v1.8.1
command:
- '--path.rootfs=/host'
pid: host
restart: unless-stopped
volumes:
- '/:/host:ro,rslave'
cadvisor:
image: gcr.io/cadvisor/cadvisor:v0.49.1
restart: unless-stopped
volumes:
- /:/rootfs:ro
- /var/run:/var/run:ro
- /sys:/sys:ro
- /var/lib/docker/:/var/lib/docker:ro
ports:
- "8080:8080"
app:
# ... vos configurations précédentes
volumes:
grafana-data:
N'oubliez pas de créer le fichier
prometheus.ymlpour dire à Prometheus de venir "scraper" l'application Express sur le port 3000.
Exemple de prometheus.yml :
global:
scrape_interval: 5s
scrape_configs:
- job_name: "node-app"
static_configs:
- targets: ["app:3000"]
- job_name: "node-exporter"
static_configs:
- targets: ["node-exporter:9100"]
- job_name: "cadvisor"
static_configs:
- targets: ["cadvisor:8080"]
Dans Docker Compose, les cibles doivent etre les noms de services (app:3000, node-exporter:9100, cadvisor:8080), pas localhost.
Pourquoi Node Exporter et cAdvisor ?
- Node Exporter : métriques système du VPS (CPU global, RAM totale/disponible, disque, load).
- cAdvisor : métriques des conteneurs (CPU, mémoire, I/O), donc la consommation par service Docker.
En pratique : - Pour répondre "combien consomme mon VPS ?" -> Node Exporter. - Pour répondre "quel service consomme le plus ?" -> cAdvisor.
Panels Grafana utiles (PromQL)
Dans Grafana, connectez Prometheus comme datasource puis créez ces panels :
URL de la datasource Prometheus dans Grafana : http://prometheus:9090
| Besoin | Requête PromQL |
|---|---|
| RAM VPS en % | 100 * (1 - (node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes)) |
| RAM VPS totale (Go) | node_memory_MemTotal_bytes / 1024 / 1024 / 1024 |
| RAM VPS utilisée (Go) | (node_memory_MemTotal_bytes - node_memory_MemAvailable_bytes) / 1024 / 1024 / 1024 |
| CPU VPS en % | 100 * (1 - avg by (instance) (rate(node_cpu_seconds_total{mode="idle"}[5m]))) |
| CPU par service (%) | sum(rate(container_cpu_usage_seconds_total{container_label_com_docker_compose_service!=""}[5m])) by (container_label_com_docker_compose_service) * 100 |
| RAM par service (MiB) | sum(container_memory_working_set_bytes{container_label_com_docker_compose_service!=""}) by (container_label_com_docker_compose_service) / 1024 / 1024 |
Si vous n'utilisez pas Docker Compose, remplacez le label
container_label_com_docker_compose_servicepar un label disponible dans vos métriques cAdvisor (ex:nameoucontainer).Pourquoi est-il plus propre d'ajouter Prometheus dans le fichier
prod.yamlplutôt que dans le fichier de basedocker-compose.yaml? Posez-vous la question du besoin de monitoring en phase de développement pur.Question : Quelle alerte Grafana mettriez-vous en place en premier sur un VPS de production ? Justifiez.
5. SBOM et pipeline CI/CD complet
La CI locale (section 2) valide que le code fonctionne. En production, le pipeline GitHub Actions y ajoute deux étapes critiques : le scan de sécurité et la génération d'une SBOM.
5.1. Qu'est-ce qu'une SBOM ?
Une SBOM (Software Bill of Materials) est l'inventaire exhaustif de tous les composants d'un logiciel : dépendances, bibliothèques, versions exactes, licences. En cas de vulnérabilité découverte (CVE), vous savez immédiatement quelles images sont affectées sans avoir à les rescanner.
5.2. Générer une SBOM
# Générer une SBOM au format CycloneDX (standard industriel)
trivy image --format cyclonedx --output sbom.json my-app:v1
5.3. Pipeline GitHub Actions
Créez un petit repository GitHub avec votre application et ajoutez un workflow dans .github/workflows/docker.yml qui :
Vous allez créer un compte Docker Hub gratuit pour pouvoir pousser vos images.
# .github/workflows/docker.yml
name: Docker CI
on:
push:
pull_request:
jobs:
build-test-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build image
run: docker build -t my-app:${{ github.sha }} .
- name: Run tests
run: docker compose run --rm test
- name: Scan image (Trivy)
uses: aquasecurity/trivy-action@master
# A compléter : configurer Trivy pour qu'il échoue si une CVE critique est détectée
- name: Generate SBOM
# A compléter : générer une SBOM au format CycloneDX et la sauvegarder comme artefact de build
- name: Push to registry
# A compléter : tagger l'image avec le SHA Git et la pousser vers un registry (ex: Docker Hub)
Ce pipeline nous garantit donc :
- Les tests passent avant toute publication.
- Aucune image avec une CVE critique n'est publiée.
- Chaque image est taguée avec son SHA Git.
- Une SBOM est générée à chaque build.
Conclusion
À la fin de ce TP, vous devez avoir un environnement capable de :
- Rejeter un build si les tests ne passent pas.
- Basculer facilement entre une configuration de test locale et une mise en ligne.
- Surveiller la santé de votre application, du VPS et des services Docker en temps réel avec Prometheus + Grafana.
- Générer une SBOM et publier une image dans un registry via un pipeline GitHub Actions.