Observabilité & Infrastructure as Code
OpenTofu pour piloter le cluster, VictoriaMetrics + Loki + OpenTelemetry + Grafana pour le superviser
Motivation
Après deux ans, le cluster tournait mais se pilotait à la main : des helm upgrade et des kubectl apply documentés dans des README, aucune vue sur ce qui se passait dedans, et des certificats auto-signés que les applications mobiles refusaient. L’objectif de cette étape : se rapprocher d’un environnement de production, avec de l’infrastructure as code, de la supervision et une vraie chaîne TLS, sans dépasser la RAM de quatre Raspberry Pi.
OpenTofu : le cluster décrit en code
OpenTofu est le fork communautaire de Terraform. Le répertoire tofu/ décrit les namespaces et les releases Helm du cluster avec les providers kubernetes et helm.
Adopter l’existant sans le recréer
Le point délicat : les services tournaient déjà. Plutôt que de tout redéployer, les ressources existantes (namespaces, releases Gitea, Immich, provisioner NFS) ont été importées dans l’état via des blocs import. Le premier tofu apply a lancé un helm upgrade à valeurs identiques : la révision Helm s’incrémente, aucun pod ne redémarre.
Depuis, tofu plan détecte la dérive : une release modifiée à la main apparaît dans le diff.
Des couches derrière des flags
Les nouvelles couches sont activées une par une avec des variables : deploy_monitoring, deploy_authentik, deploy_teleport. Chaque phase est indépendante et réversible, et le plan de déploiement liste le budget RAM de chacune (environ 2,5 Gio pour le monitoring, 2,3 Gio pour Authentik).
Certains éléments restent volontairement hors OpenTofu : Traefik, géré par K3s lui-même (deux propriétaires se battraient), et NewFlix, déployé par la CI/CD Gitea.
Aucun secret ni IP dans le dépôt
Les fichiers de values contiennent des placeholders (${INTERNAL_DOMAIN}, ${NODE_IP}, ${NFS_SERVER_IP}). Un fichier render.tf les substitue depuis un terraform.tfvars ignoré par git, et le même mécanisme sert au workflow manuel via un script render.sh. L’état OpenTofu est local et ignoré aussi, car il contient les secrets rendus. Un hook pre-commit bloque toute fuite.
Observabilité : métriques, logs, dashboards
Nœuds / Pods
├── node-exporter (DaemonSet) → métriques système (CPU, RAM, disque, température)
├── kube-state-metrics → état des objets K8s (pods, PVC…)
└── OpenTelemetry Collector → collecte des logs de tous les pods
├──▶ vmagent ──▶ VMSingle → stockage métriques (NFS 8 Gio, 3 mois)
└──▶ Loki (endpoint OTLP) → stockage logs (NFS 20 Gio, 31 jours)
└──▶ Grafana → dashboards et exploration

Le dashboard global du cluster dans Grafana, source VictoriaMetrics : 4 nœuds, 39 pods, 38 % de RAM utilisée sur 15 Gio.
VictoriaMetrics plutôt que Prometheus
Même langage de requête (PromQL), mêmes formats de scrape, mais nettement moins gourmand en RAM et en disque : sur des Raspberry Pi, c’est ce qui fait la différence. Le chart victoria-metrics-k8s-stack apporte VMSingle, vmagent, node-exporter, kube-state-metrics et Grafana. Tout Service annoté prometheus.io/scrape est découvert automatiquement ; Gitea expose déjà ses métriques de cette façon.
Loki en single-binary
Loki n’indexe que les labels (namespace, pod, conteneur), pas le contenu des lignes, ce qui le rend très léger. Ici : un seul pod, stockage fichier sur NFS, caches memcached désactivés parce qu’ils demandaient plus de RAM qu’un Pi entier.
OpenTelemetry Collector à la place de Promtail
Promtail est déprécié (fin de vie mars 2026). Le Collector OpenTelemetry fait le même travail, un DaemonSet qui lit /var/log/pods sur chaque nœud, avec deux avantages : c’est un standard neutre, et une application instrumentée avec un SDK OTel pourra lui envoyer logs et traces en OTLP sans toucher à l’infrastructure. Le preset kubernetesAttributes enrichit chaque ligne avec les métadonnées Kubernetes, et l’export se fait sur l’endpoint OTLP natif de Loki.
Les pièges rencontrés
no locks available : VMSingle en CrashLoopBackOff
Le partage NFS est monté en NFSv3 sans gestionnaire de verrous, donc flock() échoue. VictoriaMetrics prend un verrou sur un fichier flock.lock et panique quand il ne l’obtient pas ; Grafana se bloque sur sa base SQLite pour la même raison. Solution : une seconde StorageClass nfs-cluster-nolock avec l’option de montage nolock, créée par OpenTofu. Le client NFS gère alors les verrous localement, ce qui est sûr ici puisque ces volumes sont ReadWriteOnce, un seul pod écrit dedans. Loki n’a pas besoin de verrous et reste sur la classe standard.
Bootstrap avec -target
Grafana lit ses identifiants admin dans un Secret qui doit exister avant le démarrage du pod, et le Secret ne va pas dans le dépôt. La séquence : créer le namespace seul avec tofu apply -target, créer le Secret à la main, puis l’apply complet. -target reste réservé à ce genre d’amorçage.
Namespace bloqué en Terminating
En désactivant la couche monitoring, l’opérateur VictoriaMetrics est supprimé avant ses ressources personnalisées : plus personne ne traite leurs finalizers et le namespace ne se supprime jamais. Il faut supprimer les VMSingle, VMAgent et consorts avant de passer le flag à false.
Les autres couches
- cert-manager + Let’s Encrypt : certificats wildcard obtenus par challenge DNS-01 via l’API Cloudflare, renouvelés automatiquement. Fini les avertissements navigateur et les clients mobiles qui refusent l’auto-signé.
- Authentik : fournisseur d’identité OIDC auto-hébergé, auquel Grafana, Gitea et Immich délèguent l’authentification.
- Teleport (prévu, pas encore déployé) : bastion d’accès avec certificats courts, audit et enregistrement des sessions SSH et kubectl. La couche est écrite derrière son flag, il reste à l’activer.
- WAF Coraza + OWASP CRS : plugin Traefik, optionnel et non câblé par défaut. Sur un cluster non exposé à internet, sa valeur est surtout pédagogique, et la doc le dit.
Résultat
Le cluster est décrit en code, la dérive est visible, et chaque nouvelle brique s’active par un flag avec un budget mémoire connu. Grafana montre la température de chaque Pi, les pods qui redémarrent en boucle et les logs de n’importe quel namespace en une requête. Le tout documenté dans le dépôt, primers technologiques compris, pour pouvoir le reconstruire de zéro.