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 <noreply@anthropic.com>
8.3 KiB
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-01codé en dur dans lesdefaults(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 à unedge;scripts/domaines.pysait déjà lister les expositions déclarées par les applications — mais aucune app n'en déclare. - Applications (
plan/applications.yml) : seulementgroupe+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 :
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 :
# 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} :
- résoudre
vers→ entité cible (app / base / domaine) et ses attributs ; - charger le
meta/liens.ymldu rôle du consommateur ; trouveraccepte[role]; - valider que le genre de la cible ==
cibleattendue ; - substituer les attributs dans chaque gabarit de variable ;
- injecter selon
injecte_sur:consommateur→host_varsde l'hôte du consommateur (défaut) ;cible/edge→host_varsde 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: expositiond'une app vers un domaine dedomaines.yml. Sa résolution écrit sur l'edge (injecte_sur: edge) : un vhost/upstream nginxgit.chezlepro.ca → forgejo-fqdn:port.
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.ymlgarde 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/porteesur 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
versrésout vers exactement une entité (sinon erreur ; désambiguïser par registre) ; - tout
roleest accepté par le rôle du consommateur (meta/liens.yml) ; - le genre de la cible correspond à
cible; - pour une base, le
secretnommé 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.ymldes 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
versentre registres (app / base / domaine) ; host_varsdu consommateur vsgroup_varsdu groupe (défaut : host_vars, plus précis) ;- format exact des gabarits (
{cible.fqdn}) et jeu d'attributs figés ; - compat ascendante du
consommateurdes bases pendant la migration.