Un Pi qui démarre sans son disque USB : k3s repart, mais 01_system.yml échoue sur ce nœud et Docker se replie en silence sur la carte SD. Audit, rien d'appliqué #63

Open
opened 2026-09-25 20:35:13 +02:00 by arcodange · 0 comments
Owner

La contrainte (fondateur, 2026-09-25)

« il faut que la factory puisse reconfigurer l'essentiel même si les RPI démarrent sans hard disk »

L'essentiel, ici : k3s, le réseau (DNS, ingress), le control-plane. Chaque Pi démarre sur sa carte SD ; le disque USB (LABEL=arcodange_500) est monté sur /mnt/arcodange.

Audit en lecture seule : dépôt au commit c2aab58, cluster relevé le 2026-09-25 vers 18 h 30 UTC. Rien n'a été modifié. Réglage demandé en même temps : #64 (ramasse-miettes d'images du kubelet).

Réponses courtes

Question Réponse Preuve
Le nœud démarre-t-il ? Oui fstab : LABEL=arcodange_500 /mnt/arcodange ext4 defaults,nofail (prepare_disks.yml)
Docker et containerd démarrent-ils ? Oui, mais Docker démarre en silence sur un data-root vide posé sur la carte SD daemon.json garde data-root: /mnt/arcodange/docker ; le point de montage vide est un dossier de la SD ; docker.service n'a aucun RequiresMountsFor ni After= sur le montage. /var/lib/containerd est sur la SD.
k3s démarre-t-il ? Oui Son état (/var/lib/rancher/k3s, /var/lib/kubelet) est sur la SD. Aucun argument k3s ne nomme le disque. cri-dockerd n'a besoin que de dockerd.
Le playbook passe-t-il ? Non. 01_system.yml échoue pour ce nœud à l'étape 4, et la factory ne peut plus y reconfigurer ni Docker ni k3s Reproduit ci-dessous

Le playbook échoue à prepare_disks.yml

Sans partition hors mmcblk0, la tâche « Choisir une partition candidate » évalue [] | sort(attribute='size') | last. Rejoué en local avec la même expression et all_partitions: [] :

fatal: [localhost]: FAILED! => {"msg": "... Error while resolving value for 'target_partition': No last item, sequence was empty."}

system.yml enchaîne prepare_disks → system_docker → iscsi_longhorn → system_k3s → k3s_dns → k3s_ssl → k3s_config dans la même exécution. Un hôte en échec est retiré de toutes les pièces suivantes : sur un Pi démarré sans disque, 01_system.yml ne touche plus ni à Docker ni à k3s. Jouer system_k3s.yml seul passe.

Deux cas voisins :

  • Une autre clé USB est branchée : elle devient la « candidate », et le playbook propose de la formater. La pause run_once est le seul garde-fou.
  • Le disque étiqueté est présent mais pas monté (par exemple, arrivé trop tard au démarrage) : la tâche « Vérifier si la partition est déjà montée » lit target_partition, qui n'a pas été posée. Rejoué en local : 'target_partition' is undefined. Le playbook échoue donc aussi dans le cas où il devrait simplement remonter le disque.

Rejeux faits avec ansible-core 2.20.5 (le .venv du dépôt), sur localhost, en reprenant les expressions exactes des tâches.

Ce qui dépend de /mnt/arcodange

Élément Où Sans le disque
Montage du disque system/prepare_disks.yml (nofail) Le démarrage continue ✅. Le playbook échoue ❌ (voir plus haut).
data-root de Docker system/system_docker.yml : posé seulement si le disque est monté au moment du playbook, mais conservé pour toujours (le daemon.json existant est relu, puis fusionné) Docker démarre sur un /mnt/arcodange/docker vide, sur la SD. Il perd toutes les images et tous les conteneurs, car leurs définitions vivent dans le data-root. Chaque pod du nœud retélécharge ses images sur la SD. Espace libre relevé sur les SD : pi1 36,8 Go, pi2 61,1 Go, pi3 17,9 Go. ⚠ Si le disque arrive en retard (disque USB lent à tourner) et se monte par-dessus un Docker déjà lancé, le data-root vivant est masqué.
Gitea et Postgres (compose, pi2) inventory/group_vars/{gitea,postgres} Leurs données sont sur la SD (/home/pi/arcodange/docker_composes/…) ✅. Leurs conteneurs disparaissent avec le data-root : rien ne tourne avant un nouveau docker compose up depuis le playbook.
Disque Longhorn system/k3s_config.yml (defaultDataPath: /mnt/arcodange/longhorn), system/iscsi_longhorn.yml (crée le dossier, ignore_errors) Réplicas perdus sur ce nœud. Les volumes gardent leurs répliques ailleurs (default-replica-count = 3), en mode dégradé. Si les trois Pi démarrent sans disque, tous les volumes tombent. D'après la lecture du moniteur de disque de Longhorn (non éprouvé ici), il réécrit un longhorn-disk.cfg neuf sur la SD et marque le disque NotReady quand l'UUID change.
Ingress Traefik system/k3s_config.yml : plugin CrowdSec lu depuis le PVC Longhorn traefik-plugins-local, avec abortOnPluginFailure: true (#62) L'ingress dépend d'au moins un disque externe sain dans le cluster. Volume relevé : 3 répliques, attaché sur pi1, degraded. Si aucune réplique n'est servie, Traefik ne démarre pas et *.arcodange.lab tombe.
Runner CI (pi3) 03_cicd.yml : caches /mnt/arcodange/gitea-runner-{cache,act} Les caches sont recréés vides sur la SD. L'image ci-node-playwright et son conteneur d'épinglage vivent dans le data-root : ils sont perdus, et les jobs lourds échouent jusqu'au prochain 03_cicd.
Sauvegardes /mnt/backups setup/backup_nfs.yml : RWX Longhorn, nofail,_netdev,x-systemd.automount Suit la disponibilité de Longhorn ✅
Récupération Longhorn recover/longhorn.yml : pré-vol sur /mnt/arcodange/longhorn Échoue exprès : c'est une procédure de récupération.

Aucune dépendance : Pi-hole et step-ca (installés nativement, sur la SD), le datastore k3s, les données de Gitea et de Postgres.

Propositions, par ordre de valeur

  1. Faire de « pas de disque » un état normal de prepare_disks.yml. Poser un fait arcodange_hdd_present. Sans disque étiqueté : message, puis meta: end_host (le nœud reste dans les pièces suivantes). Ne jamais formater une partition sans étiquette sauf demande explicite (-e prepare_disks_allow_format=true). Et ne plus lire target_partition quand elle n'a pas été posée.
  2. Donner à Docker un repli explicite, et l'ordonner derrière le disque. Le data-root devient /var/lib/docker. Quand le disque est là, /mnt/arcodange/docker y est monté en bind : entrée fstab en bind,nofail,x-systemd.requires-mounts-for=/mnt/arcodange. Un drop-in docker.service ajoute After= et Wants= sur ce montage, pas Requires= : sans disque, Docker démarre quand même, sur la SD. Le montage du disque reçoit un x-systemd.device-timeout= court, pour qu'un disque lent soit attendu avant Docker au lieu de se monter par-dessus lui. Ce que fait systemd quand le disque arrive après le délai reste à vérifier sur un Pi.
  3. Rendre le point de montage vide immuable (chattr +i /mnt/arcodange, disque démonté, une seule fois). Plus rien ne peut écrire dans le dossier de la SD quand le disque manque : ni Docker, ni un longhorn-disk.cfg fantôme, ni les caches du runner. Le montage par-dessus reste possible. Ça suppose la proposition 2, sinon Docker ne démarre plus.
  4. Longhorn : un disque absent n'est plus ordonnançable, et c'est dit. Avec la proposition 3, Longhorn ne peut plus réécrire sa configuration sur la SD et marque le disque NotReady. Pour l'expliciter, le playbook peut passer allowScheduling: false sur le disque du nœud (nodes.longhorn.io) quand arcodange_hdd_present est faux.
  5. Découpler l'ingress de Longhorn. Le plugin CrowdSec pèse 574 Ko. Un hostPath sur la SD de pi1, ou une image, rend l'ingress indépendant de tout disque externe. Voir aussi #61.
  6. La SD devient le disque des images quand le disque externe manque. Le ramasse-miettes du kubelet à 65 % → 55 % (#64) la protège dans ce cas aussi. Sans disque, la SD de pi3 (66 % occupés, sans aucune image) serait nettoyée en continu, ce qui est le comportement voulu.

Le plus urgent pour la contrainte est la proposition 1 : c'est elle qui empêche aujourd'hui la factory de reconfigurer k3s. Les propositions 2 et 3 protègent la SD et les données.

🤖 Generated with Claude Code

## La contrainte (fondateur, 2026-09-25) > « il faut que la factory puisse reconfigurer l'essentiel même si les RPI démarrent sans hard disk » L'essentiel, ici : k3s, le réseau (DNS, ingress), le control-plane. Chaque Pi démarre sur sa carte SD ; le disque USB (`LABEL=arcodange_500`) est monté sur `/mnt/arcodange`. Audit en lecture seule : dépôt au commit `c2aab58`, cluster relevé le 2026-09-25 vers 18 h 30 UTC. **Rien n'a été modifié.** Réglage demandé en même temps : #64 (ramasse-miettes d'images du kubelet). ## Réponses courtes | Question | Réponse | Preuve | |---|---|---| | Le nœud démarre-t-il ? | **Oui** | `fstab` : `LABEL=arcodange_500 /mnt/arcodange ext4 defaults,nofail` (`prepare_disks.yml`) | | Docker et containerd démarrent-ils ? | **Oui, mais Docker démarre en silence sur un `data-root` vide posé sur la carte SD** | `daemon.json` garde `data-root: /mnt/arcodange/docker` ; le point de montage vide est un dossier de la SD ; `docker.service` n'a aucun `RequiresMountsFor` ni `After=` sur le montage. `/var/lib/containerd` est sur la SD. | | k3s démarre-t-il ? | **Oui** | Son état (`/var/lib/rancher/k3s`, `/var/lib/kubelet`) est sur la SD. Aucun argument k3s ne nomme le disque. cri-dockerd n'a besoin que de `dockerd`. | | Le playbook passe-t-il ? | **Non. `01_system.yml` échoue pour ce nœud à l'étape 4, et la factory ne peut plus y reconfigurer ni Docker ni k3s** | Reproduit ci-dessous | ### Le playbook échoue à `prepare_disks.yml` Sans partition hors `mmcblk0`, la tâche « Choisir une partition candidate » évalue `[] | sort(attribute='size') | last`. Rejoué en local avec la même expression et `all_partitions: []` : ``` fatal: [localhost]: FAILED! => {"msg": "... Error while resolving value for 'target_partition': No last item, sequence was empty."} ``` `system.yml` enchaîne `prepare_disks` → `system_docker` → `iscsi_longhorn` → `system_k3s` → `k3s_dns` → `k3s_ssl` → `k3s_config` dans la même exécution. Un hôte en échec est retiré de toutes les pièces suivantes : **sur un Pi démarré sans disque, `01_system.yml` ne touche plus ni à Docker ni à k3s.** Jouer `system_k3s.yml` seul passe. Deux cas voisins : - **Une autre clé USB est branchée** : elle devient la « candidate », et le playbook propose de la **formater**. La pause `run_once` est le seul garde-fou. - **Le disque étiqueté est présent mais pas monté** (par exemple, arrivé trop tard au démarrage) : la tâche « Vérifier si la partition est déjà montée » lit `target_partition`, qui n'a pas été posée. Rejoué en local : `'target_partition' is undefined`. Le playbook échoue donc aussi dans le cas où il devrait simplement remonter le disque. Rejeux faits avec ansible-core 2.20.5 (le `.venv` du dépôt), sur `localhost`, en reprenant les expressions exactes des tâches. ## Ce qui dépend de `/mnt/arcodange` | Élément | Où | Sans le disque | |---|---|---| | Montage du disque | `system/prepare_disks.yml` (`nofail`) | Le démarrage continue ✅. Le playbook échoue ❌ (voir plus haut). | | `data-root` de Docker | `system/system_docker.yml` : posé seulement si le disque est monté au moment du playbook, **mais conservé pour toujours** (le `daemon.json` existant est relu, puis fusionné) | Docker démarre sur un `/mnt/arcodange/docker` vide, sur la SD. Il perd **toutes** les images et **tous** les conteneurs, car leurs définitions vivent dans le `data-root`. Chaque pod du nœud retélécharge ses images sur la SD. Espace libre relevé sur les SD : pi1 36,8 Go, pi2 61,1 Go, pi3 17,9 Go. ⚠ Si le disque arrive **en retard** (disque USB lent à tourner) et se monte par-dessus un Docker déjà lancé, le `data-root` vivant est masqué. | | Gitea et Postgres (compose, pi2) | `inventory/group_vars/{gitea,postgres}` | Leurs **données** sont sur la SD (`/home/pi/arcodange/docker_composes/…`) ✅. Leurs **conteneurs** disparaissent avec le `data-root` : rien ne tourne avant un nouveau `docker compose up` depuis le playbook. | | Disque Longhorn | `system/k3s_config.yml` (`defaultDataPath: /mnt/arcodange/longhorn`), `system/iscsi_longhorn.yml` (crée le dossier, `ignore_errors`) | Réplicas perdus sur ce nœud. Les volumes gardent leurs répliques ailleurs (`default-replica-count` = 3), en mode dégradé. Si les **trois** Pi démarrent sans disque, tous les volumes tombent. D'après la lecture du moniteur de disque de Longhorn (non éprouvé ici), il réécrit un `longhorn-disk.cfg` neuf **sur la SD** et marque le disque NotReady quand l'UUID change. | | Ingress Traefik | `system/k3s_config.yml` : plugin CrowdSec lu depuis le PVC Longhorn `traefik-plugins-local`, avec `abortOnPluginFailure: true` (#62) | L'ingress dépend d'**au moins un disque externe sain dans le cluster**. Volume relevé : 3 répliques, attaché sur pi1, `degraded`. Si aucune réplique n'est servie, Traefik ne démarre pas et `*.arcodange.lab` tombe. | | Runner CI (pi3) | `03_cicd.yml` : caches `/mnt/arcodange/gitea-runner-{cache,act}` | Les caches sont recréés vides sur la SD. L'image `ci-node-playwright` et son conteneur d'épinglage vivent dans le `data-root` : ils sont perdus, et les jobs lourds échouent jusqu'au prochain `03_cicd`. | | Sauvegardes `/mnt/backups` | `setup/backup_nfs.yml` : RWX Longhorn, `nofail,_netdev,x-systemd.automount` | Suit la disponibilité de Longhorn ✅ | | Récupération Longhorn | `recover/longhorn.yml` : pré-vol sur `/mnt/arcodange/longhorn` | Échoue exprès : c'est une procédure de récupération. | **Aucune dépendance** : Pi-hole et step-ca (installés nativement, sur la SD), le datastore k3s, les données de Gitea et de Postgres. ## Propositions, par ordre de valeur 1. **Faire de « pas de disque » un état normal de `prepare_disks.yml`.** Poser un fait `arcodange_hdd_present`. Sans disque étiqueté : message, puis `meta: end_host` (le nœud reste dans les pièces suivantes). Ne **jamais** formater une partition sans étiquette sauf demande explicite (`-e prepare_disks_allow_format=true`). Et ne plus lire `target_partition` quand elle n'a pas été posée. 2. **Donner à Docker un repli explicite, et l'ordonner derrière le disque.** Le `data-root` devient `/var/lib/docker`. Quand le disque est là, `/mnt/arcodange/docker` y est monté en bind : entrée `fstab` en `bind,nofail,x-systemd.requires-mounts-for=/mnt/arcodange`. Un drop-in `docker.service` ajoute `After=` et `Wants=` sur ce montage, **pas `Requires=`** : sans disque, Docker démarre quand même, sur la SD. Le montage du disque reçoit un `x-systemd.device-timeout=` court, pour qu'un disque lent soit attendu avant Docker au lieu de se monter par-dessus lui. Ce que fait systemd quand le disque arrive après le délai reste **à vérifier** sur un Pi. 3. **Rendre le point de montage vide immuable** (`chattr +i /mnt/arcodange`, disque démonté, une seule fois). Plus rien ne peut écrire dans le dossier de la SD quand le disque manque : ni Docker, ni un `longhorn-disk.cfg` fantôme, ni les caches du runner. Le montage par-dessus reste possible. Ça suppose la proposition 2, sinon Docker ne démarre plus. 4. **Longhorn : un disque absent n'est plus ordonnançable, et c'est dit.** Avec la proposition 3, Longhorn ne peut plus réécrire sa configuration sur la SD et marque le disque NotReady. Pour l'expliciter, le playbook peut passer `allowScheduling: false` sur le disque du nœud (`nodes.longhorn.io`) quand `arcodange_hdd_present` est faux. 5. **Découpler l'ingress de Longhorn.** Le plugin CrowdSec pèse 574 Ko. Un `hostPath` sur la SD de pi1, ou une image, rend l'ingress indépendant de tout disque externe. Voir aussi #61. 6. **La SD devient le disque des images quand le disque externe manque.** Le ramasse-miettes du kubelet à 65 % → 55 % (#64) la protège dans ce cas aussi. Sans disque, la SD de pi3 (66 % occupés, sans aucune image) serait nettoyée en continu, ce qui est le comportement voulu. Le plus urgent pour la contrainte est la proposition 1 : c'est elle qui empêche aujourd'hui la factory de reconfigurer k3s. Les propositions 2 et 3 protègent la SD et les données. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign in to join this conversation.
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: arcodange-org/factory#63