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
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
Temmuz 26 2024

Zabbix’te LLD (Low-Level Discovery) ile Dinamik Host Keşfi

DevOps dünyasında “bunu elle hallederiz ya” dediğiniz her an, arkadan sinsice yaklaşan teknik borç (technical debt) canavarının nefesini ensenizde hissedersiniz. Konu sistem izleme ve monitoring olduğunda da durum farklı değil. Dinamik olarak ölçeklenen, sürekli yeni servislerin ayağa kalktığı modern altyapılarda, her yeni disk partition, ağ arayüzü veya çalışan custom servis için Zabbix’e manuel host veya item eklemek tam bir işkencedir. İşte bu noktada, zabbix dünyasının can kurtaran simidi olan lld (low-level discovery) devreye giriyor. Bu yazıda, modern altyapıların vazgeçilmezi olan otomasyon felsefesine sadık kalarak, LLD ile dinamik keşif süreçlerini, kendi custom template’lerimizi yazmayı ve trigger tanımlamayı pratik örneklerle ele alacağız.

LLD (Low-Level Discovery) Nedir ve Neden Hayat Kurtarır?

Geleneksel monitoring yaklaşımlarında, bir sunucunun üzerindeki diskleri izlemek istediğinizde /dev/sda1, /dev/sdb1 gibi diskleri tek tek elle tanımlamanız gerekirdi. Peki ya yarın sisteme yeni bir NVMe disk eklenirse? Ya da Kubernetes worker node’u üzerinde dinamik olarak yüzlerce ephemeral disk mount edilirse? Hepsini manuel olarak takip edip sisteme girmek imkansızdır.

LLD, hedef makinede belirli bir script veya agent modülü çalıştırarak mevcut kaynakları tarar, bunları bir JSON array yapısı haline getirir ve Zabbix Server’a raporlar. Zabbix Server bu JSON’ı okuyarak dinamik olarak item’lar, trigger’lar ve hatta grafikler oluşturur (bunlara prototype denir). Kaynak ortadan kalktığında ise (örneğin bir disk unmount edildiğinde) belirlediğiniz koruma süresi (keep lost resources period) sonunda bu item’ları otomatik olarak temizler. İşte gerçek otomasyon budur!

Adım 1: LLD’nin Kalbi – JSON Çıktısı ve custom LLD Makroları

Zabbix’in bir kaynağı dinamik olarak keşfedebilmesi için tek bir şartı vardır: Çıktının belirli bir JSON formatında olması. Zabbix 5.0 öncesinde katı bir {"data": [...]} formatı zorunluyken, modern Zabbix versiyonlarında (5.0 ve sonrası) düz bir JSON array de kabul edilmektedir.

LLD süreçlerinde en kritik bileşen, iki süslü parantez ve diyez işaretiyle başlayan LLD Makrolarıdır (örneğin: {#SERVICE_NAME}). Bu makrolar, keşfedilen her bir bileşenin benzersiz kimliğini temsil eder.

Gelin, sunucumuzda koşan aktif TCP portlarını otomatik keşfedecek basit bir bash script yazalım. Bu scripti hedef sunucuya yerleştireceğiz.

#!/bin/bash
# discover_ports.sh
# Sunucuda dinlenen TCP portlarını listeleyen ve JSON üreten script

ports=$(netstat -tuln | awk 'NR>2 {print $4}' | awk -F':' '{print $NF}' | sort -u | grep -E '^[0-9]+$')

echo "["
first=1
for port in $ports; do
    # JSON yapısını bozmamak için virgül kontrolü
    if [ $first -eq 0 ]; then
        echo ","
    fi
    # Servis adını bulmaya çalışalım (opsiyonel)
    service_name=$(lsof -i :$port -t | xargs ps -p -o comm= 2>/dev/null | head -n1)
    if [ -z "$service_name" ]; then
        service_name="unknown"
    fi
    printf '  { "{#TCP_PORT}": "%s", "{#PORT_SERVICE}": "%s" }' "$port" "$service_name"
    first=0
done
echo ""
echo "]"

Bu scripti çalıştırdığınızda aşağıdaki gibi temiz bir JSON çıktısı almalısınız:

[
  { "{#TCP_PORT}": "22", "{#PORT_SERVICE}": "sshd" },
  { "{#TCP_PORT}": "80", "{#PORT_SERVICE}": "nginx" },
  { "{#TCP_PORT}": "5432", "{#PORT_SERVICE}": "postgres" }
]

Adım 2: Zabbix Agent Konfigürasyonu (UserParameter)

Yazdığımız bu scripti Zabbix Agent’ın çalıştırabilmesi için bir UserParameter tanımlamamız gerekiyor. Hedef sunucudaki Zabbix Agent konfigürasyon dosyasına (/etc/zabbix/zabbix_agentd.d/custom_lld.conf) şu satırı ekliyoruz:

UserParameter=custom.ports.discovery,/etc/zabbix/scripts/discover_ports.sh

Script dosyasının izinlerini ayarlamayı ve Zabbix kullanıcısının çalıştırabileceğinden emin olmayı unutmayın:

chmod +x /etc/zabbix/scripts/discover_ports.sh
systemctl restart zabbix-agent

Şimdi Zabbix Server veya Proxy üzerinden bu anahtarın çalışıp çalışmadığını zabbix_get yardımıyla test edelim. Bu adım debug süreçleri için hayati önem taşır:

zabbix_get -s 192.168.1.50 -k custom.ports.discovery

JSON çıktısını terminalde gördüyseniz, tebrikler! Altyapı hazır, şimdi Zabbix arayüzüne geçebiliriz.

Adım 3: Template, Discovery Rule ve Item Prototype Tanımlama

Zabbix Web UI üzerinde yeni bir template (örneğin: Template App Custom Port Discovery) oluşturun veya var olan bir template içerisine girin.

1. Discovery Rule Oluşturma

  • Template içinden Discovery rules sekmesine gelin ve Create discovery rule butonuna tıklayın.
  • Name: Active TCP Ports Discovery
  • Type: Zabbix agent (veya active agent kullanıyorsanız active seçin)
  • Key: custom.ports.discovery
  • Update interval: 1h (Sık sık port değişmiyorsa 1 saat idealdir, sistemi yormaya gerek yok)
  • Keep lost resources period: 7d (Sönen portlar 7 gün sonra sistemden silinsin)

2. Item Prototype Oluşturma

Keşfedilen her bir portun ayakta olup olmadığını kontrol edecek asıl izleme bileşenini (item) burada tanımlayacağız. Oluşturduğumuz Discovery rule içindeki Item prototypes sekmesine gidin ve Create item prototype deyin.

  • Name: Port {#TCP_PORT} ({#PORT_SERVICE}) Status
  • Type: Simple check (veya Zabbix Agent net.tcp.service kontrolü)
  • Key: net.tcp.service[tcp,,{#TCP_PORT}]
  • Type of information: Numeric (unsigned)

Burada dikkat ettiyseniz Key alanında doğrudan {#TCP_PORT} makrosunu kullandık. Zabbix LLD motoru, JSON’dan gelen her satır için bu item şablonunu çoğaltacak ve dinamik olarak net.tcp.service[tcp,,22], net.tcp.service[tcp,,80] gibi gerçek izleme item’ları üretecektir.

Adım 4: Akıllı Alarmlar İçin Trigger Prototype Yazmak

Item’ları oluşturduk ancak port kapandığında alarm almamız gerekiyor. Bunun için yine aynı Discovery rule altındaki Trigger prototypes sekmesini kullanıyoruz.

  • Name: Port {#TCP_PORT} ({#PORT_SERVICE}) is DOWN on {HOST.NAME}
  • Severity: Average veya High
  • Expression: last(/Template App Custom Port Discovery/net.tcp.service[tcp,,{#TCP_PORT}])=0

Bu trigger prototype sayesinde, yeni bir port sisteme dahil olduğunda hem izleme kalemi (item) hem de alarm mekanizması (trigger) sıfır manuel müdahale ile anında aktif hale gelecektir. Nginx’i mi restart ettiniz? Port kapandığı an alarmınız hazır!

Pro-Tip: Filtreleri Kullanarak Gürültüyü Engellemek (Noise Reduction)

LLD çok güçlüdür ancak bazen “gürültüye” sebep olabilir. Örneğin dinamik ephemeral portları (örneğin 32768-60999 arasını) veya her ayağa kalkan anlamsız portu izlemek istemeyebilirsiniz. LLD içerisindeki Filters sekmesi bu iş için biçilmiş kaftandır.

Keşif kuralınızın (Discovery Rule) altındaki Filters sekmesine gelin:

  • Label A: Macro: {#TCP_PORT} – Regular expression: ^(80|443|22|5432|3306)$

Bu kuralı eklediğinizde, Zabbix script çıktısındaki tüm portları değil, sadece sizin regex filtrenize uyan kritik portları (HTTP, HTTPS, SSH, PostgreSQL, MySQL) izlemeye alacaktır.

Özet ve Kapanış

Zabbix’te dinamik keşif (LLD) mimarisini kurmak, DevOps mühendislerinin altyapı büyürken geceleri rahat uyumasını sağlayan en kritik adımlardan biridir. Bu makalede ele aldığımız mantığı sadece port keşfi için değil; docker container’ları, SSL sertifika süreleri, log dosyaları, hatta AWS üzerindeki dinamik kaynaklar için bile uygulayabilirsiniz. Tek yapmanız gereken, hedefi tarayan ve standart bir JSON çıktısı üreten ufak bir script yazıp bunu Zabbix makroları ile eşleştirmek.

Otomasyonun olmadığı yerde monitoring sadece bir yükten ibarettir. LLD ile kalın, sistemlerinizi kendi kendine keşfeden akıllı yapılara dönüştürün!

Category: Genel | LEAVE A COMMENT