Бэкап Postgres в Kubernetes: pgBackRest в Helm-чарте, который реально работает
Бэкап Postgres в k8s — это та тема, где много туториалов, но мало кто проверял restore. Я три раза попадал на ситуацию, когда бэкап был, скрипт pg_dump отрабатывал по cron, файлы лежали в S3, а восстановиться было нельзя: дамп не консистентный, версии не сходятся, или вообще файл повреждён. После третьего раза я перестал использовать pg_dump для prod-бэкапа Postgres и перешёл на pgBackRest.
Расскажу, как поднять pgBackRest в k8s через CloudNativePG-оператор и регулярно проверять, что бэкапы работают. Версии — k8s 1.30, CloudNativePG 1.24, Postgres 16.
Почему не pg_dump
pg_dump делает логический дамп: SQL-инструкции, которые потом надо psql накатить на пустую базу. Это нормально для маленьких баз и миграций между мажорами, но не для бэкапа в проде:
- Долго восстанавливать. БД на 100 ГБ из дампа — несколько часов.
- Нет PITR (point-in-time-recovery). Дамп — это снимок «во столько-то», между ними можно потерять данные.
- На большой базе сам dump блокирует таблицы (даже с
--no-locksбывают сюрпризы).
Физический бэкап через WAL и базовые файлы — единственный нормальный путь для prod. Это и делает pgBackRest.
Что такое pgBackRest и почему он
pgBackRest — инструмент для бэкапа Postgres от Crunchy Data. Делает full / differential / incremental бэкап + непрерывный архив WAL. Поддерживает шифрование, параллельную загрузку, S3/GCS/Azure backends.
Альтернативы — Barman, WAL-G, простые wal-archive скрипты. WAL-G тоже хорош и часто используется в k8s, но pgBackRest в комбинации с CloudNativePG-оператором — самый «доковыряный» путь без ручного скриптинга.
CloudNativePG: оператор для Postgres в k8s
Существует несколько операторов: Zalando postgres-operator, Crunchy PGO, CloudNativePG. Я остановился на CloudNativePG (CNPG), потому что:
- Чистый CRD-first подход, без лишних обвязок.
- Встроенная поддержка pgBackRest.
- Активная разработка.
- Hot standby и failover из коробки.
Ставится одной командой:
kubectl apply --server-side -f \
https://raw.githubusercontent.com/cloudnative-pg/cloudnative-pg/release-1.24/releases/cnpg-1.24.0.yamlПоявляется namespace cnpg-system с оператором.
Кластер с бэкапом в S3
Сначала создаём bucket для WAL и base backups. Можно один — pgBackRest сам разделит по prefix.
aws s3 mb s3://company-postgres-backup --region eu-central-1Доступы. Создаёшь IAM user (или IRSA service account на EKS) с правами read/write на bucket. Кладёшь creds в Secret:
apiVersion: v1
kind: Secret
metadata:
name: postgres-backup-creds
namespace: db
type: Opaque
stringData:
ACCESS_KEY_ID: AKIA...
ACCESS_SECRET_KEY: secret...На EKS лучше IRSA: создаёшь IAM role, аннотируешь ServiceAccount, и pod без явных credentials получает доступ к S3 через STS-токен. Безопаснее, без секретов в кластере.
Теперь сам кластер:
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: postgres-prod
namespace: db
spec:
instances: 3
postgresql:
parameters:
shared_buffers: "256MB"
max_connections: "200"
work_mem: "16MB"
storage:
size: 100Gi
storageClass: gp3
backup:
barmanObjectStore:
destinationPath: s3://company-postgres-backup/postgres-prod
s3Credentials:
accessKeyId:
name: postgres-backup-creds
key: ACCESS_KEY_ID
secretAccessKey:
name: postgres-backup-creds
key: ACCESS_SECRET_KEY
wal:
compression: gzip
maxParallel: 8
data:
compression: gzip
jobs: 4
retentionPolicy: "30d"3 instance — primary + 2 replica с автоматическим failover. WAL уезжают в S3 непрерывно, базовый бэкап делается по расписанию.
Расписание бэкапов
Отдельный CRD ScheduledBackup:
apiVersion: postgresql.cnpg.io/v1
kind: ScheduledBackup
metadata:
name: postgres-prod-daily
namespace: db
spec:
schedule: "0 2 * * * *" # каждый день в 02:00
backupOwnerReference: self
cluster:
name: postgres-prod
method: barmanObjectStoreCron-формат CNPG — 6 полей (с секундами). После apply раз в сутки запускается полный бэкап. Каждые 5 минут (по умолчанию) уезжает текущий WAL-файл.
Как происходит restore
Этот пункт обязателен к проверке. Я делаю его раз в месяц на тестовом кластере. Без проверки бэкап — это не бэкап.
Recovery нового кластера из бэкапа
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: postgres-restored
namespace: db-test
spec:
instances: 1
storage:
size: 100Gi
bootstrap:
recovery:
source: postgres-prod-cluster
recoveryTarget:
targetTime: "2026-05-15 14:30:00+00"
externalClusters:
- name: postgres-prod-cluster
barmanObjectStore:
destinationPath: s3://company-postgres-backup/postgres-prod
s3Credentials:
accessKeyId:
name: postgres-backup-creds
key: ACCESS_KEY_ID
secretAccessKey:
name: postgres-backup-creds
key: ACCESS_SECRET_KEY
wal:
compression: gzipУказал targetTime — оператор найдёт последний бэкап до этого времени и докатит WAL до нужного момента. Это и есть PITR — можно восстановиться на любую секунду в пределах хранения WAL.
После apply создаётся новый Cluster, оператор поднимает init-pod, скачивает бэкап, накатывает WAL, и через несколько минут у тебя рабочая копия БД.
Проверка: что бэкап реально работает
Я каждый месяц делаю на тестовом неймспейсе:
- Apply Cluster с recovery на конкретный
targetTime. - Жду готовности (
kubectl get cluster postgres-restored -n db-testпокажет phase: Cluster in healthy state). - Подключаюсь и делаю
SELECT count(*) FROM critical_table;— сравниваю с тем, что было в проде в тот момент. - Удаляю восстановленный кластер.
На прод-объёме (200 ГБ) восстановление занимает 15-30 минут. На больших БД — час-два.
Мониторинг бэкапов
Бэкап без алёртинга — это бэкап только до первого сбоя. CNPG отдаёт метрики Prometheus, нужные:
cnpg_pg_stat_archiver_last_archived_wal— имя последнего заархивированного WAL.cnpg_pg_replication_lag— лаг репликации.- Возраст последнего бэкапа (через CRD
Backup.status.completionTime).
Алёрты:
- alert: PostgresBackupTooOld
expr: |
time() - max by (cluster) (
cnpg_collector_last_available_backup_timestamp
) > 86400 * 2
for: 30m
annotations:
summary: "Бэкап Postgres старше 2 дней"
- alert: PostgresWALArchivingStuck
expr: cnpg_collector_pg_stat_archiver_failed_count > 0
for: 15m
annotations:
summary: "WAL archiving падает с ошибками"Без таких алертов backup может «тихо» сломаться: креды S3 ротировались, оператор не заархивировал WAL за неделю, а узнаешь только при попытке восстановиться.
Что я делаю в проде
Сноска для контекста: e-commerce, БД 800 ГБ, RPO 5 минут, RTO 30 минут.
- Полный бэкап раз в день в 02:00 UTC.
- Differential раз в 6 часов (для быстрого восстановления).
- WAL архивируется непрерывно с интервалом 5 минут.
- Retention 30 дней (compliance требует 14, я с запасом).
- Раз в месяц — drill восстановления на тестовый namespace, ручной grep по
SELECT count(*)ключевых таблиц. - Раз в полгода — полное восстановление в отдельный кластер, со снятием нагрузки от приложений и сверкой checksums.
Что запомнить
pg_dump — для миграций и маленьких БД, не для prod-бэкапа. pgBackRest через CloudNativePG-оператор — рабочий путь без скриптинга. WAL непрерывно в S3, base backup по расписанию, retention 30 дней. Главное — регулярно проверять restore. Если ты не восстанавливал бэкап последние полгода, считай что у тебя бэкапа нет.
Куда копать: документация CloudNativePG, сайт pgBackRest, и обязательно — пройди руками сценарий PITR на тестовой БД, прежде чем доверять ему в проде.