fix(ops): бэкап без выгрузки в S3 падает, а не рапортует успех #3086

Merged
lekss361 merged 1 commit from fix/3085-backups-fail-loud into main 2026-08-24 17:50:19 +00:00
Owner

Зачем

Проверка хостов за шесть дней до переезда: на Poincare пустой crontab — ни бэкапов, ни сторожей, ни конфигов. Сегодня это неважно, базы ещё на Beget, где всё работает и сентинелы свежие. 30.08 базы переезжают — и бэкапы там просто не начнутся.

Хуже, что отказ был бы тихим: выгрузка в S3 была опциональной, без четырёх переменных скрипт писал backup stays local only и выходил с кодом 0. Не ошибка — успешный прогон. Сторож свежести на новом хосте тоже не установлен, промолчали бы оба.

Что меняется

Отсутствие любой из S3_ENDPOINT / S3_BUCKET / S3_ACCESS_KEY / S3_SECRET_KEY роняет прогон с ненулевым кодом — до снятия дампа, потому что незачем тратить время и место на дамп, которому некуда уехать. Образец взят из ops/backup-forgejo.sh, где так было изначально.

BACKUP_ALLOW_LOCAL_ONLY=1 оставляет прежний путь для машины без ключей.

На Beget все четыре переменные заданы (проверено по SSH), поэтому там поведение не меняется вообще.

Что нашло ревью

Первая версия форточки возвращала ровно ту дыру, которую чинит PR. Локальный прогон всё равно писал сентинел и выходил нулём → сторож свежести оставался зелёным, а единственным следом был WARNING в логе, который никто не тailит. Достаточно раскомментировать одну строку в /etc/default — и «настроено» стало бы неотличимо от рабочего. Теперь такой прогон сентинел не пишет и сразу зовёт notify().

Попутно найден живой баг. В ops/lib-backup.sh:54 внутри блока после || стояло литеральное \n — шелл разбирал его как команду n, поэтому notify_fallback_mail не вызывался никогда. При недоступности Telegram мы теряли уведомления молча — тот же класс тишины, что и основной дефект.

Восстановимость

ops/restore-drill.sh поставлен в расписание Poincare. Он существовал, но не запускался нигде — восстановимость дампов автоматически не проверялась ни разу. gzip -t и трейлер pg_dump говорят «файл не битый», а это не то же самое, что «база поднимается».

У самой дрели своего сторожа нет и MAILTO в cron не задан, поэтому её падение тоже уходит в notify(). Заодно docker rm -f получил -v: при регулярном запуске иначе оставался бы dangling-том размером с базу до воскресной чистки, а на Poincare это конкурирует с ночным дампом за диск.

Мелочь

Имя бакета в примере исправлено на gendsgn-backups — на хостах настроен именно он, forgejo пишет туда же под префиксом forgejo/.

Что намеренно НЕ закрыто

  • Фолбэк на общий конфиг в tradein-скрипте остался elif: если появится /etc/default/tradein-backup без S3-переменных, прогон упадёт вместо чтения общего файла. Под новой политикой это желаемое поведение, а не дефект.
  • Дрель проверяет только серию gendesign: restore-drill.sh не умеет батч, расширять его здесь — смешивать задачи.
  • Бэкап волта CouchDB — отдельная задача: тар живого тома LiveSync консистентности не даёт, нужен свой подход.

Test plan

  • bash -n на всех четырёх изменённых скриптах
  • прогон backup-tradein-db.sh без переменных S3 → три строки ОШИБКА, rc=1, до строки «Dumping»
  • прогон ops/backup.sh с BACKUP_ALLOW_LOCAL_ONLY=1 → WARNING, затем доходит до дампа
  • выгрузка в S3 с Poincare подтверждена живым объектом (креды, сеть, CA-бандл)
  • после мержа: первый ночной прогон на Beget по-прежнему зелёный и сентинел обновился
  • в окно переезда: поставить crontab на Poincare и убедиться, что первый прогон реально кладёт объект

Отдельно, требует человека

Оба ключа S3 оказались write-only: PutObject есть, List, Get и Delete — AccessDenied. Как защита это правильно, но следствия три: этими ключами нельзя восстановиться (403 на GetObject), нельзя проверить содержимое бакета с хостов, и ретенцию нельзя сделать скриптами — только lifecycle-политикой на бакете. Нужен отдельный read-ключ, хранимый не на хостах.

Refs #3085, #3008

## Зачем Проверка хостов за шесть дней до переезда: **на Poincare пустой crontab** — ни бэкапов, ни сторожей, ни конфигов. Сегодня это неважно, базы ещё на Beget, где всё работает и сентинелы свежие. 30.08 базы переезжают — и бэкапы там просто не начнутся. Хуже, что отказ был бы **тихим**: выгрузка в S3 была опциональной, без четырёх переменных скрипт писал `backup stays local only` и выходил с кодом 0. Не ошибка — успешный прогон. Сторож свежести на новом хосте тоже не установлен, промолчали бы оба. ## Что меняется Отсутствие любой из `S3_ENDPOINT` / `S3_BUCKET` / `S3_ACCESS_KEY` / `S3_SECRET_KEY` роняет прогон с ненулевым кодом — **до снятия дампа**, потому что незачем тратить время и место на дамп, которому некуда уехать. Образец взят из `ops/backup-forgejo.sh`, где так было изначально. `BACKUP_ALLOW_LOCAL_ONLY=1` оставляет прежний путь для машины без ключей. На Beget все четыре переменные заданы (проверено по SSH), поэтому там поведение не меняется вообще. ## Что нашло ревью **Первая версия форточки возвращала ровно ту дыру, которую чинит PR.** Локальный прогон всё равно писал сентинел и выходил нулём → сторож свежести оставался зелёным, а единственным следом был WARNING в логе, который никто не тailит. Достаточно раскомментировать одну строку в `/etc/default` — и «настроено» стало бы неотличимо от рабочего. Теперь такой прогон сентинел **не пишет** и сразу зовёт `notify()`. **Попутно найден живой баг.** В `ops/lib-backup.sh:54` внутри блока после `||` стояло литеральное `\n` — шелл разбирал его как команду `n`, поэтому `notify_fallback_mail` не вызывался **никогда**. При недоступности Telegram мы теряли уведомления молча — тот же класс тишины, что и основной дефект. ## Восстановимость `ops/restore-drill.sh` поставлен в расписание Poincare. Он существовал, но не запускался нигде — восстановимость дампов автоматически не проверялась ни разу. `gzip -t` и трейлер `pg_dump` говорят «файл не битый», а это не то же самое, что «база поднимается». У самой дрели своего сторожа нет и `MAILTO` в cron не задан, поэтому её падение тоже уходит в `notify()`. Заодно `docker rm -f` получил `-v`: при регулярном запуске иначе оставался бы dangling-том размером с базу до воскресной чистки, а на Poincare это конкурирует с ночным дампом за диск. ## Мелочь Имя бакета в примере исправлено на `gendsgn-backups` — на хостах настроен именно он, forgejo пишет туда же под префиксом `forgejo/`. ## Что намеренно НЕ закрыто - Фолбэк на общий конфиг в tradein-скрипте остался `elif`: если появится `/etc/default/tradein-backup` без S3-переменных, прогон упадёт вместо чтения общего файла. Под новой политикой это желаемое поведение, а не дефект. - Дрель проверяет только серию `gendesign`: `restore-drill.sh` не умеет батч, расширять его здесь — смешивать задачи. - Бэкап волта CouchDB — отдельная задача: тар живого тома LiveSync консистентности не даёт, нужен свой подход. ## Test plan - [x] `bash -n` на всех четырёх изменённых скриптах - [x] прогон `backup-tradein-db.sh` без переменных S3 → три строки ОШИБКА, `rc=1`, **до** строки «Dumping» - [x] прогон `ops/backup.sh` с `BACKUP_ALLOW_LOCAL_ONLY=1` → WARNING, затем доходит до дампа - [x] выгрузка в S3 с Poincare подтверждена живым объектом (креды, сеть, CA-бандл) - [ ] после мержа: первый ночной прогон на Beget по-прежнему зелёный и сентинел обновился - [ ] в окно переезда: поставить crontab на Poincare и убедиться, что первый прогон реально кладёт объект ## Отдельно, требует человека Оба ключа S3 оказались **write-only**: `PutObject` есть, `List`, `Get` и `Delete` — AccessDenied. Как защита это правильно, но следствия три: этими ключами **нельзя восстановиться** (403 на `GetObject`), нельзя проверить содержимое бакета с хостов, и ретенцию нельзя сделать скриптами — только lifecycle-политикой на бакете. Нужен отдельный read-ключ, хранимый **не на хостах**. Refs #3085, #3008
lekss361 added 1 commit 2026-08-24 17:49:20 +00:00
fix(ops): бэкап без выгрузки в S3 падает, а не рапортует успех
All checks were successful
CI Trade-In / changes (pull_request) Successful in 7s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / changes (pull_request) Successful in 8s
CI / frontend-tests (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
1cb14c6a35
Проверка хостов за шесть дней до переезда показала, что на Poincare пустой
crontab: ни бэкапов, ни сторожей, ни конфигов. Сегодня это неважно — базы ещё
на Beget, где всё работает. Тридцатого августа они переезжают, и бэкапы там
просто не начнутся.

Хуже, что отказ был бы тихим. Выгрузка в S3 была опциональной: без четырёх
переменных скрипт писал "backup stays local only" и выходил с кодом ноль.
Не ошибка, не алерт — успешный прогон. Сторож свежести на новом хосте тоже
не установлен, так что промолчали бы оба.

Теперь отсутствие любой из S3_ENDPOINT / S3_BUCKET / S3_ACCESS_KEY /
S3_SECRET_KEY роняет прогон с ненулевым кодом — до снятия дампа, потому что
незачем тратить время и место на дамп, которому некуда уехать. Образец
поведения взят из backup-forgejo.sh, где так было с самого начала.

BACKUP_ALLOW_LOCAL_ONLY=1 оставляет прежний путь для машины без ключей.

Ревью показало, что первая версия этой форточки возвращала ровно ту дыру,
которую чинит PR: локальный прогон всё равно писал сентинел и выходил нулём,
поэтому сторож свежести оставался зелёным, а единственным следом был WARNING
в логе, который никто не читает. Достаточно было раскомментировать одну
строку в /etc/default — и «настроено» выглядело бы неотличимо от рабочего.
Теперь такой прогон сентинел НЕ пишет и сразу зовёт notify().

Попутно найден живой баг: в lib-backup.sh внутри блока после ||
литеральное `\n` разбиралось шеллом как команда `n`, поэтому запасной канал
оповещения по почте не срабатывал никогда. То есть при недоступности Telegram
мы теряли уведомления молча — тот же класс тишины.

restore-drill.sh поставлен в расписание Poincare: он существовал, но не
запускался нигде, и восстановимость дампов автоматически не проверялась ни
разу. gzip -t и трейлер pg_dump говорят «файл не битый», а это не то же
самое, что «база поднимается». У самой дрели своего сторожа нет и MAILTO в
cron не задан, поэтому её падение теперь тоже уходит в notify(). Заодно
docker rm -f получил -v: регулярный запуск иначе оставлял бы dangling-том
размером с базу до воскресной чистки.

Имя бакета в примере исправлено на gendsgn-backups — на хостах настроен
именно он, forgejo пишет туда же под префиксом forgejo/.

Две находки ревью намеренно не закрыты. Фолбэк на общий конфиг в
tradein-скрипте остался elif: если появится /etc/default/tradein-backup без
S3-переменных, прогон упадёт вместо чтения общего файла — под новой политикой
это и есть желаемое поведение. Дрель проверяет только серию gendesign:
restore-drill.sh не умеет батч, а расширять его в этом PR значит смешивать
задачи.

Хостовая часть — установка crontab и конфига на Poincare — в PR не входит.
Конфиг уже положен вручную и выгрузка с Poincare проверена живым объектом;
crontab ставится в окно переезда, потому что продуктового postgres там пока
нет и ночные прогоны падали бы впустую.

Refs #3085, #3008
lekss361 merged commit 2feb0de446 into main 2026-08-24 17:50:19 +00:00
lekss361 deleted branch fix/3085-backups-fail-loud 2026-08-24 17:50:19 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#3086
No description provided.