0033 — Contrat d'interface entre l'application (`atlas`) et le cluster
Contexte
Section intitulée « Contexte »Le pipeline de collaborations (ADR 0029)
est développé dans le dépôt atlas (code, manifestes applicatifs, plan) mais
s’exécute sur un cluster Kubernetes dont l’infrastructure vit dans un dépôt
séparé (cluster : Ansible, addons platform/<addon>/, banc Vagrant). Le
plan d’exécution répartit explicitement le travail : la Phase 1 (socle :
ingress, TLS, Argo CD, observabilité, CloudNativePG, Dagster, Marquez) est
côté cluster ; les Phases 2–6 (ingestion, transformations, index, API, PWA)
sont côté atlas.
Les deux dépôts collaborent par un contrat implicite : atlas suppose que le
cluster fournit un bucket S3 nommé d’une certaine façon, un Postgres avec
pgvector, un registry pour ses images, un Argo CD qui réconcilie ses
manifestes ; le cluster suppose que atlas lui livre des images et des
Application Argo CD dans un format donné. Tant que ce contrat reste
implicite, il peut diverger en silence : un nom de bucket différent de part et
d’autre, une image taguée autrement que ce que le manifeste référence, une
version de pgvector ou de Dagster incompatible — autant de pannes qui
compilent et déploient mais échouent à l’exécution.
L’outil étant générique et multi-tenant (ADR 0031), ce contrat ne lie pas seulement « mes deux dépôts » : il lie le code générique à n’importe quel cluster qui l’exploite. Il doit donc être explicite et unique, pour que tout déployeur sache ce que l’application attend de son infrastructure, sans avoir à lire le code.
On retient une coordination par contrat documenté plutôt que par tests d’intégration automatisés inter-dépôts : à ce stade (un opérateur, un déploiement pilote), un test end-to-end en CI serait disproportionné (cluster requis, lenteur, fragilité). Le contrat explicite suffit à empêcher la dérive ; des vérifications statiques ou un smoke-test au banc pourront être ajoutés si une douleur réelle se manifeste.
Décision
Section intitulée « Décision »Cette page est la vue applicative dérivée du contrat d’interface entre l’application
atlaset le cluster qui l’exploite. La source de vérité du contrat est publiée par le dépôtcluster(ADR cluster 0043, fichiers machine-lisiblescontract/*.example.yaml) ; cette page en est le miroir côté atlas — elle énonce les mêmes points de contact, du point de vue de ce que l’application attend et fournit. Les deux côtés s’y conforment ; tout changement d’un point de contact se reflète ici dans la même PR que le changement de code, et l’alignement avec la sourceclusterest tenu par discipline (la frontière est outillée, ADR 0077).
Les valeurs concrètes (noms d’hôtes, plages d’IP, tailles) sont propres à chaque instance et relèvent de sa configuration, pas du code générique (ADR 0022, ADR 0031) ; le contrat fixe les conventions et formats, pas les valeurs d’une instance donnée.
Ce que le cluster fournit à l’application
Section intitulée « Ce que le cluster fournit à l’application »| Point de contact | Contrat | Fourni par (Phase 1) |
|---|---|---|
| Stockage objet S3 | Un bucket dont le nom suit la convention citation (jamais la marque, ADR 0022), accessible en path-style, avec un Secret d’identifiants S3 (AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY / endpoint) généré par un ObjectBucketClaim. | Ceph RGW (déjà en prod) |
| PostgreSQL + pgvector | Un cluster PostgreSQL géré (CloudNativePG) avec l’extension pgvector activée, accessible par DSN depuis les namespaces consommateurs, dimension de vecteur 384 (modèle all-MiniLM-L6-v2). | Étape 1.6 |
| Cache de flux | Une base logique cache sur le même CloudNativePG (pas de brique Redis dédiée — sobriété, ADR cluster 0093), rôle cache + Secret pg-role-cache, DSN composé des POSTGRES_CACHE_* (HOST=pg-rw.postgres — nom court, jamais le FQDN ; PORT, DB=cache, USER, PASSWORD). L’adaptateur (table clé-valeur + UPSERT + pg_advisory_lock) vit côté application (ADR 0085). | Endpoint postgres-cache |
| Orchestrateur | Dagster déployé (webserver + daemon + run workers), event log dans Postgres ; l’application fournit la code-location (assets), pas l’orchestrateur. | Étape 1.7 |
| Lineage | Un collecteur OpenLineage (Marquez) joignable via OPENLINEAGE_URL. | Étape 1.8 |
| Suivi de modèles | Un serveur MLflow (tracking + registre de modèles) joignable via MLFLOW_TRACKING_URI, avec backend store sur CloudNativePG et artefact store sur un bucket S3 (convention citation, ADR 0022). L’application logue ses runs et enregistre le modèle ; elle ne déploie pas le serveur (ADR 0062). | Serveur MLflow (ADR cluster dédié) |
| Registry d’images | Un registry interne où l’application pousse ses images et que les manifestes/run workers référencent. | déjà en prod |
| GitOps | Argo CD réconciliant les Application de l’application, cadrées par un AppProject couvrant les namespaces citation-*. | Étape 1.4 |
| Exposition | Un ingress + TLS de bordure (cert-manager) pour exposer l’API et la PWA en HTTPS. | Étapes 1.2–1.3 |
| Observabilité | Prometheus scrappant les ServiceMonitor des services applicatifs ; Grafana + Loki. | Étape 1.5 |
Ce que l’application fournit au cluster
Section intitulée « Ce que l’application fournit au cluster »| Point de contact | Contrat |
|---|---|
| Namespaces | Les charges applicatives vivent dans citation-ingest, citation-marts, citation-serving, citation-pwa (convention ADR 0022 — jamais la marque). |
| Images | Poussées sur le registry interne, taguées explicitement (pas latest en production) ; les manifestes référencent le tag exact. |
| Manifestes | Des Application Argo CD + Deployment/CronJob/Service/Ingress conformes (validés kubeconform), réconciliables sans intervention manuelle. |
| Métriques | Chaque service expose /metrics et déclare un ServiceMonitor ; aucune donnée personnelle dans les labels (cardinalité + RGPD, ADR 0030). |
| Contrat de données | Le mart est un artefact Parquet + manifest.json sur le bucket S3 (immuable, checksummé, versionné) — l’infrastructure n’a pas à le connaître, mais elle garantit la durabilité du bucket. |
Frontière de responsabilité
Section intitulée « Frontière de responsabilité »atlas: tout ce qui est applicatif (code, images, manifestes de ses propres services, assets Dagster, schéma de l’index).cluster: tout ce qui est infrastructure (addonsplatform/<addon>/, Ansible, opérateurs, exposition, observabilité), et le déploiement réel, qui reste une action humaine validée sur le banc avant la prod.- Aucun manifeste d’infrastructure ne vit dans
atlas; aucun code applicatif ne vit danscluster.
Accepted (2026-06-02). Complète ADR 0029 (architecture) et ADR 0031 (outil générique) ; ne crée aucune contrainte technique nouvelle, il rend explicite un contrat jusqu’ici implicite.
Conséquences
Section intitulée « Conséquences »Bénéfices. Les deux dépôts (et leurs agents) ont une référence unique des
points de contact : un nom de bucket, un format de manifeste, une version
d’extension ne peuvent plus diverger en silence sans qu’un côté contredise ce
document. Tout déployeur tiers sait, en une page, ce que l’application attend
de son cluster — sans lire le code. La frontière de responsabilité est nette, ce
qui évite que de l’infrastructure ne fuite dans atlas ou que du code applicatif
ne fuite dans cluster.
Prix à payer. Le contrat est tenu par discipline, pas par un test automatisé : un changement non répercuté ici reste possible (le garde-fou est humain, pas mécanique). Il faut le maintenir à jour dans la PR qui change un point de contact — sinon il périme, comme toute documentation non vérifiée. Le couplage par valeurs d’instance (noms d’hôtes, IP) reste à la charge de chaque déployeur via sa configuration.
Garde-fous.
- Tout changement d’un point de contact (nom de bucket, namespaces, format de manifeste, version d’un composant fourni) met à jour ce document dans la même PR que le code ou l’infrastructure concernés.
- Le nommage suit ADR 0022 des deux côtés :
citation, jamais la marque, dans tout identifiant partagé (bucket, namespaces, secrets). - Les valeurs propres à une instance (hôtes, IP, tailles, base légale) vivent dans la configuration de l’instance, pas dans ce contrat ni dans le code générique (ADR 0031).
- Si une dérive silencieuse cause une panne malgré le contrat, on promeut la vérification au niveau supérieur (checks statiques inter-dépôts, puis smoke-test au banc) — pas avant qu’une douleur réelle ne le justifie.
- Le déploiement réel reste une action humaine validée sur le banc Vagrant ; aucun agent ne le déclenche automatiquement.
Évolution (2026-06-04) — Stratégie d’images de déploiement
Section intitulée « Évolution (2026-06-04) — Stratégie d’images de déploiement »Le contrat ci-dessus dit ce que l’application fournit au cluster (des images
taguées, poussées sur le registry, référencées par les manifestes). Il ne disait
rien, jusqu’ici, de comment ces images sont fabriquées. Le cadre #308
a produit le premier et unique Dockerfile du dépôt —
apps/sillage/Dockerfile —
qui sert désormais de patron recopiable sur les six unités déployables du
monorepo. Cet ajout ne modifie aucun point de contact existant : il fixe la
forme des images livrées, là où le contrat fixait déjà leur tag et leur
destination.
Patron d’image
Section intitulée « Patron d’image »- Version Node alignée sur
.nvmrc. L’image part denode:${NODE_VERSION}-alpineavecARG NODE_VERSION=24, valeur identique au.nvmrcde la racine. La version d’exécution ne peut donc pas diverger de la version de développement : un seul endroit à bumper, vérifiable d’ungrep.corepack enableactive pnpm à la version pinnée par le champpackageManagerdupackage.jsonracine. - Multi-stage
builder/runner. Un stagedepsinstalle toutes les dépendances du workspace (pnpm install --frozen-lockfile, store monté en cache) — devDeps comprises, car le build SvelteKit a besoin de la toolchain (vite/svelte-kit). Le stagebuildercompile l’unité ciblée ; le stagerunnerfinal ne garde que le runtime Node, les artefacts de build et unnode_modulesde prod élagué. Pas de pnpm, pas de sources, pas de devDeps dans l’image livrée. pnpm deploy→node_modulesautonome.pnpm --filter=<unité> --prod --legacy deploy /prodmatérialise unnode_modulesde production résolu hors du symlink-store (deps workspace internes incluses, devDeps écartées), copiable seul dans lerunner. C’est ce qui permet une image finale minimale sans embarquer le store pnpm ni le graphe workspace complet. Le flag--legacyest requis par pnpm 10 pour cibler un filtre depuis la racine du monorepo.USER nodenon-root. Lerunnerchowne/apppuis bascule sur l’usernode(uid 1000) fourni par l’image officielle : aucune charge applicative ne tourne en root, conformément à la posture de durcissement attendue côté cluster.PORT/HOSTau runtime. L’adapter Node litPORT/HOSTà l’exécution ; l’image fixeENV PORT=5173etENV HOST=0.0.0.0par défaut, surchargables par leDeployment. LeService/containerPortdu manifeste s’aligne sur cette valeur.HEALTHCHECK↔ probe Kubernetes. L’image déclare unHEALTHCHECK(HTTPGET /sur le port d’écoute) ; côté cluster, il se traduit enreadinessProbe/livenessProbedans leDeployment. Le contrat est : toute unité déployable répond sur son endpoint de santé, pour qu’Argo CD et le scheduler ne routent du trafic que vers des pods prêts.
Injection des variables : PUBLIC_* au build, PRIVATE_* au runtime
Section intitulée « Injection des variables : PUBLIC_* au build, PRIVATE_* au runtime »La frontière la plus piégeuse est l’injection de configuration, et elle découle directement de SvelteKit :
| Type | Source SvelteKit | Moment | Mécanisme image |
|---|---|---|---|
PUBLIC_* | $env/static/public | figé au build | build-args (ARG + ENV du builder), inlinés par vite/SvelteKit dans le bundle |
PRIVATE_* | $env/static/private | runtime | environment: / Secret du pod, jamais en dur dans l’image |
Les PUBLIC_* (endpoints publics, URLs de login, etc.) sont inlinés au moment du
build : ils doivent être fournis comme build-args à docker build, pas comme
variables d’environnement du conteneur — une PUBLIC_* posée au runtime serait
ignorée, le bundle étant déjà figé. À l’inverse, les valeurs privées (clés
d’API, tokens, secrets) sont lues à l’exécution par l’adapter Node et arrivent
via les Secret/environment: du pod — jamais dans l’image, donc jamais dans
une couche poussée au registry.
Garde-fous (en plus de ceux ci-dessus)
Section intitulée « Garde-fous (en plus de ceux ci-dessus) »apps/sillage/Dockerfileest la référence recopiable : toute nouvelle unité déployable repart de ce patron (multi-stage,pnpm deploy,USER node, healthcheck) plutôt que d’unDockerfilead hoc. Une divergence de forme entre images est traitée comme une dette à résorber vers le patron, pas comme une variation légitime.- Le contexte de build est la racine du monorepo (le lockfile et les
package.jsonworkspace y vivent) ; leDockerfilese passe via-f. - Une
PUBLIC_*qui doit varier par instance se passe en build-arg au moment de fabriquer l’image de cette instance ; une valeur secrète ne transite jamais par unARG/ENVdubuilder(elle resterait dans l’historique des couches). - Le bump de version Node se fait à
.nvmrcetARG NODE_VERSIONdans la même PR ; les deux ne doivent pas diverger..nvmrcest figé au patch (p. ex.24.15.0) pour une parité dev/prod exacte (facteur X), tandis queengines.nodereste un plancher de compatibilité (^24) pour ne pas bloquer l’install —engine-strict=true— sur un patch 24.x différent. - Les unités dont la config est runtime/publique (
atlas-dashboard,crf-dashboard, service) ont une image sur ce patron, construite et fumée en CI puis publiée sur GHCR (tag SHA immuable + version, jamaislatesten déploiement) — voir ADR 0043. Les apps qui lisent des secrets via$env/static/private(amarre,ecrin,find-an-expert,sillage) restent à migrer vers$env/dynamic/privateavant d’être imageables (#324) —static/privatefige les valeurs au build, ce qui violerait la règle « aucun secret dans une image ».
Évolution (2026-06-15) — Suivi de modèles (MLflow)
Section intitulée « Évolution (2026-06-15) — Suivi de modèles (MLflow) »Le passage du MLOps au niveau 2 (ADR 0062)
ajoute un point de contact Suivi de modèles : un serveur MLflow déployé
côté cluster (ADR cluster séparé, sur le patron Dagster/Marquez), joignable
par l’application via MLFLOW_TRACKING_URI, avec backend store CloudNativePG et
artefact store sur le bucket S3 (convention citation). L’application — assets
Dagster de dataops/ — logue ses runs d’embedding et enregistre le modèle au
registre ; l’instrumentation dégrade proprement (no-op) si MLFLOW_TRACKING_URI
est absent, comme le lineage sans OPENLINEAGE_URL. Cet ajout ne modifie aucun point
de contact existant : il en crée un nouveau, sur le même partage que Dagster
(orchestrateur côté cluster, code-location côté atlas).
Évolution (2026-06-29) — Cache de flux (CNPG)
Section intitulée « Évolution (2026-06-29) — Cache de flux (CNPG) »Le cache applicatif des flux (ADR 0040)
acquiert son back-end réel : un point de contact Cache de flux (ligne ajoutée au
tableau ci-dessus), servi par une base logique cache sur le CloudNativePG existant
— pas de nouvelle brique (ADR cluster 0093).
Le cluster fournit base, rôle, Secret pg-role-cache et les variables POSTGRES_CACHE_*
(endpoint postgres-cache, déjà publié de son côté) ; l’application fournit
l’adaptateur (table clé-valeur + UPSERT + pg_advisory_lock,
ADR 0085). Le DSN utilise le
nom court pg-rw.postgres (le FQDN *.svc.cluster.local timeout en prod). Cet ajout
ne modifie aucun point de contact existant : il en crée un nouveau, sur le même
CloudNativePG que pgvector.