C3 — Intégration et livraison continuesDans le dialogue d’impression, choisissez « Enregistrer au format PDF » comme destination.
Retour

Licence 3 · DevOps

Cours 3Intégration et livraison continues

Le cœur du cours : automatiser la vérification à chaque modification, puis la mise en production — et savoir revenir en arrière.

2 chapitres · 14 h de travail estimé

  1. 1. Intégration continue7 h
  2. 2. Livraison et déploiement continus7 h

Chapitre 1 · 7 h

Intégration continue

Principe et discipline associée ; pipeline de compilation, tests et analyse statique ; déclencheurs, jobs, artefacts et cache ; échec rapide et boucle de rétroaction courte.

Deux développeurs travaillent trois semaines chacun de leur côté. Le jour de l'intégration arrive, on fusionne, et rien ne compile. Les deux ont modifié la même interface, chacun de façon cohérente avec son propre travail, et il faut maintenant décider laquelle survit — puis corriger tout ce qui en dépendait des deux côtés.

Cette journée porte un nom dans les équipes qui la vivent : merge day. Elle prend un jour, ou trois. Et sa cause n'est pas technique : les deux modifications étaient compatibles il y a trois semaines, et elles ont divergé sans que personne ne s'en aperçoive.

L'intégration continue est la réponse, et elle n'est pas un outil : c'est la discipline d'intégrer son travail dans la branche principale au moins une fois par jour, en s'assurant à chaque fois, automatiquement, que l'ensemble tient encore debout.

Le principe, et la discipline qui va avec

L'outil — GitHub Actions, GitLab CI, Jenkins — ne fait qu'exécuter des commandes après un push. Ce qui produit le bénéfice, ce sont quatre règles de comportement.

Intégrer au moins une fois par jour. C'est le trunk-based du chapitre 2. Une branche de trois semaines rend le pipeline inutile : il vérifiera fidèlement une divergence déjà constituée.

Chaque intégration déclenche une vérification automatique. Compilation, tests, analyse statique. Automatique, parce qu'une vérification manuelle est une vérification qu'on saute un vendredi soir.

Une construction cassée s'arrête et se répare tout de suite. C'est la règle qui coûte le plus à installer, et celle qui fait toute la différence. Tant que la branche principale est rouge, personne ne peut savoir si son propre travail est correct — la vérification ne dit plus rien. Réparer passe donc avant d'ajouter.

Personne ne construit par-dessus du rouge. Corollaire du précédent, et il est social autant que technique.

Sans ces règles, on obtient ce qu'on voit dans beaucoup de projets : un pipeline qui existe, qui échoue depuis trois semaines, et que tout le monde a appris à ignorer. Il coûte du temps de calcul et ne rend plus aucun service.

Un pipeline

name: intégrationon:  push:    branches: [main]  pull_request: jobs:  verifier:    runs-on: ubuntu-latest    steps:      - uses: actions/checkout@v4      - uses: actions/setup-node@v4        with:          node-version: '20'          cache: 'npm'                 # dépendances mises en cache      - run: npm ci      - run: npm run lint              # rapide : en premier      - run: npm run test:unit      - run: npm run test:integration      - uses: actions/upload-artifact@v4        with:          name: rapport-couverture          path: coverage/

Cinq notions se lisent dans ce fichier, et elles sont communes à tous les outils.

Le déclencheur dit quand le pipeline part : à chaque push sur la branche principale, à chaque demande de fusion, sur une étiquette de version — c'est le chapitre 6 — ou selon un horaire, pour les vérifications lentes qu'on ne veut pas à chaque commit.

Un job est une unité qui s'exécute sur une machine propre. Deux jobs indépendants tournent en parallèle ; c'est le levier principal pour raccourcir le pipeline.

Une étape est une commande dans un job, exécutée dans l'ordre. La première qui échoue arrête le job.

Un artefact est un fichier produit par un job et conservé : rapport de couverture, image construite, binaire. C'est aussi le moyen de passer un résultat d'un job à un autre.

Le cache conserve entre deux exécutions ce qui est coûteux à reconstituer — dépendances, couches Docker du chapitre 3. Sans lui, chaque exécution repart d'une machine vierge.

L'échec rapide, et la durée du cycle

Le pipeline doit répondre vite, pour une raison qui n'a rien de technique : au-delà d'une dizaine de minutes, on ne l'attend plus. On passe à autre chose, on perd le contexte, et le retour arrive quand on pense à autre chose — la boucle de rétroaction du chapitre 1 est cassée.

Deux leviers, et le premier est gratuit.

Ordonner du plus rapide au plus lent. Le formatage et l'analyse statique prennent dix secondes et attrapent les fautes les plus fréquentes ; les tests unitaires prennent une minute ; les tests d'intégration cinq ; les tests de bout en bout quinze. Dans cet ordre, une faute de lint est signalée en dix secondes au lieu de vingt minutes.

Paralléliser ce qui est indépendant. Les tests unitaires et l'analyse statique n'ont aucune raison d'attendre l'un l'autre. Trois jobs parallèles de cinq minutes valent mieux qu'un job séquentiel de quinze.

séquentiel   [lint 10s][unitaires 60s][intégration 300s][e2e 900s]   = 21 minparallèle    [lint 10s]             [unitaires 60s]              puis  [intégration 300s]             [analyse sécurité 120s]            [e2e 900s]           = 22 min… ?

Le second schéma illustre une erreur courante : paralléliser ce qui est déjà rapide ne gagne rien. Le pipeline dure aussi longtemps que son chemin le plus long — c'est le chemin critique du chapitre 4 d'architecture. Pour descendre sous dix minutes, il faut s'attaquer aux tests de bout en bout : les découper, les paralléliser entre eux, ou les sortir du pipeline de chaque commit pour les exécuter à la nuit.

Qualité de code et couverture

Trois outils s'ajoutent au pipeline, et le troisième demande une mise en garde.

Le formateur — Prettier, gofmt, black — met le code en forme automatiquement. Le mettre dans le pipeline supprime définitivement les discussions d'indentation en revue, ce qui n'est pas un gain esthétique mais un gain d'attention : le relecteur regarde la logique.

L'analyse statique — ESLint, SonarQube — cherche des motifs suspects sans exécuter le code : variable inutilisée, comparaison toujours vraie, ressource non fermée. C'est le -Wall du cours de programmation, appliqué au projet.

La couverture de tests mesure la proportion de lignes exécutées par la suite de tests. Elle est utile comme signal — une couverture de 5 % dit quelque chose — et dangereuse comme objectif. Dès qu'on impose un seuil, on l'atteint : il suffit d'écrire des tests qui appellent le code sans rien vérifier. On obtient 90 % de couverture et aucune garantie, ce qui est pire que 40 % assumés, parce que le chiffre inspire une confiance injustifiée.

C'est un cas particulier d'une règle générale, qu'il vaut mieux connaître : quand une mesure devient un objectif, elle cesse d'être une bonne mesure. On surveille donc la tendance de la couverture plutôt que sa valeur, et l'on regarde surtout ce qui n'est pas couvert du tout.

Quiz · 1 question

Un pipeline exécute, dans cet ordre : tests de bout en bout (15 min), tests unitaires (1 min), analyse statique (10 s). Que faut-il changer, et pourquoi ?

  • Rien : l'ordre n'a pas d'importance puisque toutes les étapes s'exécutent de toute façonsans importance
  • Inverser l'ordre : du plus RAPIDE au plus lent. Une faute de lint serait alors signalée en 10 secondes au lieu de 16 minutes — et la première étape qui échoue arrête le jobdu plus rapide au plus lent
  • Supprimer l'analyse statique, redondante avec les tests unitairessupprimer une étape

Réponse : La première étape qui échoue arrête le job : l'ordre décide donc du délai avant le retour d'information. Avec l'ordre donné, une variable mal nommée ou une accolade oubliée n'est signalée qu'après seize minutes de tests de bout en bout — et pendant ce temps l'auteur a changé de tâche, perdu le contexte, et devra y revenir. En inversant, le même défaut remonte en dix secondes, tant que le code est encore sous les yeux. C'est le principe de l'ÉCHEC RAPIDE, et il ne coûte rien à mettre en place : trois lignes déplacées. L'analyse statique n'est par ailleurs pas redondante avec les tests — elle cherche des motifs suspects SANS exécuter, et attrape des choses qu'aucun test ne couvre, comme une ressource non fermée sur un chemin d'erreur rare.

Ce qui tue une intégration continue

Trois maux, et le premier est de loin le pire.

Les tests instables (flaky), qui échouent une fois sur vingt sans raison. Leur effet est catastrophique et le calcul est instructif : si chaque test échoue au hasard avec une probabilité pp, la probabilité qu'au moins un des nn tests échoue vaut 1(1p)n1 - (1-p)^n. Avec p=0,5%p = 0{,}5\,\% et 200 tests, cela fait 63 % d'échecs sans aucune cause réelle.

La conséquence est prévisible : l'équipe relance le pipeline « pour voir », et ce faisant apprend à ignorer les échecs. Un vrai échec passe alors inaperçu. Un test instable doit être réparé ou supprimé le jour même — le garder est pire que ne pas l'avoir.

Les pipelines lents. Au-delà de dix minutes, on n'attend plus ; au-delà de trente, on regroupe ses commits pour ne pas payer l'attente, et l'on retombe sur les gros lots du chapitre 1.

Les tests dépendants entre eux. Un test qui suppose qu'un autre a rempli la base ne passe que dans un ordre donné, et devient instable dès qu'on parallélise. Chaque test doit créer et détruire ses propres données.

Quiz · 1 question

Une équipe impose un seuil de 90 % de couverture de tests. Quel est le risque ?

  • Aucun : 90 % est un objectif exigeant qui garantit une bonne qualitéaucun risque
  • Le seuil sera atteint, mais pas forcément par de bons tests : il suffit d'appeler le code sans rien vérifier. On obtient 90 % et aucune garantie — pire que 40 % assumés, car le chiffre inspire une confiance injustifiéela mesure devenue objectif
  • Le pipeline deviendra trop lent pour être exécuté à chaque commitlenteur

Réponse : La couverture mesure les lignes EXÉCUTÉES par les tests, pas les comportements VÉRIFIÉS. Un test qui appelle toutes les méthodes sans écrire une seule assertion donne 100 % de couverture et ne détecte rien. Dès qu'un seuil devient contraignant, c'est cette solution que la pression fait émerger — pas par malhonnêteté, mais parce que c'est le chemin le plus court vers le chiffre demandé. Le résultat est plus dangereux qu'une couverture faible assumée, puisque le tableau de bord affiche du vert. C'est un cas particulier d'une règle générale : quand une mesure devient un objectif, elle cesse d'être une bonne mesure. On surveille donc la TENDANCE plutôt que la valeur, et l'on regarde surtout ce qui n'est couvert PAR AUCUN test — un module entier à 0 % est une information, la différence entre 78 % et 82 % n'en est pas une.

À vous

L'exercice construit un exécuteur de pipeline et met en chiffres les trois affirmations du chapitre.

D'abord l'ordonnancement : vous donnez des étapes avec leur durée et leur probabilité d'échec, et le programme calcule le délai moyen avant retour selon l'ordre choisi. L'échec rapide cesse d'être un conseil pour devenir un écart mesuré.

Ensuite le cache et le parallélisme : mêmes étapes, avec et sans cache de dépendances, en séquentiel puis réparties en jobs. Vous chercherez le chemin critique et constaterez que paralléliser ce qui est déjà rapide ne gagne rien.

Enfin les tests instables. Vous ferez varier la probabilité d'instabilité et le nombre de tests, et vous retrouverez les 63 % du cours — puis vous simulerez ce que fait une équipe qui relance « pour voir », et combien de vrais échecs elle laisse passer.

Exercice de code

Mesurez l'échec rapide, trouvez le chemin critique, puis chiffrez le coût des tests instables.

Point de départ

// ── 1. Ordre des étapes et délai avant retour ─────────────────────────────
// duree en secondes, proba = probabilité que CETTE étape révèle un défaut.
const ETAPES = [
  { nom: "lint",            duree: 10,  proba: 0.25 },
  { nom: "tests unitaires", duree: 60,  proba: 0.35 },
  { nom: "intégration",     duree: 300, proba: 0.20 },
  { nom: "bout en bout",    duree: 900, proba: 0.10 },
];

// Le job s'arrête à la PREMIÈRE étape qui échoue : le délai avant retour
// dépend donc de l'ordre.
function delaiMoyen(etapes) {
  let cumul = 0, esperance = 0, resteVrai = 1;
  for (const e of etapes) {
    cumul += e.duree;
    // ← à écrire : probabilité d'arriver jusqu'ici ET d'échouer ici,
    //   pondérée par le temps écoulé
  }
  // Cas où tout passe : on paie la totalité.
  esperance += resteVrai * cumul;
  return esperance;
}

// ── 2. Cache et parallélisme ──────────────────────────────────────────────
const AVEC_CACHE = { installation: 8 };
const SANS_CACHE = { installation: 95 };

// Des jobs, chacun avec ses étapes et ses dépendances.
const JOBS = [
  { nom: "lint",         duree: 10,  depend: ["installation"] },
  { nom: "unitaires",    duree: 60,  depend: ["installation"] },
  { nom: "sécurité",     duree: 120, depend: ["installation"] },
  { nom: "intégration",  duree: 300, depend: ["unitaires"] },
  { nom: "bout en bout", duree: 900, depend: ["intégration"] },
];

function cheminCritique(jobs, installation) {
  const fin = { installation };
  // ← à écrire : la fin d'un job est le max des fins de ses dépendances,
  //   plus sa propre durée. Le pipeline dure jusqu'à la fin la plus tardive.
  return 0;
}

// ── 3. Tests instables ────────────────────────────────────────────────────
function probaEchecPipeline(nbTests, instabilite) {
  return 0;   // ← à écrire : 1 − (1 − p)^n
}

// ── À VOUS ────────────────────────────────────────────────────────────────
// 1. Écrivez delaiMoyen, puis comparez l'ordre rapide→lent et lent→rapide.
// 2. Écrivez cheminCritique. Que gagne le cache ? Et si l'on parallélise
//    seulement les étapes déjà rapides ?
// 3. Retrouvez les 63 % du cours, puis simulez une équipe qui relance
//    « pour voir » : combien de VRAIS échecs passent inaperçus ?

console.log("   délai moyen :", delaiMoyen(ETAPES).toFixed(0), "s");

Solution

const ETAPES = [
  { nom: "lint",            duree: 10,  proba: 0.25 },
  { nom: "tests unitaires", duree: 60,  proba: 0.35 },
  { nom: "intégration",     duree: 300, proba: 0.20 },
  { nom: "bout en bout",    duree: 900, proba: 0.10 },
];

function delaiMoyen(etapes) {
  let cumul = 0, esperance = 0, resteVrai = 1;
  for (const e of etapes) {
    cumul += e.duree;
    // Espérance : (probabilité d'arriver ici sans avoir échoué avant)
    // × (probabilité d'échouer ici) × (temps écoulé à cet instant).
    esperance += resteVrai * e.proba * cumul;
    resteVrai *= 1 - e.proba;
  }
  esperance += resteVrai * cumul;   // tout passe : on paie la totalité
  return esperance;
}

console.log("— 1. l'échec rapide, en secondes —");
const rapideDabord = [...ETAPES];
const lentDabord = [...ETAPES].reverse();
for (const [nom, ordre] of [["rapide → lent", rapideDabord], ["lent → rapide", lentDabord]]) {
  console.log("   " + nom.padEnd(16) + ordre.map((e) => e.nom).join(" → "));
  console.log("      délai moyen avant retour : " + Math.round(delaiMoyen(ordre)) + " s");
}
const gain = delaiMoyen(lentDabord) - delaiMoyen(rapideDabord);
console.log("   Écart : " + Math.round(gain) + " s en moyenne, pour TROIS LIGNES déplacées.");
console.log("   Et le pire cas compte autant : une faute de lint remonte en 10 s");
console.log("   au lieu de 1270 s — tant que le code est encore sous les yeux.");

console.log("");
console.log("— 2. cache et chemin critique —");
const JOBS = [
  { nom: "lint",         duree: 10,  depend: ["installation"] },
  { nom: "unitaires",    duree: 60,  depend: ["installation"] },
  { nom: "sécurité",     duree: 120, depend: ["installation"] },
  { nom: "intégration",  duree: 300, depend: ["unitaires"] },
  { nom: "bout en bout", duree: 900, depend: ["intégration"] },
];

function cheminCritique(jobs, installation) {
  const fin = { installation };
  let reste = [...jobs];
  while (reste.length) {
    const pret = reste.filter((j) => j.depend.every((d) => fin[d] !== undefined));
    if (!pret.length) throw new Error("dépendance circulaire");
    for (const j of pret) {
      // Un job démarre quand TOUTES ses dépendances sont finies.
      fin[j.nom] = Math.max(...j.depend.map((d) => fin[d])) + j.duree;
    }
    reste = reste.filter((j) => !pret.includes(j));
  }
  const total = Math.max(...Object.values(fin));
  const dernier = Object.entries(fin).find(([, v]) => v === total)[0];
  return { total, dernier, fin };
}

for (const [nom, inst] of [["sans cache", 95], ["avec cache", 8]]) {
  const r = cheminCritique(JOBS, inst);
  console.log("   " + nom.padEnd(12) + Math.round(r.total / 60) + " min " +
              (r.total % 60) + " s   (chemin critique se termine sur « " + r.dernier + " »)");
}
console.log("   Le cache gagne 87 s. Le chemin critique, lui, vaut " +
            Math.round(cheminCritique(JOBS, 8).total / 60) + " min :");
console.log("   installation → unitaires → intégration → bout en bout. Paralléliser");
console.log("   lint et sécurité ne change RIEN au total, puisqu'ils ne sont pas");
console.log("   sur ce chemin — c'est le chemin critique du cours d'architecture.");
console.log("   Pour descendre sous dix minutes, il faut découper le bout en bout.");

console.log("");
console.log("— 3. les tests instables —");
const probaEchecPipeline = (n, p) => 1 - Math.pow(1 - p, n);
console.log("   tests | instabilité | pipeline rouge sans cause");
for (const [n, p] of [[50, 0.005], [200, 0.005], [200, 0.001], [500, 0.002], [1000, 0.005]]) {
  console.log("   " + String(n).padStart(5) + " | " + (p * 100).toFixed(1).padStart(10) + " %" +
              " | " + (probaEchecPipeline(n, p) * 100).toFixed(0).padStart(20) + " %");
}
console.log("   200 tests à 0,5 % : 63 % des exécutions échouent sans raison.");

console.log("");
console.log("— ce que fait une équipe qui relance « pour voir » —");
let vraisEchecsRates = 0, relances = 0;
const N = 1000, INSTABILITE = 0.005, NB_TESTS = 200, VRAI_DEFAUT = 0.15;
for (let i = 0; i < N; i++) {
  const vraiDefaut = Math.random() < VRAI_DEFAUT;
  const instable = Math.random() < probaEchecPipeline(NB_TESTS, INSTABILITE);
  if (!vraiDefaut && !instable) continue;
  // L'équipe a appris que « rouge » signifie souvent « instable » : elle relance.
  relances++;
  // Une relance sur un vrai défaut échoue encore ; sur une instabilité, elle
  // passe souvent — d'où le réflexe. Mais certains vrais défauts sont aussi
  // intermittents, et ceux-là passent à la relance : ils sont ignorés.
  if (vraiDefaut && Math.random() < 0.3) vraisEchecsRates++;
}
console.log("   sur " + N + " exécutions : " + relances + " relances « pour voir »,");
console.log("   et " + vraisEchecsRates + " vrais défauts passés inaperçus parce qu'ils");
console.log("   étaient eux-mêmes intermittents.");
console.log("   C'est le vrai coût de l'instabilité : elle ne fait pas perdre du");
console.log("   temps, elle DÉTRUIT LE SIGNAL. Un test instable se répare ou se");
console.log("   supprime le jour même.");

En travaux pratiques

Travaux pratiques 5 · 4 h

Construire le pipeline, et le rendre rapide

Écrire un pipeline complet sur le dépôt du fil rouge, puis le mesurer et le raccourcir — ordre des étapes, cache, parallélisme.

Avant de commencer

  • Le dépôt du TP 2 hébergé sur une forge disposant d'intégration continue
  • La pile Compose du TP 4, qui démarre
  • Une suite de tests, même minimale

Énoncé

  1. Un premier pipeline qui échoueÉcrivez un pipeline déclenché sur chaque demande de fusion : installation, analyse statique, tests. Poussez une modification qui casse volontairement le lint, et vérifiez que la demande de fusion est bloquée.
  2. Mesurer le délai avant retourPlacez délibérément les étapes de la plus lente à la plus rapide. Poussez une faute de lint et chronométrez le temps entre le push et le message d'échec. Réordonnez, refaites la mesure.
  3. Mettre les dépendances en cacheActivez le cache du gestionnaire de paquets et comparez la durée d'installation sur deux exécutions consécutives.
  4. Ajouter les tests d'intégrationFaites tourner les tests contre une vraie base, démarrée par le pipeline. Ils ne doivent PAS dépendre les uns des autres : chaque test crée et détruit ses propres données. Indice : Les forges offrent des services attachés à un job, avec une sonde de santé — c'est le depends_on du TP 4, transposé.
  5. Paralléliser, et trouver la limiteDécoupez en jobs indépendants et mesurez le gain. Identifiez ensuite le chemin critique : quelle suite d'étapes fixe la durée totale ? Que gagneriez-vous en parallélisant ce qui n'est PAS sur ce chemin ?
  6. Éprouver l'instabilitéIntroduisez un test qui échoue aléatoirement une fois sur vingt. Relancez le pipeline dix fois et comptez les échecs. Discutez : que fera l'équipe au bout d'une semaine ?
  7. Publier l'imageAjoutez un job final qui construit l'image du TP 3 et la pousse dans un registre, étiquetée avec l'empreinte du commit. C'est cet artefact que le TP 6 déploiera.

C'est réussi quand

  • Une faute de lint est signalée en moins de trente secondes
  • Le pipeline complet dure moins de dix minutes
  • Deux exécutions consécutives sans changement de dépendances ne réinstallent rien
  • L'image publiée porte l'empreinte du commit qui l'a produite

Correction

Le pipeline.github/workflows/integration.yml
name: intégration
on:
pull_request:
push:
  branches: [main]

jobs:
rapide:
  runs-on: ubuntu-latest
  steps:
    - uses: actions/checkout@v4
    - uses: actions/setup-node@v4
      with: { node-version: '20', cache: 'npm' }
    - run: npm ci
    - run: npm run lint          # 10 s : en premier
    - run: npm run test:unit     # 60 s

integration:
  runs-on: ubuntu-latest
  services:
    basededonnees:
      image: postgres:16-alpine
      env: { POSTGRES_USER: app, POSTGRES_PASSWORD: secret }
      options: >-
        --health-cmd "pg_isready -U app"
        --health-interval 5s --health-retries 10
  steps:
    - uses: actions/checkout@v4
    - uses: actions/setup-node@v4
      with: { node-version: '20', cache: 'npm' }
    - run: npm ci
    - run: npm run test:integration
      env:
        DATABASE_URL: postgres://app:secret@localhost:5432/app

image:
  needs: [rapide, integration]
  runs-on: ubuntu-latest
  steps:
    - uses: actions/checkout@v4
    - run: docker build -t registre/tickets:${{ github.sha }} .
    - run: docker push registre/tickets:${{ github.sha }}

Le job « rapide » et le job « integration » tournent en parallèle ; la publication de l'image attend les deux, parce qu'on ne publie que ce qui est vérifié. L'étiquette est l'empreinte du commit : c'est ce qui donne la traçabilité du conteneur jusqu'à la ligne de code.

Les deux mesures d'ordonnancement
ordre lent → rapide : faute de lint signalée après 16 min 10 s
ordre rapide → lent : faute de lint signalée après      12 s

La première étape qui échoue arrête le job : l'ordre décide donc du délai avant le retour. Trois lignes déplacées, et la faute remonte tant que le code est encore sous les yeux — au lieu d'arriver quand on est passé à autre chose.

Le chemin critique
installation 8 s ─┬─ lint 10 s
                ├─ unitaires 60 s ── intégration 300 s
                └─ sécurité 120 s

durée totale = 8 + 60 + 300 = 368 s

Paralléliser lint et sécurité ne change RIEN au total : ils ne sont pas sur le chemin critique. Pour descendre, il faut s'attaquer aux tests d'intégration — les découper et les répartir entre plusieurs jobs. C'est le chemin critique du cours d'architecture, appliqué à un pipeline.

Le test instable, en chiffres

Un test qui échoue une fois sur vingt fait échouer le pipeline dans 5 % des cas ; avec deux cents tests à 0,5 %, on atteint 63 %. Au bout d'une semaine, l'équipe relance « pour voir » au lieu de lire l'échec — et un vrai défaut passe alors inaperçu. La règle est de réparer ou supprimer le test le jour même : le garder est pire que ne pas l'avoir, parce qu'il détruit le signal.

Tests d'intégration indépendantstests/integration/tickets.test.js
beforeEach(async () => {
schema = "test_" + Math.random().toString(36).slice(2, 8);
await db.query("CREATE SCHEMA " + schema);
await migrer(schema);
});

afterEach(async () => {
await db.query("DROP SCHEMA " + schema + " CASCADE");
});

Chaque test crée son propre espace et le détruit. Sans cela, les tests ne passent que dans un ordre donné et deviennent instables dès qu'on les parallélise — ce qui ramène au point précédent.

Ce que la suite en fait

Le chapitre 6 prolonge le pipeline jusqu'à la production. Tout ce qui vient d'être posé y reste valable : le même déclencheur, les mêmes artefacts — et l'image construite ici devient exactement celle qui sera déployée, ce qui est la garantie centrale de tout le bloc.

C'est aussi là que la discipline de ce chapitre prend son sens complet : on ne déploie automatiquement que ce en quoi on a confiance, et cette confiance vient précisément d'un pipeline vert que personne n'a appris à ignorer.

À retenir

Flashcards · 5 cartes

Quelles sont les quatre règles de l'intégration continue, et pourquoi l'outil ne suffit pas ?
1) INTÉGRER au moins une fois par jour dans la branche principale. 2) Chaque intégration déclenche une VÉRIFICATION AUTOMATIQUE — manuelle, elle est sautée un vendredi soir. 3) Une construction CASSÉE se répare tout de suite : tant que la branche est rouge, plus personne ne peut savoir si son travail est correct. 4) PERSONNE NE CONSTRUIT par-dessus du rouge. Sans ces règles on obtient un pipeline qui échoue depuis trois semaines et que tout le monde ignore.
Nommez les cinq notions d'un pipeline.
Le DÉCLENCHEUR (push, demande de fusion, étiquette, horaire). Le JOB, unité exécutée sur une machine propre — deux jobs indépendants tournent en parallèle. L'ÉTAPE, commande dans un job, la première qui échoue arrêtant le job. L'ARTEFACT, fichier produit et conservé (rapport, image, binaire), qui sert aussi à passer un résultat d'un job à l'autre. Le CACHE, qui conserve entre deux exécutions ce qui est coûteux à reconstituer.
Quels sont les deux leviers pour raccourcir un pipeline, et quelle limite ?
1) ORDONNER DU PLUS RAPIDE AU PLUS LENT : lint 10 s, unitaires 1 min, intégration 5 min, bout en bout 15 min — une faute de lint remonte alors en dix secondes au lieu de vingt minutes. 2) PARALLÉLISER ce qui est indépendant. LIMITE : le pipeline dure aussi longtemps que son CHEMIN LE PLUS LONG — paralléliser ce qui est déjà rapide ne gagne rien, il faut s'attaquer aux tests de bout en bout. Seuil pratique : au-delà de dix minutes, on ne l'attend plus.
Pourquoi la couverture de tests est-elle un bon signal et un mauvais objectif ?
Elle mesure les lignes EXÉCUTÉES, pas les comportements VÉRIFIÉS : un test sans assertion donne 100 % et ne détecte rien. Dès qu'un seuil devient contraignant, c'est ce chemin-là que la pression fait émerger — et 90 % de couverture creuse est plus dangereux que 40 % assumés, car le tableau de bord affiche du vert. Règle générale : quand une mesure devient un objectif, elle cesse d'être une bonne mesure. On surveille la TENDANCE, et surtout ce qui n'est couvert par AUCUN test.
Pourquoi un test instable est-il pire que pas de test, et quel est le calcul ?
Si chaque test échoue au hasard avec une probabilité p, la probabilité qu'au moins un des n échoue vaut 1 − (1−p)^n : avec p = 0,5 % et 200 tests, 63 % d'échecs SANS AUCUNE CAUSE RÉELLE. L'équipe relance « pour voir » et APPREND À IGNORER les échecs — un vrai échec passe alors inaperçu. Un test instable se répare ou se supprime le jour même. Autres maux : les pipelines lents, qui poussent à regrouper les commits, et les tests DÉPENDANTS entre eux, qui deviennent instables dès qu'on parallélise.

Chapitre 2 · 7 h

Livraison et déploiement continus

Livraison contre déploiement, environnements, stratégies bleu-vert, canari et progressive, retour arrière, gestion des versions et portes de validation.

Dans beaucoup d'organisations, la mise en production est un événement. Elle a une date, une réunion de préparation, une liste de vérifications, et elle a lieu un jeudi soir pour laisser le vendredi aux réparations. Quelqu'un veille. Personne n'aime ça.

Dans les organisations dont parlait le chapitre 1, c'est un non-événement. Cela arrive plusieurs fois par jour, personne ne le remarque, et si quelque chose se passe mal, on revient en arrière en quelques secondes.

La différence n'est pas le courage des équipes : c'est le dispositif. Ce chapitre le construit.

Livraison n'est pas déploiement

Deux termes portent les mêmes initiales et désignent deux engagements différents.

La livraison continue garantit que la branche principale est toujours déployable : à tout instant, un artefact validé existe et pourrait partir en production. La décision de partir reste humaine.

Le déploiement continu va au bout : toute modification qui passe le pipeline part en production automatiquement, sans intervention.

commit → construction → tests → artefact → [recette] → ⏸ décision → production                                    livraison continue ─┘                                    déploiement continu : pas de pause

Le second n'est pas « mieux » : c'est un choix qui dépend du contexte. Un service en ligne interne y gagne. Un logiciel embarqué dans un dispositif médical, un système bancaire soumis à une validation réglementaire, ou un produit installé chez des clients ne peuvent pas s'y conformer — et n'ont pas à le faire.

Mais la livraison continue, elle, est toujours souhaitable, y compris pour ces cas-là : savoir qu'on pourrait déployer à tout instant est ce qui rend le choix de ne pas le faire délibéré plutôt que subi.

Les environnements, et la règle qui les gouverne

développement → recette (staging) → production

Chacun sert à une chose différente : le développement à écrire, la recette à valider dans des conditions proches du réel, la production à servir les utilisateurs.

La règle qui compte tient en une phrase : le même artefact traverse les environnements. L'image construite par le pipeline du chapitre 5 est déployée en recette, validée, puis promue en production — pas reconstruite.

Reconstruire par environnement ruine la garantie centrale du bloc : ce qui a été testé n'est alors plus exactement ce qui tourne. Une version de dépendance a bougé entre les deux constructions, une couche de base a été mise à jour, et le bogue n'apparaît qu'en production.

Ce qui change d'un environnement à l'autre, c'est donc uniquement la configuration — variables d'environnement, secrets, adresses de services — injectée au démarrage, comme au chapitre 3.

Deux conséquences pratiques. L'artefact est immuable et porte un identifiant unique, qui permet de remonter du conteneur en production au commit exact qui l'a produit. Et la recette doit ressembler à la production : mêmes versions, mêmes volumes de données autant que possible — une recette avec cent enregistrements ne dit rien du comportement sur dix millions.

Quatre stratégies de déploiement

C'est le cœur pratique du chapitre, et le choix se fait sur trois critères : exposition en cas de bogue, vitesse de retour arrière, coût en ressources.

StratégiePrincipeExpositionRetour arrièreCoût
Recréationarrêter, remplacer, redémarrer100 %redéployer l'anciennenul
Bleu-vertdeux environnements complets, bascule du routage100 % à la basculeinstantanédouble
Progressifremplacer les exemplaires un à uncroissanteprogressiffaible
Canarirouter une petite part du trafic vers la nouvelle version5 % puis paliersinstantanéfaible

La recréation est la seule qui provoque une interruption de service : elle reste acceptable pour un outil interne, jamais pour un service public.

Le bleu-vert maintient deux environnements complets. On déploie sur l'inactif, on le teste en conditions réelles sans trafic, puis on bascule le routage. Le retour arrière est le même geste en sens inverse : quelques secondes. Le prix est de payer deux environnements.

Le déploiement progressif remplace les exemplaires un par un, en maintenant un nombre minimal d'exemplaires disponibles. C'est le comportement par défaut de Kubernetes, au chapitre 9.

Le canari est le plus prudent : la nouvelle version reçoit d'abord une petite fraction du trafic, on mesure, et l'on progresse par paliers.

Animation · 8 étapes

Déploiement canari : la panne existe, elle ne touche pas tout le monde

  1. Avant tout déploiementLa version 1 sert l'intégralité du trafic, avec un taux d'erreur de référence de 0,1 %. C'est ce chiffre qui servira de comparant : sans ligne de base mesurée, aucune stratégie progressive n'a de sens.
  2. v2 est déployée, et ne sert personneLes deux versions tournent en parallèle. C'est le point qui distingue cette stratégie d'une mise à jour classique : à aucun moment on ne remplace, on ajoute d'abord. Le coût est de faire tourner deux versions à la fois.
  3. 5 % — les premières requêtes réellesVingt utilisateurs sur quatre cents voient la nouvelle version. Aucun jeu de tests ne remplace ce moment : c'est le premier contact avec les vraies données, les vrais volumes et les vrais navigateurs.
  4. Observation : 0,2 % contre 0,1 % de référenceL'écart est dans le bruit sur un si petit échantillon. La durée d'observation compte autant que le seuil : basculer après trente secondes ne prouve rien, et c'est l'erreur la plus fréquente des pipelines automatisés.
  5. 25 % — le palier suivantCent utilisateurs. Le taux reste acceptable, la latence aussi. Chaque palier multiplie l'exposition, donc la qualité de la mesure — et le coût d'une erreur non détectée.
  6. Le taux d'erreur s'envole4,1 % contre 0,1 % de référence : quarante fois la normale. La cause importe peu à cet instant — une requête lente sur un volume qui n'apparaissait pas en recette, par exemple. Ce qui compte est que le seuil soit franchi et que la décision soit AUTOMATIQUE.
  7. Retour arrière : rebasculer le routagev1 n'a jamais été arrêtée : le retour arrière est un changement de routage, pas un redéploiement. Quelques secondes, contre les dizaines de minutes qu'aurait demandées la reconstruction d'une version précédente.
  8. Ce qu'un déploiement d'un bloc aurait coûtéMême bogue, bascule totale : 100 % du trafic touché, et pendant tout le temps qu'il faut pour s'en apercevoir puis reconstruire — typiquement quarante minutes. Le canari n'a pas empêché la panne : il a divisé son ampleur par quatre et sa durée par dix.

Ce que l'animation montre, et qui est le point du chapitre : le canari n'empêche pas la panne. Le bogue était là, il est parti en production, et des utilisateurs l'ont subi. Ce que le dispositif a changé, c'est son ampleur — un quart du trafic au lieu de la totalité — et sa durée — quatre minutes au lieu du temps nécessaire pour s'apercevoir du problème puis reconstruire une version.

C'est le renversement conceptuel du bloc : on ne cherche plus à empêcher tout incident, ce qui est hors d'atteinte, mais à réduire le rayon d'impact et le temps de rétablissement. Les deux derniers indicateurs DORA du chapitre 1 mesurent exactement cela.

Une condition, toutefois, sans laquelle le canari ne sert à rien : il faut des mesures et un seuil décidé à l'avance. Faire du canari sans supervision, c'est exposer 5 % des utilisateurs et attendre qu'ils se plaignent — le chapitre 10 fournit ce qui manque.

Le retour arrière

C'est la capacité la plus importante du chapitre, et la plus souvent négligée.

Un retour arrière doit être testé. Un mécanisme jamais exécuté ne fonctionne pas le jour où l'on en a besoin — et ce jour-là, personne n'est en état de le déboguer. Les équipes sérieuses en déclenchent régulièrement, hors incident.

Il doit être plus rapide que la réparation. Face à un incident, la tentation est de corriger en avant : trouver le bogue, écrire le correctif, le faire passer dans le pipeline, déployer. Cela prend au minimum une demi-heure, souvent plus, et sous pression. Revenir à la version précédente prend quelques secondes, et on diagnostique ensuite, à froid.

Le point dur est la base de données. Le code revient en arrière ; les données, non. Une migration qui a supprimé une colonne rend l'ancienne version incapable de fonctionner. La parade est la migration compatible en avant, en plusieurs étapes :

étape 1   ajouter la nouvelle colonne, écrire dans les DEUXétape 2   déployer le code qui lit la nouvelleétape 3   plus tard, quand le retour arrière n'est plus envisagé,          cesser d'écrire dans l'ancienne, puis la supprimer

C'est plus long à écrire, et c'est ce qui rend le retour arrière possible à tout moment. La règle qui en découle : jamais de suppression ni de renommage destructif dans le même déploiement que le code qui en dépend.

Versions, artefacts, et portes manuelles

La version sémantiqueMAJEUR.MINEUR.CORRECTIF — communique la nature d'un changement : un MAJEUR incrémenté annonce une rupture de compatibilité. Elle est indispensable pour une bibliothèque publiée ; pour un service déployé en continu, on lui préfère souvent une simple étiquette horodatée ou l'empreinte du commit, qui donne la traçabilité : de l'incident au conteneur, du conteneur à l'artefact, de l'artefact au commit.

Restent les portes de validation manuelles. Elles sont légitimes dans trois cas : une exigence réglementaire, une fenêtre de déploiement contrainte par le métier, ou une décision commerciale sur la date de mise à disposition d'une fonctionnalité.

Elles sont du théâtre dans un cas, très fréquent : quand la personne qui valide n'a pas les moyens de décider. Cliquer « approuver » sans lire un tableau de bord ni disposer d'un critère explicite ne réduit aucun risque ; cela ajoute un délai et une signature. Si la porte est là pour rassurer, il vaut mieux se demander ce qui manque au dispositif pour qu'on n'ait plus besoin d'être rassuré — c'est presque toujours de la mesure.

Quiz · 1 question

Une équipe reconstruit son image à chaque environnement : une pour la recette, une pour la production. Où est le problème ?

  • Nulle part : reconstruire garantit que chaque environnement a bien les dernières versionsaucun problème
  • Ce qui a été TESTÉ n'est plus exactement ce qui TOURNE : entre les deux constructions, une dépendance ou une couche de base a pu bouger, et le bogue n'apparaît qu'en production. Le même artefact doit être PROMU d'un environnement à l'autrel'artefact testé n'est plus celui qui tourne
  • Le problème est seulement le temps de construction, doublé inutilementperte de temps

Réponse : C'est la garantie centrale de tout le bloc : ce qui a été validé est, au bit près, ce qui sert les utilisateurs. Reconstruire la détruit. Entre deux constructions séparées de quelques heures, une dépendance transitive a pu être publiée dans une nouvelle version, l'image de base a pu recevoir un correctif, un miroir a pu servir un paquet différent — et l'on obtient deux artefacts qui ne sont pas identiques, dont un seul a été testé. La règle est donc de construire UNE FOIS, puis de PROMOUVOIR le même artefact immuable de la recette vers la production. Ce qui change d'un environnement à l'autre n'est que la CONFIGURATION, injectée au démarrage. Le temps de construction doublé est réel mais anecdotique à côté de cela.

Quiz · 1 question

Un déploiement introduit un bogue. L'équipe cherche la cause pour corriger en avant. Que dit ce chapitre ?

  • C'est la bonne démarche : revenir en arrière ne fait que reporter le problèmecorriger en avant
  • Il faut d'abord REVENIR À LA VERSION PRÉCÉDENTE — quelques secondes — puis diagnostiquer à froid. Corriger en avant prend au minimum une demi-heure, sous pression, pendant que les utilisateurs subissentrevenir d'abord
  • Il faut attendre les premières plaintes utilisateurs pour évaluer la gravité avant d'agirattendre

Réponse : Les deux actions ne se comparent pas. Le retour arrière est une opération connue, rapide et sans réflexion : rebasculer un routage ou redéployer un artefact déjà construit prend quelques secondes. La correction en avant demande de trouver la cause, écrire un correctif, le faire passer par tout le pipeline et déployer — au minimum une demi-heure, en général plus, et sous une pression qui produit précisément les erreurs qu'on veut éviter. Revenir d'abord arrête l'hémorragie et permet de diagnostiquer à froid, avec le même artefact fautif reproduit en recette. Deux conditions y sont attachées : le retour arrière doit être TESTÉ régulièrement hors incident — un mécanisme jamais exécuté ne fonctionne pas le jour venu — et les migrations de base doivent être COMPATIBLES EN AVANT, sans quoi le code revient en arrière mais les données, non.

À vous

L'exercice met les quatre stratégies sur le même incident et calcule ce qui compte vraiment.

On injecte un bogue qui produit un taux d'erreur donné, et le programme calcule pour chaque stratégie le nombre d'utilisateurs touchés et la durée d'exposition, en tenant compte du délai de détection. Vous verrez que le classement dépend du délai de détection : sans supervision, le canari perd une grande partie de son intérêt — ce qui prépare le chapitre 10.

La seconde partie porte sur la migration de base de données. Vous exécuterez un scénario de retour arrière après une migration destructive — l'ancienne version ne démarre plus — puis vous réécrirez la migration en trois étapes compatibles en avant, et vous vérifierez que le retour arrière redevient possible à chaque étape.

Exercice de code

Chiffrez l'impact des quatre stratégies selon le délai de détection, puis rendez une migration réversible.

Point de départ

// ── 1. Quatre stratégies face au même bogue ───────────────────────────────
const TRAFIC = 1000;          // requêtes par minute
const TAUX_ERREUR = 0.30;     // 30 % des requêtes servies par v2 échouent

// paliers : [part du trafic, durée du palier en minutes]
const STRATEGIES = {
  "recréation":  { paliers: [[1.0, Infinity]], interruption: 2, retour: 8 },
  "bleu-vert":   { paliers: [[1.0, Infinity]], interruption: 0, retour: 0.2 },
  "progressif":  { paliers: [[0.2, 2], [0.4, 2], [0.6, 2], [0.8, 2], [1.0, Infinity]],
                   interruption: 0, retour: 3 },
  "canari":      { paliers: [[0.05, 10], [0.25, 10], [0.5, 10], [1.0, Infinity]],
                   interruption: 0, retour: 0.2 },
};

// detection : combien de minutes avant que quelqu'un s'en aperçoive.
function impact(strategie, detection) {
  let t = 0, requetesRatees = 0;
  for (const [part, duree] of strategie.paliers) {
    const restant = detection - t;
    if (restant <= 0) break;
    const passe = Math.min(duree, restant);
    // ← à écrire : requêtes ratées pendant ce palier
    t += passe;
  }
  // Puis le retour arrière, pendant lequel la part courante continue.
  return { requetesRatees: Math.round(requetesRatees), duree: detection + strategie.retour };
}

// ── 2. Migration de base et retour arrière ────────────────────────────────
// Un schéma, deux versions de code, et la question : peut-on revenir ?
const SCHEMA = { colonnes: ["id", "nom_complet"] };

const MIGRATION_DESTRUCTIVE = [
  { nom: "v2 : renommer nom_complet en nom", appliquer: (s) => ({
      colonnes: s.colonnes.map((c) => (c === "nom_complet" ? "nom" : c)) }) },
];

const CODE = {
  v1: { lit: ["id", "nom_complet"], ecrit: ["nom_complet"] },
  v2: { lit: ["id", "nom"],         ecrit: ["nom"] },
};

function peutTourner(version, schema) {
  const besoins = [...CODE[version].lit, ...CODE[version].ecrit];
  return besoins.every((c) => schema.colonnes.includes(c));
}

// ── À VOUS ────────────────────────────────────────────────────────────────
// 1. Écrivez impact(). Comparez les quatre stratégies avec une détection à
//    2 min (supervision), puis à 45 min (une plainte utilisateur).
// 2. Appliquez la migration destructive, déployez v2, puis tentez le retour
//    à v1 : que se passe-t-il ?
// 3. Réécrivez la migration en TROIS étapes compatibles en avant, et
//    vérifiez qu'après chacune, les DEUX versions peuvent tourner.

console.log(JSON.stringify(impact(STRATEGIES["canari"], 2)));

Solution

const TRAFIC = 1000;
const TAUX_ERREUR = 0.30;

const STRATEGIES = {
  "recréation":  { paliers: [[1.0, Infinity]], interruption: 2, retour: 8 },
  "bleu-vert":   { paliers: [[1.0, Infinity]], interruption: 0, retour: 0.2 },
  "progressif":  { paliers: [[0.2, 2], [0.4, 2], [0.6, 2], [0.8, 2], [1.0, Infinity]],
                   interruption: 0, retour: 3 },
  "canari":      { paliers: [[0.05, 10], [0.25, 10], [0.5, 10], [1.0, Infinity]],
                   interruption: 0, retour: 0.2 },
};

function impact(strategie, detection) {
  let t = 0, requetesRatees = 0, partCourante = 0;
  for (const [part, duree] of strategie.paliers) {
    const restant = detection - t;
    if (restant <= 0) break;
    const passe = Math.min(duree, restant);
    partCourante = part;
    // Seule la fraction du trafic servie par la nouvelle version échoue.
    requetesRatees += TRAFIC * part * TAUX_ERREUR * passe;
    t += passe;
  }
  // Pendant le retour arrière, la part courante continue d'échouer.
  requetesRatees += TRAFIC * partCourante * TAUX_ERREUR * strategie.retour;
  // L'interruption de service, elle, rate TOUT le trafic.
  requetesRatees += TRAFIC * strategie.interruption;
  return { requetesRatees: Math.round(requetesRatees), duree: detection + strategie.retour,
           exposition: partCourante };
}

console.log("— 1. même bogue, quatre stratégies —");
for (const detection of [2, 45]) {
  console.log("   détection après " + detection + " min " +
    (detection === 2 ? "(supervision et seuil automatique)" : "(une plainte utilisateur)"));
  const lignes = Object.entries(STRATEGIES).map(([nom, s]) => ({ nom, ...impact(s, detection) }));
  lignes.sort((a, b) => a.requetesRatees - b.requetesRatees);
  for (const l of lignes) {
    console.log("      " + l.nom.padEnd(12) +
      String(l.requetesRatees).padStart(7) + " requêtes ratées" +
      " | exposition " + (l.exposition * 100).toFixed(0).padStart(3) + " %" +
      " | incident " + l.duree.toFixed(1) + " min");
  }
  console.log("");
}
console.log("   Le classement CHANGE avec le délai de détection. Avec supervision,");
console.log("   le canari est de loin le meilleur : il n'a exposé que 5 % du trafic.");
console.log("   Sans supervision, il a eu le temps de monter à 100 % avant qu'on ne");
console.log("   s'en aperçoive, et il ne vaut plus mieux que le bleu-vert.");
console.log("   Conclusion : le canari sans mesure est un canari décoratif — c'est");
console.log("   ce que le chapitre 10 vient compléter.");

console.log("");
console.log("— 2. migration destructive et retour arrière —");
const CODE = {
  v1: { lit: ["id", "nom_complet"], ecrit: ["nom_complet"] },
  v2: { lit: ["id", "nom"],         ecrit: ["nom"] },
};
function peutTourner(version, schema) {
  const besoins = [...CODE[version].lit, ...CODE[version].ecrit];
  return besoins.every((c) => schema.colonnes.includes(c));
}
function etat(etiquette, schema) {
  console.log("      " + etiquette.padEnd(38) + "colonnes [" + schema.colonnes.join(", ") + "]" +
    "  v1 " + (peutTourner("v1", schema) ? "ok " : "KO ") +
    "  v2 " + (peutTourner("v2", schema) ? "ok" : "KO"));
}
let schema = { colonnes: ["id", "nom_complet"] };
etat("départ", schema);
schema = { colonnes: schema.colonnes.map((c) => (c === "nom_complet" ? "nom" : c)) };
etat("après le renommage destructif", schema);
console.log("      → v2 tourne, mais le RETOUR À v1 EST IMPOSSIBLE : la colonne");
console.log("        dont elle a besoin n'existe plus. Le code revient en arrière,");
console.log("        les données non.");

console.log("");
console.log("— 3. la même migration, compatible en avant —");
let s2 = { colonnes: ["id", "nom_complet"] };
etat("départ", s2);
// Étape 1 : AJOUTER, sans rien retirer. Les deux colonnes coexistent.
s2 = { colonnes: [...s2.colonnes, "nom"] };
etat("étape 1 : ajouter nom, écrire dans les deux", s2);
// Étape 2 : déployer v2, qui lit la nouvelle colonne. v1 tourne toujours.
etat("étape 2 : déployer v2", s2);
console.log("      → à cet instant, LES DEUX versions fonctionnent : le retour");
console.log("        arrière reste possible à tout moment.");
// Étape 3 : seulement quand le retour arrière n'est plus envisagé.
s2 = { colonnes: s2.colonnes.filter((c) => c !== "nom_complet") };
etat("étape 3 : supprimer nom_complet (plus tard)", s2);
console.log("      → v1 n'est plus déployable, mais on ne l'envisage plus depuis");
console.log("        plusieurs jours. La règle : JAMAIS de suppression ni de");
console.log("        renommage dans le même déploiement que le code qui en dépend.");

En travaux pratiques

Travaux pratiques 6 · 3 h

Bleu-vert, canari, retour arrière

Déployer l'artefact du TP 5 selon deux stratégies, mesurer ce que chacune expose en cas de bogue, et rendre une migration de base réversible.

Avant de commencer

  • L'image publiée par le pipeline du TP 5, étiquetée par empreinte de commit
  • La pile Compose du TP 4
  • Un outil de charge simple (ab, hey, ou une boucle curl)

Énoncé

  1. Monter le routagePlacez un répartiteur devant l'application. Faites tourner DEUX exemplaires de la pile, en version 1.0, et vérifiez que le trafic se répartit sur les deux. Indice : Un nginx en amont, avec un bloc upstream, suffit — inutile de sortir l'artillerie.
  2. Déployer en bleu-vertConstruisez une version 1.1, déployez-la à côté sans lui envoyer de trafic, testez-la directement, puis basculez le routage d'un coup. Chronométrez la bascule et le retour arrière.
  3. Fabriquer un bogueProduisez une version 1.2 qui renvoie une erreur sur 30 % des requêtes. Déployez-la en bleu-vert pendant qu'une charge tourne, et comptez les requêtes en erreur avant que vous ne réagissiez.
  4. Recommencer en canariMême version fautive, mais en n'envoyant d'abord que 5 % du trafic, puis 25 %. Comptez à nouveau les requêtes en erreur, et comparez à l'étape précédente. Indice : Des poids sur les entrées du bloc upstream suffisent à répartir 5 / 95.
  5. Décider automatiquementÉcrivez un script qui interroge le taux d'erreur toutes les dix secondes et déclenche le retour arrière au-delà d'un seuil que vous fixez d'avance. Relancez le scénario sans intervenir.
  6. La migration destructiveVersion 1.3 : renommez une colonne de la base et adaptez le code. Déployez, puis tentez un retour arrière vers 1.2. Constatez ce qui se passe.
  7. La migration en trois étapesRefaites la même évolution en la découpant, de sorte qu'à chaque étape les DEUX versions puissent tourner. Vérifiez le retour arrière après chacune.

C'est réussi quand

  • Le retour arrière prend moins de cinq secondes et ne reconstruit rien
  • Le canari expose au moins quatre fois moins de requêtes que le bleu-vert sur le même bogue
  • Le script déclenche le retour arrière sans vous
  • Après chaque étape de la migration découpée, l'ancienne version démarre encore

Correction

Le routage pondérénginx.conf
upstream tickets {
  server appli_bleu:3000  weight=95;
  server appli_vert:3000  weight=5;   # le canari
}

server {
  listen 80;
  location / {
      proxy_pass http://tickets;
      proxy_next_upstream off;   # ne pas masquer les erreurs du canari
  }
}

La dernière ligne est celle qu'on oublie : par défaut, nginx réessaie sur un autre serveur quand l'un échoue. Le canari renverrait alors des erreurs que personne ne verrait — et la mesure sur laquelle repose toute la stratégie serait fausse.

Les deux mesures attendues
bleu-vert, réaction humaine à 4 min : ~21 600 requêtes en erreur
canari 5 % puis 25 %, même bogue     :  ~5 400 requêtes en erreur
canari + seuil automatique à 20 s    :    ~300 requêtes en erreur

Le canari n'a EMPÊCHÉ aucune panne : le bogue est parti en production dans les trois cas. Il a divisé l'ampleur, et le seuil automatique a divisé la durée. C'est le renversement du chapitre — on ne cherche pas à empêcher l'incident mais à réduire son rayon et son temps.

La décision automatiquesurveiller.sh
#!/bin/sh
SEUIL=2          # % d'erreurs toléré, décidé À L'AVANCE
while true; do
taux=$(curl -s http://localhost/metriques | awk '/taux_erreur/ {print $2}')
echo "taux = $taux %"
if [ "$(echo "$taux > $SEUIL" | bc)" -eq 1 ]; then
  echo "seuil dépassé : retour arrière"
  sed -i 's/weight=5/weight=0/' nginx.conf
  docker compose exec -T proxy nginx -s reload
  exit 1
fi
sleep 10
done

Le seuil est fixé AVANT le déploiement, pas pendant l'incident. Un canari sans mesure ni seuil expose 5 % des utilisateurs et attend qu'ils se plaignent — c'est ce que le chapitre 10 viendra compléter avec de vraies métriques.

Pourquoi le retour arrière échoue après la migration
-- 1.3 : migration destructive
ALTER TABLE tickets RENAME COLUMN titre TO intitule;

-- l'ancienne version cherche « titre », qui n'existe plus
-- ERROR: column "titre" does not exist

Le code revient en arrière ; les données, non. C'est le point dur du chapitre, et la seule sortie est d'écrire la migration autrement.

La même évolution, réversible
-- étape 1 : AJOUTER, sans rien retirer
ALTER TABLE tickets ADD COLUMN intitule text;
UPDATE tickets SET intitule = titre;
-- le code 1.2 écrit dans « titre », un déclencheur recopie dans « intitule »

-- étape 2 : déployer 1.3, qui LIT « intitule » et écrit dans les DEUX
--           → à cet instant, 1.2 et 1.3 fonctionnent toutes deux

-- étape 3 : plusieurs jours plus tard, quand le retour arrière
--           n'est plus envisagé
ALTER TABLE tickets DROP COLUMN titre;

Trois déploiements au lieu d'un, et le retour arrière reste possible à tout instant sauf après le dernier. La règle générale : jamais de suppression ni de renommage dans le même déploiement que le code qui en dépend.

Ce que la suite en fait

Le bloc IV pose la question laissée de côté : sur quoi déploie-t-on ? Les serveurs, réseaux et bases de ce chapitre ont été supposés existants ; le chapitre 7 les décrit dans des fichiers versionnés, ce qui permet de reconstruire un environnement de recette identique à la production — condition de la règle posée plus haut.

Et le chapitre 10 fournira ce qui manque au canari : sans métriques et sans seuil décidé à l'avance, la décision de progresser ou de revenir n'a aucune base.

À retenir

Flashcards · 6 cartes

Distinguez livraison continue et déploiement continu.
La LIVRAISON continue garantit que la branche principale est TOUJOURS DÉPLOYABLE : un artefact validé existe à tout instant, et la décision de partir reste humaine. Le DÉPLOIEMENT continu supprime la pause : ce qui passe le pipeline part en production. Le second n'est pas « mieux » — un dispositif médical ou un système soumis à validation réglementaire ne peut pas s'y conformer. Mais la livraison continue est toujours souhaitable : elle rend le choix de ne pas déployer DÉLIBÉRÉ plutôt que subi.
Quelle règle gouverne les environnements, et pourquoi ?
LE MÊME ARTEFACT LES TRAVERSE : on construit une fois, puis on PROMEUT l'image de la recette vers la production, sans jamais reconstruire. Reconstruire détruit la garantie centrale du bloc — entre deux constructions, une dépendance ou une couche de base a pu bouger, et ce qui a été testé n'est plus ce qui tourne. Seule la CONFIGURATION change d'un environnement à l'autre, injectée au démarrage. L'artefact est immuable et porte un identifiant qui permet de remonter au commit.
Comparez recréation, bleu-vert, progressif et canari.
RECRÉATION : arrêter puis remplacer — 100 % d'exposition et INTERRUPTION DE SERVICE, acceptable pour un outil interne seulement. BLEU-VERT : deux environnements complets, bascule du routage — retour arrière INSTANTANÉ, au prix du double d'infrastructure. PROGRESSIF : remplacer les exemplaires un à un, exposition croissante, coût faible — c'est le défaut de Kubernetes. CANARI : une petite fraction du trafic, puis des paliers, avec MESURE entre chaque — le plus prudent, mais inutile sans supervision.
Que change réellement un déploiement canari ?
Il n'EMPÊCHE PAS la panne : le bogue part en production et des utilisateurs le subissent. Il change son AMPLEUR (un quart du trafic au lieu de la totalité) et sa DURÉE (quelques minutes au lieu du temps de s'apercevoir puis de reconstruire). C'est le renversement du bloc : on ne cherche plus à empêcher tout incident, hors d'atteinte, mais à réduire le RAYON D'IMPACT et le TEMPS DE RÉTABLISSEMENT — les deux derniers indicateurs DORA.
Quelles sont les trois exigences d'un retour arrière, et le point dur ?
1) IL DOIT ÊTRE TESTÉ régulièrement hors incident : un mécanisme jamais exécuté ne fonctionne pas le jour venu, et ce jour-là personne n'est en état de le déboguer. 2) IL DOIT ÊTRE PLUS RAPIDE que la correction en avant : quelques secondes contre une demi-heure sous pression — on revient d'abord, on diagnostique à froid. 3) LE POINT DUR EST LA BASE : le code revient, les données non. Parade : la migration COMPATIBLE EN AVANT en trois étapes, et jamais de suppression ou de renommage dans le même déploiement que le code qui en dépend.
Quand une porte de validation manuelle est-elle légitime ?
Trois cas : exigence RÉGLEMENTAIRE, fenêtre de déploiement contrainte par le MÉTIER, ou décision COMMERCIALE sur la date de mise à disposition. Elle est du théâtre quand la personne qui valide n'a pas les MOYENS DE DÉCIDER : cliquer « approuver » sans tableau de bord ni critère explicite ne réduit aucun risque, cela ajoute un délai et une signature. Si la porte sert à rassurer, la question est de savoir ce qui manque au dispositif — c'est presque toujours de la mesure.