Aller au contenu
Edouard Topin's Blog
Observabilité as Code sur VCF & Kubernetes / Série 03/05

Logs centralisés avec Loki et Fluent Bit sur VCF

Construire la stack de logs PLG sur VCF et VKS : déployer Fluent Bit en DaemonSet, configurer son pipeline, envoyer les logs à Loki, et les interroger avec LogQL.

Edouard Topin
4 min de lecture
Illustration éditoriale abstraite d'un pipeline de logs s'écoulant depuis les sources pods via Fluent Bit vers un backend de stockage Loki.

La stack Elasticsearch, Logstash, Kibana a été la réponse standard à la centralisation des logs pendant près d’une décennie. Elle fonctionne encore. Mais pour les équipes qui font tourner Kubernetes sur VCF, son coût opérationnel — nœuds Elasticsearch gourmands en mémoire, gestion complexe des index, requêtes froides lentes — est devenu un problème réel.

La stack PLG (Promtail ou Fluent Bit, Loki, Grafana) échange l’indexation plein texte contre une indexation par labels et délivre une réduction du coût de stockage de 10 à 20 fois pour des volumes de logs Kubernetes typiques. Le compromis est réel : sans indexation plein texte, vous ne pouvez pas faire une recherche dans le corps du log via l’index. Vous pouvez filtrer sur les labels et utiliser des regex dans les requêtes LogQL, mais cela scanne des chunks plutôt que de frapper un index. Pour les équipes plateforme dont 90% des requêtes sont “montrez-moi tous les logs d’erreur du pod X dans le namespace Y”, le compromis est valable.

Architecture de Loki

Loki est un système d’agrégation de logs inspiré de Prometheus. Sa décision de conception clé est de stocker uniquement les labels comme index et le contenu des logs comme chunks compressés en object storage.

Un déploiement Loki en production se compose de cinq composants principaux :

Distributor — reçoit les flux de logs des clients (Fluent Bit, Promtail, OTel Collector). Valide la structure du flux, applique les limites d’ingestion, et distribue vers les ingesters. Le distributor est le point d’entrée du chemin d’écriture et est stateless.

Ingester — conserve les chunks de logs entrants en mémoire jusqu’à ce qu’ils atteignent un intervalle de flush configurable (défaut : 5 minutes) ou un seuil de taille, puis les écrit en object storage. L’ingester répond aussi aux requêtes sur les données récentes avant que les chunks ne soient flushés. Il est stateful — il faut faire tourner au moins 3 ingesters avec un facteur de réplication de 2 ou 3 pour la résilience en production.

Querier — gère les requêtes LogQL en récupérant les chunks depuis les ingesters (données récentes) et l’object storage (données historiques), puis en appliquant les expressions de filtre et de métrique localement. Passe à l’échelle horizontalement.

Compactor — applique les politiques de rétention, la déduplication et la compaction des index. Un processus de fond à un seul réplica ; pas dans le chemin de requête à chaud.

Query Frontend (optionnel mais recommandé) — se place devant les queriers, divise les grandes requêtes sur des plages de temps en sous-requêtes parallèles plus petites, met les résultats en cache. Améliore significativement les performances pour les grandes plages temporelles.

Déploiement de Loki sur VKS

helm repo add grafana https://grafana.github.io/helm-charts
helm repo update

helm upgrade --install loki grafana/loki \
  --namespace logging \
  --create-namespace \
  --values loki-values.yaml \
  --version 6.x
# loki-values.yaml — production sur VKS
loki:
  auth_enabled: false
  commonConfig:
    replication_factor: 2
  storage:
    type: filesystem
    filesystem:
      chunks_directory: /var/loki/chunks
      rules_directory: /var/loki/rules
  limits_config:
    ingestion_rate_mb: 16
    ingestion_burst_size_mb: 32
    max_label_names_per_series: 15
    max_entries_limit_per_query: 5000
    retention_period: 744h       # 31 jours
  schema_config:
    configs:
      - from: "2024-01-01"
        store: tsdb
        object_store: filesystem
        schema: v13
        index:
          prefix: loki_index_
          period: 24h

deploymentMode: SimpleScalable

read:
  replicas: 2
  resources:
    requests: { cpu: 200m, memory: 256Mi }
    limits: { cpu: 1000m, memory: 1Gi }
  persistence:
    storageClass: vsan-default-storage-policy
    size: 10Gi

write:
  replicas: 3
  resources:
    requests: { cpu: 200m, memory: 512Mi }
    limits: { cpu: 1000m, memory: 2Gi }
  persistence:
    storageClass: vsan-default-storage-policy
    size: 20Gi

backend:
  replicas: 1
  persistence:
    storageClass: vsan-default-storage-policy
    size: 10Gi

Fluent Bit : le pipeline DaemonSet

Fluent Bit est le processeur et forwarder de logs léger conçu pour Kubernetes. Il tourne en DaemonSet — un pod par nœud — lisant les fichiers de logs depuis le système de fichiers hôte et les expédiant avec enrichissement de métadonnées Kubernetes.

ConfigMap complet

apiVersion: v1
kind: ConfigMap
metadata:
  name: fluent-bit-config
  namespace: logging
data:
  fluent-bit.conf: |
    [SERVICE]
        Flush         5
        Daemon        Off
        Log_Level     info
        Parsers_File  parsers.conf
        HTTP_Server   On
        HTTP_Listen   0.0.0.0
        HTTP_Port     2020

    [INPUT]
        Name              tail
        Tag               kube.*
        Path              /var/log/containers/*.log
        Parser            cri
        DB                /run/fluent-bit/flb_kube.db
        Mem_Buf_Limit     50MB
        Skip_Long_Lines   On
        Refresh_Interval  10

    [FILTER]
        Name                kubernetes
        Match               kube.*
        Kube_URL            https://kubernetes.default.svc:443
        Kube_CA_File        /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
        Kube_Token_File     /var/run/secrets/kubernetes.io/serviceaccount/token
        Kube_Tag_Prefix     kube.var.log.containers.
        Merge_Log           On
        Merge_Log_Key       log_processed
        Keep_Log            Off
        Annotations         Off
        Labels              On

    [FILTER]
        Name   grep
        Match  kube.*
        Exclude  $kubernetes['namespace_name'] monitoring

    [OUTPUT]
        Name            loki
        Match           kube.*
        Host            loki-gateway.logging.svc.cluster.local
        Port            80
        Labels          job=fluent-bit,namespace=$kubernetes['namespace_name'],pod=$kubernetes['pod_name'],container=$kubernetes['container_name'],node=$kubernetes['host']
        Line_Format     json
        Auto_Kubernetes_Labels Off
        Retry_Limit     False
        Batch_wait      1s
        Batch_size      1048576

  parsers.conf: |
    [PARSER]
        Name        cri
        Format      regex
        Regex       ^(?<time>[^ ]+) (?<stream>stdout|stderr) (?<logtag>[^ ]*) (?<log>.*)$
        Time_Key    time
        Time_Format %Y-%m-%dT%H:%M:%S.%L%z

Manifest DaemonSet

apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: fluent-bit
  namespace: logging
spec:
  selector:
    matchLabels:
      app: fluent-bit
  template:
    metadata:
      labels:
        app: fluent-bit
    spec:
      serviceAccountName: fluent-bit
      tolerations:
        - key: node-role.kubernetes.io/control-plane
          effect: NoSchedule
      containers:
        - name: fluent-bit
          image: cr.fluentbit.io/fluent/fluent-bit:3.3
          ports:
            - containerPort: 2020
              name: http-metrics
          volumeMounts:
            - name: varlog
              mountPath: /var/log
              readOnly: true
            - name: config
              mountPath: /fluent-bit/etc/
            - name: db
              mountPath: /run/fluent-bit
          resources:
            requests: { cpu: 50m, memory: 64Mi }
            limits: { cpu: 200m, memory: 256Mi }
      volumes:
        - name: varlog
          hostPath:
            path: /var/log
        - name: config
          configMap:
            name: fluent-bit-config
        - name: db
          emptyDir: {}

LogQL : interroger vos logs

LogQL est le langage de requête de Loki, modelé sur PromQL. Il existe sous deux formes : les requêtes de logs (retournent des lignes de log) et les requêtes de métriques (calculent des métriques depuis des lignes de log).

Anatomie d’une requête de log

{namespace="production", pod=~"api-.*"}         # sélecteur de flux (obligatoire)
| json                                           # parser : extraire les champs JSON
| level = "error"                               # filtre de ligne
| message =~ "timeout|connection refused"       # filtre regex sur champ extrait
| line_format "{{.timestamp}} {{.message}}"     # formater la sortie

Le sélecteur de flux {namespace="production"} est l’équivalent d’un sélecteur de labels Prometheus — il réduit les flux de logs récupérés depuis l’object storage. Tous les filtres après | sont appliqués en mémoire sur les chunks récupérés. Cela signifie que l’efficacité du sélecteur de flux détermine les performances des requêtes. Incluez toujours au minimum namespace et idéalement pod ou container dans le sélecteur de flux.

Exemples de requêtes de métriques

# Taux d'erreur par namespace sur 5 minutes
sum by(namespace) (rate({job="fluent-bit"} | json | level="error" [5m]))

# Octets reçus par pod sur 1 minute
sum by(pod) (bytes_rate({job="fluent-bit"}[1m]))

# P99 de latence extrait depuis les logs structurés
quantile_over_time(0.99,
  {namespace="production"} | json | unwrap duration_ms [10m]
) by (service)

EFK vs PLG : la comparaison honnête

Coût de stockage
Loki stocke uniquement les labels dans l'index ; le contenu des logs est stocké sous forme de chunks compressés (snappy) en object storage. Pour un cluster produisant 10 Go/jour de logs bruts, Loki nécessite typiquement 1 à 2 Go/jour d'index + chunks compressés combinés. Elasticsearch indexe le corps complet du log, résultant en 15 à 25 Go/jour de stockage d'index pour le même volume. Sur 30 jours : environ 45 Go avec Loki vs environ 600 Go avec Elasticsearch. La différence compte particulièrement sur vSAN, où le coût de stockage par Go est supérieur à celui de l'object storage cloud.
Performances des requêtes
Elasticsearch gagne sur la recherche plein texte dans les corps de logs — c'est son objectif de conception principal. Il maintient un index inversé sur chaque token de chaque message de log. Loki peut seulement scanner en regex à l'intérieur d'un chunk de log après filtrage par labels. Pour des requêtes comme 'trouver tous les logs contenant un code d'erreur spécifique', Elasticsearch retourne les résultats plus rapidement sur de grands datasets. Pour des requêtes comme 'montrer tous les logs d'erreur de ce pod dans les 30 dernières minutes', Loki est comparable et souvent plus rapide grâce à des volumes de données plus petits.
Complexité opérationnelle
Elasticsearch nécessite le réglage du heap JVM, la gestion des shards, les politiques de cycle de vie des index, les tiers de nœuds chaud/tiède/froid, et la rotation périodique des index. Loki en mode SimpleScalable sur VKS nécessite la configuration de réplicas de lecture et d'écriture, d'une classe de stockage et d'une période de rétention. La surface opérationnelle est significativement plus petite. Pour une équipe plateforme qui opère aussi Prometheus et Grafana, la similarité du modèle opérationnel (labels, requêtes style PromQL) réduit la charge cognitive d'ajouter Loki.
Quand choisir Elasticsearch
Si votre équipe sécurité a besoin de rechercher dans les corps de logs des chaînes spécifiques dans le cadre d'un flux SIEM (threat hunting, audit de conformité), Elasticsearch est le bon choix. Ses capacités de recherche plein texte et ses intégrations avec les outils d'analyse de sécurité (Kibana SIEM, OpenSearch Security Analytics) sont inégalées. Pour les environnements régulés comme la banque ou la santé où la recherche dans les logs fait partie de la forensique d'incident, ELK ou OpenSearch vaut le coût opérationnel.

Intégration avec Grafana

Ajoutez Loki comme source de données dans Grafana sous Configuration → Sources de données → Ajouter une source de données → Loki. Définissez l’URL sur http://loki-gateway.logging.svc.cluster.local:80. Activez les champs dérivés pour créer des liens automatiques depuis les lignes de log contenant un champ traceId vers la trace correspondante dans Tempo — c’est la corrélation log-vers-trace qui rend la réponse aux incidents dramatiquement plus rapide.

Références.

Reçois le prochain par email

Les nouveaux articles et séries, envoyés à leur publication. Aucun autre courrier.

Désabonnement en un clic, à tout moment.

Retour au blog
Partager

Articles similaires

  1. 17 min de lecture

    Modèles de coûts FinOps : ce que facturent AWS, Azure et GCP — et ce que VCF calcule

    Un cluster-heure EKS, un palier AKS, une requête de pod GKE et un matériel VCF amorti ne sont pas quatre valeurs de la même variable. Ce que chaque plateforme facture, et ce que VCF calcule.

  2. 23 min de lecture

    Network policies et Cilium : construire un default-deny défendable

    L'API NetworkPolicy est livrée avec Kubernetes ; l'appliquer est le travail du CNI. Ce que Cilium ajoute, ce qui reste standard, et comment atteindre le default-deny sans casser la production.

  3. 18 min de lecture

    RBAC Kubernetes : les fondations, et les pièges qui survivent à l'audit

    Chacun de ces pièges est publié sur kubernetes.io. Ce qui manque, c'est leur mise en ordre — et le chemin qui mène d'un Namespace vSphere jusqu'à cluster-admin.

Suivre le blog

Nouveaux articles, réflexions et mises à jour.