Set-OPS-Public/docs/carte-set-ops.md
Daniel Allaire 8890c997af
Some checks are pending
verifier / verifier (push) Waiting to run
runner : serveur_ops_tenant — le runner d'un tenant recoit enfin sa voute
La doctrine des runners decrit trois portees depuis le 2026-08-22 :

  calculer      plan -> inventaire        aucune voute        serveur_ops
  configurer    roles sur ses machines    voute du TENANT     <- revendiquee, jamais recue
  materialiser  creer/detruire des VM     voute du SITE       serveur_ops_site

La deuxieme ligne etait un trou. RIEN ne deposait jamais la voute d'un tenant sur
son runner. Un runner pouvait deriver son inventaire et ne rien pouvoir en faire :
chaque role qui demande un secret echouait sur son assertion, et l'echec ne disait
pas qu'il manquait un FICHIER, seulement que les valeurs etaient vides.

Decouvert en preparant la reconstruction de Chezlepro. Le runner du SITE peut
materialiser ses quinze machines ; il ne peut pas les configurer, parce que
Chezlepro a sa PROPRE autorite de certification — donc `client_pki` y reclame un
secret de Chezlepro. Le runner du site ne l'a pas, et NE DOIT PAS l'avoir : c'est
la ligne qui rend l'hebergement mutualise defendable.

`serveur_ops_tenant` est le symetrique exact de `serveur_ops_site` : un MARQUEUR,
pas un installateur. Il depose la voute `decrypt: false`, puis RELIT l'en-tete du
fichier depose — une voute dechiffree par accident est une fuite silencieuse. Le
dossier d'inventaire (`principal` ou `production`) se DECOUVRE sur la machine
plutot que d'etre ecrit.

Pourquoi un role a part et non une option de `serveur_ops` : donner sa voute a un
runner est un POUVOIR, pas un reglage. Le declarer au plan force a repondre a
« cette machine a-t-elle le droit de configurer cet ecosysteme ? » — une option
activee par defaut y repondrait a notre place.

CHEZLEPRO LISAIT ENCORE SON GENOME CHEZ PATIENT 0.

Trouve au passage, et bloquant pour la reconstruction : `serveur_ops_forge_amont`
pointait sur `10.29.16.11` — la forge de patient 0, l'amont d'avant que le site
ait la sienne. D-81 a tranche depuis. Laisse tel quel, Chezlepro se serait
reconstruit depuis un moteur perime, sur un reseau que le decoupage en zones a de
toute facon deplace.

Corrige vers la forge du site, PAR SON IP — `forge.genese.internal` appartient a
la zone souveraine du site, et un tenant ne resout que la sienne ; le certificat
SERVI porte bien `IP Address:10.0.33.11`. La racine de l'AC du site est desormais
versionnee a cote de la carte de la fabric a laquelle elle appartient, et son
chemin se DERIVE du symlink `underlay.yml` : il vaut depuis le poste comme depuis
un runner.

Le role est inscrit dans les quatre registres qui l'exigeaient — couches de
deploiement, graphe des dependances, catalogue des services, carte d'orientation.
Ce sont les preuves P08, P38 et P48 qui l'ont reclame, chacune a son tour.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 15:30:51 -04:00

119 lines
12 KiB
Markdown

# Carte d'orientation Set-OPS
> **Pour qui :** le **mainteneur** — celui qui va *modifier* le moteur. À lire avant
> d'ajouter quoi que ce soit.
>
> Tu viens plutôt **exploiter** un écosystème déjà déployé ? Wiki →
> **Reprendre l'écosystème**. Tu **apprends** le métier ? Wiki → **Accueil**.
Point d'entrée vers le corpus documentaire, et **catalogue des mécanismes transverses** —
ceux qui vivent dans le code et qu'on *re-découvre* sinon. Créée le 2026-07-03 après un audit
du dépôt, **revue le 2026-07-29**. But : ne plus re-déterrer ce qui existe.
Le dépôt est **déjà bien documenté**. Le manque n'était pas la doc du *modèle*, mais (a) un
index « par où commencer » et (b) une carte des *mécanismes* (dispersés dans le code + les
README de rôles). Cette page comble ces deux trous.
### Le dépôt en chiffres
> Ces valeurs sont **mesurées**, pas recopiées : **P48** les recompte et refuse tout écart.
> Elles étaient toutes fausses le 2026-08-26 — de 6 rôles, de 12 pièces d'audit, de 8
> décisions. Aucune ne faisait travailler personne ; mais une carte dont les faits
> vérifiables sont faux cesse d'être consultée, et c'est alors ses **pointeurs** qu'on perd.
| Ce qu'on compte | Combien | Comment on le mesure |
|---|---|---|
| rôles | 61 | `roles/*/` |
| README de rôles | 61 | `roles/*/README.md` — l'écart avec la ligne au-dessus est la dette |
| documents | 38 | `docs/*.md` |
| pièces d'audit | 27 | `docs/audit/*` |
| unités de wiki | 27 | `wiki/*.md` |
| décisions en vigueur | 78 | lignes `\| **D-nn** \|` de `decisions-architecture.md` |
| décisions renversées | 3 | lignes `\| **D-nn** —` du même document |
## 1. À lire d'abord (dans l'ordre)
| Sujet | Documents |
|---|---|
| **Autorité / gouvernance** | `AGENTS.md` (source d'autorité), `CLAUDE.md`, `docs/MISE-A-JOUR-CODEX-CLAUDE.md` |
| **Le modèle (plan)** | `docs/architecture-set-ops.md` (survol) → `docs/plan-et-generation.md` (à fond) → `docs/meta-classe.md` (concept) |
| **Services, maturité, dette** | `docs/catalogue-services.md` (**la carte de maturité + la cruft y sont déjà**) |
| **Exploitation / VM** | `docs/vm-lifecycle.md`, `docs/procedure-template-debian13-proxmox.md`, `docs/config-proxmox.md`, `docs/nomenclature-vm.md`, `docs/multi-instances.md` |
| **Conceptions de domaine** | `docs/identite-sso.md`, `docs/courriel-conception.md`, `docs/bindings-conception.md`, `docs/dns-interne.md`, `docs/dimensionnement-ressources.md`, `docs/integrations-vm.md` |
| **Réseau / pare-feu** | `docs/flux-conception.md` (le modèle) → `docs/registre-flux.md` (**généré**, matrice d'audit) → `docs/frontiere-opnsense.md` (la bordure nord/sud) ; underlay : `underlay.yml.example` + `make underlay` |
| **Ordre de déploiement** | `docs/couches-deploiement.yml` (couches) + `docs/dependances-groupes.yml` (graphe) → `playbooks/site.yml` (**généré**, `make site`) |
| **Conformité du déployé** | `docs/devis-services.md` — les **cinq devis de service** (`make identite-plan`, `certificats-plan`, `expositions-plan`, `postgresql-plan`, `courriel-plan`). Répondent à ce que `make prouver` ne demande jamais : *ce qui tourne correspond-il à ce qui est déclaré ?* |
| **Preuve / recette** | `docs/audit/affirmations.md` (registre), `make prouver` → `docs/audit/preuve-<date>.md` — **statique** : lit le dépôt, aucun appel réseau ; la conformité du déployé est l'affaire des devis de service (ligne au-dessus), `docs/audit/plan-de-recette.md` (**généré** du wiki), `docs/audit/protocole-operateur-independant.md` |
| **Décisions d'architecture** | `docs/decisions-architecture.md` — les décisions en vigueur (comptées ci-dessus), pourquoi, où lire le détail, et ce qui les garde ; plus les **décisions renversées** et leur cause |
| **SDN / routage** | `docs/sdn-evpn.md` — décision du 2026-08-02 : le routage inter-zone passe des commutateurs aux hyperviseurs (zones EVPN = VRF). **Non éprouvé** : spike avant génération |
| **Migration de tenant** | `docs/migration-tenant.md` — recette en 8 étapes, machine à états, gardes ; le receveur se construit **avant** tout gel |
| **Exploitation courante** | `docs/runbooks-exploitation.md`, `docs/intrants-communs.md`, `docs/intrants-base-gui-conception.md`, `docs/theme-forgejo-hors-flotte.md` |
| **Pédagogie (le wiki)** | `wiki/` — les unités (comptées ci-dessus) publiées par `make wiki-publier` ; entrer par `wiki/Home.md` |
| **Vision / positionnement** | `docs/ecosysteme-chezlepro.md`, `docs/positionnement.md`, `docs/pouvoirs-set-ops.md` |
## 2. Les mécanismes transverses (et OÙ ils vivent)
Ce que je re-découvre sinon. **Consulter avant de concevoir un nouveau mécanisme.**
| Mécanisme | Ce que c'est | Où, dans le code | Doc |
|---|---|---|---|
| Plan → inventaire | `plan/*.yml` → `hosts.yml` généré | `scripts/instancier.py`, `scripts/inventory_rules.py` | `plan-et-generation.md` |
| Nomenclature dérivée | VMID / IP / VLAN / FQDN dérivés | `inventory_rules.deriver_nomenclature` + `plan/nomenclature.yml` | `nomenclature-vm.md` |
| Dimensionnement | RAM/CPU/disque sommés par logiciel | `roles/*/meta/empreinte.yml` → `deriver_ressources` | `dimensionnement-ressources.md` |
| **Bindings app→app** | lien **côté app** (`liens`) résolu en host_vars | `plan/applications.yml` `liens:` + `roles/*/meta/liens.yml` + `instancier.resoudre_liens` | `bindings-conception.md` |
| **Bindings app→base** | lien **côté base** (`consommateur`/`portee`) résolu **dans le rôle** | `plan/bases-donnees.yml` + rôle utilitaire `resoudre_base` (`lookup('vars', secret)`, `no_log`) inclus par le consommateur | `bindings-conception.md` §5 |
| Résolution d'annuaire | connexion LDAP (uri/base DN/bind) **dérivée**, jamais recopiée | rôle utilitaire `resoudre_annuaire` (inclus par dovecot/postfix/keycloak/icingaweb2) | `identite-sso.md` |
| Plancher de résolution | `/etc/hosts` généré depuis l'inventaire + alias d'`expose` → l'écosystème se résout **DNS éteint** | rôle `hosts_statiques` (appliqué dans la couche socle) | `dns-interne.md` |
| Pont de certificat | cert step_ca → service, resync au renouvellement | script `*-cert-sync` + unité `.path`, dans chaque rôle serveur ; cert déposé par `client_pki` | — |
| Ordonnancement socle-first | socle/durci avant les `client_*` | `serveur_debian`/`serveur_durci` d'abord (posent `/etc/hosts` via `hosts_statiques`) | — |
| Sûreté check-mode | dry-run fiable | `when: not ansible_check_mode` sur les tâches de service + handlers | — |
| Voûte au déploiement | secret jamais en clair | `ANSIBLE_VAULT_PASSWORD_FILE` / `~/.config/setops-vault-pass` ; déréférencé par `lookup('vars', <nom>)` | — |
| Multi-instance | un dépôt par écosystème ; l'active = symlink `instance/`, les autres **découvertes par convention** (dossiers frères, aucun registre) | active : symlink `instance/` ; découverte : `scripts/instances.py` / `devis_reseau.py` (glob `../*/plan/nomenclature.yml` avec `index`) ; garde-fou collision : preuve **P21** | `multi-instances.md` |
| Exposition → edge | app expose un FQDN public servi par un edge | `plan/domaines.yml` + `expose` (applications) | `bindings-conception.md` §4 |
| Exploitation de l'hébergeur | ses **opérations** (supervision de la fabric, sauvegarde des configs, DNS d'underlay) n'appartiennent à aucun tenant et restent **hors overlay** | décidé, **non construit** : aucun équipement d'hébergeur n'est encore dans un inventaire | `hebergeur-exploitation.md` |
| Authentification | web → Keycloak ; LDAP source unique ; secours par `sudo`, formulaire local non annoncé | `<rôle>_connexion_locale: false` (grafana, forgejo, nextcloud) ; garde de version Forgejo ≥ 10 | `authentification.md` |
| Accès & habilitations | Set-OPS **amorce** un accès sysadmin puis se retire ; les appartenances aux groupes ne sont **jamais réconciliées** — c'est une personne qui gouverne | **construit et éprouvé** (2026-08-08) : rôle `amorcage_acces` (idempotence par existence, D-67), groupes projetés en rôles par `serveur_keycloak`, `meta/acces.yml` dans les 5 rôles web ; chaîne LDAP → Keycloak → groupe → service exercée de bout en bout sur Icinga Web 2 | `autorisation.md` (§6 = runbook de reprise) |
| SDN EVPN | ajouter un tenant implique **1 zone + 6 VNets + 6 sous-réseaux**, tous dérivés du seed | `scripts/devis_sdn.py` (`make devis-sdn`) ; nommage dérivé du tenant (`CHEZ17`, `chez174`), ≤ 8 caractères ; garde **P30** | `sdn-evpn.md` §2 |
| Pools Proxmox | un pool par tenant : les noms courts de VM sont **volontairement identiques** d'un tenant à l'autre (même fonction, même nom), et seule la console Proxmox en souffrait | `scripts/devis_proxmox_pools.py` (`make devis-proxmox-pools`) ; nom dérivé de l'`index` ; garde de collision = preuve **P28** | `decisions-architecture.md` D-37 |
| Routage | **aucun commutateur ne route** : la frontière est le seul équipement L3 ; les switches commutent | `passerelle` dit qui porte la passerelle, le SVI se dérive du rôle du porteur | `decisions-architecture.md` D-49/50 |
| **Devis de service** | LIT le système en marche et le compare à ce que le plan dérive ; n'écrit rien (D-23/D-24 portés du réseau aux services). Le playbook **relève**, Python **compare** | `playbooks/maintenance/devis-*.yml` + `scripts/devis_*.py` ; cible `make <sujet>-plan` | `devis-services.md` |
| Frontière nord/sud | les flux `pair: externe` — **sautés** par le pare-feu d'hôte — sont la politique de bordure | `scripts/devis_opnsense.py` (`make devis-opnsense`) ; garde d'accès admin = preuve **P24** | `frontiere-opnsense.md` |
> ⚠️ **Deux directions de binding, assumées** : `app→app` côté app (instancier),
> `app→base` côté base (registre, résolu en rôle pour que le secret ne quitte jamais le rôle).
> Ne pas unifier l'un dans l'autre sans raison. Cf. `bindings-conception.md`.
## 3. Maturité & dette
- **Maturité des rôles, échafaudages, rôles-catégories inertes** : voir
`docs/catalogue-services.md` « État d'implémentation » (source de vérité, tenue à jour).
- **Dette (audit 2026-07-03, revu le 2026-07-29)** :
- ✅ **soldé** — README de rôles : **tous les rôles en ont un** (les 12 manquants écrits le
2026-07-29 : `serveur_debian`, `hosts_statiques`, `resoudre_base`, `resoudre_annuaire`,
`serveur_dovecot`, `serveur_postfix`, `serveur_rspamd`, `client_backup`, `serveur_backup`,
`client_resolveur`, `serveur_oauth2_proxy`, `serveur_icingaweb2`).
- ✅ **soldé** — `expose` **est** consommé au déploiement : `plan/applications.yml` →
filtre `expositions_des_applications` → vhosts nginx dérivés
(`roles/serveur_nginx/tasks/main.yml`, template `expositions.conf.j2`, drapeau
`serveur_nginx_publier_expositions`), + alias `/etc/hosts` posés par `hosts_statiques`,
+ SANs des certificats d'edge dérivés par `scripts/instancier.py`.
- ⏳ **ouvert** — `meta/liens.yml` seulement sur `serveur_postfix` (`mailstore`, `milter`) :
les autres liens app→app viendront.
- ⏳ **ouvert** — `requiert` : câblé **côté GUI** (édition + validation
`valider_applications`) mais pas consommé au déploiement ; c'est un indice de dépendance
*applicative*. Les dépendances de **groupes** (celles qui pilotent l'ordre) vivent dans
`docs/dependances-groupes.yml` + `docs/couches-deploiement.yml`, et sont bel et bien
consommées par l'orchestrateur (`scripts/orchestrer.py` → `playbooks/site.yml`).
## 4. Discipline (pour ne plus re-déterrer)
Avant de concevoir ou d'ajouter un mécanisme :
1. lire **cette carte** + le doc du sujet ;
2. **arpenter le code** (`grep`) et **lire les README des rôles** concernés ;
3. **éprouver l'outil AVANT d'écrire le rôle** qui l'enveloppe — une machine jetable hors
plan se fabrique en deux minutes (`make cloner-vm`, cf. `vm-lifecycle.md` §4bis). C'est
ce qui a évité les bugs de premier déploiement de rspamd, et ce qui a tranché le pivot
Stalwart → Postfix/Dovecot. Elle est invisible au moteur : **à détruire à la main** ;
4. **étendre / factoriser** l'existant plutôt qu'ajouter un chemin parallèle.
Un seul agent IA travaille dans le dépôt à la fois (Codex **ou** Claude) — cf. `AGENTS.md`.