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,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 |
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).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.
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 ;
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-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¶
- Écrire
dynors-appchart minimal mais complet sur le squelette : workload.kind: web(autres kinds en sprints suivants — l'API du values est fixée tout de suite).- Deployment + Service + Ingress + ESO + ConfigMap.
- probes Spring Actuator (
startupProbe,readinessProbe,livenessProbe). securityContextnon-root,readOnlyRootFilesystem,seccompProfile.terminationGracePeriodSeconds: 45+preStopSLY-drain.- Annotations Ingress portant la règle URL DYNORS (
/v{n}→/api/v{n}). NetworkPolicydenyAll + ingress SLY + egress déclarés en values.- Tests :
helm lint+helm templatesur 3 profils (internal/shared,saas/shared prod,paiement/fiscal) +kubeconformstrict en CI dynors-ops. - Apprendre à SIRRAT à émettre
values.yamlaligné 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à. - Ajouter
workload.kind: worker(paiement outbox),cronjob(settlement runs J-1). - 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.
dynors-gateway(SLY lui-même) au sprint suivant.- Chart parent
dynors-namespace:ResourceQuota+LimitRange+PriorityClass(dynors-low/medium/high) — créés à la création d'un cercle K8s. - Marquer les manifestes à la main TAKKU/HEISENBERG obsolètes ; supprimer une fois
dynors-appvalidé 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)