Home lab - Usine Logicielle
Laissez moi vous présenter quelques facettes de mon "laboratoire domestique", ou "usine logicielle". J'appelle ainsi cette configuration de 3 raspberry pi qui me sert de datacenter.
Home lab - Usine Logicielle
Laissez moi vous présenter quelques facettes de mon "laboratoire domestique", ou "usine logicielle".
J'appelle ainsi cette configuration de 3 raspberry pi qui me sert de datacenter.
🐧 pi1
🐧 pi2
🐧 pi3
Équipement principal
Les Raspberry Pi (ou rpi / pi ) sont connectés en éthernet sur la livebox. La livebox sert de routeur et de passerelle/gateway vers internet.
Ils possèdent des cartes wifi fonctionnelles, mais elles surchauffent si on utilise un outil comme Longhorn qui réplique les données des disques durs externes pour prévenir des incidents.
| Service | Usage |
|---|---|
| Google Cloud | stockage de données (backup et terraform) |
| Cloudflare | DNS global, site web static, captcha |
| OVH | nom de domaine en ".fr" |
| Zoho | mail pro |
| CrowdSec | pare-feu |
| DuckDNS | DNS global gratuit (pas assez fiable) |
Réseau
Plusieurs choses apparaissent sur ce schéma.
On retrouve le cluster de 3 raspberry pi.
- J'ai installé K3S, une version plus légère de K8S (Kubernetes).
- J'ai fait le choix d'utiliser Docker, et c'est avec docker compose que je déploie Gitea, une version plus légère de Gitlab qui supporte la CI/CD de Github sur des "Gitea Act Runner".
- L'installation de chaque composant de mon usine logicielle est scripté avec Ansible.
- Le DNS local, qui me permet de désigner mes services internes sous le domaine "arcodange.lab" est assuré par Pi Hole, avec une règle "wildcard/joker" qui redirige les requêtes interne vers le point d'entrée du cluster K3S.
- Le point d'entrée du réseau est Traefik (le reverse proxy). Si une requête vient d'internet alors elle passe par des middleware de sécurité, puis Traefik route la requête vers l'application adéquate.
- Pour exposer mes applications sur internet, j'ai configuré, avec OpenTofu (le fork libre de Terraform), les zones DNS de mon domaine "arcodange.fr" acheté sur OVH et géré via Cloudflare, et déployé le client cloudflared de Cloudflare Tunnel.
Vous pouvez donc visiter cms-rec.arcodange.fr ou encore gitea.arcodange.fr hébergés sur mes Raspberry Pi.
Ce que le lab héberge vraiment
Trois cartes, et le réflexe est de croire à une maquette. Le relevé du 31 août 2026 dit autre chose : 88 conteneurs en marche, répartis en 23 applications gouvernées par ArgoCD.
| pi1 | pi2 | pi3 | |
|---|---|---|---|
| Architecture | arm64 | arm64 | arm64 |
| Cœurs | 4 | 4 | 4 |
| Mémoire | 8 Go | 8 Go | 8 Go |
| Système | Debian 12 | Debian 12 | Debian 12 |
Soit 12 cœurs et 24 Go en tout — moins qu'un serveur de bureau, et c'est exactement le point. La contrainte n'est pas décorative : c'est elle qui rend visibles des erreurs d'architecture qu'une machine confortable absorberait sans rien dire. Un build qui fuit se voit ici en quinze minutes ; sur 64 Go, il passerait des mois inaperçu.
Ce qui y tourne se range en trois familles :
- Le socle —
factory(provisionnement et déploiement) ettools(Vault, Prometheus, Grafana, CrowdSec, les poolers de connexions). - Mes services — ce site, un raccourcisseur d'URL, une passerelle Telegram qui me sert de canal d'alerte, un assistant de cours de danse.
- Le reste — des applications métier et des bases (PostgreSQL, Redis, ClickHouse, MinIO) qui ne sont pas publiques, et un environnement bac à sable qui rejoue la production.
C'est ce dernier point qui fait la différence entre un laboratoire et une démonstration : la plateforme porte des choses dont la panne me coûte quelque chose.
DevOps — qui a le droit de déployer
Un laboratoire domestique devient une usine le jour où plus personne — moi compris — ne déploie à la main. Le principe tient en une phrase : Gitea est la seule source de vérité, et ArgoCD est la seule autorité de déploiement. Tout le reste en découle.
- Je pousse du code depuis le MacBook vers Gitea, qui héberge tous les dépôts.
- Gitea déclenche un Act Runner — le même format de workflow que GitHub Actions. Les runners tournent sur pi1, pi3, et sur le MacBook lui-même pour les builds lourds.
- Le runner construit l'image et la pousse dans le registry intégré à Gitea.
- En parallèle, ArgoCD lit dans Gitea l'état désiré du cluster : il n'attend pas qu'on lui pousse quelque chose, il compare en boucle ce qui tourne à ce qui est écrit.
- ArgoCD applique les charts dans les namespaces k3s, qui tirent leurs images du registry.
- Vault injecte les secrets dans les pods ; le runner, lui, s'authentifie auprès de Vault par OIDC pour ses propres besoins (voir plus bas).
Relevé le 30 août 2026 : ArgoCD gouverne 23 applications sur les trois Raspberry Pi, en k3s v1.34.3, et le cluster tourne depuis 249 jours.
Vault, et rien que Vault
Il n'y a aucun secret dans mes dépôts — ni fichier chiffré, ni sops, ni age. Si un identifiant existe, Vault le stocke ou le fabrique à la demande. Les charts et le code d'infrastructure référencent des chemins Vault, jamais des valeurs : un dépôt qui fuite ne fait fuiter aucun identifiant, et il n'y a qu'un seul endroit où révoquer.
Deux familles de consommateurs, deux authentifications :
| Qui | Comment il prouve son identité | Ce qu'il obtient |
|---|---|---|
| Les pods | l'auth Kubernetes, via le Vault Secrets Operator | des Secret k8s repeuplés automatiquement |
| La CI et l'IaC | un JWT OIDC émis par Gitea — un rôle par application | un jeton de courte durée, limité à cette application |
C'est le second point qui change tout : mon pipeline ne détient pas de mot de passe qu'on pourrait lui voler, il détient une identité que Gitea atteste au moment de l'exécution.
Pour PostgreSQL, Vault ne stocke même pas d'identifiant : il en fabrique un neuf, à durée limitée, pour chaque consommateur. Aucun mot de passe de base de données n'est écrit nulle part, donc aucun n'est à faire tourner.
La contrepartie est réelle et je l'assume : Vault redémarre scellé. Tant qu'un humain ne l'a pas descellé, rien de ce qui dépend d'un secret ne remonte. C'est un point de blocage manuel dans une plateforme par ailleurs entièrement automatisée — et c'est un choix, pas un oubli : je préfère une plateforme qui refuse de repartir toute seule à une plateforme dont la clé traîne quelque part pour lui éviter d'attendre.
L'infrastructure comme code, jusqu'au fournisseur
OpenTofu 1.8.2 (le fork libre de Terraform) décrit ce qui vit hors du cluster : les zones DNS Cloudflare, la messagerie Zoho, les utilisateurs et jetons Gitea, les bases PostgreSQL et leurs rôles. L'état est distant et versionné — sur Google Cloud Storage pour l'infrastructure socle, sur Cloudflare R2 pour ce site.
Responsabilité des dépôts
Trois dépôts, trois responsabilités qui ne se recouvrent pas :
| Dépôt | Ce qu'il possède |
|---|---|
| factory | Le socle : provisionne le cluster (Ansible), déclare ce qui se déploie (ArgoCD), tient l'état d'infrastructure (OpenTofu) et les bases de données |
| tools | Les services de plateforme : Vault, Prometheus, Grafana, CrowdSec, les poolers de connexions |
| cms | Ce site — le contenu, le chart Helm, et l'IaC de sa zone DNS et de sa messagerie |
Et une convention de nommage qui les soude : un seul identifiant par application, réutilisé à l'identique du dépôt Gitea jusqu'au DNS, en passant par la base PostgreSQL et son rôle, les chemins Vault, le namespace Kubernetes, l'Application ArgoCD et le préfixe de l'état OpenTofu.
C'est ce qui permet d'ajouter une application sans configuration de liaison — les briques se trouvent par convention. C'est aussi la fragilité du procédé, et je la documente comme telle : une faute de frappe casse la chaîne en silence, sans erreur, juste une brique qui ne trouve pas l'autre.
Persistance — deux étages, délibérément
La question qui décide de tout : qu'est-ce qui doit pouvoir redémarrer quand le cluster ne va pas bien ? La réponse dessine deux étages de stockage.
| Étage | Support | Ce qui y vit | Pourquoi là |
|---|---|---|---|
| Dans le cluster | Longhorn, stockage bloc distribué et répliqué sur les trois Pi | Les volumes des applications, les certificats de Traefik, le volume de sauvegarde lui-même | Un volume répliqué, capable de snapshot, qui suit le pod d'un nœud à l'autre |
| Hors du cluster | docker compose, sur le disque local de pi2 | PostgreSQL et Gitea | Ce sont les fondations : Gitea sert la source que le cluster lit, Postgres porte les données des applications. Ils doivent démarrer sans que k3s soit en bonne santé |
Ce découpage est ce qui permet à la plateforme de s'amorcer elle-même : Gitea et Postgres remontent sur pi2 tout seuls, et c'est seulement ensuite qu'ArgoCD a quelque chose à lire. Un Gitea qui vivrait dans le cluster qu'il sert à déployer serait une dépendance circulaire — élégante sur le papier, ingérable un dimanche soir.
Au même relevé, 18 volumes Longhorn sont montés.
Sauvegardes : trois tâches, une même anatomie
Trois sauvegardes indépendantes tournent en cron à 4 h du matin : un pg_dumpall compressé
pour PostgreSQL, un gitea dump pour la forge, et une sérialisation des métadonnées de volumes
Kubernetes (PV, PVC et objets Longhorn). Chacune écrit une archive datée, et supprime ce qui a
plus de trois jours.
Le détail qui compte : ce répertoire de sauvegardes est lui-même un volume Longhorn partagé. Longhorn le réplique et l'expédie hors site — les tâches cron n'ont donc rien à téléverser, elles produisent le fichier et Longhorn s'occupe de le mettre à l'abri.
La sauvegarde des métadonnées de volumes semble la plus anecdotique des trois. C'est celle qui m'a le plus servi.
Dépôts miroirs
Gitea reste la source de vérité, mais chaque dépôt est poussé en miroir vers GitHub, toutes les 8 heures et à chaque commit. Rien n'est jamais rapatrié : le miroir ne va que dans un sens, et une modification faite sur GitHub serait écrasée à la synchronisation suivante.
C'est une assurance à un seul usage — si mes trois Pi brûlent, le code existe ailleurs — et c'est volontairement le seul cas où mon travail quitte la maison.
Savoir que ça tombe — avant que ça tombe
Une plateforme sans surveillance ne tombe pas moins souvent : elle tombe sans que personne l'apprenne. Prometheus scrute onze sources — l'apiserver, les nœuds, cAdvisor, les pods et les endpoints de service — et Alertmanager livre les alertes sur Telegram, parce qu'un tableau de bord ne réveille personne. Grafana sert à comprendre après coup ; l'alerte, elle, sert à être prévenu.
Onze règles sont définies. Cinq portent sur la plateforme elle-même :
| Règle | Ce qu'elle guette | Grâce | Gravité |
|---|---|---|---|
NoeudInjoignable | un nœud muet — tombé, ou noyé au point de ne plus répondre | 3 min | critique |
IngressLabIndisponible | plus aucun Traefik disponible : tout le domaine interne est HS | 3 min | critique |
NoeudPressionMemoire | moins de 500 Mo disponibles sur un nœud | 5 min | avertissement |
NoeudEnSurcharge | charge à 15 min au-dessus de 8 | 10 min | avertissement |
CertificatNonRenouvelé | moins de 4 h de validité restante sur un certificat | 15 min | critique |
Les six autres surveillent une chaîne de traitement métier, et n'ont pas leur place ici.
Ces règles ne sont pas un catalogue : ce sont des cicatrices
Les quatre premières datent du 24 juillet 2026, écrites le lendemain d'un incident : un build de CI sans limite avait épuisé la mémoire de pi1, charge à plus de 100, Traefik et l'apiserver affamés, tout le domaine interne injoignable — Gitea compris, alors qu'il tourne sur pi2 et se portait très bien. Le problème n'était pas seulement la panne. C'était que personne n'était prévenu : on la subissait jusqu'à s'en apercevoir.
Le choix qui compte dans cette liste est NoeudPressionMemoire. Elle ne détecte pas la panne,
elle détecte son précurseur — la mémoire qui se vide, cinq minutes avant que le noyau ne
se mette à tout paginer. Une alerte qui se déclenche pendant la coupure ne fait que confirmer
ce qu'on a déjà constaté. Une alerte utile se déclenche pendant qu'il reste quelque chose à
faire.
La cinquième règle vient du matin même, et pour une raison qui vaut d'être dite : les quatre premières n'auraient rien vu. Les nœuds étaient debout, Traefik répondait, aucune ressource n'était sous tension — et pourtant plus rien ne fonctionnait, parce qu'un certificat joker avait expiré après des heures de renouvellements échoués en silence. Un renouvellement qui échoue ne fait pas de bruit ; il ne fait que ne pas réussir. D'où le seuil : cert-manager renouvelle bien avant l'échéance, donc moins de quatre heures de validité restante signifie que plusieurs cycles ont déjà échoué.
C'est la leçon générale de cette section, et elle m'a coûté deux pannes pour être apprise : on n'écrit pas des alertes en imaginant les pannes, on les écrit après en avoir vu une, et la bonne question n'est jamais « qu'est-ce qui pourrait casser » mais « qu'est-ce qui, la dernière fois, ne m'a rien dit ».
Ce que l'exploitation m'apprend
Un laboratoire qui ne tombe jamais n'apprend rien. Le mien tombe, et c'est là qu'il devient autre chose qu'une maquette.
Coupure de courant, 13 avril 2026. Le pire n'a pas été la perte du courant : les disques ont survécu, les données aussi. Le piège était dans les métadonnées. Longhorn range les données de chaque volume dans un répertoire nommé d'après un identifiant de moteur ; en recréant ses objets au redémarrage, il a attribué de nouveaux identifiants, créé des répertoires vides à côté, et servi ces répertoires vides — pendant que les vraies données dormaient dans un répertoire devenu orphelin.
Le réflexe évident — renommer le répertoire orphelin pour lui donner le nouvel identifiant — est précisément celui qui détruit les données : la réconciliation peut choisir le réplica vide comme source et reconstruire par-dessus. La méthode sûre passe par un volume neuf, monté en maintenance, dans lequel on réinjecte l'image récupérée.
De cet incident je tire un ordre de redémarrage devenu procédure : restaurer Longhorn d'abord, desceller Vault ensuite, laisser l'opérateur de secrets se réauthentifier, et ne remonter les applications les plus dépendantes qu'en dernier. Chaque étape est une condition de la suivante. Cet ordre est aujourd'hui répété à froid, comme un exercice — une procédure de reprise qu'on n'a exécutée qu'une fois, sous la pression d'une panne réelle, n'est pas une procédure : c'est un souvenir.
Le même build, reparti en vrille — 15 août 2026. Trois semaines après l'incident qui avait motivé les alertes, la panne s'est reproduite : une génération de ce site a tenu 2 h 52 à environ 3 Go de mémoire résidente sur pi1 — 8 Go, sans swap. Le noyau s'est mis à tout paginer sans déclencher le tueur de mémoire : donc sans récupération automatique. L'ingress et l'apiserver affamés, tout le domaine interne injoignable — y compris Gitea, pourtant sain sur pi2.
Une même panne deux fois, c'est le signe que la première correction traitait le mauvais étage. Être prévenu d'un emballement, c'est bien ; il n'empêche pas l'emballement. La seconde fois appelait donc une réponse d'une autre nature : non plus détecter, mais borner.
Car la leçon n'est pas « il fallait plus de RAM ». Elle est que le défaut de 6 heures d'un runner de CI n'est pas une limite, c'est une absence de limite. Le plus long build réussi jamais mesuré ici fait 18 min 52 s, quand les emballements vont de 50 minutes à 3 heures : une borne à 30 minutes tranche net entre les deux. Les builds lourds ont par ailleurs été déplacés sur le MacBook, pour que pi1 — qui porte l'apiserver et l'ingress — ne soit plus jamais en concurrence avec un compilateur.
Trois pannes, trois réponses de nature différente — et c'est la distinction qui compte plus que les outils : détecter (les alertes de juillet), borner (la limite de trente minutes d'août), reprendre (l'ordre de redémarrage d'avril). Une seule des trois ne suffit jamais : une alerte n'empêche rien, une limite ne prévient personne, et une procédure de reprise n'existe que si on l'a répétée.
C'est, je crois, ce qu'un laboratoire domestique apporte de plus difficile à acquérir autrement : non pas la liste des outils, mais ce qu'ils font quand ils échouent, et le réflexe d'écrire la procédure pendant qu'on s'en souvient.