У динамічній інфраструктурі важливо мати віддалене зберігання логів додатків, адже їх життєвий цикл може бути коротким. Середовище додатку (наприклад, под, в якому додаток працює) банально вже може не існувати допоки інженер знайде час для дослідження проблеми. Тому цього разу поглянемо на роботу Fluent Bit в Kubernetes і як він здатен допомогти у вирішенні цієї задачі.
У 2014 році, розуміючи потребу в легкому процесорі логів для вбудованих систем на базі Linux, команда Fluentd в межах компанії Treasure Data створила Fluent Bit. Наразі він продовжує розвиватись під парасолею команди Fluentd як стабільний CNCF проєкт. Fluent Bit є дещо менш функціональним за материнський проєкт Fluentd, проте все ж пропонується як продукт наступної генерації.
Fluent Bit звісно не єдиний в своєму роді і ту ж роботу може виконати Grafana Alloy, Vector від Datadog, FileBeat від Elastic тощо.
1. KEY CONCEPTS
Для того, щоб рухатись далі, познайомимось із базовими концепціями Fluent Bit.
Кожен фрагмент логу, метрики, трейсу чи профайлу, що отримав Fluent Bit, називається Подією чи Записом (Event or a Record).
Для прикладу розглянемо вміст файлу Syslog:
Jan 18 12:52:16 flb systemd[2222]: Starting GNOME Terminal Server
Jan 18 12:52:16 flb dbus-daemon[2243]: [session uid=1000 pid=2243] Successfully activated service 'org.gnome.Terminal'
Jan 18 12:52:16 flb systemd[2222]: Started GNOME Terminal Server.
Jan 18 12:52:16 flb gsd-media-keys[2640]: # watch_fast: "/org/gnome/terminal/legacy/" (establishing: 0, active: 0)
Тож тут наведено 4 різні події, кожна із яких складається із наступних частин:
- позначка часу (timestamp)
- метадані ключ/значення (metadata)
- корисні дані (payload, message)
[[TIMESTAMP, METADATA], MESSAGE]
Основне завдання Fluent Bit - зробити запис структурованим, за потреби збагативши його корисними даними, і готовим до запису у віддалене сховище. Цей функціонал забезпечується плагінами, що можуть відповідати за збір даних (Input), фільтрацію (Filter), передачу (Output) тощо.
- Input - це плагіни, що збирають дані задля подальшої обробки. Вони можуть читати дані із файлів, зі сторонньої бази, операційної системи (метрики CPU, GPU) тощо.
- Processors - механізми, що працюють в межах Input чи Output плагінів та не є глобальними на відміну від Filters. Дозволяють маніпуляції із контентом, метаданими логів чи трейсів, можуть відсіювати необхідні метрики та інше.
- Parser - логіка, що перетворює неструктуровані дані (syslog дані), в межах Input плагіна, в структуровані (json). Парсери відпрацьовують до Filters та Processors та є єдиними в межах одного Input плагіна. Є можливість описувати власні парсери.
- Filters також імплементовані як плагіни. Забезпечують зміну зібраних даних (івентів) перед відправкою їх до остаточного пункту призначення. Гарний приклад, із яким ми ознайомимось пізніше, це Kubernetes фільтр.
- Buffers - після отримання даних Fluent Bit тимчасово зберігає їх в системній пам'яті (на диску чи в RAM). На основі буферів також побудований Backpressure механізм, коли віддалений сторедж для постійного зберігання логів не справляється із вчасним отриманням всіх даних. Загалом використовується для оптимізації віддалених записів.
- Routing - механізм в межах існуючих плагінів, що дозволяє коректно обробляти та направляти івенти в правильний remote storage/backend. Зазвичай це tag-based routing (tags та matching правила), коли протеганий chunk (блок) івентів йде по всьому пайплайну.
- Output - кінцеве місце призначення даних. Це можуть бути віддалені сервіси/бази, локальні файлові системи, інтерфейси тощо. Це окремі плагіни, яких існує безліч. Ми в подальшому будемо використовувати Elasticsearch та AWS S3 і однойменні плагіни для цих сервісів.
Опції до цих вищезгаданих плагінів та механізмів трансформуються в конфігураційні файли, основний із яких має наступний вигляд:
[SERVICE]
flush 1
log_level info
[INPUT]
name tail
path /var/log/containers/*.log
parser docker
[INPUT]
name forward
listen 0.0.0.0
[OUTPUT]
name stdout
match *
Це історичний .conf формат. Його нова YAML версія буде виглядати так:
service:
flush: 1
log_level: info
pipeline:
inputs:
tail:
path: "/var/log/containers/*.log"
parser: docker
forward:
listen: 0.0.0.0
outputs:
stdout:
match: "*"
Що цікаво, класичний .conf формат буде оголошено застарілим до кінця 2026 року, проте він досі використовується за замовчуванням в офіційному Kubernetes сетапі Fluent Bit. Більш того, деякі опції можуть бути активованими лише в YAML варіанті конфігураційного файлу.
2. SETUP FLUENT BIT OPERATOR
Як вже було згадано, роботу Fluent Bit ми будемо демонструвати на Kubernetes та за традицією візьмемо AWS EKS. Тож інсталюємо його як і раніше, але для установки останньої на даний момент 1.36 версії варто скористатись тегом та Тераформом версії 1.16.2.
Fluent Bit будемо встановлювати як оператор хоч це і варіант, що підтримується community. Сам проєкт Fluent Bit пропонує інший варіант - офіційний helm чарт, що виливається як деймонсет. Коли ви волієте простоти і вищої стабільності - краще скористатись офіційним способом, проте оператор має і свої переваги:
- Дозволяє оновлення конфігурації без ребуту подів.
- Завдяки новим CRD дозволяє оголошувати не лише глобальний конфіг для всіх подів (завдяки ClusterFluentBitConfig, ClusterParser, ClusterFilter, ClusterOutput та іншим CRD), а й у межах неймспейсів (аналогічні FluentBitConfig, Parser, Filter, Output тощо). Це може бути зручно для окремих команд, що бажають мати свою логіку логування і керувати нею самостійно.
- Підтримує установку Fluentd, якщо це необхідно для додаткової логіки обробки логів
Отже опишемо установку Fluent Operator в Terraform.
$ git clone git@github.com:ipeacocks/terraform-aws-example.git
$ git checkout v1.16.2
$ cd terraform-aws-example/eks-infra/vpc
$ terraform init && terraform apply --auto-approve
$ cd ../eks
$ terraform init && terraform apply --auto-approve
На установці Fluent Bit зупинимось детальніше.
$ cd ../addons/fluentbit
$ cat main.tf
data "kubectl_path_documents" "this" {
pattern = "${path.module}/templates/manifests/*.yaml.tpl"
vars = {
es_host = var.es_host
}
}
data "terraform_remote_state" "eks" {
backend = "s3"
config = {
bucket = "my-tf-state-2023-06-01"
key = "my-eks.tfstate"
region = "us-east-1"
}
}
resource "helm_release" "this" {
name = "fluent-operator"
repository = "https://fluent.github.io/helm-charts"
chart = "fluent-operator"
version = var.helm_package_version
create_namespace = true
namespace = var.namespace
values = [
templatefile("${path.module}/templates/helm/values.yaml.tpl", {
limits = {
cpu = var.resources.limits.cpu
memory = var.resources.limits.memory
}
requests = {
cpu = var.resources.requests.cpu
memory = var.resources.requests.memory
}
})
]
}
resource "kubectl_manifest" "this" {
count = length(data.kubectl_path_documents.this.documents)
yaml_body = element(data.kubectl_path_documents.this.documents, count.index)
depends_on = [helm_release.this]
}
Тут було вичитано стейт EKS кластера для того, щоб використати його дані для установки helm-чарту Fluent Bit оператора. Маніфести із директорії templates/manifests будуть застосовані в кінці, адже вони потребують кастомних CRD, котрі сам helm-chart і надає (це ClusterFilter та ClusterOutput). Це відбудеться за допомогою стороннього провайдера kubectl, котрий надає можливість роботи зі вже звичними Kubernetes YAML описами. У довгостроковій перспективі, можливо, було б не зайвим мігрувати на офіційний kubernetes провайдер від Hashicorp, але це вже зовсім інша історія.
Поглянемо на змінні, де описується версія хелм-чарту, адреса elasticsearch-а (саме його ми будемо використовувати як фінальну точку призначення наших логів) та інші дані для доступу, регіон тощо:
$ cat variables.tf
variable "region" {
description = "AWS region"
type = string
default = "us-east-1"
}
variable "credentials" {
default = ["~/.aws/credentials"]
description = "Where your access and secret_key are stored, you create the file when you run the aws config"
}
variable "helm_package_version" {
type = string
description = "Helm package version for fluent operator."
default = "4.3.0"
}
variable "namespace" {
description = "Namespace to which service will be deployed."
type = string
default = "fluent"
}
variable "es_host" {
type = string
description = "Elasticsearch host to send logs to (IP address or hostname)."
default = "elasticsearch.elastic.svc"
}
variable "resources" {
description = "FluentBit daemonset resources."
type = object({
limits = optional(object({
cpu = optional(string, "1000m")
memory = optional(string, "1000Mi")
}), {})
requests = optional(object({
cpu = optional(string, "100m")
memory = optional(string, "250Mi")
}), {})
})
default = {}
}
Зупинимось на вмісті файлу параметрів для helm-чарту templates/helm/values.yaml.tpl:
$ cat templates/helm/values.yaml.tpl
containerRuntime: containerd
Kubernetes: true
operator:
resources:
limits:
cpu: 200m
memory: 300Mi
requests:
cpu: 200m
memory: 150Mi
fluentbit:
resources:
limits:
cpu: ${limits.cpu}
memory: ${limits.memory}
requests:
cpu: ${requests.cpu}
memory: ${requests.memory}
filter:
kubernetes:
enable: false
Тут варто звернути увагу, що опція "Kubernetes: true" дозволила активувати пайплайн для збору логів всіх подів, інакше потрібно було би описувати їх явно. Далі ми описали кількість ресурсів, котрі передали оператору та деймонсетам та відключили фільтри, котрі надає helm оператор, адже вони нас не влаштовують. Про це поговоримо далі.
Проведемо установку всього написаного вище:
$ cd terraform-aws-example/eks-infra/addons/fluentbit
$ terraform init && terraform apply --auto-approve
Та перевіримо результати:
$ aws eks update-kubeconfig --region us-east-1 --name my-eks
$ kubectl get pods -n fluent
NAME READY STATUS RESTARTS AGE
fluent-bit-4vbhh 1/1 Running 0 2m55s
fluent-bit-fld9c 1/1 Running 0 2m55s
fluent-operator-5f748f7457-z92dd 1/1 Running 0 3m1s
Конфіги вище лише описують адресу Elasticsearch, без його установки. Тому встановимо його окремо, адже тут будуть зберігатись логи:
$ kubectl apply -f - <<EOF
apiVersion: v1
kind: Namespace
metadata:
name: elastic
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: elasticsearch
namespace: elastic
spec:
replicas: 1
selector:
matchLabels:
app: elasticsearch
template:
metadata:
labels:
app: elasticsearch
spec:
containers:
- name: elasticsearch
image: docker.elastic.co/elasticsearch/elasticsearch:9.5.3
ports:
- containerPort: 9200
- containerPort: 9300
env:
- name: discovery.type
value: single-node
- name: xpack.security.enabled
value: "false"
- name: ES_JAVA_OPTS
value: "-Xms1g -Xmx1g"
resources:
requests:
memory: 2Gi
cpu: 500m
limits:
memory: 2Gi
cpu: "1"
volumeMounts:
- name: elasticsearch-data
mountPath: /usr/share/elasticsearch/data
volumes:
- name: elasticsearch-data
emptyDir: {}
---
apiVersion: v1
kind: Service
metadata:
name: elasticsearch
namespace: elastic
spec:
selector:
app: elasticsearch
ports:
- name: http
port: 9200
targetPort: 9200
EOF
Ця інсталяція Elasticsearch наведена виключно для експериментів і не є persistent. Скоріш за все, при впровадженні Fluent Bit, інфраструктура вже матиме свої Kibana/Elasticsearch, котрі і потрібно використовувати.
Перевіримо, чи почали писатись індекси:
$ kubectl exec -n elastic \
deploy/elasticsearch -- \
curl -s "http://localhost:9200/_cat/indices?v&h=health,status,index,docs.count,store.size"
health status index docs.count store.size
yellow open fluent-operator-2026.09.24 1 15.7kb
yellow open unknown-2026.09.24 122 141.6kb
yellow open fluent-bit-2026.09.24 229 205.3kb
$ curl "http://localhost:9200/fluent-operator-2026.09.24/_search?pretty"
{
"took" : 2,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 1,
"relation" : "eq"
},
"max_score" : 1.0,
"hits" : [
{
"_index" : "fluent-operator-2026.09.24",
"_id" : "acc14daf-1ce2-c5ce-1294-1193e236680b",
"_score" : 1.0,
"_ignored" : [
"log.keyword"
],
"_source" : {
"@timestamp" : "2026-09-24T20:05:12.131Z",
"logtag" : "F",
"log" : "2026-09-24T20:05:12Z\tINFO\tcontrollers.FluentBitConfig\tFluent Bit main configuration has updated\t{\"logging-control-plane\": \"fluent\", \"fluentbitconfig\": \"fluent-bit-config\", \"secret\": \"fluent-bit-config\", \"config-hash\": \"e90d117dfa5edf7b76252315a306e4d36ee854c94eecd17efeb3c8581ecaf648\"}",
"kubernetes" : {
"pod_name" : "fluent-operator-5f748f7457-z92dd",
"namespace_name" : "fluent",
"labels" : {
"app.kubernetes.io/component" : "operator",
"app.kubernetes.io/name" : "fluent-operator",
"pod-template-hash" : "5f748f7457",
"topology.kubernetes.io/region" : "us-east-1",
"topology.kubernetes.io/zone" : "us-east-1a"
},
"pod_ip" : "10.0.12.240",
"container_name" : "fluent-operator",
"docker_id" : "848026c4f76ac34957a36ec4d24febe36f6c8dddea1d5f058e8300744d66f526",
"container_image" : "ghcr.io/fluent/fluent-operator/fluent-operator:3.10.0"
}
}
}
]
}
}
Поле "log", тобто рядок логу, що видав сам контейнер fluent-operator, описаний як звичайна стрінга в плейнтекст форматі, і це недобре. Якщо є можливість, то варто у всіх додатках активувати логування в JSON і тоді логи будуть кориснішими.
3. HOW FLUENT OPERATOR WORKS
Fluent оператор імплементує нові кастомні ресурси для управління сетапами Fluent Bit та Fluentd. Нас більше цікавить перший, тож це наступні CRD:
- FluentBit - описує створення деймонсету та його конфігів. Для цього використовується кастомний образ ghcr.io/fluent/fluent-operator/fluent-bit
- ClusterFluentBitConfig/FluentBitConfig - ресурс, що будує фінальний конфіг із input/filter/output плагінів та зберігає його в об'єкті секрету. Відповідно кластерна та namespace-level версії
- ClusterInput - тип ресурсу, що будує Input секцію конфігу. Є лише кластерна версія
- ClusterParser/Parser - відповідає за секцію парсерів.
- ClusterMultilineParser/MultilineParser - за секцію багаторядкових парсерів.
- ClusterFilter/Filter - кластерна та namespace-level секції фільтрів
- ClusterOutput/Output - кластерна та namespace-level секції output
Взаємодію цих CRD гарно демонструє наступна схема, що представлена в офіційному репозиторії оператора:

Kind FluentBit описує, який саме ClusterFluentBitConfig вичитується, а вже останній описує селектори, за якими він підключає ClusterInput/Output/Filter об'єкти. Fluent оператор спостерігає за цими об'єктами та формує Secret, що і є репрезентацією конфігураційного файлу, парсерів та ін. Secret монтується у кожний деймонсет та вичитується додатком Fluent Bit перед запуском.
Як було вже згадано, зміни конфігурації (тобто зміни до кастомних ресурсів) не призводять до рестарту подів деймонсету, завдяки додатковому вотчеру всередині кастомного імеджу:
Саме з цієї причини версія Fluent Bit, що використовує оператор, може бути дещо старішою за версію офіційного helm-чарту.
Перевіримо на практиці, як це виглядає і почнемо із FluentBit:
$ kubectl get FluentBit -n fluent
NAME AGE
fluent-bit 4h31m
$ kubectl get FluentBit fluent-bit -n fluent -o yaml
apiVersion: fluentbit.fluent.io/v1alpha2
kind: FluentBit
metadata:
annotations:
meta.helm.sh/release-name: fluent-operator
meta.helm.sh/release-namespace: fluent
creationTimestamp: "2026-09-24T14:16:34Z"
finalizers:
- fluentbit.fluent.io
generation: 2
labels:
app.kubernetes.io/managed-by: Helm
app.kubernetes.io/name: fluent-bit
name: fluent-bit
namespace: fluent
resourceVersion: "2151"
uid: 181dbc78-9616-4465-985f-5db3592f938e
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: node-role.kubernetes.io/edge
operator: DoesNotExist
fluentBitConfigName: fluent-bit-config
image: ghcr.io/fluent/fluent-operator/fluent-bit:5.1.0
livenessProbe:
failureThreshold: 8
httpGet:
path: /
port: 2020
initialDelaySeconds: 10
periodSeconds: 10
successThreshold: 1
timeoutSeconds: 15
namespaceFluentBitCfgSelector: {}
positionDB:
hostPath:
path: /var/lib/fluent-bit/
rbacRules:
- apiGroups:
- ""
resources:
- events
verbs:
- list
resources:
limits:
cpu: "1"
memory: 1000Mi
requests:
cpu: 100m
memory: 250Mi
service: {}
tolerations:
- operator: Exists
status: {}
ClusterFluentBitConfig описує, які саме об'єкти filter/input/тощо, через selector логіку, він буде збирати в єдиний конфігураційний файл (classic формату) та секцію [SERVICE], що стосується загальної конфігурації демону:
$ kubectl get ClusterFluentBitConfig fluent-bit-config -n fluent -o yaml
apiVersion: fluentbit.fluent.io/v1alpha2
kind: ClusterFluentBitConfig
metadata:
...
labels:
app.kubernetes.io/managed-by: Helm
app.kubernetes.io/name: fluent-bit
name: fluent-bit-config
...
spec:
configFileFormat: classic
filterSelector:
matchLabels:
fluentbit.fluent.io/enabled: "true"
inputSelector:
matchLabels:
fluentbit.fluent.io/enabled: "true"
multilineParserSelector:
matchLabels:
fluentbit.fluent.io/enabled: "true"
outputSelector:
matchLabels:
fluentbit.fluent.io/enabled: "true"
parserSelector:
matchLabels:
fluentbit.fluent.io/enabled: "true"
service:
httpServer: true
parsersFile: /fluent-bit/etc/parsers.conf
LogLevel: info
LogFormat: json
Хелм чарт також створив 2 ClusterInput-а (завдяки опції "Kubernetes: true"):
$ kubectl get ClusterInput -n fluent
NAME AGE
containerd 4h24m
tail 4h24m
Перший containerd, котрий вичитує systemd логи демонів containerd та kubelet і вішає тег service. А другий - tail, котрий читає всі логи /var/log/containers/*.log і ставить мітку kube. Далі буде більш якісно пояснено логіку роботи конфігураційного файлу, цього разу просто поглянемо на його загальний вигляд:
$ kubectl get ClusterInput -n fluent -o yaml
apiVersion: v1
items:
- apiVersion: fluentbit.fluent.io/v1alpha2
kind: ClusterInput
metadata:
...
labels:
...
fluentbit.fluent.io/enabled: "true"
name: containerd
...
spec:
systemd:
db: /fluent-bit/tail/systemd.db
dbSync: Normal
path: /var/log/journal
storageType: memory
stripUnderscores: "off"
systemdFilter:
- _SYSTEMD_UNIT=containerd.service
- _SYSTEMD_UNIT=kubelet.service
tag: service.*
- apiVersion: fluentbit.fluent.io/v1alpha2
kind: ClusterInput
metadata:
...
labels:
...
fluentbit.fluent.io/enabled: "true"
name: tail
...
spec:
tail:
db: /fluent-bit/tail/pos.db
dbSync: Normal
memBufLimit: 100MB
parser: cri
path: /var/log/containers/*.log
readFromHead: false
refreshIntervalSeconds: 10
skipEmptyLines: true
skipLongLines: true
storageType: memory
tag: kube.*
kind: List
...
Об'єкти ClusterFilter відповідають за 3 секції фільтрів (їх може бути декілька в загальному конфігураційному файлі):
$ kubectl get ClusterFilter -n fluent
NAME AGE
containerd 4h26m
kubernetes 4h26m
systemd 4h26m
$ kubectl get ClusterFilter -n fluent -o yaml
apiVersion: v1
items:
- apiVersion: fluentbit.fluent.io/v1alpha2
kind: ClusterFilter
metadata:
...
labels:
...
fluentbit.fluent.io/enabled: "true"
name: containerd
...
spec:
filters:
- lua:
call: containerd
script:
key: containerd.lua
name: fluent-bit-containerd-config
timeAsTable: true
match: kube.*
- apiVersion: fluentbit.fluent.io/v1alpha2
kind: ClusterFilter
metadata:
generation: 1
labels:
...
fluentbit.fluent.io/enabled: "true"
name: kubernetes
...
spec:
filters:
- kubernetes:
annotations: false
k8sLoggingExclude: true
keepLog: false
mergeLog: true
- nest:
addPrefix: kubernetes_
nestedUnder: kubernetes
operation: lift
- modify:
rules:
- remove: stream
- remove: time
- remove: kubernetes_pod_id
- remove: kubernetes_host
- remove: kubernetes_container_hash
- nest:
nestUnder: kubernetes
operation: nest
removePrefix: kubernetes_
wildcard:
- kubernetes_*
match: kube.*
- apiVersion: fluentbit.fluent.io/v1alpha2
kind: ClusterFilter
metadata:
...
labels:
...
fluentbit.fluent.io/enabled: "true"
name: systemd
...
spec:
filters:
- lua:
call: add_time
script:
key: systemd.lua
name: fluent-bit-lua
timeAsTable: true
match: service.*
kind: List
...
ClusterFilter із іменем kubernetes ми описали як окремий темплейт в Terraform, тож він його і вилив. Тепер настала черга ClusterOutput:
$ kubectl get ClusterOutput -n fluent
NAME AGE
es 4h29m
$ kubectl get ClusterOutput -n fluent -o yaml
apiVersion: v1
items:
- apiVersion: fluentbit.fluent.io/v1alpha2
kind: ClusterOutput
metadata:
...
labels:
...
fluentbit.fluent.io/enabled: "true"
name: es
...
spec:
es:
bufferSize: 1MB
compress: gzip
generateID: true
host: elasticsearch.elastic.svc
logstashFormat: true
logstashPrefix: unknown
logstashPrefixKey: kubernetes['labels']['app.kubernetes.io/name']
port: 9200
suppressTypeName: "On"
timeKey: '@timestamp'
matchRegex: (?:kube|service)\.(.*)
kind: List
...
Наразі ми використовуємо єдиний output в ElasticSearch, а індекси пишемо в logstash-форматі.
Перевіряємо фінальний конфігураційний файл, котрий зібрав для нас Fluent оператор:
$ kubectl get secrets -n fluent
NAME TYPE DATA AGE
fluent-bit-config Opaque 5 5h3m
Та перетворимо base64 в формат зручний для читання:
$ for key in $(kubectl get secret fluent-bit-config -n fluent -o json | jq -r '.data | keys[]'); do
echo "### $key ###"
kubectl get secret fluent-bit-config -n fluent -o json | jq -r ".data[\"$key\"]" | base64 --decode
echo -e "\n"
done
### containerd.lua ###
function containerd( tag, timestamp, record)
if(record["logtag"]~=nil)
then
timeStr = os.date("!*t", timestamp["sec"])
t = string.format("%4d-%02d-%02dT%02d:%02d:%02d.%sZ",
timeStr["year"], timeStr["month"], timeStr["day"],
timeStr["hour"], timeStr["min"], timeStr["sec"],
timestamp["nsec"]);
record["time"] = t;
record["log"] = record["message"];
record["message"] = nil;
return 1, timestamp, record
else
return 0,timestamp,record
end
end
### fluent-bit.conf ###
[Service]
Http_Server true
Parsers_File /fluent-bit/etc/parsers.conf
[Input]
Name systemd
Path /var/log/journal
DB /fluent-bit/tail/systemd.db
DB.Sync Normal
Tag service.*
Strip_Underscores off
storage.type memory
Systemd_Filter _SYSTEMD_UNIT=containerd.service
Systemd_Filter _SYSTEMD_UNIT=kubelet.service
[Input]
Name tail
Path /var/log/containers/*.log
Refresh_Interval 10
Skip_Long_Lines true
DB /fluent-bit/tail/pos.db
DB.Sync Normal
Mem_Buf_Limit 100MB
Parser cri
Tag kube.*
storage.type memory
Read_from_Head false
Skip_Empty_Lines true
[Filter]
Name lua
Match kube.*
script /fluent-bit/config/containerd.lua
call containerd
time_as_table true
[Filter]
Name kubernetes
Match kube.*
Merge_Log true
Keep_Log false
K8S-Logging.Exclude true
Annotations false
[Filter]
Name nest
Match kube.*
Operation lift
Nested_under kubernetes
Add_prefix kubernetes_
[Filter]
Name modify
Match kube.*
Remove stream
Remove time
Remove kubernetes_pod_id
Remove kubernetes_host
Remove kubernetes_container_hash
[Filter]
Name nest
Match kube.*
Operation nest
Wildcard kubernetes_*
Nest_under kubernetes
Remove_prefix kubernetes_
[Filter]
Name lua
Match service.*
script /fluent-bit/config/systemd.lua
call add_time
time_as_table true
[Output]
Name es
Match_Regex (?:kube|service)\.(.*)
Host elasticsearch.elastic.svc
Port 9200
Compress gzip
Buffer_Size 1MB
Logstash_Prefix unknown
Time_Key @timestamp
Logstash_Prefix_Key kubernetes['labels']['app.kubernetes.io/name']
Suppress_Type_Name On
Logstash_Format true
Generate_ID true
### parsers.conf ###
### parsers_multiline.conf ###
### systemd.lua ###
function add_time(tag, timestamp, record)
new_record = {}
timeStr = os.date("!*t", timestamp["sec"])
t = string.format("%4d-%02d-%02dT%02d:%02d:%02d.%sZ",
timeStr["year"], timeStr["month"], timeStr["day"],
timeStr["hour"], timeStr["min"], timeStr["sec"],
timestamp["nsec"])
kubernetes = {}
kubernetes["pod_name"] = record["_HOSTNAME"]
kubernetes["container_name"] = record["SYSLOG_IDENTIFIER"]
kubernetes["namespace_name"] = "kube-system"
new_record["time"] = t
new_record["log"] = record["MESSAGE"]
new_record["kubernetes"] = kubernetes
return 1, timestamp, new_record
end
Фінальний DaemonSet, власником якого і є FluentBit об'єкт/оператор:
$ kubectl get ds fluent-bit -n fluent -o yaml
apiVersion: apps/v1
kind: DaemonSet
metadata:
annotations:
meta.helm.sh/release-name: fluent-operator
meta.helm.sh/release-namespace: fluent
creationTimestamp: "2026-09-24T14:16:39Z"
generation: 1
labels:
app.kubernetes.io/managed-by: Helm
app.kubernetes.io/name: fluent-bit
name: fluent-bit
namespace: fluent
ownerReferences:
- apiVersion: fluentbit.fluent.io/v1alpha2
blockOwnerDeletion: true
controller: true
kind: FluentBit
name: fluent-bit
uid: 181dbc78-9616-4465-985f-5db3592f938e
resourceVersion: "2209"
uid: 03183a5e-ae93-4cc0-a12e-6a6956b86b1e
spec:
revisionHistoryLimit: 10
selector:
matchLabels:
app.kubernetes.io/managed-by: Helm
app.kubernetes.io/name: fluent-bit
template:
metadata:
labels:
app.kubernetes.io/managed-by: Helm
app.kubernetes.io/name: fluent-bit
name: fluent-bit
namespace: fluent
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: node-role.kubernetes.io/edge
operator: DoesNotExist
containers:
- env:
- name: NODE_NAME
valueFrom:
fieldRef:
apiVersion: v1
fieldPath: spec.nodeName
- name: HOST_IP
valueFrom:
fieldRef:
apiVersion: v1
fieldPath: status.hostIP
image: ghcr.io/fluent/fluent-operator/fluent-bit:5.1.0
imagePullPolicy: IfNotPresent
livenessProbe:
failureThreshold: 8
httpGet:
path: /
port: 2020
scheme: HTTP
initialDelaySeconds: 10
periodSeconds: 10
successThreshold: 1
timeoutSeconds: 15
name: fluent-bit
ports:
- containerPort: 2020
name: metrics
protocol: TCP
resources:
limits:
cpu: "1"
memory: 1000Mi
requests:
cpu: 100m
memory: 250Mi
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
volumeMounts:
- mountPath: /fluent-bit/config
name: config
readOnly: true
- mountPath: /var/log/containers
mountPropagation: None
name: varlibcontainers
readOnly: true
- mountPath: /var/log/
mountPropagation: None
name: varlogs
readOnly: true
- mountPath: /var/log/journal
mountPropagation: None
name: systemd
readOnly: true
- mountPath: /fluent-bit/tail
name: positions
dnsPolicy: ClusterFirst
restartPolicy: Always
schedulerName: default-scheduler
securityContext: {}
serviceAccount: fluent-bit
serviceAccountName: fluent-bit
terminationGracePeriodSeconds: 30
tolerations:
- operator: Exists
volumes:
- name: config
secret:
defaultMode: 420
secretName: fluent-bit-config
- hostPath:
path: /var/log/containers
type: ""
name: varlibcontainers
- hostPath:
path: /var/log
type: ""
name: varlogs
- hostPath:
path: /var/log/journal
type: ""
name: systemd
- hostPath:
path: /var/lib/fluent-bit/
type: ""
name: positions
updateStrategy:
rollingUpdate:
maxSurge: 0
maxUnavailable: 1
type: RollingUpdate
Тут можна пересвідчитись, що fluent-bit-config справді монтується в кожен под за адресою /fluent-bit/config, також прокидаються всередину всі логи /var/log/containers, journal (systemd), файли позицій тощо.
4. HOW FLUENT BIT CONFIG WORKS
Отже зупинимось на фінальному конфігураційному файлі fluent-bit.conf. Дублювати його не буду, адже завжди за його повним лістингом можна звернутись "нагору".
Отже перша секція [Service]:
[Service]
Http_Server true
Parsers_File /fluent-bit/etc/parsers.conf
Http_Server активує prometheus-like метрики, Parsers_File вказує адресу розміщення конфігу із парсерами.
Секцій Input може бути багато, у нас лише дві. Перша стосується Systemd:
[Input]
Name systemd
Path /var/log/journal
DB /fluent-bit/tail/systemd.db
DB.Sync Normal
Tag service.*
Strip_Underscores off
storage.type memory
Systemd_Filter _SYSTEMD_UNIT=containerd.service
Systemd_Filter _SYSTEMD_UNIT=kubelet.service
SQLite база /fluent-bit/tail/systemd.db потрібна для збереження позиції логів, що вже були записані (тобто зміщення/offset). Це необхідно для зменшення ймовірності втрати логів у разі перевантаження демона чи його аварійного падіння.
Цей input тегається міткою service.*, котру використовує Fluent Bit для роутингу логів. Зірочка в кінці має спеціальне значення для плагіну: до тегів буде додано значення _SYSTEMD_UNIT.
Systemd_Filter вказує, що нас цікавлять лише логи containerd та kubelet сервісів. А storage.type активує буферизацію в оперативній пам'яті.
Основний tail input:
[Input]
Name tail
Path /var/log/containers/*.log
Refresh_Interval 10
Skip_Long_Lines true
DB /fluent-bit/tail/pos.db
DB.Sync Normal
Mem_Buf_Limit 100MB
Parser cri
Tag kube.*
storage.type memory
Read_from_Head false
Skip_Empty_Lines true
Вичитуємо всі логи контейнерів /var/log/containers/*.log та із інтервалом в 10 секунд ведемо пошук нових доступних файлів. Для парсингу цих логів буде використаний парсер cri. Як і у попередньому разі виставляється обов'язковий тег, по якому і буде відбуватись подальший роутинг повідомлень. Зірочка в кінці вказує на те, що вона буде замінена на повну адресу файла (наприклад /var/log/containers/apache-logs-annotated_default_apache-aeeccc7a9f00f6e4e066aeff0434cf80621215071f1b20a51e8340aa7c35eac6.log), а слеші будуть замінені на крапки (тобто результат буде наступним - kube.var.log.containers.apache-logs-annotated_default_apache-aeeccc7a9f00f6e4e066aeff0434cf80621215071f1b20a51e8340aa7c35eac6.log). Це робиться задля того, щоб додати більше інформації до таких логів, адже наприклад файли логів в Kubernetes створюються із додаванням назви пода, неймспейсу та контейнера. Тобто це вже корисні дані, котрі не хотілось би втрачати.
Cri парсер вже описаний в дефолтних конфігураційних файлах і він допомагає розкласти рядок логу на поля.
Наступний фільтр - lua (він також описаний в секреті fluent-bit-config). Цей фільтр дозволяє робити постобробку логів за допомогою Lua скриптів (додавати нові поля, прибирати їх, додавати вкладеність тощо):
[Filter]
Name lua
Match kube.*
script /fluent-bit/config/containerd.lua
call containerd
time_as_table true
У нашому випадку він (вміст containerd.lua наведений вище, як результат вичитки k8s секрету) стандартизує мітку часу та перейменовує (насправді копіє значення в новий ключ, а старий видаляє) поле log в message. Це за умови, що в івенті присутній ключ "logtag".
Наступний фільтр - kubernetes. Завдяки "Match kube.*" він візьме логи від tail input плагіна та збагатить їх додатковими ключами, такими як лейбли подів, неймспейси тощо.
[Filter]
Name kubernetes
Match kube.*
Merge_Log true
Keep_Log false
K8S-Logging.Exclude true
Annotations false
Отримавши значення routing тегу від tail, наприклад kube.var.log.containers.apache-logs-annotated_default_apache-aeeccc7a9f00f6e4e066aeff0434cf80621215071f1b20a51e8340aa7c35eac6.log, його буде вичитано і перетворено в apache-logs-annotated_default_apache-aeeccc7a9f00f6e4e066aeff0434cf80621215071f1b20a51e8340aa7c35eac6.log, завдяки дефолтній опції kube_tag_prefix, що має значення "kube.var.log.containers". Після цього, використовуючи регулярний вираз "(?<pod_name>[a-z0-9](?:[-a-z0-9]*[a-z0-9])?(?:\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*)_(?<namespace_name>[^_]+)_(?<container_name>.+)-(?<docker_id>[a-z0-9]{64})\.log$" (цей regex прописаний в коді, проте його можна перевизначити в кастомному парсері) https://rubular.com/r/HZz3tYAahj6JCd, назва файлу буде перетворена в корисні ключі до event-у Fluent Bit: а саме назву пода, неймспейсу, назву контейнера та id контейнера.
Якщо в основному пейлоаді, тобто ключі log, знаходиться JSON (додаток одразу пише логи в JSON), то опція "Merge_Log true" дозволить додати ці поля в структуру самого івенту/логу.
"Keep_Log false" видалить оригінальне поле log, якщо опція Merge_Log активована і log справді був JSON-ом.
"K8S-Logging.Exclude true" дозволяє вимикати логування для окремих подів на рівні їх анотацій. Для деяких не-prod середовищ це може бути корисним. https://docs.fluentbit.io/manual/data-pipeline/filters/kubernetes#kubernetes-pod-annotations
"Annotations false" заборонить додавати значення анотацій подів як додаткові поля. Це зменшить обсяг даних для відправки та збереження.
Три наступні фільтри розглянемо як одне ціле, адже вони покликані вирішити єдину задачу - видалити непотрібні поля:
[Filter]
Name nest
Match kube.*
Operation lift
Nested_under kubernetes
Add_prefix kubernetes_
[Filter]
Name modify
Match kube.*
Remove stream
Remove time
Remove kubernetes_pod_id
Remove kubernetes_host
Remove kubernetes_container_hash
[Filter]
Name nest
Match kube.*
Operation nest
Wildcard kubernetes_*
Nest_under kubernetes
Remove_prefix kubernetes_
Все виглядає доволі складно, адже фільтр modify не вміє видаляти nested поля. Тому фільтр nest спочатку "піднімає" нагору все, що знаходиться у вкладеній структурі kubernetes (опція Nested_under):
...
"kubernetes" : {
"pod_name" : "fluent-operator-5f748f7457-z92dd",
"namespace_name" : "fluent",
"labels" : {
"app.kubernetes.io/component" : "operator",
"app.kubernetes.io/name" : "fluent-operator",
"pod-template-hash" : "5f748f7457",
"topology.kubernetes.io/region" : "us-east-1",
"topology.kubernetes.io/zone" : "us-east-1a"
},
"pod_ip" : "10.0.12.240",
"pod_id" : "848026c4f76ac34957a36ec4d24febe36f6c8dddea1d5f058e8300744d66f526",
"host" : "host.example.com",
"container_hash": "a36ec4d24febe36f6c8dddea1d5f058",
"container_name" : "fluent-operator",
"docker_id" : "848026c4f76ac34957a36ec4d24febe36f6c8dddea1d5f058e8300744d66f526",
"container_image" : "ghcr.io/fluent/fluent-operator/fluent-operator:3.10.0"
}
...
І результат виходить наступний (додається kubernetes_ до ключів, адже це значення опції Add_prefix):
...
"kubernetes_pod_name" : "fluent-operator-5f748f7457-z92dd",
"kubernetes_namespace_name" : "fluent",
"kubernetes_labels" : {
"app.kubernetes.io/component" : "operator",
"app.kubernetes.io/name" : "fluent-operator",
"pod-template-hash" : "5f748f7457",
"topology.kubernetes.io/region" : "us-east-1",
"topology.kubernetes.io/zone" : "us-east-1a"
},
"kubernetes_pod_ip" : "10.0.12.240",
"kubernetes_pod_id" : "848026c4f76ac34957a36ec4d24febe36f6c8dddea1d5f058e8300744d66f526",
"kubernetes_host" : "host.example.com",
"kubernetes_container_hash": "a36ec4d24febe36f6c8dddea1d5f058",
"kubernetes_container_name" : "fluent-operator",
"kubernetes_docker_id" : "848026c4f76ac34957a36ec4d24febe36f6c8dddea1d5f058e8300744d66f526",
"kubernetes_container_image" : "ghcr.io/fluent/fluent-operator/fluent-operator:3.10.0"
...
Потім modify видаляє непотрібні поля (див. оригінальний конфіг, які саме поля), а після цього nest повертає оригінальну мапу без зайвих полів:
...
"kubernetes" : {
"pod_name" : "fluent-operator-5f748f7457-z92dd",
"namespace_name" : "fluent",
"labels" : {
"app.kubernetes.io/component" : "operator",
"app.kubernetes.io/name" : "fluent-operator",
"pod-template-hash" : "5f748f7457",
"topology.kubernetes.io/region" : "us-east-1",
"topology.kubernetes.io/zone" : "us-east-1a"
},
"pod_ip" : "10.0.12.240",
"container_name" : "fluent-operator",
"docker_id" : "848026c4f76ac34957a36ec4d24febe36f6c8dddea1d5f058e8300744d66f526",
"container_image" : "ghcr.io/fluent/fluent-operator/fluent-operator:3.10.0"
}
...
Далі йде знову lua фільтр, але вже для systemd логів:
[Filter]
Name lua
Match service.*
script /fluent-bit/config/systemd.lua
call add_time
time_as_table true
Він знову ж стандартизує мітку часу та збагачує логи додатковими полями, що стосуються Кубернетіса.
І остання секція - Output, де ми відправляємо результат у віддалену базу Elasticsearch:
[Output]
Name es
Match_Regex (?:kube|service)\.(.*)
Host elasticsearch.elastic.svc
Port 9200
Compress gzip
Buffer_Size 1MB
Logstash_Prefix unknown
Time_Key @timestamp
Logstash_Prefix_Key kubernetes['labels']['app.kubernetes.io/name']
Suppress_Type_Name On
Logstash_Format true
Generate_ID true
Завдяки Match_Regex ми зібрали всі логи під тегами kube та service та відправили їх до elasticsearch.elastic.svc. Назва індексу буде визначатись динамічно за лейблом kubernetes['labels']['app.kubernetes.io/name'], проте якщо його немає - то буде проставлятись unknown. Всі індекси будуть закінчуватись міткою часу, тож загальна практика роботи із ними - це періодично видаляти старі. Десь так будуть виглядати індекси:
$ kubectl exec -n elastic \
deploy/elasticsearch -- \
curl -s "http://localhost:9200/_cat/indices?v&h=health,status,index,docs.count,store.size"
health status index docs.count store.size
yellow open fluent-operator-2026.09.24 1 15.7kb
yellow open unknown-2026.09.24 122 141.6kb
yellow open fluent-bit-2026.09.24 229 205.3kb
Ця модель запису може бути незручною, адже генерує велику кількість індексів, що часто є проблемою, якщо є бажання зберігати індекси впродовж довгого часу. Тому є варіант, хоч і дещо не елегантний, зберігати логи кожного додатку в окремому індексі, проте без мітки часу. А потім вже накрутити окрему полісі Elasticsearch, котра буде підчищати цей індекс (або datastream).
...
logstashDateFormat: eks
logstashFormat: true
logstashPrefix: unknown
logstashPrefixKey: kubernetes['labels']['app.kubernetes.io/name']
...
Тобто logstashDateFormat буде фіксований постійно. Або ж ставити лише місяць чи рік цією опцією.
5. FLUENT BIT ADVANCED TIPS
Скажімо, з'явилась необхідність відправляти деякі логи в інше середовище, не порушуючи роботу загальної схеми. Наприклад, ми хочемо, щоб логи Fluent Bit подів відправлялись до AWS S3. Для цього необхідні інший Fluent Bit output plugin та rewrite_tag filter.
Щоб авторизуватись в AWS S3, скористаємось додатковою IAM роллю та прив'яжемо її до Fluent Bit із pod identity. Для цього підправимо тераформ код:
$ cat main.tf
...
module "s3_bucket" {
source = "terraform-aws-modules/s3-bucket/aws"
version = "5.16.1"
bucket = "fluentbit-logs-2026"
versioning = {
enabled = false
}
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
data "aws_iam_policy_document" "this" {
statement {
sid = "S3Write"
effect = "Allow"
actions = [
"s3:PutObject",
"s3:GetObject",
"s3:DeleteObject"
]
resources = [
"${module.s3_bucket.s3_bucket_arn}/*"
]
}
}
module "custom_pod_identity" {
source = "terraform-aws-modules/eks-pod-identity/aws"
version = "2.9.0"
name = "eks-fluentbit-${data.terraform_remote_state.eks.outputs.cluster_name}-${var.region}"
use_name_prefix = false
attach_custom_policy = true
source_policy_documents = [
data.aws_iam_policy_document.this.json
]
associations = {
one = {
cluster_name = data.terraform_remote_state.eks.outputs.cluster_name
namespace = var.namespace
service_account = "fluent-bit"
}
}
}
Тут ми створили новий бакет fluentbit-logs-2026 (не забуваймо, що ім'я бакета унікальне), створили роль на запис в нього та асоціювали цю роль із сервіс акаунтом та іменем неймспейсу. Увесь новий код буде доступний за новим тегом.
Окрім цього було створено новий фільтр із rewriteTag плагіном:
$ cat templates/manifests/cluster-filter.yaml.tpl
...
- rewriteTag:
rules:
- $kubernetes['labels']['app.kubernetes.io/name'] ^fluent-bit$ fluentbit.$TAG true
emitterName: fluentbit
emitterMemBufLimit: 64M
Цей фільтр дозволяє зробити новий тег на деякі рекорди, що задовольняють певні умови. Про них трошки пізніше.
Правила (секція rules) мають наступний формат:
$KEY REGEX NEW_TAG KEEP
$KEY - ключ, значення якого перевіряють
REGEX - саме значення. Якщо бути точнішим, регулярка, що задовольняє всі потрібні значення
NEW_TAG - відповідно новий routing тег, під яким будуть опубліковані записи, що задовольняють попередній вираз
KEEP - зберігати чи ні оригінальне значення із попереднім тегом. Наприклад, у нас раніше на цих записах стояв тег kube.*
Тож у нас буде перевірка значення лейбла поду $kubernetes['labels']['app.kubernetes.io/name'] і якщо воно має значення fluent-bit (потраплятиме під регулярний вираз ^fluent-bit$), то воно буде затегано під новим тегом fluentbit.$TAG. Проте старий тег kube.* ми не будемо видаляти, тож ті ж значення будуть присутні і в ElasticSearch, і в AWS S3.
І, нарешті, S3 output:
$ cat templates/manifests/cluster-output-s3.yaml.tpl
---
apiVersion: fluentbit.fluent.io/v1alpha2
kind: ClusterOutput
metadata:
name: s3-fluent-bit
labels:
app.kubernetes.io/managed-by: Terraform
fluentbit.fluent.io/component: logging
fluentbit.fluent.io/enabled: 'true'
spec:
match: fluentbit.*
s3:
Region: us-east-1
Bucket: fluentbit-logs-2026
S3KeyFormat: /%Y-%m-%d-%H_$UUID.log
ContentType: text/plain
TotalFileSize: 45M
UploadTimeout: 1m
UsePutObject: true
StoreDir: /fluentbit/buffer/s3
StoreDirLimitSize: 2G
JsonDateKey: '@timestamp'
JsonDateFormat: iso8601
PreserveDataOrdering: true
RetryLimit: 5
Застосуємо всі зміни та зачекаємо декілька хвилин:
$ cd terraform-aws-example
$ git pull && git checkout v1.16.5 && git pull
$ cd eks-infra/addons/fluentbit
$ terraform init && terraform apply --auto-approve
Переглянемо новостворені записи:
$ aws s3 ls s3://fluentbit-logs-2026
2026-10-01 22:21:34 5778 2026-10-01-19_0HsQr56b.log
2026-10-01 22:18:14 52278 2026-10-01-19_HHblKQhA.log
2026-10-01 22:20:26 6518 2026-10-01-19_M2sTlPOk.log
2026-10-01 22:18:12 51561 2026-10-01-19_S2syAYdp.log
2026-10-01 22:19:16 25037 2026-10-01-19_Wr5U9Hfb.log
2026-10-01 22:21:36 5786 2026-10-01-19_Xq6muBEW.log
2026-10-01 22:19:16 21086 2026-10-01-19_a6r9lLvT.log
2026-10-01 22:20:24 5778 2026-10-01-19_hFAEdtHl.log
2026-10-01 22:19:14 23433 2026-10-01-19_kC6X1egH.log
$ aws s3 cp s3://fluentbit-logs-2026/2026-10-01-19_HHblKQhA.log -
{"@timestamp":"2026-10-01T19:17:16.770212Z","logtag":"F","log":"[2026/10/01 19:17:16.770] [ info] [reload] stop everything of the old context","kubernetes":{"pod_name":"fluent-bit-gkzvb","namespace_name":"fluent","labels":{"app.kubernetes.io/managed-by":"Helm","app.kubernetes.io/name":"fluent-bit","controller-revision-hash":"7f6dbb8d7c","eks.amazonaws.com/pod-identity":"enabled","pod-template-generation":"1","topology.kubernetes.io/region":"us-east-1","topology.kubernetes.io/zone":"us-east-1b"},"pod_ip":"10.0.21.21","container_name":"fluent-bit","docker_id":"ee09f657553234f4478b216093324ca8eaef23dac76b0cd9fa785c3b69a1918d","container_image":"ghcr.io/fluent/fluent-operator/fluent-bit:5.1.0"}}
...
Все виглядає правильно.
Посилання:
https://github.com/ipeacocks/terraform-aws-example
https://docs.fluentbit.io/manual
https://github.com/fluent/fluent-operator


Немає коментарів:
Дописати коментар