Kubernetes ortamında yüzlerce pod çalıştırıyorsunuz ve bir sorun çıktığında logları bulmak için her pod’a tek tek kubectl logs mı atıyorsunuz? Bu yaklaşım küçük ortamlarda bile can sıkıcı, production ortamında ise neredeyse imkansız hale geliyor. İşte tam bu noktada EFK Stack devreye giriyor: Elasticsearch, Fluentd ve Kibana üçlüsü, dağıtık sistemlerdeki logları merkezi bir noktada toplayıp analiz edilebilir hale getiriyor.
Bu yazıda, gerçek bir Kubernetes ortamında EFK Stack’i sıfırdan kurarak merkezi log yönetimini nasıl yapılandıracağınızı adım adım anlatacağım. Sadece teorik değil, production’da karşılaşabileceğiniz durumları da ele alacağız.
EFK Stack Nedir ve Neden ELK Değil?
Pek çok sysadmin “ELK Stack kullanmıyor musunuz?” diye soruyor. ELK Stack’teki L, Logstash‘ı temsil ediyor. Logstash güçlü bir araç ama Kubernetes ortamı için biraz hantal kalıyor. Her node üzerinde DaemonSet olarak çalışacak bir log toplayıcı için Logstash’ın kaynak tüketimi oldukça yüksek.
Fluentd ise bu iş için özel olarak optimize edilmiş, çok daha hafif bir alternatif. CNCF (Cloud Native Computing Foundation) tarafından destekleniyor ve Kubernetes ile mükemmel entegre çalışıyor. Üstelik Fluentd’nin Kubernetes metadata’sını otomatik olarak zenginleştiren pluginleri, log yönetimini çok daha anlamlı hale getiriyor.
Bileşenleri kısaca özetleyecek olursam:
- Elasticsearch: Logları indexleyen ve saklayan dağıtık arama motoru
- Fluentd: Her Kubernetes node’unda çalışıp logları toplayıp Elasticsearch’e gönderen ajan
- Kibana: Elasticsearch üzerindeki verileri görselleştiren web arayüzü
Ön Hazırlık: Ortam Gereksinimleri
Kuruluma geçmeden önce Kubernetes cluster’ınızın bazı gereksinimleri karşılaması gerekiyor. Production ortamı için minimum şartlar şunlar:
- Worker Node Bellek: Her node için en az 4GB RAM (Elasticsearch oldukça bellek yoğun)
- Disk Alanı: Log saklama süresine bağlı, ancak başlangıç için node başına 50GB öneriyorum
- Kubernetes Versiyonu: 1.19 ve üzeri
- Helm: 3.x versiyonu kurulu olmalı
Ayrıca vm.max_map_count değerinin Elasticsearch için yeterince yüksek olması şart. Bu değeri her node üzerinde ayarlamazsanız Elasticsearch pod’ları sürekli CrashLoopBackOff durumuna düşer.
# Her Kubernetes worker node üzerinde çalıştırın
sudo sysctl -w vm.max_map_count=262144
# Kalıcı hale getirmek için
echo "vm.max_map_count=262144" | sudo tee -a /etc/sysctl.conf
sudo sysctl -p
Bu adımı atlamak en sık yapılan hatalardan biri. Elasticsearch loglarında max virtual memory areas vm.max_map_count [65530] is too low hatası görüyorsanız sorun burada.
Namespace ve Temel Yapılandırma
EFK bileşenlerini kendi namespace’inde izole etmek, yönetimi kolaylaştırıyor. Ayrıca RBAC kurallarını daha temiz tutmanızı sağlıyor.
# EFK için ayrı namespace oluşturuyoruz
kubectl create namespace logging
# Namespace'i doğrulayalım
kubectl get namespace logging
Elasticsearch Kurulumu
Elasticsearch’ü Kubernetes’e deploy etmenin birkaç yolu var. Helm chart kullanmak en pratik yol, ancak ne yaptığınızı anlamak için önce temel YAML manifesto yapısını görelim.
# elasticsearch-deployment.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: elasticsearch
namespace: logging
spec:
serviceName: elasticsearch
replicas: 3
selector:
matchLabels:
app: elasticsearch
template:
metadata:
labels:
app: elasticsearch
spec:
initContainers:
- name: fix-permissions
image: busybox
command: ["sh", "-c", "chown -R 1000:1000 /usr/share/elasticsearch/data"]
volumeMounts:
- name: data
mountPath: /usr/share/elasticsearch/data
- name: increase-vm-max-map
image: busybox
command: ["sysctl", "-w", "vm.max_map_count=262144"]
securityContext:
privileged: true
containers:
- name: elasticsearch
image: docker.elastic.co/elasticsearch/elasticsearch:8.11.0
resources:
limits:
cpu: "1"
memory: 2Gi
requests:
cpu: "500m"
memory: 1Gi
env:
- name: cluster.name
value: k8s-logs
- name: node.name
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: discovery.seed_hosts
value: "elasticsearch-0.elasticsearch,elasticsearch-1.elasticsearch,elasticsearch-2.elasticsearch"
- name: cluster.initial_master_nodes
value: "elasticsearch-0,elasticsearch-1,elasticsearch-2"
- name: ES_JAVA_OPTS
value: "-Xms512m -Xmx512m"
- name: xpack.security.enabled
value: "false"
ports:
- containerPort: 9200
name: rest
- containerPort: 9300
name: inter-node
volumeMounts:
- name: data
mountPath: /usr/share/elasticsearch/data
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: standard
resources:
requests:
storage: 30Gi
---
apiVersion: v1
kind: Service
metadata:
name: elasticsearch
namespace: logging
spec:
selector:
app: elasticsearch
clusterIP: None
ports:
- port: 9200
name: rest
- port: 9300
name: inter-node
Bu yapılandırmada birkaç kritik detay var. initContainers bölümündeki fix-permissions container’ı, persistent volume’un doğru sahipliğini ayarlıyor. increase-vm-max-map ise az önce bahsettiğimiz vm.max_map_count değerini pod seviyesinde de ayarlıyor. Her iki init container da olmadan Elasticsearch çalışmayabilir.
Elasticsearch’ü deploy edelim ve hazır olmasını bekleyelim:
kubectl apply -f elasticsearch-deployment.yaml
# Pod'ların durumunu izleyelim
kubectl get pods -n logging -w
# Elasticsearch'ün ayağa kalkıp kalkmadığını kontrol edelim
kubectl port-forward -n logging svc/elasticsearch 9200:9200 &
curl http://localhost:9200/_cluster/health?pretty
Cluster health “green” veya en azından “yellow” olmalı. Single node kurulumda “yellow” normal çünkü replica shard’lar atanamıyor. Üç node’lu kurulumda “green” bekliyoruz.
Fluentd DaemonSet Kurulumu
Fluentd’nin asıl gücü, Kubernetes DaemonSet olarak çalışmasından geliyor. Her node üzerinde bir Fluentd pod’u çalışıyor ve o node’daki tüm container loglarını topluyor. Bu mimari sayesinde yeni node eklendiğinde otomatik olarak log toplama başlıyor.
Önce Fluentd’nin Elasticsearch’e yazabilmesi için RBAC izinlerini ayarlayalım:
# fluentd-rbac.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: fluentd
namespace: logging
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: fluentd
rules:
- apiGroups:
- ""
resources:
- pods
- namespaces
verbs:
- get
- list
- watch
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: fluentd
roleRef:
kind: ClusterRole
name: fluentd
apiGroup: rbac.authorization.k8s.io
subjects:
- kind: ServiceAccount
name: fluentd
namespace: logging
Şimdi Fluentd’nin konfigürasyonunu bir ConfigMap olarak oluşturalım. Bu en kritik adım çünkü log toplama ve işleme kurallarını burada tanımlıyoruz:
# fluentd-configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: fluentd-config
namespace: logging
data:
fluent.conf: |
# Kubernetes container loglarini topla
<source>
@type tail
path /var/log/containers/*.log
pos_file /var/log/fluentd-containers.log.pos
tag kubernetes.*
read_from_head true
<parse>
@type multi_format
<pattern>
format json
time_key time
time_format %Y-%m-%dT%H:%M:%S.%NZ
</pattern>
<pattern>
format /^(?<time>.+) (?<stream>stdout|stderr) [^ ]* (?<log>.*)$/
time_format %Y-%m-%dT%H:%M:%S.%N%:z
</pattern>
</parse>
</source>
# Kubernetes metadata ekle
<filter kubernetes.**>
@type kubernetes_metadata
@id filter_kube_metadata
kubernetes_url "#{ENV['FLUENT_FILTER_KUBERNETES_URL'] || 'https://' + ENV.fetch('KUBERNETES_SERVICE_HOST') + ':' + ENV.fetch('KUBERNETES_SERVICE_PORT') + '/api'}"
verify_ssl "#{ENV['KUBERNETES_VERIFY_SSL'] || true}"
ca_file "#{ENV['KUBERNETES_CA_FILE']}"
skip_labels false
skip_container_metadata false
skip_master_url false
skip_namespace_metadata false
</filter>
# Sistem namespace'lerini filtrele (opsiyonel)
<match kubernetes.var.log.containers.**kube-system**.log>
@type null
</match>
# Elasticsearch'e gonder
<match kubernetes.**>
@type elasticsearch
host "#{ENV['FLUENT_ELASTICSEARCH_HOST']}"
port "#{ENV['FLUENT_ELASTICSEARCH_PORT']}"
logstash_format true
logstash_prefix k8s-logs
include_tag_key true
tag_key @log_name
<buffer>
@type file
path /var/log/fluentd-buffers/kubernetes.system.buffer
flush_mode interval
retry_type exponential_backoff
flush_thread_count 2
flush_interval 5s
retry_forever
retry_max_interval 30
chunk_limit_size 2M
queue_limit_length 8
overflow_action block
</buffer>
</match>
Bu ConfigMap’te özellikle bölümüne dikkat edin. Elasticsearch geçici olarak erişilemez hale geldiğinde Fluentd logları buffer’da tutuyor ve bağlantı geldiğinde gönderiyor. retry_forever true ayarı sayesinde log kaybı yaşamıyorsunuz.
# fluentd-daemonset.yaml
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: fluentd
namespace: logging
labels:
app: fluentd
spec:
selector:
matchLabels:
app: fluentd
template:
metadata:
labels:
app: fluentd
spec:
serviceAccountName: fluentd
tolerations:
- key: node-role.kubernetes.io/master
effect: NoSchedule
containers:
- name: fluentd
image: fluent/fluentd-kubernetes-daemonset:v1-debian-elasticsearch
env:
- name: FLUENT_ELASTICSEARCH_HOST
value: "elasticsearch.logging.svc.cluster.local"
- name: FLUENT_ELASTICSEARCH_PORT
value: "9200"
- name: FLUENT_ELASTICSEARCH_SCHEME
value: "http"
resources:
limits:
memory: 512Mi
requests:
cpu: 100m
memory: 200Mi
volumeMounts:
- name: varlog
mountPath: /var/log
- name: varlibdockercontainers
mountPath: /var/lib/docker/containers
readOnly: true
- name: fluentd-config
mountPath: /fluentd/etc
volumes:
- name: varlog
hostPath:
path: /var/log
- name: varlibdockercontainers
hostPath:
path: /var/lib/docker/containers
- name: fluentd-config
configMap:
name: fluentd-config
tolerations bölümü önemli. Master node’lar genellikle taint’lenmiş olduğundan, Fluentd’nin master node loglarını da toplaması için bu tolerasyon gerekiyor.
Kibana Kurulumu
Kibana kurulumu en basit adım. Sadece Elasticsearch’e bağlanıp verileri görselleştiren bir web uygulaması:
# kibana-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: kibana
namespace: logging
spec:
replicas: 1
selector:
matchLabels:
app: kibana
template:
metadata:
labels:
app: kibana
spec:
containers:
- name: kibana
image: docker.elastic.co/kibana/kibana:8.11.0
resources:
limits:
cpu: "1"
memory: 1Gi
requests:
cpu: "500m"
memory: 512Mi
env:
- name: ELASTICSEARCH_HOSTS
value: http://elasticsearch.logging.svc.cluster.local:9200
ports:
- containerPort: 5601
---
apiVersion: v1
kind: Service
metadata:
name: kibana
namespace: logging
spec:
selector:
app: kibana
type: ClusterIP
ports:
- port: 5601
targetPort: 5601
kubectl apply -f fluentd-rbac.yaml
kubectl apply -f fluentd-configmap.yaml
kubectl apply -f fluentd-daemonset.yaml
kubectl apply -f kibana-deployment.yaml
# Tum bileşenleri kontrol edelim
kubectl get all -n logging
# Kibana'ya erişim için port-forward
kubectl port-forward -n logging svc/kibana 5601:5601
Kibana’ya http://localhost:5601 adresinden erişebilirsiniz. İlk girişte bir index pattern oluşturmanız gerekiyor. k8s-logs-* pattern’ini kullanın.
Gerçek Dünya Senaryosu: Uygulama Loglarını Yapılandırma
Teorik kurulum tamam, şimdi gerçek hayata bakalım. Diyelim ki bir e-ticaret uygulamanız var ve payment-service adında kritik bir mikroservisiniz hata veriyor. Geleneksel yöntemle şöyle yapardınız:
# Eski yontem - hangi pod'da hata var bilmiyorsunuz
kubectl get pods -n production | grep payment
kubectl logs payment-service-7d8b9c-xk2p9 -n production | grep ERROR
kubectl logs payment-service-7d8b9c-mn3q1 -n production | grep ERROR
# Bu süreç saatlerce sürebilir...
EFK Stack ile Kibana’da şu KQL (Kibana Query Language) sorgusunu çalıştırmanız yeterli:
kubernetes.namespace_name: "production" AND kubernetes.labels.app: "payment-service" AND log: "ERROR"
Saniyeler içinde tüm pod’lardan gelen hata loglarını görüyorsunuz, zaman damgasına göre sıralanmış şekilde.
Log Retention ve Index Management
Production ortamında günlerce, haftalarca log birikiyor ve Elasticsearch disk dolmaya başlıyor. Bu sorunu Index Lifecycle Management (ILM) ile çözüyoruz:
# ILM policy oluşturma
curl -X PUT "localhost:9200/_ilm/policy/k8s-logs-policy"
-H 'Content-Type: application/json'
-d '{
"policy": {
"phases": {
"hot": {
"actions": {
"rollover": {
"max_size": "50gb",
"max_age": "7d"
}
}
},
"warm": {
"min_age": "7d",
"actions": {
"shrink": {
"number_of_shards": 1
},
"forcemerge": {
"max_num_segments": 1
}
}
},
"cold": {
"min_age": "30d",
"actions": {
"freeze": {}
}
},
"delete": {
"min_age": "60d",
"actions": {
"delete": {}
}
}
}
}
}'
Bu policy ile loglar:
- İlk 7 gün aktif “hot” aşamasında tutuluyor ve 50GB’a ulaşınca rollover yapıyor
- 7. günden 30. güne kadar “warm” aşamasına geçiyor, disk kullanımı optimize ediliyor
- 30. günden 60. güne “cold” aşamasında dondurulmuş şekilde duruyor
- 60. günden sonra otomatik siliniyor
Fluentd ile Özel Log Filtreleme
Bazı uygulamalar JSON formatında log üretirken bazıları düz metin yazıyor. Fluentd’yi her iki durumu da handle edecek şekilde yapılandırabilirsiniz. Ayrıca hassas verileri (şifreler, kredi kartı numaraları) loglardan çıkarmak için filter kullanmak, production ortamında zorunlu bir güvenlik önlemi:
# fluentd-configmap'e eklenecek filter blogu
<filter kubernetes.var.log.containers.**>
@type record_transformer
enable_ruby true
<record>
# Kredi karti numaralarini maskele
log ${record["log"].to_s.gsub(/bd{4}[s-]?d{4}[s-]?d{4}[s-]?d{4}b/, '[MASKED_CC]')}
# Sifreleri maskele
log ${record["log"].to_s.gsub(/("password"s*:s*")[^"]*(")/i, '1[MASKED]2')}
# Ortam bilgisi ekle
cluster_name "production-k8s"
environment "production"
</record>
</filter>
Monitoring: EFK Stack’in Kendisini İzlemek
EFK Stack’i kurduğunuzda log altyapınızın sağlığını da izlemeniz gerekiyor. Elasticsearch’ün kendisi bazı önemli metrikler sunuyor:
# Elasticsearch cluster durumu
curl -s http://localhost:9200/_cluster/health?pretty
# Index boyutlari ve dokuman sayilari
curl -s http://localhost:9200/_cat/indices?v&h=index,docs.count,store.size&s=store.size:desc
# Node performans bilgisi
curl -s http://localhost:9200/_nodes/stats/jvm,os,process?pretty |
python3 -c "import sys,json; data=json.load(sys.stdin);
[print(f"Node: {v['name']}, Heap Used: {v['jvm']['mem']['heap_used_percent']}%")
for k,v in data['nodes'].items()]"
# Fluentd buffer durumunu kontrol et
kubectl exec -n logging -it $(kubectl get pod -n logging -l app=fluentd -o jsonpath='{.items[0].metadata.name}')
-- fluent-cat --dry-run -t debug.test <<< '{"message":"test"}'
Yaygın Sorunlar ve Çözümleri
Fluentd pod’ları OOMKilled oluyor: Bu genellikle yüksek log hacmi durumunda yaşanıyor. Fluentd’nin bellek limitini artırın ve chunk boyutunu küçültün. Buffer’ın file type yerine memory type kullanıyorsa, bunu kesinlikle file’a çevirin.
Elasticsearch’e log gönderilmiyor: İlk bakılacak yer Fluentd logları. kubectl logs -n logging daemonset/fluentd komutuyla connection refused veya authentication hatası var mı kontrol edin. Elasticsearch service adının doğru yazıldığından emin olun.
Kibana’da index pattern görünmüyor: Henüz hiç log gelmemiş olabilir veya index adı beklediğinizden farklı. curl http://localhost:9200/_cat/indices ile mevcut index’leri listeleyin.
Disk dolmaya başlıyor: ILM policy tanımlanmamışsa loglar birikmeye devam eder. Acil durum için eski index’leri manuel silebilirsiniz: curl -X DELETE "localhost:9200/k8s-logs-2024.01.*"
Sonuç
EFK Stack, Kubernetes ortamında merkezi log yönetimi için olgun ve savaşta test edilmiş bir çözüm. Kurulum sürecinde en çok zaman alan kısım genellikle Fluentd konfigürasyonu çünkü her ortamın log formatı ve gereksinimleri farklı. Ancak doğru yapılandırıldıktan sonra, üzeri kayıt düşülmüş bir kara kutu gibi her şeyi kaydediyor.
Production’a geçmeden önce şu noktalara özellikle dikkat edin: Elasticsearch için yeterli kaynak (RAM en kritik faktör), ILM policy ile disk yönetimi, Fluentd buffer konfigürasyonu ve hassas veri maskeleme. Bu dördünü doğru yaparsanız, geri kalan operasyonel yükü büyük ölçüde azaltmış olursunuz.
Bir de şunu ekleyeyim: EFK Stack ihtiyaçlarınız için fazla karmaşık geliyorsa ya da çok küçük bir cluster yönetiyorsanız, Grafana Loki alternatifine bakın. Loki çok daha az kaynak tüketiyor ve Grafana ile entegrasyonu mükemmel. Ancak büyük ölçekli, karmaşık sorgulama ihtiyaçları olan ortamlar için Elasticsearch’ün full-text arama gücünün rakibi yok.