Helm Charts / Detect changed charts (pull_request) Successful in 24s
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 pgbouncer (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 chart (pull_request) Successful in 29s
Helm Charts / Application charts loki (pull_request) Successful in 47s
Helm Charts / Application charts grafana (pull_request) Successful in 50s
Helm Charts / Application charts alloy (pull_request) Successful in 45s
L'étiquette `node` était absente de Loki sur une collecte par ailleurs saine : `__meta_kubernetes_node_name` n'existe QUE pour `role = "node"`. Sous `role = "pod"`, elle est vide — et un relabel dont la source est vide ne pose pas l'étiquette, SANS erreur ni avertissement. On ne s'en aperçoit qu'en DEMANDANT les valeurs de l'étiquette (`/loki/api/v1/label/node/values`) et en trouvant la liste vide. Les six composants d'Alloy restaient `healthy`, 68 fichiers activement suivis, les lignes arrivaient. Seule l'étiquette par laquelle on aurait cherché « que disait pi2 cette nuit-là » manquait. Vérifié après correction : `node` rend pi1, pi2, pi3. ⚠ Relevé au passage, à ne pas oublier : le `config-reloader` n'a PAS rechargé Alloy quand la ConfigMap a changé. Le fichier monté portait bien la correction (horodaté 15:31), le composant en mémoire exécutait toujours l'ancienne règle. Il a fallu un `rollout restart` pour que la nouvelle config prenne. Le rechargement à chaud n'est donc pas une garantie — après un changement de configuration d'Alloy, VÉRIFIER la propriété visée plutôt que supposer. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Fable 5.1 <[email protected]>
257 lines
11 KiB
YAML
257 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"
|
|
}
|
|
// ⚠ `__meta_kubernetes_POD_node_name`, PAS `__meta_kubernetes_node_name`.
|
|
// Le second n'existe QUE pour `role = "node"` ; sous `role = "pod"` il
|
|
// est vide, et un relabel dont la source est vide ne pose pas
|
|
// l'étiquette — SANS erreur, sans avertissement, sans rien. Mesuré :
|
|
// l'étiquette `node` était simplement absente de Loki, sur une
|
|
// collecte par ailleurs saine. On ne s'en aperçoit qu'en DEMANDANT
|
|
// les valeurs de l'étiquette et en trouvant la liste vide.
|
|
rule {
|
|
source_labels = ["__meta_kubernetes_pod_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
|