Kubernetes cluster-logging met Fluent Bit en de EFK-stack

Container-logs verdwijnen zodra een pod wordt verwijderd. kubectl logs toont alleen het laatste 10 MiB rotatiebestand van een enkele pod. Voor alles voorbij lokaal debuggen heb je een gecentraliseerde logging-pipeline nodig. Deze tutorial laat zien hoe je Fluent Bit als DaemonSet deployt, logs naar Elasticsearch 8.x stuurt via TLS, en ze bevraagt in Kibana.

Inhoudsopgave

Wat je gaat leren

Aan het einde van deze tutorial heb je een werkende gecentraliseerde logging-pipeline: Fluent Bit die container-logs van elke node verzamelt, ze verrijkt met Kubernetes-metadata, via TLS naar Elasticsearch 8.x stuurt, en een Kibana data view klaar voor queries. Je begrijpt waarom elke configuratiekeuze er is en wat je moet aanpassen als je cluster groeit.

Vereisten

Zorg dat je het volgende hebt:

  • Een draaiend Kubernetes-cluster, versie 1.25 of nieuwer, met containerd als container-runtime. Managed clusters (GKE, EKS, AKS) en lokale clusters (kind, minikube) werken allebei. Deze tutorial richt zich op Fluent Bit 5.0 en Elasticsearch 8.17.
  • Helm 3 geinstalleerd en geconfigureerd voor je cluster.
  • kubectl met cluster-admin rechten. De ECK-operator installeert CRDs en cluster-brede resources.
  • Voldoende clustercapaciteit voor het Elasticsearch StatefulSet: minimaal 3 nodes met 8 Gi geheugen beschikbaar per ES-pod. Voor een snelle test op kind/minikube werkt een single-node ES-setup, maar dat is niet productiewaardig.
  • Als je al Prometheus draait, Fluent Bit stelt een /api/v1/metrics/prometheus endpoint beschikbaar dat je kunt scrapen. Niet vereist voor deze tutorial, maar goed om te weten.

Hoe Kubernetes logging werkt (en waar het misgaat)

Containers schrijven naar stdout en stderr. De container-runtime (containerd) vangt die output op en schrijft het naar logbestanden op de node: /var/log/pods/<namespace>_<pod>_<uid>/<container>/0.log. Symlinks in /var/log/containers/ bieden een platte directory van alle containers op de node.

De kubelet roteert deze bestanden. De defaults zijn 10 MiB per bestand (containerLogMaxSize) en 5 geroteerde bestanden (containerLogMaxFiles). kubectl logs leest alleen het nieuwste bestand, dus je krijgt maximaal 10 MiB van de meest recente output van een enkele pod.

Voor het debuggen van een enkele pod in development is dat prima. In productie gaat het snel mis:

  • Pod-verwijdering verwijdert logs. Zodra een pod wordt verwijderd of ge-evict, ruimt de kubelet de logbestanden op. Post-mortem analyse is dan onmogelijk.
  • Geen aggregatie. Een Deployment met 10 replica's betekent 10 losse kubectl logs commando's. Correlatie tussen services vereist handwerk.
  • API-server belasting. Elke kubectl logs call gaat via de API-server naar de kubelet. Bij schaal (honderden pods, meerdere teams) wordt dit een bottleneck.
  • Een 50-node cluster met 500 pods kan 10-50 GB logs per dag genereren. Handmatig ophalen is niet realistisch.

De Kubernetes-documentatie beschrijft drie cluster-level logging-architecturen. De meest gebruikte is de node logging agent: een DaemonSet die container-logs van het nodebestandssysteem leest en doorstuurt naar een centrale opslag. Dat is het EFK-patroon.

Waarom Fluent Bit en niet Fluentd

Fluent Bit en Fluentd zijn allebei CNCF graduated projecten onder de Fluent-paraplu, wat voor verwarring zorgt. Het zijn verschillende tools, gebouwd voor verschillende rollen.

Fluentd Fluent Bit
Taal Ruby + C Puur C
Geheugengebruik ~40 MB basis Minder dan 30 MB werkgeheugen
Runtime-afhankelijkheden Ruby-runtime, gem management Geen externe afhankelijkheden
Plugin-ecosysteem 1.000+ Ruby gems ~100 ingebouwde plugins
CNCF-status Graduated Graduated
Ontwerprol Centrale aggregatie, complexe transformaties Node-level collection agent

Voor een Kubernetes DaemonSet (een pod per node) is de sub-30 MB footprint en de dependency-loze binary gewoon een betere keuze. Fluent Bit handelt logcollectie, metadata-verrijking en Elasticsearch-forwarding af in een enkel binary, zonder Fluentd als tussenlaag.

Elasticsearch en Kibana deployen met ECK

De Elastic Cloud on Kubernetes (ECK) operator automatiseert TLS-certificaten, rolling upgrades en wachtwoordbeheer. Handmatige manifests werken ook, maar ECK neemt een groot oppervlak aan certificaat- en lifecycle-beheer van je over.

Stap 1: Installeer de ECK-operator

helm repo add elastic https://helm.elastic.co
helm repo update
helm install elastic-operator elastic/eck-operator \
  -n elastic-system --create-namespace

Controleer of de operator draait:

kubectl get pods -n elastic-system

Verwachte output:

NAME                                READY   STATUS    RESTARTS   AGE
elastic-operator-<hash>             1/1     Running   0          45s

Stap 2: Maak de logging-namespace en deploy Elasticsearch

# elasticsearch.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: logging
---
apiVersion: elasticsearch.k8s.elastic.co/v1
kind: Elasticsearch
metadata:
  name: elasticsearch
  namespace: logging
spec:
  version: 8.17.0
  nodeSets:
    - name: default
      count: 3                         # minimum voor productie-HA
      config:
        node.store.allow_mmap: false   # vereist voor de meeste container-runtimes
      podTemplate:
        spec:
          containers:
            - name: elasticsearch
              resources:
                requests:
                  memory: 4Gi
                  cpu: 500m
                limits:
                  memory: 8Gi
                  cpu: "2"
              env:
                - name: ES_JAVA_OPTS
                  value: "-Xms4g -Xmx4g" # heap = 50% van geheugenlimiet, min/max gelijk
kubectl apply -f elasticsearch.yaml

De JVM-heapregel is belangrijk: stel -Xms en -Xmx in op dezelfde waarde, precies 50% van de pod-geheugenlimiet. Ga nooit boven 31 GB, want daarboven kan de JVM geen compressed object pointers gebruiken en verspil je geheugen.

Wacht tot het cluster groen is:

kubectl get elasticsearch -n logging

Verwachte output (kan 2-3 minuten duren):

NAME            HEALTH   NODES   VERSION   PHASE   AGE
elasticsearch   green    3       8.17.0    Ready   3m

Stap 3: Deploy Kibana

# kibana.yaml
apiVersion: kibana.k8s.elastic.co/v1
kind: Kibana
metadata:
  name: kibana
  namespace: logging
spec:
  version: 8.17.0
  count: 1
  elasticsearchRef:
    name: elasticsearch
kubectl apply -f kibana.yaml

Stap 4: Haal het Elasticsearch-wachtwoord op

ECK genereert een wachtwoord voor de elastic superuser en slaat het op in een Secret:

kubectl get secret elasticsearch-es-elastic-user -n logging \
  -o go-template='{{.data.elastic | base64decode}}'

Bewaar dit wachtwoord. Je hebt het nodig voor zowel de Fluent Bit-configuratie als de Kibana-login.

Checkpoint: Elasticsearch is groen met 3 nodes, Kibana-pod is Running, en je hebt het elastic wachtwoord.

Fluent Bit deployen als DaemonSet

Stap 5: Maak de RBAC-resources aan

Fluent Bit heeft leestoegang nodig tot pod- en namespace-metadata voor de Kubernetes filterplugin:

# fluent-bit-rbac.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: fluent-bit
  namespace: logging
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: fluent-bit-read
rules:
  - apiGroups: [""]
    resources:
      - namespaces
      - pods
    verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: fluent-bit-read
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: fluent-bit-read
subjects:
  - kind: ServiceAccount
    name: fluent-bit
    namespace: logging
kubectl apply -f fluent-bit-rbac.yaml

Stap 6: Maak de Elasticsearch credentials Secret

Sla de credentials op in een Secret (nooit in een ConfigMap, die is standaard leesbaar voor elke pod in de namespace):

kubectl create secret generic elasticsearch-credentials \
  -n logging \
  --from-literal=ES_USERNAME=elastic \
  --from-literal=ES_PASSWORD=<plak-wachtwoord-uit-stap-4>

Stap 7: Kopieer het ECK CA-certificaat

ECK slaat het CA-certificaat op in elasticsearch-es-http-certs-public. Omdat Kubernetes Secrets namespace-scoped zijn, kan Fluent Bit er direct bij als beide in de logging namespace draaien. Als je Fluent Bit-pods in een andere namespace draaien, kopieer dan de Secret:

kubectl get secret elasticsearch-es-http-certs-public -n logging \
  -o yaml | kubectl apply -n <fluent-bit-namespace> -f -

Stap 8: Maak de Fluent Bit-configuratie

Deze ConfigMap gebruikt het YAML-configuratieformaat dat beschikbaar is sinds Fluent Bit 3.2. YAML vereenvoudigt Kubernetes-integratie omdat parserdefinities in hetzelfde bestand kunnen staan, wat aparte parser-ConfigMaps overbodig maakt.

# fluent-bit-config.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: fluent-bit-config
  namespace: logging
data:
  fluent-bit.yaml: |
    service:
      flush: 5                    # flush-interval in seconden
      log_level: info
      http_server: true           # activeert health- en metrics-endpoints
      http_listen: 0.0.0.0
      http_port: 2020
      health_check: true
      storage.metrics: true

    pipeline:
      inputs:
        - name: tail
          path: /var/log/containers/*.log
          multiline.parser: docker, cri  # ondersteunt beide formaten; containerd gebruikt CRI
          tag: kube.*
          db: /var/log/flb_kube.db       # onthoudt leespositie na herstart
          mem_buf_limit: 5MB
          skip_long_lines: true
          refresh_interval: 10

      filters:
        - name: kubernetes
          match: kube.*
          kube_url: https://kubernetes.default.svc.cluster.local:443
          kube_ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
          kube_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token
          merge_log: true          # parse JSON-logvelden en flatten in het record
          keep_log: false          # verwijder raw log-veld na merge (bespaart opslag)
          labels: true
          annotations: false       # annotations zijn vaak groot; activeer selectief

      outputs:
        - name: es
          match: kube.*
          host: elasticsearch-es-http.logging.svc.cluster.local
          port: 9200
          http_user: ${ES_USERNAME}
          http_passwd: ${ES_PASSWORD}
          tls: true
          tls.verify: true
          tls.ca_file: /etc/ssl/elasticsearch/ca.crt
          logstash_format: true           # date-stamped indices voor ILM-compatibiliteit
          logstash_prefix: kubernetes     # indexnaam wordt kubernetes-2026.04.09
          logstash_dateformat: "%Y.%m.%d"
          replace_dots: true              # ES weigert dots in veldnamen
          suppress_type_name: true        # vereist voor ES 8.x (mapping types verwijderd)
          retry_limit: 5                  # eindige retries voorkomen onbegrensde geheugengroei
          time_key: "@timestamp"
          trace_error: true               # logt ES-foutresponses voor debugging
kubectl apply -f fluent-bit-config.yaml

De db-parameter op de Tail-input is belangrijk. Zonder dit verliest Fluent Bit zijn leespositie bij een herstart en leest het ofwel alle logs opnieuw (duplicaten) of mist logs die tijdens de herstart zijn geschreven.

De multiline.parser: docker, cri instelling laat Fluent Bit het logformaat automatisch detecteren. Moderne clusters met containerd produceren CRI-format logs met een P/F vlag die aangeeft of een regel gedeeltelijk of volledig is. Oudere Docker-gebaseerde clusters gebruiken JSON-wrapped logregels.

Stap 9: Deploy het DaemonSet

# fluent-bit-daemonset.yaml
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
          operator: Exists
          effect: NoSchedule         # verzamel ook control-plane logs
      containers:
        - name: fluent-bit
          image: cr.fluentbit.io/fluent/fluent-bit:5.0
          ports:
            - containerPort: 2020
              name: http
          resources:
            requests:
              cpu: 50m
              memory: 64Mi
            limits:
              cpu: 200m
              memory: 256Mi
          livenessProbe:
            httpGet:
              path: /api/v1/health
              port: 2020
            initialDelaySeconds: 30
            periodSeconds: 10
          readinessProbe:
            httpGet:
              path: /api/v1/health
              port: 2020
            initialDelaySeconds: 10
            periodSeconds: 5
          envFrom:
            - secretRef:
                name: elasticsearch-credentials
          volumeMounts:
            - name: config
              mountPath: /fluent-bit/etc/fluent-bit.yaml
              subPath: fluent-bit.yaml
            - name: varlog
              mountPath: /var/log
              readOnly: true
            - name: varlibdockercontainers
              mountPath: /var/lib/docker/containers
              readOnly: true
            - name: elasticsearch-ca
              mountPath: /etc/ssl/elasticsearch
              readOnly: true
      volumes:
        - name: config
          configMap:
            name: fluent-bit-config
        - name: varlog
          hostPath:
            path: /var/log
        - name: varlibdockercontainers
          hostPath:
            path: /var/lib/docker/containers
        - name: elasticsearch-ca
          secret:
            secretName: elasticsearch-es-http-certs-public
kubectl apply -f fluent-bit-daemonset.yaml

Controleer dat er een pod per node draait:

kubectl get daemonset fluent-bit -n logging

Verwachte output:

NAME         DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR   AGE
fluent-bit   3         3         3       3             3           <none>          30s

Bekijk de Fluent Bit-logs op fouten:

kubectl logs -n logging daemonset/fluent-bit --tail=20

Als je [output:es:es.0] ...connected ziet en geen [error] regels, dan is de pipeline gezond.

Checkpoint: Fluent Bit DaemonSet draait op elke node, geen fouten in de logs, verbonden met Elasticsearch.

Controleren of logs in Kibana aankomen

Stap 10: Maak een Kibana data view

Port-forward Kibana:

kubectl port-forward svc/kibana-kb-http 5601:5601 -n logging

Open https://localhost:5601 in je browser (accepteer het self-signed certificaat). Log in met gebruikersnaam elastic en het wachtwoord uit Stap 4.

Ga naar Stack Management -> Data Views -> Create data view:

  • Index pattern: kubernetes-*
  • Timestamp field: @timestamp
  • Name: Kubernetes Logs

Sla de data view op. In Kibana 8.x vervangt "Data Views" het oudere "Index Patterns" concept.

Ga naar Discover, selecteer de "Kubernetes Logs" data view, en je zou logrecords moeten zien met Kubernetes-metadatavelden: kubernetes.pod_name, kubernetes.namespace_name, kubernetes.container_name en kubernetes.labels.*.

Checkpoint: Logs zichtbaar in Kibana Discover met Kubernetes-metadata.

Productie-hardening

De setup tot nu toe werkt, maar mist productieduurzaamheid. Drie gebieden vragen aandacht.

Index lifecycle management

Een 50-node cluster dat 50 GB/dag genereert, verzamelt 4,5 TB over 90 dagen. Zonder Index Lifecycle Management (ILM) groeit de opslag tot Elasticsearch geen schijfruimte meer heeft.

Een verstandig defaultbeleid:

Fase Leeftijd Actie
Hot 0-7 dagen Actieve write-index op snelle opslag. Rollover bij 50 GB of 7 dagen.
Warm 7-30 dagen Read-only, force-merge naar 1 segment, gecomprimeerd.
Cold 30-90 dagen Frozen op goedkoopste opslagtier.
Delete 90+ dagen Verwijderen.

ILM werkt omdat Fluent Bit's logstash_format: true date-stamped indices aanmaakt (kubernetes-2026.04.09) die op een natuurlijke manier passen bij rollover-policies.

Filesystem buffering

Als Elasticsearch down gaat, loopt de in-memory buffer van Fluent Bit vol. Zonder filesystem buffering worden logs gedroopt of past Fluent Bit backpressure toe op de Tail-input, waardoor het lezen van logs helemaal stopt.

Voeg toe aan de service-sectie van de ConfigMap:

service:
  storage.path: /var/log/flb-storage/
  storage.sync: normal
  storage.checksum: false
  storage.max_chunks_up: 128
  storage.backlog.mem_limit: 5M

En stel storage.type: filesystem in op de Tail-input:

inputs:
  - name: tail
    storage.type: filesystem    # schrijft naar disk als geheugen vol is
    # ... rest van de inputconfig

Wanneer storage.type: filesystem is ingesteld, bepaalt de storage.max_chunks_up parameter hoeveel chunks in het geheugen blijven. De rest gaat naar het pad in storage.path.

Elasticsearch resource-hardening

Voor productie-workloads stel je requests gelijk aan limits op de Elasticsearch-pods. Dit geeft ze Guaranteed QoS-klasse, waardoor kubelet-eviction onder geheugendruk wordt voorkomen. Voeg een PodDisruptionBudget toe met maxUnavailable: 1 om te voorkomen dat er meer dan een ES-node tegelijk wordt gedraineerd.

Veelvoorkomende problemen

Symptoom Waarschijnlijke oorzaak Oplossing
Geen logs in Kibana Fluent Bit bereikt ES niet Check kubectl logs daemonset/fluent-bit -n logging op failed to flush chunk fouten. Controleer de TLS CA-mount en credentials.
_type unknown fout in Fluent Bit-logs ES 8.x heeft mapping types verwijderd Voeg suppress_type_name: true toe aan de ES-output.
Logs missen na herstart van Fluent Bit Geen positie-tracking Voeg db: /var/log/flb_kube.db toe aan de Tail-input.
Hoog geheugengebruik op Fluent Bit-pods Oneindige retries tijdens ES-uitval Stel retry_limit: 5 in in plaats van false (de Helm chart default).
Geen Kubernetes-metadata op logs RBAC ontbreekt Controleer of de ClusterRole get, list, watch geeft op pods en namespaces.
Kibana data view toont geen resultaten Indexpatroon-mismatch De logstash_prefix in Fluent Bit moet overeenkomen met het data view patroon. Als de prefix kubernetes is, moet het patroon kubernetes-* zijn.

Voor diepere isolatie voeg je tijdelijk een stdout output toe aan de Fluent Bit-config (name: stdout, match: kube.*). Als logs verschijnen in de output van de Fluent Bit-pod maar niet in Elasticsearch, dan zit het probleem in connectiviteit of authenticatie, niet in collectie.

Wat je hebt geleerd

Je hebt een complete gecentraliseerde logging-pipeline gedeployd: Fluent Bit leest container-logs van het nodebestandssysteem via de Tail-input, verrijkt records met Kubernetes-metadata (podnaam, namespace, labels), stuurt ze via TLS naar Elasticsearch 8.x, en Kibana biedt de query-interface. Je weet waarom het db-bestand belangrijk is voor herstart-veiligheid, waarom suppress_type_name vereist is voor ES 8.x, en waar je moet beginnen met hardening voor productie (ILM, filesystem buffering, resource-garanties).

Verder lezen

  • Als je nog geen metriek-monitoring naast logs hebt, behandelt de Prometheus en kube-prometheus-stack tutorial het opzetten van metriekcollectie en alerting op hetzelfde cluster.
  • Om te begrijpen waarom Fluent Bit-pods (of welke pod dan ook) CPU-throttling kunnen laten zien ondanks laag gemiddeld gebruik, zie Kubernetes CPU-throttling: waarom pods stilvallen bij laag gebruik.
  • Het Fluent Bit-project ondersteunt ook Loki als output-bestemming. Als je al Grafana draait voor metrics via kube-prometheus-stack, kan Loki plus Grafana Elasticsearch plus Kibana vervangen met een lagere opslag-footprint, ten koste van minder krachtige full-text search.
  • Wil je ook distributed traces en een uniform wire-formaat naast logs, dan behandelt de OpenTelemetry op Kubernetes-tutorial het deployen van de OpenTelemetry Collector. Fluent Bit voor logs houden en OpenTelemetry alleen voor traces en metrics gebruiken is een prima splitsing als je log-volume hoog is.

Terugkerende server- of deploymentproblemen?

Ik help teams productie betrouwbaar maken met CI/CD, Kubernetes en cloud—zodat fixes blijven en deploys geen stress meer zijn.

Bekijk DevOps consultancy

Doorzoek deze site

Begin met typen om te zoeken, of blader door de kennisbank en blog.