ELK Stack ile Docker ve Kubernetes Log Toplama

Konteyner dünyasında log yönetimi, klasik VM tabanlı altyapıdan çok farklı bir zihin yapısı gerektiriyor. Podlar ölüp yeniden doğuyor, konteynerler farklı node’lara taşınıyor, deployment’lar saniyeler içinde scale oluyor. Bu ortamda “sunucuya SSH aç, log dosyasına bak” yaklaşımı artık işe yaramıyor. ELK Stack’i Docker ve Kubernetes ile entegre etmek, bu kaosa düzen getirmenin en yaygın ve kanıtlanmış yollarından biri. Ben de bu yazıda hem Docker Compose üzerinde hem de Kubernetes cluster’ında log toplama mimarisini, gerçek konfigürasyonlarla birlikte ele alacağım.

Neden ELK, Neden Bu Kadar Yaygın?

Açıkçası, Grafana Loki veya Splunk gibi alternatifleri denemeden ELK’yi seçmek körlük olur. Ama ELK’nin bu kadar yaygınlaşmasının somut sebepleri var: Elasticsearch’ün güçlü full-text arama yetenekleri, Kibana’nın kutudan çıkan hazır dashboard’ları ve özellikle Logstash’ın inanılmaz esnek pipeline yapısı. Kubernetes ortamında ise Filebeat, DaemonSet olarak konuşlandırılabiliyor ve her node’daki logları otomatik olarak toplayabiliyor. Bu mimari, özellikle yüzlerce pod çalıştıran ortamlarda ciddi bir operasyonel kolaylık sağlıyor.

Ama şunu da söylemem lazım: ELK kaynak açısından pahalı bir çözüm. Elasticsearch’ün memory ihtiyacı, özellikle JVM heap ayarlarıyla boğuşmak, production’da baş ağrısına dönüşebiliyor. Bu yüzden cluster boyutlandırmasını en baştan doğru yapmak kritik.

Docker Ortamında ELK Kurulumu

Docker Compose ile Temel Stack

Önce development veya staging ortamı için Docker Compose ile hızlı bir ELK kurulumu yapalım. Bu yapı, ilerleyen bölümlerdeki Kubernetes entegrasyonunu anlamak için iyi bir temel oluşturuyor.

# docker-compose.yml
version: '3.8'

services:
  elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch:8.11.0
    container_name: elasticsearch
    environment:
      - node.name=elasticsearch
      - cluster.name=docker-cluster
      - discovery.type=single-node
      - ELASTIC_PASSWORD=changeme123
      - xpack.security.enabled=true
      - xpack.security.http.ssl.enabled=false
      - "ES_JAVA_OPTS=-Xms1g -Xmx1g"
    volumes:
      - esdata:/usr/share/elasticsearch/data
    ports:
      - "9200:9200"
    networks:
      - elk
    ulimits:
      memlock:
        soft: -1
        hard: -1

  logstash:
    image: docker.elastic.co/logstash/logstash:8.11.0
    container_name: logstash
    volumes:
      - ./logstash/pipeline:/usr/share/logstash/pipeline:ro
      - ./logstash/config/logstash.yml:/usr/share/logstash/config/logstash.yml:ro
    ports:
      - "5044:5044"
      - "5000:5000/tcp"
      - "5000:5000/udp"
      - "9600:9600"
    environment:
      - "LS_JAVA_OPTS=-Xms512m -Xmx512m"
    networks:
      - elk
    depends_on:
      - elasticsearch

  kibana:
    image: docker.elastic.co/kibana/kibana:8.11.0
    container_name: kibana
    ports:
      - "5601:5601"
    environment:
      - ELASTICSEARCH_HOSTS=http://elasticsearch:9200
      - ELASTICSEARCH_USERNAME=kibana_system
      - ELASTICSEARCH_PASSWORD=changeme123
    networks:
      - elk
    depends_on:
      - elasticsearch

  filebeat:
    image: docker.elastic.co/beats/filebeat:8.11.0
    container_name: filebeat
    user: root
    volumes:
      - ./filebeat/filebeat.yml:/usr/share/filebeat/filebeat.yml:ro
      - /var/lib/docker/containers:/var/lib/docker/containers:ro
      - /var/run/docker.sock:/var/run/docker.sock:ro
    networks:
      - elk
    depends_on:
      - logstash

volumes:
  esdata:

networks:
  elk:
    driver: bridge

Bu compose dosyasında dikkat edilmesi gereken birkaç nokta var. ES_JAVA_OPTS ile heap boyutunu sınırlamak kritik, yoksa Elasticsearch container içinde mevcut RAM’in tamamını yemeye çalışır. /var/lib/docker/containers mount’u ise Filebeat’in tüm container log’larına erişmesi için zorunlu.

Filebeat Konfigürasyonu

Filebeat’in Docker container log’larını otomatik discover etmesi için aşağıdaki konfigürasyonu kullanıyorum:

# filebeat/filebeat.yml
filebeat.inputs: []

filebeat.autodiscover:
  providers:
    - type: docker
      hints.enabled: true
      hints.default_config:
        type: container
        paths:
          - /var/lib/docker/containers/${data.container.id}/*.log

processors:
  - add_cloud_metadata: ~
  - add_docker_metadata:
      host: "unix:///var/run/docker.sock"
  - decode_json_fields:
      fields: ["message"]
      target: "json"
      overwrite_keys: true

output.logstash:
  hosts: ["logstash:5044"]
  loadbalance: true

logging.level: info
logging.to_files: true
logging.files:
  path: /var/log/filebeat
  name: filebeat
  keepfiles: 7

hints.enabled: true ayarı çok işe yarıyor. Bunu aktif ettiğinizde, container’larınıza label ekleyerek Filebeat davranışını per-container bazında kontrol edebiliyorsunuz. Örneğin bir container’a co.elastic.logs/enabled: "false" label’ı eklerseniz, o container’ın log’ları hiç toplanmıyor.

Logstash Pipeline

Logstash pipeline’ı, log’ların Elasticsearch’e gitmeden önce dönüştürüldüğü ve zenginleştirildiği yer. Temel bir pipeline şöyle görünüyor:

# logstash/pipeline/docker-logs.conf
input {
  beats {
    port => 5044
  }
}

filter {
  if [container][image][name] =~ /nginx/ {
    grok {
      match => {
        "message" => '%{IPORHOST:client_ip} - %{DATA:user} [%{HTTPDATE:timestamp}] "%{WORD:method} %{DATA:request} HTTP/%{NUMBER:http_version}" %{NUMBER:response_code} %{NUMBER:bytes}'
      }
    }
    date {
      match => ["timestamp", "dd/MMM/yyyy:HH:mm:ss Z"]
      target => "@timestamp"
    }
    mutate {
      convert => {
        "response_code" => "integer"
        "bytes" => "integer"
      }
    }
  }

  if [container][image][name] =~ /app/ {
    json {
      source => "message"
      target => "app_log"
    }
    mutate {
      add_field => {
        "log_level" => "%{[app_log][level]}"
        "service_name" => "%{[app_log][service]}"
      }
    }
  }

  mutate {
    remove_field => ["agent", "ecs", "input", "tags"]
  }
}

output {
  elasticsearch {
    hosts => ["elasticsearch:9200"]
    user => "elastic"
    password => "changeme123"
    index => "docker-logs-%{[container][image][name]}-%{+YYYY.MM.dd}"
    ssl_enabled => false
  }
}

Index naming convention’a dikkat edin. Container image adını index adına dahil etmek, Kibana’da filtreleme yaparken hayat kurtarıyor. Nginx logları ayrı bir index’e, uygulama logları ayrı bir index’e gidiyor.

Kubernetes’te ELK Entegrasyonu

Namespace ve Temel Hazırlık

Kubernetes’te ELK stack’i production-grade çalıştırmak için önce namespace yapısını düzenlemek gerekiyor:

kubectl create namespace monitoring

# Elasticsearch için secret oluştur
kubectl create secret generic elasticsearch-credentials 
  --from-literal=username=elastic 
  --from-literal=password=StrongPassword123! 
  -n monitoring

Filebeat DaemonSet

Kubernetes’te Filebeat’in asıl gücü DaemonSet olarak çalışmasından geliyor. Her node’a bir pod konuşlandırılıyor ve o node üzerindeki tüm pod log’ları toplanıyor:

# filebeat-daemonset.yaml
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: filebeat
  namespace: monitoring
  labels:
    app: filebeat
spec:
  selector:
    matchLabels:
      app: filebeat
  template:
    metadata:
      labels:
        app: filebeat
    spec:
      serviceAccountName: filebeat
      terminationGracePeriodSeconds: 30
      hostNetwork: true
      dnsPolicy: ClusterFirstWithHostNet
      containers:
        - name: filebeat
          image: docker.elastic.co/beats/filebeat:8.11.0
          args: ["-c", "/etc/filebeat.yml", "-e"]
          env:
            - name: ELASTICSEARCH_HOST
              value: "elasticsearch-service.monitoring.svc.cluster.local"
            - name: ELASTICSEARCH_PORT
              value: "9200"
            - name: ELASTIC_USERNAME
              valueFrom:
                secretKeyRef:
                  name: elasticsearch-credentials
                  key: username
            - name: ELASTIC_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: elasticsearch-credentials
                  key: password
            - name: NODE_NAME
              valueFrom:
                fieldRef:
                  fieldPath: spec.nodeName
          securityContext:
            runAsUser: 0
          resources:
            limits:
              memory: 200Mi
              cpu: 100m
            requests:
              memory: 100Mi
              cpu: 50m
          volumeMounts:
            - name: config
              mountPath: /etc/filebeat.yml
              readOnly: true
              subPath: filebeat.yml
            - name: data
              mountPath: /usr/share/filebeat/data
            - name: varlibdockercontainers
              mountPath: /var/lib/docker/containers
              readOnly: true
            - name: varlog
              mountPath: /var/log
              readOnly: true
      volumes:
        - name: config
          configMap:
            defaultMode: 0640
            name: filebeat-config
        - name: varlibdockercontainers
          hostPath:
            path: /var/lib/docker/containers
        - name: varlog
          hostPath:
            path: /var/log
        - name: data
          hostPath:
            path: /var/lib/filebeat-data
            type: DirectoryOrCreate

hostNetwork: true ve dnsPolicy: ClusterFirstWithHostNet kombinasyonu bazı ortamlarda network sorunlarına yol açabilir, dikkatli olun. Özellikle özel CNI plugin’leri kullanan cluster’larda bu ayarları test etmek lazım.

Filebeat ConfigMap for Kubernetes

# filebeat-configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: filebeat-config
  namespace: monitoring
data:
  filebeat.yml: |-
    filebeat.autodiscover:
      providers:
        - type: kubernetes
          node: ${NODE_NAME}
          hints.enabled: true
          hints.default_config:
            type: container
            paths:
              - /var/log/containers/*${data.kubernetes.container.id}.log
          templates:
            - condition:
                contains:
                  kubernetes.namespace: "production"
              config:
                - type: container
                  paths:
                    - /var/log/containers/*${data.kubernetes.container.id}.log
                  multiline.pattern: '^d{4}-d{2}-d{2}'
                  multiline.negate: true
                  multiline.match: after

    processors:
      - add_kubernetes_metadata:
          host: ${NODE_NAME}
          matchers:
            - logs_path:
                logs_path: "/var/log/containers/"
      - drop_event:
          when:
            contains:
              kubernetes.namespace: "kube-system"
      - decode_json_fields:
          fields: ["message"]
          target: ""
          overwrite_keys: true
          add_error_key: true

    output.elasticsearch:
      hosts: ['${ELASTICSEARCH_HOST}:${ELASTICSEARCH_PORT}']
      username: '${ELASTIC_USERNAME}'
      password: '${ELASTIC_PASSWORD}'
      index: "k8s-logs-%{[kubernetes.namespace]}-%{+YYYY.MM.dd}"
      ssl.enabled: false

    setup.ilm.enabled: true
    setup.ilm.rollover_alias: "k8s-logs"
    setup.ilm.pattern: "{now/d}-000001"

    logging.level: warning

Burada drop_event processor’una dikkat çekmek istiyorum. kube-system namespace’inden gelen log’ları doğrudan düşürüyoruz. Bu namespace’den akan log hacmi ciddi boyutlara ulaşabiliyor ve Elasticsearch’e gereksiz yük bindiriyor. Production’da hangi namespace’lerin log’lanacağını ve hangilerinin dışarıda tutulacağını net bir şekilde tanımlamak gerekiyor.

RBAC Ayarları

Filebeat’in Kubernetes API’sine erişmesi için gerekli RBAC tanımlamaları:

# filebeat-rbac.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: filebeat
  namespace: monitoring
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: filebeat
rules:
  - apiGroups: [""]
    resources:
      - namespaces
      - pods
      - nodes
    verbs: ["get", "watch", "list"]
  - apiGroups: ["apps"]
    resources:
      - replicasets
    verbs: ["get", "watch", "list"]
  - apiGroups: ["batch"]
    resources:
      - jobs
    verbs: ["get", "watch", "list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: filebeat
subjects:
  - kind: ServiceAccount
    name: filebeat
    namespace: monitoring
roleRef:
  kind: ClusterRole
  name: filebeat
  apiGroup: rbac.authorization.k8s.io

Index Lifecycle Management

Production’da en sık atlanan konu ILM politikaları. Elasticsearch’e akan log’lar için mutlaka bir lifecycle policy tanımlamanız lazım, yoksa disk dolması an meselesi:

curl -X PUT "http://elasticsearch:9200/_ilm/policy/k8s-logs-policy" 
  -H 'Content-Type: application/json' 
  -u elastic:changeme123 
  -d '{
  "policy": {
    "phases": {
      "hot": {
        "min_age": "0ms",
        "actions": {
          "rollover": {
            "max_primary_shard_size": "10gb",
            "max_age": "1d"
          },
          "set_priority": {
            "priority": 100
          }
        }
      },
      "warm": {
        "min_age": "3d",
        "actions": {
          "forcemerge": {
            "max_num_segments": 1
          },
          "shrink": {
            "number_of_shards": 1
          },
          "set_priority": {
            "priority": 50
          }
        }
      },
      "cold": {
        "min_age": "7d",
        "actions": {
          "set_priority": {
            "priority": 0
          }
        }
      },
      "delete": {
        "min_age": "30d",
        "actions": {
          "delete": {}
        }
      }
    }
  }
}'

Bu policy ile log’lar hot phase’de 1 gün veya 10GB boyutuna ulaşana kadar tutulacak, 3. günden itibaren warm phase’e geçerek shard sayısı küçültülecek, 7. günden itibaren cold phase’e düşecek ve 30. günde silinecek. Retention sürelerini iş gereksinimlerinize göre ayarlamanız gerekiyor, yasal zorunluluklar varsa özellikle dikkatli olun.

Sorun Giderme: Sık Karşılaşılan Durumlar

Filebeat Pod’dan Log Gelmiyor

# Filebeat loglarını kontrol et
kubectl logs -n monitoring -l app=filebeat --tail=100

# Filebeat'in doğru node'da çalışıp çalışmadığını gör
kubectl get pods -n monitoring -o wide

# Elasticsearch bağlantısını test et
kubectl exec -it -n monitoring <filebeat-pod-name> -- 
  curl -u elastic:changeme123 http://elasticsearch-service:9200/_cluster/health

Elasticsearch Heap Pressure

Kubernetes ortamında Elasticsearch pod’u sürekli OOMKilled oluyorsa:

# Mevcut heap kullanımını kontrol et
curl -u elastic:password 
  "http://elasticsearch:9200/_nodes/stats/jvm?pretty" | 
  grep -A5 "heap_used_percent"

# Hot thread'leri incele
curl -u elastic:password 
  "http://elasticsearch:9200/_nodes/hot_threads"

Production’da Elasticsearch pod’u için resources.limits memory değerini, JVM heap’inin iki katı olarak ayarlamak iyi bir başlangıç noktası. Yani 4GB heap için 8GB memory limit.

Kibana’da Log Analizi için Temel Adımlar

Loglar Elasticsearch’e düştükten sonra Kibana’da birkaç şeyi mutlaka yapmanızı öneririm. Önce Stack Management > Index Patterns altında index pattern tanımlayın, k8s-logs-* gibi wildcard kullanın. Ardından Discover ekranında en çok kullandığım field’ları sol tarafa sabitleyin: kubernetes.namespace, kubernetes.pod.name, kubernetes.container.name ve log.level.

Özellikle multi-line log’larda dikkatli olun. Java stack trace’leri veya Python traceback’leri birden fazla satıra yayılıyor ve her satır ayrı bir event olarak gelirse anlamlı bir analiz yapamıyorsunuz. Bu yüzden ConfigMap’teki multiline ayarları kritik önem taşıyor.

Kaynak Optimizasyonu ve Dikkat Edilmesi Gerekenler

Pratik olarak gördüğüm bazı anti-pattern’ler:

  • Tüm field’ları index etmek: Elasticsearch her yeni field’ı otomatik index ediyor. Yüzlerce farklı label’a sahip Kubernetes ortamında bu mapping explosion’a yol açabiliyor. dynamic: "strict" veya dynamic: false ayarını düşünün.
  • Shard sayısını ayarlamamak: Her index için varsayılan shard sayısı 1 olsa da, yoğun log akışı olan ortamlarda shard sayısını artırmak performansı ciddi iyileştiriyor.
  • Logstash’ı gereksiz yere kullanmak: Basit log toplama senaryolarında Filebeat’ten direkt Elasticsearch’e yazmak hem daha az kaynak tüketiyor hem de tek bir hata noktası ortadan kalkıyor. Logstash’ı gerçekten karmaşık dönüşüm ihtiyacınız olduğunda devreye alın.
  • Monitoring’i monitoring etmemek: ELK stack’in kendisini izlemek için Elasticsearch’ün kendi monitoring özelliklerini açık tutun. Stack monitoring Kibana’da Stack Management > Stack Monitoring altında aktif edilebiliyor.

Sonuç

Docker ve Kubernetes ortamlarında ELK Stack kurgulamak, ilk bakışta çok sayıda moving part içerdiğinden korkutucu gelebiliyor. Ama mimariyi parçalara ayırıp adım adım ilerlendiğinde, her bileşenin neden orada olduğu netleşiyor. Filebeat log’ları topluyor ve ilk işlemeyi yapıyor, Logstash karmaşık dönüşüm ihtiyaçlarını karşılıyor, Elasticsearch depolama ve arama motorunu üstleniyor, Kibana da bu veriden anlam çıkarmamızı sağlıyor.

Production’a almadan önce ILM politikalarınızı, Elasticsearch heap ayarlarınızı ve Filebeat kaynak limitlerini mutlaka gözden geçirin. Log hacmi tahminleriniz tutmazsa, Elasticsearch’ün disk dolması veya OOM sorunu yaşaması kaçınılmaz. Sistemin kendisini de monitör etmek, bu tip sürprizlerin önüne geçmenin en güvenilir yolu.

Son olarak: bu altyapıyı bir kez kurup bırakmayın. Index boyutlarını, query latency’sini ve Filebeat backpressure metriklerini düzenli olarak gözden geçirmek, uzun vadede sizi ciddi baş ağrısından kurtarıyor.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir