Ağustos 23 2024

Bash Script’lerinde Kurşun Geçirmez Hata Yönetimi: set -euo pipefail ve Ötesi

Hepimiz oradaydık: Gece yarısı gelen bir PagerDuty alarmı, yarıda kesilmiş bir CI/CD pipeline’ı ve sessizce hata fırlatıp “başarılı” (exit 0) olarak sonlanan bir bash script’i. Linux sistemlerde scripting yaparken, özellikle modern devops ve otomasyon süreçlerinde, hata yönetimi lüks değil bir zorunluluktur. Varsayılan olarak Bash, adeta bir nihilist gibi davranır: Adımlardan biri başarısız olsa bile, hiçbir şey olmamış gibi bir sonraki satıra geçer. Bu makalede, bu vurdumduymazlığı nasıl dizginleyeceğimizi ve prod-ready scriptler yazacağımızı inceleyeceğiz.

5+ yıllık deneyime sahip bir mühendis olarak muhtemelen set -e komutunu duymuş, hatta şablonlarınıza eklemişsinizdir. Ancak bu buzdağının sadece görünen kısmı. Gelin, işi profesyonel seviyeye taşıyalım.

Sihirli Üçlü: set -euo pipefail

Bash script’lerinizin başına ekleyeceğiniz bu sihirli satır, hata yönetiminizin temel taşıdır. Peki ama neden bu üç parametre birlikte kullanılmalı? Her birinin arka planda çözdüğü spesifik problemi inceleyelim.

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

1. set -e (errexit)

Herhangi bir komut sıfırdan farklı bir exit code ile dönerse, script’in çalışmasını anında durdurur.

Neden gerekli? Varsayılan senaryoda, diskte yer kalmadığı için başarısız olan bir tar komutundan sonra script çalışmaya devam eder ve bir sonraki adımdaki silme işlemini tetikleyebilir. set -e bunu engeller. Ancak alt kabuklarda (subshell) veya mantıksal operatörlerde (&&, ||) her zaman beklediğiniz gibi davranmaz. Bu yüzden tek başına kurtarıcı değildir.

2. set -u (nounset)

Tanımlanmamış bir değişken kullanılmaya çalışıldığında script’i hata vererek durdurur.

Neden gerekli? Şu felaket senaryosunu düşünün:

# set -u OLMADAN
TARGET_DIR="" # Bir hata sonucu boş kaldı
rm -rf "$TARGET_DIR/*" # Tebrikler, kök dizini (root) sildiniz.

Eğer set -u aktif olsaydı, Bash daha rm komutuna geçmeden TARGET_DIR is not set hatasıyla işlemi sonlandıracaktı.

3. set -o pipefail

Pipeline (boru hattı) içerisindeki herhangi bir komut hata aldığında, tüm pipeline’ın exit code’unu en son hata alan komutun kodu olarak belirler.

Neden gerekli? Bash varsayılan olarak sadece pipeline’ın en sonundaki komutun exit code’una bakar.

# set -o pipefail OLMADAN
non_existent_command | echo "Merhaba"
echo $? # Çıktı: 0 (Çünkü echo başarılı oldu!)

Yukarıdaki örnekte ilk komut crash olsa bile pipeline başarılı sayılır. set -o pipefail ile bu durum engellenir ve ilk komutun hatası tüm hattı başarısız kılar.

İstisnaları Yönetmek: “Peki ya hata almasını bekliyorsam?”

set -e kullandığınızda, bazen hata vermesi normal olan komutlar da script’inizi sonlandırır. Örneğin, bir portun açık olup olmadığını nc ile kontrol etmek istiyorsunuz:

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

# nc başarısız olursa script burada ölecektir.
nc -z -w3 10.0.0.5 80

echo "Bu satıra asla ulaşılamayacak."

Bunu aşmanın “temiz” yolu, komutun sonuna mantıksal || true eklemek veya hata durumunu inline olarak handle etmektir:

# Güvenli yaklaşım 1: || true ile bypass etmek
nc -z -w3 10.0.0.5 80 || true

# Güvenli yaklaşım 2: Hata durumunda alternatif aksiyon almak
if ! nc -z -w3 10.0.0.5 80; then
    echo "Port kapalı, alternatif akışa geçiliyor..."
    # Burası script'i durdurmaz çünkü if bloğu içindeyiz
fi

Trap Mekanizması ile Graceful Degradation ve Cleanup

Script’iniz yarıda kesildiğinde (ister hata sebebiyle, ister kullanıcı CTRL+C’ye bastığında), arkasında çöp bırakmamalıdır. Geçici dosyalar (temporary files), kilit mekanizmaları (lock files) veya açık SSH tünelleri mutlaka temizlenmelidir. İşte burada trap devreye girer.

Bash’teki sinyalleri (signals) yakalayarak, script sonlanırken mutlaka çalıştırılacak bir cleanup fonksiyonu tanımlayabiliriz:

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

# Geçici bir dizin oluşturalım
TMP_DIR=$(mktemp -d)
echo "Geçici dizin oluşturuldu: ${TMP_DIR}"

# Cleanup fonksiyonumuz
cleanup() {
    local exit_code=$?
    echo "Temizlik yapılıyor: ${TMP_DIR} siliniyor..."
    rm -rf "${TMP_DIR}"
    
    if [ $exit_code -ne 0 ]; then
        echo "Script HATA ile sonlandı! Exit code: ${exit_code}"
    else
        echo "Script başarıyla tamamlandı."
    fi
    exit $exit_code
}

# EXIT sinyalini yakala (Hata olsun ya da olmasın, script çıkarken çalışır)
trap cleanup EXIT

# İş yükü simülasyonu
echo "İşlem yapılıyor..."
touch "${TMP_DIR}/important_data.tmp"

# Hata simülasyonu (Burada script patlayacak ama trap çalışacak!)
grep "olmayan_pattern" "${TMP_DIR}/important_data.tmp"

Standartlara Uygun Exit Code Kullanımı

Gelişigüzel exit 1 yazıp geçmek, üst seviye bir otomasyonda kabul edilemez. Script’inizin çağıran sisteme (örneğin Jenkins, GitHub Actions veya bir cron job) neyin yanlış gittiğini net bir şekilde söylemesi gerekir.

POSIX standartlarına ve /usr/include/sysexits.h yapısına uygun olarak şu kodları tercih edin:

  • 0: Başarılı operasyon.
  • 64 (EX_USAGE): Yanlış CLI argüman kullanımı.
  • 69 (EX_UNAVAILABLE): Servis veya kaynak ulaşılamaz durumda.
  • 70 (EX_SOFTWARE): İçsel yazılım hatası (Internal error).
  • 74 (EX_IOERR): Giriş/Çıkış (I/O) hatası (Disk dolu, dosya yazılamadı).
# Örnek kullanım
if [[ $# -lt 2 ]]; then
    echo "Hata: Eksik parametre. Kullanım: $0  " >&2
    exit 64
fi

Script’lerinizi Test Edin: BATS (Bash Automated Testing System)

SRE dünyasında “test edilmeyen kod çalışmıyordur” kuralı geçerlidir. Bash script’lerinizi manuel test etmek yerine, declarative testler yazmak için bats-core kullanabilirsiniz.

Önce basit bir script’imiz olsun (deploy.sh):

#!/usr/bin/env bash
# deploy.sh
set -euo pipefail

check_env() {
    if [[ -z "${APP_ENV:-}" ]]; then
        echo "Hata: APP_ENV tanımlı değil!" >&2
        return 64
    fi
}

check_env
echo "Deploying to ${APP_ENV}"

Şimdi bu script için yazacağımız BATS test dosyası (deploy.bats):

#!/usr/bin/env bats

# Test 1: APP_ENV tanımlı değilse script 64 koduyla hata vermeli
@test "APP_ENV set edilmediğinde script hata fırlatmalı" {
  run ./deploy.sh
  [ "$status" -eq 64 ]
  [[ "$output" =~ "Hata: APP_ENV tanımlı değil!" ]]
}

# Test 2: APP_ENV tanımlı olduğunda başarılı olmalı
@test "APP_ENV set edildiğinde script başarılı olmalı" {
  export APP_ENV="production"
  run ./deploy.sh
  [ "$status" -eq 0 ]
  [[ "$output" =~ "Deploying to production" ]]
}

Bu testleri CI pipeline’ınızda bats deploy.bats komutuyla çalıştırarak, script’lerinizin gelecekteki değişikliklerde kırılmasını engelleyebilirsiniz.

Özet

Bash’te hata yönetimi, sadece set -e yazıp şansımıza güvenmekten ibaret değildir. Profesyonel otomasyon süreçlerinde, pipefail ile boru hatlarını güvenceye almak, trap ile arkamızı temizlemek, standart exit code’lar ile çağıran sistemlere anlamlı yanıtlar dönmek ve nihayetinde BATS ile bu davranışları test etmek gerekir. Unutmayın, iyi bir DevOps mühendisi sadece çalışan kod değil, güvenle patlayan kod yazar.

Etiketler: , , , ,
Copyright 20254541. All rights reserved.

Posted 23 Ağustos 2024 by Kerem Danış in category "Genel