Aller au contenu

ADR-0009 — Taxonomie des templates de déploiement : audience × tenancy × scenario × gouvernance

Date : 2026-06-10 (amendée 2026-06-21) Catégorie : A (convention d'outillage SIRRAT/TAKKU, roadmap plateforme validée) Décideur : Lead architecte DYNORS Statut : Acceptée (avec amendements 2026-06-21 — voir §Amendements) Sprint d'implémentation : amorcé (CODEX_SCENARIO, CODEX_DEPLOY_TARGET déjà constants ; reste schéma catalog + colonnes profil + élargissement catalogue + gate isolation)


Contexte

Le catalogue de templates SIRRAT (sirrat_template_catalog + sirrat_template_variable, Codex SirratCodexConstants) définit aujourd'hui quatre familles :

  • dynors-spring-saas@2.0.0
  • dynors-angular-portal@2.0.0
  • dynors-pos@2.0.0
  • dynors-worker@2.0.0

Ces familles décrivent la forme technique d'un composant (backend Spring, front Angular, POS, worker). Mais elles ne capturent pas trois dimensions qui changent réellement le déploiement, la surface d'exposition et les gates :

  1. Audience — l'app est-elle un SaaS exposé à des clients externes (DAWALALE, SuperGest, Mediconnect, ELISA) ou un outil à usage interne DYNORS sur domaine dynors.com (JARAAF, BOOKS, RAGNAR, HEISENBERG) ? Aujourd'hui JARAAF et BOOKS reçoivent le même template spring-saas qu'un SaaS client, alors qu'ils n'ont ni le même besoin d'isolation, ni les mêmes cercles d'environnement, ni le même domaine.
  2. Tenancy — base partagée multi-tenant (dynors.db.multitenant + RLS) ou instance dédiée par client. DAWALALE est en SHARED ; un déploiement on-premise client est de facto DEDICATED.
  3. Scenario — le champ existe déjà dans sirrat.config.yml (managed | on-premise | cloud-client | regie) mais ne pilote ni le choix de template, ni la convention d'URL, ni les gates. Il est descriptif, pas normatif.

Deux réalités du portefeuille montrent en outre que ces dimensions ne sont pas des propriétés figées de l'app :

  • FISCAL est un SaaS vendu à des clients, mais DYNORS l'utilise aussi pour lui-même. La même instance sert les deux usages : DYNORS est simplement un tenant de son propre SaaS (« tenant zéro », dogfooding). Classer FISCAL « interne » ou « saas » de façon exclusive serait faux dans les deux cas.
  • DAWALALE est aujourd'hui un SaaS multi-tenant opéré par DYNORS, mais sa filialisation est prévue. Filialisée avec un seul tenant, son déploiement devient plus simple (mono-tenant de fait), et sa gouvernance change (entité légale propre, compte propre).

Résultat : la distinction SaaS / interne / on-premise vit dans la tête des gens, pas dans le catalogue, et le catalogue ne sait pas représenter une app qui change de profil dans le temps ou qui en cumule deux. C'est une source de friction et d'erreur exactement là où on veut industrialiser.

Décision

Faire des familles de templates le produit cartésien d'axes explicites, faire de scenario une variable normative qui conditionne la résolution, et attacher le profil au déploiement, pas à l'app.

Principe fondateur — le profil est une propriété du déploiement

Une app n'est pas « SaaS » ou « interne » dans l'absolu : chaque déploiement (instance × cercle) porte son propre profil, et ce profil peut évoluer sans changer l'identité de l'app. Conséquences :

  • FISCAL : un seul déploiement saas · shared · managed · dynors. L'usage interne DYNORS n'est pas une seconde instance « internal » — DYNORS est le tenant dynors de son propre SaaS. Interdiction de dériver une instance interne séparée (double maintenance, divergence).
  • DAWALALE : aujourd'hui saas · shared · managed · dynors ; à la filialisation, le déploiement cible devient saas · dedicated · managed · filiale (mono-tenant de fait). Le changement de profil est un événement de gouvernance tracé (nouvelle ligne de mapping, datée), pas une réécriture de l'app. Le code garde TenantContext et un tenantCode unique — retirer le multi-tenant coûterait plus cher que de le laisser dormant, et préserve un retour au multi-tenant (réseau d'auto-écoles) sans migration.

Les quatre axes

Axe Constante Codex Valeurs Pilote
deployment_audience CODEX_DEPLOYMENT_AUDIENCE internal | saas domaine, cercles d'environnement, exposition réseau
tenancy CODEX_TENANCY shared | dedicated isolation DB (RLS vs instance), gate d'isolation tenant
scenario CODEX_SCENARIO (existant) managed | on-premise | cloud-client | regie topologie de déploiement, origine des secrets, qui opère
gouvernance CODEX_GOUVERNANCE dynors | filiale | client entité légale propriétaire : compte de reversement (ADR-0010), contrats, domaine de marque, qui arbitre

Amendement 2026-06-21 — collision sémantique évitée. L'axe est nommé deployment_audience (constante CODEX_DEPLOYMENT_AUDIENCE) et non audience, pour éviter la confusion avec TAIL_AUTH_AUDIENCE qui désigne le realm JWT (ADR-0011). Les deux concepts coexistent : l'un est un profil de déploiement, l'autre est un claim de token. Tout code, seed Liquibase ou schema JSON qui mentionne audience sans préfixe doit être considéré comme un défaut de conformité.

La forme technique (spring-saas, angular-portal, pos, worker) reste orthogonale : un template effectif = forme technique + profil (audience, tenancy, scenario, gouvernance).

L'axe gouvernance ne change pas la topologie technique à lui seul — il pilote ce qui relève de DYNORS Groupe : vers quel compte SATELLITE part le reversement (la filiale a son entité légale et son compte propre), qui signe les contrats, qui possède le domaine de marque, et qui arbitre les évolutions. Une app gouvernance = filiale reste hébergée sur les plateformes DYNORS (SLY, PAIEMENT, FISCAL) tant que la doctrine Groupe ne décide pas autrement.

Conséquences normatives par profil (extraits)

Profil Domaine Cercles Exposition API Secrets
internal · shared · managed (JARAAF, BOOKS) *.{app}.dynors.com dev, rmoa-dynors, prod API interne (pas de cercle client) Vault DYNORS
saas · shared · managed (DAWALALE, SuperGest) domaine propre en prod, *.dynors.com hors-prod dev, rmoa-dynors, rmoa-client, prod API exposée + WAF/CORS strictes Vault DYNORS
saas · dedicated · on-premise (client souverain) domaine client dev, rmoa-client, prod client API exposée chez le client Ansible Vault / secrets client
saas · dedicated · cloud-client domaine client, infra client idem idem ESO sur cluster client

Cible de déploiement par cercle — socle mutualisé vs pod dédié

À l'intérieur d'un cercle, chaque déploiement a une cible : un socle technique mutualisé (un hôte du cercle, plusieurs containers en cohabitation, ports distincts — le terme interne peut varier, ex. « omnical ») ou un pod dédié (hôte/namespace propre, isolation réseau). C'est la 5ᵉ propriété du profil, dérivée des 4 autres et de la sensibilité de l'app — pas une décision indépendante.

Règle de dérivation (vérifiée par SIRRAT Gate 0) :

Condition Cible
App de sensibilité élevée : paiement, fiscal pod (toujours, tous cercles)
scenario ∈ {on-premise, cloud-client} pod (infra séparée par définition)
cercle ∈ {rmoa-{client}, production} ET audience = saas pod
tenancy = dedicated pod
Tout le reste (internal ou saas-shared-managed hors prod) socle

Conséquence : les apps internes (JARAAF, BOOKS, RAGNAR, HEISENBERG) et les SaaS partagés en dev/rmoa-dynors tournent sur un même socle par cercle (économie + base unique à durcir). DYNORS PAIEMENT et FISCAL sont toujours en pod (argent, blast-radius ADR-0008, surface réglementaire DGI). Les cercles client et la prod SaaS sont en pod (isolation).

Conséquences techniques :

  • Codex CODEX_DEPLOY_TARGET ∈ {socle, pod} — résolu au manifeste, jamais saisi à la main.
  • Nginx (rôle dynors-nginx-proxy) : l'upstream api_upstream_host vaut socle-{circle}.intranet.dynors.com pour la cible socle, {app}-api-{circle}.pods.dynors.com (ou nom de service K8s) pour pod. Conventions de nommage par cercle à acter avec l'infra (DNS interne).
  • L'app ne sait rien de sa cible : la convention URL publique (api.{app}{suffixe}.dynors.com/v{n}) reste identique. Seul l'edge nginx connaît l'upstream — c'est précisément ce qu'on veut.

Règles dérivées

  • Une app internal n'a pas de cercle rmoa-client ni de domaine propre — TAKKU ne les génère pas, SIRRAT Gate 0 les refuse.
  • Une app saas en shared doit passer un gate d'isolation tenant (test prouvant qu'un tenant ne lit pas les données d'un autre) avant promotion vers un cercle exposé. Ce gate n'existe pas aujourd'hui — il devient obligatoire pour ce profil à partir du 2026-09-30 (cf. §Gate isolation — date butoir).
  • scenario = on-premise | cloud-client impose une origine de secrets non-DYNORS (Ansible Vault ou ESO client) — SIRRAT refuse un manifeste qui pointerait le Vault DYNORS depuis un déploiement client.
  • L'axe tenancy = dedicated se combine avec lane (ADR-0017/0019) : un déploiement dédié à un client peut être réalisé soit par un couloir dédié dans un cercle (cas standard hors-prod), soit par une stack distincte (cercle client en scenario=on-premise|cloud-client). SIRRAT Forge refuse une combinaison (tenancy=dedicated, scenario=managed, lane=ref) en cercle exposé — il faut soit nommer le couloir, soit changer de scénario.

Gate d'isolation tenant — date butoir et waiver

  • À partir du 2026-09-30 : SIRRAT Gate 2 (avant promotion vers production) refuse toute promotion d'un déploiement deployment_audience=saas · tenancy=shared sans rapport de gate d'isolation tenant vert daté de moins de 90 jours.
  • Avant le 2026-09-30 : promotion autorisée avec waiver explicite (sirrat.config.yml: gates.tenant_isolation.waiver_until: 2026-09-30) signé par le lead architecte ; le waiver est tracé dans le journal de gouvernance et un ticket de mise en conformité est ouvert automatiquement.
  • Périmètre minimal du gate (à outiller) : test automatisé multi-tenant qui (i) crée deux tenants distincts, (ii) écrit des données dans chacun, (iii) tente une lecture cross-tenant via toutes les routes API publiées, (iv) échoue si au moins une route retourne 200 avec des données du second tenant. Le rapport est stocké dans sirrat_gate_report avec digest signé.

Conséquences

Bénéfices

  • Le catalogue encode la réalité : plus de confusion SaaS / interne, plus de cercle client fantôme sur une app interne.
  • La surface d'exposition devient une propriété déclarée et vérifiée, pas une convention orale : une app interne n'expose pas d'API publique par défaut.
  • L'isolation tenant des SaaS partagés devient un gate, pas une supposition.
  • TAKKU peut générer le bon squelette du premier coup (cercles, domaine, secrets) — moins de reprise manuelle.

Coûts / contraintes induites

  • Migration du catalogue : ajouter les axes aux templates existants et reclasser les apps actuelles (mapping ci-dessous à acter).
  • Le gate d'isolation tenant doit être conçu et outillé (test type « cross-tenant read » automatisé).
  • TAKKU et SIRRAT Gate 0 doivent lire et valider les quatre axes — évolution de leur logique de validation.
  • Tout changement de profil d'un déploiement (ex. filialisation DAWALALE) est un événement de gouvernance : nouvelle ligne datée du mapping, validation CTO si l'axe gouvernance change.

Composants impactés

  • dynors-internal/applications/sirratSirratTemplateCatalog (schéma : ajout audience/tenancy/gouvernance, scenario rendu normatif), EnvironmentManifestService, validations Gate 0.
  • TAKKU — génération conditionnée par profil.
  • SirratCodexConstants — constantes d'axes (CODEX_AUDIENCE, CODEX_TENANCY, déjà CODEX_SCENARIO).
  • Convention URL (handoff) — formaliser « domaine propre = profil saas, prod uniquement ».

Mapping initial des déploiements (à acter)

Le mapping est par déploiement et daté — une ligne nouvelle (jamais une réécriture) quand un profil évolue :

Déploiement audience tenancy scenario gouvernance Note
JARAAF, BOOKS, RAGNAR, HEISENBERG internal shared managed dynors usage interne, domaines *.dynors.com
FISCAL saas shared managed dynors DYNORS = tenant dynors de son propre SaaS — pas d'instance interne séparée
DAWALALE (aujourd'hui) saas shared managed dynors
DAWALALE (cible filialisation) saas dedicated managed filiale mono-tenant de fait ; compte SATELLITE de la filiale (ADR-0010) ; date à acter par le CTO
SuperGest, Mediconnect, TRACIUM saas shared managed dynors
Déploiement client souverain (cas par cas) saas dedicated on-premise / cloud-client client secrets client, gate 3 + PV

Alternatives considérées

Alternative A — Garder les 4 familles techniques uniquement. Écartée : ne capture pas l'audience ni la tenancy, laisse la distinction SaaS/interne hors du système. C'est l'état actuel, qui produit la friction.

Alternative B — Une famille par app (templates dédiés). Écartée : explosion combinatoire, duplication, perte de la mutualisation qui est la raison d'être du catalogue.

Alternative C — Encoder seulement audience (binaire interne/saas). Insuffisant : on-premise et dedicated changent l'origine des secrets et la topologie indépendamment de l'audience. Les axes sont nécessaires et non redondants ; gouvernance est le seul qui ne change pas la topologie technique mais détermine les flux d'argent et de contrats (ADR-0010).

Plan d'exécution

Sprint 1 (immédiat, 2026-06 → 2026-07)

  1. Schéma sirrat_template_catalog — publier une nouvelle version v2.1.0 de chaque template existant (dynors-spring-saas, dynors-angular-portal, dynors-pos, dynors-worker) plutôt que muter schema_json de v2.0.0. Bénéfice : les manifestes pinnés sur v2.0.0 ne cassent pas ; la migration est explicite (takku upgrade-template). Les colonnes deployment_audience, tenancy, gouvernance sont ajoutées au catalog mais seules les versions ≥ 2.1.0 les exigent (validation conditionnelle dans EnvironmentManifestService).
  2. Constantes Codex : ajouter CODEX_DEPLOYMENT_AUDIENCE, CODEX_TENANCY, CODEX_GOUVERNANCE (en cohérence avec CODEX_SCENARIO et CODEX_DEPLOY_TARGET déjà existants).
  3. Acter le mapping initial des déploiements (tableau ci-dessous) en seed Liquibase (sirrat_deployment_profile), avec colonnes datées (valid_from, valid_until).
  4. Faire lire les axes par Gate 0 : refuser cercle client sur internal, refuser Vault DYNORS sur scénario client, refuser combinaisons interdites.
  5. Documenter dans le Codex de référence (SIRRAT_CODEX_REFERENCE.md) et la convention URL (handoff).

Sprint 2 (2026-07 → 2026-09)

  1. Élargir le catalogue : publier les nouveaux templates listés au §Catalogue élargi.
  2. Adapter TAKKU pour générer cercles/domaine/secrets selon le profil et la famille étendue (mobile, doc-site, landing…).
  3. Concevoir et outiller le gate d'isolation tenant pour deployment_audience=saas · tenancy=shared. Outil : module test transverse dans dynors-extensions/dynors-tenant-isolation-check.
  4. Filer le waiver tenant_isolation pour les SaaS existants (DAWALALE, SuperGest, Mediconnect, ELISA) jusqu'au 2026-09-30 ; ticket de mise en conformité par app.

Sprint 3 (2026-09 → 2026-10)

  1. Activer le refus Gate 2 sans rapport vert au 2026-09-30 (kill switch piloté par config SIRRAT, pas par code).
  2. ADR-0010 si accepté : câbler gouvernance sur le routage payouts DYNORS PAIEMENT (compte SATELLITE filiale).

Catalogue élargi — nouveaux templates à publier (Sprint 2)

Les 4 familles existantes (spring-saas, angular-portal, pos, worker) ne couvrent pas l'ensemble des composants réels du portefeuille. Familles à ajouter, chacune avec ses variables minimales et son schéma schema_json distinct :

Template (nouveau) Famille Cible (exemples du portefeuille) Variables clés additionnelles Cible socle/pod par défaut
dynors-mobile-rn@1.0.0 mobile-rn Mediconnect mobile, SuperGest mobile, DAWALALE mobile MOBILE_API_URL, MOBILE_DEEP_LINK_SCHEME, MOBILE_BUNDLE_ID, MOBILE_STORE_TARGETS (ios/android), MOBILE_PUSH_PROVIDER n/a (artifact store)
dynors-mobile-flutter@1.0.0 mobile-flutter apps Flutter (alt RN) idem + FLUTTER_CHANNEL n/a
dynors-bff@1.0.0 bff BFF dédié front (composition d'appels FISCAL+PAIEMENT+TRACIUM par cas d'usage métier) BFF_UPSTREAMS (liste), BFF_CACHE_TTL, BFF_AGGREGATION_TIMEOUT_MS socle
dynors-adapter@1.0.0 adapter TaxAuthorityAdapter (DGI), adapter PSP, adapter ELISA ADAPTER_PARTNER_CODE, ADAPTER_CIRCUIT_BREAKER_THRESHOLD, ADAPTER_RETRY_POLICY, ADAPTER_HMAC_SECRET_REF pod (sensibilité argent/légal)
dynors-webhook-receiver@1.0.0 webhook-receiver Réception callbacks PSP, callbacks partenaires DGI, callbacks ELISA WEBHOOK_HMAC_SECRET_REF, WEBHOOK_REPLAY_WINDOW_MS, WEBHOOK_IDEMPOTENCY_TTL pod
dynors-cron-batch@1.0.0 cron-batch Jobs périodiques (rapprochement paie, clôture comptable, génération factures programmées) — distinct du worker Kafka qui écoute en continu CRON_SCHEDULE, CRON_JOB_TIMEOUT, CRON_FAILURE_POLICY socle
dynors-migration-job@1.0.0 migration-job Liquibase one-shot, backfill, RGPD purge ponctuelle MIGRATION_TARGET_DB, MIGRATION_DRY_RUN, MIGRATION_LOCK_TIMEOUT n/a (Job K8s ou tâche Ansible)
dynors-gateway-portal@1.0.0 gateway-portal SLY lui-même (apps internes / extension SLY), Portal Edge — routage YAML, pas de DB applicative GATEWAY_ROUTES_FILE, GATEWAY_RATE_LIMIT_DEFAULT, GATEWAY_TLS_MODE pod (toujours)
dynors-doc-site@1.0.0 doc-site dynors-docs (MkDocs), docs satellites DOC_SITE_THEME, DOC_SITE_BASE_URL, DOC_SITE_SEARCH_PROVIDER socle (statique)
dynors-landing-static@1.0.0 landing-static landing dawalale.com, landing supergest.com, page institutionnelle LANDING_DOMAIN, LANDING_ANALYTICS_ID, LANDING_CONTACT_FORM_ENDPOINT socle (CDN possible)
dynors-monitoring-stack@1.0.0 monitoring-stack Stack Loki+Prometheus+Grafana+Tempo (ops/monitoring) — un déploiement par cercle MONITORING_RETENTION_LOKI, MONITORING_RETENTION_PROM, MONITORING_TEMPO_ENABLED, MONITORING_ALERT_CHANNELS pod
dynors-shared-infra@1.0.0 shared-infra Postgres mutualisé, Redis, Kafka — services techniques du cercle (jamais d'app applicative dessus) INFRA_KIND (postgres/redis/kafka), INFRA_VOLUME_SIZE, INFRA_BACKUP_POLICY pod

Principe : un nouveau composant DYNORS choisit une famille de template ; il ne peut pas mixer (ex. pas de spring-saas qui sert aussi de webhook-receiver — c'est deux apps, deux déploiements, deux profils).

Vérification SIRRAT : Gate 0 valide que le composant choisi correspond à une family du catalog active et que sa version est ≥ 2.1.0 pour bénéficier des axes de profil. Les v2.0.0 restent valides mais ne sont pas promues au-delà de dev.

Suivi

Critère de succès : créer une nouvelle app interne via TAKKU ne génère aucun cercle rmoa-client ni domaine propre ; créer une app SaaS partagée bloque la promotion tant que le gate d'isolation tenant n'est pas vert.

Signal de régression : une app interne qui expose une API publique, ou une app SaaS partagée promue sans gate d'isolation, est un défaut de conformité.

Amendements

Date Amendement Origine
2026-06-21 Renommage audiencedeployment_audience (CODEX_DEPLOYMENT_AUDIENCE) pour éviter collision avec TAIL_AUTH_AUDIENCE (realm JWT, ADR-0011). Revue critique avant acceptation.
2026-06-21 Date butoir gate isolation tenant : 2026-09-30, waiver explicite tracé jusque-là. Revue critique avant acceptation.
2026-06-21 Choix migration via publication v2.1.0 (et non mutation de schema_json des v2.0.0 existantes) — préserve les manifestes pinnés. Revue critique avant acceptation.
2026-06-21 Lien explicite tenancy=dedicated × lane (ADR-0017/0019) ; combinaison interdite (dedicated, managed, lane=ref) en cercle exposé. Revue critique avant acceptation.
2026-06-21 Catalogue élargi : ajout de 11 nouveaux templates (mobile-rn, mobile-flutter, bff, adapter, webhook-receiver, cron-batch, migration-job, gateway-portal, doc-site, landing-static, monitoring-stack, shared-infra) — les 4 familles initiales ne couvrent pas le portefeuille réel. Revue critique avant acceptation.

Références

  • Codex : dynors-internal/applications/sirrat/backend/src/main/resources/codex/SIRRAT_CODEX_REFERENCE.md
  • Concept SIRRAT : dynors-docs/docs/07-plateforme-devops/sirrat-concept.md (sirrat.config.yml, scénarios)
  • Convention URL : claude-handoff-dynors/DYNORS_URL_CONVENTION_RULES.md
  • Doctrine Groupe (séparation produit/plateforme) : docs/architecture/DOCTRINE_DYNORS_GROUPE_STRUCTURE_PORTEUSE.md
  • ADR-0011 (realm JWT, TAIL_AUTH_AUDIENCE) — collision sémantique évitée
  • ADR-0017 (couloir/lane) et ADR-0019 (modèle SIRRAT × couloir)
  • ADR-0010 (payouts/trésorerie Groupe) — câblage gouvernance conditionnel