ADR-0012 — Helm comme packaging K8s, rendu double depuis SIRRAT¶
Date : 2026-06-10 (amendée 2026-06-21)
Catégorie : A (choix d'outillage de packaging dans le cadre de la trajectoire infra déjà actée)
Décideur : Lead architecte DYNORS
Statut : Acceptée (avec amendements 2026-06-21 — voir §Amendements)
Sprint d'implémentation : Sprint 0 (repo dynors-ops/ + CI) prérequis bloquant ; chart dynors-app v0.1.0 déjà présent (à aligner sur ADR-0009 amendée).
Étend : ADR-0009 amendée (cible pod → vrai pod K8s à terme, deployment_audience au lieu de audience)
Dépend : ADR-2026-04-25 (orchestrateur, option C — acceptée en même temps 2026-06-21)
Contexte¶
L'écosystème déploie aujourd'hui en docker-compose (rôle Ansible dynors-app).
Des manifestes K8s écrits à la main existent déjà pour TAKKU et HEISENBERG
(deploy/k8s/*.yaml) : Deployment, Service, Ingress, ExternalSecret Vault.
Ces manifestes sont quasi-identiques d'une app à l'autre — seuls changent quelques noms,
ports, et l'image. C'est la duplication exacte que Helm résout.
La décision a déjà été prise informellement : on passe par Helm. Cette ADR formalise comment (structure du chart, où il vit, comment SIRRAT alimente ses valeurs), et fige le principe rendu double pour rendre la migration indolore — pas de big-bang.
ADR-0009 « cible de déploiement » dit déjà : socle ou pod. Aujourd'hui un
« pod » est un hôte/container dédié sur l'hyperviseur ; à terme c'est un vrai
pod K8s. L'app ne le sait pas — Helm est la matérialisation de ce « à terme ».
Décision¶
1. Un library chart unique dynors-app, pas N charts¶
Les apps DYNORS partagent une forme : backend Spring Boot, port HTTP, Postgres,
ESO Vault, Ingress derrière SLY. Le chart capture cette forme une fois.
Chaque app a un values.yaml de ~30 lignes, dérivé de son manifeste SIRRAT.
Ce que dynors-app fournit :
Deployment(probes liveness/readiness via Spring Actuator,securityContextnon-root, filesystem read-only, ressources par défaut).ServiceClusterIP.Ingress(annotations selon le profil ADR-0009 — la règle d'URL/v{n}→/api/v{n}sur l'hôteapi.*devient une annotationnginx.ingress.kubernetes.io/rewrite-target).ExternalSecret(alimenté parprotected:{category}.{key}du manifeste SIRRAT, résolu envault://dynors/{cercle}/{scope}/{category}#{key}— règle existante).ConfigMap(slots non secrets du manifeste).ServiceAccount+NetworkPolicy(denyAll par défaut, ouvert sur SLY uniquement — défense en profondeur ADR-0008).JobLiquibase optionnel (init).
Variantes (charts séparés réutilisant dynors-app comme dépendance) :
dynors-worker(pas de Service, juste Deployment + lien queue).dynors-gateway(cas SLY lui-même).
Pas de chart par app — l'expérience TAKKU/HEISENBERG prouve déjà l'isomorphisme.
2. Le chart vit dans dynors-ops/charts/dynors-app/¶
- Cohérent avec le porteur de l'infra : Ansible et Helm = même équipe ops, même vault password, même cycle de revue.
- À terme, un GitOps (ArgoCD / FluxCD) pull depuis ce dossier ou depuis un OCI registry alimenté par sa CI.
- Versionné en SemVer ; pas de breaking change sans bump majeur (la stabilité du chart est aussi importante que celle du BOM core).
3. Rendu double depuis SIRRAT — pas de big-bang¶
SIRRAT résout déjà un manifeste d'environnement (slots + secrets). À partir de ce même manifeste, l'injecteur SIRRAT émet deux artefacts :
docker-compose.yml+.env(chemin actuel, consommé par le rôle Ansibledynors-app).values.yamlpourdynors-app(chart Helm — peut être déployé ou non).
Aujourd'hui personne ne helm install. Mais le values.yaml est produit et
versionné dès maintenant. Conséquences :
- La divergence ne s'installe pas : si on ajoute un slot au manifeste, les deux rendus l'absorbent en parallèle, jamais l'un sans l'autre.
- Quand une app a un vrai besoin K8s (HPA paiement après pic Wave, isolation
cluster client souverain, etc.), on prend le
values.yamlexistant,helm install, on part. Pas de projet « migration K8s ». - La règle ADR-0009 « pod » s'opérationnalise sans réécriture : un déploiement
podpeut être un container dédié ou un pod K8s — l'axegouvernance/scenariodécide, le chart obéit.
4. Formes de workload, scaling et défauts par profil¶
Les apps DYNORS partagent une forme mais pas toutes la même charge ni le même cycle.
Le chart dynors-app expose un axe workload.kind et applique des défauts dérivés
du profil ADR-0009 + sensibilité — l'app ne décide rien d'elle-même, SIRRAT renseigne le
profil dans values.yaml, le chart applique le bon défaut.
Formes de workload supportées (workload.kind)¶
| Kind | Cas | Ressources K8s générées |
|---|---|---|
web (défaut) |
la grande majorité des apps | Deployment + Service + Ingress |
worker |
consommateur de queue (paiement outbox, notify dispatcher) | Deployment (pas de Service ni Ingress) |
cronjob |
FISCAL déclarations périodiques, settlement runs J-1, R2 ILM audit | CronJob |
job (amendement 2026-06-21) |
one-shot : migration-job ADR-0009 (Liquibase, backfill, RGPD purge ponctuelle) |
Job + restartPolicy: Never + backoffLimit |
static-site (amendement 2026-06-21) |
doc-site (MkDocs) et landing-static ADR-0009 |
Deployment (image nginx avec contenu mounté) + Service + Ingress (sans /api/v{n}) |
gateway |
SLY lui-même (Spring Cloud Gateway) — un seul chart, pas de dynors-gateway séparé |
Deployment + Service + Ingress (annotations spécifiques en values) |
statefulset |
réservé aux cas dédiés rares (mock client souverain) — gate Helm : refus si pas d'annotation dynors.io/statefulset-justification: "<ADR-XXXX ou ticket>" |
StatefulSet + headless Service |
Un seul chart, plusieurs kinds — pas un chart par forme. L'évolution future (gRPC, GraphQL, streaming) ajoute un kind, jamais un chart de plus.
Exclusions du périmètre dynors-app (amendement 2026-06-21). Les familles ADR-0009 suivantes n'utilisent pas ce chart :
| Famille ADR-0009 | Raison | Mécanisme |
|---|---|---|
mobile-rn, mobile-flutter |
n/a K8s (artifact store / app store) | CI mobile dédiée (Fastlane), pas de Helm |
monitoring-stack |
charts upstream matures (kube-prometheus-stack, loki-stack, tempo) | sous-charts dans dynors-ops/charts/dynors-monitoring/ |
shared-infra (Postgres, Redis, Kafka) |
opérateurs dédiés (CNPG, redis-operator, Strimzi) | ADR séparée par opérateur quand on basculera |
Défauts de scaling dérivés du profil¶
| Profil (ADR-0009) + sensibilité | Replicas | HPA | PDB minAvailable |
Anti-affinity | PriorityClass |
|---|---|---|---|---|---|
internal · shared · managed (JARAAF, BOOKS, RAGNAR) |
1 | — | — | — | dynors-low |
saas · shared · managed hors prod |
2 | — | — | soft (hosts) | dynors-medium |
saas · shared · managed prod |
2 (min) | CPU 70%, 2→8 | 1 | hard (hosts) | dynors-medium |
saas · dedicated · {on-premise/cloud-client} prod |
selon contrat | selon contrat | 1 | hard | dynors-medium |
Sensibilité élevée : paiement, fiscal (tous cercles prod) |
3 | CPU 60%, 3→12 | 2 | hard (zones si dispo) | dynors-high |
cronjob |
n/a | n/a (concurrencyPolicy: Forbid) | — | — | hérite |
Ces défauts sont lisibles dans le chart, jamais codés dans une app. L'override reste
possible dans values.yaml quand un contrat le justifie — mais devient un signal de revue
(pourquoi cette app dévie ?).
Lifecycle robuste — défauts du chart, valides pour toute nouvelle app¶
terminationGracePeriodSeconds: 45(Spring Boot a besoin de >30s pour vider proprement).preStophook : drain SLY upstream (l'Ingress retire le pod, attendre 5s avant SIGTERM), ce qui évite les 502 sur déploiement rolling.startupProbeséparée de lareadinessProbe(apps lentes au boot ne se font pas tuer).restartPolicyetbackoffLimitcôté Job/CronJob.- Liquibase :
Jobséparé enhelm hook pre-upgrade --wait, pas un init container (sinon N pods qui démarrent en parallèle migrent en concurrence — incident classique).
Isolation et placement¶
NetworkPolicydenyAll par défaut, ouverte uniquement sur SLY (ingress) et les dépendances déclarées en values (egress verspaiement,fiscal, Vault, etc.). Matérialisation directe d'ADR-0008 (rayon de souffle).TopologySpreadConstraintssur hostname et zone (si le cluster a plusieurs zones), pour qu'un host tombé n'emporte pas toutes les répliques d'une app sensible.nodeSelector/tolerationsexposés en values pour les cas spécifiques (workload type-isolé, namespace client souverain qui exige un nodepool dédié).PodSecurityContextnon-root,readOnlyRootFilesystem: true,seccompProfile: RuntimeDefault— verrouillé par le chart, non-débrayable par l'app.
Multi-cluster (vision)¶
dynors-app ne suppose rien sur le cluster. Les éléments cluster-spécifiques sont
des values explicites :
image.registry(DYNORS GitLab registry vs registry mirroré client souverain).ingress.className(cluster DYNORS =nginx, cluster client peut différer).externalSecrets.storeRef(ClusterSecretStoreDYNORS vsSecretStorenamespaced client).storageClass/nodeSelectorselon le contrat.
Conséquence : un même chart dynors-app sert un déploiement DYNORS et un déploiement client
souverain sans fork. C'est la condition pour que ADR-0009 (cible cloud-client) ne devienne
pas une dette technique.
Désignation du cluster cible (amendement 2026-06-21). Le manifeste SIRRAT porte un nouveau
codex CODEX_TARGET_CLUSTER ∈ {dynors-{cercle}, client-{tenantCode}}, lu par la CI
Helm pour sélectionner kubeconfig et namespace ({appCode}-{cercle} ou
{appCode}-{tenantCode} selon profil). Aucun cluster ne se devine — explicite dès le manifeste.
Cost & safety — bornes à la création du namespace¶
Chaque cercle K8s reçoit un ResourceQuota + LimitRange (templates du chart parent
dynors-namespace, à ajouter en même temps). Les HPA des apps sont bornés par le quota
du namespace — pas de cascade de scaling qui consomme tout le cluster sur un bug.
5. Périmètre — ce que Helm ne porte pas¶
- Provisioning du stockage objet :
dynors-storage(Ansible) reste — les buckets sont externes au cluster (MinIO du cercle, R2 prod), la convention{prefix}-mediane change pas. - Postgres : managé hors cluster aujourd'hui ; quand on basculera vers un opérateur (CNPG/Zalando), ce sera une ADR dédiée, pas un sous-produit de Helm.
- Secrets Vault : Vault reste source de vérité ; ESO est le pont vers K8s, pas un substitut.
- Auth/Routing transverse : SLY reste l'Ingress logique de l'écosystème.
Amendement 2026-06-21 : SLY est packagé via
workload.kind=gatewaydu même chartdynors-app, pas un chartdynors-gatewayséparé. La forme reste canonique, les spécificités SLY (table routage, signature transit) vivent en values + ConfigMap monté.
5bis. Rollback DB ≠ rollback Helm (amendement 2026-06-21)¶
helm rollback rétablit les manifestes (image, replicas, env) mais n'annule pas la
migration Liquibase exécutée par le Job pre-upgrade. Une app rollbackée sur l'ancienne
image peut planter en lisant un schéma déjà migré.
Règle normative :
1. Tout changeset Liquibase doit avoir son <rollback> (vérifié en CI liquibase rollback-count 1 sur le dernier changeset).
2. Procédure runbook : sur incident → helm rollback {release} {revision} puis kubectl apply d'un Job liquibase-rollback dédié (image n-1, command --rollback-count {N}). Pas l'inverse, pas l'un sans l'autre.
3. Les changements de schéma destructifs (DROP COLUMN, RENAME) nécessitent un palier expand/contract sur 2 releases — pas un seul changeset rollbackable, car un rollback Helm sur l'image n-1 ne saurait pas lire la colonne supprimée.
Conséquences¶
Bénéfices¶
- Une seule forme à durcir : un audit sécu sur
dynors-appcouvre toutes les apps. - Les manifestes à la main de TAKKU/HEISENBERG deviennent obsolètes — moins de surface à maintenir.
- La migration K8s d'une app passe de « projet » à « bouton » (chart prêt,
values résolu par SIRRAT,
helm install). - ADR-0008 (NetworkPolicy / mTLS), ADR-0010 (Vault dynamique) ont un véhicule technique standard.
Coûts / contraintes induites¶
- Investissement initial : écrire
dynors-appproprement (Deployment, Service, Ingress, ESO, NetworkPolicy, probes) avec testshelm template+kubeconformen CI. - L'injecteur SIRRAT doit apprendre à émettre
values.yaml(en plus de compose+env) — petit module Java, format simple. - Discipline de revue : tout nouveau slot du Codex doit être absorbé par les deux rendus en même temps. La divergence retomberait sinon.
Composants impactés¶
dynors-ops/charts/dynors-app/(nouveau).dynors-ops/charts/dynors-worker/,dynors-gateway/(variantes, plus tard).- SIRRAT injecteur — nouveau format de sortie
values.yaml. - Manifestes K8s à la main de TAKKU/HEISENBERG (
deploy/k8s/*) : marqués obsolètes dès quedynors-appcouvre leur cas, supprimés au sprint suivant. - CI dynors-ops —
helm lint+helm template+kubeconformsur le chart.
Alternatives considérées¶
Alternative A — Chart par app (status quo K8s amélioré). Écartée : duplication exacte que la décision veut résoudre. Coût de maintenance N fois.
Alternative B — Kustomize uniquement. Écartée : convient à la transformation d'overlays mais ne package pas une forme — Helm est meilleur quand la forme est canonique et qu'on veut versionner cette forme.
Alternative C — Operator dédié DYNORS. Écartée à ce stade : sur-ingénierie tant
qu'on n'a pas de logique runtime qui justifie un controller. À reconsidérer si on
veut un DynorsApp CRD piloté par SIRRAT directement.
Alternative D — Aller à Helm tout de suite, abandonner compose. Écartée : toutes les apps n'ont pas besoin de K8s aujourd'hui (un socle docker-compose suffit pour les internes ADR-0009). Le rendu double protège ce choix.
Plan d'exécution¶
Sprint 0 — Prérequis bloquant (amendement 2026-06-21)¶
- Créer le repo Git autonome
dynors-ops/(aujourd'hui dossier sans.gitselon ADR-2026-04-25). - CI dynors-ops :
helm lint+helm template+kubeconformstrict sur 3 profils de référence (internal/shared/managed,saas/shared/managed prod,sensitivity=highpaiement/fiscal), plus un set de values d'exemple parworkload.kind. Échec CI si dérive vsvalues.schema.json.
Sprint 1 — Alignement chart + injecteur SIRRAT¶
- Aligner le
values.yamldu chartdynors-app(déjà àv0.1.0) sur ADR-0009 amendée :profile.audience→profile.deployment_audience(clé renommée, breaking documentée dansChart.yaml annotations.artifacthub.io/changes). Bumpv0.2.0. - Ajouter au chart les
workload.kind∈ {job,static-site} et le gatestatefulset(annotation justification obligatoire). Mettre à jourvalues.schema.jsonen miroir. - Apprendre à SIRRAT à émettre
values.yamlaligné sur les slots du manifeste, avec le profil ADR-0009 amendée en clair (profile.deployment_audience,profile.tenancy,profile.scenario,profile.gouvernance,profile.deployTarget,sensitivity, etCODEX_TARGET_CLUSTER). Module JavaSirratHelmValuesEmitter; format spec figé ; tests d'équivalence compose↔values sur 3 apps pilotes.
Sprint 2 — Migration TAKKU/HEISENBERG (filet de secours préservé)¶
- Déployer
dynors-appsur TAKKU puis HEISENBERG endevetint, en parallèle des manifestes à la main (les deux coexistent). - Tests NRT verts sur les deux apps via Helm +
helm rollbacktesté sur incident simulé (cf. §5bis : rollback DB documenté). - Marquer obsolètes les
deploy/k8s/*.yamldes deux apps (en-tête# OBSOLETE — voir dynors-ops/charts/dynors-app v0.2.0+). Suppression uniquement après 14 jours sans incident enint.
Sprint 3 — Première app sensibilité élevée (stress test)¶
- DYNORS PAIEMENT déployée via
dynors-appen cercleint— stresse HPA, NetworkPolicy, lifecycle, pré-requis ADR-0010 (PayoutExecutor,FinancialPolicyProvider). Compose en parallèle, K8s en pilote ; bascule officielle quand 2 semaines sans incident.
Sprint 4 — SLY + cleanup¶
- SLY déployé via
dynors-appworkload.kind=gateway(pas de chart séparédynors-gateway). - Chart parent
dynors-namespace: déjà présent (resourcequota,limitrange,priorityclasses,networkpolicy-deny-all) — à étendre si besoin (PodSecurityStandards). - Suppression définitive des manifestes à la main TAKKU/HEISENBERG dès critères §8 vérifiés.
Critères factuels de suppression (amendement 2026-06-21)¶
Les manifestes à la main TAKKU/HEISENBERG ne sont supprimés que si toutes les conditions sont vraies :
- (i) chart dynors-app déployé sur l'app en dev + int + rmoa,
- (ii) tests NRT verts 14 j consécutifs,
- (iii) helm rollback testé en incident simulé (Sprint 2),
- (iv) procédure rollback DB Liquibase validée (§5bis),
- (v) revue archi tracée dans JOURNAL_REVUE_ARCHI.md.
Sinon, manifestes conservés comme filet — pas de suppression « parce qu'il faut ».
Suivi¶
Critères de succès :
- Créer une nouvelle app via TAKKU/Forge produit, à partir du même manifeste,
un compose et un values.yaml validés en CI.
- Une bascule compose → K8s pour cette app est helm install sans modification d'app.
- Une nouvelle app n'a jamais à modifier le chart : son profil ADR-0009 dans
values.yaml suffit à dériver replicas, HPA, PDB, anti-affinity, priority,
NetworkPolicy.
Signaux de régression :
- Un slot Codex ajouté qui n'apparaît que d'un côté (compose ou values).
- Une app qui surcharge replicas/resources en values sans justification écrite
(dévie d'un défaut profil — à challenger en revue).
- Un nouveau kind de workload qui amène à dupliquer le chart au lieu d'étendre
workload.kind.
Amendements¶
| Date | Amendement | Origine |
|---|---|---|
| 2026-06-21 | Alignement profile.audience → profile.deployment_audience (ADR-0009 amendée — collision JWT) ; bump dynors-app v0.2.0. |
Revue critique. |
| 2026-06-21 | Stratégie credentials par rail (lien ADR-0010 amendée) : majoritairement statique scope-minimal rotation 90 j, dynamique réservé aux rails supportés. ESO reste le pont K8s ; pas de promesse de « tout dynamique ». | Revue critique. |
| 2026-06-21 | Ajout workload.kind ∈ {job, static-site} pour couvrir le catalogue ADR-0009 élargi (migration-job, doc-site, landing-static). |
Revue critique. |
| 2026-06-21 | Exclusions explicites : mobile-rn/mobile-flutter (artifact store), monitoring-stack (charts upstream), shared-infra (opérateurs CNPG/redis-operator/Strimzi). |
Revue critique. |
| 2026-06-21 | SIRRAT injecteur values.yaml — module Java SirratHelmValuesEmitter à livrer Sprint 1, sinon « rendu double » théorique. |
Revue critique. |
| 2026-06-21 | CODEX_TARGET_CLUSTER ajouté pour désignation cluster explicite (multi-cluster). |
Revue critique. |
| 2026-06-21 | §5bis Rollback DB ≠ rollback Helm : <rollback> Liquibase obligatoire, runbook explicite, expand/contract pour changements destructifs. |
Revue critique. |
| 2026-06-21 | Critères factuels de suppression des manifestes TAKKU/HEISENBERG (5 conditions cumulatives). | Revue critique. |
| 2026-06-21 | Gate Helm sur statefulset (annotation justification obligatoire). |
Revue critique. |
| 2026-06-21 | SLY = workload.kind=gateway dans le même chart dynors-app, pas de chart dynors-gateway séparé. |
Revue critique. |
| 2026-06-21 | Sprint 0 prérequis bloquant : créer le repo Git autonome dynors-ops/ + CI (sinon chart non testé). |
Revue critique (ADR-2026-04-25 confirme l'absence de .git). |
Références¶
- ADR-0009 amendée (cible socle/pod,
deployment_audience, catalogue élargi) — Helm matérialise « pod » - ADR-0008 (transit SLY) — NetworkPolicy / mTLS embarqués dans
dynors-app - ADR-0010 amendée (payouts,
FinancialPolicyProvider) — stratégie credentials par rail, ESO comme pont - ADR-2026-04-25 (orchestrateur, option C — acceptée 2026-06-21) — trajectoire hybride par paliers
- Chart présent :
dynors-ops/charts/dynors-app/(v0.1.0 → v0.2.0 après amendements) - Chart parent :
dynors-ops/charts/dynors-namespace/(déjà présent) - Manifestes à supprimer après critères :
dynors-internal/applications/takku/deploy/k8s/,dynors-internal/applications/heisenberg/deploy/k8s/ - Codex :
SirratCodexConstants+SIRRAT_CODEX_REFERENCE.md(ajoutCODEX_TARGET_CLUSTER) - Convention stockage :
docs/architecture/CONVENTION_STOCKAGE_OBJET_DYNORS.md(hors Helm)