Files
tools/alloy/values.yaml
T
arcodangeandClaude Fable 5.1 c4720a1dfd
Helm Charts / Detect changed charts (pull_request) Successful in 41s
Helm Charts / Library charts tool (pull_request) Skipped
Helm Charts / Application charts crowdsec (pull_request) Skipped
Helm Charts / Application charts pgcat (pull_request) Skipped
Helm Charts / Application charts prometheus (pull_request) Skipped
Helm Charts / Application charts redis (pull_request) Skipped
Helm Charts / Application charts alloy (pull_request) Successful in 14s
Helm Charts / Application charts chart (pull_request) Successful in 36s
Helm Charts / Application charts loki (pull_request) Successful in 51s
Helm Charts / Application charts grafana (pull_request) Successful in 56s
Helm Charts / Application charts hashicorp-vault (pull_request) Skipped
Helm Charts / Application charts minio (pull_request) Skipped
Helm Charts / Application charts pgbouncer (pull_request) Skipped
Ces nœuds tournent sous Docker, et leur racine est ailleurs — la collecte lisait le vide
Trois faits mesurés au déploiement, qu'aucune lecture du chart n'aurait donnés.

1. LE CLUSTER TOURNE SOUS DOCKER, PAS SOUS CONTAINERD.
   `kubectl get nodes -o …containerRuntimeVersion` :
     pi1  docker://29.1.3
     pi2  docker://28.4.0
     pi3  docker://27.4.0
   Le commentaire du premier jet affirmait le contraire. Il était faux.

2. LES JOURNAUX NE SONT PAS SOUS /var/log — CE SONT DES LIENS.
     /var/log/pods/<ns>_<pod>_<uid>/<conteneur>/0.log
       -> /mnt/arcodange/docker/containers/<id>/<id>-json.log
   La racine Docker est déplacée sur un disque externe. Monter /var/log donne
   les liens et PAS leurs cibles : `ls` les montre, `stat` échoue dessus. Le
   drapeau `dockercontainers` du chart monterait /var/lib/docker/containers,
   qui n'existe pas ici. On monte la vraie racine, en lecture seule.
   Chemin vérifié identique sur pi1 et pi3, pod par pod.

3. LE FORMAT EST DU JSON DOCKER, DONC `stage.docker`, PAS `stage.cri`.
   Avec le mauvais décodeur, l'horodatage stocké serait celui de la LECTURE et
   non celui de l'écriture — or dater une panne à la minute est précisément
   l'usage qui motive ce lot (tools#36).

⚠⚠ CE QUI REND CE DÉFAUT DANGEREUX : IL SE PRÉSENTE COMME UNE SANTÉ PARFAITE.
Les six composants d'Alloy se déclaraient `healthy`, aucune erreur de poussée,
Loki répondait `ready`, les trois agents étaient 2/2 Running. La seule chose
qui parlait était `loki_source_file_files_active_total = 0` et l'export vide de
`local.file_match`. Une pile de journaux qui rassure sans rien garder : le
défaut même que l'issue #38 demande de ne pas reproduire.

Le hostPath est en `type: Directory` et non `DirectoryOrCreate`, exprès — si un
nœud n'a pas ce chemin, son agent doit refuser de démarrer et le DIRE, plutôt
que de monter un dossier vide et de rapporter zéro en se déclarant sain.

Corrige aussi le second conteneur non plafonné, jumeau de celui de Loki :
`config-reloader` arrive avec une REQUÊTE de 50Mi et AUCUNE limite. Une requête
sans limite n'est pas un plafond, c'est une réservation — et sur un DaemonSet
cela vaut sur les trois nœuds. On ne peut pas l'éteindre sans perdre le
rechargement à chaud : on le plafonne (32Mi / 64Mi).

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

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-09-07 17:29:24 +02:00

250 lines
11 KiB
YAML

# -----------------------------------------------------------------------------
# Grafana Alloy — l'agent de collecte, un pod par nœud.
# -----------------------------------------------------------------------------
# POURQUOI ALLOY ET PAS PROMTAIL (l'arbitrage de tools#38, mesuré le 2026-09-07)
#
# Relevé sur l'index du dépôt Helm de Grafana lui-même
# (`curl https://grafana.github.io/helm-charts/index.yaml`) :
#
# chart versions dernière publication `deprecated:` dans l'index
# promtail 141 2025-10-31 true
# alloy 57 2026-08-27 (absent)
# loki 344 2026-08-10 (absent)
#
# Promtail n'est pas « en fin de vie bientôt » : son chart porte le drapeau
# `deprecated: true` dans l'index, et sa dernière publication a plus de dix mois.
# Déployer AUJOURD'HUI, sur une installation NEUVE, un agent que l'amont a déjà
# rangé, c'est se donner une dette à échéance connue pour économiser de la
# mémoire — et l'économie, elle, est à vérifier, pas à supposer.
#
# Le coût réel d'Alloy sur CE matériel est mesuré après déploiement et reporté
# dans la PR ; c'est ce que le plafond `resources` ci-dessous encadre.
#
# ARM64 : vérifié au registre, manifeste multi-plateforme (jeton anonyme
# Docker Hub) — `grafana/alloy:v1.19.2` publie linux/amd64, **linux/arm64**,
# ppc64le, s390x. `grafana/loki:3.6.12` publie amd64, **arm64**, arm/v7.
# -----------------------------------------------------------------------------
alloy:
controller:
# C'est déjà le défaut du chart ; on l'écrit parce que c'est LA décision
# structurante — un agent par nœud, donc trois fois le coût mémoire.
type: daemonset
# Le volume que consomme le montage `alloy.mounts.extra` ci-dessous : la
# racine Docker de ces machines, où aboutissent les liens de /var/log/pods.
volumes:
extra:
- name: docker-containers
hostPath:
path: /mnt/arcodange/docker/containers
# `Directory` (et non `DirectoryOrCreate`) EXPRÈS : si un jour un
# nœud n'a pas ce chemin, on veut que son agent refuse de démarrer
# et le DISE, plutôt que de monter un dossier vide et de rapporter
# zéro journal en se déclarant sain — le défaut exact qu'on vient
# de traverser.
type: Directory
alloy:
# -- Le montage de /var/log : c'est par là que l'agent lit les journaux.
# On tail des FICHIERS plutôt que d'interroger l'API du kubelet : sur trois
# Pi, faire transiter tous les journaux par l'API du cluster ajouterait au
# serveur d'API une charge qu'il n'a pas à porter.
mounts:
varlog: true
# ⚠⚠ CE CLUSTER TOURNE SOUS DOCKER, PAS SOUS CONTAINERD — et son magasin
# d'images n'est pas là où le chart le cherche. Les deux faits ensemble
# rendent ce drapeau inutilisable.
#
# Mesuré le 2026-09-07 (`kubectl get nodes -o …containerRuntimeVersion`) :
# pi1 docker://29.1.3
# pi2 docker://28.4.0
# pi3 docker://27.4.0
#
# Et sous /var/log/pods, les journaux ne sont PAS des fichiers : ce sont
# des liens symboliques vers la racine Docker, DÉPLACÉE sur un disque
# externe (identique sur pi1 et pi3, vérifié pod par pod) :
#
# /var/log/pods/<ns>_<pod>_<uid>/<conteneur>/0.log
# -> /mnt/arcodange/docker/containers/<id>/<id>-json.log
#
# `dockercontainers: true` monterait `/var/lib/docker/containers`, qui
# n'existe pas ici. On monte donc la vraie racine, plus bas, en `extra`.
dockercontainers: false
# ⚠ SANS CE MONTAGE, LA COLLECTE EST SILENCIEUSEMENT VIDE — et c'est le
# mode d'échec le plus traître de ce lot, parce que TOUT A L'AIR SAIN :
# les six composants d'Alloy se déclarent `healthy`, aucune erreur de
# poussée, Loki répond `ready`. Le seul relevé qui le dit est
# `loki_source_file_files_active_total = 0`, et l'export vide de
# `local.file_match`. `ls` montre les liens, `stat` échoue sur la cible.
extra:
- name: docker-containers
mountPath: /mnt/arcodange/docker/containers
readOnly: true
# ⚠ LA MÉMOIRE EST LA RESSOURCE RARE, ET UN DAEMONSET LA PREND TROIS FOIS.
# Mesuré le 2026-09-07 avant tout déploiement : pi1 79 %, pi2 109 %,
# pi3 58 % de leur mémoire ALLOUABLE. pi2 est en surengagement — c'est
# précisément le nœud où un agent de trop bascule la machine.
#
# Le plafond n'est donc pas une formalité : il est ce qui garantit que
# l'agent meurt AVANT le service qu'il observe. Un OOM d'Alloy est un
# incident de collecte ; un OOM de MinIO ou de pgbouncer est un incident
# tout court.
resources:
requests:
cpu: 50m
memory: 128Mi
limits:
# Pas de limite CPU (l'étranglement d'un agent lui fait perdre des
# lignes en silence, ce qui est le défaut qu'on essaie de supprimer).
memory: 256Mi
# Pas de grappe : chaque agent lit SON nœud, il n'a rien à coordonner.
clustering:
enabled: false
configMap:
create: true
# -----------------------------------------------------------------------
# La configuration Alloy. Trois étages : découvrir, lire, pousser.
# -----------------------------------------------------------------------
content: |-
// ÉTAGE 1 — découvrir les pods de CE nœud, et de lui seul.
//
// ⚠ Sans le filtre par nœud, chacun des trois agents découvrirait les
// pods des TROIS nœuds, chercherait leurs fichiers en local, n'en
// trouverait aucun pour les deux autres, et referait ce travail en
// boucle. Le filtre se pose côté serveur d'API (`field`), pas en
// relabel : ce qui n'est pas envoyé ne coûte rien.
discovery.kubernetes "pods_du_noeud" {
role = "pod"
selectors {
role = "pod"
field = "spec.nodeName=" + sys.env("HOSTNAME")
}
}
// ÉTAGE 2 — traduire les métadonnées Kubernetes en étiquettes Loki,
// et fabriquer le chemin du fichier journal.
//
// ⚠ LES ÉTIQUETTES SONT LE SEUL INDEX DE LOKI, ET CHAQUE COMBINAISON
// DISTINCTE EST UN FLUX. On garde ce par quoi on cherche vraiment
// (namespace, pod, conteneur, nœud) et RIEN de ce qui varie sans
// qu'on l'interroge : mettre ici une étiquette à forte cardinalité
// (un identifiant de requête, un horodatage) est la façon classique
// de faire tomber un Loki. Le reste du contenu reste cherchable en
// plein texte, simplement non indexé.
discovery.relabel "journaux_de_pods" {
targets = discovery.kubernetes.pods_du_noeud.targets
rule {
source_labels = ["__meta_kubernetes_namespace"]
target_label = "namespace"
}
rule {
source_labels = ["__meta_kubernetes_pod_name"]
target_label = "pod"
}
rule {
source_labels = ["__meta_kubernetes_pod_container_name"]
target_label = "container"
}
rule {
source_labels = ["__meta_kubernetes_node_name"]
target_label = "node"
}
rule {
source_labels = ["__meta_kubernetes_pod_label_app_kubernetes_io_name"]
target_label = "app"
}
// Le chemin réel posé par le kubelet :
// /var/log/pods/<ns>_<pod>_<uid>/<conteneur>/0.log
// On vise par l'UID (unique) et le nom du conteneur.
rule {
source_labels = ["__meta_kubernetes_pod_uid", "__meta_kubernetes_pod_container_name"]
separator = "/"
action = "replace"
replacement = "/var/log/pods/*$1/*.log"
target_label = "__path__"
}
}
// ÉTAGE 3 — lire les fichiers, décoder le format CRI, pousser.
local.file_match "journaux_de_pods" {
path_targets = discovery.relabel.journaux_de_pods.output
}
loki.source.file "journaux_de_pods" {
targets = local.file_match.journaux_de_pods.targets
forward_to = [loki.process.journaux_de_pods.receiver]
}
// ⚠⚠ `stage.docker`, PAS `stage.cri` — ces nœuds tournent sous Docker
// (mesuré : docker://29.1.3, 28.4.0, 27.4.0), donc chaque ligne est un
// objet JSON {"log":…,"stream":…,"time":…} et non le format CRI
// « <horodatage> <flux> <F|P> <message> ».
//
// Ce n'est pas cosmétique : sans le bon décodeur, l'horodatage stocké
// serait celui de la LECTURE et non celui de l'écriture, et les lignes
// arriveraient encore encapsulées dans leur JSON. Or dater une panne à
// la minute est PRÉCISÉMENT l'usage qui motive ce lot (tools#36).
loki.process "journaux_de_pods" {
stage.docker {}
forward_to = [loki.write.loki.receiver]
}
loki.write "loki" {
endpoint {
url = "http://loki.tools.svc.cluster.local:3100/loki/api/v1/push"
}
}
# L'agent a besoin de LIRE les pods du serveur d'API pour les découvrir.
# Le chart pose un ClusterRole en lecture seule (pods, nodes, services,
# endpoints) — rien qui écrive.
rbac:
create: true
serviceAccount:
create: true
# Pas de collecte de métriques d'Alloy par Prometheus pour l'instant : le
# ServiceMonitor suppose l'opérateur Prometheus, que ce cluster n'a pas
# (le chart `prometheus` d'ici est le chart communautaire, pas l'opérateur).
serviceMonitor:
enabled: false
ingress:
enabled: false
# ⚠⚠ LE MÊME PIÈGE QUE LE SIDE-CAR DE LOKI, DEUXIÈME OCCURRENCE — et celle-ci
# est plus sournoise, parce qu'elle RESSEMBLE à un plafond.
#
# Le chart ajoute au pod un second conteneur, `config-reloader`, qui recharge
# Alloy quand sa ConfigMap change. Mesuré sur le premier déploiement, ses
# ressources amont sont :
#
# resources:
# requests: { cpu: 10m, memory: 50Mi }
# ← et RIEN d'autre
#
# Une requête SANS limite n'est pas un plafond : c'est une réservation. Le
# conteneur peut croître jusqu'à épuiser le nœud, et comme il partage le
# cgroup du pod, il emporte Alloy avec lui. Sur un DaemonSet, cela vaut sur
# les TROIS nœuds — pi2 compris, mesuré à 108 % de son allouable.
#
# ⚠ On ne peut pas l'éteindre : sans lui, un changement de configuration
# n'atteindrait l'agent qu'au prochain redémarrage du pod. On le PLAFONNE.
configReloader:
enabled: true
resources:
requests:
cpu: 10m
memory: 32Mi
limits:
memory: 64Mi