Skip to content

Comprendre la promotion multi-environnements

Ce guide explique comment une version d'image avance de dev vers staging puis prod, sur un cluster unique découpé en trois environnements.

ADR lié : Stratégie multi-environnements (Décision 2). Voir aussi Comprendre ArgoCD et GitOps avec ArgoCD.


En une phrase

Le tag de l'image vit dans l'overlay de chaque env (pas dans base) : dev est bumpé automatiquement à chaque merge sur develop, staging et prod avancent uniquement par une MR qui recopie le tag validé de l'env amont. Aucun rebuild : on fait avancer la même image, déjà construite et scannée.

Où vit la version déployée

Chaque environnement a son propre newTag dans son kustomization.yaml :

k8s/overlays/
  fastapi/{dev,staging,prod}/kustomization.yaml   → images[].newTag
  frontend/{dev,staging,prod}/kustomization.yaml  → images[].newTag

« Quelle version tourne où » se lit donc dans trois fichiers, dans le même commit. Les trois envs peuvent tourner sur trois versions différentes en même temps (dev en avance, prod stable) : c'est tout l'intérêt d'un pipeline de promotion.

Le flux global

flowchart LR
    merge["merge sur develop<br/>(build + scan + push ECR)"]
    merge -->|"write-back CI<br/>(automatique)"| dev["overlays/*/dev<br/>newTag = sha"]
    dev -->|"MR de promotion<br/>(promote.sh dev staging)"| stg["overlays/*/staging<br/>newTag = sha"]
    stg -->|"MR de promotion + review<br/>(promote.sh staging prod)"| prod["overlays/*/prod<br/>newTag = sha"]
    dev -.->|sync| ad[(ArgoCD dev)]
    stg -.->|sync| as[(ArgoCD staging)]
    prod -.->|sync| ap[(ArgoCD prod)]

1. dev = automatique

À chaque merge sur develop, le job CI update-image-tag (write-back GitOps) réécrit le newTag de overlays/fastapi/dev et overlays/frontend/dev, commit en [skip ci] et push sur develop. ArgoCD détecte le diff et déploie dev seul. staging et prod ne bougent pas.

sequenceDiagram
    participant Dev as Développeur
    participant CI as CI (develop)
    participant Git as Git (overlays/dev)
    participant Argo as ArgoCD
    Dev->>CI: merge sur develop
    CI->>CI: build + scan + push ECR (deployed-sha)
    CI->>Git: kustomize edit set image (dev) + commit [skip ci]
    Git-->>Argo: diff détecté sur overlays/*/dev
    Argo->>Argo: sync → déploie dev

2. staging et prod = MR de promotion

Promouvoir = recopier le tag de l'env amont vers l'aval, sur une branche, puis MR vers develop. Le script scripts/promote.sh fait la recopie et prépare la branche (il ne pousse pas, ne merge pas) :

# dev → staging
scripts/promote.sh dev staging
git push -u origin promote/staging-<sha>
# → ouvrir une MR vers develop, relire, merger

# staging → prod (après validation de staging)
scripts/promote.sh staging prod
git push -u origin promote/prod-<sha>
# → MR vers develop, review obligatoire avant merge

Une fois la MR mergée, ArgoCD voit le nouveau newTag dans l'overlay cible et déploie l'env correspondant. Le saut direct dev → prod est interdit par le script : staging est un palier obligatoire.

Pourquoi le script n'utilise plus kustomize edit set image (#151)

Il l'utilisait jusqu'au 2026-07-30, et produisait 43 lignes de diff sur 2 fichiers pour un bump de tag qui devrait en toucher 2.

kustomize edit set image ne modifie pas la ligne visée : il relit le fichier dans sa représentation interne puis le réécrit entièrement dans son style canonique. Indentation des listes renormalisée, clés des patches réordonnées, et surtout commentaires remontés au-dessus de leur section, donc détachés de ce qu'ils documentent.

Le rendu de kustomize build étant identique, il n'y avait aucun impact fonctionnel. Le dégât était ailleurs : un diff de 43 lignes rend la relecture de la MR de promotion inopérante, alors que c'est la seule barrière de relecture avant la prod, le gate Code Owner étant une fonctionnalité Premium (voir #140).

Le script édite désormais la ligne newTag: en place. Mesuré sur une promotion réelle dev staging :

Avant Après
Lignes de diff 43 2
Fichiers touchés 2 2
Différence dans kustomize build tag d'image seul tag d'image seul
kustomize requis dans le PATH oui non

newName n'est pas touché : il porte le registre ECR, identique d'un env à l'autre. Seul le tag avance.

La leçon générale : un outil qui réécrit un fichier au lieu de le modifier détruit tout ce que le format portait et que l'outil ne modélise pas — ici les commentaires, qui sont précisément ce sur quoi s'appuie la relecture humaine.

Le script exige develop, propre et à jour

Il lit le newTag dans le working tree courant et branche depuis le HEAD courant. Lancé depuis une branche obsolète, il promouvrait un tag périmé ; lancé avec des modifications en cours, il les emporterait sur la branche de promotion.

Dans les deux cas la MR produite a l'air parfaitement normale : le mode de défaillance est silencieux, et c'est ce qui le rend dangereux. Le script refuse donc de démarrer si la branche n'est pas develop, si le working tree n'est pas propre, ou si develop n'est pas aligné sur origin/develop.

Pourquoi dev gardait toujours une longueur d'avance

C'était structurel, et ça surprenait au premier abord : promouvoir vers staging faisait avancer dev.

La MR de promotion est mergée sur develop. Ce merge est un push sur develop, donc il déclenchait le pipeline complet — build, scan, promote-image, puis le write-back qui bumpe les overlays de dev. Staging recevait le tag N, et dev passait à N+1 dans la foulée.

flowchart TD
    m["MR de promotion mergée<br/>staging : N-1 → N"] --> p["pipeline sur develop<br/>(aucun filtrage par chemin)"]
    p --> b["build + scan + promote-image<br/>nouveau tag N+1"]
    b --> w["write-back<br/>overlays/*/dev : N → N+1"]
    w --> r["dev repart avec un cran d'avance"]

Staging ne rattrapait donc jamais dev. Ce n'était pas un défaut du script de promotion, mais la conséquence de deux choix : le write-back automatique sur dev, et l'absence de filtrage par chemin dans le pipeline.

Ce qui a changé : le filtrage par chemin (#157)

Depuis !314, la chaîne image ne se déclenche que si backend/, frontend/ ou .gitlab-ci.yml a changé. Une MR de promotion ne touche que k8s/overlays/** : elle ne construit plus rien, ne pousse plus dans ECR, et ne fait plus avancer dev.

Le cercle est rompu : staging peut rattraper dev.

Ce filtrage tient en trois lignes parce que #161 a d'abord déplacé le backend sous backend/. Écrite avant, la même règle aurait dû énumérer app/**, alembic/**, requirements*, Dockerfile, tests/** — une liste à ne pas oublier, exactement le défaut qu'on venait de corriger ailleurs.

Les huit jobs de la chaîne partagent le même déclencheur, volontairement

On pourrait vouloir plus fin : ne reconstruire que le backend quand seul le backend change. C'est un piège.

Les jobs sont chaînés par needs:, et update-image-tag comme tag-inuse-images dépendent des deux promote. Or un needs vers un job absent du pipeline n'est pas un job sauté, c'est une erreur de création : le pipeline n'existerait pas.

Le contournement par needs: optional: true serait pire : le write-back bumperait le tag d'une image jamais promue, et ArgoCD tirerait une référence inexistante — soit INC-062 reproduit volontairement.

Un pipeline sans aucun job est failed, pas skipped

Une fois tout filtré, une MR ne touchant que docs/ n'a plus un seul job à lancer. Un pipeline vide n'est pas neutre : GitLab le crée, le marque failed et n'émet aucune erreur YAML. C'est le mécanisme exact du schedule de 21h (pipeline 2762923575).

Le projet exigeant un pipeline vert pour merger (only_allow_merge_if_pipeline_succeeds), et allow_merge_on_skipped_pipeline ne couvrant que skipped, toute MR de documentation serait devenue immergeable.

D'où le job pipeline-scope : quelques secondes, aucune condition, il garantit qu'un pipeline n'est jamais vide et affiche le périmètre détecté — la seule trace lisible quand la chaîne image ne tourne pas.

Et l'image change vraiment, pas seulement son tag

On pourrait croire que N et N+1 désignent le même contenu, le code applicatif n'ayant pas bougé. Ce n'est pas le cas, et la raison est plus profonde qu'il n'y paraissait.

Le .dockerignore laissait passer k8s/ dans le contexte de build, donc les manifestes étaient copiés dans l'image backend et modifier un overlay suffisait à changer le digest. C'est corrigé depuis !288 : le contexte ne contient plus que l'applicatif.

Le digest change quand même. Mesuré le 2026-08-11 : deux builds sans le moindre changement de contexte produisent deux digests différents. Sur 10 couches, les 4 de l'image de base (épinglée par digest) sont stables et les 6 couches construites dérivent spontanémentgroupadd, apt-get --only-upgrade, la reconstruction du venv, la suppression des caches pip.

Conséquences, inchangées : dev redéploie réellement pour un changement qui ne le concerne pas, chaque MR de promotion consomme un build, un scan et un push ECR, et la rétention du registre se remplit d'autant plus vite (voir INC-062).

Le seul levier qui supprime l'avance est donc le filtrage du pipeline par chemin, pour qu'un changement purement manifeste ou documentaire ne déclenche aucun build. Nettoyer le contexte de build, lui, ne rend pas deux images identiques : il réduit la surface de l'image et découple son contenu des manifestes, ce qui est utile mais ne suffit pas ici. Livré en #157, voir la section suivante.

Le gate de promotion prod

La promotion vers prod est le moment sensible. Deux garde-fous :

  • La MR de promotion elle-même : sur develop (branche protégée, merge réservé Maintainer), tu relis le diff — un simple changement de tag — et tu merges. Pour ce projet, c'est le point de contrôle humain.
  • .gitlab/CODEOWNERS déclare overlays/*/prod comme possédé, ce qui auto-suggère le reviewer. L'enforcement d'une approbation obligatoire par code owner est une fonctionnalité GitLab Premium+ : sur un plan Free il ne bloque pas, il documente et suggère.
  • E2E staging vert deviendra la condition automatique (job CI) avant promotion prod — c'est le gate « dur » et gratuit, livré en #140.

Rollback

Revenir en arrière = promouvoir un ancien tag. On recopie manuellement le tag connu-bon dans l'overlay cible (ou kustomize edit set image à la main), MR, merge. Comme chaque promotion est une MR, l'historique Git donne la liste des tags par env : retrouver le dernier tag sain est trivial.

Et après ? Vers une promotion outillée (Kargo & alternatives)

Le flux actuel est manuel par choix pédagogique : on manipule toute la mécanique à la main pour la comprendre. Il évoluera vers de l'outillage dédié (noté en « Évolution possible » de l'ADR 029) :

  • Kargo — l'évolution cible. Modélise la promotion en Stages et Freight : il détecte une nouvelle image, la promeut automatiquement dev → staging après vérification (tests, health), et garde prod en promotion manuelle approuvée. C'est le pattern d'entreprise « auto jusqu'à staging, gate humaine avant prod », sans le geste promote.sh à la main.
  • Argo Rollouts — progressive delivery dans un env : canary / blue-green + analyse automatique des métriques avant de basculer 100 % du trafic.
  • ArgoCD Image Updaterécarté : il bumpe le tag automatiquement partout, contraire à une promotion contrôlée vers prod.
flowchart LR
    subgraph Aujourdhui["Aujourd'hui — manuel (promote.sh)"]
        d1[dev] -->|MR à la main| s1[staging] -->|MR à la main| p1[prod]
    end
    subgraph Cible["Cible — Kargo"]
        d2[dev] -->|"auto après<br/>vérification"| s2[staging] -->|"gate humaine<br/>approuvée"| p2[prod]
    end

Le socle multi-env (namespaces, overlays, promotion par MR) est le prérequis : Kargo s'installe par-dessus cette structure, il ne la remplace pas.