~/luccin / blog

Déployer n'est pas livrer

Luccin Masirika · 27 août 2026 · 15 min

Pourquoi vos branches d'environnement vous ralentissent, comment les équipes les plus rapides du monde s'en sont débarrassées, et ce que ça change concrètement dans votre code. L'article progresse en trois parties, de plus en plus profondes : les experts sont servis à la fin.

Un interrupteur éteint posé sur la ligne du trunk, des branches emmêlées qui s'en échappent

Il est 18 h 47, un jeudi

La feature est prête depuis neuf jours. Testée, validée, approuvée. Mais elle ne partira pas en production ce soir, ni demain : elle est coincée sur la branche staging, mélangée à deux autres features qui, elles, ne sont pas prêtes. Impossible de livrer l'une sans embarquer les autres. Alors on attend. Ou pire : on tente un cherry-pick chirurgical vers master, en croisant les doigts pour que les commits ne se marchent pas dessus.

Si cette scène vous parle, votre équipe utilise des branches d'environnement : une branche dev ou staging qui accumule les features en test, une branche master qui représente la production, et des merges rituels entre les deux. C'est le modèle le plus répandu du monde. C'est aussi celui que Google, Meta, Netflix et toutes les équipes mesurées comme les plus performantes ont abandonné il y a quinze ans.

Le problème n'est pas votre équipe. Le problème est qu'une branche Git est un très mauvais outil pour répondre à la question « qu'est-ce qui est activé, pour qui, en ce moment ? »

L'anatomie du mal

Le modèle à branches d'environnement repose sur une équivalence séduisante, une branche = un environnement = un état du produit, qui se dégrade pour trois raisons mécaniques :

Ce que disent les chiffres

Le programme de recherche DORA (Google Cloud) mesure depuis 2014 la performance de milliers d'équipes, et son résultat le plus contre-intuitif est solide : les équipes qui déploient le plus souvent sont aussi celles qui ont le moins d'incidents. La sécurité ne vient pas de la prudence des gros déploiements espacés ; elle vient de la petitesse de chaque changement.

Les solutions, dans l'ordre où on les découvre

Il n'y a pas un modèle à adopter d'un bloc, et c'est le point que la plupart des articles ratent. Le chemin passe par des solutions intermédiaires, et chacune est un état stable où une équipe peut s'installer durablement. Ce qui pousse à la suivante n'est pas la mode ; c'est un échec précis, qu'on va raconter à chaque fois.

Une seule branche de vérité

La première réponse à la dérive des branches d'environnement ne demande aucun outillage nouveau, seulement trois règles :

La règle de livraison est alors simple : on ne merge que ce qu'on est prêt à livrer. La validation se fait avant le merge, sur la branche de feature. C'est le « GitHub Flow » classique, et il résout déjà le cœur du problème : plus de dérive, ce qu'on teste est ce qu'on livre.

Vous pouvez vous arrêter ici si vous livrez une feature à la fois et que la validation est rapide. Beaucoup de petites équipes vivent très bien avec ce modèle, et n'ont besoin de rien de plus.

Trois mois plus tard, mardi, 9 h 05. Le tableau des merge requests affiche quatre features « en attente de validation ». La QA n'a pas fini la première. La quatrième a déjà deux conflits avec la branche principale ; son auteur passera la matinée à rebaser du code validé la semaine dernière. En rétrospective, quelqu'un propose : « et si on avait une branche commune où on merge tout ce qui est à tester ? » Silence gêné. C'est la branche staging qui frappe à la porte, sous un faux nom.

L'échec est instructif : le problème n'est pas de savoir poser le code à tester, c'est que la validation bloque le merge. Tant qu'une seule scène de test existe, les features font la queue devant, ou s'y mélangent. La réponse n'est pas une branche commune ; c'est de donner à chaque feature sa propre scène.

Le même artefact, des scènes jetables

Deux mécanismes s'ajoutent, toujours sans toucher au code applicatif :

Concrètement, en GitLab CI (l'équivalent existe chez GitHub Actions et les autres), l'essentiel tient en trois jobs :

# 1. Un seul build, adressé par commit : c'est lui qui voyagera partout
build:
  script:
    - docker build -t "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA" .
    - docker push "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA"

# 2. Une review app par merge request, détruite à sa fermeture
review:
  script:
    - deploy --image "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA"
  environment:
    name: review/$CI_COMMIT_REF_SLUG
    url: https://$CI_COMMIT_REF_SLUG.review.example.com
    on_stop: stop_review
  rules:
    - if: $CI_MERGE_REQUEST_IID

# 3. La prod reçoit l'image déjà validée : un retag, jamais un rebuild
promote_prod:
  script:
    - crane tag "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA" prod
  rules:
    - if: $CI_COMMIT_TAG =~ /^prod-/

C'est la réponse « sans flags » au besoin qui pousse les équipes vers les branches d'environnement : tester plusieurs features en parallèle. Chacune a son environnement, aucune ne bloque les autres.

Vous pouvez vous arrêter ici si déployer et livrer peuvent rester le même geste chez vous : pas besoin d'activer une feature client par client, ni de l'éteindre en urgence sans redéployer. C'est le régime de croisière de très nombreuses équipes produit, et il est déjà excellent.

Six mois plus tard, vendredi, 16 h 40. La release de mardi contenait quatre features, validées une à une sur leurs environnements éphémères, irréprochables. Mais l'une d'elles touche au même cache que le module de facturation, et sous le trafic réel du vendredi, ça casse. Le seul levier disponible est brutal : redéployer l'image précédente, en emportant les trois features innocentes.

Pendant l'incident, un commercial passe une tête : « on peut activer la nouveauté seulement pour le client pilote qui l'a demandée ? » Non. On ne peut pas. Le déploiement est le seul interrupteur de la maison, et il éteint tout en même temps.

Deuxième échec instructif : la validation isolée ne prouve pas le comportement en conditions réelles, et le déploiement est un interrupteur trop gros, qui allume et éteint tout d'un bloc. Il manque un levier plus fin que le déploiement. C'est exactement ce qu'est un feature flag.

L'interrupteur : découpler livrer de déployer

La dernière marche renverse la logique : on merge avant que la feature soit finie, masquée par un interrupteur, et c'est l'interrupteur qui livre. Déployer et livrer deviennent deux gestes distincts. Déployer, c'est pousser du code en production, éteint ; c'est le geste technique, fréquent, automatisé, sans enjeu produit, qui peut arriver dix fois par jour. Livrer, c'est allumer : le geste produit, une feature, un client, un instant choisi, réversible en secondes, sans redéploiement.

Au fond, un flag est trivial : une condition qui lit une configuration au moment de l'exécution.

if (flags.isEnabled('new_checkout')) {
    renderNewCheckout();
} else {
    renderLegacyCheckout();
}

Toute la valeur tient dans une question : d'où vient la valeur du flag ? Trois réponses, du plus simple au plus puissant, chacune légitime à son échelle :

NiveauSource des valeursUn « flip » c'est…Granularité
1. Config runtimeUn fichier de config chargé au démarrageChanger une variable du déploiement, redémarrer le podPar environnement
2. BackendUn objet feature_flags servi par votre APIUn appel d'API ou un champ en basePar client, par compte
3. Service dédiéUnleash, LaunchDarkly, Flagsmith…Un clic dans une UI, effectif en secondesPar utilisateur, par pourcentage, par règle

Un piège spécifique au frontend mérite d'être nommé : dans une SPA, process.env.MA_VARIABLE n'est pas une variable d'environnement. Elle est substituée au moment du build et cuite dans le bundle. Un flag lu ainsi exige un build par environnement, ce qui casse « même artefact partout ». La solution standard : un fichier config.js non bundlé, généré au démarrage du conteneur et chargé par index.html avant l'application. Le bundle reste identique partout ; seule cette config diffère.

Reste le cycle de vie. Un flag de release n'est pas une décoration permanente : c'est un échafaudage, avec une date de démontage prévue dès sa pose.

  1. Naissance : la feature démarre, le flag est créé, éteint partout. Le premier commit peut merger dans le trunk le jour même.
  2. Développement : allumé en dev, où toutes les features en cours cohabitent.
  3. Validation : allumé en staging, seul : on isole ce qu'on évalue.
  4. Livraison : allumé en prod, progressivement si l'enjeu le mérite : équipe interne, client pilote, pourcentage, tout le monde.
  5. Mort : quelques semaines après la généralisation, le flag et l'ancien chemin de code sont supprimés. Cette étape n'est pas optionnelle ; on y revient dans la partie qui suit.

Le chemin est le plan de migration

Le plan de transition est déjà écrit : c'est le chemin qu'on vient de parcourir, dans le même ordre, en ne décommissionnant l'ancien mécanisme que quand plus rien ne le lit ; une branche morte gardée « au cas où » redeviendra un entrepôt. Pour une équipe de 5 à 20 personnes, comptez un à deux trimestres. L'étape difficile n'est jamais technique : c'est le raccourcissement des branches, qui oblige à découper le travail autrement.

Deux disciplines font tenir le système :

Les objections que vous allez entendre

Cette migration se gagne en réunion d'équipe avant de se gagner dans Git. Trois phrases reviennent dans toutes les équipes ; autant arriver avec les réponses.

« On va merger du code pas fini dans master ?! » On merge du code fini au sens du pipeline : compilé, testé, revu. Ce qui n'est pas fini, c'est la feature, et elle est éteinte. Le vrai danger est ailleurs : une branche de trois semaines, c'est du code que personne n'exécute contre le reste du système ; le code éteint en production, lui, est intégré et compilé avec tout le monde, chaque jour.

« La QA doit valider avant la prod. » Elle valide toujours, et mieux qu'avant : l'objet change. Au lieu de valider une release, un paquet de features enchevêtrées, elle valide une feature, seule, sur sa review app ou derrière son flag en staging. Le contrôle ne disparaît pas ; il se déplace du paquet vers l'unité.

« Des if partout dans le code, c'est sale. » Un flag de release bien posé, c'est un point de divergence, à la frontière du module, avec une date de mort. Une branche longue est aussi un if : simplement il est invisible, il n'est jamais exécuté en continu, et il se paie d'un coup au merge final. La dette de flags se gère, on y vient ; la dette de merge se subit.

Ce qui se passe à grande échelle

Tout ce qui précède fonctionne tel quel pour une équipe. Voici ce qui casse quand on grossit, et les mécanismes que les grandes organisations ont construits en réponse.

La dette de flags est une dette exponentielle

Chaque flag vivant double le nombre de chemins d'exécution possibles : dix flags actifs, c'est en théorie 1024 combinaisons d'états, dont votre suite de tests n'en couvre qu'une poignée. Les incidents de flags sont un genre à part entière ; le plus célèbre reste Knight Capital en 2012, où du code mort réactivé par un flag recyclé a coûté 460 millions de dollars en 45 minutes.

Les défenses standard :

Rollout progressif : le bucketing, pas la loterie

« Allumer pour 10 % des utilisateurs » ne peut pas être un Math.random() par requête : un même utilisateur verrait la feature apparaître et disparaître entre deux pages. Les SDK de flags font du bucketing déterministe : hash(user_id + flag_name) % 100, comparé au pourcentage cible. Le résultat est stable par utilisateur, indépendant entre flags grâce au nom dans le hash, et passer de 10 à 30 % conserve les 10 % déjà exposés. C'est aussi la brique de base de l'A/B testing : le flag renvoie une variante au lieu d'un booléen, et l'analyse compare les cohortes.

Le schéma de base de données ne connaît pas les flags

Le code a deux chemins (flag on, flag off), mais les deux parlent à la même base. Une migration destructive, renommer une colonne, changer un type, casse le chemin éteint. La réponse standard est le pattern expand-contract : on ajoute la nouvelle colonne sans toucher à l'ancienne, le code écrit dans les deux et lit l'ancienne (expand) ; on bascule la lecture derrière le flag et on backfille l'historique (migrate) ; et quand le flag est généralisé puis supprimé, on supprime l'ancienne colonne dans une release ultérieure (contract). Corollaire : chaque version du schéma doit supporter la version courante et la précédente du code, exigence de toute façon imposée par les déploiements progressifs.

Garder le trunk vert quand cinquante personnes mergent

À petite échelle, « pipeline vert avant merge » suffit. À grande échelle, deux MR vertes individuellement peuvent casser le trunk une fois combinées : le merge skew. D'où les merge queues (GitHub Merge Queue, GitLab Merge Trains) : chaque MR candidate est testée contre le résultat des merges qui la précèdent dans la file. Avec la sélection de tests par analyse d'impact, c'est ce qui permet à un trunk de tenir des milliers de commits par jour.

Observer les flips comme des déploiements

Quand livrer = allumer un flag, le flip devient l'événement à risque, et il mérite les honneurs d'un déploiement : un marqueur dans les dashboards, la liste des flags actifs comme dimension dans les erreurs et les traces, et idéalement des gardes-fous automatiques qui ramènent le flag à zéro si le taux d'erreur de la cohorte exposée dévie. Le rollback n'est plus un geste d'urgence, c'est une politique.

Les anti-patterns qui reviennent toujours

Quand ce modèle n'est pas le bon

L'honnêteté oblige à tracer les limites. Le trunk-based avec livraison continue suppose qu'on peut déployer à volonté, et ce n'est pas toujours vrai :

Par où commencer demain matin

Trois gestes, par ordre de rendement :

  1. Protégez votre branche principale et exigez le pipeline vert avant merge. Dix minutes de configuration, effet immédiat.
  2. Livrez votre prochaine feature derrière un flag, même un simple booléen dans une config runtime. Vous découvrirez les questions concrètes (où vivent les valeurs, qui flippe, quand on nettoie) sur un cas réel et sans risque.
  3. Mesurez votre fréquence de déploiement et votre délai commit → prod. Ce sont deux des quatre métriques DORA ; elles diront, dans six mois, si la migration a tenu ses promesses.

Ces trois gestes vous installent à la première étape du chemin. Les suivantes, unifier l'artefact, monter les review apps, poser les flags, se prendront une par une, quand leur échec fondateur se produira chez vous. Le plus dur n'est pas technique : c'est d'accepter que la sécurité vienne de la fréquence, pas de la prudence.


Pour aller plus loin