fix(ops): бэкап без выгрузки в S3 падает, а не рапортует успех #3086
6 changed files with 122 additions and 18 deletions
|
|
@ -24,10 +24,12 @@
|
|||
# Usage (cron — note `bash`, not a bare path, so +x is irrelevant):
|
||||
# 30 3 * * * bash /opt/gendesign/ops/backup.sh >> /var/log/gendesign-backup.log 2>&1
|
||||
#
|
||||
# Optional S3 upload — set these in /etc/default/gendesign-backup (root-owned,
|
||||
# chmod 600, NOT in git). Without them, dumps stay local only:
|
||||
# S3 upload — REQUIRED by default (#3085). Set these in /etc/default/gendesign-backup
|
||||
# (root-owned, chmod 600, NOT in git). Without all four, the script fails
|
||||
# LOUDLY before taking a dump — set BACKUP_ALLOW_LOCAL_ONLY=1 to explicitly
|
||||
# accept a local-only backup instead (e.g. local testing):
|
||||
# S3_ENDPOINT=https://s3.ru-1.storage.selcloud.ru # Selectel S3
|
||||
# S3_BUCKET=gendesign-backups
|
||||
# S3_BUCKET=gendsgn-backups
|
||||
# S3_ACCESS_KEY=...
|
||||
# S3_SECRET_KEY=...
|
||||
# A redacted template lives at ops/gendesign-backup.default.example.
|
||||
|
|
@ -64,6 +66,28 @@ SENTINEL_FILE="${SENTINEL_FILE:-${LOCAL_BACKUP_DIR}/.last_success}"
|
|||
# Optional S3 env. Loaded from /etc/default/gendesign-backup if present.
|
||||
[[ -f /etc/default/gendesign-backup ]] && source /etc/default/gendesign-backup
|
||||
|
||||
# --- guard: S3 upload is REQUIRED by default (#3085). A backup that only
|
||||
# lives on the same disk as the DB it protects is not a backup once
|
||||
# Poincare has no configured fallback host to copy from — before this
|
||||
# guard, a missing/incomplete /etc/default/gendesign-backup made the
|
||||
# script log "backup stays local only" and exit 0, indistinguishable
|
||||
# from a real off-box backup in the cron log. Fail LOUD, before the dump
|
||||
# is even taken (no point spending time/disk on a dump that can't leave
|
||||
# the box). BACKUP_ALLOW_LOCAL_ONLY=1 is the explicit escape hatch for
|
||||
# running on a box without S3 creds on purpose (e.g. local testing) —
|
||||
# it still warns loudly so it can't be forgotten silently. ---
|
||||
if [[ -z "${S3_ENDPOINT:-}" || -z "${S3_BUCKET:-}" || -z "${S3_ACCESS_KEY:-}" || -z "${S3_SECRET_KEY:-}" ]]; then
|
||||
if [[ "${BACKUP_ALLOW_LOCAL_ONLY:-0}" == "1" ]]; then
|
||||
log "WARNING: S3 vars not fully set — BACKUP_ALLOW_LOCAL_ONLY=1, proceeding with a LOCAL-ONLY backup. This copy will NOT survive loss of this VM." >&2
|
||||
BACKUP_LOCAL_ONLY_USED=1
|
||||
else
|
||||
log "ERROR: S3 upload is not configured — missing one or more of S3_ENDPOINT / S3_BUCKET / S3_ACCESS_KEY / S3_SECRET_KEY." >&2
|
||||
log "ERROR: fill them into /etc/default/gendesign-backup (see ops/gendesign-backup.default.example)," >&2
|
||||
log "ERROR: or set BACKUP_ALLOW_LOCAL_ONLY=1 to explicitly accept a local-only backup." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Integrity check beyond "non-empty"/"big enough": a dump truncated mid-write
|
||||
# (disk full, OOM-kill, docker exec dropped) can still gzip into a structurally
|
||||
# valid, non-tiny .gz — neither `-s` nor MIN_DUMP_BYTES catch that reliably.
|
||||
|
|
@ -161,7 +185,8 @@ fi
|
|||
|
||||
log "Globals dump OK: ${globals_out} ($(du -h "$globals_out" | cut -f1))"
|
||||
|
||||
# --- optional S3 upload (only if all four vars present) — both the main dump
|
||||
# --- S3 upload (only if all four vars present — guaranteed unless the
|
||||
# BACKUP_ALLOW_LOCAL_ONLY escape hatch above was used) — both the main dump
|
||||
# and the globals dump go up, same bucket, same layout. ---
|
||||
if [[ -n "${S3_ENDPOINT:-}" && -n "${S3_BUCKET:-}" && -n "${S3_ACCESS_KEY:-}" && -n "${S3_SECRET_KEY:-}" ]]; then
|
||||
for f in "$out" "$globals_out"; do
|
||||
|
|
@ -212,5 +237,16 @@ log "Backup done. Local dumps retained: ${#remaining[@]} data + ${#remaining_glo
|
|||
# guard above (dump/globals integrity, retention, and — since this whole
|
||||
# script runs under `set -euo pipefail` — any S3 upload) has passed. See
|
||||
# ops/check-backup-staleness.sh for the piece that actually alerts if
|
||||
# this stops being refreshed. ---
|
||||
write_sentinel "$SENTINEL_FILE"
|
||||
# this stops being refreshed. A LOCAL-ONLY run (BACKUP_ALLOW_LOCAL_ONLY=1
|
||||
# escape hatch above) does NOT count as a verified backup for this
|
||||
# purpose — writing the sentinel here would leave the staleness watchdog
|
||||
# permanently green while the box has no off-box copy at all (#3085
|
||||
# reopened via the escape hatch). Instead: skip the sentinel (watchdog
|
||||
# alerts within max_age_hours) and notify() immediately so it isn't only
|
||||
# a WARNING buried in a log nobody tails. ---
|
||||
if [[ "${BACKUP_LOCAL_ONLY_USED:-0}" == "1" ]]; then
|
||||
log "Skipping sentinel write — this was a LOCAL-ONLY backup (BACKUP_ALLOW_LOCAL_ONLY=1), not a verified off-box backup." >&2
|
||||
notify "⚠️ gendesign main backup: LOCAL-ONLY run (S3 not configured, BACKUP_ALLOW_LOCAL_ONLY=1). Dump ${out} is NOT off-box. Sentinel NOT updated — staleness watchdog will alert if this isn't fixed."
|
||||
else
|
||||
write_sentinel "$SENTINEL_FILE"
|
||||
fi
|
||||
|
|
|
|||
|
|
@ -1,9 +1,20 @@
|
|||
# crontab ПРИНИМАЮЩЕГО хоста (Selectel Poincare, 188.246.224.93) — после 30.08.
|
||||
#
|
||||
# Устанавливать СРАЗУ после того, как стек поднялся и смоук прошёл:
|
||||
# mkdir -p /opt/gendesign/logs
|
||||
# crontab /opt/gendesign/ops/crontab-poincare.cron
|
||||
# crontab -l # сверить
|
||||
#
|
||||
# Перед установкой в /etc/default/ должны лежать (root-owned, chmod 600,
|
||||
# НЕ в git, переносятся руками с предыдущего хоста или создаются заново):
|
||||
# /etc/default/gendesign-backup — S3_ENDPOINT/S3_BUCKET/S3_ACCESS_KEY/S3_SECRET_KEY
|
||||
# (см. ops/gendesign-backup.default.example)
|
||||
# /etc/default/tradein-backup — свои S3_*, либо отсутствует (тогда
|
||||
# backup-tradein-db.sh падает на общий
|
||||
# /etc/default/gendesign-backup выше)
|
||||
# Без них ops/backup.sh и backup-tradein-db.sh теперь падают громко (#3085),
|
||||
# а не молча пишут локальный бэкап.
|
||||
#
|
||||
# ⚠️ САМЫЙ ОПАСНЫЙ ПРОПУСК ВСЕГО ПЕРЕЕЗДА. Если этот файл не установить,
|
||||
# на новом хосте НЕ БУДЕТ НИ ОДНОГО БЭКАПА — ни tradein, ни gendesign. И узнать
|
||||
# об этом будет неоткуда: сторож пропущенных прогонов (check-backup-staleness)
|
||||
|
|
@ -17,8 +28,9 @@
|
|||
# ── Бэкапы обоих кластеров ──────────────────────────────────────────────────
|
||||
# Off-box выгрузка в S3 включается через /etc/default/tradein-backup и
|
||||
# /etc/default/gendesign-backup — файлы НЕ переезжают сами (в git их нет и не
|
||||
# должно быть), их нужно перенести руками. Без них дампы останутся локальными,
|
||||
# то есть на том же диске, что и БД, — ровно то, из-за чего заводился #2203.
|
||||
# должно быть), их нужно перенести руками. Без них оба скрипта падают ГРОМКО
|
||||
# (exit 1, дамп не снимается вовсе) — см. #3085; они больше НЕ пишут молча
|
||||
# локальный бэкап.
|
||||
30 3 * * * bash /opt/gendesign/ops/backup.sh >> /opt/gendesign/logs/gendesign-backup.log 2>&1
|
||||
30 4 * * * bash /opt/gendesign/tradein-mvp/deploy/backup-tradein-db.sh >> /opt/gendesign/logs/tradein-backup.log 2>&1
|
||||
|
||||
|
|
@ -28,6 +40,15 @@
|
|||
0 * * * * bash /opt/gendesign/ops/check-backup-staleness.sh /opt/gendesign/backups/.last_success 26 "gendesign main backup" >> /opt/gendesign/logs/backup-staleness.log 2>&1
|
||||
0 * * * * bash /opt/gendesign/ops/check-backup-staleness.sh /opt/gendesign/backups/tradein/.last_success 26 "tradein backup" >> /opt/gendesign/logs/backup-staleness.log 2>&1
|
||||
|
||||
# ── Restore-дрель, раз в месяц (#3085) ───────────────────────────────────────
|
||||
# Восстановимость дампа никогда не проверялась автоматически — только
|
||||
# ручными прогонами. 1-е число месяца, 02:15 — до ночных бэкапов (03:30/04:30)
|
||||
# и до обогащения/геокодинга (05:00-05:45), не пересекается ни с чем.
|
||||
# Берёт САМЫЙ СВЕЖИЙ на тот момент main-дамп; ops/restore-drill.sh сам найдёт
|
||||
# рядом лежащий globals-файл и сам снесёт свой временный контейнер — прод не
|
||||
# трогает никогда (см. заголовок скрипта).
|
||||
15 2 1 * * bash -c 'f=$(ls -t /opt/gendesign/backups/gendesign_[0-9]*.sql.gz 2>/dev/null | head -1); if [ -n "$f" ]; then bash /opt/gendesign/ops/restore-drill.sh "$f"; else echo "no gendesign dump found to drill"; exit 1; fi' >> /opt/gendesign/logs/restore-drill.log 2>&1
|
||||
|
||||
# ── Обогащение и геокодирование (ходят в tradein-backend) ───────────────────
|
||||
0 5 * * * cd /opt/gendesign/tradein-mvp && docker exec tradein-backend python -m scripts.backfill_houses_dadata --limit 100 --priority both >> /opt/gendesign/logs/dadata-backfill.log 2>&1
|
||||
30 5 * * * cd /opt/gendesign/tradein-mvp && docker exec tradein-backend python -m scripts.geocode_deals_from_houses --limit 50000 >> /opt/gendesign/logs/deals-geocode.log 2>&1
|
||||
|
|
|
|||
|
|
@ -33,9 +33,11 @@
|
|||
# Without these two set, ops/check-backup-staleness.sh still logs, just
|
||||
# doesn't send a Telegram alert.
|
||||
|
||||
# --- S3 off-site upload (Selectel S3-compatible). All four required to enable. ---
|
||||
# --- S3 off-site upload (Selectel S3-compatible). All four REQUIRED (#3085) —
|
||||
# without them, backup.sh and backup-tradein-db.sh refuse to run unless
|
||||
# BACKUP_ALLOW_LOCAL_ONLY=1 is also set (see below). ---
|
||||
#S3_ENDPOINT=https://s3.ru-1.storage.selcloud.ru
|
||||
#S3_BUCKET=gendesign-backups
|
||||
#S3_BUCKET=gendsgn-backups
|
||||
#S3_ACCESS_KEY=REPLACE_WITH_REAL_ACCESS_KEY
|
||||
#S3_SECRET_KEY=REPLACE_WITH_REAL_SECRET_KEY
|
||||
|
||||
|
|
@ -44,6 +46,9 @@
|
|||
#TELEGRAM_CHAT_ID=123456789
|
||||
|
||||
# --- optional overrides (defaults are sensible; uncomment only to change) ---
|
||||
#BACKUP_ALLOW_LOCAL_ONLY=1 # explicit escape hatch: run without S3 creds and
|
||||
# accept a local-only backup (still logs a loud
|
||||
# WARNING each run). Leave unset in prod.
|
||||
#KEEP=7 # how many recent local dumps to retain (applies to both
|
||||
# the main dump series and the globals dump series)
|
||||
#MIN_DUMP_BYTES=51200 # sanity floor for the MAIN dump; a dump smaller than
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ notify() {
|
|||
-d "disable_web_page_preview=true" \
|
||||
--data-urlencode "text=${text}" \
|
||||
>/dev/null 2>&1 \
|
||||
|| { log "WARN: telegram sendMessage failed — пробую запасной канал"; \n notify_fallback_mail "$text"; }
|
||||
|| { log "WARN: telegram sendMessage failed — пробую запасной канал"; notify_fallback_mail "$text"; }
|
||||
}
|
||||
|
||||
# --- запасной канал оповещения (#3059) ----------------------------------
|
||||
|
|
|
|||
|
|
@ -25,6 +25,17 @@
|
|||
|
||||
set -euo pipefail
|
||||
|
||||
# notify() on failure (reviewer finding): unlike backup.sh/backup-tradein-db.sh
|
||||
# this drill has no sentinel/watchdog of its own — its cron entry has no
|
||||
# MAILTO, so a non-zero exit previously vanished into restore-drill.log with
|
||||
# nobody looking. Reuse the same Telegram/mail channel as the backups so an
|
||||
# unrestorable dump is loud, not discovered during an actual incident. Wired
|
||||
# into cleanup() below (not its own trap) — bash only honours the LAST trap
|
||||
# registered for a given signal, and cleanup() already owns EXIT.
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck source=./lib-backup.sh
|
||||
source "$SCRIPT_DIR/lib-backup.sh"
|
||||
|
||||
IMAGE="${RESTORE_DRILL_IMAGE:-postgis/postgis:16-3.4}"
|
||||
READY_TIMEOUT="${RESTORE_DRILL_READY_TIMEOUT:-60}" # seconds to wait for postgres startup
|
||||
DRILL_DB="drill"
|
||||
|
|
@ -96,8 +107,12 @@ IFS=',' read -r -a TABLES <<< "$tables_csv"
|
|||
# single-use, bound to 127.0.0.1, destroyed on exit. ---
|
||||
CONTAINER="restore-drill-$$-$(date -u +%s)"
|
||||
cleanup() {
|
||||
local rc=$?
|
||||
log "Cleaning up container ${CONTAINER}"
|
||||
docker rm -f "$CONTAINER" >/dev/null 2>&1 || true
|
||||
docker rm -f -v "$CONTAINER" >/dev/null 2>&1 || true
|
||||
if [[ $rc -ne 0 ]]; then
|
||||
notify "🔴 restore-drill FAILED (exit ${rc}) for ${DUMP_FILE:-<unset>} — dump may not be restorable. Check restore-drill.log on the VM." || true
|
||||
fi
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
|
|
|
|||
|
|
@ -15,12 +15,13 @@
|
|||
# docker exec -i tradein-postgres psql -U tradein -d tradein
|
||||
# Restore-дрель (безопасно, не трогает прод): ops/restore-drill.sh.
|
||||
#
|
||||
# Опциональная выгрузка в S3 — переменные в /etc/default/tradein-backup
|
||||
# (root-owned, chmod 600, НЕ в git); если файла нет — фолбэк на
|
||||
# /etc/default/gendesign-backup (общие креды с основным бэкапом). Без
|
||||
# переменных поведение прежнее: дамп остаётся только локально.
|
||||
# Выгрузка в S3 ОБЯЗАТЕЛЬНА по умолчанию (#3085) — переменные в
|
||||
# /etc/default/tradein-backup (root-owned, chmod 600, НЕ в git); если файла
|
||||
# нет — фолбэк на /etc/default/gendesign-backup (общие креды с основным
|
||||
# бэкапом). Без всех четырёх переменных скрипт падает ДО снятия дампа —
|
||||
# BACKUP_ALLOW_LOCAL_ONLY=1 явно разрешает прежнее локальное поведение.
|
||||
# S3_ENDPOINT=https://s3.ru-1.storage.selcloud.ru
|
||||
# S3_BUCKET=gendesign-backups
|
||||
# S3_BUCKET=gendsgn-backups
|
||||
# S3_ACCESS_KEY=...
|
||||
# S3_SECRET_KEY=...
|
||||
#
|
||||
|
|
@ -51,6 +52,27 @@ elif [[ -f /etc/default/gendesign-backup ]]; then
|
|||
source /etc/default/gendesign-backup
|
||||
fi
|
||||
|
||||
# --- guard: S3-выгрузка обязательна по умолчанию (#3085). Без неё дамп живёт
|
||||
# только на том же диске, что и БД, — не бэкап, если хост потерян. До
|
||||
# этой проверки отсутствие/неполнота env-файла давали в логе «дамп
|
||||
# остаётся только локально» и код возврата 0, неотличимо от настоящего
|
||||
# off-box бэкапа. Падаем ГРОМКО, до снятия дампа — незачем тратить время
|
||||
# и место на дамп, которому всё равно некуда уехать. BACKUP_ALLOW_LOCAL_ONLY=1 —
|
||||
# явная форточка (та же переменная, что у ops/backup.sh) для намеренного
|
||||
# локального прогона; всё равно шумит предупреждением, чтобы это не
|
||||
# забылось молча. ---
|
||||
if [[ -z "${S3_ENDPOINT:-}" || -z "${S3_BUCKET:-}" || -z "${S3_ACCESS_KEY:-}" || -z "${S3_SECRET_KEY:-}" ]]; then
|
||||
if [[ "${BACKUP_ALLOW_LOCAL_ONLY:-0}" == "1" ]]; then
|
||||
log "ПРЕДУПРЕЖДЕНИЕ: S3-переменные заданы не полностью — BACKUP_ALLOW_LOCAL_ONLY=1, продолжаю с ЛОКАЛЬНЫМ бэкапом. Эта копия НЕ переживёт потерю этой VM." >&2
|
||||
BACKUP_LOCAL_ONLY_USED=1
|
||||
else
|
||||
log "ОШИБКА: выгрузка в S3 не настроена — не хватает одной или нескольких из S3_ENDPOINT / S3_BUCKET / S3_ACCESS_KEY / S3_SECRET_KEY." >&2
|
||||
log "ОШИБКА: заполни их в /etc/default/tradein-backup (или /etc/default/gendesign-backup) —" >&2
|
||||
log "ОШИБКА: см. ops/gendesign-backup.default.example, — либо выстави BACKUP_ALLOW_LOCAL_ONLY=1, чтобы явно принять локальный бэкап." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Проверка целостности сверх «не пустой»: битый посреди записи дамп (диск
|
||||
# кончился, OOM-kill, оборвался docker exec) может дать структурно валидный,
|
||||
# не крошечный .gz — ни `-s`, ни MIN_DUMP_BYTES это не ловят. Две проверки,
|
||||
|
|
@ -169,4 +191,9 @@ log "backup ok: $out ($size), копий хранится: $count данных +
|
|||
# Детект пропущенного запуска (#2203): отмечаем успех только тут, после всех
|
||||
# проверок выше (и — под `set -euo pipefail` — после S3-выгрузки, если она
|
||||
# включена). См. ops/check-backup-staleness.sh.
|
||||
write_sentinel "$SENTINEL_FILE"
|
||||
if [[ "${BACKUP_LOCAL_ONLY_USED:-0}" == "1" ]]; then
|
||||
log "Sentinel не пишется — это ЛОКАЛЬНЫЙ прогон (BACKUP_ALLOW_LOCAL_ONLY=1), не подтверждённый off-box бэкап." >&2
|
||||
notify "⚠️ tradein backup: ЛОКАЛЬНЫЙ прогон (S3 не настроен, BACKUP_ALLOW_LOCAL_ONLY=1). Дамп ${out} НЕ ушёл off-box. Sentinel не обновлён — сторож свежести подаст сигнал, если это не поправить."
|
||||
else
|
||||
write_sentinel "$SENTINEL_FILE"
|
||||
fi
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue