gendesign/ops/crontab-poincare.cron
bot-backend de940c7534
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 10s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
feat(ops): off-box копия рантайм-конфига — зашифрованной, иначе никак
Четвёртый пункт приёмки #2203. В дампах есть колонки под pgp_sym_encrypt, ключ
к ним лежит на самой машине и никуда не уезжает: потеря машины означает «дампы
есть, расшифровать нечем». Ради этой связки шифрование в базе и заводилось.

КУДА И ПОЧЕМУ ИМЕННО ТУДА. В тот же S3, отдельным префиксом env/, и только
зашифрованным. Разобранные варианты:

  - рядом с дампами открытым — нельзя: ключ рядом с шифротекстом обнуляет
    шифрование, одна утечка доступа к бакету отдаёт и то и другое;
  - на соседний хост по SSH — потребовало бы завести доверие между машинами,
    которого нет (проверено: Permission denied (publickey)), то есть РАСШИРИТЬ
    периметр ровно тогда, когда #3075 его сужает;
  - отдельный бакет с отдельными ключами — правильнее всего, но требует новых
    учётных данных.

Выбран единственный исполнимый без расширения доступа: шифротекст в S3,
парольная фраза — вне S3.

FAIL-CLOSED. Без фразы скрипт не выгружает файл открытым, а падает с явным
сообщением и алертом. Молчаливая выгрузка ключа в бакет с дампами хуже
отсутствия копии: создаёт ложное чувство защищённости.

Фраза уходит через дескриптор, а не аргументом — иначе видна в ps любому
пользователю машины. После шифрования файл проверяется обратной расшифровкой:
без этого можно годами возить нечитаемый мусор и узнать в тот момент, когда он
понадобился.

Прогон на хосте 27.08 (подставной исходник, боевой не трогался):
  без фразы → код 1, файлов создано 0
  с фразой  → «Зашифровано и проверено расшифровкой», 110 байт
  своей фразой читается, чужой — нет

ОДИН ШАГ ЗА ВЛАДЕЛЬЦЕМ, и он неустраним: фраза обязана жить там, где переживёт
смерть машины, иначе копия бесполезна — расшифровать будет нечем. Сгенерировать
её здесь и оставить на хосте нельзя по построению. Инструкция — в шапке скрипта.

Пять тестов сторожат ровно те свойства, ради которых всё сделано.
Прогон: 85 ops-тестов зелёные, ruff чист.

Refs #2203
2026-08-27 16:32:27 +03:00

98 lines
9.7 KiB
Text
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# crontab ПРИНИМАЮЩЕГО хоста (Selectel Poincare, 188.124.37.140) — после 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)
# сам живёт в этом же crontab'е, то есть его тоже не будет. Отсутствие бэкапов
# обнаружится в момент, когда они понадобятся.
#
# Времена оставлены те же, что были на Beget — чтобы не гадать, изменилось ли
# поведение из-за переезда или из-за нового расписания. Подгонять окна имеет
# смысл ПОСЛЕ того, как всё поедет стабильно.
# ── Бэкапы обоих кластеров ──────────────────────────────────────────────────
# Off-box выгрузка в S3 включается через /etc/default/tradein-backup и
# /etc/default/gendesign-backup — файлы НЕ переезжают сами (в git их нет и не
# должно быть), их нужно перенести руками. Без них оба скрипта падают ГРОМКО
# (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
# Сторожа пропущенных прогонов, порог 26ч. Едут вместе со своими бэкапами:
# сторож должен жить там же, где sentinel, иначе он сторожит файл, который
# никто не обновляет, и кричит вечно.
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 — она НЕ покрывалась строкой выше (#2203).
# Строка выше берёт только `gendesign_*`, то есть восстановимость базы
# клиентских оценок автоматически не проверялась вовсе: ни листинги, ни дома,
# ни сами оценки. Замер 27.08: дамп 343 МБ разворачивается за 58 c и сходится
# с боевой базой построчно (houses 10400, listings 109978, deals 108623 —
# расхождения только по строкам, созданным ПОСЛЕ снятия дампа).
#
# Еженедельно, а не раз в месяц: две минуты работы против месяца, в течение
# которого битый дамп остаётся незамеченным. Понедельник 03:00 — после
# монтажной дрели gendesign (02:15 первого числа, ей хватает форы даже когда
# первое выпадает на понедельник) и до ночных бэкапов в 03:30.
0 3 * * 1 bash -c 'f=$(ls -t /opt/gendesign/backups/tradein/tradein-[0-9]*.sql.gz 2>/dev/null | head -1); if [ -n "$f" ]; then bash /opt/gendesign/ops/restore-drill.sh "$f"; else echo "no tradein dump found to drill"; exit 1; fi' >> /opt/gendesign/logs/restore-drill.log 2>&1
# ── Off-box копия рантайм-конфига (#2203) ────────────────────────────────────
# Четвёртый пункт приёмки #2203. В дампах есть колонки под pgp_sym_encrypt;
# ключ к ним лежит на самой машине и никуда не уезжает. Потеря машины =
# дампы есть, расшифровать нечем.
#
# 04:10 — после обоих ночных бэкапов (03:30 и 04:30 идёт tradein, но он берёт
# свой дамп независимо), до обогащения в 05:00. Ежедневно, потому что копия
# должна соответствовать дампу того же дня: конфиг меняют редко, но если
# поменяли, старая копия не расшифрует свежий дамп.
#
# Скрипт fail-closed: без ENV_OFFBOX_PASSPHRASE в /etc/default/gendesign-backup
# он НЕ выгружает файл открытым, а падает с явным сообщением и алертом.
# Открытым в тот же бакет, где лежат дампы, класть нельзя — это обнулило бы
# шифрование колонок (ключ рядом с шифротекстом).
10 4 * * * bash /opt/gendesign/ops/backup-env-offbox.sh >> /opt/gendesign/logs/env-offbox.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
45 5 * * * cd /opt/gendesign/tradein-mvp && docker exec tradein-backend python -m scripts.geocode_deals_nominatim --limit 5000 >> /opt/gendesign/logs/deals-nominatim.log 2>&1
# ── Уборка докера ───────────────────────────────────────────────────────────
# СВОЯ запись, не «переехавшая»: на Beget она чистит тома CI-раннеров, здесь —
# образы продуктового стека. Нужны обе, на обоих хостах.
0 4 * * 0 bash /opt/gendesign/ops/docker-prune.sh >> /opt/gendesign/logs/docker-prune.log 2>&1
# ── Логи: /opt/gendesign/logs, а не /tmp ────────────────────────────────────
# На Beget половина записей писала в /tmp — он чистится при перезагрузке, и
# именно поэтому историю отказов бэкапов там восстановить не удавалось.
# Каталог создать до установки crontab: mkdir -p /opt/gendesign/logs
#
# ── ЧЕГО ЗДЕСЬ НЕТ ──────────────────────────────────────────────────────────
# backup-forgejo.sh + его сторож — Forgejo остаётся на Beget
# (см. ops/crontab-beget.cron)