Rsync ile Çoklu Sunucu Yedekleme Scripti

Birden fazla sunucuyu yönetiyorsanız ve her gece manuel yedekleme yapmaktan yorulduysanız, bu yazı tam size göre. Rsync tabanlı otomatik yedekleme sistemi kurmak, hem basit hem de son derece güvenilir bir çözüm sunuyor. Büyük kurumsal araçlara para harcamadan, sadece bash ve rsync ile production-grade bir yedekleme altyapısı oluşturabilirsiniz.

Neden Rsync?

Rsync, delta transfer algoritmasıyla sadece değişen blokları kopyalar. Yani ilk yedeklemenin ardından sonraki yedeklemeler çok daha hızlı tamamlanır. Bunun yanında:

  • Bant genişliği tasarrufu sağlar, sadece değişen veriler aktarılır
  • SSH üzerinden şifreli transfer yapabilir
  • Checksum doğrulaması ile veri bütünlüğünü kontrol eder
  • Paralel çalışma desteğiyle birden fazla sunucuyu eş zamanlı yedekleyebilirsiniz
  • Dry-run modu ile gerçek yedekleme yapmadan neyin kopyalanacağını görebilirsiniz

Kurumsal çözümlerin aksine, rsync’i tam anlamıyla kontrol edersiniz. Bir şey ters giderse kodu açıp bakarsınız.

Senaryo: 5 Sunuculu Bir Altyapı

Gerçek dünya örneğimizde şu yapı var:

  • web-01 (192.168.1.10): Nginx + PHP uygulaması
  • web-02 (192.168.1.11): Yük dengeleme için ikinci web sunucusu
  • db-01 (192.168.1.20): PostgreSQL ana veritabanı
  • db-02 (192.168.1.21): Redis önbellekleme sunucusu
  • app-01 (192.168.1.30): Node.js servisler

Hedef: Bu 5 sunucunun kritik dizinlerini her gece merkezi bir yedekleme sunucusuna taşımak.

Temel Yapı ve Dizin Organizasyonu

Önce yedekleme sunucusunda gerekli dizin yapısını oluşturalım:

#!/bin/bash
# setup_backup_structure.sh

BACKUP_ROOT="/opt/backups"
SERVERS=("web-01" "web-02" "db-01" "db-02" "app-01")

for server in "${SERVERS[@]}"; do
    mkdir -p "${BACKUP_ROOT}/${server}/daily"
    mkdir -p "${BACKUP_ROOT}/${server}/weekly"
    mkdir -p "${BACKUP_ROOT}/${server}/monthly"
    mkdir -p "${BACKUP_ROOT}/${server}/logs"
done

echo "Dizin yapisi olusturuldu: ${BACKUP_ROOT}"
ls -la "${BACKUP_ROOT}"

Bu yapıyı benimsemenin sebebi; günlük, haftalık ve aylık yedeklemeleri ayrı tutmak ve log dosyalarını sunucu bazında organize etmek.

SSH Anahtarı Yapılandırması

Script şifre sormadan çalışacaksa SSH anahtar tabanlı kimlik doğrulama şart. Yedekleme sunucusunda:

# Yedekleme servisi icin ozel kullanici olustur
useradd -m -s /bin/bash backupuser
su - backupuser

# SSH anahtar cifti olustur (passphrase olmadan)
ssh-keygen -t ed25519 -C "backup-service-$(date +%Y%m%d)" -f ~/.ssh/backup_key -N ""

# Anahtari her sunucuya kopyala
for server in web-01 web-02 db-01 db-02 app-01; do
    ssh-copy-id -i ~/.ssh/backup_key.pub backupuser@${server}
    echo "${server} icin anahtar kopyalandi"
done

Güvenlik açısından önemli bir not: Uzak sunuculardaki authorized_keys dosyasına kısıtlamalar ekleyin:

# Uzak sunucularda ~/.ssh/authorized_keys dosyasina eklenecek
# Bu sekilde bu anahtar sadece rsync komutlarini calistirabilir
command="rsync --server --sender -logDtpre.iLsfxCIvu . /",no-agent-forwarding,no-port-forwarding,no-pty,no-user-rc,no-X11-forwarding ssh-ed25519 AAAA...anahtariniz...

Ana Yedekleme Scripti

Şimdi asıl işi yapan scripte geçelim. Bu script her şeyi hallediyor: loglama, hata yönetimi, bildirim ve döngüsel yedekleme:

#!/bin/bash
# /opt/scripts/multi_server_backup.sh
# Versiyon: 2.1
# Aciklama: Coklu sunucu rsync yedekleme sistemi

set -euo pipefail

# ============================================
# KONFIGÜRASYON
# ============================================

BACKUP_ROOT="/opt/backups"
SSH_KEY="/home/backupuser/.ssh/backup_key"
SSH_USER="backupuser"
LOG_DIR="/var/log/backups"
NOTIFICATION_EMAIL="[email protected]"
SLACK_WEBHOOK="https://hooks.slack.com/services/XXX/YYY/ZZZ"
LOCK_FILE="/var/run/backup.lock"
MAX_PARALLEL=3
BACKUP_DATE=$(date +%Y-%m-%d)
BACKUP_HOUR=$(date +%H)
TIMESTAMP=$(date +%Y%m%d_%H%M%S)

# Rsync genel parametreleri
RSYNC_OPTS="-avz --delete --delete-excluded 
    --timeout=300 
    --bwlimit=50000 
    --exclude='*.tmp' 
    --exclude='*.log' 
    --exclude='.git' 
    --exclude='node_modules' 
    --exclude='__pycache__'"

# ============================================
# SUNUCU TANIMLARI
# ============================================

declare -A SERVER_PATHS
SERVER_PATHS["web-01"]="/var/www/html /etc/nginx /etc/php"
SERVER_PATHS["web-02"]="/var/www/html /etc/nginx /etc/php"
SERVER_PATHS["db-01"]="/var/lib/postgresql /etc/postgresql /opt/scripts"
SERVER_PATHS["db-02"]="/var/lib/redis /etc/redis"
SERVER_PATHS["app-01"]="/opt/apps /etc/systemd/system /opt/configs"

declare -A SERVER_IPS
SERVER_IPS["web-01"]="192.168.1.10"
SERVER_IPS["web-02"]="192.168.1.11"
SERVER_IPS["db-01"]="192.168.1.20"
SERVER_IPS["db-02"]="192.168.1.21"
SERVER_IPS["app-01"]="192.168.1.30"

# ============================================
# YARDIMCI FONKSIYONLAR
# ============================================

log() {
    local level="$1"
    local message="$2"
    local logfile="${LOG_DIR}/backup_${BACKUP_DATE}.log"
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] [${level}] ${message}" | tee -a "${logfile}"
}

send_notification() {
    local status="$1"
    local message="$2"
    
    # Email bildirimi
    echo "${message}" | mail -s "[BACKUP] ${status} - ${BACKUP_DATE}" "${NOTIFICATION_EMAIL}"
    
    # Slack bildirimi
    if [ -n "${SLACK_WEBHOOK}" ]; then
        local emoji="✅"
        [ "${status}" == "HATA" ] && emoji="🚨"
        
        curl -s -X POST "${SLACK_WEBHOOK}" 
            -H 'Content-type: application/json' 
            --data "{"text":"${emoji} Yedekleme ${status}: ${message}"}" > /dev/null
    fi
}

check_disk_space() {
    local required_gb="$1"
    local available_kb
    available_kb=$(df "${BACKUP_ROOT}" | tail -1 | awk '{print $4}')
    local available_gb=$((available_kb / 1024 / 1024))
    
    if [ "${available_gb}" -lt "${required_gb}" ]; then
        log "HATA" "Yetersiz disk alani! Mevcut: ${available_gb}GB, Gereken: ${required_gb}GB"
        return 1
    fi
    
    log "BILGI" "Disk alani yeterli: ${available_gb}GB mevcut"
    return 0
}

Sunucu Yedekleme Fonksiyonu

Her sunucu için çalışan core fonksiyon:

# Bu kisim multi_server_backup.sh icine eklenir

backup_server() {
    local server_name="$1"
    local server_ip="${SERVER_IPS[$server_name]}"
    local paths="${SERVER_PATHS[$server_name]}"
    local dest_base="${BACKUP_ROOT}/${server_name}/daily/${BACKUP_DATE}"
    local server_log="${BACKUP_ROOT}/${server_name}/logs/backup_${TIMESTAMP}.log"
    local start_time
    start_time=$(date +%s)
    
    log "BILGI" "${server_name} yedeklemesi basliyor (IP: ${server_ip})"
    mkdir -p "${dest_base}"
    
    # Sunucuya ulasabilir miyiz kontrol et
    if ! ssh -i "${SSH_KEY}" -o ConnectTimeout=10 -o StrictHostKeyChecking=no 
        "${SSH_USER}@${server_ip}" "echo ok" &>/dev/null; then
        log "HATA" "${server_name} sunucusuna ulasilamiyor! Atlaniyor..."
        echo "${server_name}" >> "${LOG_DIR}/failed_servers_${BACKUP_DATE}.txt"
        return 1
    fi
    
    local success=true
    
    for path in ${paths}; do
        local dest_path="${dest_base}${path}"
        mkdir -p "${dest_path}"
        
        log "BILGI" "${server_name}:${path} yedekleniyor -> ${dest_path}"
        
        # Onceki yedek varsa link-dest kullan (incremental)
        local link_dest_opt=""
        local prev_backup
        prev_backup=$(find "${BACKUP_ROOT}/${server_name}/daily" -maxdepth 1 -type d 
            -name "????-??-??" | sort | tail -2 | head -1)
        
        if [ -n "${prev_backup}" ] && [ "${prev_backup}" != "${dest_base}" ]; then
            link_dest_opt="--link-dest=${prev_backup}${path}"
        fi
        
        if ! rsync ${RSYNC_OPTS} ${link_dest_opt} 
            -e "ssh -i ${SSH_KEY} -o StrictHostKeyChecking=no" 
            "${SSH_USER}@${server_ip}:${path}/" 
            "${dest_path}/" 
            >> "${server_log}" 2>&1; then
            log "UYARI" "${server_name}:${path} yedekleme hatasi! Log: ${server_log}"
            success=false
        fi
    done
    
    local end_time
    end_time=$(date +%s)
    local duration=$((end_time - start_time))
    local backup_size
    backup_size=$(du -sh "${dest_base}" 2>/dev/null | cut -f1)
    
    if ${success}; then
        log "BASARI" "${server_name} yedekleme tamamlandi. Sure: ${duration}s, Boyut: ${backup_size}"
        return 0
    else
        log "UYARI" "${server_name} yedekleme kismi hatalarla tamamlandi"
        return 1
    fi
}

Paralel Yedekleme ve Lock Mekanizması

5 sunucuyu sırayla yedeklemek yerine paralel çalıştıralım, ama sistemin gözden kaçmasını önlemek için lock mekanizması ekleyelim:

# Lock mekanizmasi
acquire_lock() {
    if [ -f "${LOCK_FILE}" ]; then
        local pid
        pid=$(cat "${LOCK_FILE}")
        if kill -0 "${pid}" 2>/dev/null; then
            log "HATA" "Baska bir yedekleme sureci calisıyor (PID: ${pid}). Cikiliyor."
            exit 1
        else
            log "UYARI" "Eski lock dosyasi bulundu, temizleniyor"
            rm -f "${LOCK_FILE}"
        fi
    fi
    echo $$ > "${LOCK_FILE}"
    trap 'rm -f ${LOCK_FILE}; exit' INT TERM EXIT
}

# Paralel yedekleme ana fonksiyonu
run_parallel_backups() {
    local pids=()
    local failed_servers=()
    local server_count=0
    
    for server in "${!SERVER_PATHS[@]}"; do
        # Max paralel limit kontrolu
        while [ "${#pids[@]}" -ge "${MAX_PARALLEL}" ]; do
            for i in "${!pids[@]}"; do
                if ! kill -0 "${pids[$i]}" 2>/dev/null; then
                    wait "${pids[$i]}" || failed_servers+=("${server}")
                    unset 'pids[$i]'
                fi
            done
            pids=("${pids[@]}")
            sleep 2
        done
        
        log "BILGI" "${server} icin arka plan prosesi baslatiliyor"
        backup_server "${server}" &
        pids+=($!)
        server_count=$((server_count + 1))
    done
    
    # Kalan tum proseslerin bitmesini bekle
    for pid in "${pids[@]}"; do
        if ! wait "${pid}"; then
            log "UYARI" "Bir yedekleme prosesi hatayla bitti (PID: ${pid})"
        fi
    done
    
    return ${#failed_servers[@]}
}

# Ana calisma blogu
main() {
    mkdir -p "${LOG_DIR}"
    log "BILGI" "============================================"
    log "BILGI" "Coklu sunucu yedekleme basliyor: ${TIMESTAMP}"
    log "BILGI" "============================================"
    
    acquire_lock
    check_disk_space 50 || { send_notification "HATA" "Disk alani yetersiz!"; exit 1; }
    
    run_parallel_backups
    local exit_code=$?
    
    if [ "${exit_code}" -eq 0 ]; then
        send_notification "BASARILI" "Tum sunucular basariyla yedeklendi. Tarih: ${BACKUP_DATE}"
        log "BILGI" "Tum yedeklemeler tamamlandi"
    else
        send_notification "HATA" "${exit_code} sunucuda hata olustu. Logları kontrol edin."
        log "HATA" "${exit_code} sunucuda sorun olustu"
    fi
}

main "$@"

Eski Yedeklerin Temizlenmesi (Retention Policy)

Disk doldurmasını önlemek için retention policy şart. Günlük 7, haftalık 4, aylık 12 yedek tutuyoruz:

#!/bin/bash
# /opt/scripts/cleanup_backups.sh

BACKUP_ROOT="/opt/backups"
LOG_FILE="/var/log/backups/cleanup_$(date +%Y%m%d).log"

cleanup_old_backups() {
    local server="$1"
    local server_backup_dir="${BACKUP_ROOT}/${server}"
    
    echo "[$(date)] ${server} icin temizlik basliyor" | tee -a "${LOG_FILE}"
    
    # Gunluk: Son 7 gunu tut
    find "${server_backup_dir}/daily" -maxdepth 1 -type d -name "????-??-??" 
        | sort | head -n -7 | while read -r old_backup; do
        echo "Siliniyor (gunluk): ${old_backup}" | tee -a "${LOG_FILE}"
        rm -rf "${old_backup}"
    done
    
    # Haftalik: Son 4 haftayi tut
    find "${server_backup_dir}/weekly" -maxdepth 1 -type d 
        | sort | head -n -4 | while read -r old_backup; do
        echo "Siliniyor (haftalik): ${old_backup}" | tee -a "${LOG_FILE}"
        rm -rf "${old_backup}"
    done
    
    # Aylik: Son 12 ayi tut
    find "${server_backup_dir}/monthly" -maxdepth 1 -type d 
        | sort | head -n -12 | while read -r old_backup; do
        echo "Siliniyor (aylik): ${old_backup}" | tee -a "${LOG_FILE}"
        rm -rf "${old_backup}"
    done
}

# Haftalik ve aylik arsivleme
archive_backup() {
    local server="$1"
    local today_day_of_week
    today_day_of_week=$(date +%u)
    local today_day_of_month
    today_day_of_month=$(date +%d)
    local latest_daily
    latest_daily=$(find "${BACKUP_ROOT}/${server}/daily" -maxdepth 1 -type d 
        -name "????-??-??" | sort | tail -1)
    
    # Her Pazar: Gunlugu haftaliga kopyala
    if [ "${today_day_of_week}" -eq 7 ] && [ -d "${latest_daily}" ]; then
        local weekly_dest="${BACKUP_ROOT}/${server}/weekly/week_$(date +%Y_W%V)"
        cp -al "${latest_daily}" "${weekly_dest}"
        echo "Haftalik arsiv olusturuldu: ${weekly_dest}" | tee -a "${LOG_FILE}"
    fi
    
    # Her ayin 1'i: Gunlugu ayliya kopyala
    if [ "${today_day_of_month}" -eq "01" ] && [ -d "${latest_daily}" ]; then
        local monthly_dest="${BACKUP_ROOT}/${server}/monthly/$(date +%Y-%m)"
        cp -al "${latest_daily}" "${monthly_dest}"
        echo "Aylik arsiv olusturuldu: ${monthly_dest}" | tee -a "${LOG_FILE}"
    fi
}

for server_dir in "${BACKUP_ROOT}"/*/; do
    server=$(basename "${server_dir}")
    cleanup_old_backups "${server}"
    archive_backup "${server}"
done

echo "[$(date)] Temizlik tamamlandi" | tee -a "${LOG_FILE}"

cp -al komutu burada kritik: hard link kullanarak kopyalama yapar, yani aynı dosya için disk alanı harcanmaz. Sadece değişen dosyalar gerçek alan tüketir.

Cron Yapılandırması

# crontab -e (backupuser olarak)
# Her gece 02:00'de ana yedekleme
0 2 * * * /opt/scripts/multi_server_backup.sh >> /var/log/backups/cron.log 2>&1

# Her sabah 06:00'da temizlik
0 6 * * * /opt/scripts/cleanup_backups.sh >> /var/log/backups/cron.log 2>&1

# Her 6 saatte bir yedekleme durumu raporu
0 */6 * * * /opt/scripts/backup_status_report.sh >> /var/log/backups/status.log 2>&1

Yedek Doğrulama Scripti

Yedek aldınız, güzel. Peki gerçekten çalışıyor mu? Doğrulama adımı kritik:

#!/bin/bash
# /opt/scripts/verify_backup.sh

BACKUP_ROOT="/opt/backups"
REPORT_FILE="/tmp/backup_verify_$(date +%Y%m%d).txt"

verify_backup_integrity() {
    local server="$1"
    local latest_backup
    latest_backup=$(find "${BACKUP_ROOT}/${server}/daily" -maxdepth 1 
        -type d -name "????-??-??" | sort | tail -1)
    
    if [ -z "${latest_backup}" ]; then
        echo "HATA: ${server} icin hic yedek bulunamadi!" >> "${REPORT_FILE}"
        return 1
    fi
    
    local backup_age_hours
    backup_age_hours=$(( ( $(date +%s) - $(stat -c %Y "${latest_backup}") ) / 3600 ))
    
    # Son yedek 25 saatten eskiyse uyar
    if [ "${backup_age_hours}" -gt 25 ]; then
        echo "UYARI: ${server} son yedegi ${backup_age_hours} saat once alindi!" >> "${REPORT_FILE}"
    fi
    
    # Dosya sayisi ve boyut kontrolu
    local file_count
    file_count=$(find "${latest_backup}" -type f | wc -l)
    local backup_size
    backup_size=$(du -sh "${latest_backup}" | cut -f1)
    
    # Minimum dosya sayisi beklentisi
    if [ "${file_count}" -lt 10 ]; then
        echo "UYARI: ${server} yedeginde cok az dosya var: ${file_count}" >> "${REPORT_FILE}"
        return 1
    fi
    
    echo "TAMAM: ${server} | Yedek: $(basename ${latest_backup}) | Dosya: ${file_count} | Boyut: ${backup_size}" >> "${REPORT_FILE}"
    
    # Rastgele 5 dosyanin checksum dogrulamasi
    find "${latest_backup}" -type f | shuf | head -5 | while read -r file; do
        if ! md5sum -c <(md5sum "${file}") &>/dev/null; then
            echo "HATA: Bozuk dosya: ${file}" >> "${REPORT_FILE}"
        fi
    done
}

echo "=== Yedek Dogrulama Raporu: $(date) ===" > "${REPORT_FILE}"

for server_dir in "${BACKUP_ROOT}"/*/; do
    server=$(basename "${server_dir}")
    verify_backup_integrity "${server}"
done

echo "" >> "${REPORT_FILE}"
echo "Rapor olusturuldu: $(date)" >> "${REPORT_FILE}"

cat "${REPORT_FILE}"

# Hata varsa email gonder
if grep -q "HATA|UYARI" "${REPORT_FILE}"; then
    mail -s "[BACKUP UYARI] Dogrulama sorunlari tespit edildi" [email protected] < "${REPORT_FILE}"
fi

Yaygın Sorunlar ve Çözümleri

Sahada sık karşılaşılan sorunları paylaşayım:

Rsync “permission denied” hatası: Uzak sunucuda backupuser‘ın okuma iznine sahip olmadığı dizinler için ya sudo yetkisi verin ya da ACL kullanın. /etc/sudoers.d/backupuser dosyasına backupuser ALL=(ALL) NOPASSWD: /usr/bin/rsync ekleyip rsync komutuna --rsync-path="sudo rsync" parametresi ekleyin.

Büyük dosyalar transfer sırasında kesiliyor: --partial --append-verify parametreleri ekleyin. Böylece kesilen transferler kaldığı yerden devam eder.

SSH bağlantı zaman aşımı: ~/.ssh/config dosyasına şunu ekleyin:

Host 192.168.1.*
    ServerAliveInterval 60
    ServerAliveCountMax 3
    ConnectTimeout 30
    IdentityFile /home/backupuser/.ssh/backup_key

Sembolik linkler doğru kopyalanmıyor: Rsync parametrelerine -L (linkleri gerçek dosyaya çevir) veya -l (link olarak koru) ekleyin. Genellikle -l daha güvenlidir.

Disk doluyor ama du büyük göstermiyor: Hard link kullanan yedeklemelerde du -sh yanıltıcı olabilir. Gerçek disk kullanımı için df -h veya du --apparent-size kullanın.

Script Güvenliği ve İzinler

# Scripti sadece root ve backupuser calistirabilsin
chown root:backupuser /opt/scripts/multi_server_backup.sh
chmod 750 /opt/scripts/multi_server_backup.sh

# Yedekleme dizinine sadece backupuser yazabilsin
chown -R backupuser:backupuser /opt/backups
chmod -R 750 /opt/backups

# Log dizini
chown -R backupuser:syslog /var/log/backups
chmod -R 770 /var/log/backups

# SSH anahtarini koru
chmod 600 /home/backupuser/.ssh/backup_key
chmod 644 /home/backupuser/.ssh/backup_key.pub

İzleme ve Monitoring Entegrasyonu

Eğer Prometheus/Grafana kullanıyorsanız, basit bir metric dosyası oluşturun:

# Backup metriklerini node_exporter textfile collector icin olustur
METRIC_FILE="/var/lib/node_exporter/textfile_collector/backup_metrics.prom"

for server in web-01 web-02 db-01 db-02 app-01; do
    latest=$(find "/opt/backups/${server}/daily" -maxdepth 1 -type d 
        -name "????-??-??" | sort | tail -1)
    
    if [ -n "${latest}" ]; then
        age=$(( $(date +%s) - $(stat -c %Y "${latest}") ))
        size=$(du -sb "${latest}" | cut -f1)
        
        echo "backup_age_seconds{server="${server}"} ${age}" >> "${METRIC_FILE}"
        echo "backup_size_bytes{server="${server}"} ${size}" >> "${METRIC_FILE}"
    fi
done

Sonuç

Bu yapıyı düzgün kurduğunuzda günlük yedekleme operasyonu tamamen otomatikleşiyor. Kritik noktalara bir daha değinelim:

  • SSH anahtar kısıtlamaları ile güvenlik açığı minimuma iner
  • Paralel çalışma yedekleme süresini önemli ölçüde kısaltır
  • Hard link tabanlı arşivleme disk alanını verimli kullanır
  • Doğrulama scripti size “yedek aldım” hissi yerine gerçek güvence verir
  • Retention policy diski kontrol altında tutar

En büyük hata, yedekleme kurduğunu sanan ama hiç test etmeyen sysadmin olmaktır. Scripti kurduğunuzda ilk işiniz bir test sunucusunda gerçek restore denemesi yapın. Yedek almak işin yüzde ellisi, geri yüklemenin çalıştığını doğrulamak diğer yüzde ellisi. Bu ikisini birlikte yaptığınızda, gece 3’te telefon çaldığında paniğe kapılmak yerine sakin bir şekilde sistemi ayağa kaldırabilirsiniz.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir