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ı:
| Veri | Yöntem | Sıklık | Saklama | Araç |
|---|---|---|---|---|
| PostgreSQL (tam) | pg_dump -Fc → S3 postgres/ | günlük 03:30 | 30 gün | infra/scripts/backup.sh (install-cron.sh → /etc/cron.d/pushizo) |
| PostgreSQL (WAL) | wal-g wal-push → S3 wal/ | sürekli | 30 gün | infra/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 30 | günlük 03:00 | 30 tam yedek | infra/scripts/walg-backup.sh (cron) |
| Redis | AOF (appendfsync everysec) + saatlik RDB | sürekli | volume | compose redis servisi |
| RustFS/S3 | nesne sürümleme (yerel) | sürekli | 30 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:30 | ayna (sürümleme sunucu dışı kovada) | infra/scripts/backup.sh |
| Yapılandırma | .env.prod, infra/compose/secrets/*, TLS (certs volume) | değişimde | SOPS/parola kasası | elle |
RPO 1 saat yalnızca WAL arşivi açıkken sağlanır; yalnızca günlük
pg_dumpile RPO 24 saattir. ÜretimdePOSTGRES_IMAGEwal-g'li imaj (ghcr.io/exec0de/pushizo-postgres-walg:<sürüm>, Debian tabanı — varsayılanpostgres:16-bookwormile aynı libc) vePG_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.shverestore.shsunucu dışı yedek kovasına ayrı kimlik bilgisiyle erişir:.env.prodiçindeBACKUP_S3_ACCESS_KEY/BACKUP_S3_SECRET_KEY(isteğe bağlıBACKUP_S3_REGION). Bu kullanıcı yalnızcaBACKUP_S3_BUCKETkovasında listeleme/okuma/yazma/silme yetkisine sahip olmalıdır; uygulamanınS3_ACCESS_KEYservis kullanıcısı kullanılmaz (uygulama ele geçirilirse yedekler silinemesin). Tanımlı değilsebackup.shaçık bir hata ile durur.
Zamanlama ve izleme:
sudo bash infra/scripts/install-cron.sh(idempotent)/etc/cron.d/pushizo(03:30backup.sh, wal-g açıksa 03:00walg-backup.sh,flockile çakışma koruması) ve/etc/logrotate.d/pushizoyazar; günlük/var/log/pushizo-backup.log. Her başarılı adımBACKUP_METRICS_DIR(varsayılan/var/lib/node_exporter/textfile) altınapushizo_backup_last_success_timestamp_seconds{kind="postgres"|"storage"}yazar; node-exporter textfile collector ile okunur, 26 saati aşıncaPushizoBackupStale(critical), metrik hiç yoksaPushizoBackupMetricMissing. 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-cliimajı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)
docs/runbooks/incident.mdakışını başlat: status page'de olay aç (/admin/incidents, bileşenlerapi,delivery), Slack#pushizo-oncall.- 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). - 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 | | | | |Helm ile üretim dağıtımı (Kubernetes)
v2 Faz 16 madde 4 (docs/plan/10). Chart: infra/k8s/pushizo. Değer örnekleri: values-production.yaml, values-staging.yaml. Doğrulama: values.schema.json (tek alan) + templates/validate.yaml (alanlar arası kurallar,…
Status sayfası ve izleme
status.json ve herkese açık durum sayfası, olay (incident) yönetimi, sağlık probları, Prometheus metrikleri, alarmlar, Grafana panoları, Loki logları ve sentetik işlemsel izleme.