Aller au contenu

ADR-0012 — Helm comme packaging K8s, rendu double depuis SIRRAT

Date : 2026-06-10 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 : Proposée Sprint d'implémentation : à planifier — le chart de base avant la première app qui passe en pod K8s Étend : ADR-0009 (cible pod → vrai pod K8s à terme)


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
gateway SLY lui-même (Spring Cloud Gateway) Deployment + Service + Ingress (annotations spécifiques)
statefulset réservé aux cas dédiés rares (mock client souverain) 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.

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.

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 ; Helm packagera SLY aussi (chart dynors-gateway), mais sans changer la table de routage.

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

  1. Écrire dynors-app chart minimal mais complet sur le squelette :
  2. workload.kind: web (autres kinds en sprints suivants — l'API du values est fixée tout de suite).
  3. Deployment + Service + Ingress + ESO + ConfigMap.
  4. probes Spring Actuator (startupProbe, readinessProbe, livenessProbe).
  5. securityContext non-root, readOnlyRootFilesystem, seccompProfile.
  6. terminationGracePeriodSeconds: 45 + preStop SLY-drain.
  7. Annotations Ingress portant la règle URL DYNORS (/v{n}/api/v{n}).
  8. NetworkPolicy denyAll + ingress SLY + egress déclarés en values.
  9. Tests : helm lint + helm template sur 3 profils (internal/shared, saas/shared prod, paiement/fiscal) + kubeconform strict en CI dynors-ops.
  10. Apprendre à SIRRAT à émettre values.yaml aligné sur les slots du manifeste, avec le profil ADR-0009 en clair (profile.audience, profile.tenancy, profile.scenario, profile.deployTarget, sensitivity). Le chart applique les défauts à partir de là.
  11. Ajouter workload.kind: worker (paiement outbox), cronjob (settlement runs J-1).
  12. Première app cible déployée : DYNORS PAIEMENT — sensibilité élevée, pod, justifie l'investissement et stresse les défauts (HPA, NetworkPolicy, lifecycle). Compose en parallèle, K8s en pilote.
  13. dynors-gateway (SLY lui-même) au sprint suivant.
  14. Chart parent dynors-namespace : ResourceQuota + LimitRange + PriorityClass (dynors-low/medium/high) — créés à la création d'un cercle K8s.
  15. Marquer les manifestes à la main TAKKU/HEISENBERG obsolètes ; supprimer une fois dynors-app validé sur eux.

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.

Références

  • ADR-0009 (cible socle/pod) — Helm matérialise « pod »
  • ADR-0008 (transit SLY) — NetworkPolicy / mTLS embarqués dans dynors-app
  • ADR-0010 (payouts) — credentials Vault dynamiques compatibles ESO/Vault Agent
  • Manifestes existants : dynors-internal/applications/takku/deploy/k8s/, dynors-internal/applications/heisenberg/deploy/k8s/
  • Codex : SirratCodexConstants + SIRRAT_CODEX_REFERENCE.md
  • Convention stockage : docs/architecture/CONVENTION_STOCKAGE_OBJET_DYNORS.md (hors Helm)