
Сбой диска, ошибка в миграции, баг в коде, удаление данных пользователем — всё это не «если», а «когда». Бэкапы снижают риск, но пользу приносят только тогда, когда:
Разберёмся без лишней теории: что означают RPO/RTO на практике, как построить работающую схему для PostgreSQL, S3/MinIO и Kubernetes, как зашифровать и удешевить хранение, и как проверять восстановление «в боевых условиях» — так, чтобы простои не съедали выручку.
Эти величины надо привязать к деньгам: сколько стоит минута простоя и минута потери данных. Так проще принимать решения о частоте бэкапов, типах хранилищ и автоматизации.
Важно: бэкап без понятной процедуры восстановления — это не бэкап, а самоуспокоение. Дальше — конкретика.
Для Postgres надёжная схема — полные бэкапы + архивирование WAL (журналов транзакций) и восстановление до нужного момента (PITR). Инструменты: WAL‑G или pgBackRest. Возьмём WAL‑G: он простой, быстрый, понимает S3 и шифрование.
Предположим, у нас Postgres 14+, доступ к S3/MinIO с версионированием и ключ шифрования в KMS.
# Устанавливаем wal-g (пример для Debian/Ubuntu)
curl -L https://github.com/wal-g/wal-g/releases/download/v2.0.1/wal-g.linux-amd64.tar.gz | sudo tar -xz -C /usr/local/bin
# Переменные окружения (поместите в /etc/wal-g/env или systemd unit)
export WALG_S3_PREFIX="s3://my-backups/postgres-main"
export AWS_REGION="eu-central-1"
export AWS_ACCESS_KEY_ID="AKIA..."
export AWS_SECRET_ACCESS_KEY="..."
export WALG_S3_SSE_KMS_ID="arn:aws:kms:eu-central-1:123456789012:key/abcd-efgh..." # шифрование на стороне S3/KMS
export WALG_COMPRESSION_METHOD="brotli"
export PGDATA="/var/lib/postgresql/14/main"
export PGPASSWORD="supersecret"
export PGHOST="localhost"
export PGUSER="postgres"
# Включаем archiving в postgresql.conf
# (пример командой; можно отредактировать файл вручную)
psql -U postgres -c "ALTER SYSTEM SET archive_mode = 'on';"
psql -U postgres -c "ALTER SYSTEM SET archive_command = 'wal-g wal-push %p';"
psql -U postgres -c "ALTER SYSTEM SET wal_level = 'replica';"
psql -U postgres -c "SELECT pg_reload_conf();"
# Делаем базовый полный бэкап
wal-g backup-push "$PGDATA"
# Проверяем список бэкапов
wal-g backup-list
Cron для регулярных full и очистки старых:
# /etc/cron.d/pg-backup
# Full бэкап раз в сутки в 02:10
10 2 * * * postgres . /etc/wal-g/env && /usr/local/bin/wal-g backup-push /var/lib/postgresql/14/main >> /var/log/wal-g.log 2>&1
# Тримминг старых бэкапов: храним 7 штук
30 3 * * * postgres . /etc/wal-g/env && /usr/local/bin/wal-g delete retain FULL 7 --confirm >> /var/log/wal-g.log 2>&1
Важно: архивирование WAL идёт постоянно через archive_command — это даёт RPO в минуты.
Процедура на «чистой» машине:
# Останавливаем Postgres и чистим PGDATA
sudo systemctl stop postgresql
sudo -u postgres rm -rf /var/lib/postgresql/14/main/*
# Восстанавливаем последнюю базу
sudo -u postgres env -i bash -c '
source /etc/wal-g/env
/usr/local/bin/wal-g backup-fetch /var/lib/postgresql/14/main LATEST
'
# Указываем желаемое время (или метку) в recovery.signal
# Для Postgres 12+ используется параметр в postgresql.auto.conf или файл recovery.signal
sudo -u postgres bash -c "echo "restore_command = 'wal-g wal-fetch %f %p'" >> /var/lib/postgresql/14/main/postgresql.auto.conf"
# Восстановление, например, на момент за 2 минуты до инцидента
sudo -u postgres bash -c "echo "recovery_target_time = '2026-03-30 12:15:00+00'" >> /var/lib/postgresql/14/main/postgresql.auto.conf"
sudo systemctl start postgresql
Проверяйте: правильные пользователи/расширения, последовательности не «убежали», отчёты сходятся.
Скрипт, который разворачивает временный Postgres, восстанавливает «вчерашний» бэкап и гоняет SQL‑проверки. Запускайте по расписанию.
#!/usr/bin/env bash
set -euo pipefail
WHEN=$(date -u -d "yesterday" +"%Y-%m-%d 03:00:00+00")
TMP=/var/lib/postgresql/restore-check
systemctl stop postgresql || true
rm -rf "$TMP" && mkdir -p "$TMP"
# Восстанавливаем в TMP
sudo -u postgres env -i bash -c '
source /etc/wal-g/env
export PGDATA="'$TMP'"
/usr/local/bin/wal-g backup-fetch "$PGDATA" LATEST
echo "restore_command = 'wal-g wal-fetch %f %p'" >> "$PGDATA"/postgresql.auto.conf
echo "recovery_target_time = '"'$WHEN'"'" >> "$PGDATA"/postgresql.auto.conf
pg_ctl -D "$PGDATA" -o "-p 55432" start
'
# Простая проверка
sleep 5
psql -h localhost -p 55432 -U postgres -c "SELECT current_database(), now();"
# Бизнес-проверки: есть ли вчерашние заказы
psql -h localhost -p 55432 -U postgres -d app -c "SELECT count(*) FROM orders WHERE created_at::date = (CURRENT_DATE - 1);"
# Остановка тестового инстанса
sudo -u postgres pg_ctl -D "$TMP" stop
rm -rf "$TMP"
echo "restore check ok"
Если вы храните пользовательские файлы в S3/MinIO, «бэкап» — это правильно настроенное ведро (bucket): включённое версионирование, правила жизненного цикла, возможно — репликация в другой регион/аккаунт.
# Пример для AWS S3
provider "aws" {
region = "eu-central-1"
}
resource "aws_s3_bucket" "files" {
bucket = "myapp-prod-files"
force_destroy = false
}
resource "aws_s3_bucket_versioning" "files" {
bucket = aws_s3_bucket.files.id
versioning_configuration {
status = "Enabled"
}
}
# Object Lock (WORM) — требует создание ведра со включенным lock заранее!
resource "aws_s3_bucket_object_lock_configuration" "files" {
bucket = aws_s3_bucket.files.id
rule {
default_retention {
mode = "COMPLIANCE"
days = 7
}
}
}
resource "aws_s3_bucket_server_side_encryption_configuration" "files" {
bucket = aws_s3_bucket.files.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "aws:kms"
kms_master_key_id = "arn:aws:kms:eu-central-1:123456789012:key/abcd-efgh..."
}
}
}
resource "aws_s3_bucket_lifecycle_configuration" "files" {
bucket = aws_s3_bucket.files.id
rule {
id = "noncurrent-to-glacier"
status = "Enabled"
noncurrent_version_transition {
noncurrent_days = 30
storage_class = "GLACIER"
}
noncurrent_version_expiration {
noncurrent_days = 365
}
}
}
Пример скрипта проверки последних бэкапов и метрики для Prometheus (через текстовый экспорт):
#!/usr/bin/env bash
# s3_backup_age.sh — выводит возраст последнего бэкапа в секундах
set -euo pipefail
BUCKET="my-backups"
PREFIX="postgres-main/basebackups_005"
OUT="/var/lib/node_exporter/textfile_collector/backup_age.prom"
LATEST=$(aws s3api list-objects-v2 --bucket "$BUCKET" --prefix "$PREFIX" --query 'reverse(sort_by(Contents,&LastModified))[:1].LastModified' --output text)
if [ "$LATEST" = "None" ]; then
echo "backup_age_seconds -1" > "$OUT"
exit 1
fi
TS=$(date -d "$LATEST" +%s)
NOW=$(date +%s)
AGE=$((NOW-TS))
echo "backup_age_seconds $AGE" > "$OUT"
Заведите алерт: если backup_age_seconds > 36 часов — тревога.
В Kubernetes «приложение» — это манифесты + секреты + тома (PersistentVolume). Удобный инструмент — Velero: снимает «снимки» объектов API и может бэкапить тома через плагины.
# Установка в кластер (пример c AWS и S3)
velero install \
--provider aws \
--plugins velero/velero-plugin-for-aws:v1.8.0 \
--bucket my-k8s-backups \
--secret-file ./credentials-velero \
--backup-location-config region=eu-central-1,s3ForcePathStyle=true,s3Url=https://s3.eu-central-1.amazonaws.com \
--snapshot-location-config region=eu-central-1
# Создать ежедневный бэкап всего неймспейса prod, хранить 14 дней
kubectl apply -f - <<'YAML'
apiVersion: velero.io/v1
kind: Schedule
metadata:
name: daily-prod
namespace: velero
spec:
schedule: "0 3 * * *"
template:
includedNamespaces:
- prod
storageLocation: default
ttl: 336h # 14 дней
YAML
velero restore create --from-backup daily-prod-20260330 --wait
Минимум раз в месяц проводите «пожарную тренировку»:
Полезно: хранить чек‑листы и «шпаргалки» (runbook) в репозитории рядом с кодом.
Оптимизировать расходы помогают: хранение инкрементальных бэкапов, перевод старых версий в «холодные» классы (Glacier/архив), дедупликация, сжатие (brotli/zstd), агрессивные политики удаления тестовых окружений после restore‑тренировок.
Чек‑лист:
Частые ошибки:
Бэкап — это сервис, а не папка с файлами. Сформулируйте RPO/RTO на языке денег, постройте схему «Postgres PITR + версионированный S3 + Velero для K8s», включите шифрование и неизменяемость, автоматизируйте проверки восстановления и заведите метрики. Тогда любая авария превратится в понятную процедуру с прогнозируемым временем и стоимостью — без сюрпризов для клиентов и выручки.