Bash’te Hata Yönetimi: set -euo pipefail, Trap ve Bats ile Defansif Scripting
Gece saat 03:00. PagerDuty çalıyor. Sorun: Kubernetes cluster’ına yeni bir deployment çıkılırken pipeline yarıda kalmış ama deploy başarılıymış gibi davranıp eski stabil replicaları da temizlemiş. Loglara bakıyorsunuz; kritik bir environment variable tanımlanmadığı için script sessizce patlamış, fakat exit code 0 döndüğü için CI/CD pipeline’ı her şeyin yolunda olduğunu varsayarak devam etmiş. Hepimiz bu filmi en az bir kere izledik. Günümüz modern altyapılarında bash scripting ve linux tabanlı otomasyon süreçleri, devops mühendisliğinin görünmez omurgasını oluşturuyor. Ancak bu omurga genellikle oldukça kırılgan temeller üzerine kurulu.
Bu makalede, Bash betiklerinizi kurumsal seviyede, hata toleransı yüksek ve test edilebilir yapılara dönüştürmenin yollarını inceleyeceğiz. Temel kavramları bir kenara bırakıp, doğrudan production ortamlarında hayat kurtaran pratik yaklaşımlara odaklanacağız.
Sessiz Katilleri Durdurun: “set -euo pipefail” Anatomisi
Bash, varsayılan olarak aşırı iyimser bir kabuktur. Bir komut hata verse de, bir değişken tanımlanmamış olsa da “yola devam et” felsefesini benimser. Bu felsefe interaktif terminal kullanımı için harika olsa da, otomasyon scriptleri için tam bir felakettir. Bu felaketi önlemenin ilk adımı, script’lerin başına o meşhur üçlüyü eklemektir:
set -euo pipefail
Peki bu parametreler arka planda tam olarak ne yapar ve neden hayati önem taşırlar? Tek tek inceleyelim.
1. set -e (errexit)
Bu parametre, script içindeki herhangi bir komut non-zero (sıfır dışı) bir exit code ile sonuçlandığında script’in anında sonlandırılmasını sağlar. Varsayılan davranışta Bash, komut hata alsa bile bir sonraki satıra geçmeye çalışır.
Neden kritik? Aşağıdaki senaryoyu düşünün:
cd /tmp/non_existent_directory
rm -rf *
Eğer ilk satırdaki cd komutu dizin bulunamadığı için başarısız olursa ve set -e aktif değilse, script bir sonraki satıra geçer ve o an hangi dizinde bulunuyorsa (muhtemelen script’in çalıştığı root dizini) oradaki tüm dosyaları siler. set -e bu felaketi engeller.
2. set -u (nounset)
Eğer tanımlanmamış bir değişkeni okumaya çalışırsanız, Bash bunu boş bir string olarak kabul eder ve hata vermez. set -u, tanımlanmamış her değişkeni ölümcül bir hata (fatal error) olarak kabul eder ve script’i durdurur.
Neden kritik?
# set -u aktif değilse:
TARGET_DIR="" # ya da yanlışlıkla typo yapılmış bir değişken
rm -rf "${TARGET_DIR}/bin" # Sistem "rm -rf /bin" komutunu çalıştırır!
set -u aktif olduğunda, Bash bu satıra geldiğinde TARGET_DIR: unbound variable hatası fırlatacak ve execution’ı hemen durduracaktır.
3. set -o pipefail
Bash’te pipeline (boru) işlemlerinde varsayılan olarak sadece son komutun exit code’u dikkate alınır. Örneğin komut1 | komut2 | komut3 zincirinde komut1 patlasa bile, komut3 başarılı olursa tüm pipeline başarılı (exit 0) sayılır.
Neden kritik?
# pipefail aktif değilse:
curl -s https://invalid-url-destination.tar.gz | tar -xzf -
echo $? # Çıktı: 0
Yukarıdaki örnekte curl başarısız olacak ve hiçbir veri indiremeyecektir. Ancak tar komutu boş girdi aldığında (veya pipe kapandığında) hata üretse de, exit code bazen yanıltıcı olabilir. Daha da kötüsü, aradaki kritik log analiz araçlarında hatalar yutulur. set -o pipefail aktif edildiğinde, pipeline içindeki en sağdaki non-zero exit code tüm pipeline’ın dönüş kodu kabul edilir.
—
İstisnaları Yönetmek: set -e Altında Güvenli Hata Toleransı
set -e kullanmaya başladığınızda karşılaşacağınız ilk sorun, bazı komutların hata vermesinin aslında beklenen bir durum olmasıdır. Örneğin bir dizinin varlığını kontrol etmek veya grep ile bir log dosyasında spesifik bir kelimeyi aramak gibi durumlarda, exit code’un 1 gelmesi script’i patlatmamalıdır.
Hata Kodunu Güvenli Bir Şekilde Absorbe Etmek
Eğer bir komutun başarısız olabileceğini biliyor ve bunun script’i durdurmasını istemiyorsanız, OR (||) operatörünü kullanarak hata durumunu bypass edebilirsiniz:
# grep satır bulamazsa normalde exit 1 verir ve script set -e yüzünden durur.
# || true kullanarak bunu engelliyoruz.
result=$(grep "ERROR" /var/log/app.log || true)
# Veya alternatif olarak lokal bir hata değişkenine atayabilirsiniz:
grep "CRITICAL" /var/log/app.log || local_exit=$?
if [ ${local_exit:-0} -ne 0 ]; then
echo "Kritik log bulunamadı, ama yola devam ediyoruz."
fi
Bir diğer profesyonel yaklaşım ise conditional block’lar kullanmaktır. if, elif, while veya until ifadelerinin içinde çalıştırılan komutlar, hata verseler dahi set -e engeline takılmazlar:
# Bu blok set -e aktif olsa bile güvenle çalışır
if ! ping -c 1 8.8.8.8 > /dev/null 2>&1; then
echo "İnternet bağlantısı yok, ancak script devam ediyor."
fi
—
Trap Mekanizması: Kaynakları Temizlemek ve Graceful Shutdown
Script’iniz ister başarıyla tamamlansın, ister yarıda hata alıp dursun; arkasında çöp bırakmamalıdır. Geçici dosyalar (temp files), oluşturulan socket’ler, kilit dosyaları (lock files) veya aktif edilen SSH tünelleri her durumda temizlenmelidir. İşte bu noktada Linux kernel sinyallerini yakalayan trap devreye girer.
Aşağıdaki boilerplate şablonu, production seviyesindeki script’lerinizde güvenle kullanabilirsiniz:
#!/usr/bin/env bash
set -euo pipefail
# Geçici dizin oluşturuluyor
TMP_DIR=$(mktemp -d -t my-app-XXXXXX)
echo "Geçici çalışma alanı: ${TMP_DIR}"
# Temizlik fonksiyonu
cleanup() {
local exit_code=$?
echo "Temizlik işlemi başlatılıyor... Son exit code: ${exit_code}"
rm -rf "${TMP_DIR}"
# Eğer script bir hata yüzünden durduysa ek bildirimler buraya yazılabilir (Slack webhook vb.)
if [ ${exit_code} -ne 0 ]; then
echo "Script beklenmedik bir şekilde sonlandı!" >&2
fi
}
# EXIT sinyalini yakala. Script nasıl biterse bitsin cleanup çalışacak.
trap cleanup EXIT
Neden EXIT Sinyali?
Birçok junior geliştirici trap için sadece ERR veya INT sinyallerini tanımlar. Ancak EXIT pseudosignal’i, script normal yollarla bittiğinde de, set -e yüzünden yarıda kesildiğinde de veya bir syntax hatası alındığında da tetiklenir. Bu yüzden tek bir merkezden temizlik yönetimi için en güvenli limandır.
—
Exit Code Standartları: Kurumsal Entegrasyon
Yazdığınız script’lerin diğer otomasyon araçları (Ansible, Jenkins, ArgoCD vb.) tarafından doğru yorumlanabilmesi için POSIX standartlarına uygun exit code’lar dönmesi gerekir. Sadece exit 1 yazıp geçmek, hata analizi yaparken size zaman kaybettirir.
| Exit Code | Anlamı | Kullanım Senaryosu |
|---|---|---|
| 0 | Success | Script görevini başarıyla tamamladı. |
| 1 | Catchall for general errors | Genel hatalar (izin yetersizliği, parse hataları vb.) |
| 2 | Misuse of shell builtins | Eksik veya hatalı argüman kullanımı. |
| 126 | Command invoked cannot execute | Çalıştırılmak istenen dosya execute yetkisine sahip değil. |
| 127 | Command not found | Script içindeki bir binary sistemde yüklü değil. |
Scriptlerinizde custom hata kodları tanımlarken 64-113 (BSD standardı) veya 128’den büyük olmayan serbest kodları (örneğin 10, 20, 30 gibi) kullanmanız, sistem hata kodlarıyla çakışmayı önler:
readonly ERR_INVALID_ARG=2
readonly ERR_DB_CONNECTION=10
readonly ERR_DISK_FULL=11
if [ $# -lt 1 ]; then
echo "Hata: En az bir argüman girmelisiniz." >&2
exit ${ERR_INVALID_ARG}
fi
—
Bats (Bash Automated Testing System) ile Unit Test Yazımı
“Bash script’inin testi mi olur?” demeyin. Eğer yazdığınız script kritik bir deploy sürecini yönetiyorsa, her değişiklikten sonra manuel test yapmak hem riskli hem de zaman alıcıdır. Bats, Bash script’lerinizi gerçek birer yazılım projesi gibi test etmenizi sağlar.
Öncelikle test edeceğimiz basit ama fonksiyonel bir script yazalım (isim: deploy.sh):
#!/usr/bin/env bash
set -euo pipefail
deploy_artifact() {
local env=$1
if [ -z "${env}" ]; then
echo "Hata: Environment belirtilmedi." >&2
return 2
fi
if [ "${env}" == "prod" ]; then
echo "Production deployment başarılı."
return 0
else
echo "Bilinmeyen environment: ${env}" >&2
return 10
fi
}
# Script doğrudan çalıştırıldıysa (test import etmediyse) main'i çalıştır
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
deploy_artifact "${1:-}"
fi
Şimdi bu script için yazacağımız unit test dosyasına bakalım (isim: deploy.bats):
#!/usr/bin/env bats
# Test öncesi script'i load et
setup() {
source ./deploy.sh
}
@test "Parametre verilmediğinde script exit code 2 ile patlamalı" {
run deploy_artifact ""
[ "$status" -eq 2 ]
[ "$output" = "Hata: Environment belirtilmedi." ]
}
@test "Prod environment sağlandığında başarılı dönmeli" {
run deploy_artifact "prod"
[ "$status" -eq 0 ]
[ "$output" = "Production deployment başarılı." ]
}
@test "Bilinmeyen env verildiğinde exit code 10 dönmeli" {
run deploy_artifact "staging"
[ "$status" -eq 10 ]
}
Bu testleri lokalinizde veya CI/CD pipeline’ınızda çalıştırmak için tek yapmanız gereken bats binary’sini çağırmaktır:
bats deploy.bats
Sonuçlar yeşil yandığında, refactoring yaparken hiçbir şeyi bozmadığınızdan emin olarak production’a güvenle push edebilirsiniz.
—
Sonuç: Defansif Scripting’i Alışkanlık Haline Getirin
Bash scripting, küçümsenen ama sistem mühendisliğinin en kritik katmanlarından biridir. Script yazarken kodun sadece “mutlu senaryoda” (happy path) nasıl çalışacağını değil, nerede ve nasıl patlayabileceğini tasarlamak gerekir. set -euo pipefail ile sessiz hataların önüne geçmek, trap ile sistem kaynaklarını her durumda temiz bırakmak ve bats ile bu süreçleri test edilebilir kılmak, sizi sıradan bir script yazıcısından profesyonel bir sistem mimarına dönüştürür.