From 4454e1e18bff73eed1290b5135db4a3fedb23b3c Mon Sep 17 00:00:00 2001 From: Daniel Allaire Date: Fri, 3 Jul 2026 05:29:24 -0400 Subject: [PATCH] docs: conception des bindings (relations app/base/serveur/domaine) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Note d'architecture. Direction retenue : liens déclarés côté application (liens: [{vers, role}]), résolus par instancier.py en variables Ansible ; chaque rôle décrit les liens acceptés dans meta/liens.yml ; FQDN cible dérivé de la nomenclature. Domaines = lien exposition (écrit sur l'edge). Preuve de migration ciblée : les 3 liens mail. Implémentation phasée à suivre. Co-Authored-By: Claude Opus 4.8 --- CHANGELOG.md | 9 ++ docs/bindings-conception.md | 194 ++++++++++++++++++++++++++++++++++++ 2 files changed, 203 insertions(+) create mode 100644 docs/bindings-conception.md diff --git a/CHANGELOG.md b/CHANGELOG.md index e1d0131..9c16cf4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,15 @@ ## 2026-07-02 ### Décidé +- **Bindings — conception des relations app/base/serveur/domaine (`docs/bindings-conception.md`).** + Les relations service→service sont aujourd'hui codées en dur, éparpillées dans des group_vars + (ex. Postfix→Dovecot/rspamd/LDAP), ce qui casse la portabilité multi-tenant. Direction retenue : + **liens déclarés côté application** (`liens: [{vers, role}]`), résolus par `instancier.py` en + variables Ansible ; chaque rôle décrit les liens qu'il accepte dans `meta/liens.yml` (comme + `meta/empreinte.yml`) ; FQDN cible **dérivé de la nomenclature** (jamais codé en dur). Domaines + publics traités comme lien `exposition` (écrit sur l'edge). Réconcilie l'existant (bases + `consommateur`, `domaines.edge`). Preuve de migration ciblée : les 3 liens mail. Implémentation + à suivre (phasée). - **Licence : passage de CC BY-NC-SA 4.0 à AGPLv3.** Les licences Creative Commons ne sont pas faites pour du logiciel (position de CC elle-même) et la clause **NonCommercial contredisait le principe fondateur « tout est libre »** — en plus de bloquer les artisans/coopératives diff --git a/docs/bindings-conception.md b/docs/bindings-conception.md new file mode 100644 index 0000000..92d9fea --- /dev/null +++ b/docs/bindings-conception.md @@ -0,0 +1,194 @@ +# Bindings — conception des relations entre applications, bases, serveurs et domaines + +> Note de conception, 2026-07-02. Décision d'architecture à valider avant implémentation. +> Direction retenue : **liens déclarés côté application (le consommateur déclare ses besoins)**. + +## 1. Problème + +Les relations **service → service** ne sont nulle part dans le plan : elles sont codées en dur, +éparpillées dans des `group_vars` / `defaults`. Exemples réels, produits à la main pour la +messagerie : + +- `serveur_postfix_mailstore_hote: "infra-mail-01.lab.chezlepro.internal"` (Postfix → Dovecot) ; +- `serveur_postfix_rspamd_milter: true` (Postfix → rspamd) ; +- `id-ldap-01` codé en dur dans les `defaults` (Postfix / Dovecot → OpenLDAP). + +Conséquence : **la topologie n'est pas déclarative**. Déplacer Dovecot sur un autre hôte oblige à +éditer des variables à la main. **Cela échoue à l'épreuve de la portabilité multi-tenant** — +pourtant au cœur de la mission. + +## 2. Ce qui existe déjà (partiel) + +Le concept est à moitié né, mais éclaté et non câblé : + +- **Bases** (`plan/bases-donnees.yml`) : `consommateur` + `portee` (groupe|hote|application) + + `secret` (Vault) + `usage` + `proprietaire`. Un vrai binding base → consommateur, mais vide et + non injecté dans l'inventaire. +- **Domaines** (`plan/domaines.yml`) : un domaine se lie à un `edge` ; `scripts/domaines.py` sait + déjà lister les **expositions déclarées par les applications** — mais aucune app n'en déclare. +- **Applications** (`plan/applications.yml`) : seulement `groupe` + `hote`. Aucun lien app → app. +- **Rôles** : portent déjà une méta auto-descriptive (`meta/empreinte.yml`). + +## 3. Modèle proposé + +**Un binding est une arête typée et dirigée**, d'un **consommateur** (une application) vers une +**cible** (une autre application, une base, ou un domaine public). `scripts/instancier.py` le +**résout en variables Ansible**. On déclare la *relation* ; l'outil dérive le *câblage*. + +### 3.1 Où vivent les liens : côté application (décision) + +Le consommateur déclare ses besoins, dans `plan/applications.yml` : + +```yaml +applications: + postfix: + groupe: serveur_postfix + hote: edge-mta-01 + liens: + - vers: dovecot # autre application + role: mailstore # nature du lien + - vers: rspamd + role: milter + - vers: openldap + role: annuaire +``` + +Rationale : **localité et lisibilité** (on lit une app et on voit de quoi elle dépend), et +symétrie avec le déploiement (on pousse une app → ses liens sont là). Les entités cibles restent +définies dans leur propre registre (`bases-donnees.yml`, `domaines.yml`) ; seules les **arêtes** +vivent sur le consommateur. + +### 3.2 Les rôles décrivent les liens qu'ils acceptent + +Chaque rôle Ansible porte un `meta/liens.yml` (comme `meta/empreinte.yml`) qui dit quels liens il +accepte et **quelles variables ils remplissent** : + +```yaml +# roles/serveur_postfix/meta/liens.yml +--- +setops_liens: + accepte: + mailstore: + cible: application + injecte_sur: consommateur + variables: + serveur_postfix_mailstore_hote: "{cible.fqdn}" + milter: + cible: application + injecte_sur: consommateur + variables: + serveur_postfix_rspamd_milter: true + annuaire: + cible: application + injecte_sur: consommateur + variables: + serveur_postfix_ldap_serveur: "ldaps://{cible.fqdn}:636" +``` + +Le **moteur reste générique** ; la connaissance « lien → variables » reste **dans le rôle**. + +### 3.3 Attributs de la cible (substitution) + +Les gabarits de variables utilisent les attributs résolus de la cible : + +| Placeholder | Application | Base | Domaine | +|---|---|---|---| +| `{cible.hote}` | nom de VM | hôte de la base | — | +| `{cible.fqdn}` | FQDN interne dérivé | FQDN interne | — | +| `{cible.ip}` | IP dérivée | IP | — | +| `{cible.port}` | — | port | — | +| `{cible.secret}` | — | nom de la variable Vault | — | +| `{cible.base}` | — | nom de la base | — | +| `{cible.domaine}` | — | — | nom public | +| `{cible.edge}` | — | — | edge qui sert | + +Le **FQDN interne est dérivé de la nomenclature** (`hote` + `domaine_interne`) — jamais codé en +dur. C'est ce qui rend la topologie portable. + +### 3.4 Algorithme de résolution (instancier) + +Pour chaque application portant des `liens`, pour chaque lien `{vers, role}` : + +1. résoudre `vers` → entité cible (app / base / domaine) et ses attributs ; +2. charger le `meta/liens.yml` du rôle du **consommateur** ; trouver `accepte[role]` ; +3. valider que le genre de la cible == `cible` attendue ; +4. substituer les attributs dans chaque gabarit de variable ; +5. injecter selon `injecte_sur` : + - `consommateur` → `host_vars` de l'hôte du consommateur (défaut) ; + - `cible` / `edge` → `host_vars` de l'hôte cible / de l'edge (cas exposition). + +## 4. Les domaines : un genre de lien distinct + +À considérer **avec** les bindings, sans les confondre : + +- **FQDN interne** (service → service) : dérivé de la nomenclature. Pas de `domaines.yml`. +- **Domaine public** (exposition) : espace d'adressage + edge. Modélisé comme un lien + `role: exposition` d'une app vers un domaine de `domaines.yml`. Sa résolution écrit **sur + l'edge** (`injecte_sur: edge`) : un vhost/upstream nginx `git.chezlepro.ca → forgejo-fqdn:port`. + +```yaml +applications: + forgejo: + groupe: serveur_forgejo + hote: git-01 + liens: + - vers: git.chezlepro.ca # domaine public (domaines.yml) + role: exposition + - vers: postgresql-forgejo # base (bases-donnees.yml) + role: base +``` + +`domaines.yml` reste le **registre** des domaines publics (autorité, DNSSEC, secondaires, edge) ; +le lien app ↔ domaine relie enfin app + domaine + edge, aujourd'hui séparés. + +## 5. Réconcilier l'existant (bases) + +Le binding base existe aujourd'hui **côté base** (`consommateur` pointe l'app). On l'aligne sur la +direction retenue : + +- `bases-donnees.yml` garde la **définition** de la base (type, hôte, port, `secret`, + `proprietaire`) — la base reste une entité à part entière ; +- le **lien de consommation** passe côté app (`role: base`) ; la résolution injecte chez le + consommateur la chaîne de connexion + le **nom** de la variable Vault (jamais le secret) ; +- `consommateur` / `portee` sur la base deviennent une **rétro-référence calculée** (ou sont + dépréciés). Migration douce. + +## 6. Règles de validation + +- tout `vers` résout vers **exactement une** entité (sinon erreur ; désambiguïser par registre) ; +- tout `role` est **accepté** par le rôle du consommateur (`meta/liens.yml`) ; +- le genre de la cible correspond à `cible` ; +- pour une base, le `secret` nommé existe dans la voûte ; +- cycles tolérés s'ils sont légitimes (ex. mTLS mutuel), signalés sinon. + +## 7. Interface (GUI) + +- vue « Liens » / éditeur de bindings par application ; +- **graphe des liens** — visualisation naturelle (le GUI a déjà un graphe d'inventaire), très + parlante pour la **démo de portabilité** (déplacer un nœud, voir les arêtes suivre). + +## 8. Preuve de migration (les 3 liens mail) + +Premier cas concret, à faire en régression (les mêmes variables doivent être générées) : + +| Avant (codé en dur) | Après (déclaratif) | +|---|---| +| `serveur_postfix_mailstore_hote` (group_var) | `postfix.liens: [mailstore → dovecot]` | +| `serveur_postfix_rspamd_milter` (group_var) | `postfix.liens: [milter → rspamd]` | +| `id-ldap-01` (defaults) | `postfix.liens: [annuaire → openldap]`, `dovecot.liens: [annuaire → openldap]` | + +## 9. Phasage + +- **Phase 0** : cette note + décision. ✅ (direction : liens côté app) +- **Phase 1** : résolveur de liens dans `instancier.py` + `meta/liens.yml` des rôles mail ; + migrer les 3 liens mail ; prouver que le déploiement reste identique (régression). +- **Phase 2** : réconcilier les bases (consommateur → liens app ; chaîne de connexion). +- **Phase 3** : exposition / domaines (vhosts nginx dérivés des liens). +- **Phase 4** : vue GUI des liens + graphe. + +## 10. Décisions ouvertes + +- désambiguïsation de `vers` entre registres (app / base / domaine) ; +- `host_vars` du consommateur vs `group_vars` du groupe (défaut : host_vars, plus précis) ; +- format exact des gabarits (`{cible.fqdn}`) et jeu d'attributs figés ; +- compat ascendante du `consommateur` des bases pendant la migration.