Kasım 22 2024

Kubernetes Kaynak Yönetimi: Request, Limit ve VPA Ayarları

Hepimiz o sancılı anı en az bir kez yaşamışızdır: Hafta sonu tam kahvenizden ilk yudumu almışken, Slack veya PagerDuty üzerinden bir “Pod crashed” uyarısı düşer. Hemen terminale koşup cluster’a sızarsınız, kubectl describe pod komutunu yapıştırırsınız ve karşınızda o meşhur ibareyi görürsünüz: OOMKilled (Exit Code 137). Ya da daha sinsisi; CPU kullanımı %10 seviyelerindeyken uygulamanın response time’ları bir anda saniyelere fırlar, çünkü arkada amansız bir CPU throttling savaşı dönmektedir.

Kubernetes dünyasında kaynak yönetimi (resources), teoride çok basit görünse de pratikte prod ortamlarının en büyük baş belasıdır. Bu makalede, deneyimli bir devops mühendisinin gözünden, kubernetes üzerinde CPU/Memory dengesini nasıl kuracağımızı, vpa (Vertical Pod Autoscaler) ile kaynak yönetimini nasıl otomatize edeceğimizi ve throttling/OOMKill canavarlarıyla nasıl savaşacağımızı derinlemesine ele alacağız.

Request ve Limit: scheduler’ın Terazi Hassasiyeti

Kubernetes dünyasında kaynakları tanımlarken iki temel kavram kullanırız: requests ve limits. Bunların arasındaki farkı netleştirmek, sistemin kararlılığı için atılacak ilk adımdır.

  • Request: Bir pod’un schedule edilebilmesi için garanti edilen minimum kaynak miktarıdır. Kubernetes scheduler, bir node üzerinde pod’u konumlandırırken sadece bu değere bakar. Eğer node üzerinde boşta yeterli “request” kapasitesi yoksa, pod o node’a yerleşemez (Pending durumunda kalır).
  • Limit: Bir pod’un fiziksel olarak tüketebileceği maksimum kaynaktır. Pod bu sınırı aşmaya çalıştığında kernel seviyesinde kısıtlamalar devreye girer.

QoS (Quality of Service) Sınıfları Neden Önemli?

Request ve limit değerlerini nasıl tanımladığınız, Kubernetes’in podunuza atayacağı QoS sınıfını belirler. Node üzerinde kaynak darboğazı yaşandığında, kubelet ilk olarak hangi pod’u feda edeceğine (eviction) bu sınıfa göre karar verir:

  1. Guaranteed (Garantili): Pod içerisindeki tüm container’ların request ve limit değerleri birbirine eşittir (hem CPU hem Memory için). Kubernetes bu pod’ları en son gözden çıkarır. Veritabanları ve stateful servisler için idealdir.
  2. Burstable (Esneyebilir): Request değeri limit değerinden küçüktür veya sadece request tanımlanmıştır. Web uygulamalarımızın %90’ı bu sınıfa girer. Node sıkıştığında, limitlerine yaklaşan burstable pod’lar ilk hedef tahtasındadır.
  3. BestEffort (Gönlünden Ne Koparsa): Ne request ne de limit tanımlanmıştır. Node üzerinde kaynak bittiği an, kubelet bu pod’ları acımadan öldürür. Prod ortamında asla ama asla kullanılmamalıdır!
# Burstable QoS sınıfına örnek bir pod tanımı
apiVersion: v1
kind: Pod
metadata:
  name: billing-service
  namespace: production
spec:
  containers:
  - name: app
    image: internal/billing:v2.1.0
    resources:
      requests:
        memory: "256Mi"
        cpu: "200m"
      limits:
        memory: "512Mi"
        cpu: "1000m"

CPU Throttling ve OOMKill: Sessiz Katiller

Memory ve CPU, işletim sistemi kernel’ı tarafından farklı şekillerde yönetilir. Bu fark, pod’larımızın hayatta kalma mücadelesini doğrudan etkiler.

OOMKill (Out of Memory)

Memory, sıkıştırılamaz (non-compressible) bir kaynaktır. Bir pod, kendisine tanımlanan memory limit değerini aşarsa, kernel’ın oom-killer mekanizması devreye girer ve pod’u anında öldürür. Pod’unuz durduk yere 137 exit kodu ile çöküyorsa, memory limitiniz uygulamanın anlık yük altında ihtiyaç duyduğu heap size’a yetmiyor demektir.

Bunu debug etmek için şu komutla son çöken pod’ların nedenlerine bakabilirsiniz:

kubectl get pods -n production -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.status.containerStatuses[*].lastState.terminated.reason}{"\n"}{end}' | grep OOMKilled

CPU Throttling

CPU ise sıkıştırılabilir (compressible) bir kaynaktır. Pod’unuz CPU limitine ulaştığında işletim sistemi onu öldürmez; bunun yerine pod’a tahsis edilen CPU zaman dilimlerini (CFS shares) kısar. Buna throttling denir.

Uygulamanız çökmez ama bir anda istekleri işlememeye başlar, thread’ler kuyruğa girer ve latency tavan yapar. Birçok junior mühendis bu durumu fark edemez çünkü pod “Running” durumundadır ve CPU kullanımı %100 görünmez (çünkü limit bazlı kırpılmıştır).

Prometheus üzerinde CPU throttling olayını yakalamak için şu PromQL sorgusunu kullanabilirsiniz:

sum(rate(container_cpu_cfs_throttled_seconds_total[5m])) by (pod, container) / sum(rate(container_cpu_usage_seconds_total[5m])) by (pod, container) * 100

Eğer bu oran %5’in üzerindeyse, uygulamanız ciddi şekilde yavaşlatılıyor demektir. Limit değerinizi artırmanın vakti gelmiştir.

VPA (Vertical Pod Autoscaler) ile Otomatik Kaynak Yönetimi

Yazılımcılara “Uygulamanız ne kadar kaynak tüketiyor?” diye sormak, bir çocuğa “Ne kadar dondurma istersin?” diye sormaya benzer. Cevap hep “Çok!” olur. İşte bu noktada vpa devreye giriyor. VPA, pod’ların geçmiş kaynak tüketimlerini izleyerek en ideal request ve limit değerlerini otomatik olarak hesaplar.

VPA üç ana bileşenden oluşur:

  • Recommender: Tarihsel metrikleri (Prometheus veya Metrics Server üzerinden) inceleyerek ideal kaynak önerilerini hesaplar.
  • Updater: Eğer bir pod önerilen değerlerin çok dışındaysa, pod’u silerek yeni değerlerle yeniden ayağa kalkmasını tetikler (eğer mode: Auto ise).
  • Admission Controller: Pod yeniden oluşturulurken mutasyon aşamasında araya girer ve yeni request/limit değerlerini pod spec’ine enjekte eder.

Aşağıda prod ortamında güvenle kullanabileceğiniz bir VPA konfigürasyon örneği yer alıyor:

apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
  name: auth-service-vpa
  namespace: production
spec:
  targetRef:
    apiVersion: "apps/v1"
    kind: Deployment
    name: auth-service
  updatePolicy:
    updateMode: "Auto" # "Off", "Initial" veya "Auto" yapabilirsiniz.
  resourcePolicy:
    containerPolicies:
      - containerName: '*'
        minAllowed:
          cpu: "100m"
          memory: "128Mi"
        maxAllowed:
          cpu: "2000m"
          memory: "4Gi"
        controlledResources: ["cpu", "memory"]

Pro-Tip: Eğer pod’larınızın durduk yere restart olmasını istemiyorsanız, updateMode: "Off" olarak ayarlayın. VPA sadece öneri (recommendation) üretecektir. Siz de bu önerileri inceleyerek (kubectl describe vpa auth-service-vpa) manuel olarak güncelleme yapabilirsiniz.

VPA ve HPA Çelişkisi: İki Kaptan Bir Gemiyi Batırır

Eğer cluster mimarinizde hem hpa (Horizontal Pod Autoscaler) hem de VPA kullanıyorsanız dikkat etmeniz gereken çok kritik bir kural var: İkisini de aynı kaynak metriğine (örn: CPU kullanımına) göre ölçeklenecek şekilde ayarlayamazsınız!

Eğer HPA CPU kullanımına göre pod sayısını artırmaya çalışırken, VPA de aynı CPU metriğine bakıp pod’un dikey boyutunu büyütmeye çalışırsa, sistem kararsız bir döngüye girer. HPA pod ekler, VPA pod’u yeniden başlatıp büyütür, tam bir kaos yaşanır.

Bu Çıkmazdan Nasıl Kurtuluruz?

  1. HPA’yı Custom/External Metriklere Geçirin: HPA’yı CPU/Memory yerine Prometheus üzerinden gelen saniye başına istek sayısı (RPS), kuyruk uzunluğu (RabbitMQ queue size) veya aktif veritabanı bağlantı sayısı gibi işlevsel metriklere göre ölçeklendirin. VPA ise arkada sessizce pod’ların fiziksel limitlerini optimize etsin.
  2. VPA’yı Sadece Öneri Modunda Çalıştırın: VPA’yı updateMode: "Off" modunda bırakıp, Goldilocks gibi open-source araçlar kullanarak sadece doğru kaynak değerlerini bulmak için bir pusula olarak kullanın.

Altın Kurallar ve Best Practices

  • CPU Limitlerini Çok Sıkı Tutmayın (Veya Hiç Koymayın): Modern Kubernetes topluluklarında (özellikle büyük ölçekli altyapılarda) CPU limitlerinin kaldırılması tartışılıyor. Çünkü CPU limitleri CFS throttling nedeniyle gereksiz latency oluşturabiliyor. Bunun yerine yüksek CPU request’i tanımlayıp limit koymamak (veya çok yüksek tutmak) node üzerindeki işlemci gücünü daha efektif kullanabilir. Ancak Memory limitleri kesinlikle zorunludur!
  • Overcommit Oranını İyi Yönetin: Node’larınızdaki toplam Memory limitlerinin, fiziksel node kapasitesini aşırı derecede geçmesine (overcommit) izin vermeyin. Aksi takdirde bir yük anında tüm node kilitlenebilir ve kubelet pod’ları kontrolsüzce öldürmeye başlar.
  • Liveness ve Readiness Probe’lara Dikkat Edin: Probe’ların kullandığı endpoint’lerin yüksek CPU/Memory tüketmediğinden emin olun. Throttling anında probe’lar timeout’a düşerse, Kubernetes sağlam podunuzu “unhealthy” sayıp restart döngüsüne sokabilir.

Kubernetes üzerinde kaynak yönetimi, sürekli izlenmesi ve dinamik olarak ayarlanması gereken canlı bir süreçtir. VPA gibi otomasyon araçlarını sisteminize entegre ederek hem cloud maliyetlerinizi düşürebilir hem de o meşhur cumartesi gecesi uykularınızı garanti altına alabilirsiniz.

Category: Genel | LEAVE A COMMENT
Kasım 15 2024

Ansible ile Idempotent Playbook Yazmanın İncelikleri

Sektörde beş yılı deviren her devops mühendisinin ortak kabuslarından biri, “benden önce yazılmış” ve her çalıştığında farklı bir sürpriz sunan ansible playbook’larıdır. Hepimiz o yollardan geçtik: Sadece bir config dosyasındaki parametreyi değiştirmek için çalıştırdığınız playbook, sunucudaki üç servisi yeniden başlatır, SSL sertifikalarını sıfırlar ve deployment pipeline’ını kilitler. İşte bu noktada, modern iac (Infrastructure as Code) felsefesinin kalbi olan idempotent (eşgüçlü) kavramı devreye giriyor. Gerçek bir altyapı otomasyon süreci, aynı playbook’u ister 1 ister 1000 kez çalıştırın, hedef sistemi her zaman tam olarak hedeflediğiniz kararlı durumda (desired state) bırakmalıdır. Bu makalede, işin “YAML yazmaktan” çıkıp gerçek bir yazılım mühendisliği disiplinine dönüştüğü o ince çizgiyi inceleyeceğiz.

1. “Her Şey Yolunda” İllüzyonu: changed_when ve failed_when

Ansible modüllerinin büyük çoğunluğu idempotent çalışacak şekilde tasarlanmıştır. Örneğin ansible.builtin.apt veya ansible.builtin.template modülleri, hedef sistemin durumunu kontrol eder ve bir değişiklik gerekmiyorsa yeşil yanarak yoluna devam eder. Ancak iş shell veya command modüllerine geldiğinde Ansible körleşir. Bu modüller doğası gereği her çalıştığında sisteme bir etki ettiklerini varsayarlar ve her zaman sarı (changed: true) dönerler.

Bu durum sadece göz zevkimizi bozmakla kalmaz; Ansible handler’larının (örneğin servis restart işlemleri) gereksiz yere tetiklenmesine yol açarak production ortamında kesintilere sebep olur. İşte bu kontrolsüzlüğü dizginlemek için elimizdeki en güçlü silahlar: changed_when ve failed_when.

İyi, Kötü ve Çirkin: shell Modülünü Terbiye Etmek

Farz edelim ki bir uygulamanın CLI aracıyla bir konfigürasyon yapacaksınız. Eğer bu konfigürasyon zaten yapılmışsa, komutu tekrar çalıştırmamalı veya çalıştırsak bile Ansible’a “hey, burada yeni bir şey yapmadın, sakin ol” demeliyiz.

Aşağıdaki kötü pratiğe bir göz atalım:

# KÖTÜ PRATİK (Her çalışmada "changed" döner, handler'ları tetikler)
- name: Enable custom application plugin
  ansible.builtin.shell: "myapp-cli plugin enable prometheus"
  register: plugin_output
  notify: Restart MyApp

Şimdi bunu profesyonelce revize edelim. Önce eklentinin durumunu sorgulayalım, ardından sadece eklenti aktif değilse işlem yapalım ve durumu Ansible’a doğru şekilde bildirelim:

# İYİ PRATİK (Idempotent ve güvenli)
- name: Check if prometheus plugin is already enabled
  ansible.builtin.shell: "myapp-cli plugin list | grep -E '^prometheus.*enabled'"
  register: plugin_status
  failed_when: false
  changed_when: false

- name: Enable custom application plugin if not enabled
  ansible.builtin.shell: "myapp-cli plugin enable prometheus"
  when: plugin_status.rc != 0
  register: enable_result
  changed_when: "'plugin successfully enabled' in enable_result.stdout"
  notify: Restart MyApp

Burada ne yaptık? İlk task’ta failed_when: false ve changed_when: false diyerek, sadece bir durum sorguladığımızı ve bu sorgunun sistemi değiştirmediğini, ayrıca grep başarısız olsa bile (yani plugin kurulu değilse) pipeline’ın kırılmaması gerektiğini belirttik. İkinci task’ta ise sadece plugin aktif değilse çalıştık ve çıktıdaki spesifik bir loga bakarak değişikliğin gerçekten gerçekleşip gerçekleşmediğini teyit ettik.

2. Sırları Sızdırmadan Yönetmek: Ansible Vault ve Best Practice’ler

IaC kodlarınızı Git reposunda tutuyorsanız (ki tutmalısınız), API key’ler, veritabanı şifreleri veya SSL private key’ler gibi hassas verileri (secrets) asla düz metin (plain text) olarak commit etmemelisiniz. Ansible bu problemi çözmek için dahili bir şifreleme mekanizması olan Ansible Vault‘u sunar.

Ancak projelerde sıklıkla yapılan hata, tüm group_vars/all.yml dosyasını toptan şifrelemektir. Bu, kod incelemelerinde (Code Review) hangi değişkenlerin değiştiğini görmeyi imkansız hale getirir. Bunun yerine, sadece hassas değerleri şifreleyip referans vermek çok daha temiz bir yaklaşımdır.

Tek Satır Şifreleme (vault_encrypted) Pratiği

Tüm dosyayı şifrelemek yerine, sadece gizli tutmak istediğiniz değeri terminalde şifreleyin:

ansible-vault encrypt_string 'super_secret_db_password' --name 'vault_db_password'

Bu komut size YAML formatında şifrelenmiş bir blok verecektir. Bu bloğu değişken dosyanıza güvenle yapıştırabilirsiniz:

# group_vars/production.yml
db_username: "app_user" # Düz metin olarak kalmasında sakınca yok
db_password: "{{ vault_db_password }}" # Şifrelenmiş değere referans

# Şifrelenmiş blok:
vault_db_password: !vault |
          $ANSIBLE_VAULT;1.1;AES256
          3565393065646134373463326139613661646231363539303362333633643730306265353866
          62376663363236373961633535366430333766396662366264370a3736363339386333343632
          3431663030313534663831633633636636316262333066366432366164343164376435383561
          32303533663731306132373335623233390a3666363435303964313032393335396161613936
          653063616664653838323630

Playbook’u çalıştırırken Vault şifresini güvenli bir şekilde okutmak için --vault-password-file parametresini veya çevresel değişkenleri (environment variables) kullanabilirsiniz. CI/CD pipeline’larında bu şifreyi runner’a bir secret olarak tanımlamak en güvenli yoldur.

3. Altyapıyı Test Etmek: Molecule ile TDD Yaklaşımı

“Yazdığım playbook production’da çalışır mı?” sorusunun cevabı hiçbir zaman “deneyip görelim” olmamalıdır. Yazılım dünyasındaki Unit/Integration test kavramının IaC dünyasındaki karşılığı Molecule‘dur. Molecule; Ansible rollerinizi izole ortamlarda (genellikle Docker veya Podman üzerinde, bazen de Vagrant/AWS’te) otomatik olarak ayağa kaldırır, playbook’unuzu çalıştırır (idempotency testi dahil) ve ardından ortamı temizler.

Molecule Kurulumu ve Örnek Senaryo

Molecule’ü projenize dahil etmek için Docker driver’ı ile birlikte yükleyin:

pip install molecule molecule-plugins[docker] ansible-lint

Bir Ansible rolü içinde Molecule senaryosu başlatmak için:

molecule init scenario --driver-name docker

Bu komut, rolünüzün altında molecule/default adında bir klasör oluşturur. Buradaki en kritik dosya testlerin nasıl koşulacağını belirleyen molecule.yml dosyasıdır:

# molecule/default/molecule.yml
dependency:
  name: galaxy
driver:
  name: docker
platforms:
  - name: test-ubuntu-target
    image: geerlingguy/docker-ubuntu2204-ansible:latest
    pre_build_image: true
    volumes:
      - /sys/fs/cgroup:/sys/fs/cgroup:rw
    cgroupns_mode: host
    privileged: true
provisioner:
  name: ansible
  playbooks:
    converge: ${MOLECULE_PLAYBOOK:-converge.yml}
verifier:
  name: ansible

molecule test komutunu çalıştırdığınızda şu adımlar sırasıyla işletilir:

  1. Dependency: Gerekli harici roller indirilir.
  2. Lint: Ansible-lint ile kod standartları taranır.
  3. Destroy/Create: Eski test container’ları silinir ve yenileri ayağa kaldırılır.
  4. Converge: Playbook’unuz ilk kez çalıştırılır (kurulumlar yapılır).
  5. Idempotence: Playbook ikinci kez çalıştırılır. Eğer herhangi bir task “changed” dönerse test başarısız sayılır! İşte gerçek idempotent testi budur.
  6. Verify: Belirlediğiniz test script’leri (örneğin servis gerçekten çalışıyor mu kontrolü) koşturulur.

4. Performans Optimizasyonu: Dakikaları Saniyelere İndirmek

Ansible, mimarisi gereği her bir task için hedef makineye SSH bağlantısı açar, modülü transfer eder, çalıştırır ve sonucu geri alır. Yüzlerce sunucudan oluşan bir envanterde bu durum ciddi bir performans darboğazı (bottleneck) yaratır. Neyse ki birkaç ince ayarla bu süreyi dramatik şekilde azaltabiliriz.

SSH Pipelining Aktifleştirme

Ansible’ın varsayılan davranışında, modül dosyaları önce hedef makineye kopyalanır ve ardından SSH üzerinden çalıştırılır. SSH Pipelining aktifleştirildiğinde ise bu işlemler kopyalama adımı atlanarak doğrudan SSH oturumu üzerinden (piped) gerçekleştirilir.

# ansible.cfg
[ssh_connection]
pipelining = True
ssh_args = -o ControlMaster=auto -o ControlPersist=60s

Not: Pipelining kullanabilmek için hedef sunuculardaki /etc/sudoers dosyasında requiretty seçeneğinin aktif olmaması gerekir (modern dağıtımlarda varsayılan olarak pasiftir).

Fact Gathering Mekanizmasını Optimize Etmek

Ansible her playbook başlangıcında hedef sisteme dair tüm sistem bilgilerini (IP, CPU, RAM, disk durumları vb.) toplar (Gathering Facts). Eğer playbook’unuzda bu değişkenleri (örn: ansible_distribution) kullanmıyorsanız, bu adımı tamamen kapatın:

- hosts: webservers
  gather_facts: false
  tasks:
    # ...

Eğer bazı task’lar için bu bilgilere ihtiyacınız varsa, akıllı cache (fact caching) mekanizmasını devreye sokarak her çalıştırmada bu süreyi tekrar ödemekten kurtulabilirsiniz:

# ansible.cfg
[defaults]
gathering = smart
fact_caching = jsonfile
fact_caching_connection = /tmp/ansible_fact_cache
fact_caching_timeout = 86400 # 24 saat cache'le

Özet ve Kapanış

Yazması kolay, yönetmesi zor bir araçtır Ansible. Onu sıradan bir shell script tetikleyicisinden profesyonel bir iac aracına dönüştüren şey, sizin yazdığınız playbook’lardaki detay seviyesidir. changed_when ile kontrolü elinizde tutmak, secrets yönetiminde hassas davranmak, Molecule ile test güvencesi sağlamak ve performans parametrelerini optimize etmek sizi ekipten bir adım öne çıkaracaktır. Altyapınızın her zaman tahmin edilebilir, kararlı ve hızlı kalması dileğiyle!

Category: Genel | LEAVE A COMMENT
Kasım 8 2024

Docker Multi-Stage Build ile İmaj Boyutunu %80 Küçültme

Modern devops süreçlerinde, CI/CD pipeline’larının hızını doğrudan etkileyen en kritik unsurlardan biri optimize edilmiş container imajlarıdır. Üretim ortamına (production) her deployment çıktığımızda, şişkin bir docker imajının registry’ye push edilmesi ve ardından Kubernetes node’ları tarafından pull edilmesi ciddi bir zaman ve network maliyeti yaratır. İşte tam bu noktada, multi-stage build yaklaşımı imaj boyutlarını radikal bir şekilde düşürerek deployment süreçlerimizi hızlandırır, cold-start sürelerini minimize eder ve güvenlik açıklarını (vulnerabilities) büyük oranda azaltır. Bu yazıda, sadece teoriden bahsetmeyeceğiz; hantal bir imajı alıp adım adım ameliyat masasına yatıracağız.

Acı Gerçek: 800 MB’lık Hello World İmajı Nasıl Yapılır?

Pek çok yazılım ekibinde, “çalışıyor işte kurcalama” mantığıyla yazılmış Dockerfile’lar görürüz. Özellikle Go, Rust gibi statik olarak derlenen dillerde veya Node.js gibi devasa node_modules klasörüne sahip ortamlarda bu durum tam bir faciaya dönüşebilir. Gelin, basit bir Go uygulaması üzerinden durumu simüle edelim.

Aşağıda, sadece HTTP 200 dönen basit bir Go web sunucusunun Dockerfile’ı yer alıyor:

FROM golang:1.21

WORKDIR /app

COPY . .

RUN go build -o main .

EXPOSE 8080

CMD ["./main"]

Bu Dockerfile ile build aldığınızda, karşınıza çıkacak imaj boyutu yaklaşık 850 MB civarında olacaktır. Peki neden? Çünkü Go derleyicisine, paket yöneticisine, Debian tabanlı işletim sisteminin tüm araçlarına (curl, git, apt vb.) sadece uygulamayı çalıştırmak için ihtiyacımız yok. Bunlar sadece derleme (build-time) aşamasında lazım olan araçlar. Production runtime’ında bu araçların durması hem gereksiz bir disk yükü hem de potansiyel birer güvenlik açığıdır.

Multi-Stage Build Nedir ve Neden Hayat Kurtarır?

Multi-stage build, tek bir Dockerfile içerisinde birden fazla FROM ifadesi kullanarak imaj oluşturma sürecini aşamalara (stage) bölmemizi sağlar. Derleme araçlarını ilk aşamada bırakır, sadece ortaya çıkan nihai çıktıyı (artifact) bir sonraki temiz ve hafif aşamaya kopyalarız.

Bu yöntemle, ilk aşamada (builder) tüm ağır SDK’leri kullanabilir, işimiz bittiğinde ise sadece binary dosyasını alıp minimalist bir base image üzerine koyabiliriz. Böylece, yüzlerce megabaytlık derleme araçları nihai imajın katmanlarında (layer) yer almaz.

Dockerfile’ı Ameliyat Ediyoruz

Şimdi yukarıdaki kötü senaryoyu multi-stage kullanarak optimize edelim. Dockerfile’ımızı iki aşamaya bölüyoruz: “builder” ve “runner”.

# 1. Aşama: Derleme (Build Stage)
FROM golang:1.21-alpine AS builder

WORKDIR /app

# Bağımlılıkları kopyala ve indir
COPY go.mod go.sum ./
RUN go mod download

# Kaynak kodları kopyala ve derle
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o /app/main .

# 2. Aşama: Çalıştırma (Runtime Stage)
FROM alpine:3.18

WORKDIR /app

# Sadece derlenmiş binary'yi ilk aşamadan kopyalıyoruz
COPY --from=builder /app/main .

EXPOSE 8080

# Non-root user tanımlayarak güvenliği artırıyoruz
RUN adduser -D appuser
USER appuser

CMD ["./main"]

Sonuç inanılmaz: İmaj boyutu 850 MB’tan 20 MB seviyelerine düştü! Yaklaşık %97 oranında bir küçülme elde ettik. Peki burada tam olarak ne yaptık? Neden bu kadar büyük bir fark oluştu?

  • AS builder: İlk satırda bu aşamaya bir isim verdik. Böylece sonraki aşamalarda bu isme referans verebiliyoruz.
  • Alpine Base Image: Derleme aşamasında Debian yerine hafif bir Alpine imajı seçtik.
  • CGO_ENABLED=0: Go uygulamasını derlerken C kütüphanelerine statik bağımlılık olmamasını sağladık. Bu sayede binary dosyamız tamamen taşınabilir hale geldi.
  • COPY –from=builder: İşte sihirli an! İkinci aşamada, ilk aşamada ürettiğimiz /app/main dosyasını çekip aldık. Geri kalan tüm Go SDK ve intermediate katmanlar çöpe gitti.

Layer Caching: Docker’ı Akıllıca Kandırmak

Multi-stage build kullanmak harika, ancak CI/CD pipeline’larımızın saniyeler içinde tamamlanmasını istiyorsak layer caching mekanizmasını doğru kullanmalıyız. Docker, Dockerfile’daki her bir satırı (instruction) yukarıdan aşağıya doğru sırayla çalıştırır ve her satır için bir cache katmanı oluşturur.

Eğer bir satırda değişiklik tespit edilirse (örneğin kopyalanan kodlar değiştiyse), Docker o satırdan sonraki tüm adımların cache’ini geçersiz kılar (cache invalidation).

Sık yapılan bir hata şudur:

COPY . .
RUN go mod download

Bu senaryoda, projedeki herhangi bir .go dosyasında tek bir satır değiştirdiğinizde, Docker tüm bağımlılıkları (go dependencies) internetten tekrar indirmeye başlar. Çünkü COPY . . satırı cache’i bozmuştur. Bunu önlemek için bağımlılık tanımlarını (go.mod, package.json vb.) kaynak koddan önce kopyalamalıyız:

COPY go.mod go.sum ./
RUN go mod download  # Bu satır, bağımlılıklar değişmediği sürece cache'den gelir!
COPY . .

Bu basit yer değişimi, CI/CD pipeline’larınızdaki build sürelerini dakikalardan saniyelere indirecektir. “Neden her seferinde npm install bekliyoruz?” sorusunun yanıtı tam olarak buradadır.

Distroless Base Image: İçeride Kimse Var mı?

İmaj boyutunu Alpine kullanarak 20 MB’a düşürdük, ancak daha da ileriye gidebiliriz. Alpine imajları her ne kadar küçük olsa da içinde hala bir shell (sh, ash) ve bir paket yöneticisi (apk) barındırır. Eğer bir saldırgan container içerisine sızmayı başarırsa, bu araçları kullanarak container içinde exploit çalıştırabilir veya internal network’ünüzü tarayabilir.

Google tarafından geliştirilen distroless imajlar, sadece uygulamanızın çalışması için gereken minimum çalışma zamanı (runtime) bağımlılıklarını içerir. İçinde shell yoktur, paket yöneticisi yoktur, temel linux komutları (ls, cd, ping) bile yoktur.

Şimdi runtime stage kısmını distroless static imajı ile güncelleyelim:

# 2. Aşama: Distroless Runtime
FROM gcr.io/distroless/static-debian12:latest

WORKDIR /

# Binary'yi kopyala
COPY --from=builder /app/main /main

# Distroless imajlarında varsayılan olarak nonroot kullanıcısı hazırdır
USER nonroot:nonroot

EXPOSE 8080

ENTRYPOINT ["/main"]

Bu değişiklikten sonra imaj boyutumuz yaklaşık 10-12 MB bandına çekilir. En önemlisi, container’ın saldırı yüzeyini (attack surface) neredeyse sıfıra indirmiş olduk. Container içinde /bin/sh olmadığı için uzaktan kod çalıştırma (RCE) açıklarının sömürülmesi imkansıza yakın hale gelir.

Güvenlik Tarama (Security Scanning) Testi

Yaptığımız bu optimizasyonların güvenlik tarafındaki yansımasını görmek için popüler açık kaynaklı tarama aracı Trivy kullanabiliriz. İlk yazdığımız 850 MB’lık imajı tarattığımızda muhtemelen düzinelerce “High” ve “Critical” seviyeli işletim sistemi açığı ile karşılaşacaktık.

Distroless imajımızı taratmak için terminalde şu komutu çalıştırabiliriz:

trivy image my-distroless-app:latest

Çıktıda göreceğiniz üzere, işletim sistemi seviyesindeki açık sayısı (Vulnerabilities) sıfıra yakın çıkacaktır. Bu, DevSecOps süreçlerinizi otomatize ederken security gate’lerden (güvenlik bariyerleri) takılmadan geçmenizi sağlar.

Özet ve Best Practice’ler

Özetlemek gerekirse, production seviyesinde container imajları hazırlarken şu altın kuralları asla unutmamalıyız:

  • Her zaman multi-stage build kullanın. Derleme ortamı ile çalışma ortamını birbirinden kesin çizgilerle ayırın.
  • Değişme sıklığı en az olan adımları (bağımlılık yükleme) Dockerfile’ın en üstüne, sık değişenleri (application code) en altına yazarak layer caching avantajını maksimize edin.
  • Çalışma ortamında Alpine veya daha iyisi distroless imajları tercih edin.
  • Container’ları asla root kullanıcısı ile çalıştırmayın; Dockerfile içinde mutlaka yetkisiz bir kullanıcı (non-root) tanımlayın.

Bu pratikleri uygulayarak hem bulut maliyetlerinizi (network egress ve storage) düşürebilir hem de çok daha güvenli ve hızlı deployment süreçlerine sahip olabilirsiniz. Unutmayın, en iyi imaj, içinde ihtiyacınız olmayan hiçbir şey barındırmayan imajdır!

Category: Genel | LEAVE A COMMENT
Ekim 25 2024

Terraform State Yönetimi: Remote Backend ve State Locking

Modern devops dünyasında, Altyapıyı Kod Olarak Yönetmek (iac) denince akla gelen ilk araç tartışmasız terraform. Ancak projeler büyüdükçe ve ekip arkadaşlarınızla aynı cloud kaynakları üzerinde çalışmaya başladığınızda, o meşhur local terraform.tfstate dosyası tam bir kabusa dönüşebilir. Bu yazımızda, state yönetimini local’den kurtarıp AWS üzerinde S3 ve DynamoDB kullanarak nasıl kurumsal seviyede güvenli, kilitlenebilir (state locking) ve ölçeklenebilir bir yapıya kavuşturacağımızı adım adım inceleyeceğiz.

Eğer production ortamınızda hala local state kullanıyorsanız, muhtemelen adrenaline aşırı bağımlı bir hayat yaşıyorsunuzdur. Gelin, o state dosyasını ait olduğu yere, buluta taşıyalım ve işleri profesyonelleştirelim.

Local State’in Vedası: Neden Remote Backend?

Terraform ile çalışırken oluşturduğunuz her kaynak, default olarak projenizin kök dizinindeki terraform.tfstate dosyasına kaydedilir. Tek başınıza çalışırken bu durum idare edilebilir gibi görünse de, ekibe ikinci bir kişi dahil olduğu anda şu sorunlar baş gösterir:

  • Race Condition (Yarış Durumu): Aynı anda iki mühendisin terraform apply çalıştırması, kaynakların çakışmasına ve state dosyasının bozulmasına neden olur.
  • Güvenlik ve Hassas Veriler: State dosyası, oluşturulan kaynakların şifrelerini, private key’lerini ve tüm meta datasını plain-text (açık metin) olarak tutar. Bu dosyayı Git deposuna göndermek, güvenlik ekiplerinin kapınıza dayanması için en kestirme yoldur.
  • Tek Nokta Hatası (Single Point of Failure): Bilgisayarınızın diski bozulduğunda veya kahve döküldüğünde tüm altyapı geçmişiniz yok olur.

Bu sorunların çözümü, state dosyasını merkezi ve güvenli bir yerde tutmaktır. AWS ekosisteminde bunun endüstri standardı karşılığı S3 (Storage) + DynamoDB (Locking) ikilisidir.

Adım Adım S3 ve DynamoDB Backend Kurulumu

Buradaki en büyük ironi şudur: Remote backend’i oluşturmak için de Terraform kullanmak isteriz. Ancak backend henüz var olmadığı için, bootstrap aşamasında local state kullanıp, kaynakları oluşturduktan sonra state’i yeni oluşturduğumuz bu remote backend’e migrate ederiz (taşırız).

1. S3 Bucket ve DynamoDB Kaynaklarının Tanımlanması

Öncelikle backend kaynaklarımızı tanımlayacağımız bir main.tf dosyası oluşturalım. S3 bucket’ımızın versiyonlamaya (versioning) açık olması hayati önem taşır. Bu sayede olası bir state bozulmasında geçmişe dönebiliriz.

provider "aws" {
  region = "eu-west-1"
}

# State dosyasını saklayacağımız S3 Bucket
resource "aws_s3_bucket" "terraform_state" {
  bucket        = "kertenkerem-tf-state-bucket"
  force_destroy = false

  lifecycle {
    prevent_destroy = true
  }
}

# S3 Bucket versiyonlamasını aktif ediyoruz
resource "aws_s3_bucket_versioning" "state_versioning" {
  bucket = aws_s3_bucket.terraform_state.id
  versioning_configuration {
    status = "Enabled"
  }
}

# State dosyasının diskte şifrelenmesi (Encryption)
resource "aws_s3_bucket_server_side_encryption_configuration" "state_encryption" {
  bucket = aws_s3_bucket.terraform_state.id

  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm = "AES256"
    }
  }
}

# State locking mekanizması için DynamoDB Tablosu
resource "aws_s3_bucket_public_access_block" "block_public" {
  bucket = aws_s3_bucket.terraform_state.id

  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

resource "aws_dynamodb_table" "terraform_locks" {
  name         = "kertenkerem-tf-state-locks"
  billing_mode = "PAY_PER_REQUEST"
  hash_key     = "LockID"

  attribute {
    name = "LockID"
    type = "S"
  }
}

Kritik Detay: DynamoDB tablosunun partition key (hash_key) değeri tam olarak LockID (büyük-küçük harf duyarlı) olmalıdır. Terraform, kilit bilgisi yazarken bu spesifik anahtarı arar.

Bu aşamada projemizi initialize edip kaynakları AWS üzerinde oluşturalım:

terraform init
terraform apply

2. Backend Konfigürasyonunun Eklenmesi

Kaynaklarımız AWS üzerinde başarıyla oluşturulduktan sonra, projemize bu backend’i kullanmasını söyleme zamanı geldi. Proje dizininde bir backend.tf dosyası oluşturalım:

terraform {
  backend "s3" {
    bucket         = "kertenkerem-tf-state-bucket"
    key            = "global/s3/terraform.tfstate"
    region         = "eu-west-1"
    dynamodb_table = "kertenkerem-tf-state-locks"
    encrypt        = true
  }
}

Büyük Göç: State Dosyasını Local’den S3’e Taşımak (Migration)

Backend tanımını ekledikten sonra Terraform’a bu değişikliği bildirmemiz gerekiyor. Terminale dönüp şu sihirli komutu çalıştırıyoruz:

terraform init

Terraform, local’de mevcut bir state dosyası olduğunu ve konfigürasyonda yeni bir S3 backend tanımlandığını algılayacaktır. Karşınıza şu şekilde bir uyarı gelecektir:

Do you want to copy existing state to the new backend?
  Pre-existing state was found while migrating the previous "local" backend to the
  newly configured "s3" backend. No existing state was found in the "s3" backend.
  Do you want to copy this state to the new "s3" backend? Enter "yes" to copy and "no"
  to start with an empty state.

  Enter a value: yes

Bu soruya yes yanıtını verdiğinizde, Terraform yerel diskinizdeki tüm state geçmişini güvenli bir şekilde AWS S3 bucket’ına yükler. Artık projenizin kök dizinindeki local terraform.tfstate dosyasını güvenle silebilirsiniz (yine de silmeden önce yedeklemek her zaman iyi bir refleksdir).

Kriz Anı: State Lock Çakışması ve “force-unlock” Sanatı

Remote backend’imizin en güzel yanı, birisi terraform apply veya terraform plan çalıştırdığında DynamoDB’ye bir lock (kilit) kaydı yazmasıdır. Bu esnada başka biri aynı işlemi yapmaya çalışırsa, Terraform işlemi durdurur ve şu hatayı fırlatır:

Error: Error acquiring the state lock
Error info:
   ID:     e9b72a44-dfc1-4b11-9a73-519280d8f0f0
   Path:   kertenkerem-tf-state-bucket/global/s3/terraform.tfstate
   Who:    ahmet@kertenkerem-macbook
   Version: 1.5.0
   Created: 2023-10-27 12:00:00 +0000 UTC
   Info:    org: kertenkerem-devops

Peki ya bu kilitlenme, Jenkins/GitLab pipeline’ınızın yarıda kesilmesi, internetinizin kopması veya Docker container’ının crash olması nedeniyle havada asılı kaldıysa? Kimse işlem yapamadığı için deployment’lar kilitlenir.

Çözüm: force-unlock

Böyle bir durumda, hatanın size verdiği Lock ID‘yi kullanarak kilidi manuel olarak kırmak zorundasınız. Ancak dikkatli olun: Bu komutu çalıştırmadan önce, kilidin gerçekten “sahipsiz” kaldığından ve arka planda başka bir mühendisin veya pipeline’ın canlı bir apply işlemi yürütmediğinden emin olmalısınız.

terraform force-unlock e9b72a44-dfc1-4b11-9a73-519280d8f0f0

Komut başarıyla çalıştığında DynamoDB üzerindeki kilit kaydı silinecek ve pipeline’larınız tekrar çalışabilir hale gelecektir.

Workspace Stratejileri: Hangisini Seçmeliyiz?

Birden fazla ortamı (dev, stage, prod) yönetirken state dosyalarını nasıl izole edeceğiz? Burada iki temel yaklaşım var: Terraform Workspace CLI ve Directory-based (Klasör Tabanlı) İzolasyon.

Yöntem A: Terraform Workspace CLI

Terraform’un built-in workspace özelliğidir. Tek bir konfigürasyon kodunu kullanarak arka planda farklı state dosyaları oluşturur.

terraform workspace new dev
terraform workspace new prod

# Geçiş yapmak için:
terraform workspace select dev

S3 backend tarafında Terraform, state dosyalarınızı otomatik olarak env:/dev/global/s3/terraform.tfstate ve env:/prod/global/s3/terraform.tfstate yollarına kaydeder.

Neden Dikkatli Olunmalı? Workspace’ler CLI üzerinden kolayca değiştirilebildiği için, yanlışlıkla prod ortamındayken dev zannedip yıkıcı bir apply komutu çalıştırmak çok kolaydır. Ayrıca her iki ortam da aynı AWS account’unu ve aynı yetki sınırlarını paylaşıyorsa, güvenlik zafiyeti doğurabilir.

Yöntem B: Klasör Tabanlı İzolasyon (Önerilen)

Büyük ölçekli ve multi-account (çoklu AWS hesabı) yapılarında en güvenli yol, ortamları dizin bazında tamamen ayırmaktır.

├── environments
│   ├── dev
│   │   ├── backend.tf  # Dev AWS Account S3 kovasını gösterir
│   │   ├── main.tf
│   │   └── variables.tf
│   └── prod
│       ├── backend.tf  # Prod AWS Account S3 kovasını gösterir
│       ├── main.tf
│       └── variables.tf

Bu yaklaşımda, Dev ve Prod ortamlarının state dosyaları tamamen farklı AWS hesaplarındaki S3 bucket’larında durur. Prod ortamına dokunmak için Prod AWS credentials’ına ihtiyacınız vardır. Dolayısıyla, local’deki bir kaza eseri tüm prod altyapısını silme ihtimaliniz sıfıra iner.

Son Sözler ve Altın Kurallar

Terraform ile profesyonel bir altyapı yönetmek istiyorsanız, state dosyanızı gözünüz gibi korumalısınız. Özetlemek gerekirse:

  • Asla ama asla local state ile production yönetmeyin.
  • S3 bucket’ınızda Versioning ve Encryption özelliklerini her zaman açık tutun.
  • State lock için mutlaka DynamoDB entegrasyonunu yapın.
  • Büyük ve kritik projelerde CLI workspace’leri yerine, dizin tabanlı (ve tercihen Terragrunt destekli) izole yapılar kurun.
  • .gitignore dosyanıza .terraform/, *.tfstate ve *.tfstate.backup maskelerini eklemeyi unutmayın.

State’iniz kilitli, pipeline’larınız yeşil kalsın!

Category: Genel | LEAVE A COMMENT
Eylül 20 2024

Bash’te Hata Yönetimi: set -euo pipefail ile Güvenli Script Yazımı

Hepimiz oradaydık: Gece yarısı gelen bir PagerDuty alarmı, çöken bir production sunucusu ve kaynağı belirsiz, yarıda kesilmiş bir deploy süreci. Loglara baktığınızda ise “exit 0” ile başarıyla tamamlanmış görünen ama arkasında enkaz bırakmış bir script görüyorsunuz. Modern devops ve altyapı otomasyonu dünyasında, linux üzerinde koşan bash scripting süreçleri hala sistemlerin can damarıdır. Ancak Kubernetes manifestleri veya Terraform kodları yazarken gösterdiğimiz özeni, maalesef bu kabuk script’lerine göstermiyoruz. “Çalışıyorsa dokunma” felsefesi, ilk production kazasına kadar kulağa hoş gelir.

Bu yazıda, Bash script’lerinizi “çocuk oyuncağı” olmaktan çıkarıp, kurumsal seviyede hata toleranslı ve güvenli hale getirecek teknikleri ele alacağız. Sadece komutları sıralamayacağız; arkasındaki “neden” sorusuna yanıt arayacağız.

Sessiz Katilleri Durdurun: set -euo pipefail

Bash’in varsayılan davranışı inanılmaz derecede affedicidir. Bir satır hata verse bile, script bir sonraki satırdan neşeyle çalışmaya devam eder. Bu durum, otomasyon süreçlerinde tam bir felaket senaryosudur. Bu vurdumduymazlığı engellemenin yolu, script’in en başına o sihirli satırı eklemektir:

#!/usr/bin/env bash
set -euo pipefail

Peki bu parametreler tam olarak ne işe yarıyor? Tek tek inceleyelim ve neden hayati olduklarını görelim.

1. set -e (Exit on Error)

Varsayılan olarak Bash, sıfırdan farklı (non-zero) bir exit code ile dönen komutları önemsemez. Örneğin, veritabanı yedeği almaya çalışan bir script düşünün:

pg_dump -U admin mydb > backup.sql
tar -czf backup.tar.gz backup.sql
aws s3 cp backup.tar.gz s3://my-bucket/

Eğer pg_dump komutu yetki hatasından dolayı başarısız olursa, script durmaz. Boş bir backup.sql dosyasını sıkıştırır ve S3’e yükler. Pipeline’ınız yeşil yanar ama elinizde yedek yoktur! set -e (veya uzun adıyla set -o errexit), herhangi bir komut başarısız olduğunda script’in anında sonlanmasını sağlar.

2. set -u (Nounset / Unset Variables)

Bash’te tanımlanmamış bir environment variable kullanmaya çalışırsanız, Bash bunu sessizce boş bir string olarak kabul eder. Şu meşhur felaket senaryosuna bakalım:

TARGET_DIR="" # Bir hata sonucu boş kaldı
rm -rf "$TARGET_DIR/*"

Eğer set -u (veya set -o nounset) aktif değilse, bu komut rm -rf /* olarak çalışacak ve sisteminizi silecektir. Bu parametre açık olduğunda, tanımlanmamış bir değişken kullanıldığı anda Bash çalışmayı durdurur ve unbound variable hatası verir.

3. set -o pipefail

İşte en çok gözden kaçan parametre. set -e tek başına pipeline (boru hattı) kullanan komutlardaki hataları yakalayamaz. Örneğin:

non_existent_command | grep "foo"

Burada ilk komut hata verecektir (exit code 127). Ancak pipeline’ın toplam exit code’u, en son komutun (yani grep’in) exit code’udur. Grep başarıyla çalıştığı için (veya eşleşme bulamadığı için) tüm satır başarılı kabul edilir. set -o pipefail eklediğimizde, pipeline içindeki herhangi bir komut hata verirse, tüm zincir başarısız kabul edilir.

Geri Temizlik (Cleanup) ve Trap Mekanizması

Script’imiz hata aldığında veya yarıda kesildiğinde (örneğin kullanıcı Ctrl+C yaptığında), arkasında geçici dosyalar, kilit (lock) dosyaları veya açık portlar bırakabilir. Linux dünyasında bu durum “resource leak” olarak adlandırılır. Bash’in sunduğu trap mekanizması, script nasıl sonlanırsa sonlansın (ister başarıyla, ister hata ile) çalışacak temizlik rutinleri yazmamızı sağlar.

Aşağıdaki örneği inceleyelim:

#!/usr/bin/env bash
set -euo pipefail

# Geçici bir dosya oluşturalım
TEMP_FILE=$(mktemp /tmp/api_response.XXXXXX)

# Temizlik fonksiyonu
cleanup() {
    echo "⚙️ Temizlik yapılıyor: ${TEMP_FILE} siliniyor..."
    rm -f "$TEMP_FILE"
}

# EXIT sinyalini yakala ve cleanup fonksiyonunu çalıştır
trap cleanup EXIT

# Script ana gövdesi
echo "Veri çekiliyor..."
curl -s https://api.kertenkerem.net/status > "$TEMP_FILE"

# Eğer burada bir hata olursa bile, trap sayesinde cleanup çalışacaktır.
grep -q "SUCCESS" "$TEMP_FILE"
echo "İşlem başarıyla tamamlandı."

Burada trap cleanup EXIT tanımı sayesinde, script normal bir şekilde bittiğinde veya aradaki bir komut hata verip script’i sonlandırdığında cleanup fonksiyonu otomatik olarak tetiklenir.

Hata Detaylarını Yakalamak: ERR Sinyali

Sadece temizlik yapmak yetmez, bazen hatanın hangi satırda ve hangi fonksiyon içinde gerçekleştiğini loglamak isteriz. Bunun için ERR sinyalini yakalayabiliriz:

failure_handler() {
    local exit_code=$?
    local line_no=$1
    echo "❌ HATA: Script ${line_no}. satırda, exit code ${exit_code} ile çöktü!" >&2
    # Buraya Slack/Teams webhook entegrasyonu eklenebilir.
}

trap 'failure_handler ${LINENO}' ERR

Exit Code Standartları

Yazdığınız script’lerin birer “iyi vatandaş” olması gerekir. Yani başka bir program (örneğin Jenkins, GitHub Actions veya GitLab CI) sizin script’inizi çağırdığında, neyin yanlış gittiğini sadece loglardan değil, exit code’dan da anlayabilmelidir.

Her zaman sadece exit 1 kullanmak tembelliktir. POSIX standartlarına göre bazı exit code’ların özel anlamları vardır:

  • 0: Başarılı sonlanma.
  • 1: Genel bilinmeyen hatalar.
  • 2: Hatalı argüman veya CLI parametresi kullanımı (Misuse of shell builtins).
  • 126: Komut çalıştırılamadı (Permission denied / Not executable).
  • 127: Komut bulunamadı (Command not found).
  • 128+n: Fatal error signal “n” (Örn: Ctrl+C ile sonlandırma için 130).

Kendi script’lerinizde özel hata durumları için 64-113 arasındaki değerleri kullanabilirsiniz. Örneğin:

readonly ERR_DB_CONNECTION=74
readonly ERR_INVALID_CONFIG=78

if ! ping -c 1 "$DB_HOST" &> /dev/null; then
    echo "Veritabanına erişilemiyor!" >&2
    exit "$ERR_DB_CONNECTION"
fi

Bash Script Test Edilir mi? Karşınızda: Bats (Bash Automated Testing System)

“Script’i yazdım, bir kere manuel çalıştırdım, çalışıyor” mantığı modern DevOps pratiklerine aykırıdır. Altyapı kodunuz değiştikçe, yazdığınız script’lerin de test edilmesi gerekir. Bunun için en popüler framework Bats-core‘dur.

Bats, script’lerinizi gerçek assertion’lar ile test etmenizi sağlayan TAP (Test Anything Protocol) uyumlu bir test aracıdır.

Örnek Bir Bats Testi

Öncelikle test etmek istediğimiz küçük bir fonksiyonumuz olsun (helper.sh):

# helper.sh
is_semver() {
    local version=$1
    if [[ $version =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
        return 0
    else
        return 1
    fi
}

Şimdi bu fonksiyon için yazacağımız test dosyası (helper.bats):

# helper.bats
setup() {
    source ./helper.sh
}

@test "Geçerli semver formatını doğrula" {
    run is_semver "1.2.3"
    [ "$status" -eq 0 ]
}

@test "Geçersiz semver formatını reddet" {
    run is_semver "v1.2"
    [ "$status" -eq 1 ]
}

CI/CD pipeline’ınızda bats helper.bats komutunu çalıştırarak bu testleri otomatik hale getirebilirsiniz. Böylece birisi script’i refactor ettiğinde bir şeylerin kırılıp kırılmadığını anında görebilirsiniz.

Özet: Kurşun Geçirmez Bir Bash Şablonu

Tüm bu öğrendiklerimizi bir araya getiren, yeni projelerinizde doğrudan kopyalayıp kullanabileceğiniz güvenli bir Bash şablonu ile yazıyı sonlandıralım:

#!/usr/bin/env bash

# Güvenlik flag'leri
set -euo pipefail
IFS=$'\n\t'

# Script dizinini bul (Path bağımsız çalışabilmek için)
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

# Global Hata Yakalama
error_handler() {
    local exit_code=$?
    local line_no=$1
    echo "❌ Hata oluştu! Satır: ${line_no} | Exit Code: ${exit_code}" >&2
    cleanup
    exit "$exit_code"
}

cleanup() {
    echo "🧹 Geçici kaynaklar temizleniyor..."
    # Temizlik komutları buraya
}

# Sinyalleri dinle
trap 'error_handler ${LINENO}' ERR
trap cleanup EXIT

# Ana Kod
main() {
    echo "🚀 Script başlatılıyor, dizin: ${SCRIPT_DIR}"
    # Uygulama mantığınız buraya gelecek
}

main "$@"

Bash scripting, doğru yapılandırılmadığında production ortamlarında saatli bir bombaya dönüşebilir. Ancak yukarıdaki pratikleri benimseyerek, kodunuzun öngörülebilir, test edilebilir ve en önemlisi güvenli olmasını sağlayabilirsiniz.

Category: Genel | LEAVE A COMMENT
Eylül 13 2024

Elastic Fleet ile Merkezi Agent Yönetimi: Elveda Config Karmaşası!

DevOps dünyasında monitoring ve log yönetimi denince akla gelen ilk isimlerden biri şüphesiz Elastic Stack. Ancak her sunucuya ayrı Filebeat, Metricbeat, Heartbeat kurup, Ansible playbook’ları içinde YAML indentasyon hatalarıyla boğuştuğumuz günler geride kaldı. Modern altyapılarda artık “single binary” felsefesi ve merkezi yönetim ön planda. İşte bu noktada sahneye elastic stack’in kurtarıcısı olan fleet ve onun her işe koşan askeri agent çıkıyor.

Bu makalede, production ortamlarında hayat kurtaran Elastic Fleet mimarisini, Fleet Server kurulumunu, policy yönetimini ve sisteme dinamik entegrasyonlar eklemeyi derinlemesine inceleyeceğiz. Çayınızı kahvenizi alın; Ansible playbook’larınızdaki Beats rollerini silmeye hazırlanıyoruz.

Neden Elastic Fleet? Eski Usul Beats vs. Modern Agent

Eski günleri hatırlayalım. Bir web sunucunuz var ve hem sistem metriklerini (CPU, RAM) hem de Nginx loglarını toplamak istiyorsunuz. Süreç kabaca şöyle işliyordu:

  • Sunucuya Metricbeat indir, kur, metricbeat.yml konfigüre et, servisi başlat.
  • Filebeat indir, kur, Nginx modülünü aktif et, filebeat.yml içinde Elasticsearch adresini gir, servisi başlat.
  • Elasticsearch cluster adresi veya şifresi değiştiğinde, tüm sunuculardaki o meşhur YAML dosyalarını CM (Configuration Management) araçlarıyla güncelle.

Bu yaklaşım ölçeklendikçe tam bir kabusa dönüşüyor. Elastic Agent ise bu karmaşayı tek bir binary altında topluyor. Agent arka planda gereken tüm “Beat” işlevlerini barındırıyor ve en önemlisi, konfigürasyonunu lokal bir dosyadan değil, doğrudan Kibana üzerinde koşan Fleet Server’dan dinamik olarak çekiyor. Siz Kibana arayüzünden tek bir tıkla yeni bir log kaynağı eklediğinizde, hedef sunucudaki Elastic Agent saniyeler içinde yeni policy’yi uyguluyor. Ne restart gerekiyor, ne de sunucuya SSH atmak.

Fleet Server Mimarisi: Olayın Arkasındaki Beyin

Fleet yapısını kurmadan önce mimariyi anlamak şart. Fleet, merkezi yönetim katmanıdır ve Kibana ile entegre çalışır. Ancak Kibana, binlerce agent ile doğrudan ham veri alışverişi yapacak şekilde tasarlanmamıştır. Bu yüzden araya Fleet Server girer.

Fleet Server, Elastic Agent’ların bağlandığı, policy güncellemelerini aldığı ve durum raporladığı bir kontrol merkezidir. Kendisi de aslında özel bir policy ile çalışan bir Elastic Agent’tır. Agent’lar Fleet Server’a HTTPS üzerinden bağlanır (genellikle port 8220). Fleet Server ise aldığı durumları ve metrikleri doğrudan Elasticsearch’e yazar.

Adım Adım Fleet Server Kurulumu (Self-Hosted)

Eğer Elastic Cloud kullanıyorsanız Fleet Server sizin için otomatik olarak yönetilir. Ancak gerçek DevOps mühendisleri olarak biz self-hosted yapıları severiz (veya mecbur kalırız). Gelin, kendi altyapımızda Fleet Server’ı Docker üzerinde ayağa kaldıralım.

Adım 1: Elasticsearch ve Kibana Hazırlığı

Öncelikle halihazırda HTTPS korumalı bir Elasticsearch cluster’ınız ve Kibana’nız olduğunu varsayıyoruz. Fleet Server’ın Elasticsearch ile haberleşebilmesi için bir “Service Token” üretmemiz gerekiyor. Kibana Console (Dev Tools) üzerinden şu komutla token’ımızı alalım:

POST _security/service/elastic/fleet-server/token/my-fleet-token

Dönen yanıttaki value değerini güvenli bir yere kaydedin. Bu bizim FLEET_SERVER_ELASTICSEARCH_SERVICE_TOKEN değerimiz olacak.

Adım 2: Fleet Server’ı Docker ile Çalıştırma

Aşağıdaki Docker komutu ile Fleet Server’ı ayağa kaldırabiliriz. Üretim ortamında mutlaka geçerli SSL sertifikaları kullanmalısınız, ancak test ortamı için doğrulama adımlarını esnetebiliriz.

docker run -d \
  --name fleet-server \
  -p 8220:8220 \
  -e FLEET_SERVER_ENABLE=1 \
  -e FLEET_SERVER_ELASTICSEARCH_HOST=https://elasticsearch.local:9200 \
  -e FLEET_SERVER_ELASTICSEARCH_SERVICE_TOKEN=AAEAAWVsYXN0aWMvZmxlZXQtc2VydmVyL3Rva2VuLTE2N... \
  -e FLEET_URL=https://fleet-server.local:8220 \
  -e FLEET_SERVER_CERT=/usr/share/elastic-agent/certs/fleet.crt \
  -e FLEET_SERVER_KEY=/usr/share/elastic-agent/certs/fleet.key \
  -v /path/to/certs:/usr/share/elastic-agent/certs \
  docker.elastic.co/beats/elastic-agent:8.11.1

Bu komutla Fleet Server, 8220 portundan agent bağlantılarını kabul etmeye hazır hale gelecektir. Kibana arayüzünde Management > Fleet sekmesine gittiğinizde Fleet Server’ın “Healthy” durumuna geçtiğini görmelisiniz.

Elastic Agent Kurulumu ve Fleet’e Enroll Etme

Sırada log ve metrik toplayacağımız hedef sunuculara (örneğin bir Linux VM) Elastic Agent kurmak var. Agent kurulumu son derece basittir çünkü tek bir binary indirip çalıştırmaktan ibarettir.

Adım 1: Agent Paketini İndirme

Hedef sunucuda terminale çıkıp işletim sistemimize uygun paketi indiriyoruz:

curl -L -O https://artifacts.elastic.co/downloads/beats/elastic-agent/elastic-agent-8.11.1-linux-x86_64.tar.gz
tar xzvf elastic-agent-8.11.1-linux-x86_64.tar.gz
cd elastic-agent-8.11.1-linux-x86_64

Adım 2: Enrollment (Kayıt) İşlemi

Agent’ı sisteme kurarken onu Fleet Server’a “enroll” etmemiz, yani kaydetmemiz gerekir. Bunun için Kibana > Fleet > Enrollment Tokens sekmesinden hedef policy için üretilmiş token’ı alıyoruz. Ardından şu komutla kurulumu tetikliyoruz:

sudo ./elastic-agent install \
  --url=https://fleet-server.local:8220 \
  --enrollment-token=U0ZVMWhvMEJGUnp6eE5iNUp0eXU6cUpnZXA3V... \
  --certificate-authorities=/path/to/ca.crt

Bu komut arka planda şunları yapar:

  • Agent’ı sistem servisi olarak kaydeder (systemd).
  • Gerekli dizin yapılarını (/etc/elastic-agent, /var/log/elastic-agent) oluşturur.
  • Fleet Server ile el sıkışarak ilk policy’sini indirir ve servisi başlatır.

Servisin durumunu doğrulamak için:

sudo systemctl status elastic-agent

Policy Yönetimi: Tek Merkezden Gücü Dağıtmak

Elastic Fleet’in en güçlü yanı policy tabanlı yönetimdir. Bir policy; hangi logların toplanacağını, hangi metriklerin hangi sıklıkla çekileceğini belirten bir kurallar bütünüdür. Sunucularınızı rollerine göre gruplayabilirsiniz (örn: web-servers-policy, db-servers-policy).

Bir sunucunun davranışını değiştirmek istediğinizde, tek yapmanız gereken Kibana arayüzünden ilgili policy’yi editlemektir. Örneğin, tüm web sunucularında Apache log takibini kapatıp yerine Nginx log takibini açmak istiyorsunuz. Bu değişikliği policy üzerinde yaptığınız an, o policy’ye bağlı olan 500 sunucudaki Elastic Agent saniyeler içinde kendini günceller.

Pratik Örnek: Nginx Entegrasyonu Ekleme

Gelin, çalışan bir Elastic Agent’a Kibana üzerinden nasıl dinamik entegrasyon ekleyeceğimize bakalım:

  1. Kibana > Fleet > Agent Policies sayfasına gidin.
  2. Agent’ınızın bağlı olduğu policy’yi seçin.
  3. Add Integration butonuna tıklayın.
  4. Arama çubuğuna Nginx yazın ve entegrasyonu seçin.
  5. Nginx log dosyalarının yollarını (varsayılan: /var/log/nginx/access.log) ve metrik endpoint’ini (stub_status) konfigüre edin.
  6. Save Integration butonuna basarak kaydedin.

Arkanıza yaslanın. Hedef sunucudaki agent, konfigürasyon değişikliğini algılayacak, arka planda Nginx loglarını okumaya başlayacak ve Kibana’da hazır gelen “Nginx Overview” dashboard’larını verilerle dolduracaktır. Sıfır konfigürasyon dosyası editlendi, sıfır servis restart edildi!

Prodüksiyon Tavsiyeleri ve Savaş Hikayeleri

Her şey kağıt üzerinde harika görünse de, production ortamlarında Fleet koştururken dikkat etmeniz gereken bazı can alıcı noktalar var:

1. Resource Limits (Kaynak Sınırları)

Elastic Agent, eski Beats’lere göre biraz daha fazla memory tüketebilir çünkü arka planda birden fazla süreci yönetir. Özellikle çok sayıda log dosyası (wildcard ile binlerce log) izleniyorsa, agent memory limitlerine takılabilir. Sunucularınızda elastic-agent‘ın CPU ve RAM tüketimini mutlaka monitor edin.

2. SSL/TLS Sertifikaları

Fleet Server ile Agent arasındaki iletişim kesinlikle HTTPS olmalıdır. Self-signed sertifika kullanıyorsanız, agent kurulumu sırasında --certificate-authorities parametresiyle CA sertifikanızı belirtmeyi unutmayın. Aksi takdirde agent’lar “x509: certificate signed by unknown authority” hatasıyla kaydolmayacaktır.

3. Fleet Server Ölçekleme

Tek bir Fleet Server kabaca 1000 ila 2000 agent’a kadar sorunsuz hizmet verebilir. Ancak binlerce agent’ın olduğu büyük altyapılarda, Fleet Server’ı arkasında bir Load Balancer (HAProxy, AWS ALB vb.) olacak şekilde multi-node mimaride kurmalısınız.

Fleet Server’ları ölçeklerken şu parametre ile JVM heap boyutunu ve max connection limitlerini optimize edebilirsiniz:

# Örnek environment değişkeni
-e FLEET_SERVER_MAX_CONNECTIONS=5000

Özet

Elastic Fleet ve Agent ikilisi, modern DevOps dünyasındaki monitoring operasyonlarını büyük ölçüde kolaylaştırıyor. Altyapınızı kodla yönetirken (IaC), sunuculara sadece tek bir agent kurup geri kalan tüm konfigürasyonu UI veya Fleet API’leri üzerinden yönetmek, operasyonel yükü ciddi oranda azaltır. Eğer hala eski usul Beats konfigürasyonlarıyla uğraşıyorsanız, Fleet’e geçiş planını hemen sprint’inize eklemenizi öneririm.

Category: Genel | LEAVE A COMMENT
Ağustos 30 2024

Elasticsearch ILM (Index Lifecycle Management) ile Disk Tasarrufu

Her devops mühendisinin kabusudur: Gece yarısı gelen ve uykuyu piç eden o meşhur “Disk Space Low” uyarısı. Hele ki bu uyarı, log analizi için canla başla koşturduğumuz, saniyede on binlerce satır log yutan bir elasticsearch cluster’ından geliyorsa durum daha da can sıkıcı olur. Sürekli büyüyen log yığınlarını yönetmek, sadece daha fazla ve daha pahalı disk satın almakla çözülmez; akıllıca bir ilm (Index Lifecycle Management) stratejisi kurmak gerekir. Bu yazımızda, bütçe dostu ve yüksek performanslı hot-warm-cold-delete mimarisini nasıl kuracağımızı, shrink ve rollover operasyonlarının perde arkasını pratik örneklerle inceleyeceğiz.

Neden Sadece “Daha Büyük Disk” Çözüm Değil?

Kapasite sorunuyla karşılaştığımızda ilk refleks genellikle disk boyutunu artırmak veya cluster’a yeni node’lar eklemek olur. Ancak bu yaklaşım sürdürülebilir değildir. Neden mi?

  • Maliyet: Üretim ortamlarında hızlı yazma (indexing) performansı için NVMe SSD diskler kullanırız. 90 günlük log verisinin tamamını bu pahalı disklerde tutmak bütçeyi sarsar.
  • Performans: Eski ve nadiren sorgulanan loglar, aktif olarak yazılan güncel index’ler ile aynı kaynakları (özellikle heap memory) tüketerek sorgu performansını düşürür.
  • Yönetilebilirlik: Tek bir devasa index yerine, yönetilebilir boyutlarda shard’lara bölünmüş bir yapı hem yedekleme (snapshot) hem de cluster kurtarma süreçlerinde hayat kurtarır.

Hot-Warm-Cold-Delete Mimarisi Nedir?

Verinin yaşlandıkça değerini kaybetmesi felsefesine dayanan bu mimari, veriyi yaşam döngüsüne göre farklı donanım özelliklerine sahip node gruplarına dağıtır:

  • Hot Phase (Sıcak Evre): Yeni logların yazıldığı, yoğun CPU ve NVMe SSD disklere sahip node’lardır. Veri tazedir ve sürekli sorgulanır.
  • Warm Phase (Ilık Evre): Veri artık yazılmaz (read-only), ancak sorgulanmaya devam eder. Daha ucuz SATA SSD diskler barındıran node’lara taşınır. Burada shard sayısı azaltılarak (shrink) sistem üzerindeki yük hafifletilir.
  • Cold Phase (Soğuk Evre): Nadiren sorgulanan, arşiv niteliğindeki verilerdir. HDD tipi ucuz depolama birimlerinde saklanır. Arama performansı düşüktür ama maliyeti minimumdur.
  • Delete Phase (Silme Evresi): Belirlenen saklama süresi (retention period) dolan verilerin güvenli bir şekilde yok edildiği evredir.

Adım Adım Elasticsearch ILM Kurulumu

Gelin, teoriyi pratiğe dökelim. Senaryomuzda günlük uygulama loglarımızı toplayacağız, bunları 50 GB veya 30 gün sınırlamasıyla yeni index’lere böleceğiz (rollover), ardından shrink işlemiyle shard sayısını azaltıp daha ucuz disklere taşıyacağız ve 90 günün sonunda sileceğiz.

1. Node Rollerinin Tanımlanması

Öncelikle Elasticsearch cluster’ınızdaki node’ların hangi katmanda yer alacağını elasticsearch.yml dosyalarında belirtmeniz gerekir. Modern Elasticsearch versiyonlarında (7.10+) veri katmanları (data tiers) kullanılır:

# Hot Node konfigürasyonu
node.roles: [ master, data_content, data_hot ]

# Warm Node konfigürasyonu
node.roles: [ data_warm ]

# Cold Node konfigürasyonu
node.roles: [ data_cold ]

2. ILM Policy Tanımlama

Şimdi Elasticsearch API’sini kullanarak ILM politikamızı tanımlayalım. Bu politikada hot, warm, cold ve delete fazlarındaki geçiş kurallarını ve yapılacak operasyonları belirtiyoruz.

PUT _ilm/policy/kertenkerem_log_policy
{
  "policy": {
    "phases": {
      "hot": {
        "min_age": "0ms",
        "actions": {
          "rollover": {
            "max_primary_shard_size": "50gb",
            "max_age": "30d"
          }
        }
      },
      "warm": {
        "min_age": "0d",
        "actions": {
          "shrink": {
            "number_of_shards": 1
          },
          "forcemerge": {
            "max_num_segments": 1
          },
          "allocate": {
            "number_of_replicas": 1
          }
        }
      },
      "cold": {
        "min_age": "60d",
        "actions": {
          "readonly": {}
        }
      },
      "delete": {
        "min_age": "90d",
        "actions": {
          "delete": {}
        }
      }
    }
  }
}

Buradaki “Neden?” Sorularını Cevaplayalım:

  • Neden max_primary_shard_size? Toplam index boyutu yerine tek bir primary shard boyutunu baz almak en doğru yaklaşımdır. Replica shard’lar hesaba katılmaz ve dengeli bir shard yapısı korunur.
  • Neden warm fazında shrink ve forcemerge yapıyoruz? Yazma işlemi bittiği için artık çoklu shard yapısına ihtiyacımız yok. Shard sayısını 1’e düşürerek (shrink) overhead’i azaltıyoruz. forcemerge ile segment sayısını 1’e indirip disk alanından ciddi tasarruf sağlıyor ve arama hızını optimize ediyoruz.

3. Index Template Oluşturma

Oluşturduğumuz ILM politikasının yeni açılacak log index’lerine otomatik olarak uygulanması için bir index template tanımlıyoruz. Burada dikkat etmemiz gereken en önemli nokta index.lifecycle.rollover_alias tanımıdır.

PUT _index_template/kertenkerem_logs_template
{
  "index_patterns": ["kertenkerem-app-logs-*"],
  "template": {
    "settings": {
      "index.lifecycle.name": "kertenkerem_log_policy",
      "index.lifecycle.rollover_alias": "kertenkerem-app-logs",
      "number_of_shards": 3,
      "number_of_replicas": 1
    }
  }
}

4. İlk Index’in Tetiklenmesi (Bootstrap)

Rollover mekanizmasının doğru çalışabilmesi için ilk index’i bizim manuel olarak oluşturmamız ve yazma iznini (write index) vermemiz gerekir. Index isminin sonunun sıralı bir sayı formatında (örn: -000001) bitmesi şarttır.

PUT kertenkerem-app-logs-000001
{
  "aliases": {
    "kertenkerem-app-logs": {
      "is_write_index": true
    }
  }
}

Bu adımdan sonra log toplayıcı araçlarınızın (Logstash, Fluentd, Vector vb.) doğrudan spesifik index ismine değil, sadece kertenkerem-app-logs alias’ına (takma ad) veri göndermesi gerekir. Elasticsearch, arka planda ILM kurallarına göre yeni index’ler açarak trafiği otomatik yönlendirecektir.

Shrink ve Rollover Operasyonlarının Kritik Detayları

Sistem tıkır tıkır çalışırken bazen arka planda işler karışabilir. DevOps mühendislerinin bu süreçte bilmesi gereken bazı “under the hood” detaylar şunlardır:

Shrink İşlemi Nasıl Gerçekleşir?

Bir index’i shrink edebilmek için Elasticsearch öncelikle o index’e ait tüm primary shard’ları cluster’daki tek bir node üzerinde toplamak zorundadır. ILM bunu otomatik olarak yönetir. Ancak o node üzerinde yeterli disk alanı yoksa shrink işlemi askıda (blocking) kalabilir. Bu yüzden warm node’larınızın disk kapasitesini planlarken bu geçici yoğunlaşmayı hesaba katmalısınız.

Neden Günlük Index (Daily Index) Yerine Rollover?

Eski usul logstash-YYYY.MM.DD yapısında, hafta sonu az log gelen günlerde de, hafta içi devasa log gelen günlerde de aynı sayıda index açılırdı. Bu durum cluster içinde “over-sharding” (aşırı shard birikmesi) sorununa yol açar ve master node’un canını okurdu. Rollover ise boyuta göre tetiklendiği için her shard’ın ideal boyut olan 30-50 GB aralığında kalmasını garanti eder.

Sonuç ve “DevOps Tavsiyeleri”

Bu makalede kurduğumuz ILM yapısı sayesinde, pahalı SSD disklerimizi sadece aktif olarak yazılan taze veriler için rezerve etmiş olduk. Yaşlanan logları daha ucuz disklere taşıyarak, shrink operasyonuyla cluster üzerindeki shard yükünü hafifleterek ve nihayetinde eski verileri silerek disk maliyetlerinde %60’a varan tasarruf sağlayabilirsiniz.

Unutmayın, iyi bir DevOps mühendisi sadece çalışan sistemler kurmaz; aynı zamanda şirketin bulut faturasını da optimize eden kişidir. Loglarınızı kendi haline bırakmayın, ILM ile onları disipline edin!

Category: Genel | LEAVE A COMMENT
Ağustos 16 2024

Prometheus Alerting Rules ve Recording Rules Yazma Rehberi

Gece saat 03:00. Telefonunuz acı acı çalıyor. Gözlerinizi kırpıştırarak ekrana bakıyorsunuz: “CPU Usage is High!”. Kalbiniz küt küt atarak bilgisayarı açıyor, Kubernetes cluster’ına bağlanıyor ve aslında her şeyin normal olduğunu, sadece anlık bir cron job’ın CPU’yu tetiklediğini görüyorsunuz. Tebrikler, “Alert Fatigue” (alarm yorgunluğu) kulübüne hoş geldiniz! Modern devops ve sre dünyasında, yanlış yapılandırılmış bir prometheus ve alerting altyapısı kadar ekipleri yıpratan çok az şey vardır. Bu rehberde, uykularınızı kaçırmayacak, gerçekten aksiyon alınabilir alert kuralları yazmayı ve recording rules kullanarak Prometheus sunucunuzun CPU’sunu nasıl rahatlatacağınızı konuşacağız.

1. Alert Felsefesi: “Semptom” vs. “Sebep”

Bir alert kuralı yazmadan önce kendimize sormamız gereken ilk soru şudur: “Bu alarm çaldığında birinin yataktan kalkıp hemen bir şey yapması gerekiyor mu?” Cevap hayır ise, o bir alert değil, sadece bir metric veya en fazla bir dashboard grafiğidir.

SRE dünyasında alert’leri ikiye ayırırız:

  • Semptom Tabanlı (Symptom-based): “Kullanıcılar 500 hatası alıyor.” (Hemen müdahale edilmeli!)
  • Sebep Tabanlı (Cause-based): “Node CPU kullanımı %92.” (Belki de Kubernetes bunu zaten scale edecek, neden uyandırılıyorum?)

Her zaman semptomlara alert kurmaya çalışın. CPU yükselmesi tek başına bir felaket değildir; eğer bu durum latency (gecikme) artışına veya hata oranının (error rate) yükselmesine sebep olmuyorsa, bırakın Prometheus sessizce metric toplamaya devam etsin.

2. Etkin Prometheus Alerting Rules Yazmak

Prometheus üzerinde alert tanımlarken PromQL dilinin gücünden faydalanırız. Ancak verimsiz yazılan sorgular hem Prometheus’u yorar hem de yanlış alarmlara (false positive) yol açar.

Kötü Bir Örnek ve Analizi

- alert: HighCPUUsage
  expr: node_cpu_seconds_total{mode="idle"} < 10
  for: 1m
  labels:
    severity: critical
  annotations:
    summary: "CPU usage is high on {{ $labels.instance }}"

Bu kural neden kötü?

  1. Çok kısa süre (for: 1m): 1 dakikalık bir CPU yükselmesi geçici bir spike (anlık sıçrama) olabilir. Hemen alarm tetiklemek anlamsızdır.
  2. Ham metric kullanımı: `node_cpu_seconds_total` doğrudan kullanılmamalı, rate() veya irate() fonksiyonları ile normalize edilmelidir.
  3. Aksiyon yok: “CPU yüksek” bilgisini alan bir mühendis ne yapmalı? Annotation içinde bir runbook linki yok.

Daha İyi Bir Yaklaşım

- alert: NodeCPUUtilizationHigh
  expr: (100 - (avg by (instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100)) > 85
  for: 15m
  labels:
    severity: warning
    team: platform
  annotations:
    summary: "Node {{ $labels.instance }} CPU utilization is above 85% for 15 minutes."
    description: "High CPU might impact application performance. Check Pod distribution."
    runbook_url: "https://wiki.kertenkerem.net/ops/runbooks/high-node-cpu"

Burada neyi değiştirdik? Sorguyu 5 dakikalık pencerelerde `rate` alacak şekilde optimize ettik, bekleme süresini (`for`) 15 dakikaya çıkararak geçici spike’ları eledik ve en önemlisi, alarmı alan mühendis için bir `runbook_url` ekledik.

3. Prometheus’u Coşturmak: Recording Rules

Prometheus her `evaluation_interval` süresinde (genelde 15-30 saniye) tüm alert kurallarını sıfırdan hesaplar. Eğer yukarıdaki gibi ağır matematiksel işlemler içeren yüzlerce kuralınız ve binlerce node/pod içeren bir cluster’ınız varsa, Prometheus sunucunuzun CPU ve Memory kullanımı tavan yapacaktır. İşte burada sahneye recording rules çıkıyor.

Recording rules, karmaşık ve sık çalıştırılan PromQL sorgularının sonuçlarını önceden hesaplayıp yeni bir metric olarak kaydetmenizi sağlar. Böylece alert kuralları veya Grafana dashboard’ları bu karmaşık sorguyu her seferinde çalıştırmak yerine, önceden hesaplanmış hazır veriyi okur.

Yavaş Sorgu Örneği (Kubernetes Pod CPU Utilization)

Grafana dashboard’unuzda veya alert kuralınızda her 15 saniyede bir şu sorgunun çalıştığını hayal edin:

sum by (namespace, pod) (rate(container_cpu_usage_seconds_total{container!=""}[5m]))

Bunu optimize etmek için bir recording rule tanımlayalım:

groups:
  - name: kertenkerem.cpu.rules
    rules:
      - record: namespace_pod:container_cpu_usage:rate5m
        expr: sum by (namespace, pod) (rate(container_cpu_usage_seconds_total{container!=""}[5m]))

Artık Prometheus arkada bu sorguyu sessizce çalıştırıp sonucu `namespace_pod:container_cpu_usage:rate5m` isimli yeni ve tertemiz bir metric olarak kaydedecek. Şimdi alert kuralımızı bu yeni metric üzerinden yazalım:

- alert: PodCPULimitApproaching
  expr: namespace_pod:container_cpu_usage:rate5m > 0.90
  for: 10m
  labels:
    severity: warning

Prometheus artık her sorgulamada diskten gigabaytlarca veri okuyup CPU hesaplaması yapmak zorunda kalmayacak. Doğrudan hazır hesaplanmış tek bir metric değerine bakacak. Disk I/O ve CPU tüketiminizde inanılmaz bir düşüş göreceksiniz.

4. Alertmanager ile Gürültüyü Engelleme: Inhibition ve Silence

Prometheus alert’i üretir, ancak bu alert’lerin nereye (Slack, PagerDuty, Webhook) nasıl gideceğini yöneten yer Alertmanager bileşenidir. Alert gürültüsünü kesmek için iki can kurtaran silahımız var: Inhibition Rules ve Silences.

Inhibition Rules (Engelleme Kuralları)

Bir veri merkezindeki tüm sunucuların bağlı olduğu ana switch çöktüğünde ne olur? Muhtemelen o switch arkasındaki 50 sunucu ve üzerindeki 500 pod için ayrı ayrı “Node Down” ve “Pod Down” alarmı alırsınız. Telefonunuz bildirim yağmurundan kilitlenir.

Inhibition rules, belirli bir ana alarm aktifken, onunla ilişkili alt alarmların susturulmasını sağlar. Klasik bir Alertmanager konfigürasyon örneği:

inhibit_rules:
  - source_matchers: [alertname="NodeNetworkDown"]
    target_matchers: [alertname="InstanceDown"]
    equal: ['node', 'instance']

Yukarıdaki kural der ki: Eğer bir node için `NodeNetworkDown` alarmı zaten tetiklendiyse, aynı node üzerindeki servisler için `InstanceDown` (servis ulaşılamaz) alarmı gönderme, onu block’la. Mühendis zaten ağın gittiğini biliyor, servislerin de gitmiş olacağını tahmin etmek için dahi olmaya gerek yok!

Silencing (Susturma) ve Maintenance

Planlı bir bakım çalışması (maintenance window) yapıyorsunuz ve Kubernetes cluster’ındaki bazı node’ları restart edeceksiniz. Bu sırada Slack kanalınızın “CRITICAL” loglarıyla dolmasını istemezsiniz.

Bunun için Alertmanager UI üzerinden veya `amtool` CLI aracını kullanarak hızlıca geçici “silence” tanımları yapabilirsiniz:

amtool silence add alertname="NodeCPUUtilizationHigh" node="node-01" --duration=2h --comment="Planli kernel upgrade calismasi"

Bu komut, `node-01` üzerindeki yüksek CPU alarmlarını 2 saat boyunca askıya alır. Bakım bittiğinde sistem otomatik olarak normal haline döner.

Sonuç: Temiz Alarmlar, Huzurlu Geceler

Prometheus ile monitoring kurmak kolaydır; ancak akıllıca tasarlanmış bir alarm yapısı kurmak gerçek bir mühendislik disiplini gerektirir. Özetlemek gerekirse:

  1. Sadece aksiyon almanız gereken durumlarda telefonunuzu çaldırın.
  2. Ağır PromQL sorgularınızı recording rules ile önceden hesaplayarak Prometheus’u rahatlatın.
  3. Alertmanager’ın inhibition kurallarını kullanarak kriz anlarında alarm bombardımanına tutulmaktan kaçının.

Bir sonraki yazımızda görüşmek üzere, metrics’iniz bol, geceleriniz sessiz olsun!

Category: Genel | LEAVE A COMMENT
Ağustos 9 2024

Grafana Loki ile Merkezi Log Yönetimi: ELK’nın Hantallığından Kaçış Rehberi

Sistem yöneticilerinin ve DevOps mühendislerinin ortak rüyası nedir? Tabii ki gece yarısı “Disk Full” uyarısıyla uyanmamak. Ancak modern mikroservis mimarilerinde observability sağlamak ve devasa log yığınları arasında arama yapmak tam bir karın ağrısı olabiliyor. Yıllarca ELK (Elasticsearch, Logstash, Kibana) üçlüsünün JVM bellek canavarlığıyla mücadele ettikten sonra, hafif, dinamik ve bütçe dostu bir alternatif arıyorsanız doğru yerdesiniz. Bu rehberde, grafana ekosisteminin parlayan yıldızı loki ile nasıl ölçeklenebilir ve az maliyetli bir log management altyapısı kuracağımızı adım adım inceleyeceğiz.

Neden Loki? (Yani, Neden Elasticsearch Değil?)

Elasticsearch harika bir arama motorudur; bunu kimse inkar edemez. Ancak log yönetimi söz konusu olduğunda, Elasticsearch her şeyi (log satırının tamamını) indexler. Bu durum devasa index boyutlarına ve dolayısıyla korkunç bir RAM tüketimine yol açar. Kendi sunucularında ELK koşturanlar, Elasticsearch pod’unun 16GB RAM’i “kahvaltı niyetine” tükettiğini çok iyi bilir.

Loki ise farklı bir felsefe benimser: “Prometheus, ama loglar için.”

Loki, log içeriğinin tamamını indexlemez. Sadece logların metadata’sını (yani label/etiketlerini; örneğin app="nginx", env="production") indexler. Log satırlarının kendisini ise sıkıştırılmış chunk’lar halinde doğrudan nesne depolama servislerinde (AWS S3, Google Cloud Storage veya lokalde MinIO) saklar. Bu yaklaşım bize ne kazandırır?

  • Düşük Altyapı Maliyeti: RAM tüketimi MB’lar seviyesine düşer, disk maliyeti ise S3 kullanımı sayesinde neredeyse devede kulak kalır.
  • Kolay Korelasyon: Prometheus metriklerinizle aynı etiketleri (labels) kullanarak metrikten loga tek tıkla geçiş yapabilirsiniz.
  • Kolay Ölçeklenme: Loki’nin mikroservis mimarisi sayesinde yazma (ingester) ve okuma (querier) katmanlarını birbirinden bağımsız olarak ölçekleyebilirsiniz.

Sistem Mimarisi: Parçaları Birleştirelim

Loki tek başına çalışmaz. Log yönetim sistemimiz üç temel bileşenden oluşur:

  1. Promtail (Agent): Logları toplar, etiketler (labeling) ve Loki’ye push eder (Kubernetes dünyasında her node üzerinde bir DaemonSet olarak koşar).
  2. Loki (Veri Deposu): Gelen logları alır, indeksler ve depolar.
  3. Grafana (Görselleştirme): Loki’yi bir datasource olarak ekler, LogQL sorguları yazar ve şık dashboard’lar oluştururuz.

Adım Adım Kurulum: Kubernetes Üzerinde Loki ve Promtail

Lafı fazla uzatmadan klavyenin başına geçelim. Kurulum için en pratik yol Helm kullanmaktır. Kubernetes cluster’ınızın hazır olduğunu varsayarak işlemlere başlıyoruz.

1. Helm Depolarını Ekleme

İlk olarak Grafana’nın resmi Helm reposunu sistemimize ekleyelim ve güncelleyelim:

helm repo add grafana https://grafana.github.io/helm-charts
helm repo update

2. Loki Kurulumu

Üretim ortamı için Loki’yi S3 entegrasyonu ile kurmak en doğrusudur. Ancak bu rehberde pratiklik açısından lokal disk (StatefulSet) kullanan basit bir kurulum yapacağız. Aşağıdaki loki-values.yaml dosyasını oluşturalım:

loki:
  commonConfig:
    replication_factor: 1
  storage:
    type: 'filesystem'
  auth_enabled: false

singleBinary:
  replicas: 1

persistence:
  enabled: true
  size: 10Gi
  storageClassName: "standard" # Kendi storage class'ınızla değiştirin

Şimdi Loki’yi bu konfigürasyonla kuralım:

helm install loki grafana/loki -f loki-values.yaml --namespace logging --create-namespace

3. Promtail Kurulumu

Promtail, Kubernetes cluster’ındaki pod loglarını otomatik olarak keşfedecek şekilde konfigüre edilmiştir. promtail-values.yaml dosyamızı oluşturalım. Burada en kritik nokta Promtail’e Loki’nin adresini doğru vermektir:

config:
  clients:
    - url: http://loki-gateway.logging.svc.cluster.local/loki/api/v1/push

# Kubernetes namespace ve pod etiketlerini otomatik olarak loglara eklemesi için relabeling kuralları hazır gelir.

Promtail’i kuralım:

helm install promtail grafana/promtail -f promtail-values.yaml --namespace logging

LogQL 101: Loglar İçinde Kaybolmadan Arama Yapmak

Artık loglarımız Loki’ye akıyor. Peki bunları nasıl sorgulayacağız? Karşınızda LogQL. Eğer Prometheus’un sorgu dili olan PromQL’e aşinaysanız, LogQL size çocuk oyuncağı gelecektir. LogQL sorguları iki ana bölümden oluşur: Log Stream Selector ve Filter Expression.

Basit Filtreleme

Belirli bir namespace altındaki, belirli bir uygulamanın loglarını çekmek ve içinde “error” geçen satırları yakalamak için:

{namespace="production", app="payment-api"} |= "error"

Eğer “timeout” içeren ama “connection” içermeyen logları istiyorsak zincirleme filtre kullanabiliriz:

{app="nginx"} |= "timeout" != "connection"

Parser ve Yapılandırılmış (Structured) Loglar

Eğer uygulamanız JSON formatında log üretiyorsa, Loki bu logları çalışma anında (on-the-fly) ayrıştırabilir. Bu, Loki’nin en güçlü özelliklerinden biridir:

{app="auth-service"} | json | status = "500"

Yukarıdaki sorguda | json ifadesi, log satırını JSON olarak parse eder ve içerideki tüm JSON anahtarlarını geçici etiketlere (labels) dönüştürür. Sonrasında status = "500" filtresi ile sadece HTTP 500 hatası veren istekleri süzebiliriz.

Loglardan Metrik Üretmek

Diyelim ki son 5 dakikada Nginx loglarındaki HTTP 500 hatalarının saniyedeki oranını (rate) görmek istiyorsunuz. LogQL bunu yapabilir:

sum(rate({app="nginx"} | json | status =~ "5.." [5m]))

Bu sorguyu Grafana’da bir Graph panelinde çalıştırarak doğrudan loglardan türetilmiş canlı metrik grafikleri elde edebilirsiniz. Prometheus’a yük bindirmeden mükemmel bir alarm altyapısı!

Grafana ile Log Görselleştirme

Logları sorgulamak için Grafana arayüzünü kullanacağız. İşte izlemeniz gereken adımlar:

  1. Grafana arayüzüne gidin ve sol menüden Connections -> Data Sources yolunu izleyin.
  2. Add data source butonuna tıklayın ve listeden Loki‘yi seçin.
  3. Connection URL kısmına Loki servisinizin adresini yazın: http://loki-gateway.logging.svc.cluster.local (Eğer Grafana aynı cluster içindeyse).
  4. Sayfanın altındaki Save & Test butonuna tıklayın. Yeşil onay kodunu gördüyseniz bağlantı tamamdır!

Şimdi sol menüden Explore sekmesine gidin. Üst taraftan veri kaynağı olarak Loki‘yi seçin. Log browser üzerinden etiketlerinizi seçerek veya yukarıda paylaştığımız LogQL sorgularını yazarak loglarınızı canlı olarak (Live tailing) izlemeye başlayabilirsiniz.

Grafana Dashboard Visualization

Üretim Ortamı İçin Altın Tavsiyeler (Pro-Tips)

Loki’yi production ortamında koştururken canınızın sıkılmasını istemiyorsanız şu üç kurala mutlaka dikkat edin:

  1. Yüksek Cardinality’den Kaçının (High Cardinality): Loglarınıza asla user_id, ip_address veya request_id gibi binlerce benzersiz değere sahip dinamik etiketler (labels) eklemeyin. Bu, Loki’nin indeks yapısını şişirir ve sorgu performansını yerle bir eder. Bu tarz dinamik verileri log satırının içinde bırakın; aramak için LogQL filtrelerini (|= veya | json) kullanın.
  2. S3 Entegrasyonu: Lokal disk yerine her zaman AWS S3 veya MinIO gibi nesne depolama çözümlerini tercih edin. Bu sayede log retention (logların silinme süresi) politikanızı kolayca yönetebilir ve disk dolma riskini sıfıra indirebilirsiniz.
  3. Query Frontend Kullanımı: Yoğun log sorgulaması yapılan ortamlarda Loki’yi mikroservis modunda kurup query-frontend bileşenini aktif edin. Bu bileşen, büyük zaman aralıklarındaki sorguları parçalara ayırarak paralel çalıştırır ve caching mekanizması sunar.

Özet

Grafana Loki, modern DevOps pratiklerinde log yönetimi için ezber bozan bir araç. ELK’nın getirdiği o devasa operasyonel yük ve maliyet olmadan, sadece ihtiyacımız olanı indexleyerek harika bir observability altyapısı kurduk. Promtail ile logları topladık, LogQL ile derinlemesine analiz ettik ve Grafana ile taçlandırdık.

Siz de production ortamlarınızda kaynak tüketimini optimize etmek istiyorsanız, Loki’ye bir şans verin. Bir sonraki yazımızda görüşmek üzere, loglarınız temiz, sistemleriniz ayakta kalsın!

Category: Genel | LEAVE A COMMENT
Temmuz 19 2024

Zabbix 7.x ile Kubernetes Cluster Monitoring

Kubernetes ekosisteminde monitoring denildiğinde akla ilk gelen Prometheus-Grafana ikilisi olsa da, modern devops dünyasında zabbix 7.x sürümüyle kartları yeniden dağıtıyor. Bu yazımızda, legacy sistemlerden aşina olduğumuz bu devasa canavarın, kubernetes cluster’larındaki container metriklerini auto-discovery (LLD) yetenekleriyle nasıl saniyeler içinde yakaladığını, verileri nasıl işlediğini ve Grafana ile nasıl görselleştirdiğini ele alacağız. “Prometheus kurduk ama RAM tüketimi ocağımıza incir ağacı dikti” diyenlerdenseniz, kemerlerinizi bağlayın; Zabbix 7.x’in hafif ve güçlü Kubernetes entegrasyonuna dalıyoruz.

Neden Zabbix 7.x? (Eski Dostun Yeni Numaraları)

Yıllarca Zabbix’i sadece VM’lerin CPU ve disk doluluk oranlarını izlemek için kullandıysanız, kendinizi güncellemenin vakti geldi demektir. Zabbix 7.x, Kubernetes dünyasını yabancı bir gezegen gibi görmüyor. HTTP agent, out-of-the-box gelen gelişmiş Kubernetes template’leri ve JSONPath preprocessing yetenekleri sayesinde API Server üzerinden doğrudan cluster’ın kalbine inebiliyor.

Peki neden Prometheus yerine Zabbix? Cevap basit: Unified Monitoring ve Uzun Vadeli Veri Saklama (Long-term Retention). Eğer cluster dışında fiziksel switch’leriniz, legacy veritabanlarınız ve sanal makineleriniz varsa, hepsini tek bir panele toplamak ve bunu yaparken TSDB (Time Series Database) kaynaklı çılgın RAM maliyetlerinden kaçınmak istiyorsanız, Zabbix 7.x tam size göre.

Adım 1: Kubernetes Cluster Üzerinde Zabbix Helm Deployment

Zabbix’in cluster içindeki gözü kulağı olacak Zabbix Agent 2 ve Zabbix Proxy yapılandırmasını kurarak işe başlıyoruz. Kubernetes API’si ile güvenli bir şekilde konuşabilmek için service account ve rol tanımlamalarına ihtiyacımız var. Neyse ki resmi Helm chart bu işi bizim yerimize hallediyor.

İlk olarak Zabbix topluluk reposunu sistemimize ekleyelim:

helm repo add zabbix-community https://zabbix-community.github.io/helm-charts
helm repo update

Şimdi, cluster metriklerini toplamak için optimize edilmiş bir values.yaml dosyası hazırlayalım. Bu dosyada Zabbix Server adresimizi ve cluster adımızı belirtiyoruz. Aşağıdaki konfigürasyonu zabbix-values.yaml olarak kaydedebilirsiniz:

zabbixProxy:
  enabled: true
  resources:
    limits:
      cpu: 500m
      memory: 512Mi
    requests:
      cpu: 100m
      memory: 128Mi
  env:
    - name: ZBX_HOSTNAME
      value: "k8s-production-proxy"
    - name: ZBX_SERVER_HOST
      value: "zabbix-server.kertenkerem.local" # Zabbix Server IP veya FQDN'iniz

zabbixAgent:
  enabled: true
  resources:
    limits:
      cpu: 200m
      memory: 256Mi
    requests:
      cpu: 50m
      memory: 64Mi
  env:
    - name: ZBX_HOSTNAMEITEM
      value: "system.hostname"

Bu konfigürasyon ile cluster içinde bir DaemonSet olarak Zabbix Agent 2 ve cluster dışındaki ana sunucumuza verileri güvenli ve sıkıştırılmış şekilde iletecek bir Zabbix Proxy ayağa kaldırıyoruz. Kurulumu şu komutla tetikleyelim:

kubectl create namespace monitoring
helm install zabbix zabbix-community/zabbix-helm-ch --namespace monitoring -f zabbix-values.yaml

Adım 2: Auto-Discovery (LLD) ile Metrik Avcılığı

Kubernetes dinamik bir yapıya sahiptir; pod’lar saniyeler içinde ölür, yenileri doğar. Statik IP’ler veya host tanımlamaları burada sökmez. Zabbix 7.x, bu kaosu çözmek için Low-Level Discovery (LLD) mekanizmasını kullanır. Kubernetes API Server üzerinden pods, nodes, namespaces ve services discovery kurallarını çalıştırır.

Zabbix Web UI arayüzüne giriş yaptıktan sonra şu adımları izleyin:

1. API Token Oluşturma

Zabbix Server’ın cluster API’si ile konuşabilmesi için Helm kurulumu sırasında otomatik oluşturulan service account token’ını almamız gerekiyor:

kubectl get secret $(kubectl get serviceaccount zabbix-service-account -n monitoring -o jsonpath="{.secrets[0].name}") -n monitoring -o jsonpath="{.data.token}" | base64 --decode

Not: Kubernetes 1.24 ve üzeri kullanıyorsanız, secret manuel oluşturulmuş olabilir. Helm chart bunu sizin için halletmediyse, service account için bir `kubernetes.io/service-account-token` tipinde Secret oluşturup token’ı oradan okumalısınız.

2. Zabbix Üzerinde Host Tanımlama

  • Configuration -> Hosts -> Create Host yolunu izleyin.
  • Host Name alanına Kubernetes Cluster Production yazın.
  • Templates kısmına “Kubernetes by HTTP” template’ini ekleyin. Bu template, Zabbix 7.0 ile gelen out-of-the-box harika bir canavardır.
  • Macros sekmesine geçiş yapın ve aşağıdaki kritik değişkenleri tanımlayın:
{$KUBE.API.URL} -> https://<kubernetes-api-endpoint>:6443
{$KUBE.API.TOKEN} -> <Bir önceki adımda elde ettiğiniz token>

Bu tanımlamadan sonra Zabbix, HTTP agent yardımıyla cluster API’nize sorgu atmaya başlayacak. Birkaç dakika içinde “Discovery rules” altındaki kuralların tetiklendiğini ve cluster’ınızdaki tüm node’ların, pod’ların ve container’ların otomatik olarak sisteme “Graph” ve “Item” olarak eklendiğini göreceksiniz. İşte auto-discovery’nin gücü!

Adım 3: Grafana Entegrasyonu (Görsel Şölen)

Zabbix’in kendi arayüzündeki grafikler son yıllarda iyileşmiş olsa da, hiçbirimiz yöneticilerimize veya development ekiplerine o 90’lardan kalma görünümlü panelleri göstermek istemeyiz. Bize Grafana lazım.

Grafana üzerinde Zabbix veri kaynağını (datasource) aktifleştirmek için şu adımları izliyoruz:

grafana-cli plugins install alexanderzobnin-zabbix-app
systemctl restart grafana-server

Grafana arayüzüne gidip Connections -> Data Sources -> Add New Data Source dedikten sonra listeden Zabbix’i seçiyoruz. Konfigürasyon ayarlarında dikkat etmeniz gereken kritik noktalar şunlar:

  • URL: http://zabbix-server-ip/zabbix/api_jsonrpc.php
  • Access: Server (Default)
  • Zabbix API Connection: Zabbix admin kullanıcı adı ve şifreniz.

Bağlantıyı test edip kaydettikten sonra, Kubernetes cluster metriklerimizi içeren dashboard’ları tasarlayabiliriz. Örneğin, her pod’un CPU tüketimini dinamik olarak çeken bir panel yazmak istersek, Grafana query alanına şu regex’i girebiliriz:

Group: Kubernetes
Host: /.*/
Application: CPU
Item: /Calculated CPU usage percent/

Bu sorgu sayesinde, cluster’a yeni bir mikroservis deploy edildiğinde hiçbir manuel müdahale yapmadan o servisin CPU grafiği saniyeler içinde Grafana panelinizde belirecektir.

Adım 4: Akıllı Alerting Kuralları

Gecenin saat 3’ünde “Pod restart attı” diye uyanmak istemiyorsanız, Zabbix’in esnek trigger yapısını doğru kurgulamanız gerekir. Klasik CPU doldu alert’leri yerine Kubernetes dünyasına uygun akıllı trigger’lar yazmalıyız.

Örnek 1: CrashLoopBackOff Durumu tespiti

Bir pod sürekli hata alıp kapanıyorsa ve restart sayısı artıyorsa, bu kritik bir durumdur. Zabbix template’i içinde gelen varsayılan kuralı özelleştirebiliriz:

last(/Kubernetes by HTTP/kube.pod.restart_rate[{#POD.NAME}]) > 3 and last(/Kubernetes by HTTP/kube.pod.status[{#POD.NAME}]) = 0

Bu trigger, pod’un son 5 dakika içindeki restart hızı 3’ten fazla ise ve pod “Ready” durumunda değilse alarm üretir. Geçici network kesintilerinde boşuna Slack kanallarınızı spam’lemez.

Örnek 2: Node Disk Basıncı (Disk Pressure)

Kubelet’in node üzerindeki pod’ları evict etmeye başlamadan (yani sistem dışına atmadan) önce diskin dolduğunu anlamamız gerekir:

last(/Kubernetes by HTTP/kube.node.status.disk_pressure[{#NODE.NAME}]) = 1

Bu alarm tetiklendiğinde, Zabbix’in Webhook entegrasyonu sayesinde Slack veya PagerDuty üzerinden anlık bildirim alabilir, hatta otomatik aksiyon (Remote Command) tanımlayarak ilgili node üzerindeki gereksiz docker imajlarını temizleyen bir script çalıştırabilirsiniz.

Zabbix 7.x Kubernetes Monitoring’in Avantaj ve Dezavantajları

Her sistemde olduğu gibi bu mimarinin de güçlü ve zayıf yanları var. Kararınızı vermeden önce bunları masaya yatıralım:

Avantajları Dezavantajları
Düşük kaynak tüketimi (RAM/CPU dostu) Kubernetes native olmaması (Prometheus operator kadar cluster içine entegre değil)
Merkezi yönetim ve güçlü LLD yetenekleri Custom Metric API ile HPA (Horizontal Pod Autoscaler) entegrasyonu görece zor
Uzun vadeli verileri saklamada veritabanı esnekliği (PostgreSQL/TimescaleDB) Grafana template kütüphanesinin Prometheus kadar zengin olmaması

Sonuç

Zabbix 7.x, Kubernetes cluster’larını izlemek için hantal ve “outdated” bir çözüm olmaktan çok uzak. Aksine, kaynak tüketimini minimize etmek isteyen ve tüm IT altyapısını (fiziksel, sanal, container) tek bir merkezi noktadan yönetmeyi hedefleyen kıdemli sistem ve devops mühendisleri için biçilmiş kaftan. Auto-discovery mekanizmasının gücü ve Grafana’nın görsel yetenekleri birleştiğinde, production ortamınızda uçtan uca izlenebilirlik (observability) sağlamak işten bile değil.

Siz de cluster’larınızda Zabbix kullanmayı denediniz mi? Yaşadığınız tecrübeleri veya takıldığınız yerleri yorumlarda bizimle paylaşın, debugging seansına beraber devam edelim!

Category: Genel | LEAVE A COMMENT