Kubernetes’te Elastic Cloud ile Log Yönlendirme

Kubernetes’te Elastic Cloud ile Log Yönlendirme

Merhaba, Kubernetes’i kullanırken özellikle managed service olarak kullanırken karşılaştığımız en önemli problemlerden biri logların nasıl saklanacağıdır. İşin aslı Kubernetes pod loglarını ilgili podun çalıştığı node üzerinde bir dosyaya yazıyor. Ancak bu dosyalar kalıcı olarak saklanmıyor, podun silinmesi ya da nodeun down olması gibi durumlarda dosyalar siliniyor. Bu durum logları sağlıklı bir şekilde yönlendirme ihtiyacı doğuruyor. Bu yazıda da en popüler stacklerden biri olan ELK Stack’ini inceleyeceğiz.

Logların dosyalarda geçici olarak saklandığını az önce söylemiştim. Burada yapacağımız ilk işlem bu dosyaları takip edip işleme almak olacak. Bunun için Elastic Beats’i kullanacağız. Burada Beats, Kubernetes’in logları geçici olarak sakladığı dosyaları sürekli takip ederek yeni logları işleme alıp stackin bir sonraki elemanına iletmekle yükümlü olacaktır. Stackin bir sonraki elemanı ise Elastic Logstash olacak ve Logstash’in yükümlülüğü de Beats’ten aldığı logları işleyerek Elasticsearch’e göndermek olacaktır. Buradaki işleme eylemini ham logu parse edip içinden bilgi elde etmek olarak düşünebilirsiniz. Logstash, Elasticsearch’e işlenmiş logları ilettikten sonra Elasticsearch bu logları diskte saklama işini üstlenecektir. Son olaraksa Elastic Kibana da görselleştirme ve arayüz işlemleri için yardımımıza koşacaktır. Uzun bir girizgahtan sonra kuruluma geçebiliriz 🙂

1.) Elastic Cloud’un Deploy Edilmesi:

İlk olarak Elastic Cloud’un CRD’sini deploy edelim.

kubectl create -f https://download.elastic.co/downloads/eck/2.3.0/crds.yaml

Ardından Elastic Cloud’un operatörünü deploy edelim.

kubectl apply -f https://download.elastic.co/downloads/eck/2.3.0/operator.yaml

Aşağıdaki komut ile operatörün durumunu izleyebiliriz.

kubectl get po -n elastic-system

Operatör aşağıdaki gibi hazır olduğunda devam edebiliriz.

NAME                 READY   STATUS    RESTARTS   AGE
elastic-operator-0   1/1     Running   0          4m

2.) Elasticsearch’ün Deploy Edilmesi

Aşağıdaki komut ile de Elasticsearch’ü deploy edelim.

cat <<EOF | kubectl apply -f -
apiVersion: elasticsearch.k8s.elastic.co/v1
kind: Elasticsearch
metadata:
  name: blog
spec:
  version: 8.3.2
  nodeSets:
  - name: default
    count: 1
    config:
      node.store.allow_mmap: false
    volumeClaimTemplates:
    - metadata:
        name: elasticsearch-data
      spec:
        accessModes:
        - ReadWriteOnce
        resources:
          requests:
            storage: 5Gi
        storageClassName: standard
  http:
    tls:
      selfSignedCertificate:
        disabled: true
EOF

Burada ihtiyaca göre yapmamız gereken birkaç değişiklik olabilir.

  1. metadata.name: Deploy edeceğimiz Elasticsearch instance’ının ön adını belirliyoruz.
  2. spec.nodeSets.count: Elasticsearch cluster’ının eleman sayısını belirliyoruz.
  3. spec.nodeSets[0].volumeClaimTemplates[0].spec.resources.requests.storage: Elasticsearch için ayırılacak volume’ün boyutunu belirliyoruz.
  4. spec.nodeSets[0].volumeClaimTemplates[0].spec.storageClassName: Elasticsearch’ün verileri diske yazacağı volume’ü ayıracak olan storageClass’ı belirtiyoruz. Bu kısım çok önemli eğer clusterınızda olmayan bir storageClass girerseniz deployment Pending aşamasında takılı kalacaktır.

3.) Logstash’in Deploy Edilmesi

Logstash Elastic Cloud bünyesinde bulunan bir paket olmadığı için Helm ile deploy edeceğiz. İlk olarak aşağıdaki bloku values.yaml adında bir dosyaya kaydedin.

logstashPipeline:
  logstash.conf: |
    input {
      beats {
        host => "0.0.0.0"
        port => 5044
      }
    }

    filter {
      mutate {
        rename => ["host", "hostname"]
        convert => {"hostname" => "string"} 
      }

      if [kubernetes][namespace] == "ingress-nginx" {
        if [stream] == "stdout" {
          grok {
            match => { "message" => ["%{IPORHOST:[nginx][access][remote_ip]} - %{DATA:[nginx][access][user_name]} \[%{HTTPDATE:[nginx][access][time]}\] \"%{WORD:[nginx][access][method]} %{DATA:[nginx][access][url]} HTTP/%{NUMBER:[nginx][access][http_version]}\" %{NUMBER:[nginx][access][response_code]} %{NUMBER:[nginx][access][body_sent][bytes]} \"%{DATA:[nginx][access][referrer]}\" \"%{DATA:[nginx][access][agent]}\" %{NUMBER:[nginx][access][request_length]} %{NUMBER:[nginx][access][request_time]} \[%{DATA:[nginx][access][proxy_upstream_name]}\] \[%{DATA:[nginx][access][proxy_alternative_upstream_name]}\] %{NOTSPACE:[nginx][access][upstream_addr]} %{NUMBER:[nginx][access][upstream_response_length]} %{NUMBER:[nginx][access][upstream_response_time]} %{NUMBER:[nginx][access][upstream_response_code]} %{NOTSPACE:[nginx][access][req_id]}"] }
            remove_field => "message"
          }
          mutate {
            add_field => { "read_timestamp" => "%{@timestamp}" }
          }
          date {
            match => [ "[nginx][access][time]", "dd/MMM/YYYY:H:m:s Z" ]
            remove_field => "[nginx][access][time]"
          }
          useragent {
            source => "[nginx][access][agent]"
            target => "[nginx][access][user_agent]"
            remove_field => "[nginx][access][agent]"
          }
          geoip {
            source => "[nginx][access][remote_ip]"
            target => "[nginx][access][geoip]"
          }
        }
        else if [stream] == "stderr" {
          grok {
            match => { "message" => ["%{DATA:[nginx][error][time]} \[%{DATA:[nginx][error][level]}\] %{NUMBER:[nginx][error][pid]}#%{NUMBER:[nginx][error][tid]}: (\*%{NUMBER:[nginx][error][connection_id]} )?%{GREEDYDATA:[nginx][error][message]}"] }
            remove_field => "message"
          }
          mutate {
            rename => { "@timestamp" => "read_timestamp" }
          }
          date {
            match => [ "[nginx][error][time]", "YYYY/MM/dd H:m:s" ]
            remove_field => "[nginx][error][time]"
          }
        }
      }
    }

    output {
      stdout { codec => rubydebug }
      elasticsearch {
          hosts => [ "blog-es-http:9200" ]
          user => 'elastic'
          password => 'P@$$w0rD'
      }
    }

extraEnvs:
  - name: XPACK_MONITORING_ENABLED
    value: "false"
service:
  type: ClusterIP
  loadBalancerIP: ""
  ports:
    - name: beats
      port: 5044
      protocol: TCP
      targetPort: 5044

values.yaml dosyasının içeriğini biraz açıklamak gerekirse aslında temelde 3 farklı değer verdik. Bunlar logstash.conf dosyasının içeriği, eklemek istediğimiz ortam değişkenleri ve açmak istediğimiz servis portlarıdır. Son iki değeri açıklayacak olursak XPACK_MONITORING_ENABLED: “false” diyerek kullanmayacağımız monitoring modülünü kapatmış olduk, servis tarafında ise Filebeat’in logları göndereceği 5044 portunu serviste tanımlamış olduk. Gelelim logstash.conf dosyasına bu dosya logstash ile kullanacağımız pipelineları tanımladığımız bir dosyadır. Peki bizim pipeline’ımız nasıl işleyecek? Öncelikle beats tipinde bir input tanımlıyoruz buraya istediğimiz portu tanımlayabiliriz ama service kısmında da o portu tanımlamayı unutmayalım. Filter kısmı ise logları Elasticsearch’e göndermeden önce düzenlediğimiz kısım. Burada ilk satırdaki mutate scope’unda inputtan yani bizim için Filebeat’ten gelen fieldlardan host fieldının adını hostname ve tipini de string olarak değiştiriyoruz. Alttaki if scope’u ise logların ingress-nginx’ten gelip gelmediğini kontrol ediyor ve eğer öyleyse gerekli fieldları parse ediyor son olaraksa output bölümünde elasticsearch servisinin bilgilerini giriyoruz burada ek olarak elasticsearch clusterına ait bir parolaya ihtiyacımız var bu parolayı aşağıdaki komut ile öğrenebilirsiniz.

kubectl get secret blog-es-elastic-user -o=jsonpath='{.data.elastic}' | base64 --decode; echo

Şimdi sıra geldi Helm ile deploy etmeye… Bunun için values.yaml dosyasını kaydettiğimiz dizinde şu komutu çalıştırmamız yeterli olacaktır.

helm repo add elastic https://helm.elastic.co
helm install logstash elastic/logstash -f values.yaml

4.) Filebeat’in Deploy Edilmesi

Filebeat de Logstash gibi Elastic Cloud’a dahil bir paket değil o yüzden yine Helm kullnarak deploy edeceğiz.

Aşağıdaki bloku values.yaml olarak kaydedelim.

---
daemonset:
  filebeatConfig:
    filebeat.yml: |
      filebeat.inputs:
      - type: container
        paths:
          - /var/log/containers/*.log
        processors:
        - add_kubernetes_metadata:
            host: ${NODE_NAME}
            matchers:
            - logs_path:
                logs_path: "/var/log/containers/"
        - drop_event:
            when:
              or:
              - equals:
                  kubernetes.namespace: "kube-system"
              - equals:
                  kubernetes.namespace: "elastic-system"
              - regexp:
                  kubernetes.pod.name: "filebeat-*"
              - regexp:
                  kubernetes.pod.name: "elasticsearch-*"
              - regexp:
                  kubernetes.pod.name: "logstash-*"
      output.logstash:
        host: '${NODE_NAME}'
        hosts: '[logstash-logstash]'
deployment:
  enabled: false
secretMounts: []

values.yaml dosyasında container loglarının hangi dizinden alınacağını, hariç tutulacak logları ve logstash adresini tanımladık. Helm ile deploy edelim. Aşağıdaki komutu yeni values.yaml dosyamızın olduğu dizinde çalıştıralım.

helm install filebeat elastic/filebeat -f values.yaml

5.) Kibana’nın Deploy Edilmesi

cat <<EOF | kubectl apply -f -
apiVersion: kibana.k8s.elastic.co/v1
kind: Kibana
metadata:
  name: blog
spec:
  version: 8.3.2
  count: 1
  elasticsearchRef:
    name: blog
EOF

Kibana’yı deploy etmek için yukarıdaki komutu çalıştırmanız yeterli olacaktır. Burada dikkat etmeniz gereken kısım koddaki elasticsearchRef.name kısmı ikinci aşamadaki Elasticsearh’ün name’i ile aynı olmalıdır.

Arayüze erişmek için blog-kb-http servisinin 5601 portu için bir ingress tanımlayabilirsiniz. Arayüze eriştiğinizde sizden kullanıcı adı ve parola isteyecektir. Parolayı 3. aşamadaki gibi elde edebilirsiniz.

Enes UYSAL avatarı

Bir yanıt yazın

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