Les jobs CI du dépôt kadans réinstallent, à CHAQUE exécution, des choses qui
changent tous les trimestres. Mesuré le 2026-07-29 sur le run 628 (runner
ARM64, 2 vCPU / 3 Gio) :
npm install -g bun → 8,6 s
playwright install --with-deps chromium → 104,0 s (apt-get, à chaque run)
────────
112,6 s jetées par run, sur le
job qui EST le chemin critique
Le cache `actions/cache` ne peut rien contre ces 104 s : il couvre le NAVIGATEUR
(299 Mo déjà mis en cache), pas ses dépendances SYSTÈME — `--with-deps` relance
apt quoi qu'il arrive.
POURQUOI CONSTRUIRE ICI PLUTÔT QUE POUSSER UNE IMAGE. La tentative de publier
l'image au registre a échoué (kadans#224 puis #225) : 3,81 Go dont une couche
unique de 1,36 Go, `docker push` casse en « connection reset by peer » — 7
couches passent, 3 sont retentées 50 fois puis abandonnées. À titre de
comparaison, runner-images:ubuntu-latest-ca (534,7 Mo, plus grosse couche
261 Mo) passe sans problème : la limite est entre 261 Mo et ~500 Mo par couche.
Construire localement supprime le problème — aucune couche ne traverse le
réseau.
Et c'est bien sur CHAQUE machine : avec `capacity: 1`, le parallélisme vient de
plusieurs Raspberry, et `container:` est résolu par le runner. Un job qui
atterrit là où l'image manque échoue AVANT sa première étape.
Trois choix de conception :
1. L'image hérite de runner-images:ubuntu-latest-ca, donc du certificat de la CA
interne (step-ca). Repartir de node:20-bookworm obligerait à réinjecter le CA
à la main, et tout job parlant à gitea.arcodange.lab échouerait en TLS.
2. TROIS `RUN` séparés (bun / dépendances système / navigateur), délibérément.
La version qui a échoué faisait une couche de 1,36 Go. Ne pas les fusionner
pour « gagner une couche ».
3. L'image est ÉPINGLÉE par un conteneur factice — ce qui implémente enfin la
section 1 de docs/adr/20260407-docker-storage-gitea-runner.md, restée à
l'état de proposition : system_docker.yml n'applique que le data-root sur
disque externe et les log-opts. Sans épinglage, le ramasse-miettes de Docker
supprime l'image dès que le disque se remplit — panne déjà constatée sur les
images de runner elles-mêmes.
Le rôle vérifie sa sortie (`docker run … bun --version`) au lieu de supposer que
le build a suffi, et l'image écrit ses versions dans /etc/ci-base.versions pour
que la CI de kadans puisse les confronter à son bun.lock et échouer FORT sur une
dérive, plutôt que de la découvrir en « Executable doesn't exist ».
Nouveau label runner `ci-node-playwright`. `container.force_pull: false` est
déjà en place et devient REQUIS pour ce label : sans lui, act_runner tenterait
un pull d'une image qui n'est dans aucun registre.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
The never-yet-applied k3s_dns.yml placed 'import /etc/coredns/custom/*.server'
INSIDE the .:53 server block. *.server files hold full server blocks
(arcodange.lab:53 {…}), which only parse at Corefile root — inside a block
CoreDNS dies at startup with "Unknown directive 'arcodange.lab:53'"
(CrashLoopBackOff, cluster DNS fully down; lived it on 2026-07-24 while
restoring the expired *.arcodange.lab certificate).
Also restores the stock 'loadbalance' plugin dropped by the playbook.
Context: cluster CoreDNS forwarded to the node's resolv.conf, which lists the
ISP box's IPv6 RDNSS next to the Pi-holes — NXDOMAIN roulette for *.lab names.
That's what left step-issuer unable to reach ssl-ca.arcodange.lab:8443 and let
the 24h wildcard cert expire this morning. The (fixed) playbook pins .lab
resolution to the Pi-holes via the coredns-custom ConfigMap; applied live on
2026-07-24, wildcard renewed, strict TLS verified on gitea/argocd/grafana.
Co-Authored-By: Claude Fable 5 <[email protected]>
Incident 2026-07-23: an uncapped nuxt generate (3.5G RSS) on pi1 starved the
k3s control-plane and traefik (load >150, no swap, no OOM-kill) — every
*.arcodange.lab endpoint went dark, Gitea included, while Gitea itself was
healthy on pi2. Job containers are spawned via the host docker socket, so
cgroup caps on the job container are the only guardrail.
Applied live on pi1+pi3 via 03_cicd.yml on 2026-07-24 (both runners
re-registered; pi3's runner was down and is back in service).
Co-Authored-By: Claude Fable 5 <[email protected]>
Removes the commented PACKAGES_TOKEN/HOMELAB_CA_CERT blocks and the legacy
"Deploy Argo CD" play that were left behind during the migration to
Helm-based ArgoCD.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
ansible/arcodange/factory/ansible.cfg sets collections_path so ansible
commands run from inside the collection directory still find user-installed
collections under ~/.ansible/collections.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Re-running the role would leave behind crowdsec pods stuck in Failed phase
(typically after a config error on a previous run), which then blocked the
Traefik middleware refresh. Delete them up front so the next reconcile
schedules fresh pods.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Pins the actcache server to a fixed port (43707) and exposes it, then
mounts /mnt/arcodange/gitea-runner-cache and /mnt/arcodange/gitea-runner-act
into the runner so the actions/cache and act image layer cache survive
container restarts. Moves the runner onto a dedicated `gitea_action_network`
so CI job containers can reach the cache server by name without sharing the
host network.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Replaces the placeholder "Success Metrics" section with a detailed
walkthrough of the internal PKI: Step CA provisioners, cert-manager +
StepClusterIssuer wiring, certificate issuance/renewal sequence diagram,
device-trust installation steps, and troubleshooting playbook for the
common stuck-CertificateRequest / Traefik TLS / device-trust failures.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Captures the post-mortem of the April 13 power-cut: incident timeline,
retrospective, and architecture/role diagrams. Adds an ADR explaining why
Longhorn cannot re-associate orphaned replica directories after a nuclear
reinstall (engine-id naming), plus block-device recovery runbooks and the
`playbooks/recover/longhorn_data.yml` automation that wires `merge-longhorn-layers.py`
to rebuild PVCs from raw `volume-head-*.img` chains.
Also extends the k3s_pvc backup to capture Longhorn `volumes`/`settings` CRDs
(needed for the fast-path restore) and rewrites the restore script with a
fallback dir + English messages.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Moves the local ansible runtime from a global `uv tool install ansible-core`
(which required remembering `--with kubernetes --with jmespath --with dnspython`)
to a project-managed venv described by `pyproject.toml` + `uv.lock`. Fixes the
"Failed to import the required Python library (kubernetes)" error on localhost.
The localhost inventory entry now derives `ansible_python_interpreter` from
`{{ ansible_playbook_python }}`, so `uv run ansible-playbook` is enough — no
more hardcoded user-specific paths.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
During the 2026-04-13 power cut recovery, DNS resolution failures blocked
Longhorn reinstall. Root causes:
- CoreDNS forwarded to a single hardcoded Pi-hole IP instead of both HA instances
- CoreDNS main Corefile forwarded to /etc/resolv.conf which pointed to itself on pi3
- Pi-hole lacked explicit upstream DNS, relying on DHCP-provided config
- dnsmasq system service conflicted with pihole-FTL on port 53
Changes:
- k3s_dns: forward CoreDNS to both Pi-hole HA instances (pi1 + pi3) dynamically
- k3s_dns: update main CoreDNS Corefile to forward to Pi-holes instead of resolv.conf
- pihole defaults: add explicit upstream DNS servers (8.8.8.8, 1.1.1.1, 8.8.4.4)
- pihole ha_setup: write /etc/dnsmasq.d/99-upstream.conf with explicit upstreams
- rpi: add dnsmasq user to dip group and disable conflicting dnsmasq service on Pi-hole nodes
See docs/adr/20260414-internal-dns-architecture.md for full rationale.
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Two bugs caused daemon.json to be overwritten with invalid content:
- Invalid `when` condition using unsupported Ansible inline stat syntax,
causing the existing file read to be silently skipped and docker_config
to always reset to {}
- Folded scalar `>` in set_fact converted the dict to a Python string
representation, which to_nice_json serialized as a JSON string instead
of an object
Fixes identified during 2026-04-13 power cut incident post-mortem.
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
This commit adds a detailed sequence diagram to the Docker storage optimization ADR, illustrating the workflow for configuring Docker storage, pinning images, and maintaining Longhorn performance.
Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <[email protected]>
This commit moves Architecture Decision Records (ADRs) from ../../../docs/adr/ to docs/adr/ in the arcodange/factory repository. This centralizes all ADRs in one location for better maintainability and discoverability.
Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <[email protected]>
This commit updates the README to include a detailed timeline of the playbook execution sequence, organized into sections for system setup, application setup, CI/CD, tools, and backups.
Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <[email protected]>
This commit uncomments the PostgreSQL backup section in the backup playbook to enable regular backups of the PostgreSQL database.
Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <[email protected]>
The default kube-rbac-proxy image (gcr.io/kubebuilder/kube-rbac-proxy:v0.15.0) is AMD64-only and fails on pi3 (ARM64). This commit overrides the image to use quay.io/brancz/kube-rbac-proxy:v0.15.0, which supports ARM64.
Note: pi2 (ARMv7) may work with AMD64 images, but pi3 (ARM64) requires an ARM64-compatible image.
Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <[email protected]>