Ce qu'un pod a dit lui survit — Loki et Alloy, sous plafond (#38)
Helm Charts / Detect changed charts (pull_request) Successful in 3m18s
Helm Charts / Library charts tool (pull_request) Skipped
Helm Charts / Application charts crowdsec (pull_request) Skipped
Helm Charts / Application charts hashicorp-vault (pull_request) Skipped
Helm Charts / Application charts minio (pull_request) Skipped
Helm Charts / Application charts prometheus (pull_request) Skipped
Helm Charts / Application charts pgbouncer (pull_request) Skipped
Helm Charts / Application charts pgcat (pull_request) Skipped
Helm Charts / Application charts redis (pull_request) Skipped
Helm Charts / Application charts chart (pull_request) Successful in 29s
Helm Charts / Application charts alloy (pull_request) Successful in 59s
Helm Charts / Application charts grafana (pull_request) Successful in 1m1s
Helm Charts / Application charts loki (pull_request) Successful in 48s

Le cluster n'avait AUCUNE collecte de journaux : la seule lecture possible
était `kubectl logs`, c'est-à-dire le tampon du kubelet, qui tourne. Mesuré
le 2026-09-07 en demandant à chaque journal jusqu'où il remonte :

  kube-system/traefik   pod démarré il y a 12 j — journal remontant à 10 MIN
  tools/clickhouse-0    pod démarré il y a  8 j — journal remontant à  2 MIN

Les deux composants les plus bavards du cluster gardent entre deux et dix
minutes d'histoire. C'est pire que ce que l'issue supposait.

Ce lot pose Loki (binaire unique, stockage fichier sur Longhorn) et Alloy
(l'agent, un pod par nœud), et branche la source de données au Grafana qui
tourne déjà.

L'AGENT : Alloy, pas Promtail — et c'est mesuré, pas une préférence. L'index
Helm de Grafana marque le chart `promtail` `deprecated: true`, dernière
publication 2025-10-31. Alloy : 2026-08-27. On n'installe pas à neuf un agent
que l'amont a déjà rangé.

LA RÉTENTION : 30 jours, sur un débit mesuré de ~305 Mio/jour bruts pour tout
le cluster, soit 0,9 à 1,8 Gio compressés. 7 jours auraient perdu le début des
deux pannes qui motivent ce lot (kadans#1033, des SEMAINES ; tools#36, 3 jours).

LES PLAFONDS NE SONT PAS OPTIONNELS : pi2 est mesuré à 108 % de sa mémoire
allouable. Les défauts amont du chart Loki y auraient posé un memcached
réclamant 8 Gio (`chunksCache.allocatedMemory: 8192`), un second memcached, un
nginx, un DaemonSet canari et trois jeux de répliques. Chaque extinction est
nommée dans `loki/values.yaml`, avec sa raison.

⚠ La CI de ce dépôt monte une matrice CODÉE EN DUR : un chart absent de la
liste n'est jamais construit et sa PR est verte quand même (tools#32). `loki`
et `alloy` y sont ajoutés — sans ça, ce lot n'aurait aucune preuve.

Deux découvertes faites en déployant, pas en lisant :
  - `admin_api_directory` est accepté par le chart mais REFUSÉ par le binaire
    OSS (champ d'édition entreprise) : CrashLoopBackOff jusqu'à ce qu'on l'ôte ;
  - le chart ajoute un side-car `loki-sc-rules` SANS aucune limite de
    ressources — un conteneur non plafonné dans un pod plafonné annule le
    plafond. Éteint (le ruler est à 0 réplique).

Closes #38

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
2026-09-07 17:19:20 +02:00
co-authored by Claude Fable 5.1
parent dc48d48c1b
commit b66e79aadc
9 changed files with 529 additions and 2 deletions
+23
View File
@@ -0,0 +1,23 @@
# Patterns to ignore when building packages.
# This supports shell glob matching, relative path matching, and
# negation (prefixed with !). Only one pattern per line.
.DS_Store
# Common VCS dirs
.git/
.gitignore
.bzr/
.bzrignore
.hg/
.hgignore
.svn/
# Common backup files
*.swp
*.bak
*.tmp
*.orig
*~
# Various IDEs
.project
.idea/
*.tmproj
.vscode/
+35
View File
@@ -0,0 +1,35 @@
apiVersion: v2
name: loki
description: >-
Le garde-mémoire des journaux du cluster — Loki en binaire unique, stockage
fichier sur Longhorn. Ce qu'un pod a dit continue de se lire après sa mort.
# ⚠ PAS de dépendance au registre Helm interne, CONTRAIREMENT à grafana et
# prometheus — et c'est délibéré.
#
# La bibliothèque `tool` ne sert QUE via les gabarits `templates/helm-chart*.yaml`,
# eux-mêmes gardés par `{{- if eq .Values.tool.kind "HelmChart" -}}`. Avec
# `kind: SubChart` — le mode de grafana, de prometheus, et le nôtre — ces
# gabarits ne rendent RIEN : la bibliothèque est un poids mort.
#
# Or ce poids mort a un coût mesuré : le runner de CI ne résout pas
# `gitea.arcodange.lab` (relevé du 2026-09-07, run 6728, `curl` code 6). Un
# repli sur `GITHUB_SERVER_URL` a été posé dans `helmcharts.yaml`, mais il reste
# un point de panne EN AMONT du `helm template`. Si la dépendance ne se
# télécharge pas, l'étape qui éprouve VRAIMENT ce chart ne s'exécute jamais et
# le job meurt sur une cause étrangère au chart.
#
# On ne dépend donc que de `grafana.github.io`, dont le même relevé a constaté
# qu'il répond depuis ce runner. Le jour où l'on veut basculer en
# `kind: HelmChart` (CRD k3s), on rajoute la dépendance ET les deux gabarits.
dependencies:
- name: loki
version: 7.3.0
repository: https://grafana.github.io/helm-charts
type: application
version: 0.1.0
# ⚠ 3.6.11, PAS 3.6.12. L'index Helm annonce `appVersion: 3.6.12` pour le chart
# 7.3.0, mais le chart épingle `loki.image.tag: 3.6.11` dans ses valeurs — et
# c'est le tag qui est tiré. Vérifié sur le pod déployé.
appVersion: "3.6.11"
+234
View File
@@ -0,0 +1,234 @@
# -----------------------------------------------------------------------------
# Loki — binaire unique, stockage fichier, sur trois Raspberry Pi.
# -----------------------------------------------------------------------------
# ⚠⚠ LES DÉFAUTS AMONT SONT ÉCRITS POUR UN CLUSTER DE SERVEURS, PAS POUR CECI.
# Mesurés sur le chart 7.3.0 (`helm show values grafana/loki --version 7.3.0`) :
#
# deploymentMode SimpleScalable → read + write + backend, 3 jeux
# chunksCache.enabled true → un memcached à …
# chunksCache.allocatedMemory 8192 → … 8 Gio, sur un nœud qui en
# alloue 5,6 (pi2)
# resultsCache.enabled true → un second memcached
# gateway.enabled true → un nginx de plus
# lokiCanary.enabled true → un DaemonSet de plus
# test.enabled true → un pod de test
# loki.commonConfig.replication_factor 3 → 3 exemplaires de chaque flux
# loki.storage.type s3 → un magasin objet qu'on n'a pas
# loki.auth_enabled true → multi-locataire (en-tête
# X-Scope-OrgID exigé à chaque appel)
#
# Laisser ces défauts, c'est exactement la deuxième propriété de tools#38
# violée : « la collecte ne doit pas faire tomber ce qu'elle observe ». Chaque
# `false` ci-dessous est donc une décision, pas une omission.
# -----------------------------------------------------------------------------
loki:
# -- Un seul processus, une seule réplique. Le reste des modes est éteint plus bas.
deploymentMode: SingleBinary
loki:
# Locataire unique : personne d'autre que le homelab n'écrit ici. Laisser
# `true` obligerait chaque requête (et chaque source de données Grafana) à
# porter un en-tête X-Scope-OrgID, pour aucun bénéfice.
auth_enabled: false
commonConfig:
path_prefix: /var/loki
# -- UNE réplique de chaque flux : il n'y a qu'un ingesteur.
# Le défaut amont (3) ferait refuser toute écriture, faute de quorum.
replication_factor: 1
storage:
type: filesystem
filesystem:
chunks_directory: /var/loki/chunks
rules_directory: /var/loki/rules
# ⚠ PAS de `admin_api_directory` ici. Le chart l'accepte (il est dans
# ses valeurs par défaut), mais le binaire OSS le REFUSE :
# failed parsing config: field admin_api_directory not found in
# type common.FilesystemConfig
# C'est un champ de l'édition entreprise (GEL). Mesuré : le pod part en
# CrashLoopBackOff, 3 redémarrages en 4 min. Le chart ne valide pas la
# config qu'il écrit — seul le démarrage réel le dit.
# ⚠ Le chart n'écrit AUCUN schéma par défaut (`schemaConfig: {}`) et refuse
# de démarrer sans. tsdb + v13 est le couple courant de Loki 3.x.
schemaConfig:
configs:
- from: "2026-09-07"
store: tsdb
object_store: filesystem
schema: v13
index:
prefix: loki_index_
period: 24h
# ⚠⚠ UNE RÉTENTION QUI NE SUPPRIME RIEN EST UNE RÉTENTION QUI MENT.
# `limits_config.retention_period` seul ne fait RIEN : c'est le compacteur
# qui efface, et il ne le fait que si `retention_enabled` est vrai. Le
# couple est indissociable — l'un sans l'autre laisse le volume grossir
# jusqu'à saturation, en silence.
compactor:
working_directory: /var/loki/compactor
retention_enabled: true
delete_request_store: filesystem
limits_config:
# -- 30 jours. Voir `RETENTION` en fin de fichier pour la mesure.
retention_period: 720h
# Le défaut amont refuse tout échantillon de plus de 168 h. À la première
# remontée d'un agent resté hors ligne, ces lignes seraient jetées.
reject_old_samples: true
reject_old_samples_max_age: 720h
max_cache_freshness_per_query: 10m
split_queries_by_interval: 15m
query_timeout: 300s
volume_enabled: true
# Débit mesuré du cluster : ~305 Mio/jour TOUS pods confondus, soit
# ~3,6 Kio/s. Les bornes amont (4 Mio/s par flux) sont hors sujet ici ;
# on les laisse, elles ne mordent pas.
allow_structured_metadata: true
# Pas de vitrine : personne d'extérieur n'interroge ce Loki.
analytics:
reporting_enabled: false
singleBinary:
replicas: 1
persistence:
enabled: true
# ⚠ DEUX StorageClass portent le drapeau `(default)` sur ce cluster
# (`local-path` ET `longhorn`, mesuré le 2026-09-07). Un PVC sans classe
# explicite est donc un tirage au sort — et `local-path` ne survit pas au
# déplacement du pod, ce qui viderait la mémoire qu'on vient de bâtir.
# On NOMME la classe.
storageClass: longhorn
# 30 j × 305 Mio/j = 8,9 Gio BRUTS ; Loki compresse (facteur 5 à 10 sur
# du journal), donc 0,9 à 1,8 Gio attendus. 10 Gi laisse 5 à 10× de marge
# pour la croissance de kadans, sans immobiliser le disque.
size: 10Gi
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
# Pas de limite CPU : l'étranglement d'un compacteur est pire que sa
# lenteur. La mémoire, elle, est PLAFONNÉE — c'est la ressource rare
# (pi2 mesuré à 109 % de son allouable le 2026-09-07).
memory: 512Mi
# ---------------------------------------------------------------------------
# Tout ce qui suit existe pour un déploiement distribué. Ici, chacun de ces
# blocs est du poids mort sur des Raspberry Pi — on les éteint NOMMÉMENT
# plutôt que par un `deploymentMode` qui « devrait » suffire.
# ---------------------------------------------------------------------------
write: { replicas: 0 }
read: { replicas: 0 }
backend: { replicas: 0 }
ingester: { replicas: 0 }
distributor: { replicas: 0 }
querier: { replicas: 0 }
queryFrontend: { replicas: 0 }
queryScheduler: { replicas: 0 }
indexGateway: { replicas: 0 }
compactor: { replicas: 0 }
bloomPlanner: { replicas: 0 }
bloomBuilder: { replicas: 0 }
bloomGateway: { replicas: 0 }
patternIngester: { replicas: 0 }
ruler: { replicas: 0 }
overridesExporter: { replicas: 0 }
# Les deux memcached. Le premier réclame 8 Gio à lui seul par défaut.
chunksCache:
enabled: false
resultsCache:
enabled: false
# Un nginx devant un service que seul Grafana interroge, depuis le cluster.
gateway:
enabled: false
# Un DaemonSet qui écrit des journaux pour vérifier qu'on lit les journaux :
# trois pods de plus sur trois nœuds, pour une propriété que le sabotage de
# la PR démontre mieux (écrire une ligne, tuer le pod, la relire).
lokiCanary:
enabled: false
test:
enabled: false
# Le chart sait déployer SON PROPRE MinIO. On en a déjà un, et on n'en veut
# pas ici : le stockage est le disque.
minio:
enabled: false
monitoring:
dashboards:
enabled: false
rules:
enabled: false
serviceMonitor:
enabled: false
metricsInstance:
enabled: false
selfMonitoring:
enabled: false
grafanaAgent:
installOperator: false
# Pas d'opérateur de déploiement progressif pour une réplique unique.
rollout_operator:
enabled: false
# ⚠ UN SECOND CONTENEUR SANS PLAFOND ANNULE LE PLAFOND DU PREMIER.
# Le chart ajoute au pod un side-car `loki-sc-rules` (kiwigrid/k8s-sidecar)
# qui surveille des ConfigMaps de règles d'alerte. Mesuré sur le premier
# déploiement : il arrive avec `resources: {}` — ni requête ni limite — dans
# un pod posé sur pi2, le nœud à 108 % de son allouable. On l'éteint : le
# `ruler` est à 0 réplique, il n'y a aucune règle à ingérer, donc ce side-car
# surveille des ConfigMaps qui n'existent pas.
sidecar:
rules:
enabled: false
# -----------------------------------------------------------------------------
# RETENTION — l'arbitrage, et la mesure qui le porte
# -----------------------------------------------------------------------------
# Mesuré le 2026-09-07 sur ce cluster, par l'API (donc ce que Loki ingérerait) :
#
# fenêtre de 10 min, tous pods : 2 182 015 octets
# fenêtre de 1 h, tous pods : 7 897 505 octets
#
# Les deux instruments ne s'accordent pas (l'heure ne vaut que 0,60 fois six
# fois les dix minutes) — et le désaccord est LE résultat. Par conteneur, le
# rapport 1 h / 10 min vaut ~6 partout (débit stable) SAUF :
#
# kube-system/traefik ratio 1,18 → la fenêtre d'une heure est TRONQUÉE
# tools/clickhouse-0 ratio 3,98 → tronquée aussi
#
# Vérifié directement, en demandant à chaque journal jusqu'où il remonte :
#
# traefik pod démarré il y a 12 j — journal remontant à 10 MINUTES
# clickhouse pod démarré il y a 8 j — journal remontant à 2 MINUTES
#
# Autrement dit, les deux composants les plus bavards du cluster gardent entre
# deux et dix minutes d'histoire. C'est l'issue tools#38 en un chiffre.
#
# Estimation corrigée (max(1 h, 10 min × 6) par conteneur, pour ne pas hériter
# de la troncature) : 13 333 645 octets/h = 305 Mio/jour bruts.
#
# 30 j bruts 8,94 Gio
# 30 j compressés (facteur 5) 1,79 Gio
# 30 j compressés (facteur 10) 0,89 Gio
#
# POURQUOI 30 JOURS et pas 7 : les deux pannes qui motivent ce lot ont duré des
# SEMAINES avant d'être vues (la mise à l'abri de kadans#1033) ou trois jours
# (tools#36). Une rétention de 7 jours aurait perdu le début des deux. 30 jours
# couvre le délai réel entre « ça casse » et « quelqu'un s'en aperçoit », et
# coûte 1 à 2 Gio — sur 99 Gio libres au plus serré des trois nœuds Longhorn.
#
# ⚠ CE QUI RENDRAIT CE CHIFFRE FAUX : la mesure est une photo d'un mardi
# après-midi. Elle ne dit rien d'un pod qui part en boucle d'erreur, cas où le
# débit d'un seul conteneur peut dépasser à lui seul tout le reste du cluster.
# La marge de 5 à 10× est là pour ça, pas pour la croissance ordinaire.
# -----------------------------------------------------------------------------