# ----------------------------------------------------------------------------- # 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 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 # k3s utilise containerd, pas Docker : ce montage-là ne servirait à rien. dockercontainers: false # ⚠ 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/__//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] } // ⚠ containerd n'écrit PAS du JSON de Docker : chaque ligne est // « ». Sans `stage.cri`, l'horodatage // stocké serait celui de la LECTURE et non celui de l'écriture — ce qui // ruinerait précisément l'usage qui motive ce lot : dater une panne. loki.process "journaux_de_pods" { stage.cri {} 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