Skip to content

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.yaml gérant les multi-environnements.
  • Le fichier Dockerfile (ou plusieurs si nécessaire pour le CI).
  • Le fichier docker-compose.prod.yaml enrichi avec prometheus, grafana, node-exporter et cadvisor.
  • Le fichier prometheus.yml scrappant l'application, le VPS et les conteneurs.
  • Le fichier .github/workflows/docker.yml complé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)

{
  "scripts": {
    "start": "node server.js",
    "test": "jest",
    "lint": "eslint ."
  }
}

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.

  1. Ajouter des tests : Ajoutez un test simple avec jest ou vitest pour 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.
  2. Linting : Configurez un linter genre eslint pour vérifier la qualité du code.
  3. Service de test dans Docker : Créez un service test dans votre docker-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 :

# Dans votre docker-compose.yaml
services:
  test:
    build: .
    command: npm test  # <-- Si cette commande échoue, l'exit code sera... ?

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.

services:
  app:
    build: .
    environment:
      - APP_ENV=dev
      - PORT=3000
    ports:
      - "3000:3000"

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 :

docker compose -f docker-compose.yaml -f docker-compose.prod.yaml up -d

Pour verifier le resultat de la fusion (et detecter les mauvaises surprises), utilisez :

docker compose -f docker-compose.yaml -f docker-compose.prod.yaml config

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").

  1. Simulation d'usage : Lancez un petit script ou utilisez curl en boucle pour appeler la racine / et faire monter vos compteurs. Provoquez manuellement des erreurs via /error.
  2. Vérification** : Consultez /metrics pour 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.yml pour 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_service par un label disponible dans vos métriques cAdvisor (ex: name ou container).

Pourquoi est-il plus propre d'ajouter Prometheus dans le fichier prod.yaml plutôt que dans le fichier de base docker-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 :

  1. Les tests passent avant toute publication.
  2. Aucune image avec une CVE critique n'est publiée.
  3. Chaque image est taguée avec son SHA Git.
  4. 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.