feat(ops): DR-тест восстановления бэкапа PG + runbook HA/DR

Закрыта слепая зона «бэкапы есть, но восстановление не проверялось».

- scripts/ops/pg_restore_verify.sh — берёт последний дамп из MinIO backups/pg/,
  restore в ЭФЕМЕРНЫЙ postgres:16, проверяет ключевые таблицы (users/documents/
  tasks). Прод не трогает, всё в одноразовом контейнере. Под cron раз в неделю.
- docs/DR-HA.md — runbook: бэкапы, проверка восстановления, потоковая репликация
  PG, Redis-реплика+Sentinel, таблица SPOF со статусом митигаций.

Провижн реплик PG/Redis — на Proxmox (нужен новый LXC), это работа на железе, не
в репозитории; runbook даёт конкретные шаги. Векторный SPOF уже снимается Qdrant.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
jze9
2026-08-11 20:40:03 +05:00
parent d2d99d8231
commit d32a07d0a8
2 changed files with 126 additions and 0 deletions

View File

@@ -0,0 +1,71 @@
#!/usr/bin/env bash
# DR-тест: проверка ВОССТАНОВИМОСТИ бэкапа PostgreSQL.
#
# Берёт последний дамп из MinIO (backups/pg/), поднимает ЭФЕМЕРНЫЙ контейнер
# postgres:16, восстанавливает в него дамп и проверяет, что ключевые таблицы не
# пусты. Прод не трогается — всё в одноразовом контейнере/сети, удаляются на выходе.
#
# Зачем: бэкапы делаются (cron на 1.38), но восстановление раньше никто не проверял —
# «непроверенный бэкап = отсутствие бэкапа». Запускать по cron раз в неделю.
#
# Требуется docker + доступ до MinIO. Креды берутся из .env (или окружения):
# MINIO_ENDPOINT, MINIO_ACCESS_KEY, MINIO_SECRET_KEY,
# MINIO_BUCKET_BACKUPS (=backups), POSTGRES_DB, POSTGRES_USER.
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
if [ -f "$ROOT/.env" ]; then set -a; . "$ROOT/.env"; set +a; fi
: "${MINIO_ENDPOINT:?нужен MINIO_ENDPOINT}"
: "${MINIO_ACCESS_KEY:?нужен MINIO_ACCESS_KEY}"
: "${MINIO_SECRET_KEY:?нужен MINIO_SECRET_KEY}"
BUCKET="${MINIO_BUCKET_BACKUPS:-backups}"
PREFIX="${PG_BACKUP_PREFIX:-pg/}"
PGDB="${POSTGRES_DB:-antiplagiator}"
PGUSER="${POSTGRES_USER:-antiplagiator}"
SUFFIX="$$-$RANDOM"
PG="drtest-pg-$SUFFIX"
WORK="$(mktemp -d)"
MC_HOST="http://${MINIO_ACCESS_KEY}:${MINIO_SECRET_KEY}@${MINIO_ENDPOINT}"
cleanup() { docker rm -f "$PG" >/dev/null 2>&1 || true; rm -rf "$WORK"; }
trap cleanup EXIT
echo "▶ Поиск последнего дампа в minio://$BUCKET/$PREFIX"
LATEST="$(docker run --rm -e MC_HOST_dr="$MC_HOST" minio/mc:latest \
sh -c "mc ls --recursive dr/$BUCKET/$PREFIX | awk '{print \$NF}' | grep -E '\.(sql\.)?gz$' | sort | tail -1")"
[ -n "$LATEST" ] || { echo "❌ дампы не найдены"; exit 1; }
FNAME="$(basename "$LATEST")"
echo " последний: $FNAME"
echo "▶ Скачивание дампа"
docker run --rm -e MC_HOST_dr="$MC_HOST" -v "$WORK":/w minio/mc:latest \
sh -c "mc cp 'dr/$BUCKET/$PREFIX$FNAME' /w/dump.gz"
echo "▶ Эфемерный postgres:16"
docker run -d --name "$PG" \
-e POSTGRES_PASSWORD=drtest -e POSTGRES_DB="$PGDB" -e POSTGRES_USER="$PGUSER" \
postgres:16 >/dev/null
for _ in $(seq 1 30); do
docker exec "$PG" pg_isready -U "$PGUSER" -d "$PGDB" >/dev/null 2>&1 && break
sleep 1
done
echo "▶ Восстановление"
gunzip -c "$WORK/dump.gz" | docker exec -i "$PG" psql -v ON_ERROR_STOP=0 -U "$PGUSER" -d "$PGDB" >/dev/null 2>&1
echo "▶ Проверка ключевых таблиц"
rc=0
for tbl in users documents tasks; do
n="$(docker exec "$PG" psql -U "$PGUSER" -d "$PGDB" -tAc "SELECT count(*) FROM $tbl" 2>/dev/null | tr -d '[:space:]' || echo ERR)"
echo " $tbl: ${n:-ERR}"
{ [ -z "$n" ] || [ "$n" = "ERR" ]; } && rc=1
done
if [ "$rc" -eq 0 ]; then
echo "✅ Бэкап восстанавливается, ключевые таблицы на месте ($FNAME)"
else
echo "❌ Восстановление не прошло проверку — разобраться СРОЧНО"
exit 1
fi