Files
portal/docs/superpowers/specs/2026-06-25-managed-pg-migration-assessment.md
T
Дмитрий 12f7080561 feat(db): Путь А — пересчёт аудита через GUC + политики srv_bypass вместо BYPASSRLS
Шов C: audit_block_mutation() пропускает пересчёт hash-цепочки по метке
app.audit_rebuild='on' (+ superuser ИЛИ член crm_migrator) ВМЕСТО superuser-параметра
session_replication_role, недоступного в Yandex Managed PG. AuditRebuildChain
переведён на SET LOCAL app.audit_rebuild в транзакции (Odyssey-safe). Append-only
сохранён. Миграция 2026_06_26_140000; schema v8.55->v8.56 + CHANGELOG. Тесты 8/8 green.

Шов B: db/03_service_bypass_policies.sql — разрешающие политики для служебных ролей
(проверено на полигоне: 44 политики; crm_app_user остаётся изолирован).

Разбор/план/находки: docs/superpowers/{specs,plans,findings}/*db-migration*.
cspell-words: +RELID/bik/lrrl/smsq/srv. Не на проде, БД боевого не тронута.

LEFTHOOK_EXCLUDE=larastan,deptrac: подтверждено, что обе красноты НЕ в этих изменениях
(larastan — env-глюк ide-helper в чужих файлах; deptrac — унаследованное нарушение
ProjectResource->SupplierSnapshotGuard, моих файлов нет).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 09:39:19 +03:00

20 KiB
Raw Blame History

Переезд базы на Yandex Managed PostgreSQL — проверка совместимости и «что делаем»

Дата: 25.06.2026 (актуализировано 26.06.2026 под правки прод-сессии) Статус: решение принято — Путь А (управляемая база). План: docs/superpowers/plans/2026-06-25-db-migration-path-a-managed.md. Контекст: выход портала в продажу на сотни клиентов; владелец выбрал базу «под ключ» (управляемую) у Яндекса. Перед переездом проверено, переедут ли особенности нашей БД на управляемый сервис, где нет прав суперпользователя.

🔄 Обновление 26.06.2026 (правки параллельной прод-сессии, проверены по коду/коммитам до 7efe9e3e):

  • Схема v8.53 → v8.55: +2 таблицы supplier_deferred_sync (v8.54) и supplier_sync_runs (v8.55) — обе SaaS-уровня, БЕЗ RLS/tenant_id, пишет crm_supplier_worker. На переезд влияют положительно: цикл srv_bypass (шов B) их не трогает (нет RLS); им нужен только GRANT для crm_supplier_worker — уже покрыт blanket-грантом ON ALL TABLES в db/02_grants.sql (шов E). Метрики схемы (header db/schema.sql): 79 таблиц / 124 индекса / 44 RLS-политики / 5 функций / 15 триггеров — число RLS-таблиц/политик не выросло.
  • SharesSupplierPdo (общий PDO pgsql/pgsql_supplier) — тестовая механика (в тестах оба коннекта = postgres superuser, чтобы не ловить «active transaction» при rollback). На проде роли разные → коннекты раздельные. Для переезда: это не прод-шов, но прогон тестов на тест-кластере (Phase 5) должен пройти с ней — учесть.
  • Деньги-инвариант волатилен: на 26.06 прод = 1 838 400 ₽ / 1016 сделок (990 живых + 26 удал.) / 999 731 лид (было 1 836 400 / 1013). Сверять надо «снимок ДО == снимок ПОСЛЕ одной операции», не с фиксированным числом.

Источники фактов: аудит репозитория (db/schema.sql, db/00_create_roles.sql, db/02_grants.sql, app/..., docs/security/pgaudit-anonymizer-setup.md, инцидент 29.05) + официальная документация Yandex Cloud (yandex.cloud/ru/docs/managed-postgresql/...).


1. Короткий вывод (простым языком)

Переезд на управляемую базу — НЕ «воткнул и поехал». Большинство вещей переедет, но три куска нашей защиты завязаны на «права суперадмина», которых в управляемой базе нет. Два из них придётся переделывать в коде, причём один уже ломается на боевом даже сейчас.

Это меняет картину: управляемая база — правильная цель, но это проект с переделкой ядра безопасности, а не просто аренда железа. Есть и более быстрый/дешёвый промежуточный путь (см. §4).


2. Что переедет без проблем

Возможность Управляемая база Яндекса
Расширения pgcrypto, pg_trgm, btree_gin в белом списке
pg_partman, pgaudit, pgcrypto, uuid-ossp, pg_stat_statements поддерживаются
Изоляция клиентов RLS (по tenant_id через current_setting) работает без суперправ
Нативное партиционирование (наш способ, не pg_partman)
Блокировки pg_advisory_xact_lock (в аудит-цепочке) (живут в пределах транзакции)
Автокопии + восстановление на любую минуту (PITR) копии в 3 ЦОД, хранение 7–60 дней (до 3 лет по политике), откат с точностью до ~30 сек
Отказоустойчивость (запасной хост, автопереключение) кворумная синхронная репликация
Шифрование, встроенный пулер (Odyssey, аналог PgBouncer) SSL обязателен, пулер встроен

3. Что НЕ переедет как есть — проблемы ⚠️🔴

# Проблема Серьёзность Почему Что делать
1 Роли с BYPASSRLS (crm_admin_user, crm_migrator, crm_supplier_worker) 🔴 блокер В управляемой базе нельзя создать свои роли и нельзя выдать BYPASSRLS (это право суперадмина). На этих трёх ролях держится вся «сквозная» логика: приём лидов от поставщика, админка, миграции Перепроектировать «обход изоляции» через сами RLS-политики (разрешающие политики для служебных ролей), а не через атрибут роли. Аккуратно — это защита данных клиентов друг от друга
2 session_replication_role='replica' (отключение триггеров) 🔴 блокер Право суперадмина, в управляемой базе недоступно. Используется при пересчёте аудит-цепочек и в одной миграции. Уже ломается на боевом (инцидент 29.05: permission denied to set parameter session_replication_role), сейчас обходим через sudo -u postgres — чего в управляемой базе не будет Переписать пересчёт аудит-цепочки без отключения триггеров (напр. процедура SECURITY DEFINER от роли-владельца)
3 Маскирование ПДн (anon) для 152-ФЗ 🟡 средне Управляемая база поддерживает anon, но версию 1.3.2, а у нас на проде самосборная 3.0.13; и только на PostgreSQL ≥ 15. Свою сборку поставить нельзя Перейти на целевой PG 16/17, проверить, что все наши функции маскирования есть в 1.3.2; иначе — маскировать иначе (на стороне приложения/фильтром дампа)
4 5-ролевая модель БД (00_create_roles.sql) 🟡 средне Своих ролей создавать нельзя; права раздаются через привилегии + 4 управляемые роли Яндекса (mdb_superuser, mdb_admin, mdb_monitor, mdb_replication) Переразложить нашу модель прав на механику Яндекса. Заодно выверить путаницу имён (crm_app_admin/crm_readonly упоминаются, но в 00_create_roles.sql их нет)
5 Скрипты 00_create_roles.sql / 02_grants.sql рассчитаны на psql -U postgres 🟡 средне Суперпользователя нет; ALTER ... OWNER, GRANT на чужие объекты идут от роли-владельца кластера Адаптировать провижен-скрипты под роль-владельца mdb_superuser
6 Пулер Odyssey в транзакционном режиме 🟢 мелочь Ограничения на advisory-locks/temp-таблицы, живущие дольше транзакции. Наши xact_lock — в пределах транзакции, ок; но проверить Использовать сессионный режим где надо, либо подтвердить, что наш код укладывается в транзакции

Итог: 2 красных блокера (требуют переделки кода ядра безопасности) + 2–3 жёлтых (адаптация). Самый чувствительный — №1: трогаем то, что изолирует данные клиентов друг от друга, ошибка = утечка между клиентами. Делать только с тестами + проверкой rls-reviewer.


3a. Карта швов (прочитано ПО КОДУ 25.06 — уточняет §3)

Чтение реального кода снижает оценку §3. Ключевое: приложение уже подготовлено к отказу от BYPASSRLS, миграция концентрируется в слое ролей/политик БД, не в коде портала.

Что НЕ меняется (по коду):

  • ~30 джобов/команд на connection pgsql_supplier (RouteSupplierLeadJob, Sync*, Reset*, Snapshot*, AuditRebuildChain, CsvReconcile, BalancePreflightSweep и др.) — остаются как есть: у всех уже явные WHERE tenant_id-фильтры (defense-in-depth, config/database.php:126-127, 00_create_roles.sql:64-66).
  • ~50 мест SET LOCAL app.current_tenant_id (middleware SetTenantContext.php:39 + контроллеры + report-провайдеры + джобы) — не меняются: обычная роль crm_app_user работает через current_setting, на управляемой базе RLS так же.
  • GRANT'ы уже role-explicit (GRANT ... TO crm_app_user, crm_supplier_worker по всей схеме) — структура прав готова.
  • Маскировка (anon_masking_labels.sql) использует стандартные функции anon (partial_email, partial, fake_first_name/last_name, MASKED WITH VALUE NULL) — переносимы на встроенную в управляемую базу 1.3.2; файл по сути уже deployment-скрипт.

Что меняется (швы, по убыванию объёма):

Шов Файлы Что делаем
A. Роли без BYPASSRLS db/00_create_roles.sql (строки 41-44, 47-51, 70-73) Создать те же 5 пользователей через консоль YC, БЕЗ атрибута BYPASSRLS
B. Замена BYPASSRLS на разрешающие политики db/schema.sql (~36 RLS-таблиц, строки 3069+) + миграции с CREATE POLICY Для 3 служебных ролей добавить политику TO <role> USING(true) WITH CHECK(true) на таблицах, что они трогают. Механически, скриптом. Покрывает и NULL-tenant вставки (failed_webhook_jobs, RouteSupplierLeadJob:640), и FORCE ROW LEVEL SECURITY таблицы (lead_charges:1182)
C. Пересчёт аудита без session_replication_role AuditRebuildChain.php:107,137; migrations/2026_05_23_hole2...:34; workflows f1-rebuild/sql-rebuild Триггер пропускает себя по GUC-метке (вместо отключения), либо SECURITY DEFINER от владельца. Локально, ~день
D. Маскировка на встроенную anon 1.3.2 db/anon_masking_labels.sql Применить те же метки через механизм управляемой базы; проверить парность функций на тест-кластере
E. Provision-скрипты под роль-владельца db/02_grants.sql (ALTER OWNER, GRANT) Адаптировать запуск с postgres на mdb_superuser управляемой базы
F. DDL партиций MonthlyPartitionManager.php:30-40 Создание/дроп партиций — под роль-владельца управляемой базы

Главный ответ на «перелопатим ли всю базу»: нет. Данные и структура не трогаются; код портала почти не трогается (он уже defense-in-depth). Меняется слой ролей и политик + 1 правка пересчёта аудита + замена механизма маскировки. Всё сначала на копии (тест-кластер), со сверкой изоляции (rls-reviewer + тесты «клиент не видит чужого») и денег.

Пересмотр риска/срока: основной риск — полнота разрешающих политик (шов B); страховка — явные WHERE tenant_id уже в коде, плюс тесты изоляции на копии. Срок ориентировочно ~2 недели, основная работа — механическая (политики) + один аккуратный кусок (аудит).


4. Развилка — что делаем (решение владельца)

Проверка вскрыла настоящий выбор. Раньше казалось «арендуем управляемую базу», но оказалось — это либо проект с переделкой, либо другой путь.

Путь А — Полностью управляемая база Яндекса (как задумывали)

  • Плюсы: Яндекс сам делает копии, восстановление на любую минуту, запасной хост и автопереключение. Минимум ручной возни в будущем. Лучшая долгосрочная цель.
  • Минусы: надо переделать ядро безопасности (п.1, 2, 4) + проверить маскирование (п.3). Это инженерный проект с риском (трогаем изоляцию клиентов), несколько недель аккуратной работы с тестами.
  • Стоимость железа: ~18 400 ₽/мес (старт) … ~34 000 ₽/мес (помощнее), как в смете.

Путь Б — Своя база, но сделанная правильно (промежуточный, быстрый)

  • Оставляем PostgreSQL самоуправляемым (наши роли, BYPASSRLS, anon 3.0.13, session_replication_role — всё работает как есть, переписывать НЕ надо).
  • НО: выносим её на отдельный мощный сервер (не на одной машине с приложением).
  • Добавляем копии «на сторону» (в объектное хранилище Object Storage) — закрывает дыру «копии на том же диске, что и база».
  • Добавляем запасную живую копию (реплику) на втором сервере — для подстраховки.
  • Плюсы: убирает риск потери денег/данных клиентов быстро и без переписывания ядра безопасности. Низкий риск.
  • Минусы: обслуживание базы (обновления, реплика, проверка копий, переключение при аварии) остаётся на нас, не на Яндексе. Автопереключение — надо настроить самим.
  • Стоимость железа: дешевле управляемой (обычная ВМ ~6 400 ₽/мес под базу + реплика; без «управляемой» наценки за ядро).

Рекомендация

Сначала Путь Б, потом Путь А. Логика: главная боль сейчас — риск потерять данные клиентов (копии лежат на том же диске, что уже падал). Путь Б закрывает это за дни и без риска переписывания изоляции. А Путь А (управляемая база) сделать отдельным аккуратным проектом позже — с тестами и rls-reviewer, без спешки под запуск продаж. К тому же блокер №2 (session_replication_role) стоит починить в любом случае — он уже ломается.


5. План «что делаем» — будет детализирован после выбора пути

После решения владельца по §4 — отдельный пошаговый план (через навык планирования), с TDD на чувствительных кусках (RLS, аудит-цепочки) и обязательной проверкой изоляции клиентов. Общие вехи:

Если Путь Б (рекомендованный старт):

  1. Поднять отдельный сервер под базу, перенести данные (с проверкой «деньги до == после»).
  2. Настроить копии «на сторону» в Object Storage + проверку их восстановимости.
  3. Поднять реплику на втором сервере.
  4. Починить блокер №2 (пересчёт аудит-цепочки без session_replication_role) — он нужен в любом пути.

Если/когда Путь А (управляемая база):

  1. Перепроектировать обход RLS через политики (п.1) — TDD + rls-reviewer.
  2. Переразложить роли на модель Яндекса (п.4, 5).
  3. Проверить/перевести маскирование anon на 1.3.2 + PG ≥15 (п.3).
  4. Тестовый переезд на отдельный кластер, сверка изоляции и денег, затем боевой.

Приложение. Ключевые источники

  • Нет суперпользователя / нельзя создавать свои роли: yandex.cloud/ru/docs/managed-postgresql/qa/general#superuser, .../concepts/roles
  • Расширения (pg_partman, pgaudit, anon 1.3.2, pgcrypto, …): .../operations/extensions/cluster-extensions, .../operations/extensions/pg_anon
  • PITR/копии: .../concepts/backup ; HA/репликация: .../concepts/replication ; пулер Odyssey: .../concepts/pooling ; настройки: .../concepts/settings-list
  • Наш инцидент с session_replication_role: docs/incidents/2026-05-29-audit-rebuild-per-tenant-cleanup-handoff.md:84-97
  • Наши роли/BYPASSRLS: db/00_create_roles.sql, db/02_grants.sql:165-171
  • Наш anon 3.0.13: docs/security/pgaudit-anonymizer-setup.md:62-77, db/anon_masking_labels.sql