Nesne Depolama ile Yedekleme Otomasyonu

Yedekleme konusu her sysadmin’in kâbusu haline gelmiş olabilir; diskler dolar, bantlar bozulur, FTP sunucuları çöker. Ama nesne depolama bu denklemi köklü biçimde değiştirdi. S3 uyumlu depolama sistemleri, hem maliyet hem de otomasyon açısından klasik yedekleme çözümlerinin önüne geçti. Bu yazıda sıfırdan bir yedekleme otomasyonu kurgulamanın tüm adımlarını ele alacağız.

Nesne Depolama Neden Yedekleme İçin İdeal?

Klasik dosya sistemleri hiyerarşik yapıda çalışır; dizinler, alt dizinler, izinler. Nesne depolama ise bunların hiçbirini umursamaz. Her şey düz bir namespace içinde key-value mantığıyla tutulur. Bu basitlik, özellikle yedekleme için büyük avantaj sağlar.

Dayanıklılık açısından bakıldığında AWS S3, Azure Blob Storage ve MinIO gibi sistemler genellikle veriyi birden fazla availability zone’a yazar. Yani bir veri merkezi yanarsa yedeğin de yanma ihtimali son derece düşük. Buna ek olarak versioning özelliği sayesinde aynı dosyanın birden fazla sürümünü tutabilirsiniz; bir yedek bozulsa ya da üzerine yazılsa bile önceki versiyona dönmek mümkün.

Maliyet modeli de cazip. Sadece kullandığın kadar öde. 10 TB klasik sunucu diski kirası yerine nesne depolama genellikle çok daha ucuza gelir, üstelik lifecycle policy ile eski yedekleri otomatik olarak daha ucuz katmanlara taşıyabilirsin.

S3 uyumlu API ise ekosistemi inanılmaz genişletiyor. AWS S3, MinIO, Backblaze B2, Wasabi, DigitalOcean Spaces hepsi aynı API’yi kullanıyor. Bir kez yazdığın otomasyon kodu tüm bu platformlarda çalışıyor.

Ortamı Hazırlamak

AWS CLI Kurulumu ve Yapılandırması

Otomasyon scriptlerinin temelini oluşturacak araç awscli. Linux dağıtımına göre kurulum:

# Ubuntu/Debian
apt update && apt install -y awscli

# RHEL/CentOS/Rocky
dnf install -y awscli

# Ya da pip ile en güncel sürüm
pip3 install awscli --upgrade

# Kurulumu doğrula
aws --version

Kurulumdan sonra kimlik doğrulamasını yapılandır. Üretim ortamında IAM rolü kullanmak en güvenlisi ama script tabanlı işler için dedicated bir servis hesabı da kabul edilebilir:

aws configure --profile backup-user
# AWS Access Key ID: AKIA...
# AWS Secret Access Key: xxxx
# Default region name: eu-central-1
# Default output format: json

# Yapılandırmayı test et
aws s3 ls --profile backup-user

Profil kullanmak önemli çünkü aynı sunucuda birden fazla S3 uyumlu servis için farklı kimlik bilgisi tutabiliyorsun. Production bucket’ı için bir profil, test ortamı için başka bir profil.

MinIO ile Self-Hosted Seçenek

Bulut faturalarından kaçınmak ya da veriyi kendi altyapında tutmak istiyorsan MinIO mükemmel bir alternatif. Docker ile dakikalar içinde ayağa kalkıyor:

docker run -d 
  --name minio 
  -p 9000:9000 
  -p 9001:9001 
  -v /data/minio:/data 
  -e MINIO_ROOT_USER=admin 
  -e MINIO_ROOT_PASSWORD=guclu-sifre-buraya 
  --restart unless-stopped 
  quay.io/minio/minio server /data --console-address ":9001"

# MinIO'yu AWS CLI ile kullanmak için endpoint belirt
aws s3 ls 
  --endpoint-url http://localhost:9000 
  --profile minio-local

Temel Yedekleme Scripti

Artık teoriden pratiğe geçelim. Çoğu ortamda ihtiyaç duyulan şey şu: veritabanı dump’ı al, sıkıştır, şifrele, S3’e yükle, eski yedekleri temizle. Bunu adım adım yapalım.

PostgreSQL Yedekleme Scripti

#!/bin/bash
# /usr/local/bin/pg-backup.sh

set -euo pipefail

# Değişkenler
DB_HOST="localhost"
DB_PORT="5432"
DB_USER="backup_user"
DB_NAME="production_db"
S3_BUCKET="s3://sirket-yedekler/postgresql"
S3_PROFILE="backup-user"
BACKUP_DIR="/tmp/pg_backups"
RETENTION_DAYS=30
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="${BACKUP_DIR}/${DB_NAME}_${DATE}.sql.gz"
LOG_FILE="/var/log/pg-backup.log"

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

cleanup() {
    log "Gecici dosyalar temizleniyor..."
    rm -f "${BACKUP_FILE}"
}

trap cleanup EXIT

# Backup dizinini oluştur
mkdir -p "${BACKUP_DIR}"

log "PostgreSQL yedeği başlıyor: ${DB_NAME}"

# Dump al ve sıkıştır
PGPASSWORD="${PG_PASSWORD}" pg_dump 
    -h "${DB_HOST}" 
    -p "${DB_PORT}" 
    -U "${DB_USER}" 
    -d "${DB_NAME}" 
    --format=custom 
    --compress=9 
    --no-password 
    | gzip > "${BACKUP_FILE}"

BACKUP_SIZE=$(du -sh "${BACKUP_FILE}" | cut -f1)
log "Yedek alındı: ${BACKUP_FILE} (${BACKUP_SIZE})"

# S3'e yükle
aws s3 cp "${BACKUP_FILE}" "${S3_BUCKET}/" 
    --profile "${S3_PROFILE}" 
    --storage-class STANDARD_IA 
    --metadata "hostname=$(hostname),database=${DB_NAME},date=${DATE}"

log "S3'e yükleme tamamlandı"

# Eski yedekleri S3'ten temizle
aws s3 ls "${S3_BUCKET}/" --profile "${S3_PROFILE}" 
    | awk '{print $4}' 
    | while read -r file; do
        file_date=$(echo "${file}" | grep -oP 'd{8}' | head -1)
        if [[ -n "${file_date}" ]]; then
            days_old=$(( ( $(date +%s) - $(date -d "${file_date}" +%s) ) / 86400 ))
            if [[ ${days_old} -gt ${RETENTION_DAYS} ]]; then
                log "Eski yedek siliniyor: ${file} (${days_old} gün)"
                aws s3 rm "${S3_BUCKET}/${file}" --profile "${S3_PROFILE}"
            fi
        fi
    done

log "Yedekleme tamamlandı"

Bu script birkaç önemli şeyi yapıyor. set -euo pipefail ile herhangi bir komut hata verirse script duruyor, beklenmedik davranışların önüne geçiyorsun. trap cleanup EXIT ile script ne şekilde biterse bitsin geçici dosyalar temizleniyor. --storage-class STANDARD_IA ile S3’te daha ucuz Infrequent Access katmanına yazıyorsun çünkü yedeklere sürekli erişmiyorsun.

MySQL/MariaDB için Versiyon

#!/bin/bash
# /usr/local/bin/mysql-backup.sh

set -euo pipefail

MYSQL_HOST="localhost"
MYSQL_USER="backup_user"
MYSQL_PASSWORD="${MYSQL_BACKUP_PASSWORD}"
S3_BUCKET="s3://sirket-yedekler/mysql"
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_DIR="/tmp/mysql_backups"

mkdir -p "${BACKUP_DIR}"

# Tüm veritabanlarını listele ve her biri için ayrı yedek al
mysql -h "${MYSQL_HOST}" -u "${MYSQL_USER}" -p"${MYSQL_PASSWORD}" 
    -e "SHOW DATABASES;" 
    | grep -Ev "^(Database|information_schema|performance_schema|sys)$" 
    | while read -r db; do
        backup_file="${BACKUP_DIR}/${db}_${DATE}.sql.gz"
        echo "Yedekleniyor: ${db}"
        
        mysqldump 
            -h "${MYSQL_HOST}" 
            -u "${MYSQL_USER}" 
            -p"${MYSQL_PASSWORD}" 
            --single-transaction 
            --quick 
            --lock-tables=false 
            "${db}" | gzip > "${backup_file}"
        
        aws s3 cp "${backup_file}" "${S3_BUCKET}/${db}/" 
            --storage-class STANDARD_IA
        
        rm -f "${backup_file}"
        echo "${db} yedeklendi ve S3'e yüklendi"
    done

--single-transaction özellikle InnoDB tablolar için kritik. Tablo kilitlemeden tutarlı bir snapshot alıyor.

Şifreleme: Güvenli Yedekleme

Yedek almak yeterli değil, yedeğin içeriği de korunmalı. Özellikle bulut ortamlarında bucket politikaları hatalı yapılandırılmış olabilir ya da servis sağlayıcının erişimi konusunda endişelerin olabilir. GPG ile şifreleme hem güçlü hem de ücretsiz.

#!/bin/bash
# Şifreli yedekleme fonksiyonu

encrypt_and_upload() {
    local source_file="$1"
    local s3_destination="$2"
    local gpg_recipient="[email protected]"
    
    encrypted_file="${source_file}.gpg"
    
    # GPG ile asimetrik şifreleme
    gpg --batch 
        --yes 
        --trust-model always 
        --recipient "${gpg_recipient}" 
        --output "${encrypted_file}" 
        --encrypt "${source_file}"
    
    # Şifreli dosyayı yükle, orijinalini değil
    aws s3 cp "${encrypted_file}" "${s3_destination}" 
        --storage-class GLACIER_IR
    
    # Her ikisini de temizle
    shred -u "${source_file}" "${encrypted_file}"
    
    echo "Şifreli yedek yüklendi: ${s3_destination}"
}

# Kullanım örneği
DB_DUMP="/tmp/production_20240115.sql.gz"
encrypt_and_upload "${DB_DUMP}" "s3://sirket-yedekler/encrypted/$(basename ${DB_DUMP}).gpg"

shred komutu dosyayı sadece silmekle kalmıyor, üzerine rastgele veri yazarak kurtarılmasını zorlaştırıyor. Disk recovery senaryolarında önemli.

Lifecycle Policy ile Akıllı Saklama

Manuel temizlik yazmak yerine S3’ün kendi lifecycle sistemini kullan. Bu çok daha güvenilir ve maliyet optimizasyonu açısından çok daha etkili.

# Lifecycle policy JSON dosyası oluştur
cat > /tmp/lifecycle-policy.json << 'EOF'
{
    "Rules": [
        {
            "ID": "backup-lifecycle",
            "Status": "Enabled",
            "Filter": {
                "Prefix": "postgresql/"
            },
            "Transitions": [
                {
                    "Days": 7,
                    "StorageClass": "STANDARD_IA"
                },
                {
                    "Days": 30,
                    "StorageClass": "GLACIER_IR"
                },
                {
                    "Days": 90,
                    "StorageClass": "DEEP_ARCHIVE"
                }
            ],
            "Expiration": {
                "Days": 365
            }
        }
    ]
}
EOF

# Policy'i bucket'a uygula
aws s3api put-bucket-lifecycle-configuration 
    --bucket sirket-yedekler 
    --lifecycle-configuration file:///tmp/lifecycle-policy.json 
    --profile backup-user

echo "Lifecycle policy uygulandı"

Bu konfigürasyonla ne oluyor:

  • İlk 7 gün Standard depolamada kalıyor (hızlı erişim)
  • 7. günden sonra Infrequent Access’e geçiyor (yüzde 40-50 daha ucuz)
  • 30. günden sonra Glacier Instant Retrieval’a geçiyor (dakikalar içinde erişim, çok daha ucuz)
  • 90. günden sonra Deep Archive’a geçiyor (12 saatte erişim, en ucuz seçenek)
  • 365. günde otomatik siliniyor

Çoklu Sunucu Senaryosu

Gerçek ortamlarda onlarca, belki yüzlerce sunucu var. Hepsine ayrı ayrı script kurmak yerine merkezi bir yedekleme orkestrasyon yaklaşımı daha mantıklı.

#!/bin/bash
# /usr/local/bin/distributed-backup.sh
# Merkezi sunucuda çalışır, tüm hedefleri yönetir

SERVERS_FILE="/etc/backup/servers.list"
S3_BASE="s3://sirket-yedekler"
LOG_DIR="/var/log/backups"
PARALLEL_JOBS=5
DATE=$(date +%Y%m%d)

mkdir -p "${LOG_DIR}"

backup_server() {
    local server="$1"
    local server_type="$2"
    local log_file="${LOG_DIR}/${server}_${DATE}.log"
    
    echo "[$(date '+%H:%M:%S')] Başlıyor: ${server}" | tee -a "${log_file}"
    
    case "${server_type}" in
        "postgresql")
            ssh -o ConnectTimeout=10 
                -o StrictHostKeyChecking=no 
                "backup@${server}" 
                'pg_dumpall --globals-only 2>/dev/null; pg_dump -d production' 
                | gzip 
                | aws s3 cp - 
                    "${S3_BASE}/servers/${server}/pg_${DATE}.sql.gz" 
                    --expected-size 1073741824
            ;;
        "files")
            ssh -o ConnectTimeout=10 
                "backup@${server}" 
                'tar czf - /etc /var/www /home 2>/dev/null' 
                | aws s3 cp - 
                    "${S3_BASE}/servers/${server}/files_${DATE}.tar.gz"
            ;;
        *)
            echo "Bilinmeyen sunucu tipi: ${server_type}" >> "${log_file}"
            return 1
            ;;
    esac
    
    local exit_code=$?
    if [[ ${exit_code} -eq 0 ]]; then
        echo "[$(date '+%H:%M:%S')] Tamamlandı: ${server}" | tee -a "${log_file}"
    else
        echo "[$(date '+%H:%M:%S')] HATA: ${server} (exit: ${exit_code})" | tee -a "${log_file}"
    fi
    
    return ${exit_code}
}

export -f backup_server
export S3_BASE DATE LOG_DIR

# servers.list formatı: hostname tip
# web01 files
# db01 postgresql
grep -v '^#' "${SERVERS_FILE}" | 
    parallel --jobs "${PARALLEL_JOBS}" 
    backup_server {1} {2}

Bu scriptte ssh üzerinden pipe ile doğrudan S3’e yazıyor. Yani veriler hiçbir zaman merkezi sunucunun diskine değmiyor. Bant genişliği yeterli olduğu sürece bu yaklaşım hem hızlı hem de disk sorunu yaratmıyor.

Yedek Doğrulama: En Sık Atlanan Adım

Yedek aldın, harika. Ama o yedeği hiç restore ettin mi? Çoğu felaket kurtarma senaryosunda yedekler ya bozuk çıkıyor ya da restore süreci tahmin edilenden çok daha uzun sürüyor. Bunu düzenli olarak test etmek şart.

#!/bin/bash
# /usr/local/bin/backup-verify.sh
# Rastgele bir yedeği seç, indir, test et

S3_BUCKET="s3://sirket-yedekler/postgresql"
TEST_DB="backup_verify_test"
VERIFY_LOG="/var/log/backup-verify.log"

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

# En son yedeği bul
LATEST_BACKUP=$(aws s3 ls "${S3_BUCKET}/" 
    | sort 
    | tail -n 1 
    | awk '{print $4}')

if [[ -z "${LATEST_BACKUP}" ]]; then
    log "KRITIK: S3'te yedek bulunamadı!"
    exit 1
fi

log "Doğrulanacak yedek: ${LATEST_BACKUP}"

# İndir
TEMP_FILE="/tmp/verify_${LATEST_BACKUP}"
aws s3 cp "${S3_BUCKET}/${LATEST_BACKUP}" "${TEMP_FILE}"

# MD5 kontrolü (S3 metadata ile karşılaştır)
LOCAL_MD5=$(md5sum "${TEMP_FILE}" | cut -d' ' -f1)
S3_ETAG=$(aws s3api head-object 
    --bucket sirket-yedekler 
    --key "postgresql/${LATEST_BACKUP}" 
    --query 'ETag' 
    --output text | tr -d '"')

log "Local MD5: ${LOCAL_MD5}"
log "S3 ETag: ${S3_ETAG}"

# Test veritabanına restore et
PGPASSWORD="${PG_PASSWORD}" createdb -U postgres "${TEST_DB}" 2>/dev/null || true

if zcat "${TEMP_FILE}" | PGPASSWORD="${PG_PASSWORD}" psql -U postgres -d "${TEST_DB}" -q; then
    TABLE_COUNT=$(PGPASSWORD="${PG_PASSWORD}" psql -U postgres -d "${TEST_DB}" 
        -t -c "SELECT count(*) FROM information_schema.tables WHERE table_schema='public'")
    log "BASARILI: Restore tamamlandı, ${TABLE_COUNT} tablo doğrulandı"
    
    # Başarı durumunu bir dosyaya yaz (monitoring için)
    echo "$(date '+%Y-%m-%d %H:%M:%S') OK ${LATEST_BACKUP}" >> /var/log/backup-verify-status.log
else
    log "HATA: Restore başarısız!"
    exit 1
fi

# Temizlik
PGPASSWORD="${PG_PASSWORD}" dropdb -U postgres "${TEST_DB}"
rm -f "${TEMP_FILE}"

log "Doğrulama tamamlandı"

Bu scripti haftada bir çalıştırıyorum ve çıktısını Slack’e veya email’e gönderiyorum. “Yedek alındı” logunu görmek yeterli değil, o yedeğin gerçekten çalışıp çalışmadığını bilmek gerek.

Cron ile Zamanlama

Scriptler hazır, şimdi zamanlamayı yapalım:

# crontab -e ile açıp ekle

# Her gece 02:00'de PostgreSQL yedeği
0 2 * * * /usr/local/bin/pg-backup.sh >> /var/log/pg-backup.log 2>&1

# Her gece 03:00'de MySQL yedeği
0 3 * * * /usr/local/bin/mysql-backup.sh >> /var/log/mysql-backup.log 2>&1

# Her Pazar 04:00'de yedek doğrulama
0 4 * * 0 /usr/local/bin/backup-verify.sh >> /var/log/backup-verify.log 2>&1

# Her gün 06:00'da distributed backup
0 6 * * * /usr/local/bin/distributed-backup.sh >> /var/log/distributed-backup.log 2>&1

Cron yerine systemd timer kullanmayı da düşünebilirsin. Daha iyi loglama, bağımlılık yönetimi ve başarısız işleri yeniden deneme gibi avantajları var.

Monitoring ve Alarm

Yedeklemenin başarılı olduğunu sadece log dosyasından takip etmek yeterli değil. Bir yedekleme başarısız olduğunda anında haberdar olman gerekiyor.

#!/bin/bash
# Yedekleme sonrası bildirim fonksiyonu
# Slack webhook ile entegre

SLACK_WEBHOOK="${SLACK_BACKUP_WEBHOOK_URL}"
HOSTNAME=$(hostname -f)

send_notification() {
    local status="$1"
    local message="$2"
    local color="good"
    
    [[ "${status}" == "error" ]] && color="danger"
    [[ "${status}" == "warning" ]] && color="warning"
    
    curl -s -X POST "${SLACK_WEBHOOK}" 
        -H 'Content-type: application/json' 
        --data "{
            "attachments": [{
                "color": "${color}",
                "title": "Yedekleme Bildirimi - ${HOSTNAME}",
                "text": "${message}",
                "ts": $(date +%s)
            }]
        }" > /dev/null
}

# Backup script içinde kullanım:
# send_notification "success" "PostgreSQL yedeği başarıyla tamamlandı (250MB)"
# send_notification "error" "MySQL yedeği başarısız: Connection refused"

Bunun yanı sıra S3 bucket’ına CloudWatch (AWS için) ya da benzeri bir monitoring bağlayarak günlük yüklenen nesne sayısını ve boyutunu izleyebilirsin. Eğer bir gün hiç nesne yüklenmemişse otomatik alarm devreye girer.

İnce Ayarlar ve İpuçları

Prodüksiyonda öğrendiğim bazı pratik detaylar:

Bant genişliği sınırlaması: Yedekleme trafiği üretim trafiğini ezmemeli. AWS CLI’da bu var:

aws s3 cp buyuk-yedek.tar.gz s3://bucket/ 
    --cli-read-timeout 300 
    --cli-connect-timeout 60 
    --expected-size 10737418240

Multipart upload: 100MB’dan büyük dosyalar için otomatik devreye girer ama manuel ayar da yapabilirsin:

aws configure set default.s3.multipart_threshold 64MB
aws configure set default.s3.multipart_chunksize 16MB
aws configure set default.s3.max_concurrent_requests 10

Bucket versioning: Kazara silmelere karşı mutlaka etkinleştir:

aws s3api put-bucket-versioning 
    --bucket sirket-yedekler 
    --versioning-configuration Status=Enabled

Server-side encryption: Yasal uyumluluk gerektiriyorsa:

aws s3 cp dosya.tar.gz s3://sirket-yedekler/ 
    --sse aws:kms 
    --sse-kms-key-id alias/backup-key

İnternet bağlantısı kesilirse: --no-progress ve retry mekanizması ekle, büyük dosyalar için özellikle önemli:

aws s3 cp buyuk-dosya.tar.gz s3://bucket/ 
    --no-progress 
    --retry-delay 5 
    --retry-max-attempts 3

Gerçek Dünya Senaryo: E-ticaret Platformu

Bir müşteride 8 PostgreSQL sunucusu, 2 MySQL sunucusu, 15 uygulama sunucusu vardı. Eski yedekleme sistemi her sunucuya farklı bir disk takılı, elle yönetilen bir kaos. Disklerin yarısı doluydu, kimse temizlemiyordu.

Nesne depolama tabanlı sisteme geçince neler değişti:

  • Tüm yedekler tek bir bucket altında toplanıyor, merkezi izleme mümkün
  • Lifecycle policy ile eski yedekler otomatik Glacier’a geçiyor, maliyet yüzde 65 azaldı
  • Haftalık otomatik restore testi sayesinde 3 ay içinde 2 bozuk yedek yakalandı (eski sistemde bunlar felaket anında fark edilirdi)
  • Yeni sunucu eklemek 15 dakika: sunucuya script kopyala, cron’a ekle, bitti

Tek zorluk başlangıçta IAM politikalarını doğru kurmaktı. Her uygulamaya sadece kendi bucket prefix’ine yazma izni vermek biraz zaman aldı ama doğru yapıldığında güvenlik açısından çok daha iyi bir noktaya gelinmişti.

Sonuç

Nesne depolama tabanlı yedekleme sistemi kurmak göründüğü kadar karmaşık değil. Temel olarak şu adımları takip edersen sağlam bir altyapı elde edersin:

  • AWS CLI ya da uyumlu bir araçla S3 erişimini kur
  • Her uygulama tipi için ayrı yedekleme scripti yaz, şifrelemeyi unutma
  • Lifecycle policy ile depolama maliyetini otomatik olarak optimize et
  • Yedek doğrulamayı cron’a ekle ve sonuçları izle
  • Başarısız yedekler için anında bildirim kur

En kritik hatırlatma: Alınan yedek değil, restore edilen yedek önemlidir. Her kurduğun sistemde ilk iş olarak restore testini otomatize et. Bir felaket anında “yedekler vardı ama açılmıyor” demek istemezsin.

Nesne depolama bu işi kolaylaştırdı ama düzenli test ve izleme olmadan en iyi sistemin bile çökeceğini unutma.

Bir yanıt yazın

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