Skip to content

ADR 029 — Stratégie multi-environnements : dev/staging/prod sur cluster unique par namespace + overlays (2026-07-07)

Statut

Accepté (2026-07-07), pris en amont du code (action rétro Sprint 5 n°2 : « ADR stratégie multi-env AVANT tout code »). Cadre le socle du Sprint 6 (itérations It.0 → It.4 : fondations, isolation par namespace/overlays, data/DNS/secrets par env, promotion par MR, tests E2E + prod). Décisions couvertes : D1 (isolation namespace + overlays), D2 (promotion par MR overlay-driven), D4 (1 RDS multi-database), D5 (main reste archive). Reportés hors socle et notés en « Évolution possible » : Kargo (automatisation de la promotion), red team (tests offensifs, It.5), Kyverno (policy as code, It.6), vCluster (isolation control plane). La validation live (isolation effective, promotion de bout en bout, gate E2E) sera consignée en fin d'It.4, à la manière des ADR 025/026.

Contexte

Depuis le Sprint 3, l'application tourne sur un environnement unique : ce qui est déployé sur le cluster est la production de fait (api.devopsyouss.com). Toute modification (code, manifeste, montée de version) est donc éprouvée directement sur l'environnement qui sert les utilisateurs. Il n'existe aucun palier pour valider un changement avant qu'il n'atteigne la prod : pas de dev où casser sans conséquence, pas de staging iso-prod où jouer des tests de bout en bout. C'est le principal angle mort du projet à ce stade.

La promotion actuelle est mono-env : la CI construit l'image, la pousse sur ECR, écrit le tag dans k8s/base (write-back GitOps, ADR 013), ArgoCD réconcilie. Un seul tag, un seul destinataire. Le mécanisme est sain mais ne sait pas exprimer « cette version est bonne en dev, faisons-la monter en staging puis en prod ».

Contrainte structurante assumée : le projet dispose d'un seul cluster EKS, pour des raisons de coût (projet portfolio). Multiplier les clusters managés par environnement est hors budget. La question n'est donc pas « combien de clusters » mais « comment découper proprement 3 environnements dans un cluster, avec un chemin de promotion tracé, sans dégrader la sécurité ».

Deux dettes identifiées à la rétro Sprint 5 trouvent ici leur place : - les jobs infra start/stop peuvent entrer en collision avec le teardown programmé (2 récidives), à cadenasser (resource_group) avant les sessions live multi-env ; - la question du déploiement prod depuis main (#57, aujourd'hui branche d'archive) doit être tranchée par cette stratégie.

Décision 1 — Isolation par namespace + overlays Kustomize

On matérialise les trois environnements par trois namespaces par workload (fastapi-dev / fastapi-staging / fastapi-prod, idem frontend-*), chacun alimenté par un overlay Kustomize qui pointe sur le base commun et n'exprime que les différences d'environnement (namespace cible, host DNS, nombre de replicas, ressources, chemin des secrets). Le code déployé est identique d'un env à l'autre ; seule la configuration diffère.

Arborescence cible :

k8s/
  base/            # manifests fastapi communs (inchangé)
  frontend/        # manifests frontend communs (inchangé)
  overlays/
    fastapi/{dev,staging,prod}/     # patchs par env (ns, host, replicas, ressources)
    frontend/{dev,staging,prod}/

Instanciation côté ArgoCD : un ApplicationSet avec un générateur de liste [dev, staging, prod] génère les Applications par env, au lieu d'écrire et maintenir 3 Application quasi identiques à la main. Une seule source de vérité, ajouter un env = ajouter une entrée. (Alternative écartée : 3 Application dupliquées, plus verbeux et sujet au copier-coller divergent.)

Isolation apportée (chaque brique = une story d'It.1), du plus fort au plus faible : - NetworkPolicy par namespace : default-deny (déjà le pattern dans base) + autorisations explicites intra-env uniquement. Un pod de fastapi-dev ne doit atteindre ni la DB des autres envs ni leurs pods. C'est la barrière qui rend l'isolation inter-env réelle et non déclarative. - ResourceQuota par namespace : plafonne la somme des limits/requests d'un env → un env ne peut pas affamer les autres sur le nœud partagé. C'est précisément le ResourceQuota reporté par l'ADR 025 (« reporté Sprint 6 ») : il est livré ici. - LimitRange par namespace : valeurs par défaut à l'admission (plus de pod BestEffort). Créé au bootstrap Ansible, avant les pods, comme établi par l'ADR 025 (un LimitRange n'agit qu'à l'admission). - RBAC par namespace : des rôles scopés à un env, pour que la manipulation d'un env ne touche pas les autres (et discipliner le kubectl de session live). - Pod Security Admission restricted par namespace (déjà en place sur frontend et fastapi), reconduit sur chaque env.

Honnêteté sur le niveau d'isolation. Cette approche est une isolation « soft » : les trois environnements partagent le même control plane et les mêmes nœuds (même noyau). Un cluster-admin voit tout, et un incident au niveau du control plane ou d'un nœud a un blast radius commun aux trois envs. On l'assume : le coût d'un cluster EKS managé par environnement est injustifié pour un projet portfolio, et l'isolation soft bien configurée (NetworkPolicy + Quota + RBAC + PSA) couvre le besoin réel — se donner un dev où casser et un staging où valider avant la prod. Le cran d'isolation supérieur (vCluster : control plane virtuel par env) est documenté comme évolution (voir « Évolution possible »), pas retenu ici pour ne pas empiler une couche à opérer avant d'avoir stabilisé le multi-env.

Décision 2 — Promotion par MR, modèle overlay-driven

Où vit la version déployée

Le tag d'image quitte base pour devenir un réglage par overlay : chaque env fixe son propre images[].newTag (transformer Kustomize). base ne porte plus de tag « vivant ». Conséquence : les trois environnements peuvent tourner sur trois versions différentes en même temps (dev en avance, prod stable), ce qui est tout l'intérêt d'un pipeline de promotion.

Le flux

  • dev = automatique. Le job CI update-image-tag (write-back GitOps existant, ADR 013) est reciblé : au lieu d'écrire dans base, il écrit le nouveau tag dans overlays/fastapi/dev à chaque merge sur develop. ArgoCD déploie dev tout seul. C'est le seul env en continuous deployment.
  • staging puis prod = promotion par MR. Promouvoir = ouvrir une MR qui copie le tag validé de l'overlay amont vers l'overlay aval (devstaging, puis stagingprod). Rien d'autre ne change dans la MR : même image, déjà construite et scannée, on ne fait que la faire avancer.
flowchart LR
    ci["merge sur develop<br/>build + scan + push ECR"] -->|write-back auto| dev["overlays/fastapi/dev<br/>newTag = sha"]
    dev -->|"MR de promotion"| stg["overlays/fastapi/staging<br/>newTag = sha"]
    stg -->|"MR de promotion<br/>(review obligatoire)"| prod["overlays/fastapi/prod<br/>newTag = sha"]
    dev -.sync.-> ad[(ArgoCD dev)]
    stg -.sync.-> as[(ArgoCD staging)]
    prod -.sync.-> ap[(ArgoCD prod)]

La gate de promotion

Dans le socle (It.0→It.4), la promotion vers prod est gardée par deux mécanismes : - protection de branche + review obligatoire sur la MR de promotion prod (approbation explicite, un humain valide la montée) ; - E2E staging vert comme condition (It.4) : on ne promeut vers prod que si les tests de bout en bout sur staging sont passés.

Cette gate est manuelle par choix pédagogique (on voit toute la mécanique). Son automatisation (verification qui bloque la promo si les tests échouent) est le rôle de Kargo, retenu comme itération suivante hors socle (voir « Évolution possible »).

Pourquoi PAS « une branche par environnement »

La tentation classique est de faire develop = dev, une branche staging, main = prod, et de promouvoir en mergeant d'une branche à l'autre. On l'écarte explicitement, c'est un anti-pattern GitOps : - les branches divergent dans le temps (un hotfix sur main qui n'est pas dans staging, de la config qui dérive) ; - la promotion devient un merge bruyant mêlant code ET config d'env, au lieu d'un simple changement de tag ; - on perd la lisibilité : impossible de voir d'un coup d'œil « quelle version tourne où » sans comparer des branches.

Le modèle overlay-driven garde une seule branche de config (develop) où les trois envs coexistent comme trois dossiers. La version du code est la même partout, seul le tag par overlay distingue les envs. « Quelle version tourne où » se lit dans trois fichiers, dans le même commit.

Sort de main (#57)

Cette décision tranche #57 : main reste une branche d'archive/release, sans déploiement associé. La prod, c'est overlays/fastapi/prod sur develop, pas un déploiement « depuis main ». On ne réintroduit pas de déploiement branché sur main (ce serait retomber dans le branch-per-env).

Décision 3 — Data / DNS / secrets par environnement

Base de données — 1 RDS, 3 databases

On garde une seule instance RDS et on y crée trois databases (app_dev, app_staging, app_prod), chacune avec son propre utilisateur PostgreSQL dont les droits (GRANT) ne portent que sur sa database. Compromis budget assumé face à trois instances RDS séparées (le coût d'une instance managée par env est injustifié ici).

Honnêteté : c'est le point le moins isolé de la stratégie. La RDS étant managée hors cluster et partagée, tous les envs joignent le même endpoint ; la NetworkPolicy in-cluster ne peut donc pas distinguer un env d'un autre au niveau réseau. La barrière réelle entre environnements est au niveau base : databases distinctes + utilisateurs distincts + GRANT scopés (l'utilisateur de dev ne peut pas lire/écrire app_prod). Les données de test de staging ne peuvent donc pas contaminer prod. Une isolation plus forte (instances séparées, ou security groups par env) est notée en évolution.

DNS — sous-domaines par env

Chaque env expose ses propres hosts via ExternalDNS (Cloudflare, déjà en place) : - API : dev.api.devopsyouss.com, staging.api.devopsyouss.com, api.devopsyouss.com (prod) - Frontend : dev.app.devopsyouss.com, staging.app.devopsyouss.com, app.devopsyouss.com (prod)

Le host est un patch d'overlay (HTTPRoute). Le certificat wildcard *.devopsyouss.com (DNS-01, déjà émis) couvre tous ces sous-domaines sans émission supplémentaire.

Secrets — chemins ASM séparés + IRSA scopé

Chaque env lit ses secrets depuis un chemin AWS Secrets Manager dédié (/fastapi/dev/*, /fastapi/staging/*, /fastapi/prod/*) via un ExternalSecret propre à l'overlay (credentials DB de la bonne database, etc.).

L'isolation ici est réelle et forte : le ServiceAccount de chaque env assume, via IRSA, un rôle IAM dont la policy n'autorise que son préfixe de chemin ASM. Le pod de dev ne peut donc pas lire les secrets de prod, même s'il essayait. C'est la brique qui compense la faiblesse d'isolation de la RDS partagée : même endpoint DB, mais des credentials que seul l'env légitime peut obtenir.

Conséquences

  • On gagne un palier de validation : un changement passe par dev (auto) puis staging (iso-prod, tests E2E) avant d'atteindre prod. On arrête de tester en production.
  • Un merge sur develop ne touche plus jamais la prod directement : seul dev est auto-déployé. La prod n'avance que par une MR de promotion revue. Blast radius d'une régression réduit à dev.
  • « Quelle version tourne où » se lit dans Git : trois newTag dans trois overlays, dans le même commit. Traçabilité totale de la promotion (une MR = une montée).
  • Le ResourceQuota reporté par l'ADR 025 est livré ; chaque env est borné et ne peut pas affamer les autres sur le nœud partagé.
  • Charge opérationnelle en hausse : 3× les namespaces, quotas, NetworkPolicy, ExternalSecrets, Applications à opérer et surveiller. L'ApplicationSet et les overlays limitent la duplication, mais le nombre d'objets vivants triple.
  • Coût maîtrisé : 1 cluster, 1 RDS. L'augmentation reste marginale (compute des pods dev/staging, souvent dimensionnés bas).

Alternatives écartées (détail dans le cadrage import/cadrage-sprint6-multi-env.md) : - Cluster EKS par environnement : l'isolation de référence, écartée pour le coût (3 control planes managés) sur un projet portfolio. - vCluster : isolation control plane par env pour un coût moindre, mais une couche à opérer/debug de plus — reporté en évolution, pas avant que le multi-env namespace soit stable. - Branch-per-environment (develop/staging/main = envs) : anti-pattern GitOps (branches qui divergent, promotion = merge bruyant, lisibilité perdue). Remplacé par l'overlay-driven. - Kargo dès ce sprint : l'outil résout la promotion gatée, mais on veut d'abord manipuler la mécanique à la main — reporté en évolution. - ArgoCD Image Updater : bump auto du tag, contraire à une promotion contrôlée vers prod. Écarté.

Évolution possible

  • Kargo : automatiser la promotion dev → staging (après verification/E2E) et garder prod en promotion manuelle approuvée — le pattern d'entreprise « auto jusqu'à staging, gate humaine avant prod ».
  • vCluster : monter le cran d'isolation (control plane virtuel par env) si le besoin d'isolation forte se justifie.
  • Argo Rollouts : progressive delivery (canary/blue-green) + analyse automatique des métriques dans un env — testing in production avancé.
  • Kyverno : policy as code à l'admission (image signée, resource limits obligatoires, latest interdit) — sécurité préventive par env.
  • Red team / bilan sécurité : tests offensifs (Schemathesis, OWASP ZAP, validation NetworkPolicy inter-env, RBAC, CIS) exploitant justement la séparation par env — cadré section 4.3 du document de cadrage.
  • Isolation DB renforcée : instances RDS séparées ou security groups par env, si la RDS partagée devient un point dur.

Date : 2026-07-07 Sprint : 6 Issue : #133