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.
