EFK Stack ile Kubernetes’te Merkezi Log Yönetimi

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.

Yorum yapın