Aller au contenu

Résultats — banc Lima

Résultats — banc Lima

Première exécution : 2026-06-04, branche feat/127-banc-lima-industrialise, banc test/lima/ sur Mac Apple Silicon + Lima 2.1.2, Kubernetes v1.34.8.

Honnêteté des Runs (ADR 0023) : ce fichier consigne le déroulé réel et les drifts (écarts banc, pas bugs du dépôt) rencontrés en montant le banc Lima de bout en bout. Le banc Vagrant a son propre log : ../RESULTS.md.

Le chemin — pourquoi ces drifts comptent

Ce fichier n’est pas une liste d’erreurs : c’est la trace du travail patient qui rend le processus digne de confiance. Trois campagnes successives, aucune n’a fonctionné e2e du premier coup :

  • L1–L11 (bootstrap K8s, #127) — porter kubeadm sur de vraies VM Lima.
  • L12–L20 (chaîne DataOps en shell, #148) — assembler CNPG/Dagster/Marquez.
  • L21–L33 (portage Ansible, #173) — refaire la chaîne en rôles idempotents, validée e2e le 2026-06-07 (lineage réel ingéré dans Marquez).

À chaque campagne, le même schéma : le code passe tout le lint au vert, puis le run réel from-scratch révèle des drifts que seul un vrai cluster expose — et on les verrouille un par un. C’est exactement pourquoi ADR 0034 pose que la validation est un run e2e, pas le lint. La synthèse par catégorie et le tableau de bord (matériel + temps) : leçons des Runs.

La répétition n’est pas un échec — c’est la courbe de fiabilisation. Chaque drift traversé devient un invariant durable et un savoir réutilisable pour les terrains suivants (cloud, x86, HA).

Topologie testée

VMRéseau user-v2RôleDisques (virtio-blk)
cp1192.168.104.1control planevda=OS 20G, vdb-vdd=HDD 10G ×3, vde=block.db 5G
node1192.168.104.3worker(idem)
node2192.168.104.4worker(idem)
  • Image : _images/debian-13 (Lima), kernel 6.12.90+deb13-cloud-arm64.
  • vdf (263 MiB, iso9660 cidata) = disque cloud-init de Lima, ignoré par Ceph.
  • API jointe depuis l’hôte via le portForward Lima 127.0.0.1:6443 (l’IP user-v2 n’est pas routable depuis macOS) + tls-server-name: cluster-api.

Chemin obligatoire testé

#Étape (phase)Résultat
0up — 3 VMs Lima + disques bruts✅ disques vdb-vde bruts détectés sur chaque nœud
1bootstrap — checks/cri/kubeadm✅ 3 nœuds, containerd + kubeadm/kubelet v1.34.8
2bootstrap — control-planes/init✅ après fixes drifts L1/L2/L3 (kubeadm init OK)
3bootstrap — cni.sh (Cilium)✅ après fix drift L4, Cilium 1.19.4 + WireGuard (3/3 nodes)
4bootstrap — join-workers✅ après fix drift L2bis, node1 + node2 joints
5bootstrap — gate 3 nœuds Ready✅ après fix drift L5 (kubeconfig hôte)
6astorage-simple — local-path✅ provisioner Ready, PVC local-pathBound (mode rapide)
6bceph — operator + cluster✅ images dé-épinglées arm64, operator Ready
7ceph — OSD + HEALTH_OK✅ après fix drift L6 (lvm2), 9 OSD up/in, HEALTH_OK
8sc — StorageClasses + PVC test✅ PVC rook-ceph-block-replicatedBound
9down — destruction✅ VMs + disques nommés supprimés, rien ne subsiste

Stockage modulaire (#151) : all par défaut = mode rapide (up → bootstrap → storage-simple/local-path) ; WITH_CEPH=1 … all ajoute le stockage réel (Ceph). Le banc complet ci-dessus = mode Ceph.

Drifts détectés et correctifs

Préfixe L = spécifique au banc Lima (vs les drifts numériques du banc Vagrant). Tous corrigés dans ce chantier ; aucun n’est un bug du dépôt — ce sont des écarts entre l’environnement Lima et les hypothèses du bootstrap/banc.

#SymptômeCauseCorrectif
L1initialisation : ansible_user is undefinedinventaire posait l’utilisateur via SSH (ssh.config) mais pas la variable ansible_userinventaire généré : cloud.vars.ansible_user: lima
L2initialisation : Permission denied: /home/lima (.kube)home Lima = /home/lima.guest (≠ /home/lima) ; rôle construisait le home via ansible_userrôles k8s-initialization/k8s-rollback : home résolu via ansible_env.HOME (le vrai home)
L2bisjoin-workers : Unable to change directory (/home/debian)chdir: /home/debian codé en dur dans k8s-join-cluster ; absent sur Limachdir via ansible_env.HOME
L3initialisation : taint "…control-plane" not foundkubeadm v1.34 : le control-plane n’a pas le taint control-plane → taint …- échouetâche tolérante : failed_when ignore « not found »
L4cni.sh : cluster unreachable: localhost:8080cni.sh lancé en sudo → kubectl/cilium pointent sur le kubeconfig root absentlancer cni.sh en tant qu’utilisateur (sudo interne où nécessaire seulement)
L5gate kubeconfig : API injoignable depuis l’hôteIP user-v2 (192.168.104.x) non routable depuis macOSréécrire server: sur 127.0.0.1:<portForward> + tls-server-name: cluster-api
L6OSD-prepare CrashLoopBackOff : binary lvm does not existmetadataDevice (block.db) → Rook en mode LVM → ceph-volume exige lvm ; absent de l’image Limainstaller lvm2 dans le provision de la VM (profiles/node.yaml.tmpl)
L7gate Ceph vert à 0 OSDceph health = HEALTH_OK sur un cluster neuf SANS pool (rien à dégrader)gate renforcé : HEALTH_OK ET OSD attendus up (nœuds × disques data)

Validation e2e Dagster (#144, 2026-06-04)

Chaîne DataOps Dagster validée de bout en bout sur le banc Lima arm64 (mode rapide local-path), débloquée par le fix des digests multi-arch (#140) :

ÉtapeRésultat
cert-manager + CNPG operator✅ après fix drift L8 (CRDs Gateway API)
CNPG cluster pg + base dagster✅ Healthy 3/3 (PG18 + pgvector), base dagster créée
registry interne (image registry:3)✅ pull arm64 OK (digest d’index, #140) après fix drift L10 (PVC SC)
image Dagster arm64 → registry:80✅ buildée + poussée (via nerdctl sur un nœud), architecture: arm64
pods Dagster pull registry:80✅ après fix drift L9 (containerd insecure) + drift L11 (namespace)
storage Dagster22 tables dans Postgres (base dagster), pas de SQLite
run e2e via K8sRunLauncherJob K8s dagster-run-… Complete, run SUCCESS, 21 événements PG

Séquence d’événements du run (event log Postgres) : PIPELINE_ENQUEUED → PIPELINE_STARTING → STEP_WORKER_STARTED → STEP_START → STEP_SUCCESS → PIPELINE_SUCCESS. Exemple jetable retiré ensuite.

#SymptômeCauseCorrectif
L8cert-manager controller CrashLoop : « Gateway API CRDs not present »cni.sh active gatewayAPI ; Cilium n’embarque pas les CRDs ; le bootstrap nu non plusphase platform-prereqs : pose les CRDs Gateway API v1.4.1
L9ImagePullBackOff : « HTTP response to HTTPS client » (registry:80)containerd tente HTTPS sur le registry interne HTTP ; nom registry non résolu côté nœudphase platform-prereqs : /etc/hosts + certs.d/registry:80/hosts.toml HTTP
L10registry pod Pending : unbound PersistentVolumeClaimPVC du registry hardcodé rook-ceph-block-replicated, absent du banc (local-path)override banc : PVC sur local-path (à paramétrer comme CNPG storageClass)
L11kubectl apply -f dagster.yaml → ressources dans defaultle helm template figé ne porte pas metadata.namespaceREADME : kubectl apply -n dagster … (corrigé)

Réserves

  • os-upgrade non rejoué (contrairement au banc Vagrant) : image Lima fraîche — divergence assumée (cf. README.md).
  • arm64 : images Ceph dé-épinglées (digests amd64 → exec format error) côté banc seulement ; le livrable garde ses digests.
  • StorageClass default unique : le banc pose is-default-class sur UNE seule SC à la fois (set_default_sc) — local-path en mode rapide, rook-ceph-block-replicated en mode Ceph. La bascule local-path → Ceph a été validée (le default passe proprement de l’un à l’autre). Une SC résiduelle d’un autre outil ne fausse donc plus le gate.
  • Gate Ceph sous charge : sur un hôte chargé (peu de RAM libre), la montée HEALTH_OK peut dépasser la fenêtre de 20 min du gate alors que Ceph converge ensuite normalement — relancer ceph (idempotent) ou libérer de la RAM. Le mode rapide (local-path) évite ce coût au quotidien.

Chaîne DataOps assemblée — phase dataops-chain (#148, étape 1.8) — 2026-06-05

✅ Validé e2e sur banc Lima arm64 (2026-06-05). La chaîne monitoring → CNPG → Dagster → Marquez a été déployée et vérifiée assemblée, et le lineage d’un run Dagster RÉEL est ingéré et visible dans Marquez — preuve attendue par l’épopée #148.

Chaîne validée de bout en bout (run réel, mode rapide local-path) :

ÉtapeRésultat
infra (up → bootstrap → storage-simple)✅ 3 nœuds Ready (K8s v1.34.8, Cilium 1.19.4 + WireGuard), PVC local-path Bound
registry interne + 4 images arm64✅ buildées (nerdctl+buildkitd) + poussées : marquez, marquez-web, dagster-celery-k8s, dagster-openlineage-emit
CNPG cluster pgHealthy 3/3, bases dagster/marquez/pgvector créées
Dagster webserver + daemon✅ Ready, storage CNPG (connexion authentifiée OK)
Marquez API + web✅ Ready, migration Flyway OK sur la base marquez
émetteur jetable → lineage✅ run Dagster toy_dataset RUN_SUCCESS ; ingéré dans Marquez : GET /api/v1/namespaces/dagster/jobstotalCount: 1, job toy_dataset, latestRun.state: COMPLETED

Preuve #148 : totalCount passé de 0 → 1 après le run. Le job toy_dataset (namespace OpenLineage dagster) apparaît dans Marquez avec son dernier run COMPLETED. La chaîne Dagster → sensor OpenLineage → API Marquez → ingestion est prouvée assemblée, pas seulement verte-en-CI.

Drifts rencontrés et correctifs (L12–L19)

Préfixe L = banc Lima. Plusieurs sont de vrais bugs du livrable (pas des écarts banc) — corrigés dans le dépôt, pas seulement contournés.

#SymptômeCauseCorrectif
L12bootstrap exit 141 (SIGPIPE) — kubeconfig non récupéréun cilium status | grep ferme le pipe avant fetch_kubeconfigcontournement : phase kubeconfig (le cluster était sain) ; à durcir dans le bootstrap
L13Pull registry:80 HTTP échoue : « HTTP response to HTTPS client »containerd v2.2 (Debian 13) route le pull via le transfer service qui IGNORE certs.duse_local_image_pull = true dans containerd config (les 3 nœuds) — fix racine
L14CNPG bloqué : « unknown plugin being required »cluster.yaml exige le plugin Barman, non installé par dataops-chainsurcharge banc : retirer le bloc plugins: (backups hors périmètre lineage)
L15PVC pg Pending : « unbound immediate PersistentVolumeClaims »cluster.yaml hardcode storageClass: standard, absent du banc (local-path)surcharge banc : standard → local-path (cf. #158)
L16Dagster « too many retries for DB connection » (BUG LIVRABLE)managed.roles CNPG SANS passwordSecret → rôles créés sans mot de passe (rolpassword NULL)fix dépôt : passwordSecret par rôle + role-secrets.example.yaml
L17Secret dérivé appli ≠ pwd réel du rôlenécessitait une recopie manuellerésolu par L16 : .example de rôle et dérivés alignés sur les mêmes valeurs de test
L18Build d’images impossible dans les VMsgit absent + buildkitd (service présent mais disabled)apt install git + systemctl enable --now buildkit (à intégrer dans la prépa-build)
L19Run émetteur timeout sur le POST OpenLineage (BUG LIVRABLE)aucune NetworkPolicy egress dagster → marquez:5000 (seul l’ingress côté marquez existait)fix dépôt : platform/network-policies/dagster/allow-marquez-egress.yaml
L20webserver/daemon Dagster CrashLoop : « no [tool.dagster] block » (BUG)l’orchestrateur vide est lancé sans workspace (-w)fix dépôt : ConfigMap dagster-workspace (load_from: []) monté + -w dans dagster.yaml

Enseignement (→ refonte Ansible)

Ces drifts en cascade viennent tous de la couture shell/kubectl de la couche plateforme (le bootstrap, lui — en Ansible — n’en a produit qu’un, cosmétique). Chacun (config containerd, secrets de rôles, attentes Ready, surcharges par topologie) est nativement géré par Ansible (kubernetes.core, lineinfile, gestion de secrets, templating). D’où la décision de porter la couche plateforme DataOps en rôles Ansible (issue dédiée) : transformer ces drifts en tâches idempotentes plutôt que les redécouvrir à chaque run.

Chaîne DataOps en rôles Ansible — phase dataops (#173) — 2026-06-07

✅ Validé e2e sur banc Lima arm64 (2026-06-07), en MODE CEPH. Le portage de la couche plateforme en rôles Ansible (bootstrap/dataops.yaml, ADR 0033) a été monté de bout en bout par le playbook (plus de shell impératif) et le lineage d’un run Dagster réel est ingéré dans Marquez.

⚠️ Honnêteté du Run (ADR 0023). Le vert initial a été atteint après 13 correctifs intermédiaires (drifts L21–L33) : la chaîne a échoué et été relancée de nombreuses fois avant de passer — fidèle au constat que rien ne marche e2e du premier coup (ADR 0034). Un run propre from-scratch d’une traite (banc détruit puis remonté) a ensuite confirmé le résultat : all + datalake + dataops sans intervention, vert (cf. encadré « Run from-scratch confirmé » plus bas). Le dernier drift (L33, gate RGW) n’a d’ailleurs été révélé que par ce run propre — preuve qu’il fallait le faire.

Log brut générisé (preuve, ADR 0023) : runs/2026-06-07-dataops-ansible.log. Séquence : WITH_CEPH=1 all → datalake → dataops.

ÉtapeRésultat
up → bootstrap → ceph → sc✅ 3 nœuds Ready (K8s v1.34.8, Cilium 1.19.4), Ceph HEALTH_OK, SC Bound
datalake (RGW)CephObjectStore datalake, rook-ceph-rgw-datalake-a 3/3 (cible S3 Barman)
registry + CRDs Gateway API✅ via platform-registry (kubernetes.core), gate Ready ; containerd use_local_image_pull
cert-manager✅ via platform-cert-manager, webhook Ready, CA interne posée
CNPG cluster pg + BarmanHealthy 3/3 AVEC plugin Barman → backups vers le RGW Ceph (OBC cnpg-backups)
build images arm64dagster-celery-k8s, marquez, marquez-web buildées+poussées (sources copiées sur nœud)
Dagster webserver + daemon✅ Ready, storage CNPG (Secret dérivé du rôle CNPG)
Marquez API + web✅ Ready, migration Flyway OK
émetteur → lineage✅ run toy_dataset COMPLETED ingéré : namespaces/dagster/jobs → job présent, run COMPLETED

Preuve #173/#148 par Ansible : toute la chaîne est désormais une commande reproductible (run-phases.sh dataopsansible-playbook dataops.yaml). Le lineage est prouvé assemblé, et Barman archive vers le RGW Ceph (vs le banc #148 qui retirait le plugin, drift L14 — éliminé à la racine).

Drifts rencontrés et correctifs (L21–L32)

Drifts du portage Ansible + de l’exécution depuis l’hôte / mode Ceph. Tous corrigés dans le dépôt ; aucun n’est un bug de conception — ce sont des écarts d’environnement que seul un run e2e révèle.

#SymptômeCauseCorrectif
L21ansible_user_id is undefined (play cluster)gather_facts: false sur le play localhost, requis par audit-logretrait de l’audit-log du play cluster (cf. L22)
L22sudo: a password is required (audit-log sur localhost)audit-log écrit un log SYSTÈME (become) — n’a pas de sens sur le posteaudit-log retiré de dataops.yaml (reste sur les playbooks de nœuds)
L23SSL: CERTIFICATE_VERIFY_FAILED (get_url/k8s)le Python d’Ansible (Homebrew) n’utilise pas le CA systèmeSSL_CERT_FILE via certifi, résolu en pré-tâche par le bon interpréteur
L24volet node du rôle registry tourne sur localhostimport_role charge tout le rôle ; le tag ne filtre pas les blocs internesrôle scindé cluster.yaml/node.yaml, importés via tasks_from
L25Secret dérivé : « namespaces postgres not found »secrets posés avant que cluster.yaml ne crée le namespacenamespace postgres créé en premier dans platform-cnpg
L26build : dict has no attribute 'clone_subdir'ternary Jinja évalue les deux branches (image local sans clone_subdir)default('') sur les attributs optionnels
L27build : Dockerfile no such file or directory sur nœudbanc Lima mounts: [] → sources du dépôt absentes de la VMcopier contextes/Dockerfiles sur le nœud avant build
L28build marquez-web OOM-killed (rc 137)webpack/npm sature la VM 5 GiB (déjà k8s+Ceph+CNPG)VM_MEMORY 5 → 8 GiB
L29operator CNPG CrashLoop après reboot (RAM)reboot cp1 → Cilium pas reconvergé → ClusterIP plugin injoignableartefact de reboot (cf. réserve « restore non fidèle ») ; restart operator
L30pods Dagster ImagePullBackOff registry:80 (HTTP/HTTPS)containerd des workers pas rechargé après pose de la config insecure-reg.restart containerd sur les nœuds (handler à fiabiliser)
L31preuve lineage : image émetteur absente du registryl’émetteur jetable n’était pas dans build_images (hors prod, ADR 0022)build_emitter_image=true au banc (câblé conditionnellement)
L32« aucun job ingéré (1 → 1) » alors que le lineage est làle prédicat exigeait un delta ; le run est idempotent (namespace gardé)classify_marquez_ingest teste la présence (after >= 1) + bats à jour
L33RGW datalake pas Ready alors que les 3 pods sont 2/2 Runningle gate datalake testait readyReplicas == 1, or le CephObjectStore a instances: 3gate >= 1 (au moins une instance up) — révélé par le run from-scratch

Run from-scratch confirmé (2026-06-07). Après correction de L33, le banc a été détruit puis remonté d’une traite : WITH_CEPH=1 all est passé sans intervention (socle + Ceph), et dataops a abouti vert (0 échec)dataops mesuré à 13m37s (M3 Max, 8 GiB/VM), lineage 0 → 1 ingéré dans Marquez. Total banc complet ≈ 30 min. Métriques émises par run-phases.sh (cf. tableau de bord).

Enseignement

Le portage Ansible tient sa promesse : les drifts L12-L20 (couture shell) ont disparu — la chaîne se monte d’une commande idempotente. Les nouveaux drifts L21-L32 sont d’une autre nature : modèle d’exécution (localhost vs nœud : L21-L24), isolation/ressources du banc (L27-L30) et harnais de test (L31-L32). Aucun n’est un défaut du livrable — ils sont corrigés une fois et deviennent des invariants du run. Barman archive désormais vers le RGW Ceph (L14 éliminé à la racine), au prix d’un banc en mode Ceph (8 GiB/VM).

Observabilité paramétrable — phase monitoring (#158/#186) — 2026-06-07

✅ Validé e2e sur banc Lima arm64 (2026-06-07), sur les DEUX profils S3. Portage en rôles Ansible de kube-prometheus-stack (Prometheus + Alertmanager

  • Grafana), loki et seaweedfs (ADR 0036), avec storageClass paramétrable (#158) et backing S3 par topologie (#186). Deux runs from-scratch du namespace : profil léger (local-path + SeaweedFS) puis profil Ceph (rook-ceph + RGW via OBC).

⚠️ Honnêteté du Run. 6 correctifs intermédiaires (drifts L34–L39), tous de code/universels. Fait notable validant la stratégie deux-bancs : 2 drifts (L38/L39) n’apparaissent qu’en RGW — le profil léger (creds admin SeaweedFS) les masquait. C’est la preuve concrète qu’un chemin de code partagé doit être validé sur chaque backing réellement employé (cf. Leçons des Runs, cat. 7).

ProfilstorageClass PVCBacking S3 LokiVerdictTemps monitoring
Léger (rapide)local-pathSeaweedFS (s3 ns, buckets nommés)✅ Prometheus/AM/Grafana/Loki Ready, S3 réel3m05s
Ceph (fidèle)rook-ceph-block-replicatedRGW via OBC (bucket unique)✅ idem, Loki started sur le bucket OBC2m53s

Preuve #158 : tous les storageClass codés en dur sont désormais paramétrés (registry, CNPG, monitoring, Loki) — PVC Bound en local-path et en rook-ceph-block-replicated selon la topologie, sans modifier le code. Preuve #186 : Loki tourne en profil S3 réel (jamais filesystem) sur SeaweedFS et sur RGW — même chemin de code, endpoint et bucket résolus par variable.

Drifts rencontrés et correctifs (L34–L39)

#SymptômeCauseCorrectifProfil
L34CRDs monitoring rejetées par le module k8senum - = non quoté → PyYAML SafeLoader rejette (tag value)appliquer les CRDs via kubectl apply --server-side (Go), pas le moduleles 2
L35« no matches for cert-manager.io/v1.Certificate »le manifeste monitoring contient des Certificate/Issuerplatform-cert-manager appliqué avant monitoring (dans monitoring.yaml)les 2
L37cert-manager CrashLoop au démarragetourne avec --enable-gateway-api mais les CRDs Gateway API sont absentesle rôle platform-cert-manager pose lui-même les CRDs Gateway API (1.4.1)les 2
L36PrometheusRule rejetés en masse (HTTP 500 webhook)appliqués avant que l’operator (porteur du webhook) soit Readydeux passes : stack sauf PrometheusRule → attente operator → PrometheusRuleles 2
L38Loki CrashLoop NoSuchBucket (compactor)l’OBC Rook n’expose qu’un bucket auto-nommé + creds restreints → buckets nommés impossiblesen RGW : résoudre le bucket OBC, l’employer pour chunks et ruler ; init-buckets skippéRGW
L39init-buckets « prêt » alors que rien n’est créégate faux : grep make_bucket matchait aussi make_bucket **failed**script réécrit (set -eu) : échec franc si le bucket n’est ni créé ni déjà présentRGW

Enseignement

Le paramétrage tient : un seul code, deux topologies (#158/#186). La leçon forte est la portée des drifts : L34–L37 cassaient les deux profils (ordre CRD/webhook/contrôleur — invariants d’admission), mais L38/L39 ne se révèlent qu’en RGW. Le banc léger, avec ses creds admin SeaweedFS, validait une version plus permissive que la prod. D’où la règle inscrite en ADR 0036 : un changement S3 validé en léger doit être revalidé en Ceph avant prod.

Re-validation from-scratch des deux bancs (2026-06-08)

But : confirmer zéro drift résiduel après les chantiers #158/#186, par deux runs from-scratch d’une traite (banc détruit puis remonté), un par profil (ADR 0034).

Un drift de plus est apparu — précisément ce que cherche un from-scratch :

| # | Symptôme | Cause | Correctif | Portée | | --- | ------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | ------ | ------------------------------------- | ---- | | L40 | platform-prereqs meurt (EXIT 1) sur banc léger | set -e + reg_ip=$(kubectl get svc registry …) : sans le ns registry (monitoring seul, pas de dataops), kubectl sort en 1 → l’assignation tue le script avant le garde [ -z reg_ip ] | … | | true sur l’assignation (skip propre) | code |

L40 n’apparaît que sur le profil léger (monitoring sans dataops, donc sans registry) : les runs précédents montaient toujours le registry via dataops, qui masquait le bug. Encore un drift qu’un seul profil révèle (cf. Leçons des Runs, cat. 7).

Verdicts (M3 Max, 8 GiB/VM, multi-node-3 arm64, local) :

ProfilSéquence from-scratchVerdictTemps notables
légerall → platform-prereqs → monitoring0 drift (après L40)up 3m14s · bootstrap 6m40s · monitoring ~3m
CephWITH_CEPH=1 all → datalake → platform-prereqs → dataops → monitoring0 driftceph 3m09s · dataops 14m33s · monitoring 2m34s

Les scénarios d’observabilité 24–26 (Prometheus scrape, alerte Watchdog firing, Loki round-trip LogQL) ont été écrits puis passés au vert sur le banc Ceph (profil RGW) — la stack monitoring passe de montée à éprouvée.

DataOps sans Ceph — backing S3 factorisé (2026-06-08)

But : rendre la chaîne DataOps (CNPG/Barman) montable sans Ceph, en la découplant du RGW via un rôle S3 factorisé platform-s3-bucket (backing rgw | seaweedfs, ADR 0036). Loki et CNPG partagent désormais cette brique (fin de la duplication OBC/creds).

Validé e2e sur les deux profils (M3 Max, multi-node-3 arm64, local) — le refactor S3 ne casse pas le chemin prod (RGW) et débloque le banc léger :

ProfilBacking S3 (CNPG + Loki)Chaîne DataOpsTemps notables
légerSeaweedFS (sans Ceph)✅ CNPG sain + Dagster + Marquez, lineage 0→1bootstrap 6m31s · monitoring 3m05s · dataops 11m01s
CephRGW (OBC)✅ idem (non-régression rgw), lineage 0→1ceph 3m09s · datalake 1m31s · dataops ~14m · monitoring ~3m

Drifts de cette campagne (détail : registre-drifts.yaml) : L41 (storageClass registry/CNPG non aligné au profil → paramétré), L42/L43 (single-node : gate Dagster lent, éviction CNPG disque — caducs, topologie abandonnée ADR 0040), L44 (dépendance à WITH_CEPH en variable d’env — ouvert : dataops/monitoring devraient détecter le profil).

Métrologie du banc — run from-scratch (#216/#217/#219, 2026-06-08)

✅ Socle validé from-scratch en mode Ceph (2026-06-08), branche feat/banc-metrologie-cache. Première preuve consignée automatiquement dans runs-history.yaml par run-phases.sh (plus de saisie manuelle). Log brut : runs/2026-06-08-banc-metrologie-e2e.log.

NO_CACHE=1 WITH_CEPH=1 run-phases.sh all — up → bootstrap → ceph → sc, monté de zéro (preuve ADR 0034) :

PhaseDuréeGate
up2m45sdisques vd* présents sur les 3 VMs
bootstrap6m39s3 nœuds Ready (Cilium 1.19 + WireGuard)
ceph3m09s9 OSD up, HEALTH_OK
sc0m06sPVC test Bound (rook-ceph-block-replicated)
total12m39sentrée runs-history.yaml appendée
  • #219 cache du socle prouvé : un second WITH_CEPH=1 run-phases.sh all (socle inchangé, VMs up) a sauté up+bootstrap+ceph+sc et rendu la main en ~1 s (clé socle:ceph:… inchangée). NO_CACHE=1 force le rebuild (preuve).
  • #217 métriques échantillonnées depuis Prometheus (fenêtre 900 s, profil Ceph) : CPU 272 cœur·s, RAM pic 7606 MiB, moy 7489 MiB.

Drifts de cette campagne :

  • L44 reproduit (déjà ouvert) : la phase monitoring lancée sans propager WITH_CEPH=1 a choisi le profil léger (SeaweedFS/local-path) sur un cluster dont le default storageClass est Ceph → PVC local-path Pending (unbound immediate PersistentVolumeClaims). Confirme l’utilité de l’auto-détection de profil que L44 réclame. Pas un bug du livrable — un oubli d’invocation, exactement le piège que L44 documente.
  • L47 (harnais, corrigé) : metro_sample_prometheus interrogeait Prometheus via kubectl exec dans son pod distroless (ni wget ni sh — même piège que le drift #14 etcd) → métriques toujours ?. Et ses logs partaient sur stdout, capturé dans le bloc YAML → pollution ANSI du runs-history.yaml. Correctif : requête via pod busybox éphémère ciblant le Service prometheus-operated ; tous les logs routés sur stderr. Couvert par deux tests bats anti-régression (stdout sans ANSI, Prometheus absent → stdout vide).

Socle GitOps — Gitea + Argo CD sans Ceph (#230, 2026-06-09)

✅ Socle GitOps validé sur banc (2026-06-09), branche feat/230-gitea-banc-atlas. Première mise en service d’Argo CD par le banc (jusqu’ici posé en kubectl manuel) + Gitea (forge git intra-banc air-gapped, ADR 0044). Profil léger : local-path, sans Ceph.

run-phases.sh gitops (par-dessus up → bootstrap → storage-simple) — déploie Gitea puis Argo CD via bootstrap/gitops.yaml :

BriqueGateRésultat
Gitea (rootless arm64)deploy/gitea Ready✅ 1/1 (image par digest index)
Argo CDdeploy/argocd-server Ready✅ 7 pods 1/1 (après fix L48)
  • Image Gitea rootless arm64 : démarre sans souci (digest d’index multi-arch ADR 0006 — pas de exec format error).

Drifts de cette campagne :

  • L48 (livrable, corrigé) : argocd-server restait 0/1 — tous les composants Argo CD en CreateContainerConfigError (secret "argocd-redis" not found). Cause : l’initContainer secret-init de argocd-redis crée ce Secret via l’API K8s, mais échouait (exit 20) car la NetworkPolicy préexistante platform/network-policies/argocd/allow-egress.yaml autorisait l’egress apiserver via to.ipBlock 0.0.0.0/0. Sous Cilium, un ipBlock EXCLUT les entités réservées (host/kube-apiserver) → l’apiserver (ClusterIP 10.96.0.1:443 ET endpoint :6443) était injoignable. Correctif : egress sans to: (idiome du dépôt, calque dagster/allow-apiserver-egress) + ports 443 et 6443. Bug latent révélé par la 1re mise en service d’Argo CD par le banc (avant : kubectl manuel sur un datapath différent). Vérifié : 10.96.0.1:443 joignable, Secret créé, cascade résolue, tous pods 1/1.
  • L49 (harnais, corrigé) : un all relancé sur socle en cache (#219) consignait dans runs-history.yaml une entrée « run complet » avec total_s tronqué et phases partielles (gitops: 12 seul) — fausse preuve pour le garde-fou de fraîcheur (ADR 0042). Cause : record_full_run appelé inconditionnellement, même sur cache hit (up/bootstrap/storage sautés, absents de PHASE_DURATIONS). Correctif : ne consigner que les runs from-scratch (flag socle_built) ; run sur cache → message, pas d’entrée. Fausse entrée retirée.

✅ Run from-scratch consigné (2026-06-09, commit 24cb7e5). NO_CACHE=1 run-phases.sh all monté de zéro : up 169s → bootstrap 406s → storage-simple 13s → gitops 75s (total 663s, profil local-path). Entrée appendée automatiquement dans runs-history.yaml — preuve ADR 0034/0042 (et validation du correctif L49 : l’entrée est complète, pas tronquée).

Chemins d’installation + scénarios atlas (#237, 2026-06-09)

✅ Chemin atlas validé from-scratch (commit 98e7467). ADR 0045 implémenté : cibles socle/atlas/cluster. NO_CACHE=1 run-phases.sh atlas de zéro : up 194s → bootstrap 402s → storage-simple 11s → monitoring 188s → gitops 75s → dataops 694s (total 1564s, local-path, RAM pic 9191 MiB). Première exécution de l’ordre monitoring AVANT dataops : SeaweedFS posé par monitoring, consommé par dataops — ordre validé. Consigné dans runs-history.yaml.

Scénarios pertinents joués sur le banc atlas (9/9 PASS) — preuve de comportement par-dessus les gates d’intégration (sous-ensemble applicable au profil local-path, cf. plan de tests) :

#ScénarioRésultat
10Pod Security admission✅ pod dangereux rejeté, conforme admis
11NetworkPolicy default-deny✅ egress coupé, allow-dns ciblé rouvre
12securityContext runtime✅ non-root + rootfs RO vérifiés au runtime
17Pod d’évasion → PSA✅ hostPath/hostPID/hostIPC rejetés
18Exfiltration → NetworkPolicy✅ canal coupé, DNS légitime préservé
23Marquez ← OpenLineage✅ lineage d’un run Dagster réel ingéré
24Prometheus scrape + Grafana✅ 22 targets UP, Grafana health ok
25PrometheusRule → alerteWatchdog firing (pipeline d’alerting vivant)
26Loki ← LogQL✅ round-trip push → LogQL (+ backing S3 SeaweedFS)

Drift de cette campagne :

  • L50 (harnais, corrigé) : run-all.sh lancé avec un KUBECONFIG en chemin relatif faisait échouer/skipper tous les scénarios (localhost:8080 refused) — le runner fait cd dans son dossier, invalidant le chemin relatif. Correctif : résoudre KUBECONFIG en absolu avant le cd. Validé (ONLY=24 avec KUBECONFIG relatif passe).

Chaîne GitOps → workflows atlas — scénario 27 prouvé (#231, 2026-06-09)

✅ Cœur du banc atlas prouvé sur banc (2026-06-09), branche feat/231-scenario27-gitops-dataops. Un push sur Gitea déclenche, via webhook, le déploiement par Argo CD du workflow atlas, qui lance un run Dagster réel dont le lineage est ingéré par Marquez. Objectif ADR 0044/0045 atteint : atlas lance sa GitOps qui pilote toute la chaîne DataOps.

run-phases.sh gitops-seed (init dépôt Gitea + webhook + Application) puis STRICT_GITOPS=1 ONLY='27' run-all.shscénario 27 PASS :

Étape (gate)Résultat
Gitea + Argo CD + Application présents
push commit de déclenchement (Gitea)✅ nouveau commit
Argo CD réconcilie via webhooknouvelle révision (fc6d860 → 57ff5f4), Synced/Healthy
run Dagster + lineage Marquez✅ Job Complete, lineage ingéré

Drifts de cette campagne (détail : registre-drifts.yaml) — 4 bugs du livrable révélés par le run réel, corrigés dans les manifestes versionnés (appliqués par le rôle Ansible platform-argocd, plus de kubectl patch) :

  • L51 : sourceRepos /*/** (le glob Argo ne traverse pas les /).
  • L52 : egress interne argocd (controller → repo-server:8081 / redis) — le default-deny coupait le trafic intra-Argo.
  • L53 : egress repo-server → Gitea (namespaceSelector) — ipBlock 0.0.0.0/0 exclut les pods cluster sous Cilium (même piège que L48).
  • L54 : workflow jouet sans securityContext durci (Dagster écrit son DAGSTER_HOME ; aligné sur l’émetteur de référence ; durcissement = #234).

Note : l’update Contents API de gitea-init.sh (PUT avec sha) a été soupçonné défaillant en cours de run, mais vérification faite, il fonctionne (un changement local du workflow produit bien un commit update workflow dans Gitea, contenu mis à jour). La fausse piste venait d’un comptage erroné (grep readOnlyRootFilesystem matchait les commentaires du fichier, pas le YAML effectif) et de l’idempotence (relancer sans changement ne crée pas de commit — comportement attendu). Pas de bug d’update. Le correctif « push vérifié » (commit de la campagne) échoue désormais explicitement si un PUT/POST ne renvoie pas de commit, ce qui aurait rendu un vrai défaut visible.

Banc atlas-ceph complet + portail UI — scénario 28 (#232, 2026-06-09)

Run from-scratch atlas-ceph (socle Ceph + hardening + datalake + monitoring + gitops + dataops + gitops-seed) réussi : 3 nœuds Ready, chaîne GitOps → DataOps fonctionnelle (workflow Completed, lineage Marquez). Durées consignées dans runs-history.yaml (pic RAM 14,8 GiB / 36 alloués).

Puis exposition tout-Cilium prouvée de bout en bout (objectif #232) — le scénario 28 a joué son rôle de révélateur : il a découvert que les UI n’étaient PAS joignables alors que le Gateway était posé. Deux bugs du livrable (pas du test) trouvés, corrigés dans le code, et prouvés à chaud (HTTP 200 via le Gateway) :

Vérification (à chaud, banc up)Résultat
GatewayClass cilium acceptéeAccepted=True
Gateway argocd programmé + IP LB-IPAM192.168.104.240 (pool dérivé)
Service cilium-gateway-argocd type LoadBalancer + IPEXTERNAL-IP 192.168.104.240
sonde HTTPS via Gateway (SNI argocd.cluster.lan)HTTP 200 (backend HTTP:80 OK)
scénario 28PASS (1 UI atteignable)

Drifts révélés (détail : registre-drifts.yaml) :

  • L55 (disque) : VM_DISK=20 GiB codé en dur → pression ephemeral-storage en Ceph+dataops (évictions postgres/rgw/exporter, pas la RAM). Fix : VM_DISK dérive du profil (40 GiB Ceph / 20 GiB léger ; qcow2 thin-provisionné) — même classe de bug que la RAM hardcodée (ADR 0046).
  • L56 (exposition, 2 bugs) : (1) cni.sh armait les features Cilium mais n’appliquait jamais les CRs (GatewayClass + LB-IPAM pool + L2 policy) ; (2) ordre cassé — l’operator Cilium vérifie les CRDs Gateway API au démarrage (lancé par cni.sh), or elles étaient posées 7 min plus tard (platform-prereqs) → contrôleur Gateway jamais armé. Fix : CRDs Gateway API avant cni.sh + cni.sh applique les CRs inline (plage LB-IPAM + interface L2 dérivées du réseau réel, ADR 0023). Bonus : les CRs ciblaient l’ancien Vagrant (192.168.67/eth1), réalignés sur le banc Lima (192.168.104/eth0).

Bug du scénario 28 lui-même (corrigé) : il sondait avec wget --header Host busybox, qui ne pose pas le SNI TLS. Le Gateway Envoy sélectionne le certificat par SNI → Connection reset (faux négatif) alors que curl --resolve host:443:IP (SNI correct) renvoie 200. Le scénario utilise désormais curl --resolve.

À re-prouver par un run from-scratch (ADR 0034) : les fixes L55/L56 ont été validés à chaud sur le banc courant (diagnostic + restart operator) ; le prochain run atlas-ceph doit reproduire le scénario 28 PASS nativement (sans intervention) — c’est la vraie preuve (ADR 0046).

Observabilité incomplète sur le chemin atlas léger (2026-06-10)

Run from-scratch atlas (profil local-path, multi-node-3 arm64) réussi : 3 nœuds Ready, monitoring → gitops → dataops → gitops-seed, Application atlas-workflows Synced/Healthy. Durées dans runs-history.yaml (total ~26 min, pic RAM 9,3 GiB). En testant l’accès local aux UIs depuis l’hôte (port-forward), deux manques d’observabilité — hors run, non bloquants — ont été relevés. Ce ne sont pas des écarts de montage mais des trous de périmètre du chemin, suivis dans l’issue #252 (prioritaire) :

#SymptômeCauseCorrectif (visé #252)Portée
L57accès Grafana cassé dans run-phases.sh statusstatus propose kubectl … port-forward svc/grafana (ns monitoring) — service inexistantpointer le vrai service kube-prometheus-stack-grafana (port 80) + secret admincode
L58kubectl top nodes → « Metrics API not available »le chemin atlas ne pose pas metrics-server (palier 1, ADR 0016) ; apply manuel rend top opérantposer platform/metrics-server/ automatiquement dans le chemin atlasatlas

Pourquoi ce ne sont pas des drifts « classiques ». Le run lui-même n’échoue pas : L57/L58 ont été trouvés en consommant le banc (accès UI/top), pas par une phase. Conformément à l’honnêteté des Runs (ADR 0023) on les consigne tels quels — manques de périmètre à corriger dans le code (#252) puis à re-prouver par un run (ADR 0034/0046), jamais par un kubectl apply laissé en l’état.

Egress Internet du ns dagster prouvé — snapshot OpenAlex (#256, 2026-06-10)

Run atlas (local-path, multi-node-3 arm64). La phase dataops exécute désormais la preuve egress Internet (dataops_egress_internet_check) après la preuve lineage : un curl https://1.1.1.1 depuis un pod éphémère du ns dagster, avec puis sans la NP allow-internet-egress (#256), pour montrer que la policy — et elle seule — ouvre la sortie 443 sous default-deny (ADR 0019). Indispensable au sync du snapshot OpenAlex (aws s3 sync … --no-sign-request).

TestAttenduObtenu
sortie 443 avec la NPaboutit301
sortie 443 sans la NP (NP retirée)bloquée000 (timeout)
NP réappliquée après le test (reconverge)présente✅ (trap RETURN)

Verdict du harnais : ok|Egress : flux Internet ouvert par la NP (avec=301, sans=bloqué). La NP est correcte ; le default-deny mord bien sans elle.

Drift rencontré et correctif (L59)

#SymptômeCauseCorrectif
L591er run : avec=000000, sans=000000 → faux échec « ça passe sans la NP »la probe faisait curl -w '%{http_code}' … 2>/dev/null || printf '000' ; quand l’egress est bloqué, curl imprime déjà 000 ET sort en erreur → le || ajoutait un second 000 (= 000000), verdict fausséretirer le fallback || printf 000 (curl émet déjà 000) + normaliser la sortie à « 3 chiffres sinon 000 » ; re-prouvé sur banc (avec=301, sans=000)

Diagnostic d’abord, correctif dans le code (ADR 0046). Le 000000 a été reproduit à la main sur le banc (probe manuelle = 301 avec la NP → la policy n’était PAS en cause), la cause isolée dans la fonction egress_probe_code, le correctif porté dans run-phases.sh (code versionné), puis re-prouvé par un run — jamais corrigé par un kubectl/patch laissé en l’état.

Chemin atlas opérationnel pour atlas — scénarios 27/28 + metrics-server (#252/#256, 2026-06-10)

Objectif : le chemin atlas doit livrer un banc consommable par les développements du dépôt atlas. Vérifié sur le banc atlas (local-path, multi-node-3 arm64) en jouant les scénarios d’intégration en mode STRICT (d’abord sur un banc complété à la main, puis re-prouvé from-scratch — cf. sous-section dédiée plus bas) :

PreuveRésultat
metrics-server (phase_metrics_server)✅ APIService Available:True, kubectl top nodes opérant (#252, L58)
Scénario 27 (STRICT_GITOPS=1)✅ push Gitea → webhook → Argo CD Synced/Healthy → run Dagster → lineage Marquez
Scénario 28 (STRICT_UI=1)✅ 5 UI via Gateway : argocd 200, dagster 200, gitea 403, marquez 200, grafana 302

Corrections de fond apportées au chemin (code versionné) :

  • metrics-server désormais posé nativement par le chemin atlas (palier 1, avant monitoring) — plus de kubectl apply manuel (#252).
  • NP allow-internet-egress ajoutée à la loop du rôle platform-dagster : elle existait dans le repo mais n’était PAS posée from-scratch → le sync OpenAlex serait resté coupé (#256).
  • Disques bruts conditionnés à Ceph dans phase_up (#235) : le banc léger ne crée plus que le disque OS.

Re-preuve from-scratch (run atlas léger neuf, 2026-06-10)

Un run atlas from-scratch (VMs détruites + remontées) a levé les réserves ci-dessus — vérifié sur le banc neuf, sans complétion manuelle (hors Gateways, posés par access.sh, ce qui est le flux d’accès dev normal) :

Vérifié from-scratchPreuve
NP allow-internet-egress posée par le rôlenetpol/allow-internet-egress présente (créée 08:26, bootstrap)
metrics-server natif dans le chemin✅ deploy créé 08:10, kubectl top nodes opérant
#235 — VMs sans disque brut en local-path✅ nœuds : vda (OS) + vdb (cidata Lima iso9660) seuls, aucun vdc/vdd/vde
Scénario 27 (STRICT_GITOPS) sur banc neuf✅ push → Argo CD Synced/Healthy → run Dagster → lineage Marquez
Scénario 28 (STRICT_UI) sur banc neuf✅ 5 UI via Gateway (200/200/403/200/302)

Réserve restante (ADR 0034/0046) — deb822_repository NON prouvé par ce run. Ce run a démarré le bootstrap (08:08 UTC) avant le commit de migration apt_repository → deb822_repository (08:12 UTC). Preuve directe sur la VM : /etc/apt/sources.list.d/ contient des *.list (ancien format apt_repository), pas des *.sources (format deb822). La migration deb822 reste donc à re-prouver par un PROCHAIN bootstrap from-scratch. Tout le reste de cette entrée (NP egress, metrics-server, #235, scénarios 27/28) est prouvé par ce run.

DataOps consommé par atlas : SeaweedFS débloqué + harnais code-location externe (#264, 2026-06-10)

Le dépôt atlas (première code-location citation-dagster, sync OpenAlex par rclone) a remonté deux points infra (issue #264). Les deux résolus et prouvés sur le banc atlas léger.

Volet 1 — « lenteur rclone » : la vraie cause était SeaweedFS, pas l’egress

Diagnostic par mesures isolées depuis un pod du ns dagster (915 Ko) :

SegmentMesureVerdict
Download S3 public (egress Internet)1 sjamais en cause (egress #256 OK)
Upload vers SeaweedFS (S3 interne)timeout > 90 sle goulot

Cause racine (logs SeaweedFS) : volume_growth: create volume, created 0: Not enough data nodes found / topo failed to pick 1 from 0 node candidates. Le data node était saturé (Max:13, Volumes:13, Free:0) : -volume.max=0 plafonnait le nombre de volumes trop bas → toute écriture d’une nouvelle collection (datalake, cnpg-backups, Loki) restait bloquée en TCP retries (d’où le rc=0 lent de plusieurs minutes observé par atlas). Pas de throttling pod, pas de MTU, pas d’egress.

Fix CODE (platform/seaweedfs/seaweedfs.yaml) : -volume.max=100 (au lieu de 0)

  • -master.volumeSizeLimitMB=1024. Re-prouvé (phase monitoring Ansible, ADR 0046) : Max:100, Free:80 → upload 915 Ko = 1 s (de >90 s à 1 s). Ajout aussi de la NetworkPolicy allow-s3-egress (ns dagster → S3 interne 8333/RGW 80-8080), posée par le rôle platform-dagster.

Volet 2 — harnais E2E paramétrable pour code-location externe

Nouveau test/scenarios/29-codelocation-externe.sh : généralise dataops_chain_emit_and_verify (asset jouet) en scénario paramétrable — le cluster fournit le « comment valider » (location chargée ? job exposé ? run SUCCESS ? aval reçu ?), le consommateur fournit le « contenu » (image/location/ job/runConfig). Run lancé par GraphQL launchRun (le chemin réel de l’UI), pas un Job synthétique. Frontière ADR 0022/0045 respectée.

Prouvé avec la code-location atlas RÉELLE (citation/ingestion_job), bornée comme atlas (sample_size: 1, partition: updated_date=2016-06-24, entities: [works]) :

✓ location chargée → ✓ job présent → ✓ run lancé (GraphQL) → ✓ run SUCCESS (14 s) → ✓ lineage Marquez

Le harnais a aussi attrapé un runConfig invalide (RunConfigValidationInvalid remonté) avant correction — preuve qu’il valide bien le schéma du job.

Outil déclaratif cluster_topology (depuis renommé nestor) — validation partielle sur banc vivant (P6, 2026-06-13)

Première confrontation de l’outil déclaratif (ADR 0056, paliers P0-P6) à un banc Lima vivant (3 nœuds lima-cp1/node1/node2 Ready, k8s v1.34.8, API 127.0.0.1:6443 — kubeconfig test/lima/.work/kubeconfig, jamais le contexte prod). Décrit une topologie multi-node-3 / dataops / ceph du banc et lance les commandes :

  • read-only (validate, status, generate --kind lima, epreuves, runs, metrics, next) : toutes correctes. epreuves filtre 29/29 jouables sur ce banc (lima ⇒ offensifs autorisés ; ceph ⇒ scénarios stockage ; dataops ⇒ toute la chaîne) ; metrics --last ré-expose les agrégats du run consigné (cpu_core_s=664, ram_peak=9340 MiB) ; next suggère la 1ʳᵉ phase manquante.
  • smoke (réversibilité, exig. 7) — cycle réel sur le cluster :
✓ créer (namespace topology-smoke-live) → ✓ vérifier présent → ✓ détruire → ✓ vérifier détruit (6,4 s) → réversible

Le namespace est confirmé disparu (kubectl get ns → NotFound) ; aucun résidu sur le banc. La revue avait signalé que vérifier l’absence juste après le delete donnerait un faux négatif (un namespace passe en Terminating quelques secondes) : le run réel l’a confirmé — l’attente du 404 réel (_wait_gone) prend ~6 s ici. Garde-fou de sûreté vérifié aussi : smoke contre un cluster injoignable échoue en 5 s (cluster injoignable, code 2), jamais de blocage ni de faux « non réversible ».

Reste non éprouvé sur banc : next --apply (lancement d’une phase via ansible-runner) et status --real (SSH) — à consigner lors d’un run dédié.