0053 — Isolation multi-cible : banc Lima et prod sur le même poste
Contexte
Section intitulée « Contexte »Le dépôt est un catalogue de topologies (« plusieurs infra déclarées, une
activée », ADR 0023). En pratique, un
même poste de contrôle peut héberger deux cibles vivantes simultanément : un
banc Lima monté et opérationnel (topologie multi-nœuds, VMs cp1/node1…) et
l’intention d’opérer une prod réelle (4 serveurs lame). Rien dans le dépôt ne
proscrit cette coexistence — elle est même normale : on garde un banc up pour
itérer pendant qu’on prépare ou audite la prod. Or les deux cibles sont plus
proches que ne le suggère leur isolation par fichier, et certains chemins
visent la cible ambiante du shell plutôt qu’une cible nommée.
L’isolation par fichier est acquise, mais c’est tout ce qu’il y a :
- Banc = kubeconfig
bench/lima/.work/kubeconfig+ inventairebench/lima/.work/inventory.yaml(générés, gitignorés) ; SSH userlimavia-F ~/.lima/<vm>/ssh.config. - Prod =
~/.kube/config(ou fichier opérateur) +bootstrap/hosts.yaml(gitignoré, copié dehosts.example.yaml) ; SSH userdebian.
Quatre fragilités transforment cette isolation par fichier en faux sentiment de sûreté dès que les deux cibles coexistent :
- kubectl nu = cible ambiante implicite. Le banc est sûr —
bench/lima/run-phases.shforce toujoursKUBECONFIG=.work/kubeconfigetkubectl --kubeconfig …, il ne déborde jamais. Maisbootstrap/state.sh(kubectl_q/kubectl_ready, couches Cilium/Ceph/StorageClass) etbootstrap/cni.shappellent kubectl nu, sans--kubeconfig: ils lisent leKUBECONFIGambiant du shell. En prod, c’est l’intention. Mais si le shell porte le kubeconfig du banc,state.shaudite le banc en croyant auditer la prod —2>/dev/nullrend l’erreur muette, l’en-tête affiche l’hôte prod. Faux verdict de conformité silencieux. - Contextes kubeconfig homonymes. Les deux clusters kubeadm naissent avec
les mêmes noms par défaut (cluster
kubernetes, userkubernetes-admin, contextekubernetes-admin@kubernetes). Le banc sait renommer (fetch_kubeconfig_nodeprend un argumentctxoptionnel) mais l’appel dephase_bootstrapne le passe pas ; la prod copieadmin.confverbatim. Une fusionKUBECONFIG=banc:prodécrase alors les deux contextes du même nom —use-contextne désambiguïse rien, on pilote le mauvais cluster sans le voir. - Inventaires structurellement indiscernables. Banc et prod déclarent les
mêmes groupes (
cloud/control/workers) et les mêmes noms d’hôtes (cp1/node1…) ; seule diffère la valeur interneansible_user(limavsdebian). Un playbook n’a aucun moyen de savoir contre quelle topologie il tourne : la séparation tient à la discipline d’invocation (-i …), sans garde-fou. Un mauvais-irejoue un hardening prod sur les VMs jetables (faux drift résiduel) ou, pire, mute les serveurs réels. - Le helper
env.shdevine la cible.bench/lima/env.shauto-détectelimadès qu’une VM Lima existe — un fait orthogonal à l’intention. Un opérateur qui prépare une commande prod se voit proposer le banc ; soneval "$(env.sh export)"poseKUBECONFIG=.work/kubeconfigdans le shell — c’est précisément le vecteur qui arme la cible ambiante du banc pour le point 1.
Le mode de défaillance commun n’est pas une panne bruyante : c’est un faux résultat silencieux — un audit « vert » de la prod qui a en réalité lu le banc, ou une mutation appliquée à la mauvaise topologie sans erreur. C’est exactement la classe de preuve invalide que proscrit ADR 0052 : un audit de prod n’a de valeur que s’il a prouvablement visé la prod. L’isolation par fichier est nécessaire mais pas suffisante ; il manque la règle qui nomme la cible au lieu de la déduire de l’état du shell.
Décision
Section intitulée « Décision »En contexte multi-cible, toute commande nomme explicitement sa cible. La cible n’est jamais déduite de l’état ambiant du shell ni d’un fait d’environnement orthogonal à l’intention. L’isolation par fichier (kubeconfig + inventaire gitignorés) est conservée ; on lui adjoint une désignation explicite rendue opposable côté kubectl, contexte, inventaire et helper. Quatre règles.
(a) Règle d’or — kubectl nu interdit en multi-cible
Section intitulée « (a) Règle d’or — kubectl nu interdit en multi-cible »Toute invocation kubectl désigne sa cible : --kubeconfig <fichier>
explicite, OU un contexte nommé (--context … / use-context) sur un
kubeconfig dont les contextes sont distincts (cf. (b)). Aucun kubectl nu
dans un chemin susceptible de tourner sur le poste partagé. Conséquence directe
sur les deux scripts à kubectl nu (bootstrap/state.sh, bootstrap/cni.sh) :
tant qu’ils n’ont pas de cible désignée, ils refusent d’émettre un verdict
plutôt que d’auditer une cible ambiante non confirmée. La désignation passe par
une cible explicitement nommée (variable d’intention) comparée à
l’identité réelle du cluster — l’empreinte du CA du contexte courant, ou à
défaut son server:, identifiant stable et disjoint banc/prod, insensible à
l’homonymie kubeadm. À cible absente ou divergente, les couches kubectl passent
en skip bruyant (message « cible non confirmée »), jamais en faux ok.
C’est l’inverse du 2>/dev/null actuel : on rend l’erreur de cible
bruyante, pas muette. L’étiquette d’intention vit en config locale
gitignorée (empreinte enregistrée une fois), jamais en défaut versionné
(ADR 0023).
(b) Contextes kubeconfig renommés par cible
Section intitulée « (b) Contextes kubeconfig renommés par cible »On tue l’homonymie à la source, des deux côtés, par des noms génériques
distincts : cluster-banc (banc) et cluster-prod (prod) — étiquettes
d’exemple, pas une valeur de déploiement.
- Banc : armer le rename déjà codé —
phase_bootstrap(et la ciblekubeconfig) passe toujours l’argumentctxàfetch_kubeconfig_node, valeur dérivée du profil (jamais codée en dur, ADR 0046). Le contexte banc naîtcluster-banc, jamaiskubernetes-admin@kubernetes. - Prod : nommer le cluster au
kubeadm init(clusterNamedérivé d’une var d’inventaire générique surchargeable) — voie canonique kubeadm, le contexte naîtkubernetes-admin@cluster-prod. Pour un parc déjà installé où l’init ne sera pas rejoué, une tâcherename-contextidempotente (post-copie deadmin.conf, rejeuchanged=0, ADR 0052 règle 2) corrige l’existant. Le RUNBOOK importe alors le contexte par fusion--flatten+use-context cluster-prodexplicite, jamais par écrasement de~/.kube/config.
Résultat : une fusion KUBECONFIG=banc:prod ne collisionne plus ; le
current-context et l’empreinte de (a) deviennent lisibles à l’œil. (À
répercuter sur le spike Cluster Mesh
ADR 0027 qui pose plusieurs
clusters.)
(c) Inventaires séparés + garde-fou anti-mauvais-inventaire
Section intitulée « (c) Inventaires séparés + garde-fou anti-mauvais-inventaire »On conserve les deux inventaires distincts, et on rend la mauvaise cible
Ansible bloquante avant toute mutation par un marqueur déclaratif
target_kind porté par chaque inventaire (au niveau du groupe cloud, donc
hérité par tous les hôtes) : target_kind: prod dans hosts.example.yaml,
target_kind: lima émis par le générateur d’inventaire du banc. Une assertion
native (module assert, run_once + delegate_to: localhost) compare
target_kind à l’intention de l’invocation (EXPECTED_TARGET_KIND, défaut
prod — une invocation nue du RUNBOOK exige donc un inventaire prod ; le
banc déclare lima). L’assertion vit dans un rôle déjà importé en pre_tasks
par quasiment tous les playbooks (audit-log), donc couverture quasi
automatique, avant tout become/toute mutation distante. Un inventaire
passé par erreur fait échouer immédiatement, zéro task mutante, avec un
message nommant les deux inventaires. Marqueur prod/lima = générique,
conforme ADR 0023 ; transforme un
faux-résultat silencieux en échec bruyant reproductible
(ADR 0052).
(d) env.sh exige une cible explicite
Section intitulée « (d) env.sh exige une cible explicite »Le helper cesse de deviner. La cible n’est auto-détectée que si une seule
est plausible (uniquement des VMs Lima → lima ; uniquement
bootstrap/hosts.yaml → prod) ; dès que les deux coexistent, il refuse
(exit 2) et exige lima|prod explicite. L’ergonomie du poste mono-cible est
préservée ; la friction est ciblée précisément sur le cas dangereux.
Symétriquement, export (qui pose KUBECONFIG du banc dans le shell — le
vecteur d’armement de (a)) exige lima explicite dès que la prod coexiste,
annonce sur stderr ce qu’il charge, et pose un marqueur d’intention lisible
par le garde-fou de (a).
(e) topology.py porte le miroir Python de (a) et (d)
Section intitulée « (e) topology.py porte le miroir Python de (a) et (d) »L’outil cluster (scripts/topology.py) interroge le cluster via kubectl.
Sans garde, un banc absent le faisait retomber sur ~/.kube/config (= la prod)
: sa section RÉEL affichait des nœuds de prod, et pire,
up/next/destroy/scale --apply pouvaient muter la prod par erreur.
Deux protections, miroir de (a) et (d) :
- Repli kubeconfig SÛR (
_bench_kubeconfig) :KUBECONFIGexporté (intention, cf. (a)) → respecté ; sinon le banc s’il existe ; sinon un kubeconfig VIDE (/dev/null), jamais~/.kube/config. Un banc absent rend des lectures vides (« pas de banc », honnête), il ne lit plus la prod par accident. - Garde de mutation (
_assert_bench_target) : les commandes BANC mutantes REFUSENT (code 2, miroir duexit 2de (d)) si le banc est absent ET que le contexte courant ne vise pas le banc (marqueurserver: https://127.0.0.1:de (a)). Échappatoire =KUBECONFIGexporté (intention explicite, ADR 0065). La lecture (preview) n’est pas bloquée mais avertit ;discover(usage prod légitime, ADR 0074) n’est jamais gardé.stack selectinvalide le kubeconfig de l’ancienne stack et avertit si aucun banc n’est monté.
Ces quatre règles (a-d) sont opposables : une revue ou la CI peut refuser un
résultat produit par un chemin à cible ambiante (kubectl nu, inventaire non
marqué, export deviné). Elles ne bypassent aucun garde-fou et n’introduisent
aucune valeur de déploiement versionnée : étiquettes (cluster-banc,
cluster-prod, prod/lima) et empreintes vivent en défaut générique ou en
config locale gitignorée (ADR 0023).
Accepted.
Conséquences
Section intitulée « Conséquences »- Gain principal : un audit de prod ne peut plus « réussir » en ayant lu le
banc, ni une mutation Ansible s’appliquer à la mauvaise topologie sans erreur.
Le faux-résultat-silencieux devient un échec bruyant ou un
skipexplicite — la preuve porte prouvablement sur la cible annoncée (ADR 0052). - Coût : faible et entièrement natif (bash/kubectl pur, module Ansible
assert, rename-context kubeadm), zéro dépendance nouvelle. Friction ergonomique assumée et ciblée : l’audit cluster destate.shdevient opt-in (cible désignée),env.shexigelima|prodquand les deux coexistent, unhosts.yamldéjà copié doit recevoir sontarget_kind: prod. Idempotence préservée (rename-contextrejouablechanged=0, ADR 0052). - Migration non rétroactive : les kubeconfig/inventaires déjà générés
gardent l’ancien nom. Côté banc, c’est gratuit (régénérable à volonté) ; côté
prod, la variante
rename-contextcorrige un parc en place, et unhosts.yamlpré-existant sans marqueur refuse de tourner (fail-safe : message « ajoutertarget_kind: prod») — action de migration ponctuelle à documenter au RUNBOOK. - Garde-fous par signal/refus, pas verrou universel : ces règles ferment le
chemin par défaut, silencieux et facile vers l’accident. Elles ne
protègent pas un
kubectl deletetapé à la main dans un shell mal pointé, ni un opérateur qui force sciemmentEXPECTED_TARGET_KIND=limasur une invocation prod : c’est alors un opt-in volontaire et visible, pas une distraction. On supprime l’accident par inadvertance, pas la liberté de l’opérateur déterminé (esprit ADR 0046 : corriger le code/chemin, pas contraindre par un wrapper coercitif fragile). - Couverture (c) = présence du rôle : un playbook qui n’importe pas
audit-logenpre_tasksn’est pas protégé — à auditer (lint/test bats listant les playbooks sansaudit-log) et soit y ajouter l’import, soit y dupliquer l’assert. - Détection (a) par identité de cluster : si le CA est référencé en fichier
(non
…-datainline), l’empreinte est vide → fallback sur leserver:endpoint (banc127.0.0.1:<port>vs prod VIP/hostname réel, disjoints ici).
Alternatives écartées
Section intitulée « Alternatives écartées »Wrapper coercitif liant nom-de-contexte ↔ nom-d’hôte (refuser de tourner si le contexte ne matche pas l’hôte ciblé). Écarté : plus sûr en apparence, mais intrusif et fragile (couplage rigide, casse au moindre renommage), contraire à l’esprit « signal + source correcte » plutôt que garde-fou bloquant (ADR 0046).
Conserver l’auto-détection env.sh en tranchant lima par défaut. Écarté :
c’est précisément la devinette qui arme l’accident ; le cas « coexistence »
est l’ambiguïté à ne pas trancher à la place de l’opérateur.
S’en remettre à la seule isolation par fichier + discipline d’invocation (statu quo). Écarté : nécessaire mais insuffisant — l’homonymie des contextes et le kubectl nu rendent l’erreur invisible, donc non détectable à la revue, donc non reproductible comme preuve (ADR 0052).