Chapitre 13.4 — Stratégies de release
⏱️ TL;DR — Déployer en écrasant le dossier de prod, c’est deux problèmes : pendant la copie, le site sert un état incohérent (downtime), et si la nouvelle version est buggée, tu n’as aucun moyen simple de revenir en arrière. La solution éprouvée (façon Capistrano/Deployer) : ne jamais écraser. Chaque déploiement crée un dossier
releases/<timestamp>neuf, on y prépare tout (code + dépendances + build), on le relie aux fichiers persistants (shared/: le.env, les uploads), puis on bascule un lien symboliquecurrentvers cette nouvelle release — une opération atomique, instantanée, sans coupure. On garde les N dernières releases : revenir en arrière, c’est repointercurrentsur la précédente, en une seconde. On survole blue-green et rolling, on traite les migrations de base (ordre, rétrocompatibilité) et le healthcheck avant bascule. C’est ce chapitre qui rend le déploiement pro.
🎯 Objectifs
- Comprendre pourquoi écraser le dossier de prod cause downtime et rend le rollback impossible.
- Mettre en place le schéma releases + symlink
current+shared/. - Réaliser une bascule atomique du symlink et un rollback instantané.
- Situer blue-green et rolling par rapport aux releases symlink.
- Gérer les migrations de base pendant un déploiement (ordre, rétrocompatibilité).
- Placer un healthcheck au bon endroit : avant la bascule.
Le problème : écraser, c’est casser
La façon naïve de déployer une nouvelle version, c’est de mettre à jour le dossier de prod : git pull, npm ci, npm run build, dans /var/www/formacampus. Deux failles.
Le downtime. Pendant que npm ci réécrit node_modules et que npm run build régénère le build, le dossier est dans un état intermédiaire : des fichiers de l’ancienne version, d’autres de la nouvelle, un build à moitié écrit. Si un visiteur arrive à ce moment, il tombe sur une erreur. Le site n’est pas « en cours de mise à jour », il est cassé pendant quelques secondes à quelques minutes.
Le rollback impossible. La nouvelle version a un bug grave. Tu veux revenir à la précédente… mais elle n’existe plus : tu l’as écrasée. Il faut re-checkout l’ancien commit, re-npm ci, re-build — plusieurs minutes de stress pendant que la prod est en carafe. Or, en incident, la seule chose qui compte, c’est de revenir vite à ce qui marchait.
⚠️ Piège — Déployer sans plan de rollback. Tout le monde se concentre sur « comment mettre la nouvelle version en ligne » et personne sur « comment revenir si elle est mauvaise ». Résultat : le jour de l’incident, on débogue en prod sous pression, on aggrave, on improvise. Un déploiement pro se juge autant sur sa capacité à avancer que sur sa capacité à reculer — et reculer doit être plus rapide qu’avancer.
La solution : releases + symlink
L’idée tient en une phrase : on n’écrase jamais, on ajoute à côté, et on bascule un pointeur. Concrètement, l’arborescence de déploiement ressemble à ça :
Trois éléments :
releases/— un dossier par déploiement, nommé par un horodatage (ex.releases/20260715_142230). Chaque release est complète et figée : le code de ce déploiement, ses dépendances, son build. Elles ne se touchent pas entre elles.current— un lien symbolique qui pointe vers la release active. C’est lui que Nginx et le service systemd visent (/var/www/formacampus/current). Le trafic passe toujours parcurrent, sans savoir vers quelle release il pointe.shared/— les fichiers qui doivent survivre aux déploiements et être communs à toutes les releases : le.env(les secrets), les uploads des utilisateurs, parfois des caches ou des logs. Chaque release y accède par un lien versshared/, au lieu d’avoir sa propre copie.
Le déroulé d’un déploiement devient :
# Variables (dans le vrai déploiement, elles viennent de deploy.sh)
BASE=/var/www/formacampus
TS=$(date +%Y%m%d_%H%M%S) # horodatage unique de cette release
REL="$BASE/releases/$TS"
# 1. Créer la nouvelle release à partir de l'artefact (rsync depuis la CI, ou git)
mkdir -p "$REL"
rsync -a ./build/ "$REL/" # on POSE la nouvelle version À CÔTÉ des autres
# 2. Relier les fichiers persistants partagés (jamais dans la release elle-même)
ln -sfn "$BASE/shared/.env" "$REL/.env"
ln -sfn "$BASE/shared/uploads" "$REL/uploads"
# 3. Healthcheck AVANT bascule : la nouvelle release démarre-t-elle bien ?
# (on teste la release sur un port temporaire — voir plus bas)
# 4. BASCULE ATOMIQUE : on repointe current vers la nouvelle release
ln -sfn "$REL" "$BASE/current" # -f force, -n ne suit pas le lien existant
# 5. Recharger le service pour qu'il serve la nouvelle current
sudo systemctl reload formacampus-web
# 6. Nettoyer : ne garder que les N dernières releases (rollback + disque maîtrisé)
ls -1dt "$BASE"/releases/*/ | tail -n +6 | xargs -r rm -rf # garde les 5 plus récentesLe cœur, c’est l’étape 4. Repointer un lien symbolique est une opération atomique au niveau du système de fichiers : à un instant T, current pointe vers l’ancienne release ; à l’instant T+1, vers la nouvelle. Il n’y a pas d’entre-deux. Aucune requête ne voit un état incohérent. Le reload (étape 5) prend ensuite la nouvelle cible en compte sans couper les connexions en cours. Zéro downtime.
💡 Réflexe — Mets tout ce qui est mutable ou secret dans
shared/, jamais dans une release. Le.env(secrets), les fichiers uploadés par les utilisateurs, les caches persistants : ils doivent survivre au déploiement suivant et être communs à toutes les releases. Une release doit être jetable : la supprimer ne doit rien détruire d’important. Si supprimer une vieille release efface des uploads, c’est que tu as mis les uploads au mauvais endroit.
Le rollback instantané
Voilà la récompense de toute cette mécanique. La nouvelle release est buggée ? Tu ne répares pas dans l’urgence : tu repointes current sur la release précédente, qui est toujours là, intacte.
# Lister les releases, de la plus récente à la plus ancienne
ls -1dt /var/www/formacampus/releases/*/
# Repointer current sur la release PRÉCÉDENTE (la 2e de la liste)
PREV=$(ls -1dt /var/www/formacampus/releases/*/ | sed -n 2p)
ln -sfn "$PREV" /var/www/formacampus/current
sudo systemctl reload formacampus-web
# En une seconde, la prod re-sert la version qui marchait. Incident contenu.C’est ça, « rollback en un clic » : pas de rebuild, pas de re-checkout, juste rebasculer un pointeur sur un dossier déjà prêt. C’est plus rapide que le déploiement lui-même — exactement ce qu’on veut en situation de crise.
🧭 Sur FormaCampus — Le déploiement de FormaCampus adopte ce schéma. La CI (13.3) build le front Next.js, puis rsync l’artefact dans une nouvelle
releases/<timestamp>sur le VPS Ubuntu. Ledeploy.shrelie le.envet le dossier d’uploads depuisshared/, vérifie que la release démarre (healthcheck), bascule le symlinkcurrent, etreloadle service. Les 5 dernières releases sont conservées. Quand une mise à jour de l’API a introduit une régression un mardi soir, l’astreinte a fait un seulln -sfnvers la release précédente : la prod était réparée en moins d’une minute, sans rien rebuilder, le temps de corriger tranquillement le bug. Le rollback « en un clic » promis en 13.1 est ici, concret.
Blue-green et rolling, en comparaison
Les releases + symlink sont la stratégie idéale pour un serveur (le cas de ce cours). Deux autres approches existent, surtout utiles à plus grande échelle — on les situe, sans les détailler :
- Blue-green — tu fais tourner deux environnements complets en parallèle : « blue » (l’actuel, qui sert le trafic) et « green » (la nouvelle version, déployée à côté et testée à froid). Quand green est prêt et validé, on bascule tout le trafic de blue vers green (au niveau du load balancer ou du reverse proxy). Rollback = rebasculer sur blue. C’est le même principe que le symlink, mais à l’échelle d’environnements entiers (deux copies de l’app, parfois deux serveurs). Coût : il faut faire tourner deux fois l’infra pendant la bascule.
- Rolling — avec plusieurs instances de l’app (plusieurs serveurs ou conteneurs), on les met à jour une par une : on retire une instance du pool, on la met à jour, on la remet, puis la suivante. Le service reste disponible en continu car il reste toujours des instances actives. C’est la stratégie des orchestrateurs (Kubernetes, docker swarm). Elle suppose que plusieurs versions coexistent un instant — d’où l’importance de la rétrocompatibilité (voir les migrations).
| Stratégie | Idéale pour | Rollback | Coût |
|---|---|---|---|
| Releases + symlink | 1 serveur (ce cours) | repointer current | quasi nul |
| Blue-green | bascule d’environnement entier | rebasculer sur l’ancien | double infra temporaire |
| Rolling | flotte d’instances | remettre l’ancienne image | orchestrateur requis |
Pour un VPS unique, releases + symlink couvre 100 % du besoin. Blue-green et rolling deviennent pertinents quand tu passes à plusieurs machines (scaling horizontal, Partie 15).
Les migrations de base pendant un déploiement
C’est le point qui casse le plus de déploiements « propres ». Ton code change, mais la base de données aussi (nouvelle colonne, nouvelle table). Deux dangers :
- Si tu bascules le nouveau code avant d’avoir migré la base, il plante sur une colonne qui n’existe pas encore.
- Pendant un rollback ou une stratégie où deux versions coexistent (rolling), l’ancien code doit continuer à tourner sur la base déjà migrée.
La règle d’or : rendre les migrations rétrocompatibles, pour que l’ancien et le nouveau code fonctionnent sur le schéma migré, au moins le temps de la bascule.
- Ajouter une colonne/table : sûr, l’ancien code l’ignore. On migre avant de basculer le code.
- Renommer/supprimer une colonne : dangereux, car l’ancien code l’utilise encore. On procède en plusieurs étapes (expand/contract) : d’abord ajouter la nouvelle colonne et écrire dans les deux (déploiement 1), migrer les données, puis supprimer l’ancienne colonne dans un déploiement ultérieur (déploiement 2), quand plus aucun code ne l’utilise.
⚠️ Piège — Une migration destructive (renommer/supprimer une colonne) livrée en même temps que le code qui en dépend, sans étape intermédiaire. Si tu dois rollback, l’ancien code retombe sur une base déjà mutilée : la colonne qu’il attend a disparu. Ton rollback de code « en un clic » ne te sauve pas, parce que la base, elle, ne revient pas en arrière aussi facilement. Les migrations destructives se font en deux temps, jamais d’un bloc.
🔒 Sécurité — Les migrations tournent avec un accès en écriture à la base : ce compte a des droits puissants. Utilise un utilisateur de base dédié aux migrations si tu peux, sauvegarde toujours la base avant une migration (
pg_dump, Partie 11), et teste tes migrations sur une copie avant la prod. Une migration ratée sur des données réelles peut être irréversible — bien plus grave qu’un bug de code, que le rollback corrige.
Le healthcheck : vérifier avant de basculer
Basculer current vers une release qui ne démarre pas revient à mettre une panne en ligne d’un coup. D’où le healthcheck : avant la bascule, on vérifie que la nouvelle release répond correctement.
En pratique, on démarre la nouvelle release sur un port temporaire (ou une instance secondaire du service), on teste une URL de santé (une route légère qui renvoie 200 OK, ex. /healthz), et on ne bascule le symlink que si le healthcheck passe :
# La nouvelle release tourne temporairement sur le port 3001
curl -fsS http://127.0.0.1:3001/healthz || {
echo "Healthcheck ÉCHOUÉ — on ne bascule PAS, on garde l'ancienne current"
exit 1 # set -e propage : le déploiement s'arrête ici, prod intacte
}
# Healthcheck OK -> on peut basculer current en toute confianceAinsi, une release cassée n’atteint jamais le trafic : elle échoue au healthcheck, le script s’arrête (grâce à set -euo pipefail, 13.2), et current continue de pointer sur la version qui marche. Le déploiement échoue sans casser la prod — précisément l’objectif du pipeline idéal.
📚 La doc — Tu n’as pas à réécrire toute cette mécanique à la main : des outils l’implémentent en standard. Capistrano (monde Ruby, mais générique), Deployer (PHP/Symfony , donc pertinent pour l’API de FormaCampus) codifient exactement ce schéma
releases/,current,shared/, rollback et hooks. Les comprendre « à la main » comme ici te permet de les utiliser en connaissance de cause — ou de les reproduire en quelques lignes de bash quand tu veux rester léger.
Ce qu’il faut retenir
Écraser le dossier de prod cause du downtime et rend le rollback impossible. La parade : ne jamais écraser. Chaque déploiement crée une releases/<timestamp> neuve, reliée aux fichiers persistants de shared/ ; on bascule le symlink current de façon atomique (zéro downtime) après un healthcheck ; on garde les N dernières releases pour un rollback instantané (repointer current). Blue-green et rolling étendent l’idée à plusieurs machines. Les migrations se veulent rétrocompatibles et les destructives se font en deux temps. C’est le chapitre qui fait passer le déploiement d’« amateur » à « pro ».
✏️ Exercices
Exercice 1 — Explique la bascule atomique. Un collègue demande : « Pourquoi repointer un symlink current évite-t-il le downtime, alors que réécrire le dossier de prod le provoque ? » Réponds-lui clairement.
✅ Solution
Réécrire le dossier de prod met le site dans un état intermédiaire pendant plusieurs secondes/minutes : npm ci et npm run build mélangent anciens et nouveaux fichiers, un build à moitié écrit. Un visiteur qui arrive à ce moment voit une erreur. Repointer current est atomique : la nouvelle release est entièrement préparée à côté (dans releases/<timestamp>), et le changement de pointeur est instantané, sans état intermédiaire. À l’instant T, current vise l’ancienne ; à T+1, la nouvelle. Aucune requête ne voit un dossier « à moitié à jour ». Le trafic passe toujours par current, qui pointe toujours vers une release complète.
Exercice 2 — Migration risquée. Tu veux renommer la colonne email en contact_email. Un collègue propose de livrer, dans le même déploiement, la migration de renommage et le nouveau code qui utilise contact_email. Pourquoi est-ce dangereux, et comment ferais-tu ?
✅ Solution
C’est dangereux car un rollback deviendrait impossible sans casse : si le nouveau code pose problème et que tu repointes current sur l’ancienne release, cet ancien code cherche la colonne email… qui a été renommée en contact_email. La base ne « revient » pas en arrière avec le symlink. On procède en expand/contract, en plusieurs déploiements : (1) ajouter contact_email sans supprimer email, faire écrire le code dans les deux (ou copier les données) ; (2) une fois le nouveau code stable en prod et les données migrées, supprimer email dans un déploiement ultérieur, quand plus aucun code déployé (ni rollbackable) ne l’utilise. Toujours sauvegarder la base avant toute migration.
🧠 Quiz de révision
1. À quoi servent respectivement releases/, current et shared/ ?
releases/, current et shared/ ?releases/ contient un dossier complet et figé par déploiement (horodaté). current est un lien symbolique vers la release active — c’est lui que Nginx et le service visent. shared/ regroupe les fichiers persistants et communs à toutes les releases (le .env, les uploads utilisateurs), reliés dans chaque release par un lien, pour qu’ils survivent aux déploiements.
2. Pourquoi la bascule du symlink est-elle « atomique » et sans downtime ?
Parce que repointer un lien symbolique est une opération instantanée au niveau du système de fichiers : il n’y a pas d’état intermédiaire entre « pointe vers l’ancienne » et « pointe vers la nouvelle ». La nouvelle release est entièrement prête avant la bascule, donc aucune requête ne tombe sur un dossier à moitié mis à jour. Le trafic bascule d’un coup, proprement.
3. Comment fait-on un rollback avec ce schéma, et pourquoi est-ce rapide ?
On repointe current sur la release précédente, qui est toujours présente dans releases/ (on garde les N dernières), puis on reload le service. C’est rapide car il n’y a rien à rebuilder ni re-télécharger : la version qui marchait est déjà prête sur le disque. Un seul ln -sfn suffit, plus rapide que le déploiement lui-même.
4. En quoi les stratégies blue-green et rolling diffèrent-elles des releases symlink ?
Blue-green fait tourner deux environnements complets (actuel + nouveau) et bascule tout le trafic de l’un à l’autre — même idée que le symlink mais à l’échelle d’environnements entiers, avec le coût d’une double infra. Rolling met à jour une par une plusieurs instances d’un pool, sans coupure, ce qui suppose un orchestrateur et la coexistence temporaire de versions. Les releases symlink suffisent pour un serveur ; les deux autres visent les flottes de machines.
5. Pourquoi une migration destructive doit-elle se faire en deux temps ?
Parce que supprimer/renommer une colonne en même temps que le code qui en dépend rend le rollback dangereux : l’ancien code, s’il redevient actif, retombe sur une base déjà modifiée où la colonne attendue a disparu. On procède en expand/contract : d’abord ajouter la nouvelle structure sans casser l’ancienne (déploiement 1), puis supprimer l’ancienne dans un déploiement ultérieur, quand plus aucun code déployé ne l’utilise. Et on sauvegarde la base avant toute migration.
Chapitre suivant : 13.5 — Gérer les secrets — le dernier maillon : ne jamais committer un secret, savoir où vivent le .env serveur, les secrets GitHub et ceux de systemd, et comment les faire tourner en toute sécurité.