# Переезд базы на 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`
