Bacula ile Kubernetes Persistent Volume Yedekleme Stratejisi

Kubernetes ortamlarında veri yönetimi, geleneksel sistemlere kıyasla çok daha fazla dikkat gerektiriyor. Pod’lar gelip geçici olsa da Persistent Volume’lerdeki veriler kalıcı olması gerekiyor. Peki bu verileri güvenilir şekilde nasıl yedekleyeceğiz? İşte burada Bacula devreye giriyor. Açık kaynaklı, kurumsal düzeyde yeteneklere sahip bu yedekleme sistemi, doğru yapılandırıldığında Kubernetes PV’lerini oldukça etkin şekilde koruyabiliyor.

Neden Bacula ve Kubernetes Birlikteliği?

Kubernetes, kendi başına bir yedekleme çözümü sunmuyor. Velero gibi Kubernetes’e özgü araçlar mevcut olsa da mevcut altyapısında zaten Bacula kullanan bir ekip için ayrı bir sistem kurmak hem maliyet hem de operasyonel yük açısından mantıklı değil. Bacula’nın modüler yapısı, özelleştirilebilir betik desteği ve güçlü zamanlama özellikleri sayesinde Kubernetes PV yedeklemelerini mevcut yedekleme iş akışınıza entegre edebilirsiniz.

Gerçek dünya senaryosuna bakacak olursak: Bir e-ticaret şirketinde PostgreSQL veritabanı Kubernetes üzerinde çalışıyor, veriler PVC’lerde saklanıyor. Geceleri yapılan Bacula yedeklemeleri hem bu PV’leri hem de fiziksel sunuculardaki verileri kapsıyor. Tek konsol, tek raporlama, tek sorumluluk. Bu senaryo hem operasyonel karmaşıklığı azaltıyor hem de mevcut Bacula lisans yatırımınızı değerlendirmenizi sağlıyor.

Mimari Genel Bakış

Bacula ile Kubernetes PV yedekleme stratejisi temel olarak iki yaklaşım üzerine inşa ediliyor:

  • Doğrudan Volume Mount Yaklaşımı: Bacula File Daemon’ı Kubernetes node’larında çalıştırarak PV dosya sistemlerine doğrudan erişim
  • Sidecar Container Yaklaşımı: Her Pod’un yanına Bacula FD içeren bir sidecar container ekleyerek uygulama tutarlılığını sağlama

Her iki yaklaşımın da avantajları ve dezavantajları var. Biz bu yazıda her ikisini de ele alacağız ancak production ortamlar için sidecar yaklaşımını önereceğiz çünkü uygulama tutarlılığı kritik önem taşıyor.

Ön Gereksinimler ve Ortam Hazırlığı

Başlamadan önce aşağıdakilere ihtiyacınız olacak:

  • Çalışan bir Bacula Director, Storage Daemon ve Catalog (en az Bacula 9.x, tercihen 11.x)
  • Kubernetes cluster (1.20+)
  • kubectl erişimi ve gerekli RBAC yetkileri
  • Bacula FD binary’sinin container image’a eklenebilmesi için Docker build ortamı

Önce Bacula Director’da Kubernetes node’larını veya pod’larını temsil edecek Client tanımlarını yapılandırmamız gerekiyor.

# Bacula Director konfigürasyon dosyasını düzenle
vim /etc/bacula/bacula-dir.conf
# Kubernetes PV yedeklemesi için Client tanımı
Client {
  Name = k8s-postgres-fd
  Address = 10.0.1.45        # Bacula FD'nin dinleyeceği IP
  FDPort = 9102
  Catalog = MyCatalog
  Password = "guclu-sifre-buraya"
  File Retention = 30 days
  Job Retention = 6 months
  AutoPrune = yes
}

# Kubernetes PV'leri için ayrı bir FileSet tanımı
FileSet {
  Name = "K8S-PersistentVolumes"
  Include {
    Options {
      signature = MD5
      compression = GZIP
      noatime = yes
    }
    # NFS veya hostPath PV mount noktaları
    File = /mnt/k8s-pvs/postgres-data
    File = /mnt/k8s-pvs/redis-data
    File = /mnt/k8s-pvs/elasticsearch-data
  }
  Exclude {
    File = /mnt/k8s-pvs/postgres-data/pg_wal
    File = /mnt/k8s-pvs/postgres-data/postmaster.pid
  }
}

Node Üzerinde Bacula File Daemon Kurulumu

İlk yaklaşım olan node üzerinde doğrudan FD çalıştırma yöntemi için Kubernetes worker node’larına Bacula File Daemon kurulumu yapıyoruz.

# Worker node'larda Bacula FD kurulumu (Debian/Ubuntu için)
apt-get update && apt-get install -y bacula-fd

# bacula-fd.conf konfigürasyonu
cat > /etc/bacula/bacula-fd.conf << 'EOF'
FileDaemon {
  Name = k8s-node1-fd
  FDport = 9102
  WorkingDirectory = /var/lib/bacula
  Pid Directory = /run/bacula
  Maximum Concurrent Jobs = 5
  Plugin Directory = /usr/lib/bacula
}

Director {
  Name = bacula-dir
  Password = "guclu-sifre-buraya"
}

Messages {
  Name = Standard
  director = bacula-dir = all, !skipped, !restored
}
EOF

# Servisi başlat
systemctl enable bacula-fd
systemctl start bacula-fd

# PV mount noktalarının erişilebilir olduğunu doğrula
ls -la /mnt/k8s-pvs/

Bu yaklaşımda kritik bir adım var: Kubernetes PV’lerinin node üzerindeki fiziksel konumlarını tespit etmek.

# PVC'lerin fiziksel konumlarını bulmak için
kubectl get pv -o json | jq '.items[] | {name: .metadata.name, path: .spec.hostPath.path, nfs: .spec.nfs}'

# Belirli bir PVC için node bilgisi
kubectl get pod postgres-0 -o jsonpath='{.spec.nodeName}'
kubectl describe pod postgres-0 | grep -A 5 "Volumes:"

# PV mount noktasını node üzerinde doğrula
kubectl get pvc postgres-data-pvc -o jsonpath='{.spec.volumeName}'
# Çıktı: pvc-abc123def456

# Node üzerinde fiziksel yolu bul
find /var/lib/kubelet/pods -name "*.mount" 2>/dev/null | head -20
ls /var/lib/kubelet/plugins/kubernetes.io/

Sidecar Container Yaklaşımı (Önerilen Yöntem)

Production ortamlar için sidecar yaklaşımı çok daha güvenilir. Bunun temel sebebi uygulama tutarlılığı. Veritabanı dump’ı almadan önce dosyaları yedeklemek tutarsız bir backup üretebilir.

Önce Bacula FD içeren bir container image oluşturalım:

# Dockerfile.bacula-fd
FROM ubuntu:22.04

RUN apt-get update && 
    apt-get install -y bacula-fd bacula-common && 
    apt-get clean && 
    rm -rf /var/lib/apt/lists/*

# Özel başlangıç betiği
COPY bacula-fd-entrypoint.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/bacula-fd-entrypoint.sh

EXPOSE 9102

ENTRYPOINT ["/usr/local/bin/bacula-fd-entrypoint.sh"]
# bacula-fd-entrypoint.sh
#!/bin/bash
set -e

# Ortam değişkenlerinden konfigürasyon oluştur
cat > /etc/bacula/bacula-fd.conf << EOF
FileDaemon {
  Name = ${FD_NAME:-pod-fd}
  FDport = 9102
  WorkingDirectory = /var/lib/bacula
  Pid Directory = /run
  Maximum Concurrent Jobs = 3
}

Director {
  Name = ${DIRECTOR_NAME:-bacula-dir}
  Password = "${FD_PASSWORD}"
}

Messages {
  Name = Standard
  director = ${DIRECTOR_NAME:-bacula-dir} = all, !skipped, !restored
}
EOF

mkdir -p /var/lib/bacula /run/bacula

echo "Bacula FD başlatılıyor: ${FD_NAME}"
exec /usr/sbin/bacula-fd -f -c /etc/bacula/bacula-fd.conf

Şimdi bu image’ı kullanan bir Kubernetes Deployment oluşturalım:

# postgres-with-bacula-sidecar.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: postgres
  namespace: production
spec:
  serviceName: postgres
  replicas: 1
  selector:
    matchLabels:
      app: postgres
  template:
    metadata:
      labels:
        app: postgres
    spec:
      containers:
      - name: postgres
        image: postgres:15
        env:
        - name: POSTGRES_DB
          value: myapp
        - name: POSTGRES_PASSWORD
          valueFrom:
            secretKeyRef:
              name: postgres-secret
              key: password
        volumeMounts:
        - name: postgres-data
          mountPath: /var/lib/postgresql/data

      # Bacula FD Sidecar
      - name: bacula-fd
        image: registry.example.com/bacula-fd:11.0
        env:
        - name: FD_NAME
          value: "k8s-postgres-fd"
        - name: DIRECTOR_NAME
          value: "bacula-dir"
        - name: FD_PASSWORD
          valueFrom:
            secretKeyRef:
              name: bacula-secret
              key: fd-password
        ports:
        - containerPort: 9102
          name: bacula-fd
        volumeMounts:
        - name: postgres-data
          mountPath: /backup/postgres-data
          readOnly: true
        resources:
          requests:
            memory: "64Mi"
            cpu: "50m"
          limits:
            memory: "128Mi"
            cpu: "200m"

  volumeClaimTemplates:
  - metadata:
      name: postgres-data
    spec:
      accessModes: ["ReadWriteOnce"]
      storageClassName: fast-ssd
      resources:
        requests:
          storage: 50Gi

Uygulama Tutarlılığı için Pre/Post Betikler

Yedekleme öncesi ve sonrası çalıştırılacak betikler, uygulama tutarlılığının anahtarı. Bacula’nın ClientRunBeforeJob ve ClientRunAfterJob direktifleri tam da bu iş için var.

# /etc/bacula/scripts/postgres-pre-backup.sh
#!/bin/bash
# PostgreSQL için yedekleme öncesi betik

LOG_FILE="/var/log/bacula/pre-backup.log"
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')

echo "[$TIMESTAMP] PostgreSQL pre-backup başlatılıyor..." >> $LOG_FILE

# Pod adını ve namespace'i bul
POD_NAME=$(kubectl get pods -n production -l app=postgres 
  -o jsonpath='{.items[0].metadata.name}')

if [ -z "$POD_NAME" ]; then
  echo "[$TIMESTAMP] HATA: PostgreSQL pod bulunamadı!" >> $LOG_FILE
  exit 1
fi

# PostgreSQL checkpoint al (WAL'ı temizle ve tutarlı durum sağla)
kubectl exec -n production $POD_NAME -c postgres -- 
  psql -U postgres -c "CHECKPOINT;" >> $LOG_FILE 2>&1

# pg_start_backup ile yedekleme moduna gir
kubectl exec -n production $POD_NAME -c postgres -- 
  psql -U postgres -c "SELECT pg_start_backup('bacula_backup', true);" 
  >> $LOG_FILE 2>&1

echo "[$TIMESTAMP] PostgreSQL backup modu aktif, yedekleme başlayabilir." >> $LOG_FILE
exit 0
# /etc/bacula/scripts/postgres-post-backup.sh
#!/bin/bash
# PostgreSQL için yedekleme sonrası betik

LOG_FILE="/var/log/bacula/post-backup.log"
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')

echo "[$TIMESTAMP] PostgreSQL post-backup başlatılıyor..." >> $LOG_FILE

POD_NAME=$(kubectl get pods -n production -l app=postgres 
  -o jsonpath='{.items[0].metadata.name}')

# Backup modundan çık
kubectl exec -n production $POD_NAME -c postgres -- 
  psql -U postgres -c "SELECT pg_stop_backup();" >> $LOG_FILE 2>&1

echo "[$TIMESTAMP] PostgreSQL normal operasyon moduna döndü." >> $LOG_FILE

# Yedekleme başarı durumunu kontrol et ve bildirim gönder
if [ $? -eq 0 ]; then
  echo "[$TIMESTAMP] Yedekleme başarıyla tamamlandı." >> $LOG_FILE
  # Opsiyonel: Slack veya mail bildirimi
  # curl -X POST -H 'Content-type: application/json' 
  #   --data '{"text":"Postgres PV backup tamamlandı"}' 
  #   $SLACK_WEBHOOK_URL
else
  echo "[$TIMESTAMP] UYARI: pg_stop_backup hatası!" >> $LOG_FILE
  exit 1
fi

exit 0

Bacula Director’da bu betikleri kullanan Job tanımı:

# bacula-dir.conf içinde Job tanımı
Job {
  Name = "K8S-Postgres-PV-Backup"
  Type = Backup
  Client = k8s-postgres-fd
  FileSet = "K8S-PersistentVolumes"
  Schedule = "WeeklyCycleAfterMidnight"
  Storage = FileStorage
  Messages = Standard
  Pool = K8S-PV-Pool
  Priority = 10
  Write Bootstrap = "/var/lib/bacula/%c.bsr"

  # Uygulama tutarlılığı için betikler
  ClientRunBeforeJob = "/etc/bacula/scripts/postgres-pre-backup.sh"
  ClientRunAfterJob = "/etc/bacula/scripts/postgres-post-backup.sh"

  # Yedekleme başarısız olursa post betiği yine de çalıştır
  RunAfterFailedJob = "/etc/bacula/scripts/postgres-post-backup.sh"
}

Schedule {
  Name = "WeeklyCycleAfterMidnight"
  Run = Full 1st sun at 01:05
  Run = Differential 2nd-5th sun at 01:05
  Run = Incremental mon-sat at 01:05
}

Pool {
  Name = K8S-PV-Pool
  Pool Type = Backup
  Recycle = yes
  AutoPrune = yes
  Volume Retention = 30 days
  Maximum Volume Bytes = 50G
  Maximum Volumes = 20
  Label Format = "K8S-PV-"
}

RBAC ve Güvenlik Yapılandırması

Kubernetes ortamında Bacula’nın çalışabilmesi için doğru RBAC tanımları şart.

# bacula-rbac.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: bacula-backup
  namespace: production

---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: bacula-backup-role
rules:
- apiGroups: [""]
  resources: ["pods", "pods/exec"]
  verbs: ["get", "list", "create"]
- apiGroups: [""]
  resources: ["persistentvolumes", "persistentvolumeclaims"]
  verbs: ["get", "list"]
- apiGroups: ["apps"]
  resources: ["statefulsets", "deployments"]
  verbs: ["get", "list"]

---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: bacula-backup-binding
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: bacula-backup-role
subjects:
- kind: ServiceAccount
  name: bacula-backup
  namespace: production
# RBAC'ı uygula
kubectl apply -f bacula-rbac.yaml

# Bacula'nın kullanacağı kubeconfig oluştur
kubectl create token bacula-backup -n production --duration=8760h 
  > /etc/bacula/k8s-token

# Kubeconfig dosyasını oluştur
kubectl config set-credentials bacula-user 
  --token=$(cat /etc/bacula/k8s-token)
kubectl config set-context bacula-context 
  --cluster=production-cluster 
  --user=bacula-user

Restore Prosedürü ve Testler

Yedekleme kadar önemli olan restore sürecini de otomatize etmeliyiz. Her yedeklemenin geri yüklenebilir olduğunu periyodik olarak test etmek kritik öneme sahip.

# restore-test.sh - Haftalık otomatik restore testi
#!/bin/bash

TEST_NAMESPACE="backup-test"
TIMESTAMP=$(date '+%Y%m%d_%H%M%S')
LOG_FILE="/var/log/bacula/restore-test-${TIMESTAMP}.log"

echo "Restore testi başlıyor: $TIMESTAMP" | tee -a $LOG_FILE

# Test namespace oluştur
kubectl create namespace $TEST_NAMESPACE 2>/dev/null || true

# Bacula restore job'u başlat
bconsole << 'BCON'
restore
5
mark *
done
yes
quit
BCON

# Restore edilen verileri doğrula
sleep 300  # Restore'un tamamlanmasını bekle

# PostgreSQL veri bütünlüğünü kontrol et
TEST_POD=$(kubectl get pods -n $TEST_NAMESPACE -l app=postgres-restore 
  -o jsonpath='{.items[0].metadata.name}')

if kubectl exec -n $TEST_NAMESPACE $TEST_POD -- 
  pg_dump -U postgres myapp > /dev/null 2>&1; then
  echo "BASARILI: Restore test geçti - $TIMESTAMP" | tee -a $LOG_FILE
  # Bildirim gönder
else
  echo "HATA: Restore test başarısız - $TIMESTAMP" | tee -a $LOG_FILE
  # Acil bildirim gönder
  exit 1
fi

# Test namespace'i temizle
kubectl delete namespace $TEST_NAMESPACE

echo "Restore testi tamamlandı." | tee -a $LOG_FILE

Restore testini Bacula tarafında da zamanlamak için:

# bacula-dir.conf içine restore test job'u ekle
Job {
  Name = "K8S-Postgres-Restore-Test"
  Type = Restore
  Client = k8s-postgres-fd
  FileSet = "K8S-PersistentVolumes"
  Storage = FileStorage
  Messages = Standard
  Pool = K8S-PV-Pool
  Where = /tmp/bacula-restore-test
  RunScript {
    Command = "/etc/bacula/scripts/restore-test.sh"
    RunsOnClient = yes
    RunsWhen = After
    FailJobOnError = yes
  }
}

İzleme ve Uyarı Entegrasyonu

Yedekleme durumunu izlemek için Bacula’nın Events tablosunu sorgulayan basit bir izleme betiği hazırlayalım:

#!/bin/bash
# bacula-k8s-monitor.sh

BACULA_DB="bacula"
DB_HOST="localhost"
DB_USER="bacula"
ALERT_EMAIL="[email protected]"

# Son 24 saatteki başarısız job'ları kontrol et
FAILED_JOBS=$(mysql -h $DB_HOST -u $DB_USER $BACULA_DB -se 
  "SELECT Job.Name, Job.StartTime, Job.JobStatus
   FROM Job
   WHERE Job.Name LIKE 'K8S%'
   AND Job.StartTime > DATE_SUB(NOW(), INTERVAL 24 HOUR)
   AND Job.JobStatus NOT IN ('T', 'W')
   ORDER BY Job.StartTime DESC;")

if [ -n "$FAILED_JOBS" ]; then
  echo "UYARI: Kubernetes PV yedekleme hataları tespit edildi!" | 
    mail -s "[KRITIK] Bacula K8S Backup Hatasi" $ALERT_EMAIL << EOF
Son 24 saat içinde başarısız olan K8S yedekleme job'ları:

$FAILED_JOBS

Lütfen Bacula konsolunu kontrol ediniz.
EOF
fi

# PV kapasitesi kontrolü
kubectl get pv --no-headers | awk '{
  if ($5 == "Released" || $5 == "Failed")
    print "UYARI: PV durumu sorunlu - " $1 " : " $5
}'

# Son başarılı full backup kontrolü
LAST_FULL=$(mysql -h $DB_HOST -u $DB_USER $BACULA_DB -se 
  "SELECT MAX(StartTime) FROM Job
   WHERE Name = 'K8S-Postgres-PV-Backup'
   AND Level = 'F'
   AND JobStatus = 'T';")

echo "Son başarılı full backup: $LAST_FULL"

Sık Karşılaşılan Sorunlar ve Çözümleri

Pratik deneyimlerimizden derlediğimiz sorun ve çözüm listesi:

  • “Connection refused on port 9102”: Sidecar container’da network policy sorunu olabilir. kubectl exec ile container içinden Bacula Director’a telnet atarak bağlantıyı test edin. NetworkPolicy kurallarını gözden geçirin.
  • “Permission denied on /var/lib/kubelet”: Node tabanlı yaklaşımda kubelet dizinine doğrudan erişim kısıtlanmış olabilir. FSGroup ve supplementalGroups ayarlarını Pod security context’inde düzenleyin.
  • “Inconsistent backup data”: Pre-backup betiğinin çalışmadığını gösterir. ClientRunBeforeJob çıkış kodunu kontrol edin; betik 0 dışında bir değer döndürürse Bacula job’u durdurmaz, sadece uyarı verir. Abort Job On Error = yes direktifini Job tanımına ekleyin.
  • “PVC bound to different node after pod restart”: ReadWriteOnce PVC’ler pod farklı bir node’a schedule edilirse mount edilemez. Bu durumda yedekleme de çalışmaz. Node affinity kuralları ekleyerek pod’un her zaman aynı node’da çalışmasını sağlayın.
  • Incremental backup boyutu beklenenden büyük: PostgreSQL WAL dosyaları sürekli değiştiği için Exclude direktifleriyle pg_wal dizinini FileSet’ten çıkarın. WAL arşivini ayrı bir job ile yedekleyin.

Kapasite Planlama Notları

Kubernetes PV yedeklemelerinde kapasite planlaması yaparken şu faktörleri göz önünde bulundurun:

  • Değişim oranı (change rate): Veritabanı workload’una bağlı olarak günlük değişim oranı toplam veri boyutunun yüzde 10 ile 30’u arasında değişebilir. Incremental yedekleme boyutlarını ilk iki haftada ölçerek Pool tanımınızdaki Maximum Volume Bytes değerini buna göre ayarlayın.
  • Retention politikası: Production ortamda minimum 30 günlük, kritik veriler için 90 günlük retention önerilir. Volume Retention değerini iş gereksinimlerinize göre belirleyin.
  • Deduplication: Aynı base image’dan türeyen birden fazla PV varsa, Bacula’nın deduplication özelliği depolama alanından önemli tasarruf sağlar. Deduplication = yes direktifini FileSet’e ekleyin.
  • Ağ bant genişliği: Sidecar yaklaşımında Bacula FD ile Storage Daemon arasındaki trafik cluster iç ağını kullanır. Büyük full backup’lar için gece saatlerini tercih edin ve Maximum Bandwidth Per Job direktifiyle bant genişliğini sınırlandırın.

Sonuç

Bacula ile Kubernetes Persistent Volume yedeklemesi, doğru yapılandırıldığında kurumsal düzeyde güvenilir bir çözüm sunuyor. Sidecar container yaklaşımı, uygulama tutarlılığı açısından node tabanlı yönteme kıyasla çok daha sağlam bir temel oluşturuyor. Pre ve post backup betikleri sayesinde PostgreSQL gibi veritabanları tutarlı bir şekilde yedeklenebiliyor.

Bu stratejinin en büyük avantajı mevcut Bacula altyapınızla bütünleşmesi. Ayrı bir araç öğrenmek, ayrı lisans maliyeti, ayrı bir yedekleme politikası yönetmek yerine Kubernetes PV’lerinizi diğer tüm sistemlerinizle birlikte tek bir konsoldan yönetebiliyorsunuz. Restore testlerini otomatize etmek ve izleme entegrasyonunu kurmak, “yedekleme var mı?” sorusunu “yedekleme çalışıyor mu?” sorusuna dönüştürüyor ki bu fark operasyonel olgunluk açısından son derece önemli.

Bir sonraki adım olarak Bacula’nın plugin altyapısını kullanarak Kubernetes CSI snapshot’larını doğrudan Bacula kataloğuna entegre etmeyi düşünebilirsiniz. Bu konu başlı başına bir yazıyı hak ediyor.

Bir yanıt yazın

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