Ekim 4 2024

Cron İşlerini Bash ile Monitör Etme ve Alerting

Modern altyapılarda Kubernetes cronjob’ları, serverless scheduler’lar havada uçuşsa da, günün sonunda hepimizin bir yerlerde tıkır tıkır çalışan (ya da çalıştığını umduğu) emektar bir linux sunucusu ve içinde barınan klasik cron işleri vardır. Ancak cron’un en büyük problemi “sessiz ölüm” (silent failure) dediğimiz durumdur. Bir backup script’i patladığında, disk dolduğunda veya API key’in süresi geçtiğinde cron bunu size söylemez. Bu yazıda, kendi yazacağımız esnek bir bash wrapper script ile tüm cron işlerinizi merkezi olarak monitoring dünyasına entegre edecek, hata durumlarında anında alert üretecek ve üçüncü parti servislerle nasıl haberleştireceğimizi göreceğiz.

Klasik Cron Yaklaşımlarının Sorunu Nedir?

Hepimiz o yollardan geçtik. Bir cron işinin çıktısını almak için satırın sonuna efsanevi >> /var/log/myjob.log 2>&1 yönlendirmesini yazarız. Sonra ne mi olur? O log dosyası gigabaytlarca büyür, diski doldurur ve bir gün script hata verdiğinde kimsenin haberi olmaz. Çünkü logları aktif olarak okuyan kimse yoktur.

Eski usul Linux yöneticileri size “MTA (Mail Transfer Agent) kurun, cron hata aldığında lokal mail atsın” diyecektir. Yıl 2024+. ephemeral VM’lerin, cloud instance’larının cirit attığı bu çağda localhost mail kuyruklarıyla uğraşmak, postfix yapılandırmak ve spame düşen lokal mailleri takip etmek tam bir operasyonel işkencedir. Bizim daha proaktif, modern ve DevOps pratiklerine uygun bir çözüme ihtiyacımız var.

Wrapper Script Pattern Neden En İyi Çözümdür?

Elinizde 20 farklı cron script’i olduğunu düşünün. Her birinin içine tek tek Slack webhook entegrasyonu, hata yakalama (error handling) ve çalışma süresi ölçümü eklemek tam bir anti-pattern’dir. Kod tekrarı yaratır ve yarın öbür gün Slack yerine Teams’e geçildiğinde 20 farklı script’i update etmeniz gerekir.

Wrapper Script Pattern, çalıştırılacak asıl komutu sarmalayan, onun exit code’unu yakalayan, standart output (stdout) ve standart error (stderr) akışlarını yöneten merkezi bir aracıdır. Crontab içerisindeki tanımımız şuna benzer:

0 4 * * * /usr/local/bin/cron-wrapper.sh --job "db-backup" --cmd "/opt/scripts/backup.sh --force"

Bu yaklaşım sayesinde, asıl işi yapan script’in monitoring mantığından tamamen bağımsız (decoupled) kalmasını sağlarız. Script sadece işini yapar ve exit code döner; wrapper ise raporlamayı üstlenir.

Adım 1: Savaşçı Ruhlu Bash Wrapper Script

Lafı uzatmadan production ortamında güvenle kullanabileceğiniz, hata durumunda exit code’u ve logları yakalayan, zaman aşımı (timeout) kontrolü yapabilen wrapper script’imizi yazalım. Bu script’i /usr/local/bin/cron-wrapper.sh olarak kaydedebilirsiniz.

#!/usr/bin/env bash

# Robust Bash Ayarları
set -o nounset
set -o pipefail

# Değişken Tanımları
JOB_NAME=""
COMMAND=""
SLACK_WEBHOOK_URL="${SLACK_WEBHOOK_URL:-""}"
HEALTHCHECK_URL="${HEALTHCHECK_URL:-""}"

# Kullanım Yardımcısı
usage() {
    echo "Kullanım: $0 --job [JOB_NAME] --cmd [COMMAND]"
    exit 1
}

# Parametreleri Parse Etme
while [[ $# -gt 0 ]]; do
    case "$1" in
        --job)
            JOB_NAME="$2"
            shift 2
            ;;
        --cmd)
            COMMAND="$2"
            shift 2
            ;;
        *)
            usage
            ;;
    esac
done

if [[ -z "$JOB_NAME" || -z "$COMMAND" ]]; then
    usage
fi

# Geçici Log Dosyası Oluşturma (Güvenli yöntem)
LOG_FILE=$(mktemp /tmp/cron_wrapper_${JOB_NAME}.XXXXXX)

# Her durumda temizlik yapılması için trap kullanımı
cleanup() {
    rm -f "$LOG_FILE"
}
trap cleanup EXIT

# Zamanı Ölçmeye Başla
START_TIME=$(date +%s)

echo "[$(date '+%Y-%m-%d %H:%M:%S')] Job '$JOB_NAME' başlatılıyor..." >> "$LOG_FILE"
echo "Komut: $COMMAND" >> "$LOG_FILE"
echo "--------------------------------------------------" >> "$LOG_FILE"

# Komutu çalıştır ve tüm çıktıyı log dosyasına yönlendir
# set +e kullanıyoruz çünkü komut hata verirse wrapper script'in hemen çökmesini istemiyoruz,
# hatayı yakalayıp alert üreteceğiz.
set +e
eval "$COMMAND" >> "$LOG_FILE" 2>&1
EXIT_CODE=$?
set -e

# Zamanı Hesapla
END_TIME=$(date +%s)
DURATION=$((END_TIME - START_TIME))

echo "--------------------------------------------------" >> "$LOG_FILE"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Job bitti. Süre: ${DURATION}sn, Exit Code: $EXIT_CODE" >> "$LOG_FILE"

# Sonuçları Değerlendir
if [ $EXIT_CODE -eq 0 ]; then
    echo "Başarılı: $JOB_NAME sorunsuz tamamlandı."
    # Eğer tanımlıysa Healthcheck ping'i gönder (Mavi Tik)
    if [ -n "$HEALTHCHECK_URL" ]; then
        curl -fsS --retry 3 "$HEALTHCHECK_URL" > /dev/null 2>&1 || true
    fi
else
    echo "HATA: $JOB_NAME başarısız oldu! Exit code: $EXIT_CODE" >&2
    
    # Slack Alert Tetikleme fonksiyonunu çağır
    if [ -n "$SLACK_WEBHOOK_URL" ]; then
        # Log dosyasının son 20 satırını alalım ki Slack mesajı devasa olmasın
        LAST_LOGS=$(tail -n 20 "$LOG_FILE" | sed 's/"/\\"/g' | sed ':a;N;$!ba;s/\n/\\n/g')
        
        PAYLOAD=$(cat <<EOF
{
  "text": "🚨 *Cron Job Hatası!*",
  "attachments": [
    {
      "color": "danger",
      "fields": [
        { "title": "Job Adı", "value": "${JOB_NAME}", "short": true },
        { "title": "Exit Code", "value": "${EXIT_CODE}", "short": true },
        { "title": "Süre", "value": "${DURATION} saniye", "short": true },
        { "title": "Sunucu", "value": "$(hostname)", "short": true }
      ],
      "text": "*Son Loglar:*\n\`\`\`${LAST_LOGS}\`\`\`"
    }
  ]
}
EOF
)
        curl -X POST -H 'Content-type: application/json' --data "$PAYLOAD" "$SLACK_WEBHOOK_URL" > /dev/null 2>&1 || true
    fi
    
    # Wrapper kendisi de hata koduyla çıksın ki pipeline'lar veya üst sistemler anlasın
    exit $EXIT_CODE
fi

Neden “set +e” ve “set -o pipefail” Kullandık?

Bash’te hata yönetimi hassas bir konudur. set -o pipefail komutu, bir pipe zincirindeki herhangi bir komut hata verirse tüm satırın hata kodu dönmesini sağlar. Wrapper script’in başında bunu aktif ettik. Ancak, sarmaladığımız asıl komut hata verdiğinde script’imizin patlayıp Slack alert’ini gönderemeden sonlanmasını engellemek için, çalıştırma aşamasında geçici olarak set +e ile esneklik sağladık, exit code’u güvenli bir şekilde $? ile lokal değişkene aldık ve ardından set -e ile güvenli modumuza geri döndük.

Ayrıca mktemp kullanımı, paralel çalışan cron işlerinin aynı log dosyası üzerine yazarak yarış durumuna (race condition) girmesini engeller.

Adım 2: “Dead Man’s Snitch” Mantığı ile Healthcheck Entegrasyonu

Peki ya sunucunun kendisi çökerse? Ya da cron daemon durursa? Bu durumlarda “hata aldığımda Slack’e yaz” mantığı tamamen devre dışı kalır. Çünkü çalışmayan bir sistem hata mesajı da gönderemez. Buna monitoring dünyasında silent failure denir.

Çözüm: Inverted Monitoring (Ters İzleme) ya da bilinen adıyla Heartbeat / Dead Man’s Snitch pattern. Sistem çalışmayı durdurduğunda alarm çalmasını istiyorsak, sistemin düzenli aralıklarla “Ben hayattayım!” sinyali göndermesi gerekir. Eğer sinyal gelmezse, monitoring sunucumuz bizi uyarır.

Bu iş için healthchecks.io veya kendi host ettiğiniz open-source alternatiflerini (örneğin Uptime Kuma) kullanabilirsiniz. Script’imizdeki HEALTHCHECK_URL tam olarak bunu yapar. Cron başarılı biterse ilgili adrese bir curl isteği atar. Eğer o istek zamanında gitmezse, dış dünya sunucunun patladığını anlar.

Adım 3: Slack Webhook ile Gerçek Zamanlı Alerting

Yukarıdaki script’imizde Slack için JSON payload’unu dinamik olarak oluşturduk. Bash içinde JSON oluştururken kaçış karakterleri (escaping) her zaman baş ağrıtır. Bu yüzden log dosyasından okuduğumuz son 20 satırı (LAST_LOGS) temizlemek için sed ile yeni satır karakterlerini (\n) ve çift tırnakları escape ettik.

Slack’te görünecek mesaj, hata anında debug yapmanızı inanılmaz kolaylaştıracaktır. Hangi sunucuda, ne kadar sürede patladığını ve hatanın son satırlarını terminale girmeden telefonunuzdan görebilirsiniz:

Slack Notification Alerting

Adım 4: Crontab Yapılandırması

Şimdi yazdığımız bu sistemi gerçek bir senaryoda test edelim. Sunucumuzdaki env variable’ları crontab ortamına geçirmek bazen can sıkıcı olabilir. En temiz yöntem webhook url gibi hassas verileri wrapper script içine hardcode etmek yerine wrapper’ı çağırırken bir konfigürasyondan beslemek veya crontab’ın üst kısmında tanımlamaktır.

# /etc/cron.d/my-monitored-crons veya crontab -e

SLACK_WEBHOOK_URL="https://hooks.slack.com/services/T00/B00/X00"
HEALTHCHECK_URL="https://hc-ping.com/your-uuid-here"

# Her gece 03:00'te yedek al, hata olursa Slack'e bildir, başarılı olursa ping at
0 3 * * * root /usr/local/bin/cron-wrapper.sh --job "database-backup" --cmd "/opt/backups/pg_dump.sh"

Dikkat Edilmesi Gereken İpuçları

  • Mutlak Yol (Absolute Paths) Kullanın: Cron ortamında $PATH değişkeni kısıtlıdır. Hem wrapper script’in içinde çağırdığınız tool’ların (curl, sed vb.) hem de crontab içindeki komutların tam yollarını yazmaya özen gösterin.
  • Yetkilendirme: Wrapper script’in çalıştırılabilir olduğundan emin olun: chmod +x /usr/local/bin/cron-wrapper.sh
  • Log Rotasyonu: Biz script içinde trap kullanarak geçici logları işlem sonunda siliyoruz. Ancak işlem yarıda kesilirse (örneğin kill -9) loglar kalabilir. Periyodik olarak /tmp dizininin temizlendiğinden emin olun (çoğu modern Linux dağıtımı bunu otomatik yapar).

Özet

Cron işlerini kendi haline bırakmak, prod ortamında saatli bir bomba üzerinde oturmak gibidir. Bu makalede yazdığımız esnek bash wrapper script sayesinde, mevcut hiçbir altyapınızı bozmadan, tek bir merkezi mekanizma ile tüm linux cron işlerinizi modern birer microservice gibi monitoring ve alert süreçlerine dahil ettik. Unutmayın, en iyi sistem tıkır tıkır çalışan değil, bozulduğunda neresinin bozulduğunu size ilk söyleyen sistemdir.

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