Yedekleme işi “bir gün yapacağım” listesinde en uzun kalan görevlerden biridir. Ta ki bir şeyler ters gidene kadar. Disk çöker, yanlışlıkla rm -rf çalışır ya da bir uygulama güncellemesi her şeyi berbat eder ve o an geldiğinde fark edersiniz ki son yedek iki ay önceymiş. Bu yazıda Python ile sağlam, esnek ve gerçekten işe yarayan bir otomatik yedekleme scripti yazacağız. Hem Linux hem Windows ortamlarını göz önünde bulundurarak, production’da kullanabileceğiniz seviyede bir çözüm geliştireceğiz.
Neden Python ile Yedekleme?
Bash scripti de yazabilirsiniz, cron job da kurabilirsiniz ama Python size çok daha fazlasını verir. Hata yönetimi, loglama, e-posta bildirimi, cross-platform çalışma ve karmaşık mantık kurma konularında Python açık ara öndedir. Üstüne üstlük standart kütüphaneler ile çoğu işi dış bağımlılık olmadan halledebilirsiniz.
Yazacağımız script şunları yapacak:
- Belirtilen dizinleri tarball veya zip olarak sıkıştırıp yedekleyecek
- Tarih damgalı dosya adları oluşturacak
- Eski yedekleri otomatik temizleyecek (retention policy)
- Tüm işlemleri log dosyasına yazacak
- Hata durumunda e-posta gönderecek
- Konfigürasyonu ayrı bir dosyadan okuyacak
Ortam Hazırlığı
Önce Python versiyonumuzu kontrol edelim ve gerekli yapıyı oluşturalım:
python3 --version
# Python 3.8+ olması yeterli, ekstra paket gerekmez
mkdir -p ~/backup_scripts
cd ~/backup_scripts
touch backup.py
touch backup_config.json
touch backup.log
Eğer e-posta bildirimi için daha gelişmiş bir kütüphane kullanmak isterseniz sadece şu paketi kurmanız yeterli:
pip3 install schedule
# Periyodik çalıştırma için, cron yerine Python içinde zamanlama yapmak isteyenler için
Standart kütüphaneler olan os, shutil, tarfile, zipfile, logging, smtplib ve json modülleri Python ile zaten geliyor.
Konfigürasyon Dosyası
Her şeyi script içine gömmek yerine ayrı bir JSON konfigürasyon dosyası kullanmak çok daha temiz bir yaklaşım. Böylece script’e dokunmadan ayarları değiştirebilirsiniz:
cat > backup_config.json << 'EOF'
{
"backup_sources": [
"/var/www/html",
"/etc/nginx",
"/home/deploy/app",
"/var/lib/postgresql/data"
],
"backup_destination": "/mnt/backup",
"backup_format": "tar.gz",
"retention_days": 30,
"max_backup_count": 10,
"log_file": "/var/log/backup.log",
"compression_level": 9,
"email_notifications": {
"enabled": true,
"smtp_server": "smtp.gmail.com",
"smtp_port": 587,
"sender": "[email protected]",
"recipients": ["[email protected]", "[email protected]"],
"on_success": false,
"on_failure": true
},
"exclude_patterns": [
"*.tmp",
"*.log",
"__pycache__",
"node_modules",
".git"
]
}
EOF
on_success: false ayarı dikkat çekici. Her başarılı yedekte mail almak istemezsiniz, posta kutunuz çöp olur. Ama hata olduğunda anında haberdar olmak şart.
Temel Yedekleme Fonksiyonları
Şimdi asıl scripti yazmaya başlayalım. Önce temel yapıyı ve yardımcı fonksiyonları oluşturalım:
cat > backup.py << 'PYEOF'
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os
import sys
import json
import shutil
import tarfile
import zipfile
import logging
import smtplib
import hashlib
import fnmatch
from datetime import datetime, timedelta
from pathlib import Path
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
def load_config(config_path: str) -> dict:
"""Konfigürasyon dosyasını yükler ve doğrular."""
try:
with open(config_path, 'r', encoding='utf-8') as f:
config = json.load(f)
# Zorunlu alanları kontrol et
required_keys = ['backup_sources', 'backup_destination']
for key in required_keys:
if key not in config:
raise ValueError(f"Konfigürasyonda eksik alan: {key}")
return config
except FileNotFoundError:
print(f"HATA: Konfigürasyon dosyası bulunamadı: {config_path}")
sys.exit(1)
except json.JSONDecodeError as e:
print(f"HATA: Konfigürasyon dosyası geçersiz JSON: {e}")
sys.exit(1)
def setup_logging(log_file: str) -> logging.Logger:
"""Logging sistemini yapılandırır."""
logger = logging.getLogger('BackupScript')
logger.setLevel(logging.DEBUG)
formatter = logging.Formatter(
'%(asctime)s | %(levelname)-8s | %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
# Dosyaya yaz
file_handler = logging.FileHandler(log_file, encoding='utf-8')
file_handler.setLevel(logging.DEBUG)
file_handler.setFormatter(formatter)
# Konsola yaz
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)
console_handler.setFormatter(formatter)
logger.addHandler(file_handler)
logger.addHandler(console_handler)
return logger
PYEOF
Sıkıştırma ve Yedek Oluşturma
Birden fazla format desteği ekleyerek hem Linux hem Windows uyumlu hale getirelim:
cat >> backup.py << 'PYEOF'
def should_exclude(path: str, exclude_patterns: list) -> bool:
"""Dosyanın exclude listesinde olup olmadığını kontrol eder."""
filename = os.path.basename(path)
for pattern in exclude_patterns:
if fnmatch.fnmatch(filename, pattern):
return True
# Tam yol eşleşmesi de kontrol et
if pattern in path:
return True
return False
def create_tar_backup(source_path: str, dest_file: str,
exclude_patterns: list, compression: int,
logger: logging.Logger) -> bool:
"""TAR.GZ formatında yedek oluşturur."""
try:
mode = 'w:gz'
def exclude_filter(tarinfo):
if should_exclude(tarinfo.name, exclude_patterns):
logger.debug(f"Atlandı: {tarinfo.name}")
return None
return tarinfo
with tarfile.open(dest_file, mode,
compresslevel=compression) as tar:
tar.add(source_path,
arcname=os.path.basename(source_path),
filter=exclude_filter)
return True
except tarfile.TarError as e:
logger.error(f"TAR oluşturma hatası: {e}")
return False
except PermissionError as e:
logger.error(f"Yetki hatası: {e}")
return False
def create_zip_backup(source_path: str, dest_file: str,
exclude_patterns: list,
logger: logging.Logger) -> bool:
"""ZIP formatında yedek oluşturur (Windows uyumlu)."""
try:
with zipfile.ZipFile(dest_file, 'w',
zipfile.ZIP_DEFLATED) as zipf:
if os.path.isfile(source_path):
zipf.write(source_path, os.path.basename(source_path))
else:
for root, dirs, files in os.walk(source_path):
# Exclude listesindeki dizinleri atla
dirs[:] = [d for d in dirs
if not should_exclude(d, exclude_patterns)]
for file in files:
if should_exclude(file, exclude_patterns):
logger.debug(f"Atlandı: {file}")
continue
file_path = os.path.join(root, file)
arcname = os.path.relpath(file_path,
os.path.dirname(source_path))
zipf.write(file_path, arcname)
return True
except Exception as e:
logger.error(f"ZIP oluşturma hatası: {e}")
return False
def calculate_checksum(file_path: str) -> str:
"""SHA256 checksum hesaplar."""
sha256 = hashlib.sha256()
with open(file_path, 'rb') as f:
for chunk in iter(lambda: f.read(8192), b''):
sha256.update(chunk)
return sha256.hexdigest()
PYEOF
Checksum hesaplama kritik bir detay. Yedek dosyası oluştu ama bozuk mu? Checksum olmadan bilemezsiniz. Yedek aldınız ama restore edemiyorsanız hiç almamıştan farksız.
Ana Yedekleme Mantığı
cat >> backup.py << 'PYEOF'
def run_backup(config: dict, logger: logging.Logger) -> dict:
"""Ana yedekleme işlemini çalıştırır."""
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
backup_dest = config['backup_destination']
backup_format = config.get('backup_format', 'tar.gz')
exclude_patterns = config.get('exclude_patterns', [])
compression = config.get('compression_level', 6)
# Hedef dizini oluştur
os.makedirs(backup_dest, exist_ok=True)
results = {
'timestamp': timestamp,
'successful': [],
'failed': [],
'total_size': 0
}
for source in config['backup_sources']:
if not os.path.exists(source):
logger.warning(f"Kaynak bulunamadı, atlanıyor: {source}")
results['failed'].append({
'source': source,
'error': 'Kaynak dizin/dosya bulunamadı'
})
continue
# Dosya adını oluştur
source_name = source.strip('/').replace('/', '_')
if backup_format == 'zip':
filename = f"{source_name}_{timestamp}.zip"
else:
filename = f"{source_name}_{timestamp}.tar.gz"
dest_file = os.path.join(backup_dest, filename)
logger.info(f"Yedekleniyor: {source} --> {dest_file}")
start_time = datetime.now()
# Yedek oluştur
success = False
if backup_format == 'zip':
success = create_zip_backup(source, dest_file,
exclude_patterns, logger)
else:
success = create_tar_backup(source, dest_file,
exclude_patterns, compression, logger)
if success:
elapsed = (datetime.now() - start_time).seconds
file_size = os.path.getsize(dest_file)
checksum = calculate_checksum(dest_file)
# Checksum dosyası oluştur
checksum_file = dest_file + '.sha256'
with open(checksum_file, 'w') as f:
f.write(f"{checksum} {filename}n")
results['successful'].append({
'source': source,
'dest': dest_file,
'size_mb': round(file_size / (1024 * 1024), 2),
'duration_sec': elapsed,
'checksum': checksum
})
results['total_size'] += file_size
logger.info(f"Tamamlandı: {filename} | "
f"Boyut: {file_size/(1024*1024):.2f} MB | "
f"Süre: {elapsed}s")
else:
results['failed'].append({
'source': source,
'error': 'Yedek oluşturma başarısız'
})
logger.error(f"Başarısız: {source}")
return results
PYEOF
Eski Yedekleri Temizleme (Retention Policy)
Yedek alıp temizlememek en klasik hatalardan biridir. Disk dolar, sistem çöker, günah başa döner:
cat >> backup.py << 'PYEOF'
def cleanup_old_backups(config: dict, logger: logging.Logger) -> int:
"""Retention policy'e göre eski yedekleri temizler."""
backup_dest = config['backup_destination']
retention_days = config.get('retention_days', 30)
max_count = config.get('max_backup_count', 0)
if not os.path.exists(backup_dest):
return 0
deleted_count = 0
cutoff_date = datetime.now() - timedelta(days=retention_days)
# Tüm yedek dosyalarını bul
backup_files = []
for f in Path(backup_dest).glob('*.tar.gz'):
backup_files.append(f)
for f in Path(backup_dest).glob('*.zip'):
backup_files.append(f)
# Tarih bazlı temizlik
for backup_file in backup_files:
file_mtime = datetime.fromtimestamp(backup_file.stat().st_mtime)
if file_mtime < cutoff_date:
try:
backup_file.unlink()
# Checksum dosyasını da sil
checksum_file = Path(str(backup_file) + '.sha256')
if checksum_file.exists():
checksum_file.unlink()
logger.info(f"Eski yedek silindi: {backup_file.name} "
f"({file_mtime.strftime('%Y-%m-%d')})")
deleted_count += 1
except Exception as e:
logger.error(f"Silme hatası: {backup_file.name}: {e}")
# Maksimum sayı bazlı temizlik
if max_count > 0:
remaining_files = sorted(
[f for f in Path(backup_dest).glob('*.tar.gz')] +
[f for f in Path(backup_dest).glob('*.zip')],
key=lambda x: x.stat().st_mtime,
reverse=True
)
if len(remaining_files) > max_count:
for old_file in remaining_files[max_count:]:
try:
old_file.unlink()
checksum_file = Path(str(old_file) + '.sha256')
if checksum_file.exists():
checksum_file.unlink()
logger.info(f"Limit aşımı, silindi: {old_file.name}")
deleted_count += 1
except Exception as e:
logger.error(f"Silme hatası: {e}")
return deleted_count
PYEOF
E-posta Bildirimi
cat >> backup.py << 'PYEOF'
def send_notification(config: dict, results: dict,
logger: logging.Logger) -> None:
"""Yedekleme sonucunu e-posta ile bildirir."""
email_config = config.get('email_notifications', {})
if not email_config.get('enabled', False):
return
has_failure = len(results['failed']) > 0
# Ne zaman mail gönderileceğini kontrol et
if has_failure and not email_config.get('on_failure', True):
return
if not has_failure and not email_config.get('on_success', False):
return
status = "BASARISIZ" if has_failure else "BASARILI"
hostname = os.uname().nodename if hasattr(os, 'uname') else os.environ.get('COMPUTERNAME', 'unknown')
subject = f"[{status}] Yedekleme Raporu - {hostname} - {results['timestamp']}"
# Mail içeriği oluştur
body_lines = [
f"Sunucu: {hostname}",
f"Tarih/Saat: {results['timestamp']}",
f"Toplam boyut: {results['total_size']/(1024*1024):.2f} MB",
"",
f"Basarili: {len(results['successful'])} kaynak",
f"Basarisiz: {len(results['failed'])} kaynak",
""
]
if results['successful']:
body_lines.append("=== BASARILI YEDEKLER ===")
for item in results['successful']:
body_lines.append(
f" + {item['source']} -> {item['size_mb']} MB ({item['duration_sec']}s)"
)
if results['failed']:
body_lines.append("")
body_lines.append("=== BASARISIZ YEDEKLER ===")
for item in results['failed']:
body_lines.append(f" - {item['source']}: {item['error']}")
body = "n".join(body_lines)
try:
msg = MIMEMultipart()
msg['From'] = email_config['sender']
msg['To'] = ', '.join(email_config['recipients'])
msg['Subject'] = subject
msg.attach(MIMEText(body, 'plain', 'utf-8'))
with smtplib.SMTP(email_config['smtp_server'],
email_config['smtp_port']) as server:
server.ehlo()
server.starttls()
server.login(
email_config.get('username', email_config['sender']),
email_config.get('password', '')
)
server.sendmail(
email_config['sender'],
email_config['recipients'],
msg.as_string()
)
logger.info("Bildirim maili gönderildi")
except Exception as e:
logger.error(f"Mail gönderilemedi: {e}")
PYEOF
Main Fonksiyonu ve Çalıştırma
cat >> backup.py << 'PYEOF'
def main():
"""Ana giriş noktası."""
import argparse
parser = argparse.ArgumentParser(description='Otomatik Yedekleme Scripti')
parser.add_argument('--config', default='backup_config.json',
help='Konfigürasyon dosyası yolu')
parser.add_argument('--dry-run', action='store_true',
help='Gerçekte yedek almadan simüle et')
parser.add_argument('--cleanup-only', action='store_true',
help='Sadece eski yedekleri temizle')
args = parser.parse_args()
# Konfigürasyonu yükle
config = load_config(args.config)
# Logging'i başlat
log_file = config.get('log_file', '/var/log/backup.log')
logger = setup_logging(log_file)
logger.info("=" * 60)
logger.info("Yedekleme scripti başlatıldı")
if args.dry_run:
logger.info("DRY-RUN modu: Gerçek yedek alınmayacak")
for source in config['backup_sources']:
exists = "VAR" if os.path.exists(source) else "YOK"
logger.info(f" [{exists}] {source}")
return
if args.cleanup_only:
deleted = cleanup_old_backups(config, logger)
logger.info(f"Temizlik tamamlandı: {deleted} dosya silindi")
return
# Yedekleme çalıştır
results = run_backup(config, logger)
# Eski yedekleri temizle
deleted = cleanup_old_backups(config, logger)
logger.info(f"Temizlenen eski yedek: {deleted}")
# Özet
total_mb = results['total_size'] / (1024 * 1024)
logger.info(f"Tamamlandi | Basarili: {len(results['successful'])} | "
f"Basarisiz: {len(results['failed'])} | "
f"Toplam: {total_mb:.2f} MB")
# Bildirim gönder
send_notification(config, results, logger)
# Başarısız yedek varsa hata kodu ile çık
if results['failed']:
sys.exit(1)
if __name__ == '__main__':
main()
PYEOF
Cron Job ile Otomatik Zamanlama
Script hazır, şimdi bunu düzenli çalışacak hale getirelim:
# Scripti çalıştırılabilir yap
chmod +x /opt/backup/backup.py
# Crontab'a ekle
crontab -e
# Her gece 02:00'de çalıştır
0 2 * * * /usr/bin/python3 /opt/backup/backup.py --config /opt/backup/backup_config.json >> /var/log/backup_cron.log 2>&1
# Pazar günleri haftalık tam yedek (farklı konfigürasyon ile)
0 3 * * 0 /usr/bin/python3 /opt/backup/backup.py --config /opt/backup/backup_weekly.json >> /var/log/backup_weekly.log 2>&1
# Her ayın 1'inde eski yedekleri temizle
0 4 1 * * /usr/bin/python3 /opt/backup/backup.py --cleanup-only --config /opt/backup/backup_config.json
Test ve Doğrulama
Scripti production’a almadan önce test etmek kritik:
# Dry-run ile test et
python3 backup.py --config backup_config.json --dry-run
# Gerçek yedek al (terminal'de izleyerek)
python3 backup.py --config backup_config.json
# Oluşan yedekleri listele
ls -lh /mnt/backup/
# Checksum'ları doğrula
cd /mnt/backup
sha256sum -c *.sha256
# TAR dosyasının bütünlüğünü kontrol et
tar -tzf var_www_html_20241215_020001.tar.gz | head -20
# Test restore (geçici dizine)
mkdir /tmp/test_restore
tar -xzf var_www_html_20241215_020001.tar.gz -C /tmp/test_restore
ls -la /tmp/test_restore/
Önemli kural: Restore etmediğiniz yedek, yedek değildir. Ayda bir kez test restore yapın.
Gerçek Dünya Senaryosu: PostgreSQL Yedekleme
Veritabanını tarball ile yedeklemek doğru değil. Önce dump alıp sonra sıkıştırmak gerekiyor. Bunu konfigürasyona eklemek için script’e pre-backup hook ekleyebilirsiniz:
# PostgreSQL dump'ını önceden al
cat > /opt/backup/pre_backup.sh << 'EOF'
#!/bin/bash
DUMP_DIR="/var/backup/db_dumps"
mkdir -p $DUMP_DIR
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
# Tüm veritabanlarını dump al
pg_dumpall -U postgres > "$DUMP_DIR/all_databases_${TIMESTAMP}.sql"
gzip "$DUMP_DIR/all_databases_${TIMESTAMP}.sql"
echo "DB dump tamamlandı: $DUMP_DIR"
EOF
chmod +x /opt/backup/pre_backup.sh
# Crontab'da önce pre-backup, sonra Python scripti çalıştır
# 55 1 * * * /opt/backup/pre_backup.sh
# 0 2 * * * python3 /opt/backup/backup.py --config /opt/backup/backup_config.json
/var/backup/db_dumps dizinini backup_sources listesine eklediğinizde hem uygulama dosyaları hem de veritabanı yedeği tek scriptle yönetilmiş olur.
Uzak Sunucuya Yedek Kopyalama
Yerel disk yedeği tek başına yeterli değil. Aynı sunucu yanarsa ne olacak? Basit bir rsync entegrasyonu ekleyelim:
# Yedekleri uzak sunucuya kopyala
cat > /opt/backup/sync_to_remote.sh << 'EOF'
#!/bin/bash
LOCAL_BACKUP="/mnt/backup"
REMOTE_USER="backup_user"
REMOTE_HOST="backup-server.sirketim.com"
REMOTE_PATH="/backup/$(hostname)"
# SSH key ile kimlik doğrulama kullan (parola KULLANMA)
rsync -avz --delete
--exclude='*.tmp'
-e "ssh -i /home/deploy/.ssh/backup_key -p 2222"
"$LOCAL_BACKUP/"
"${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_PATH}/"
if [ $? -eq 0 ]; then
echo "$(date): Uzak senkronizasyon basarili" >> /var/log/backup_sync.log
else
echo "$(date): Uzak senkronizasyon BASARISIZ" >> /var/log/backup_sync.log
fi
EOF
chmod +x /opt/backup/sync_to_remote.sh
Sonuç
Bu yazıda sıfırdan başlayıp production’a hazır bir yedekleme sistemi inşa ettik. Script’in en güçlü yanları şunlar: konfigürasyon bazlı esnek yapı, checksum doğrulaması, retention policy ve e-posta bildirimi. Ama şunu unutmayın: bu script çalışıyor diye işiniz bitti değil.
Yapmanız gerekenler:
- Ayda bir restore testi yapın, gerçekten geri dönebildiğinizi doğrulayın
- Log dosyalarını düzenli gözden geçirin, sessiz hataları yakalayın
- Disk doluluk oranını izleyin, yedek birikmesi sistemi çökertebilir
- Birden fazla lokasyon kullanın, 3-2-1 kuralını (3 kopya, 2 farklı medya, 1 uzak lokasyon) takip edin
- SMTP parolalarını config dosyasına düz metin yazmak yerine environment variable kullanın
Script’i kendi ortamınıza göre uyarlamaktan çekinmeyin. İçinde bulunduğumuz kaynak kod yapısı modüler tasarlandığı için yeni özellik eklemek oldukça kolay. Bir sonraki adım olarak AWS S3 veya Minio’ya yükleme, Telegram bildirimi ya da Prometheus metriği ekleme gibi geliştirmeler yapabilirsiniz. Kod GitHub’da tutun, versiyonlayın ve değişiklikleri takip edin. Yedekleme scriptiniz de verileriniz kadar değerli.