Если у вас нет тестируемой стратегии бэкапа — у вас нет бэкапа. У бота в MAX это особенно болезненно: один уронил миграцию — потерял историю диалогов 50 000 пользователей; забыл продлить S3-ключ — три недели нет копий; не проверил восстановление — обнаружил, что pg_dump делался без --clean и при ресторе всё конфликтует. В этой статье разберём прикладную стратегию backup для бота MAX: что и как часто бэкапить (PostgreSQL, Redis, медиа), куда складывать (Yandex Object Storage, MinIO, Selectel), как шифровать, как тестировать восстановление, и какие RTO/RPO выбирать под реальный SLA.
Что бэкапить в боте MAX
Минимальный список:
| Источник | Что внутри | Критичность | RPO | Метод |
|---|---|---|---|---|
| PostgreSQL | пользователи, FSM-state, заказы, платежи, логи диалогов | Критично | 5–15 мин | WAL archiving + base backup |
| Redis | кеши, rate limit, сессии, FSM (если в Redis) | Среднее | 1 час | RDB snapshot + AOF |
| Object storage медиа | присланные пользователями фото, документы | Среднее | 1 сутки | репликация бакета |
| Конфиги (.env, secrets) | bot_token, API-ключи, secret_token | Критично | при изменении | Vault snapshot |
| Код | git | — | — | git remote |
| Метрики (Prometheus) | history алертов | Низкое | 1 неделя | snapshot |
RPO — recovery point objective, сколько данных мы готовы потерять. RTO — recovery time objective, как быстро поднимаемся.
PostgreSQL: pg_basebackup + WAL archiving
pg_dump хорош для миграций и dev, но для прода — только pg_basebackup + непрерывный архив WAL. Это даёт PITR (point-in-time recovery): восстановление на любую секунду.
postgresql.conf:
wal_level = replica
archive_mode = on
archive_command = 'aws --endpoint-url=https://storage.yandexcloud.net s3 cp %p s3://botmax-wal/%f'
archive_timeout = 300 # принудительный switch WAL раз в 5 минут
max_wal_senders = 3
wal_keep_size = 2GB
База бэкапа — раз в сутки:
#!/bin/bash
set -euo pipefail
DATE=$(date +%F)
DEST="/var/backups/pg/base-$DATE"
pg_basebackup -h localhost -U replicator -D "$DEST" -Ft -z -Xs -P
aws --endpoint-url=https://storage.yandexcloud.net s3 sync \
"$DEST" "s3://botmax-pg-base/$DATE/" \
--storage-class COLD \
--sse AES256
rm -rf "$DEST"
Lifecycle policy в Object Storage: STANDARD 7 дней → COLD 30 дней → удаление через 90 дней. Это закрывает ретроспективу 3 месяца.
Recovery: разворот PostgreSQL из base + WAL
Сценарий: вчера вечером кто-то выполнил DELETE FROM users WHERE created_at < '2026-01-01'. Хотим откатить на 18:30.
# 1. Стоп Postgres
systemctl stop postgresql
# 2. Сохранить текущее состояние (для разбора инцидента)
mv /var/lib/postgresql/16/main /var/lib/postgresql/16/main.broken
# 3. Скачать base backup (последний до 18:30)
mkdir -p /var/lib/postgresql/16/main
aws s3 sync s3://botmax-pg-base/2026-01-19/ /var/lib/postgresql/16/main/
tar -xzf /var/lib/postgresql/16/main/base.tar.gz -C /var/lib/postgresql/16/main/
# 4. recovery.signal + restore_command
cat > /var/lib/postgresql/16/main/postgresql.auto.conf <<EOF
restore_command = 'aws s3 cp s3://botmax-wal/%f %p'
recovery_target_time = '2026-01-19 18:30:00 MSK'
recovery_target_action = 'promote'
EOF
touch /var/lib/postgresql/16/main/recovery.signal
# 5. Старт
chown -R postgres:postgres /var/lib/postgresql/16/main
systemctl start postgresql
Проверка: SELECT pg_is_in_recovery(); → f после promote.
Redis: RDB + AOF
Redis в боте обычно хранит rate limit, временные сессии, FSM-state. Что критично — переезжает в PostgreSQL. Что не критично — RDB-снапшот раз в час хватит.
redis.conf:
save 900 1
save 300 10
save 60 10000
dir /var/lib/redis
dbfilename dump.rdb
appendonly yes
appendfsync everysec
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
Скрипт бэкапа Redis в S3:
#!/bin/bash
set -e
TS=$(date +%F-%H%M)
redis-cli BGSAVE
sleep 5
while [ "$(redis-cli LASTSAVE)" -lt "$(date +%s -d '1 minute ago')" ]; do
sleep 1
done
gpg --batch --yes --passphrase-file /etc/backup/gpg.pass --symmetric --cipher-algo AES256 \
-o "/tmp/dump-$TS.rdb.gpg" /var/lib/redis/dump.rdb
aws --endpoint-url=https://storage.yandexcloud.net s3 cp \
"/tmp/dump-$TS.rdb.gpg" "s3://botmax-redis/dump-$TS.rdb.gpg"
rm "/tmp/dump-$TS.rdb.gpg"
Шифрование на стороне клиента
Хранение чувствительных бэкапов в S3 безопасно «по умолчанию», но для ПДн (152-ФЗ) нужно шифрование на стороне клиента — ключ известен только вам:
# Шифрование AES-256 через gpg
gpg --batch --yes --passphrase-file /etc/backup/gpg.pass \
--symmetric --cipher-algo AES256 \
-o backup.tar.gz.gpg backup.tar.gz
# Расшифровка
gpg --batch --yes --passphrase-file /etc/backup/gpg.pass \
-o backup.tar.gz -d backup.tar.gz.gpg
GPG-пароль храните в Vault / KMS / SOPS. Не в том же S3, где бэкапы. Иначе теряет смысл.
Тестирование восстановления
Самая частая ошибка — backup делается, но восстановление никто не пробовал. К моменту реального инцидента выясняется, что pg_basebackup за полгода сменил формат, S3-ключ протух, или на recovery-сервере не хватает места.
Минимум — раз в месяц, автоматизированный «restore drill»:
#!/bin/bash
# Раз в месяц cron поднимает временный контейнер,
# восстанавливает последний бэкап, прогоняет smoke test
docker run -d --name pg-restore-test \
-e POSTGRES_PASSWORD=test postgres:16
# Скачать последний base + WAL
LATEST=$(aws s3 ls s3://botmax-pg-base/ | tail -1 | awk '{print $4}')
aws s3 sync "s3://botmax-pg-base/$LATEST" /tmp/restore/
docker exec pg-restore-test bash -c "
pg_restore -d postgres -h localhost -U postgres /tmp/restore/base.tar
psql -c 'SELECT count(*) FROM users' | grep -E '^[0-9]+$'
"
# Сравнить количество строк с ожидаемым
RESTORED=$(docker exec pg-restore-test psql -t -c "SELECT count(*) FROM users")
EXPECTED=$(psql -h prod -t -c "SELECT count(*) FROM users")
DIFF=$((EXPECTED - RESTORED))
if [ "$DIFF" -gt 100 ]; then
/usr/local/bin/alert "Restore drill failed: diff=$DIFF rows"
fi
docker rm -f pg-restore-test
Бэкап медиа в Object Storage
Если бот хранит присланные пользователями фото/документы в S3-совместимом хранилище — настройте репликацию бакетов между регионами:
- Источник:
botmax-mediaв Yandex Cloud, регионru-central1 - Назначение:
botmax-media-drвru-central1-b(другая зона)
Для критичных данных (медицинский бот, юридические документы) — реплика в другой провайдер: например, основной — Yandex Object Storage, реплика — Selectel S3. Это страхует от инцидентов на уровне облака.
Структура бэкапа
s3://botmax-backups/
├── pg/
│ ├── base/
│ │ ├── 2026-01-15/
│ │ ├── 2026-01-16/
│ │ └── ...
│ └── wal/
│ ├── 000000010000000000000001
│ └── ...
├── redis/
│ └── dump-2026-01-19-1200.rdb.gpg
├── vault/
│ └── snapshot-2026-01-19.snap.gpg
└── config/
└── ansible-2026-01-19.tar.gz.gpg
Ретеншн:
- WAL — 14 дней (для PITR на 2 недели);
- base — ежедневный 30 дней + еженедельный 12 недель + ежемесячный 12 месяцев;
- Redis RDB — 7 дней;
- Vault snapshot — 90 дней.
Мониторинг бэкапа
Без алертов «бэкап не сделался» легко прожить полгода без копий. Минимум:
- Prometheus exporter, который раз в час проверяет timestamp последнего файла в S3;
- Алерт «backup older than 25 hours» (для дневных) и «WAL not archived 30 min» (для непрерывных);
- Дашборд: размер последнего бэкапа vs средний (резкий рост — заполнили всё, резкое падение — что-то отвалилось);
- Smoke test после каждого восстановления.
# Prometheus alert
- alert: BackupTooOld
expr: time() - max(s3_object_last_modified_timestamp{bucket="botmax-pg-base"}) > 90000
for: 5m
labels:
severity: critical
annotations:
summary: "PostgreSQL base backup older than 25 hours"
RPO/RTO — реалистичные цифры
Для среднего коммерческого бота в MAX (выручка через бота 100K–10M ₽/мес):
| Сценарий | RPO | RTO | Стоимость инфраструктуры |
|---|---|---|---|
| Корпоративный/HR бот | 24 ч | 4 ч | 500–2 000 ₽/мес |
| Магазин, запись на услуги | 1 ч | 1 ч | 2 000–5 000 ₽/мес |
| Платежи, медкабинет | 5 мин | 30 мин | 5 000–15 000 ₽/мес |
| Финтех, биржа | < 1 мин | < 5 мин | 30 000+ ₽/мес |
Снижение RPO с 1 ч до 5 мин требует WAL archiving + быстрый network до S3 (часто канал в Object Storage в том же регионе).
Disaster recovery: full failover
Если упал весь регион (редко, но бывает): runbook должен быть написан и прорепетирован.
- Поднимаем PostgreSQL на standby в другом регионе (Patroni / managed service).
- Расшифровываем последний base + WAL до точки сбоя.
- Поднимаем Redis из RDB.
- Меняем DNS A-запись бэкенда (TTL 60 секунд!).
- Перерегистрируем webhook на новый URL через
setWebhook. - Smoke test: тестовое сообщение боту → проверка ответа.
Цель — RTO < 1 час. Достигается только тренировкой команды раз в квартал.
Защита от ransomware и compromised credentials
Объёмный риск 2026 года — атакующий получает доступ к S3 и удаляет все бэкапы. Защита:
- Object Lock / WORM на бакете бэкапов: файлы нельзя удалить или перезаписать в течение N дней.
- Версионирование бакета: даже delete оставляет старую версию.
- Отдельный read-only ключ для бэкап-сервера → запись разрешена, удаление запрещено через bucket policy.
- Air-gapped копия раз в неделю — на оффлайн-носитель / в другой облачный аккаунт.
- MFA на удаление для админ-консоли.
Итого
Backup бота MAX строится на стеке: PostgreSQL с WAL archiving + ежедневным pg_basebackup, Redis RDB + AOF раз в час, медиа через репликацию S3-бакетов в другую зону, Vault snapshot для секретов. Всё шифруется на стороне клиента (gpg AES-256), складывается в S3-совместимое хранилище с lifecycle policy и Object Lock. Восстановление тестируется раз в месяц автоматически. Алерты на «backup older than 25h» и «WAL not archived 30 min» в Prometheus. RPO 5–15 минут и RTO 30 минут — реальная цель для коммерческого бота за 5–15 тыс. ₽/мес. Главное правило: незатестированный бэкап не считается бэкапом.
Частые вопросы
Чем pg_basebackup лучше pg_dump для бэкапа PostgreSQL?
pg_dump делает логический бэкап в виде SQL-скрипта или бинарного дампа — он удобен для миграций и dev, но восстановление крупной базы занимает часы. pg_basebackup делает физическую копию файлов кластера, поверх неё разворачивается WAL для PITR. Это даёт восстановление на любую секунду, а не только на момент дампа. Для прода с RPO < 1 часа выбор однозначный — pg_basebackup + непрерывный архив WAL. pg_dump оставьте для миграций между версиями и переноса схемы.
Зачем шифровать бэкап на стороне клиента, если S3 уже шифрует?
Server-side encryption (SSE) защищает только от физической кражи дисков провайдера — данные расшифровываются автоматически любым, у кого есть ключ доступа к бакету. Если ключ AWS-API утечёт через скомпрометированный CI или сотрудника — атакующий скачает все бэкапы в plain. Client-side шифрование (gpg AES-256 с паролем в Vault) добавляет второй слой: даже с доступом к бакету без passphrase ничего не получить. Для ПДн по 152-ФЗ это фактически обязательно.
Как часто тестировать восстановление?
Минимум — раз в месяц автоматический restore drill: cron поднимает временный контейнер, скачивает последний бэкап, восстанавливает, прогоняет smoke test (сравнение количества строк в ключевых таблицах с продом). Раз в квартал — ручной DR-учения с командой: имитируем падение региона, поднимаем всё в DR-окружении, замеряем фактический RTO. Без этого реальное восстановление в кризисе превращается в гадание — формат бэкапа изменился, ключ протух, на сервере не хватает места.
Какой RPO/RTO выбрать для бота с записью на услуги?
Для коммерческого бота в MAX с записью на услуги (салон, клиника, фитнес) реалистичные цели — RPO 1 час и RTO 1 час. Это означает WAL archiving раз в час, ежедневный base backup, отдельный standby Postgres в горячем резерве. Стоимость инфраструктуры 2–5 тыс. ₽/мес. Если в боте есть платежи и хранение медданных — RPO 5–15 минут и RTO 30 минут с непрерывным WAL archiving в S3 и автоматическим failover через Patroni.
Как защитить бэкапы от удаления при компрометации?
Включайте Object Lock (WORM-режим) на бакете бэкапов — файлы нельзя удалить N дней. Добавьте версионирование, чтобы даже delete оставлял предыдущую версию. Доступ для бэкап-сервера выдавайте отдельным IAM-ключом с правами put-object и list, без delete-object. Раз в неделю делайте air-gapped копию в отдельный облачный аккаунт или физический оффлайн-носитель. Для админ-консоли S3 включайте MFA. Ransomware-сценарий 2026 года стал слишком частым, чтобы игнорировать эту угрозу.
Где хранить ключи шифрования бэкапов?
Не в том же бакете, что бэкапы — это убивает смысл шифрования. Варианты: HashiCorp Vault, Yandex KMS, SOPS-encrypted файл в отдельном приватном git-репозитории, аппаратный YubiKey. Доступ к ключу — только сервис-аккаунту бэкап/восстановления и 1–2 админам через MFA. Дополнительно — Shamir's Secret Sharing: ключ разбит на 5 частей, для восстановления нужно 3 — защищает от потери одного админа и от компрометации одного источника.
Нужно ли бэкапить Redis, если данные в нём временные?
Зависит от того, что вы кладёте в Redis. Если только rate limit, кеш и короткие сессии — потеря не критична, бэкап не нужен. Если в Redis FSM-state открытых диалогов, незавершённые корзины, временные коды двухфакторки — RDB snapshot раз в час + AOF с appendfsync everysec даёт RPO 1 секунду. Лучшая стратегия — критичные данные (заказы, согласия ПДн, платежи) хранить в PostgreSQL, а Redis оставить под действительно временные структуры.