Bash ile Yedekleme Scripti Yazımı: Adım Adım Pratik Örnek

Üretim ortamında bir gün gelir ve o korkulan an yaşanır: disk bozulur, yanlışlıkla rm -rf çalışır ya da bir uygulama güncellemesi her şeyi mahveder. O an “keşke yedeğim olsaydı” demek istemiyorsanız, şimdi harekete geçme zamanı. Bu yazıda sıfırdan, gerçekten işe yarar bir yedekleme scripti yazacağız. Sadece teorik değil, benim de aktif olarak kullandığım ve üretim sunucularında çalışan bir yapıyı adım adım inceleyeceğiz.

Neyi, Neden Yedekliyoruz?

Yedekleme scripti yazmadan önce şunu netleştirmek gerekiyor: neyi yedekleyeceğiz ve bu yedekler ne kadar süre saklanacak? Çoğu sysadmin bu soruları atlayıp direkt koda dalıyor, sonra da tutarsız yedeklerle boğuşuyor.

Tipik bir web sunucusunda yedeklenmesi gereken şeyler şunlardır:

  • /etc dizini: Sistem konfigürasyonları, nginx/apache ayarları, cron dosyaları
  • Uygulama dizinleri: /var/www, /opt altındaki uygulamalar
  • Veritabanları: MySQL/PostgreSQL dump dosyaları
  • Kullanıcı verileri: /home dizinleri
  • SSL sertifikaları: /etc/letsencrypt veya benzeri dizinler

Saklama politikası açısından genellikle şu strateji işe yarar: günlük yedekler 7 gün, haftalık yedekler 4 hafta, aylık yedekler 6 ay. Bu yazıda bu yapıyı otomatik olarak yönetecek bir script geliştireceğiz.

Temel Yapıyı Kuralım

Önce script’in iskeletini oluşturalım. İyi bir bash script’i her zaman değişken tanımlamaları, fonksiyonlar ve ana akış olmak üzere üç bölümden oluşur. Ayrıca set -euo pipefail kullanmak kritik önem taşır; bu satır scriptin hata aldığında sessizce devam etmesini engeller.

#!/bin/bash
set -euo pipefail

# ============================================
# Yedekleme Scripti v2.1
# Yazar: [email protected]
# Son Güncelleme: 2024
# ============================================

# --- YAPILANDIRMA ---
BACKUP_ROOT="/mnt/backup"
SOURCE_DIRS=("/etc" "/var/www" "/home")
DB_USER="backup_user"
DB_PASS="guvenli_sifre"
DB_NAMES=("uygulama_db" "kullanici_db")
LOG_FILE="/var/log/backup.log"
RETENTION_DAILY=7
RETENTION_WEEKLY=4
RETENTION_MONTHLY=6
HOSTNAME=$(hostname -s)
DATE=$(date +%Y-%m-%d)
TIMESTAMP=$(date +%Y%m%d_%H%M%S)

# Bildirim için (opsiyonel)
MAIL_TO="[email protected]"
SLACK_WEBHOOK=""

Bu yapıda tüm ayarlar script’in başında toplanmış durumda. Bir şeyi değiştirmeniz gerektiğinde kodu karıştırmanıza gerek kalmıyor.

Log Fonksiyonu: Her Şeyi Kayıt Altına Alın

Yedekleme scriptlerinde en çok yapılan hata log tutmamak ya da yetersiz log tutmaktır. Sabah 3’te çalışan bir script başarısız olduğunda, neyin yanlış gittiğini ancak iyi bir log sayesinde anlayabilirsiniz.

# Log seviyeleri: INFO, WARN, ERROR, SUCCESS
log() {
    local level="$1"
    shift
    local message="$*"
    local log_line="[$(date '+%Y-%m-%d %H:%M:%S')] [$level] $message"
    
    echo "$log_line" | tee -a "$LOG_FILE"
    
    # ERROR durumunda mail gönder
    if [[ "$level" == "ERROR" && -n "$MAIL_TO" ]]; then
        echo "$log_line" | mail -s "HATA: Yedekleme Başarısız - $HOSTNAME" "$MAIL_TO" 2>/dev/null || true
    fi
}

# Kullanım örnekleri:
# log "INFO" "Yedekleme başladı"
# log "ERROR" "Disk alanı yetersiz"
# log "SUCCESS" "Tüm yedekler tamamlandı"

tee -a kullanarak hem terminale hem de log dosyasına aynı anda yazıyoruz. || true kısmı ise mail gönderimi başarısız olsa bile script’in durmadığından emin oluyor.

Ön Kontroller: Script Başlamadan Her Şeyi Doğrula

Script’in işe başlamadan önce gerekli koşulları kontrol etmesi gerekiyor. Disk alanı yeterli mi? Hedef dizin mount edilmiş mi? Gerekli araçlar kurulu mu?

pre_checks() {
    log "INFO" "Ön kontroller başlıyor..."
    
    # Root yetkisi kontrolü
    if [[ $EUID -ne 0 ]]; then
        log "ERROR" "Bu script root olarak çalıştırılmalıdır"
        exit 1
    fi
    
    # Backup dizini var mı ve yazılabilir mi?
    if [[ ! -d "$BACKUP_ROOT" ]]; then
        log "ERROR" "Yedekleme dizini bulunamadı: $BACKUP_ROOT"
        exit 1
    fi
    
    if [[ ! -w "$BACKUP_ROOT" ]]; then
        log "ERROR" "Yedekleme dizinine yazma izni yok: $BACKUP_ROOT"
        exit 1
    fi
    
    # Disk alanı kontrolü (minimum 10GB)
    local available_space
    available_space=$(df -BG "$BACKUP_ROOT" | awk 'NR==2 {print $4}' | tr -d 'G')
    
    if [[ "$available_space" -lt 10 ]]; then
        log "ERROR" "Yetersiz disk alanı: ${available_space}GB mevcut, minimum 10GB gerekli"
        exit 1
    fi
    
    log "INFO" "Kullanılabilir disk alanı: ${available_space}GB"
    
    # Gerekli araçların varlığını kontrol et
    local required_tools=("tar" "gzip" "mysqldump" "rsync")
    for tool in "${required_tools[@]}"; do
        if ! command -v "$tool" &>/dev/null; then
            log "WARN" "Araç bulunamadı: $tool - ilgili yedekler atlanacak"
        fi
    done
    
    log "INFO" "Ön kontroller tamamlandı"
}

Bu fonksiyon özellikle NFS veya uzak bir depolama alanına yedekleme yapıyorsanız kritik önem taşır. Mount edilmemiş bir dizine yedek almaya çalışmak, lokal diske dolmaya başlamanıza neden olabilir.

Dizin Yedekleme Fonksiyonu

Dosya sistemi yedeklemesi için tar ile sıkıştırma yapacağız. Burada dikkat edilmesi gereken nokta, hataların doğru şekilde yakalanmasıdır. tar bazı durumlarda uyarı vererek çalışmaya devam eder (örneğin değişen dosyalar), bu yüzden çıkış kodunu akıllıca yorumlamak gerekiyor.

backup_directory() {
    local source_dir="$1"
    local backup_name
    backup_name=$(echo "$source_dir" | tr '/' '_' | sed 's/^_//')
    local backup_file="${BACKUP_ROOT}/daily/${HOSTNAME}_${backup_name}_${TIMESTAMP}.tar.gz"
    
    if [[ ! -d "$source_dir" ]]; then
        log "WARN" "Kaynak dizin bulunamadı, atlanıyor: $source_dir"
        return 0
    fi
    
    log "INFO" "Yedekleniyor: $source_dir -> $backup_file"
    
    local start_time=$SECONDS
    
    # tar çıkış kodu 1 = uyarılar (dosya değişti vs.), 2 = ciddi hata
    if tar -czf "$backup_file" 
        --exclude="*.log" 
        --exclude="*.tmp" 
        --exclude="*.cache" 
        --exclude="*/node_modules/*" 
        --exclude="*/.git/*" 
        "$source_dir" 2>/tmp/tar_errors; then
        
        local elapsed=$((SECONDS - start_time))
        local file_size
        file_size=$(du -sh "$backup_file" | cut -f1)
        log "SUCCESS" "$source_dir yedeklendi: $file_size, ${elapsed}s sürdü"
        
    elif [[ $? -eq 1 ]]; then
        # Sadece uyarılar var, yedek muhtemelen kullanılabilir
        log "WARN" "$source_dir yedeklendi ancak uyarılar var:"
        cat /tmp/tar_errors | head -5 | while read -r line; do
            log "WARN" "  $line"
        done
    else
        log "ERROR" "$source_dir yedeklenirken ciddi hata oluştu"
        rm -f "$backup_file"
        return 1
    fi
    
    # Yedek dosyasının bütünlüğünü test et
    if ! tar -tzf "$backup_file" &>/dev/null; then
        log "ERROR" "Yedek dosyası bozuk: $backup_file"
        rm -f "$backup_file"
        return 1
    fi
    
    return 0
}

--exclude parametreleriyle node_modules ve .git gibi gereksiz dizinleri dışarıda bırakıyoruz. Bu sayede hem yedek boyutu küçülüyor hem de süre kısalıyor.

Veritabanı Yedekleme

Veritabanı yedeklemesi en kritik kısım. Burada dikkat edilmesi gereken birkaç nokta var: MySQL’de --single-transaction kullanmak canlı veritabanını kilitlemeden tutarlı bir yedek almanızı sağlar.

backup_databases() {
    log "INFO" "Veritabanı yedeklemeleri başlıyor..."
    
    local db_backup_dir="${BACKUP_ROOT}/daily/databases"
    mkdir -p "$db_backup_dir"
    
    # MySQL bağlantısını test et
    if ! mysql -u"$DB_USER" -p"$DB_PASS" -e "SELECT 1;" &>/dev/null; then
        log "ERROR" "MySQL bağlantısı kurulamadı"
        return 1
    fi
    
    local success_count=0
    local fail_count=0
    
    for db_name in "${DB_NAMES[@]}"; do
        local dump_file="${db_backup_dir}/${HOSTNAME}_${db_name}_${TIMESTAMP}.sql.gz"
        
        log "INFO" "Veritabanı yedekleniyor: $db_name"
        
        if mysqldump 
            -u"$DB_USER" 
            -p"$DB_PASS" 
            --single-transaction 
            --routines 
            --triggers 
            --events 
            --add-drop-database 
            --databases "$db_name" | gzip -9 > "$dump_file"; then
            
            local file_size
            file_size=$(du -sh "$dump_file" | cut -f1)
            log "SUCCESS" "$db_name yedeklendi: $file_size"
            ((success_count++))
        else
            log "ERROR" "$db_name yedeklenirken hata oluştu"
            rm -f "$dump_file"
            ((fail_count++))
        fi
    done
    
    log "INFO" "Veritabanı özeti: $success_count başarılı, $fail_count başarısız"
    
    [[ $fail_count -eq 0 ]] && return 0 || return 1
}

–single-transaction: InnoDB tablolarında tutarlı snapshot alır, tabloları kilitlemez. –routines: Stored procedure ve function’ları da dahil eder. –triggers: Trigger tanımlarını yedekler. –events: Zamanlanmış event’leri yedekler.

Eski Yedeklerin Temizlenmesi

Yedek almak kadar önemli olan diğer konu eski yedekleri temizlemek. Temizlik yapmazsanız disk dolar, tüm sistem çöker ve ironik biçimde yedek alamaz hale gelirsiniz.

cleanup_old_backups() {
    log "INFO" "Eski yedekler temizleniyor..."
    
    local daily_dir="${BACKUP_ROOT}/daily"
    local weekly_dir="${BACKUP_ROOT}/weekly"
    local monthly_dir="${BACKUP_ROOT}/monthly"
    
    # Günlük yedekler: 7 günden eski olanları sil
    local deleted_daily
    deleted_daily=$(find "$daily_dir" -type f -name "*.tar.gz" -mtime +"$RETENTION_DAILY" -print -delete 2>/dev/null | wc -l)
    log "INFO" "Silinen günlük yedek: $deleted_daily dosya"
    
    # SQL dump'ları da temizle
    find "$daily_dir/databases" -type f -name "*.sql.gz" -mtime +"$RETENTION_DAILY" -delete 2>/dev/null || true
    
    # Haftalık yedekler: 4 haftadan eski olanları sil
    if [[ -d "$weekly_dir" ]]; then
        local deleted_weekly
        deleted_weekly=$(find "$weekly_dir" -type f -name "*.tar.gz" -mtime +$((RETENTION_WEEKLY * 7)) -print -delete 2>/dev/null | wc -l)
        log "INFO" "Silinen haftalık yedek: $deleted_weekly dosya"
    fi
    
    # Aylık yedekler: 6 aydan eski olanları sil
    if [[ -d "$monthly_dir" ]]; then
        local deleted_monthly
        deleted_monthly=$(find "$monthly_dir" -type f -name "*.tar.gz" -mtime +$((RETENTION_MONTHLY * 30)) -print -delete 2>/dev/null | wc -l)
        log "INFO" "Silinen aylık yedek: $deleted_monthly dosya"
    fi
    
    # Boş dizinleri temizle
    find "$BACKUP_ROOT" -type d -empty -delete 2>/dev/null || true
    
    log "SUCCESS" "Temizlik tamamlandı"
}

# Haftalık/Aylık yedeklerin kopyalanması
rotate_backups() {
    local day_of_week=$(date +%u)  # 1=Pazartesi, 7=Pazar
    local day_of_month=$(date +%d)
    
    # Her Pazar günü haftalık yedeğe kopyala
    if [[ "$day_of_week" -eq 7 ]]; then
        log "INFO" "Haftalık yedek oluşturuluyor..."
        mkdir -p "${BACKUP_ROOT}/weekly"
        rsync -a "${BACKUP_ROOT}/daily/" "${BACKUP_ROOT}/weekly/$(date +%Y-W%V)/" 2>/dev/null || 
            log "WARN" "Haftalık rotasyon kısmen başarısız oldu"
    fi
    
    # Ayın 1'inde aylık yedeğe kopyala
    if [[ "$day_of_month" -eq "01" ]]; then
        log "INFO" "Aylık yedek oluşturuluyor..."
        mkdir -p "${BACKUP_ROOT}/monthly"
        rsync -a "${BACKUP_ROOT}/daily/" "${BACKUP_ROOT}/monthly/$(date +%Y-%m)/" 2>/dev/null || 
            log "WARN" "Aylık rotasyon kısmen başarısız oldu"
    fi
}

Özet Raporu ve Bildirim

Script tamamlandığında ne kadar veri yedeklendiğini, ne kadar sürdüğünü ve varsa hataları özetleyen bir rapor göndermek son derece faydalı.

send_summary() {
    local status="$1"
    local duration="$2"
    local total_size
    total_size=$(du -sh "${BACKUP_ROOT}/daily" 2>/dev/null | cut -f1 || echo "hesaplanamadı")
    
    local subject
    if [[ "$status" == "SUCCESS" ]]; then
        subject="BASARILI: Yedekleme Tamamlandi - $HOSTNAME - $DATE"
    else
        subject="HATA: Yedekleme Basarisiz - $HOSTNAME - $DATE"
    fi
    
    local body
    body=$(cat <<EOF
Sunucu: $HOSTNAME
Tarih: $DATE
Durum: $status
Süre: ${duration} saniye
Toplam Boyut: $total_size

Son Log Kayıtları:
$(tail -20 "$LOG_FILE")

Yedek Dizini: $BACKUP_ROOT
EOF
)
    
    # Mail gönder
    if [[ -n "$MAIL_TO" ]]; then
        echo "$body" | mail -s "$subject" "$MAIL_TO" 2>/dev/null || 
            log "WARN" "Mail gönderilemedi: $MAIL_TO"
    fi
    
    # Slack bildirimi (webhook tanımlıysa)
    if [[ -n "$SLACK_WEBHOOK" ]]; then
        local emoji="✅"
        [[ "$status" != "SUCCESS" ]] && emoji="🚨"
        
        curl -s -X POST "$SLACK_WEBHOOK" 
            -H 'Content-type: application/json' 
            --data "{"text":"${emoji} *${subject}*nSüre: ${duration}s | Boyut: ${total_size}"}" 
            &>/dev/null || true
    fi
}

Ana Fonksiyon: Her Şeyi Bir Araya Getir

main() {
    local start_time=$SECONDS
    local exit_code=0
    
    # Dizin yapısını oluştur
    mkdir -p "${BACKUP_ROOT}/daily/databases"
    mkdir -p "${BACKUP_ROOT}/weekly"
    mkdir -p "${BACKUP_ROOT}/monthly"
    
    log "INFO" "========================================"
    log "INFO" "Yedekleme başladı: $HOSTNAME"
    log "INFO" "Tarih: $(date '+%Y-%m-%d %H:%M:%S')"
    log "INFO" "========================================"
    
    # 1. Ön kontroller
    pre_checks
    
    # 2. Dizin yedeklemeleri
    for dir in "${SOURCE_DIRS[@]}"; do
        backup_directory "$dir" || exit_code=1
    done
    
    # 3. Veritabanı yedeklemeleri
    backup_databases || exit_code=1
    
    # 4. Rotasyon
    rotate_backups
    
    # 5. Temizlik
    cleanup_old_backups
    
    # 6. Süre hesapla ve rapor gönder
    local total_duration=$((SECONDS - start_time))
    
    if [[ $exit_code -eq 0 ]]; then
        log "SUCCESS" "Yedekleme başarıyla tamamlandı. Toplam süre: ${total_duration}s"
        send_summary "SUCCESS" "$total_duration"
    else
        log "ERROR" "Yedekleme hatalarla tamamlandı. Toplam süre: ${total_duration}s"
        send_summary "HATA" "$total_duration"
    fi
    
    exit $exit_code
}

# Script direkt çalıştırıldığında main'i çağır
main "$@"

Crontab Kurulumu

Script hazır, şimdi otomatik çalışmasını sağlayalım:

# Script'i çalıştırılabilir yap
chmod 700 /opt/scripts/backup.sh

# Sahipliği root'a ver (güvenlik için)
chown root:root /opt/scripts/backup.sh

# Crontab düzenle
crontab -e

Crontab içeriği:

# Her gece saat 02:30'da yedekleme
30 2 * * * /opt/scripts/backup.sh >> /var/log/backup_cron.log 2>&1

# Log dosyasını logrotate ile yönet
# /etc/logrotate.d/backup dosyası oluştur:
# /var/log/backup.log {
#     weekly
#     rotate 4
#     compress
#     missingok
#     notifempty
# }

Gerçek Dünya Senaryoları ve İpuçları

Senaryo 1: E-ticaret Sitesi

Bir e-ticaret sitesinde yedekleme yapıyorsanız, yedek alırken sitenin maintenance moduna geçmesi gerekmez ama sıra önemlidir. Önce veritabanı, sonra dosya sistemi yedeklenmelidir. Böylece dosyalar ile veritabanı tutarlı kalır.

Senaryo 2: Uzak Sunucuya Yedekleme

Yedekleri aynı sunucuda tutmak felaket kurtarma açısından yetersizdir. rsync ile uzak sunucuya göndermek için aşağıdaki satırı main() fonksiyonuna ekleyin:

# Uzak sunucuya sync (SSH key auth gerektirir)
sync_to_remote() {
    local remote_host="backup-server.sirket.com"
    local remote_path="/backups/${HOSTNAME}/"
    local ssh_key="/root/.ssh/backup_key"
    
    log "INFO" "Uzak sunucuya senkronizasyon başlıyor..."
    
    if rsync -avz --delete 
        -e "ssh -i $ssh_key -o StrictHostKeyChecking=no -o ConnectTimeout=30" 
        "${BACKUP_ROOT}/daily/" 
        "backup@${remote_host}:${remote_path}" 
        --bwlimit=10240 
        2>>/tmp/rsync_errors; then
        log "SUCCESS" "Uzak senkronizasyon tamamlandı"
    else
        log "WARN" "Uzak senkronizasyon başarısız oldu, yerel yedekler mevcut"
    fi
}

–bwlimit=10240: Bant genişliğini 10MB/s ile sınırlar, canlı trafik etkilenmez.

Yedek Bütünlüğünü Düzenli Test Edin

Yedek aldığınızı sanmak ile gerçekten kullanılabilir bir yedeğe sahip olmak farklı şeyler. Haftada bir yedek dosyalarının açılabildiğini test eden küçük bir script çalıştırın:

#!/bin/bash
# test_backup.sh - Haftalık bütünlük testi

BACKUP_ROOT="/mnt/backup"
LOG_FILE="/var/log/backup_test.log"

echo "[$(date)] Bütünlük testi başladı" | tee -a "$LOG_FILE"

fail_count=0
pass_count=0

while IFS= read -r -d '' backup_file; do
    if tar -tzf "$backup_file" &>/dev/null; then
        ((pass_count++))
    else
        echo "[HATA] Bozuk yedek: $backup_file" | tee -a "$LOG_FILE"
        ((fail_count++))
    fi
done < <(find "${BACKUP_ROOT}/daily" -name "*.tar.gz" -mtime -1 -print0)

echo "[$(date)] Test tamamlandı: $pass_count geçti, $fail_count başarısız" | tee -a "$LOG_FILE"

if [[ $fail_count -gt 0 ]]; then
    echo "UYARI: $fail_count bozuk yedek dosyası bulundu!" | 
        mail -s "Yedek Bütünlük Hatası - $(hostname)" [email protected]
fi

Güvenlik Dikkat Noktaları

Script’inizde dikkat etmeniz gereken güvenlik konuları:

  • Şifreler: Veritabanı şifrelerini script içinde düz metin olarak tutmak yerine /root/.my.cnf dosyası kullanın ya da şifreyi environment variable olarak geçirin.
  • İzinler: Script sadece root tarafından okunabilir ve çalıştırılabilir olmalı (chmod 700).
  • Yedek Dizini: Yedek dizinine sadece backup sürecinin erişimi olmalı.
  • SSH Key: Uzak yedeklemede kullanılan SSH key sadece yedekleme işlemi için kısıtlanmış yetkilerle oluşturulmalı.

MySQL için güvenli kimlik doğrulama örneği:

# /root/.my.cnf
[client]
user=backup_user
password=guvenli_sifre_buraya

Bu dosyanın izinlerini chmod 600 /root/.my.cnf olarak ayarlarsanız, script içinde şifre kullanmak zorunda kalmazsınız. mysqldump otomatik olarak bu dosyayı okur.

Sonuç

İyi bir yedekleme scripti sadece tar komutu çalıştırmaktan ibaret değildir. Ön kontroller, anlamlı loglama, eski yedeklerin temizlenmesi, bütünlük testleri ve bildirimler hep birlikte çalıştığında gerçekten güvenilir bir yedekleme altyapısı elde edersiniz.

Bu yazıda geliştirdiğimiz scripti temel alarak kendi ortamınıza adapte etmenizi öneririm. SOURCE_DIRS, DB_NAMES ve RETENTION_* değişkenlerini kendi ihtiyaçlarınıza göre ayarlayın. İlk kurulumda scripti cron’a eklemeden önce manuel olarak birkaç kez çalıştırın, logları inceleyin, üretilen yedeklerin beklediğiniz gibi olduğunu doğrulayın.

En önemlisi: yedek almak iş değil, yedekten geri dönmeyi test etmek iştir. Ayda en az bir kez yedeklerinizden gerçek bir restore yaparak sistemin çalıştığından emin olun. O kritik an geldiğinde sakinliğinizi koruyabilmenizin tek yolu budur.

Yorum yapın