Блокер 30.08: на Poincare нет бэкапов вообще — пустой crontab, и отказ будет тихим #3085

Closed
opened 2026-08-24 17:26:16 +00:00 by bot-backend · 2 comments
Collaborator

Блокер окна переезда 30.08. Проверено по SSH на обоих хостах 24.08.

Симптом

На Poincare (188.246.224.93) пустой crontab. Ноль строк.

$ ssh selectel crontab -l | grep -vcE '^\s*#|^\s*$'
0

Там же нет ни /etc/default/gendesign-backup, ни /etc/default/gendesign-backup-forgejo, ни каталогов /opt/gendesign/backups*, ни сентинелов.

На Beget всё живо и работает — 10 задач, все четыре переменные S3 заданы, сентинелы сегодняшние:

/opt/gendesign/backups/.last_success          2026-08-24 03:43
/opt/gendesign/backups/forgejo/.last_success  2026-08-24 04:15
/opt/gendesign/backups/tradein/.last_success  2026-08-24 04:31

То есть бэкапы продуктовых баз живы ровно потому, что базы пока на Beget. 30.08 они уезжают на Poincare, и бэкапы там просто не начнутся.

Почему это не заметят

Отказ двойной и оба раза тихий.

  1. ops/backup.sh:166,184 — выгрузка в S3 опциональна. Без четырёх переменных скрипт пишет backup stays local only и выходит с кодом 0. Не ошибка, не алерт — успешный прогон.
  2. Сторож свежести check-backup-staleness.sh на Poincare тоже не установлен (его строки — в том же неустановленном crontab). Некому заметить, что сентинел не обновляется.

Файл ops/crontab-poincare.cron в репозитории готов и содержит всё нужное. Его туда никто не поставил.

Смежные дыры, найденные тем же проходом

  • Волт CouchDB не бэкапится вообще. Ни скрипта в ops/, ни строки в обоих crontab; единственное упоминание томов couchdb_data / couchdb_config — исключение из ops/docker-prune.sh:132, чтобы их не снесло. А в волте runbook'и и meta/00_credentials.md. Сейчас его единственная копия — ручной бэкап на машине владельца (168 МБ, 23.08), не по расписанию.
  • Восстановимость никогда не проверялась автоматически. ops/restore-drill.sh существует, но не запланирован ни в одном crontab. Автоматом проверяются только gzip -t, трейлер pg_dump, MIN_DUMP_BYTES и возраст сентинела — это проверки «файл не битый», а не «из него поднимается база».
  • У бакета S3 нет ретенции. Ни lifecycle, ни версионирования, ни s3 rm в скриптах — копится бесконечно.
  • Имя бакета расходится между конфигами: gendesign-backups в ops/gendesign-backup.default.example:38 против gendsgn-backups в ops/gendesign-backup-forgejo.default.example:26. Перед переносом сверить с фактическим.

Что сделать

До окна 30.08:

  • установить ops/crontab-poincare.cron на Poincare
  • завести там /etc/default/gendesign-backup со всеми четырьмя переменными S3 (иначе тихий local-only)
  • создать каталоги /opt/gendesign/backups{,/tradein} и /opt/gendesign/logs
  • прогнать ops/backup.sh и backup-tradein-db.sh руками, убедиться что объект появился в бакете, а не только на диске
  • проверить, что check-backup-staleness.sh шумит, если сентинела нет (а не молчит)

Отдельными задачами:

  • сделать выгрузку в S3 обязательной для продуктовых баз, как она уже сделана для forgejo (ops/backup-forgejo.sh:128-131 падает без ключа). Молчаливый exit 0 при отсутствии конфига — корень этой проблемы, а не частный случай
  • завести бэкап волта CouchDB
  • поставить restore-drill.sh в расписание, хотя бы раз в месяц
  • ретенция или lifecycle на бакете

Как проверить

ssh selectel crontab -l | grep -c backup          # должно быть > 0
ssh selectel ls -la /opt/gendesign/backups/       # свежие дампы
ssh selectel date -r /opt/gendesign/backups/.last_success

Плюс: намеренно убрать одну переменную S3 и убедиться, что прогон теперь падает, а не рапортует успех.

Контекст

Копия вне провайдера у нас есть — ручной бэкап на машине владельца (gendsgn-backup.ps1, полный: forgejo БД + data + bundle, glitchtip, gendesign, tradein, auth, секреты, couchdb, SHA256SUMS). Но задача в планировщике не зарегистрирована — скрипт умеет -Install (ежемесячно, Keep=6), его не запускали. Так что это разовый снимок от 23.08, а не работающая ротация.

Refs #3008

Блокер окна переезда 30.08. Проверено по SSH на обоих хостах 24.08. ## Симптом **На Poincare (188.246.224.93) пустой crontab. Ноль строк.** ``` $ ssh selectel crontab -l | grep -vcE '^\s*#|^\s*$' 0 ``` Там же нет ни `/etc/default/gendesign-backup`, ни `/etc/default/gendesign-backup-forgejo`, ни каталогов `/opt/gendesign/backups*`, ни сентинелов. На Beget всё живо и работает — 10 задач, все четыре переменные S3 заданы, сентинелы сегодняшние: ``` /opt/gendesign/backups/.last_success 2026-08-24 03:43 /opt/gendesign/backups/forgejo/.last_success 2026-08-24 04:15 /opt/gendesign/backups/tradein/.last_success 2026-08-24 04:31 ``` То есть бэкапы продуктовых баз живы ровно потому, что базы пока на Beget. **30.08 они уезжают на Poincare, и бэкапы там просто не начнутся.** ## Почему это не заметят Отказ двойной и оба раза тихий. 1. `ops/backup.sh:166,184` — выгрузка в S3 **опциональна**. Без четырёх переменных скрипт пишет `backup stays local only` и **выходит с кодом 0**. Не ошибка, не алерт — успешный прогон. 2. Сторож свежести `check-backup-staleness.sh` на Poincare тоже не установлен (его строки — в том же неустановленном crontab). Некому заметить, что сентинел не обновляется. Файл `ops/crontab-poincare.cron` в репозитории готов и содержит всё нужное. Его туда никто не поставил. ## Смежные дыры, найденные тем же проходом - **Волт CouchDB не бэкапится вообще.** Ни скрипта в `ops/`, ни строки в обоих crontab; единственное упоминание томов `couchdb_data` / `couchdb_config` — исключение из `ops/docker-prune.sh:132`, чтобы их не снесло. А в волте runbook'и и `meta/00_credentials.md`. Сейчас его единственная копия — ручной бэкап на машине владельца (168 МБ, 23.08), не по расписанию. - **Восстановимость никогда не проверялась автоматически.** `ops/restore-drill.sh` существует, но не запланирован ни в одном crontab. Автоматом проверяются только `gzip -t`, трейлер `pg_dump`, `MIN_DUMP_BYTES` и возраст сентинела — это проверки «файл не битый», а не «из него поднимается база». - **У бакета S3 нет ретенции.** Ни lifecycle, ни версионирования, ни `s3 rm` в скриптах — копится бесконечно. - **Имя бакета расходится между конфигами:** `gendesign-backups` в `ops/gendesign-backup.default.example:38` против `gendsgn-backups` в `ops/gendesign-backup-forgejo.default.example:26`. Перед переносом сверить с фактическим. ## Что сделать До окна 30.08: - [ ] установить `ops/crontab-poincare.cron` на Poincare - [ ] завести там `/etc/default/gendesign-backup` со всеми четырьмя переменными S3 (иначе тихий local-only) - [ ] создать каталоги `/opt/gendesign/backups{,/tradein}` и `/opt/gendesign/logs` - [ ] прогнать `ops/backup.sh` и `backup-tradein-db.sh` руками, убедиться что объект появился в бакете, а не только на диске - [ ] проверить, что `check-backup-staleness.sh` шумит, если сентинела нет (а не молчит) Отдельными задачами: - [ ] сделать выгрузку в S3 **обязательной** для продуктовых баз, как она уже сделана для forgejo (`ops/backup-forgejo.sh:128-131` падает без ключа). Молчаливый `exit 0` при отсутствии конфига — корень этой проблемы, а не частный случай - [ ] завести бэкап волта CouchDB - [ ] поставить `restore-drill.sh` в расписание, хотя бы раз в месяц - [ ] ретенция или lifecycle на бакете ## Как проверить ``` ssh selectel crontab -l | grep -c backup # должно быть > 0 ssh selectel ls -la /opt/gendesign/backups/ # свежие дампы ssh selectel date -r /opt/gendesign/backups/.last_success ``` Плюс: намеренно убрать одну переменную S3 и убедиться, что прогон теперь **падает**, а не рапортует успех. ## Контекст Копия вне провайдера у нас есть — ручной бэкап на машине владельца (`gendsgn-backup.ps1`, полный: forgejo БД + data + bundle, glitchtip, gendesign, tradein, auth, секреты, couchdb, `SHA256SUMS`). Но задача в планировщике не зарегистрирована — скрипт умеет `-Install` (ежемесячно, `Keep=6`), его не запускали. Так что это разовый снимок от 23.08, а не работающая ротация. Refs #3008
bot-backend added the
scope/devops
data
status/ready
priority/p0
labels 2026-08-24 17:26:16 +00:00
Owner

Сделано

Репозиторная часть — PR #3086: бэкап без S3 падает вместо тихого exit 0, restore-drill.sh поставлен в расписание, имя бакета исправлено.

Ревью поймало, что первая версия форточки BACKUP_ALLOW_LOCAL_ONLY=1 возвращала ровно эту дыру: локальный прогон писал сентинел и выходил нулём, сторож оставался зелёным. Теперь такой прогон сентинел не пишет и зовёт notify().

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

Хостовая часть на Poincare, вручную:

  • /etc/default/gendesign-backup скопирован с Beget напрямую хост-в-хост (значения нигде не печатались), права 600 root:root
  • созданы /opt/gendesign/backups, /opt/gendesign/backups/tradein, /opt/gendesign/logs
  • выгрузка в S3 с Poincare проверена живым объектом — учётные данные, сеть и CA-бандл работают

Crontab туда пока не ставился намеренно: продуктового postgres на Poincare ещё нет, ночные прогоны падали бы впустую и приучали игнорировать алерты. Это шаг окна переезда.

Новое: оба ключа S3 write-only

Проверка round-trip'ом с Poincare:

операция результат
PutObject OK
ListObjectsV2 AccessDenied
GetObject / HeadObject 403 Forbidden
DeleteObject AccessDenied

У forgejo-ключа на Beget то же самое — ListObjectsV2 AccessDenied.

Как защита это правильно: утёкший ключ не даёт ни прочитать архив, ни стереть его. Но следствия надо зафиксировать явно.

  1. Этими ключами нельзя восстановиться. В аварии нужен отдельный read-ключ из панели Selectel — то есть сценарий «всё легло» упирается в доступ к панели.
  2. Содержимое бакета невозможно проверить с хостов. Единственное свидетельство, что бэкап уехал, — код возврата s3 cp.
  3. Ретенцию скриптами не сделать. DeleteObject запрещён, значит только lifecycle-политика на стороне бакета. Пункт чек-листа «ретенция или lifecycle» сужается до одного варианта.

Добавляю в чек-лист:

  • завести read-ключ для восстановления, хранить не на хостах — рядом с локальным бэкапом на машине владельца
  • lifecycle-политика на бакете gendsgn-backups (скриптами нельзя)
  • удалить мой тестовый объект _connectivity-check/_check_20260824_203627.txt (45 байт) — ключом с хоста не стирается, только через панель

Заодно про локальную копию

Задача в планировщике на машине владельца зарегистрирована: «GenDesign monthly backup», следующий запуск 01.09.2026 05:00, хранение 6 поколений. Две оговорки: режим Interactive only — не отработает при выключенной машине или без логина; и следующий запуск после окна 30.08, поэтому перед переездом снимок нужно снять руками.

## Сделано Репозиторная часть — PR #3086: бэкап без S3 падает вместо тихого `exit 0`, `restore-drill.sh` поставлен в расписание, имя бакета исправлено. Ревью поймало, что первая версия форточки `BACKUP_ALLOW_LOCAL_ONLY=1` возвращала ровно эту дыру: локальный прогон писал сентинел и выходил нулём, сторож оставался зелёным. Теперь такой прогон сентинел не пишет и зовёт `notify()`. Попутно найден живой баг в `ops/lib-backup.sh:54`: литеральное `\n` внутри блока после `||` разбиралось шеллом как команда `n`, поэтому `notify_fallback_mail` не вызывался **никогда** — при недоступности Telegram уведомления терялись молча. Хостовая часть на Poincare, вручную: - `/etc/default/gendesign-backup` скопирован с Beget напрямую хост-в-хост (значения нигде не печатались), права `600 root:root` - созданы `/opt/gendesign/backups`, `/opt/gendesign/backups/tradein`, `/opt/gendesign/logs` - **выгрузка в S3 с Poincare проверена живым объектом** — учётные данные, сеть и CA-бандл работают Crontab туда пока не ставился намеренно: продуктового postgres на Poincare ещё нет, ночные прогоны падали бы впустую и приучали игнорировать алерты. Это шаг окна переезда. ## Новое: оба ключа S3 write-only Проверка round-trip'ом с Poincare: | операция | результат | |---|---| | `PutObject` | OK | | `ListObjectsV2` | AccessDenied | | `GetObject` / `HeadObject` | 403 Forbidden | | `DeleteObject` | AccessDenied | У forgejo-ключа на Beget то же самое — `ListObjectsV2` AccessDenied. Как защита это правильно: утёкший ключ не даёт ни прочитать архив, ни стереть его. Но следствия надо зафиксировать явно. 1. **Этими ключами нельзя восстановиться.** В аварии нужен отдельный read-ключ из панели Selectel — то есть сценарий «всё легло» упирается в доступ к панели. 2. **Содержимое бакета невозможно проверить с хостов.** Единственное свидетельство, что бэкап уехал, — код возврата `s3 cp`. 3. **Ретенцию скриптами не сделать.** `DeleteObject` запрещён, значит только lifecycle-политика на стороне бакета. Пункт чек-листа «ретенция или lifecycle» сужается до одного варианта. Добавляю в чек-лист: - [ ] завести read-ключ для восстановления, хранить **не на хостах** — рядом с локальным бэкапом на машине владельца - [ ] lifecycle-политика на бакете `gendsgn-backups` (скриптами нельзя) - [ ] удалить мой тестовый объект `_connectivity-check/_check_20260824_203627.txt` (45 байт) — ключом с хоста не стирается, только через панель ## Заодно про локальную копию Задача в планировщике на машине владельца зарегистрирована: «GenDesign monthly backup», следующий запуск 01.09.2026 05:00, хранение 6 поколений. Две оговорки: режим `Interactive only` — не отработает при выключенной машине или без логина; и следующий запуск **после** окна 30.08, поэтому перед переездом снимок нужно снять руками.
Owner

Закрыто — бэкапы на Poincare отработали автономно, полным ночным циклом, без ручного вмешательства.

Блокер подтвердился ровно в описанном виде и был шире пустого crontab: каталоги /opt/gendesign/backups и /opt/gendesign/logs приехали как 755 root:root вместо 775 gendesign:gendesign, а /etc/default/gendesign-backup — как 600 root:root вместо 640 root:gendesign. Боевой крон стоит у пользователя gendesign, у root'а крона нет — то есть писать было некуда, а guard свалил бы прогон с «S3 upload is not configured». Потеряли группу файла при переносе.

Исправлено: chown -R gendesign:gendesign + chmod 775 на каталоги, chgrp gendesign + chmod 640 на конфиг, crontab развёрнут (9 строк).

Проверено фактом ночью 26.08, а не правами:

Что Окно UTC Итог
Птица (backup.sh) 00:30 → 00:39 дамп 1,47 ГБ + globals, S3 upload OK
МЕРА (backup-tradein-db.sh) 01:30 → 01:31 дамп 323 МБ + globals, Выгрузка в S3 ok

Сторожевые метки живы: /opt/gendesign/backups/.last_success = 2026-08-26T00:39:37Z, .../tradein/.last_success = 2026-08-26T01:31:00Z. Ежечасный check-backup-staleness.sh пишет OK ... < 26h threshold каждый час подряд. Ретенция сработала — хранится по 2 копии данных и 2 globals.

Мелочь на будущее: образ amazon/aws-cli:latest всё равно тянулся при первом ночном прогоне, хотя был скачан заранее. Заливке не помешало, но предзагрузка себя не оправдала.

Закрыто — бэкапы на Poincare отработали автономно, полным ночным циклом, без ручного вмешательства. Блокер подтвердился ровно в описанном виде и был шире пустого crontab: каталоги `/opt/gendesign/backups` и `/opt/gendesign/logs` приехали как `755 root:root` вместо `775 gendesign:gendesign`, а `/etc/default/gendesign-backup` — как `600 root:root` вместо `640 root:gendesign`. Боевой крон стоит у пользователя `gendesign`, у root'а крона нет — то есть писать было некуда, а guard свалил бы прогон с «S3 upload is not configured». Потеряли группу файла при переносе. Исправлено: `chown -R gendesign:gendesign` + `chmod 775` на каталоги, `chgrp gendesign` + `chmod 640` на конфиг, crontab развёрнут (9 строк). Проверено фактом ночью 26.08, а не правами: | Что | Окно UTC | Итог | |---|---|---| | Птица (`backup.sh`) | 00:30 → 00:39 | дамп 1,47 ГБ + globals, `S3 upload OK` | | МЕРА (`backup-tradein-db.sh`) | 01:30 → 01:31 | дамп 323 МБ + globals, `Выгрузка в S3 ok` | Сторожевые метки живы: `/opt/gendesign/backups/.last_success` = `2026-08-26T00:39:37Z`, `.../tradein/.last_success` = `2026-08-26T01:31:00Z`. Ежечасный `check-backup-staleness.sh` пишет `OK ... < 26h threshold` каждый час подряд. Ретенция сработала — хранится по 2 копии данных и 2 globals. Мелочь на будущее: образ `amazon/aws-cli:latest` всё равно тянулся при первом ночном прогоне, хотя был скачан заранее. Заливке не помешало, но предзагрузка себя не оправдала.
Sign in to join this conversation.
No milestone
No project
No assignees
2 participants
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#3085
No description provided.