Otomatik Yedekleme: Cron ile MySQL Backup Scripti

Veritabanı yedeklemesi, sysadmin’in uyku düzenini doğrudan etkileyen konuların başında gelir. Gece 3’te çalan telefon, “veritabanı uçtu” haberiyle seni uyandırıyorsa ve elinde güncel bir yedek yoksa, o gece çok uzun geçer. Bu yüzden MySQL/MariaDB yedekleme sistemini bir kez doğru kurman, sonraki onlarca geceyi rahat uyuman anlamına gelir. Bu yazıda, cron ile çalışan, production’da gerçekten kullandığım yedekleme scriptini adım adım inceleyeceğiz.

Neden Özel Script, Neden Hazır Araç Değil?

Piyasada Percona XtraBackup, MySQLDump gibi araçlar var, hatta Bacula, Amanda gibi enterprise çözümler de mevcut. Ama basit bir VPS’te ya da orta ölçekli bir sunucuda, kendi yazdığın bir bash scripti sana çok daha fazla kontrol sağlar. Ne zaman çalıştığını bilirsin, log formatını sen belirlersin, hata durumunda ne olacağını sen yazarsın. Kara kutu çözümlere güvenmek yerine şeffaf, okunabilir bir sistem kurmak her zaman kazandırır.

Ayrıca şunu da söyleyeyim: hazır araçlar bozulduğunda debug etmek için o aracın tüm dokümantasyonunu okuman gerekir. Kendi scriptinde bir şey yanlış giderse, 50 satır bash kodu okuyarak sorunu 5 dakikada bulursun.

Temel Gereksinimler

Scripti yazmadan önce ortamı kontrol edelim.

Gerekli paketler:

  • mysql-client veya mariadb-client: mysqldump komutunu sağlar
  • gzip veya pigz: Yedekleri sıkıştırmak için (pigz çok çekirdekli sistemlerde çok daha hızlı)
  • mailutils veya msmtp: Mail bildirimleri için
  • openssl: Şifreli yedekleme istiyorsan

Ubuntu/Debian üzerinde kurulum:

apt update && apt install -y mysql-client gzip mailutils

CentOS/RHEL için:

yum install -y mysql mariadb gzip mailx

Bir de MySQL kullanıcısı oluşturalım. Root şifresini script içine gömmek hem güvensiz hem de kötü bir alışkanlık. Sadece dump alabilecek yetkiyle sınırlı bir kullanıcı oluşturalım:

CREATE USER 'backup_user'@'localhost' IDENTIFIED BY 'guclu_bir_sifre_buraya';
GRANT SELECT, SHOW DATABASES, LOCK TABLES, RELOAD, SHOW VIEW, EVENT, TRIGGER ON *.* TO 'backup_user'@'localhost';
FLUSH PRIVILEGES;

Bu kullanıcı sadece okuma ve dump işlemleri yapabilir, veri yazmaz, tablo silemez. Principle of least privilege, yani en az yetki prensibi. Production’da her zaman bunu uygula.

MySQL Option File ile Güvenli Kimlik Bilgisi Saklama

Şifreleri script içine yazma. Bunun yerine MySQL’in .my.cnf formatını kullan. Bu dosyayı backup_user‘ın home dizinine ya da özel bir konuma koyarız:

cat > /etc/mysql/backup.cnf << 'EOF'
[client]
user=backup_user
password=guclu_bir_sifre_buraya
host=localhost
EOF

chmod 600 /etc/mysql/backup.cnf

Bu dosyanın izinleri mutlaka 600 olmalı, yoksa MySQL okumayı reddeder. chown ile de doğru sahipliği ver:

chown root:root /etc/mysql/backup.cnf

Ana Yedekleme Scripti

Şimdi asıl işe gelelim. Aşağıdaki script, production’da kullandığım versiyonun basitleştirilmiş ama tam işlevsel hali:

#!/bin/bash
# =============================================================
# MySQL Otomatik Yedekleme Scripti
# Versiyon: 2.1
# Yazan: [email protected]
# =============================================================

# --- YAPILANDIRMA ---
BACKUP_DIR="/var/backups/mysql"
MYSQL_OPTS="--defaults-extra-file=/etc/mysql/backup.cnf"
DATE=$(date +%Y-%m-%d_%H-%M-%S)
RETENTION_DAYS=7
LOG_FILE="/var/log/mysql_backup.log"
MAIL_TO="[email protected]"
HOSTNAME=$(hostname -s)

# Sıkıştırma: gzip veya pigz (çok çekirdekli sistemler için pigz tercih edilir)
if command -v pigz &> /dev/null; then
    COMPRESS="pigz"
else
    COMPRESS="gzip"
fi

# --- FONKSİYONLAR ---
log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}

send_mail() {
    local subject="$1"
    local body="$2"
    echo "$body" | mail -s "[$HOSTNAME] MySQL Backup: $subject" "$MAIL_TO"
}

# --- HAZIRLIK ---
mkdir -p "$BACKUP_DIR"
log "=========================================="
log "Yedekleme başladı"

BACKUP_SUCCESS=0
BACKUP_FAILED=0
FAILED_DBS=""

# --- VERİTABANLARINI LİSTELE ---
DATABASES=$(mysql $MYSQL_OPTS -e "SHOW DATABASES;" | grep -Ev "^(Database|information_schema|performance_schema|sys)$")

if [ -z "$DATABASES" ]; then
    log "HATA: Veritabanı listesi alınamadı veya yedeklenecek DB bulunamadı"
    send_mail "KRITIK HATA" "Veritabanı listesi alınamadı. Sunucu: $HOSTNAME"
    exit 1
fi

# --- HER VERİTABANINI YEDEKLE ---
for DB in $DATABASES; do
    log "Yedekleniyor: $DB"
    BACKUP_FILE="${BACKUP_DIR}/${DB}_${DATE}.sql.gz"

    if mysqldump $MYSQL_OPTS 
        --single-transaction 
        --routines 
        --triggers 
        --events 
        --hex-blob 
        --databases "$DB" | $COMPRESS > "$BACKUP_FILE"; then

        SIZE=$(du -sh "$BACKUP_FILE" | cut -f1)
        log "OK: $DB -> $BACKUP_FILE ($SIZE)"
        ((BACKUP_SUCCESS++))
    else
        log "HATA: $DB yedeklenemedi!"
        FAILED_DBS="$FAILED_DBS $DB"
        ((BACKUP_FAILED++))
        rm -f "$BACKUP_FILE"
    fi
done

# --- ESKİ YEDEKLERİ TEMİZLE ---
log "Eski yedekler temizleniyor (${RETENTION_DAYS} günden eski)..."
DELETED=$(find "$BACKUP_DIR" -name "*.sql.gz" -mtime +$RETENTION_DAYS -delete -print | wc -l)
log "$DELETED adet eski yedek silindi"

# --- SONUÇ RAPORU ---
log "Tamamlandı: $BACKUP_SUCCESS başarılı, $BACKUP_FAILED başarısız"
log "=========================================="

if [ $BACKUP_FAILED -gt 0 ]; then
    send_mail "HATA - Bazı yedekler alınamadı" 
        "Başarılı: $BACKUP_SUCCESSnBaşarısız: $BACKUP_FAILEDnHatalı DB'ler: $FAILED_DBSnLog: $LOG_FILE"
    exit 1
else
    send_mail "BAŞARILI" 
        "Tüm veritabanları yedeklendi.nBaşarılı: $BACKUP_SUCCESSnTarih: $DATE"
fi

Scripti kaydedelim ve çalıştırılabilir yapalım:

chmod +x /usr/local/bin/mysql_backup.sh

mysqldump Parametrelerini Anlamak

Scripteki mysqldump parametrelerine bakalım, her biri önemli:

–single-transaction: InnoDB tablolar için kritik. Dump sırasında tabloları kilitlemez, transaction başlatır. Production’da veri tutarlılığı için şart.

–routines: Stored procedure ve function’ları da yedekler. Bunları atlarsan schema restore’da sorun yaşarsın.

–triggers: Trigger’ları dahil eder. –routines gibi varsayılan olarak kapalıdır.

–events: Scheduled event’leri yedekler.

–hex-blob: BLOB alanlarını hex formatında yazar. Özellikle binary data içeren tablolarda karakter encoding sorunlarını önler.

–databases: Veritabanı adını CREATE DATABASE ve USE statement’larıyla birlikte yazar. Restore sırasında veritabanının var olup olmadığını kontrol etmene gerek kalmaz.

MyISAM tablolar da varsa dikkatli ol. --single-transaction sadece InnoDB’de işe yarar. Karma ortamlarda --lock-tables eklemeyi düşün, ama bu production trafiğini etkiler.

Cron ile Zamanlama

Script hazır, şimdi zamanlamayı yapalım. Crontab’ı düzenle:

crontab -e

Gece 2’de her gün çalışacak şekilde ayarla:

# MySQL Yedekleme - Her gece 02:00'de
0 2 * * * /usr/local/bin/mysql_backup.sh >> /var/log/mysql_backup_cron.log 2>&1

Eğer büyük bir veritabanın varsa ve yedekleme süresinin öngörülebilir olmasını istiyorsan, nice ve ionice ile işlem önceliğini düşür. Bu sayede yedekleme sistemi yavaşlatmaz:

0 2 * * * nice -n 10 ionice -c 2 -n 7 /usr/local/bin/mysql_backup.sh >> /var/log/mysql_backup_cron.log 2>&1

nice -n 10: CPU önceliğini düşürür (0 normal, 19 en düşük, biz 10 seçtik)

ionice -c 2 -n 7: Disk I/O önceliğini en düşük best-effort sınıfına alır

Ayrıca /etc/cron.d/ altına da koyabilirsin, bu genellikle daha düzenlidir:

cat > /etc/cron.d/mysql-backup << 'EOF'
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
MAILTO=""

# MySQL Yedekleme
0 2 * * * root /usr/local/bin/mysql_backup.sh >> /var/log/mysql_backup_cron.log 2>&1
EOF

Gelişmiş Özellik: Şifreli Yedekleme

Yedekleri uzak bir konuma göndereceksen ya da yedekler hassas veri içeriyorsa, şifreleme şart. OpenSSL ile basit ama etkili bir yöntem:

#!/bin/bash
# Şifreli yedekleme örneği - ana scripte entegre et

ENCRYPTION_KEY_FILE="/etc/mysql/backup.key"
BACKUP_FILE_ENCRYPTED="${BACKUP_FILE}.enc"

# Anahtar dosyası oluşturma (sadece bir kez çalıştır)
# openssl rand -base64 32 > /etc/mysql/backup.key
# chmod 400 /etc/mysql/backup.key

# Şifreli yedekleme
mysqldump $MYSQL_OPTS --single-transaction --routines --triggers "$DB" | 
    $COMPRESS | 
    openssl enc -aes-256-cbc -salt -pbkdf2 
    -pass file:"$ENCRYPTION_KEY_FILE" 
    -out "${BACKUP_DIR}/${DB}_${DATE}.sql.gz.enc"

# Restore için:
# openssl enc -aes-256-cbc -d -pbkdf2 
#   -pass file:/etc/mysql/backup.key 
#   -in backup.sql.gz.enc | gunzip | mysql $MYSQL_OPTS database_name

Anahtar dosyasını yedeklerden ayrı bir yerde sakla. Yedek ve anahtarı aynı yerde tutarsan şifrelemenin anlamı kalmaz.

Yedek Doğrulama Scripti

Yedek aldım diyip geçmek yetmez. “Yedek var” ile “yedek çalışıyor” arasında dağlar kadar fark vardır. Şu senaryoyu duymuşsunuzdur: yıllarca yedek alınmış, restore günü gelmiş, yedekler bozuk çıkmış. Bu yüzden düzenli olarak yedeklerin açılıp açılamadığını test etmek gerekir.

#!/bin/bash
# /usr/local/bin/mysql_backup_verify.sh
# Yedeklerin bütünlüğünü kontrol eder

BACKUP_DIR="/var/backups/mysql"
LOG_FILE="/var/log/mysql_backup_verify.log"
MAIL_TO="[email protected]"

log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}

log "Yedek doğrulama başladı"
ERROR_COUNT=0

# Son 24 saatte alınan yedekleri kontrol et
find "$BACKUP_DIR" -name "*.sql.gz" -mtime -1 | while read BACKUP_FILE; do
    # gzip bütünlük kontrolü
    if gzip -t "$BACKUP_FILE" 2>/dev/null; then
        SIZE=$(du -sh "$BACKUP_FILE" | cut -f1)
        log "OK: $(basename $BACKUP_FILE) [$SIZE]"
    else
        log "BOZUK YEDEK: $BACKUP_FILE"
        echo "Bozuk yedek tespit edildi: $BACKUP_FILE" | 
            mail -s "[KRITIK] MySQL Backup Bozuk - $(hostname)" "$MAIL_TO"
        ((ERROR_COUNT++))
    fi

    # Dosya boyutu sıfır mı kontrol et
    if [ ! -s "$BACKUP_FILE" ]; then
        log "BOŞ DOSYA: $BACKUP_FILE"
        ((ERROR_COUNT++))
    fi
done

log "Doğrulama tamamlandı. Hata sayısı: $ERROR_COUNT"

Bu scripti de cron’a ekle, her sabah çalıştır:

# Yedek doğrulama - Her sabah 08:00'de
0 8 * * * root /usr/local/bin/mysql_backup_verify.sh >> /var/log/mysql_backup_verify_cron.log 2>&1

Uzak Sunucuya Yedek Aktarımı

Yedekler sadece aynı sunucuda duruyorsa felaketten korunma sağlamaz. Sunucu yanarsa, yedekle birlikte yanar. Uzak bir konuma aktarmalısın:

#!/bin/bash
# Yedekleri uzak sunucuya rsync ile gönder
# Bu kodu ana scriptin sonuna ekle

REMOTE_SERVER="[email protected]"
REMOTE_DIR="/backup/mysql/$(hostname -s)"
SSH_KEY="/root/.ssh/backup_key"

log "Yedekler uzak sunucuya aktarılıyor..."

rsync -avz --delete 
    -e "ssh -i $SSH_KEY -o StrictHostKeyChecking=yes -o BatchMode=yes" 
    "$BACKUP_DIR/" 
    "${REMOTE_SERVER}:${REMOTE_DIR}/"

if [ $? -eq 0 ]; then
    log "Uzak aktarım başarılı: ${REMOTE_SERVER}:${REMOTE_DIR}"
else
    log "HATA: Uzak aktarım başarısız!"
    send_mail "UYARI - Uzak Aktarım Hatası" 
        "Yedekler $REMOTE_SERVER'a aktarılamadı. Manuel kontrol gerekiyor."
fi

SSH key’i önceden oluştur ve uzak sunucuya kopyala:

ssh-keygen -t ed25519 -f /root/.ssh/backup_key -N ""
ssh-copy-id -i /root/.ssh/backup_key.pub [email protected]

Log Rotasyonu

Uzun süre çalışan bir sistemde log dosyaları büyür. Logrotate ile bunu yönet:

cat > /etc/logrotate.d/mysql-backup << 'EOF'
/var/log/mysql_backup.log
/var/log/mysql_backup_verify.log
/var/log/mysql_backup_cron.log {
    daily
    rotate 30
    compress
    delaycompress
    missingok
    notifempty
    create 640 root root
}
EOF

Restore Prosedürü

Yedek almak kadar önemli olan konu restore’u bilmek. Panikleyerek restore yapmak hata üretir. Standart prosedürünü önceden yaz ve bir yere kaydet.

Tek bir veritabanını restore etmek için:

# Sıkıştırılmış yedeği aç ve yükle
gunzip -c /var/backups/mysql/mydb_2024-01-15_02-00-01.sql.gz | 
    mysql --defaults-extra-file=/etc/mysql/backup.cnf

# Eğer veritabanı yoksa önce oluştur
mysql --defaults-extra-file=/etc/mysql/backup.cnf -e "CREATE DATABASE IF NOT EXISTS mydb;"
gunzip -c /var/backups/mysql/mydb_2024-01-15_02-00-01.sql.gz | 
    mysql --defaults-extra-file=/etc/mysql/backup.cnf mydb

Restore süresini önceden test et. 50GB’lık bir veritabanı restore’unun kaç saat sürdüğünü kriz anında değil, sessiz bir günde öğren.

Gerçek Dünya Senaryosu: E-ticaret Sitesi

Diyelim ki bir e-ticaret platformunun veritabanını yönetiyorsun. Üç kritik veritabanın var:

  • shop_production: Ana veritabanı, 15GB
  • shop_analytics: Raporlama DB’si, 80GB
  • shop_sessions: Oturum verileri, 2GB

shop_sessions için gerçek bir yedeklemeye gerek yok, kullanıcı session’ları anlık veri. shop_analytics büyük ve kritik değil, haftada bir tam yedek yeterli. shop_production ise her gece tam yedek, her 6 saatte bir artımlı yedek alınmalı.

Bu senaryoda tek bir script yerine özelleştirilmiş iki script çalıştırırsın. Ana scripti EXCLUDE_DBS değişkeniyle genişletebilirsin:

# Belirli DB'leri hariç tut
EXCLUDE_DBS="shop_sessions information_schema performance_schema sys"

DATABASES=$(mysql $MYSQL_OPTS -e "SHOW DATABASES;" | grep -v "^Database$" | 
    grep -Ev "^($(echo $EXCLUDE_DBS | tr ' ' '|'))$")

Bu küçük değişiklik, scriptini çok daha esnek hale getirir.

İzleme ve Alarm

Scriptin çalışıp çalışmadığını dışarıdan da izlemelisin. Cron çalıştı mı, script tamamlandı mı, son başarılı yedek ne zaman? Bunları Nagios, Zabbix ya da basit bir heartbeat servisiyle izleyebilirsin.

En basit yöntem: başarılı yedeklemeden sonra bir dosyanın timestamp’ini güncelle, monitoring da bu dosyanın yaşını kontrol etsin.

# Ana scriptin sonuna ekle, başarılı tamamlanınca:
touch /var/run/mysql_backup_last_success

Zabbix ya da Nagios’ta bu dosyanın son değiştirilme zamanını izleyecek bir kontrol yazarsın. 26 saatten eski olduysa alarm ver.

Sonuç

Doğru yapılandırılmış bir yedekleme sistemi kurmak bir kez yapılır ama sürekli meyvesini verir. Bu yazıda anlattığımız sistemi özetleyelim:

  • Minimum yetkili ayrı bir MySQL kullanıcısı oluşturuldu
  • Şifreler option file ile güvenli tutuldu
  • Her veritabanı ayrı ayrı yedeklendi, hata durumunda rapor gönderildi
  • Eski yedekler otomatik temizlendi
  • Yedeklerin bütünlüğü ayrı bir script ile doğrulandı
  • Uzak sunucuya rsync ile aktarım eklendi
  • Log rotasyonu ayarlandı

Bunların hepsini kurup “tamam” demeden önce bir test restore yap. Boş bir test ortamında bir veritabanını restore et, verilerin tutarlı olduğunu kontrol et. Sadece o zaman gerçekten güvende olduğunu söyleyebilirsin.

En iyi yedekleme sistemi, restore’u test edilmiş olandır. Bunu hiç unutma.

Yorum yapın