PushizoDocs
Operasyon

Runbook: Yedekten geri yükleme ve felaket kurtarma (DR)

Hedefler (docs/plan/08 §8.10): RPO 1 saat, RTO 2 saat. Yedek kaynakları:

Hedefler (docs/plan/08 §8.10): RPO 1 saat, RTO 2 saat. Yedek kaynakları:

VeriYöntemSıklıkSaklamaAraç
PostgreSQL (tam)pg_dump -Fc → S3 postgres/günlük 03:3030 güninfra/scripts/backup.sh (install-cron.sh → /etc/cron.d/pushizo)
PostgreSQL (WAL)wal-g wal-push → S3 wal/sürekli30 güninfra/scripts/wal-g.env.example → depo kökünde .wal-g.env, imaj pushizo-postgres-walg (release)
PostgreSQL (wal-g temel yedek)backup-push + delete retain FULL 30günlük 03:0030 tam yedekinfra/scripts/walg-backup.sh (cron)
RedisAOF (appendfsync everysec) + saatlik RDBsüreklivolumecompose redis servisi
RustFS/S3nesne sürümleme (yerel)sürekli30 gün (yaşam döngüsü)storage-init
RustFS (sunucu dışı)aws s3 sync kova → yerel ayna (BACKUP_STORAGE_MIRROR_DIR) → S3 storage/<S3_BUCKET>/günlük 03:30ayna (sürümleme sunucu dışı kovada)infra/scripts/backup.sh
Yapılandırma.env.prod, infra/compose/secrets/*, TLS (certs volume)değişimdeSOPS/parola kasasıelle

RPO 1 saat yalnızca WAL arşivi açıkken sağlanır; yalnızca günlük pg_dump ile RPO 24 saattir. Üretimde POSTGRES_IMAGE wal-g'li imaj (ghcr.io/exec0de/pushizo-postgres-walg:<sürüm>, Debian tabanı — varsayılan postgres:16-bookworm ile aynı libc) ve PG_ARCHIVE_COMMAND="wal-g-env wal-push %p" olmalıdır; PITR için ayrıca günlük temel yedek (walg-backup.sh) gerekir (GO_LIVE_CHECKLIST §5).

backup.sh ve restore.sh sunucu dışı yedek kovasına ayrı kimlik bilgisiyle erişir: .env.prod içinde BACKUP_S3_ACCESS_KEY / BACKUP_S3_SECRET_KEY (isteğe bağlı BACKUP_S3_REGION). Bu kullanıcı yalnızca BACKUP_S3_BUCKET kovasında listeleme/okuma/yazma/silme yetkisine sahip olmalıdır; uygulamanın S3_ACCESS_KEY servis kullanıcısı kullanılmaz (uygulama ele geçirilirse yedekler silinemesin). Tanımlı değilse backup.sh açık bir hata ile durur.

Zamanlama ve izleme: sudo bash infra/scripts/install-cron.sh (idempotent) /etc/cron.d/pushizo (03:30 backup.sh, wal-g açıksa 03:00 walg-backup.sh, flock ile çakışma koruması) ve /etc/logrotate.d/pushizo yazar; günlük /var/log/pushizo-backup.log. Her başarılı adım BACKUP_METRICS_DIR (varsayılan /var/lib/node_exporter/textfile) altına pushizo_backup_last_success_timestamp_seconds{kind="postgres"|"storage"} yazar; node-exporter textfile collector ile okunur, 26 saati aşınca PushizoBackupStale (critical), metrik hiç yoksa PushizoBackupMetricMissing. Betikler .env.prod'u kaynak olarak çalıştırmaz (güvenli satır okuma, infra/scripts/env-file.sh) ve AWS CLI'yi ana makineye kurmadan sabit sürümlü amazon/aws-cli imajıyla çalıştırır (AWS_CLI_IMAGE). Elle AWS CLI komutu gerektiğinde aynı imaj kullanılır (kimlik bilgileri değer yazmadan ortamdan geçer):

export AWS_ACCESS_KEY_ID="$(grep -E '^BACKUP_S3_ACCESS_KEY=' .env.prod | tail -n1 | cut -d= -f2-)"
export AWS_SECRET_ACCESS_KEY="$(grep -E '^BACKUP_S3_SECRET_KEY=' .env.prod | tail -n1 | cut -d= -f2-)"
export AWS_DEFAULT_REGION=eu-central-1
docker run --rm -e AWS_ACCESS_KEY_ID -e AWS_SECRET_ACCESS_KEY -e AWS_DEFAULT_REGION amazon/aws-cli:2.36.40 \
  --endpoint-url https://s3.eu-central-1.amazonaws.com s3 ls s3://pushizo-backups/postgres/

1. Ne zaman?

  • Veri kaybı / bozulma (yanlış migration, hatalı toplu silme, disk arızası).
  • Sunucu kaybı (yeni sunucuya taşıma).
  • Haftalık otomatik tatbikat (infra/scripts/verify-restore.sh, staging, Pazar 04:00) ve üç ayda bir elle tam tatbikat.

2. Olay başlangıcı (T+0)

  1. docs/runbooks/incident.md akışını başlat: status page'de olay aç (/admin/incidents, bileşenler api, delivery), Slack #pushizo-oncall.
  2. Yazma trafiğini durdur: docker compose … stop nginx worker-dispatch worker-system (API kapalı → SDK'lar çevrimdışı kuyruğa alır, sunucu SDK'ları 5xx alır ve yeniden dener).
  3. Kesinti anını (UTC) not et: T_outage. RPO = T_outage − son yedek/WAL anı.

3. Seçenek A — Tam yedekten geri yükleme (pg_dump)

Yeni/aynı sunucuda, depo /opt/pushizo ve .env.prod hazır:

# 1) Veri katmanı (yeni sunucuda volume boş)
docker compose --env-file .env.prod -f infra/compose/docker-compose.prod.yml up -d postgres redis storage storage-init

# 2) En yeni yedeği ayrı bir veritabanına yükle ve doğrula (üretim DB'sinin üstüne YAZMAZ)
./infra/scripts/restore.sh --latest --target pushizo_restore --jobs 4
#    rapor: migration sayısı, org/app/abonelik sayıları, son teslimat/olay zamanı (RPO), süreler (RTO)

# 3) Doğrulama geçtiyse üretim veritabanını değiştir (bozuk DB silinmez, yeniden adlandırılır)
BROKEN="pushizo_broken_$(date +%Y%m%d)"
docker compose --env-file .env.prod -f infra/compose/docker-compose.prod.yml exec -T postgres \
  psql -U pushizo -d postgres -v ON_ERROR_STOP=1 <<SQL
select pg_terminate_backend(pid) from pg_stat_activity where datname = 'pushizo' and pid <> pg_backend_pid();
alter database pushizo rename to $BROKEN;
alter database pushizo_restore rename to pushizo;
SQL

# 4) Eksik migration varsa uygula, uygulamayı başlat
docker compose --env-file .env.prod -f infra/compose/docker-compose.prod.yml run --rm migrate up
IMAGE_TAG=<çalışan sürüm> ./infra/scripts/deploy.sh prod
```text

## 4. Seçenek B — WAL ile belirli bir ana dönüş (PITR, wal-g)
Yanlış toplu silme gibi durumlarda son tam yedek + WAL ile hatadan **hemen önceki** ana dönülür:

```bash
docker compose … stop api-1 api-2 worker-dispatch worker-system
# Boş veri dizinine en yeni tam yedeği indir
docker compose … run --rm --entrypoint sh -v postgres-restore:/restore postgres -c \
  'wal-g-env backup-fetch /restore LATEST'
# recovery ayarları: hedef zaman (UTC) ve WAL getirme komutu
cat > /tmp/recovery.conf <<'CONF'
restore_command = 'wal-g-env wal-fetch %f %p'
recovery_target_time = '2026-10-13 09:41:00+00'
recovery_target_action = 'promote'
CONF
# postgresql.auto.conf içine ekle, recovery.signal oluştur, volume'u postgres servisine bağla ve başlat; log'da
# "database system is ready" + "recovery stopping before commit" görünmeli. Ardından 3. adımdaki doğrulama sorguları.
```text
Ayrıntılı wal-g PITR adımları: https://github.com/wal-g/wal-g/blob/master/docs/PostgreSQL.md#backup-fetch

## 5. Redis kaybı (kuyruklar)
Redis AOF'tan dönemediyse (volume kaybı), teslimatlar veritabanında `pending` olarak durur:

```bash
# worker imajından (migrate/worker konteynerinde DATABASE_URL/REDIS_URL hazır)
docker compose --env-file .env.prod -f infra/compose/docker-compose.prod.yml run --rm worker-dispatch \
  node apps/worker/dist/cli/recover.js --dry-run       # kaç teslimat/iş kuyruklanacak
docker compose --env-file .env.prod -f infra/compose/docker-compose.prod.yml run --rm worker-dispatch \
  node apps/worker/dist/cli/recover.js                 # idempotent; jobId = batch'in ilk teslimat kimliği
# geliştirme ortamı: npm run queue:recover [-- --dry-run --app app_…]
```text
`reconcile-deliveries` bakım işi (10 dk'da bir) kaçanları zaten toplar; `queue:recover` beklemeden hepsini yeniden kuyruklar.
Zamanlayıcı (BullMQ job scheduler) kayıtları worker yeniden başlatılınca `upsertJobScheduler` ile yeniden oluşur.

## 6. Nesne depolama (RustFS/S3)
Sürümleme açık: yanlışlıkla silinen export/import dosyaları önceki sürümden geri alınır (S3 `ListObjectVersions` +
`GetObject?versionId=`, ör. `aws s3api list-object-versions` / `get-object --version-id`; kök kimlik bilgisiyle). Export dosyaları
24 saat, import dosyaları 7 gün saklanır (`cleanup-data-files`); kalıcı veri değildir — yeniden üretilebilir.

**Sunucu kaybı / `storage-data` volume kaybı:** `backup.sh` kovanın birebir aynasını sunucu dışı kovaya
`storage/<S3_BUCKET>/` altında tutar (`--delete` ile; silinen nesnelere karşı sunucu dışı kovada **sürümleme + 30 gün eski
sürüm saklama** kuralını açın). Geri yükleme (yeni sunucuda `storage` ve `storage-init` ayaktayken, root olarak):
```bash
cd /opt/pushizo && MIRROR=/var/backups/pushizo/storage-mirror && install -d -m 700 "$MIRROR"
# 1) Sunucu dışı kovadan aynaya (yedek kimlik bilgisi; yukarıdaki gibi ortamdan geçirilir)
docker run --rm -e AWS_ACCESS_KEY_ID -e AWS_SECRET_ACCESS_KEY -e AWS_DEFAULT_REGION -v "$MIRROR:/mirror" \
  amazon/aws-cli:2.36.40 --endpoint-url https://s3.eu-central-1.amazonaws.com \
  s3 sync s3://pushizo-backups/storage/pushizo/ /mirror --only-show-errors
# 2) Aynadan yerel RustFS'e (uygulama servis kullanıcısı S3_ACCESS_KEY/S3_SECRET_KEY ortamdan; compose `app` ağı)
docker run --rm --network pushizo_app -e AWS_ACCESS_KEY_ID -e AWS_SECRET_ACCESS_KEY -e AWS_DEFAULT_REGION \
  -e AWS_REQUEST_CHECKSUM_CALCULATION=when_required -e AWS_RESPONSE_CHECKSUM_VALIDATION=when_required \
  -v "$MIRROR:/mirror:ro" amazon/aws-cli:2.36.40 --endpoint-url http://storage:9000 \
  s3 sync /mirror s3://pushizo --only-show-errors
```text
İkinci adımda `AWS_ACCESS_KEY_ID`/`AWS_SECRET_ACCESS_KEY` değişkenlerini `.env.prod`'daki `S3_ACCESS_KEY`/`S3_SECRET_KEY`
ile değiştirin. Ayna sunucuda duruyorsa (yalnızca volume kaybı) 1. adım atlanır.

## 7. Kapanış
- Status page olayını `resolved` yap, müşteri etkisini (kaybolan zaman aralığı) özetle.
- RPO/RTO ölçümünü aşağıdaki tabloya işle; 2 saati aşan RTO için post-mortem (docs/runbooks/incident.md §Post-mortem).
- `MASTER_KEY` değişmediyse şifreli sağlayıcı kimlik bilgileri geçerlidir; yeni sunucuya taşındıysa `.env.prod` birebir aynı olmalıdır
  (anahtar kaybı = tüm sağlayıcı kimlik bilgileri yeniden girilir, bkz. `key-rotation.md`).

## 8. Tatbikat kaydı
`verify-restore.sh` her hafta `/var/log/pushizo-restore-drills.tsv` dosyasına satır ekler; üç aylık elle tatbikatlar buraya işlenir.

| Tarih | Ortam | Yedek (anahtar) | Yöntem | RPO (ölçülen) | RTO (ölçülen) | Sonuç | Notlar / aksiyon |
|-------|-------|-----------------|--------|---------------|---------------|-------|------------------|
| | staging | | pg_dump / PITR | | | | |

Bu sayfada