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 11 2024

Linux Namespace ve Cgroup: Container’ların Altındaki Mekanizma

Eğer bir teknik mülakatta ya da kahve molasında “Container nedir?” sorusuna “Hizmetleri izole eden hafifletilmiş sanal makinelerdir (VM)” yanıtını verdiyseniz, bugün bu ezberi bozuyoruz. Çünkü aslında işletim sistemi seviyesinde “container” diye bir nesne veya teknoloji bulunmuyor. Bizim docker, containerd ya da podman adını verdiğimiz araçların yaptığı her şey, linux çekirdeğinin (kernel) sunduğu iki temel özelliğin etrafına örülmüş şık birer kullanıcı arayüzü (wrapper) olmaktan ibaret: namespace ve cgroup.

Bu makalede, işin kolayına kaçıp hazır CLI araçları kullanmak yerine, modern bir container runtime’ın (örneğin containerd) yaptığı işi tamamen elle (manual) yapacağız. Sıfırdan izole bir root filesystem (rootfs) hazırlayacak, kendi ağ geçidimizi kuracak, prosesleri izole edecek ve cgroup v2 ile kaynak sınırları koyacağız. Arkanıza yaslanın, terminalinizi açın ve container dünyasının arka bahçesine hoş geldiniz.

1. Hazırlık: Minimal Bir Root Filesystem (rootfs) Oluşturmak

Bir container’ın kendi dünyasında yaşayabilmesi için öncelikle bağımsız bir dosya sistemine ihtiyacı vardır. Docker imajları aslında katmanlaştırılmış (layered) tar dosyalarından başka bir şey değildir. Biz de işe minimal bir Alpine Linux rootfs indirerek başlayacağız.

# Çalışma dizinimizi oluşturalım
mkdir -p /tmp/kerten-container/rootfs
cd /tmp/kerten-container

# Alpine rootfs indiriyoruz
curl -sSL https://dl-cdn.alpinelinux.org/alpine/v3.18/releases/x86_64/alpine-minirootfs-3.18.4-x86_64.tar.gz -o alpine.tar.gz
tar -xzf alpine.tar.gz -C rootfs/
rm alpine.tar.gz

Artık elimizde izole bir işletim sisteminin sahip olması gereken tüm temel dizin yapısı (bin, sbin, etc, lib, proc…) mevcut. Sıradaki adım, bu dizini yeni dünyamızın kök dizini (root) haline getirmek.

2. Chroot Değil, Pivot Root: Güvenli Mount Namespace

Dosya sistemi izolasyonu denince akla gelen ilk syscall (sistem çağrısı) genellikle chroot olur. Ancak chroot esnektir ve root yetkilerine sahip bir proses kolayca bu hapishaneden kaçabilir (jailbreak). Modern container dünyası bunun yerine çok daha güvenli olan pivot_root sistem çağrısını kullanır.

pivot_root, mevcut mount namespace’in root mount noktasını yeni bir dizine taşır ve eski root’u başka bir dizine bind eder. Bunu elle simüle etmek için öncelikle yeni bir mount namespace oluşturmamız gerekir.

İşte sihirli komutumuz: unshare. Bu komut, belirtilen namespace türlerini sıfırdan oluşturarak yeni bir proses başlatır.

# Mount, UTS (hostname) ve IPC namespace'lerini izole ederek yeni bir bash oturumu açıyoruz
unshare --mount --uts --ipc --fork /bin/bash

Şu andan itibaren açılan bu yeni shell oturumunda yaptığımız mount işlemleri host sistemimizi etkilemeyecek. Şimdi rootfs dizinimizi bir mount noktasına dönüştürelim ve pivot_root için hazırlayalım:

# rootfs dizinimizi bir bind mount olarak işaretliyoruz (pivot_root bunu şart koşar)
mount --bind /tmp/kerten-container/rootfs /tmp/kerten-container/rootfs

# Eski root dizinini koyacağımız geçici bir klasör oluşturuyoruz
mkdir -p /tmp/kerten-container/rootfs/put_old

# pivot_root komutunu çalıştırıyoruz: [yeni_root] [eski_root_un_duracagi_yer]
cd /tmp/kerten-container/rootfs
pivot_root . put_old

# Artık yeni root içerisindeyiz. Eski sisteme ait mount noktalarını temizleyelim
cd /
umount -l /put_old
rmdir /put_old

# Hostname'i değiştirelim (UTS namespace sayesinde host etkilenmez)
hostname kerten-container

Harika! Şu an sadece kendi hazırladığımız rootfs içerisindeki ikili dosyaları (binaries) görebilen, izole edilmiş bir dosya sistemindeyiz. Ancak eksik bir şeyler var: Prosesler nerede?

3. PID Namespace: “Ben Kimim?” Sorusu

Eğer az önce oluşturduğumuz ortamda ps aux çalıştırmayı denerseniz, hata alırsınız. Çünkü proseslerin yönetildiği ve kernel bilgilerinin okunduğu sanal dosya sistemi olan /proc henüz mount edilmedi. Daha da önemlisi, hala host sistemin PID (Process ID) uzayını paylaşıyoruz.

Gelin, yeni bir PID namespace oluşturalım. Bunun için yeni bir terminal penceresi açıp host üzerinde çalışmaya devam edeceğiz. Bu sayede hem host hem de container arasındaki ilişkiyi daha net görebiliriz.

Bu sefer PID namespace’i de işin içine katarak unshare ile yeni bir container başlatalım (dosya sistemi adımlarını bu yeni namespace içinde hızlıca tekrarladığımızı varsayalım veya doğrudan aşağıdaki komutla temiz bir başlangıç yapalım):

# PID ve Mount namespace izole edilmiş şekilde başlatıyoruz
unshare --mount --pid --fork --mount-proc /bin/bash

--mount-proc parametresi, bizim için otomatik olarak yepyeni ve izole bir /proc dosya sistemi mount eder. Şimdi bu oturumda prosesleri listeleyelim:

ps aux

Çıktıya dikkat edin:

PID   USER     TIME  COMMAND
    1 root      0:00 /bin/bash
    2 root      0:00 ps aux

İşte container dünyasının kutsal kasesi! Host üzerinde binlerce çalışan proses varken, bizim container’ımız kendisini dünyadaki tek proses (PID 1) olarak görüyor. PID 1 olmak büyük bir sorumluluktur; eğer bu proses ölürse, kernel tüm namespace’i sonlandırır.

4. Network Namespace: Kablolama İşlemleri

Şu ana kadar dosya sistemini ve prosesleri izole ettik ancak container’ımızın dış dünya ile bağlantısı yok (loopback arayüzü bile kapalı). Docker arkada bu işi sanal bir switch (docker0 bridge) ve veth pair (sanal ethernet kablosu) kullanarak çözer.

Gelin bu kablolamayı host üzerinde elle yapalım. Bu senaryo için host üzerinde root yetkileriyle yeni bir terminal açın.

Öncelikle container’ımızın namespace’ini kalıcı hale getirmemiz gerekir ki host üzerinden oraya erişebilelim. Linux’ta her namespace /proc/[PID]/ns/ altında bir dosya olarak temsil edilir.

# Container'ımızın PID'sini host üzerinde bulalım (örneğin 12345 olsun)
# Host üzerinde:
ip netns attach kerten-netns 12345

Şimdi sanal ethernet çiftimizi oluşturalım. Bu işlem, bir ucu hostta, diğer ucu container içinde olan sanal bir kablo yaratacaktır:

# veth çiftini oluştur
ip link add veth-host type veth peer name veth-container

# Kablonun container ucunu container'ın network namespace'ine taşıyalım
ip link set veth-container netns kerten-netns

# Host tarafındaki uca IP verelim ve ayağa kaldıralım
ip addr add 10.200.0.1/24 dev veth-host
ip link set veth-host up

# Container tarafındaki uca IP verelim ve ayağa kaldıralım
ip netns exec kerten-netns ip addr add 10.200.0.2/24 dev veth-container
ip netns exec kerten-netns ip link set veth-container up
ip netns exec kerten-netns ip link set lo up # Loopback'i unutmayalım

# Container için default gateway tanımlayalım
ip netns exec kerten-netns ip route add default via 10.200.0.1

Artık container içinden host tarafındaki 10.200.0.1 IP adresine ping atabilirsiniz. Container runtime’ların her container için saniyeler içinde yaptığı o karmaşık ağ konfigürasyonunun temel mekanizması tam olarak budur.

5. Cgroup v2 Sihri: Kaynakları Sınırlandırmak

Güzel, izole bir ortamımız ve ağımız var. Peki ya bu container içerisindeki bir proses çıldırır ve host sistemin tüm CPU ve RAM kaynaklarını tüketmeye çalışırsa? İşte burada devreye cgroup (Control Groups) giriyor.

Modern Linux dağıtımları artık varsayılan olarak cgroup v2 kullanıyor. Cgroup v2, v1’deki dağınık yapıyı tek bir hiyerarşik ağaç altında birleştirerek işleri inanılmaz derecede kolaylaştırdı. Cgroup v2 hiyerarşisi varsayılan olarak /sys/fs/cgroup dizininde yaşar.

Şimdi elle “kerten-limit” adında bir kontrol grubu oluşturalım ve container’ımızın bellek kullanımını 100 MB ile sınırlayalım:

# Host üzerinde cgroup dizinine gidelim
cd /sys/fs/cgroup

# Yeni bir grup oluşturmak sadece bir dizin oluşturmaktan ibarettir!
mkdir kerten-limit
cd kerten-limit

# Kernel, bu dizini oluşturduğumuz an içine kontrol dosyalarını otomatik olarak yerleştirir.
ls -la

Şimdi bellek sınırımızı (memory limit) 100MB (104857600 bytes) olarak ayarlayalım:

echo "104857600" > memory.max

Peki bu sınırı container prosesimize nasıl uygulayacağız? Çok basit: Container prosesimizin host üzerindeki PID’sini (örneğin 12345) cgroup altındaki cgroup.procs dosyasına yazmamız yeterli:

echo "12345" > cgroup.procs

Artık bu proses veya bu prosesten türeyecek (fork) olan tüm alt süreçler toplamda 100 MB bellek sınırını aşamazlar. Sınırı aşmaya çalıştıkları anda kernel’ın meşhur OOM-Killer (Out of Memory Killer) mekanizması devreye girecek ve o prosesi acımasızca sonlandıracaktır. Tıpkı Kubernetes ortamında aldığınız o meşhur OOMKilled hatası gibi!

Özet: Container Aslında Bir İllüzyondur

Gördüğünüz gibi, arka planda çalışan gizemli hipervizörler, sanal donanımlar veya ağır sanallaştırma katmanları yok. Yaptığımız her şey, Linux kernel’ına “Bu prosese sadece şu dizini göster (mount namespace), sadece şu prosesleri görmesine izin ver (PID namespace), ağ trafiğini şu sanal kabloya yönlendir (net namespace) ve şu kadar kaynak tüketmesine izin ver (cgroups)” demekten ibaretti.

Docker ve containerd gibi araçlar, bu karmaşık syscall ve CLI yönetimini otomatize ederek bize pratik birer imaj paketleme ve dağıtım standardı sunar. Bu temel mekanizmayı kavramak, Kubernetes ortamlarında veya büyük ölçekli altyapılarda karşılaştığınız ağ, performans ve izolasyon sorunlarını (troubleshooting) çok daha hızlı ve profesyonelce çözmenizi sağlayacaktır.

Bir sonraki derin dalış makalemizde görüşmek üzere, sistemleriniz ayakta, container’larınız hafif 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