Aller au contenu

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, securityContext non-root, filesystem read-only, ressources par défaut).
  • Service ClusterIP.
  • Ingress (annotations selon le profil ADR-0009 — la règle d'URL /v{n}/api/v{n} sur l'hôte api.* devient une annotation nginx.ingress.kubernetes.io/rewrite-target).
  • ExternalSecret (alimenté par protected:{category}.{key} du manifeste SIRRAT, résolu en vault://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).
  • Job Liquibase 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 :

  1. docker-compose.yml + .env (chemin actuel, consommé par le rôle Ansible dynors-app).
  2. values.yaml pour dynors-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.yaml existant, helm install, on part. Pas de projet « migration K8s ».
  • La règle ADR-0009 « pod » s'opérationnalise sans réécriture : un déploiement pod peut être un container dédié ou un pod K8s — l'axe gouvernance/ scenario dé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).
  • preStop hook : drain SLY upstream (l'Ingress retire le pod, attendre 5s avant SIGTERM), ce qui évite les 502 sur déploiement rolling.
  • startupProbe séparée de la readinessProbe (apps lentes au boot ne se font pas tuer).
  • restartPolicy et backoffLimit côté Job/CronJob.
  • Liquibase : Job séparé en helm 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

  • NetworkPolicy denyAll par défaut, ouverte uniquement sur SLY (ingress) et les dépendances déclarées en values (egress vers paiement, fiscal, Vault, etc.). Matérialisation directe d'ADR-0008 (rayon de souffle).
  • TopologySpreadConstraints sur 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 / tolerations exposés en values pour les cas spécifiques (workload type-isolé, namespace client souverain qui exige un nodepool dédié).
  • PodSecurityContext non-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 (ClusterSecretStore DYNORS vs SecretStore namespaced client).
  • storageClass / nodeSelector selon 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}-media ne 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=gateway du même chart dynors-app, pas un chart dynors-gateway sé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-app couvre 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-app proprement (Deployment, Service, Ingress, ESO, NetworkPolicy, probes) avec tests helm template + kubeconform en 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 que dynors-app couvre leur cas, supprimés au sprint suivant.
  • CI dynors-ops — helm lint + helm template + kubeconform sur 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)

  1. Créer le repo Git autonome dynors-ops/ (aujourd'hui dossier sans .git selon ADR-2026-04-25).
  2. CI dynors-ops : helm lint + helm template + kubeconform strict sur 3 profils de référence (internal/shared/managed, saas/shared/managed prod, sensitivity=high paiement/fiscal), plus un set de values d'exemple par workload.kind. Échec CI si dérive vs values.schema.json.

Sprint 1 — Alignement chart + injecteur SIRRAT

  1. Aligner le values.yaml du chart dynors-app (déjà à v0.1.0) sur ADR-0009 amendée : profile.audienceprofile.deployment_audience (clé renommée, breaking documentée dans Chart.yaml annotations.artifacthub.io/changes). Bump v0.2.0.
  2. Ajouter au chart les workload.kind ∈ {job, static-site} et le gate statefulset (annotation justification obligatoire). Mettre à jour values.schema.json en miroir.
  3. Apprendre à SIRRAT à émettre values.yaml aligné 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, et CODEX_TARGET_CLUSTER). Module Java SirratHelmValuesEmitter ; format spec figé ; tests d'équivalence compose↔values sur 3 apps pilotes.

Sprint 2 — Migration TAKKU/HEISENBERG (filet de secours préservé)

  1. Déployer dynors-app sur TAKKU puis HEISENBERG en dev et int, en parallèle des manifestes à la main (les deux coexistent).
  2. Tests NRT verts sur les deux apps via Helm + helm rollback testé sur incident simulé (cf. §5bis : rollback DB documenté).
  3. Marquer obsolètes les deploy/k8s/*.yaml des deux apps (en-tête # OBSOLETE — voir dynors-ops/charts/dynors-app v0.2.0+). Suppression uniquement après 14 jours sans incident en int.

Sprint 3 — Première app sensibilité élevée (stress test)

  1. DYNORS PAIEMENT déployée via dynors-app en cercle int — 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

  1. SLY déployé via dynors-app workload.kind=gateway (pas de chart séparé dynors-gateway).
  2. Chart parent dynors-namespace : déjà présent (resourcequota, limitrange, priorityclasses, networkpolicy-deny-all) — à étendre si besoin (PodSecurityStandards).
  3. 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.audienceprofile.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 (ajout CODEX_TARGET_CLUSTER)
  • Convention stockage : docs/architecture/CONVENTION_STOCKAGE_OBJET_DYNORS.md (hors Helm)