Chapitres
Sur cette page
DOC-05 / Référence technique · Chapitre 09
Déploiement & infrastructure
L'asymétrie ship/deploy, le build sur l'hôte, le swap zéro-downtime et l'inventaire unique en base.
L'asymétrie ship vs deploy
Deux entrypoints vivent à la racine du dépôt. Ils ne font pas la même chose et n'ont pas le même propriétaire. Cette asymétrie est une loi du Synedre, pas une commodité.
./deploy | ./ship | |
|---|---|---|
| Cible | préprod (ou runtime live du mothership) | PRODUCTION |
| Propriétaire | l'intelligence (agent / worker / cascade), systématique | le Fondateur uniquement, geste manuel |
| Git | auto-commit du dirty, on reste sur la branche de préprod | fusion préprod → branche principale, push, puis retour sur la branche de préprod (garanti par un trap de sortie) |
| Garde-fou dirty | auto-commit silencieux, sauf si on est sur la branche principale (refus dur) | bloquant : refus si l'arbre de travail est sale, sauf option explicite |
| Vérification de dérive | une seule cible préprod câblée ; tout autre tenant est ignoré explicitement. Bypass possible (hotfix urgent) | dérive du mothership strictement bloquante (pas de bypass), plus la base de prod du tenant si la cible est connue |
| Smoke post-déploiement | une suite de smoke-tests sur les tenants | vérification de l'environnement du process (anti-faux-positif) puis smoke-tests |
| Migrations | — | affiche les .sql en attente du périmètre, non bloquant et jamais auto-appliqué |
| Clôture | — | propose la fermeture des chantiers en statut test |
La doctrine se résume ainsi :
./ship= PROD, jamais en autonome, exclusif au Fondateur. L'intelligence ne tape jamais./ship../deploy= toujours l'intelligence, systématiquement et sans demander — y compris sur le mothership où./deployreconstruit le runtime à chaud (pas de préprod).
┌─────────────┐ ./deploy <tenant> ┌──────────────┐
IA → │ branche │ ───────────────────→ │ préprod / │
│ préprod │ (auto, N fois) │ runtime live │
└─────────────┘ └──────────────┘
│
│ le Fondateur relit la préprod, valide
▼
┌─────────────┐ ./ship <tenant> ┌──────────────┐
Fond → │ fusion vers │ ───────────────────→ │ PRODUCTION │
│ principale │ (manuel uniquement) │ │
└─────────────┘ └──────────────┘
Exception founder. Certains tenants « fondateur » n'ont pas de préprod : leur site public vit directement sur un VPS. Pour eux, l'intelligence déclenche bien un déploiement, mais celui-ci pointe une cible de prod. La lecture naïve « deploy = jamais prod » ne tient donc pas universellement — ce qui compte, c'est qui a la main au moment de livrer.
Le pipeline de mise en production
La commande de production enchaîne, dans l'ordre, une séquence d'étapes dont chacune peut bloquer la suite :
- Parsing des options : bypass du garde-fou dirty, saut de la clôture post-ship.
- Garde-fou dirty bloquant : sortie en erreur si l'arbre de travail est sale.
- Trap de sortie : retour garanti sur la branche de préprod même en cas d'erreur (cicatrice : rester coincé sur la branche principale).
- Nettoyage des artefacts root laissés par d'anciens déploiements, qui bloqueraient sinon l'écriture.
- Vérification de dérive du mothership (bloquante, sans bypass) et de la base de prod du tenant si la cible est connue.
- Fusion préprod → branche principale, pull, fusion, push.
- Migrations : affichage seulement (voir plus bas).
- Audit des agents (non bloquant).
- Déploiement de prod : appelle le script du tenant.
- Smoke prod : vérification de l'environnement du process avant la suite de smoke-tests.
- Clôture post-ship : proposition de fermeture des chantiers en statut
test.
Asymétrie du bypass de dérive. Le déploiement accepte un bypass de la vérification de dérive (pour un hotfix urgent). La mise en production n'en a aucun : on ne ship jamais en prod sur un schéma de base divergent.
Migrations à la mise en production
La mise en production affiche les fichiers .sql en attente mais ne les applique jamais : aucun runner de migration automatique n'est branché. Le bloc est purement informatif (non bloquant) ; il imprime les commandes à exécuter à la main. Un mapping tenant → périmètre évite de mélanger les migrations du mothership et celles d'un tenant, et un cas spécial all agrège toutes les cibles en excluant les migrations déjà appliquées.
Le dispatcher piloté par YAML
Sous les deux entrypoints vit un dispatcher unique, piloté par un fichier deploy.yaml propre à chaque tenant. C'est le pipeline standard pour tous les tenants distants. Le mothership, lui, suit son propre chemin de self-deploy (voir plus bas).
Résolution de la configuration
Le dispatcher localise le deploy.yaml selon le tenant : la racine du mothership pour le vaisseau-mère, le dossier du tenant sinon ; une option de cible permet de choisir une variante (par exemple une préprod).
Étapes du dispatcher
- Parse des options : cible, indicateur « tout », nettoyage optionnel.
- Chargement + validation du YAML via un parseur Python qui émet des variables shell-quotées ; toute erreur abort le déploiement.
- Bannière de départ et init du chrono.
- Vérification de dérive (si déclarée).
- Hooks en arrière-plan (audit des agents).
- Build Nuxt.
- Seed i18n (si déclaré).
- Source maps (si déclaré).
- Pack en archive compressée.
- Push git en arrière-plan (si déclaré).
- Upload de l'archive vers la machine cible.
- Reload distant dispatché sur la variante : gestionnaire de process (reload graceful ou restart dur) ou conteneur (restart).
- Attente des hooks en arrière-plan.
- Health check.
- Bannière de fin.
Schéma de configuration
Le deploy.yaml est validé contre un schéma. Les sections couvrent le nom, le build (client local requis, environnement Node par défaut production), l'accès SSH (hôte requis, utilisateur par défaut, clé optionnelle), une dérive optionnelle, le seed i18n optionnel, le reload distant requis (variante parmi gestionnaire de process et conteneur), un health check (URL requise, délai d'attente max) et des hooks optionnels. Des garde-fous de cohérence interdisent de mélanger les champs propres au gestionnaire de process et ceux propres aux conteneurs. Exemple anonymisé :
name: exemple
build:
local_client: codemyshop/tenants/exemple
node_env: production
ssh:
host: <hôte du VPS>
remote:
variant: pm2
dir: <dossier de déploiement>
pm2_app: exemple-nuxt
health:
url: https://exemple.fr
hooks:
git_push: exemple
Build sur l'hôte, jamais sur le VPS
C'est l'invariant central des tenants distants : le VPS ne build jamais. Le bundle Nuxt est compilé sur le mothership (l'hôte), packé, uploadé, extrait puis rechargé sur la cible.
┌──────────────────── MOTHERSHIP (hôte) ────────────────────┐
│ 1. install des dépendances (delta racine monorepo) │
│ 2. smoke invariants (tests de forme d'URL) │
│ 3. build Nuxt → .output/ │
│ 4. pack → archive compressée │
└──────────────────────────┬──────────────────────────────────┘
│ upload (scp)
▼
┌──────────────────────── VPS ──────────────────────────────┐
│ 5. mv .output → .output_old (rollback prêt) │
│ 6. extraction de l'archive │
│ 7. reload du gestionnaire de process | restart conteneur │
│ 8. si online → purge .output_old sinon → rollback │
└──────────────────────────┬──────────────────────────────────┘
│
▼ poll HTTP 200 (jusqu'au délai max)
health check
Détails par étape :
- Build Nuxt : purge de
.output(et du cache si nettoyage demandé), install des dépendances, smoke-test bloquant sur l'invariant de forme d'URL produit, puis build avec retry sur nettoyage si le build incrémental échoue. - Pack : archive de
.outputavec un compresseur parallèle si disponible, sinon fallback systématique sur le compresseur standard. - Upload : copie de l'archive vers la machine cible, puis suppression locale.
- Reload conteneur : SSH, rotation
.output → .output_old, extraction, purge d'un module natif incompatible (cicatrice de tree-shaking), restart du conteneur, vérification du statutrunningsinon rollback. - Reload gestionnaire de process : reload graceful qui conserve les connexions en cours, avec support d'un utilisateur dédié et d'un lien symbolique optionnel pour les fichiers statiques.
- Health check : boucle de poll HTTP jusqu'au code 200 ou au délai d'attente max.
Le rollback est assuré par .output_old : conservé pendant le reload, purgé si le process repart, restauré sinon.
Anti-faux-positif sur l'environnement
Un health check HTTP
200ne suffit pas à valider un déploiement. Cicatrice vécue : un tenant redéployé sans ses variables de base de données rendait une page squelette vide mais valide — le smoke HTTP renvoyait200pendant deux jours alors que toutes les routes data-driven jetaient une 500 côté serveur. C'est pourquoi la mise en production lance, avant les smoke-tests, une vérification SSH de la présence des variables critiques dans l'environnement du process distant : OK, manquantes (bloquant), ou process injoignable (bloquant). Sans ce garde-fou, une 500 systématique passe inaperçue derrière unHTTP 200.
Le self-deploy du mothership
Le mothership ne passe pas par le dispatcher ni par le pattern build-hôte/upload. C'est un self-deploy local : le vaisseau-mère build à l'intérieur de son propre conteneur Docker.
- Pas de préprod : déployer le mothership pousse directement sur le runtime live du cockpit, sans la cérémonie git de la mise en production. C'est le sens de «
./deploy= rebuild live ». - Transfert local par copie d'une archive source minimale dans le conteneur — pas de transfert réseau, puisque le mothership et son runtime sont sur le même hôte. L'archive exclut le cache de build et les dépendances installées ; côté conteneur, le cache Nuxt est mis de côté puis restauré, ce qui fait du build un build incrémental et non un cold build — le vrai levier de perf.
- Build dans le conteneur : l'ancien serveur reste online pendant le build, d'où un swap quasi sans interruption (~7 s).
- Standalone : la config Nuxt du mothership n'étend plus le core, ce qui réduit fortement la taille de l'archive.
- Fast-path : une empreinte du lockfile permet de sauter l'install des dépendances quand il est inchangé.
- Variante accélérée opt-in : un build sur l'hôte avec swap atomique, pour gagner encore du temps.
La mise en production du mothership ajoute la cérémonie (fusion, push, dérive, audit, smoke prod, clôture) mais appelle in fine le même script de self-deploy.
L'inventaire VPS : une seule source de vérité
La topologie de l'infrastructure (adresses, utilisateurs SSH, conteneurs de base) ne vit jamais en dur dans le code ou la doc. Elle vit dans une table de la base du mothership, source de vérité unique. Une option de listing du déploiement dump cette table, formatée pour la console.
| Colonne | Rôle |
|---|---|
| identifiant tenant | clé logique du tenant |
| usage | production | staging | infra | legacy | audit |
| accès | adresse, domaine, utilisateur SSH, chemin de clé |
| base associée | conteneur, nom de base, utilisateur, nom de la variable portant le mot de passe (jamais la valeur) |
| runtime | conteneur web, présence des stacks |
| business | criticité, revenu récurrent, offre |
| codename de déploiement | codename pour la commande exacte |
| fédération client | clé étrangère vers la table client |
Doctrine : avant chaque déploiement ou mise en production, ouvrir la fiche client du hub pour copier la commande exacte. Les fiches d'archi en markdown peuvent dériver ; la base, elle, fait foi.
La doctrine des secrets
Principe : chaque secret a un seul fichier canonique ; la même clé à deux endroits est un bug. La hiérarchie va du plus local au plus partagé :
| # | Fichier | Périmètre | Tracké git |
|---|---|---|---|
| 1 | .env du tenant | un seul tenant (lu par son conteneur et sa config de déploiement) | non |
| 2 | .env du mothership | core du vaisseau-mère (cockpit) | non |
| 3 | .env.host du mothership | mothership + scripts hôte (cron, automates, SSH) | non |
| 4 | fichier d'env partagé hors-repo | cross-projet (clés des fournisseurs IA, clé de chiffrement, SMTP maître) | non (hors-repo) |
| 5 | templates .env.example | templates publics OSS | oui |
L'ordre de chargement va du plus partagé au plus local : en cas de doublon, le niveau le plus local gagne — raison de plus pour ne jamais déposer la même clé à deux niveaux.
Anti-leak P0. Aucun secret en clair dans un fichier tracké par git. Quand un fichier tracké doit référencer un secret, on cite uniquement le nom de la variable et le fichier d'env qui la porte — jamais la valeur. La config de déploiement et l'inventaire en base ne portent que des noms de variables.
Commit avant déploiement
Cicatrice P0 : un fix appliqué à la main mais jamais commité a été écrasé par un sync entre deux actions, cassant la prod plusieurs heures. La leçon : une modification locale non commitée ne survit que dans le build extrait et est écrasée par le checkout que fait le déploiement. Conséquence en règle dure : on commit toujours avant de déployer.
Un garde-fou scopé par cible (un fichier du mothership ne bloque pas le ship d'un tenant, et inversement) applique deux comportements :
- Déploiement : si l'arbre est sale → auto-commit silencieux + push en arrière-plan, sauf si on est sur la branche principale → refus dur.
- Mise en production : si l'arbre est sale → bloquant, sortie en erreur, message « commit d'abord puis relance ». Bypass réservé aux hotfix volontaires.
Un hook de fin de session bloque aussi la clôture tant que l'arbre de travail n'est pas propre. Règle racine : aucun travail terminé ne reste non commité — l'intelligence commit, le Fondateur ne tape jamais les commandes git d'ajout ou de commit.