From cbf3aaa4850fb1160a97d9e88d249815b00cf49b Mon Sep 17 00:00:00 2001 From: Denozordec Date: Fri, 3 Apr 2026 21:36:21 +0700 Subject: [PATCH] init --- .../plans/evobgp_архитектура_0e73ef02.plan.md | 368 ++++++++++++++++++ 1 file changed, 368 insertions(+) create mode 100644 .cursor/plans/evobgp_архитектура_0e73ef02.plan.md diff --git a/.cursor/plans/evobgp_архитектура_0e73ef02.plan.md b/.cursor/plans/evobgp_архитектура_0e73ef02.plan.md new file mode 100644 index 0000000..ad5460a --- /dev/null +++ b/.cursor/plans/evobgp_архитектура_0e73ef02.plan.md @@ -0,0 +1,368 @@ +--- +name: EvoBGP архитектура +overview: "Control-plane на Go в Docker: микросервисы (API, планировщик, ingest, генерация BIRD, доставка на узлы), MySQL, брокер очередей; префиксы из AS/CDN/доменов, расписания, DoH, community-справочник, REST, ревизии и откат; data-plane — BIRD и агент на узлах." +todos: + - id: schema-mysql + content: "Схема MySQL: модули, расписания, CDN-источники, DoH-профили, справочник community, привязки, пиры, узлы, ревизии, jobs" + status: pending + - id: bird-generator + content: Определить формат bird.conf фрагментов, фильтры и точки reload/configure + status: pending + - id: rest-jobs + content: Спецификация REST (refresh модуля, apply, preview, rollback) и async jobs + status: pending + - id: node-agent + content: Протокол доставки конфига на до 20 узлов (агент + версии + canary) + status: pending + - id: observability + content: Метрики, алерты на дрейф префиксов, статус пиров + status: pending + - id: docker-ms + content: Dockerfile сервисов, compose (dev), сети/volumes, healthchecks + status: pending + - id: go-modules + content: Структура Go-модулей, общие пакеты (db, models, bird templating) + status: pending +isProject: false +--- + +# EvoBGP: быстрый анонс префиксов через BIRD + MySQL + REST + +Репозиторий сейчас без кода — план описывает целевую архитектуру «с нуля». **Backend — Go.** Развёртывание control-plane — **контейнеры Docker**; оркестрация: `docker compose` для разработки, в production — Kubernetes / Nomad / Swarm по выбору. + +## Целевая картина (логическая) + +- **BIRD** — источник истины на уровне маршрутизации; **MySQL** — для политики, ревизий и материализованных префиксов. +- **Быстрота:** очередь задач, идempotent-воркеры, при необходимости `birdc configure` после атомарной подмены include-файлов. + +### Диаграмма: микросервисы и Docker (control-plane + data-plane) + +```mermaid +flowchart TB + subgraph edge [Периметр] + LB[Traefik или Nginx] + end + subgraph docker [Docker host или кластер] + API[evobgp-api Go] + SCH[evobgp-scheduler Go] + ING[evobgp-ingest Go] + REN[evobgp-render Go] + DEP[evobgp-deploy Go] + MQ[(NATS или Redis Streams)] + DB[(MySQL)] + end + subgraph nodes [До 20 узлов] + AG1[evobgp-agent Go] + BR1[BIRD] + AG1 --> BR1 + end + LB --> API + API --> DB + API --> MQ + SCH --> DB + SCH --> MQ + ING --> MQ + ING --> DB + REN --> MQ + REN --> DB + DEP --> MQ + DEP --> DB + DEP -->|mTLS pull или push| AG1 +``` + + + +Назначение сервисов (можно объединять на раннем MVP, границы — контракты между пакетами): + + +| Сервис | Роль | +| -------------------- | ---------------------------------------------------------------------------------------------------------------- | +| **evobgp-api** | REST, аутентификация, CRUD сущностей, постановка задач (`refresh`, `apply`, `rollback`), `GET /jobs`. | +| **evobgp-scheduler** | Читает интервалы модулей и CDN-строк из MySQL, публикует события «пора обновить модуль/источник» в очередь. | +| **evobgp-ingest** | Fetch CDN, DoH-резолв доменов, загрузка AS/префиксов; пишет материализованные строки и сырые метаданные в MySQL. | +| **evobgp-render** | Собирает итоговый набор префиксов + community, создаёт ревизию, генерирует артефакты BIRD (текст конфигов). | +| **evobgp-deploy** | Доставка артефактов на узлы, учёт `node_config_version`, canary. | +| **evobgp-agent** | Отдельный образ для узла: получение конфига, запись в volume, вызов `birdc`, отчёт о версии. | + + +Инфраструктурные контейнеры: **MySQL**, **брокер очередей** (NATS JetStream или Redis), опционально **Valkey/Redis** для кэша и rate-limit по модулям. + +--- + +## 1. MySQL: сущности и версионирование + +Рекомендуемые группы таблиц: + + +| Область | Назначение | +| ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Модули** | Тип: `AS_PREFIXES` / `CDN_CIDRS` / `DOMAINS`; включён/выключен; приоритет; ссылки на расписание и (для доменов) DoH-профиль. | +| **Расписания обновления** | Базовый интервал на **модуль** (`refresh_interval_sec`, cron или interval); см. ниже про переопределение на CDN. | +| **Источники CDN внутри модуля** | Для типа `CDN_CIDRS`: несколько записей «URL/статический список» на модуль; у **каждой** записи свой опциональный `refresh_interval_sec` (если NULL — брать интервал модуля). | +| **Профили DoH** | URL HTTPS DoH (`https://…/dns-query`), опционально имя для SNI, таймауты, доверие к сертификату (политика); привязка к модулям `DOMAINS` или глобальный default. | +| **Содержимое модуля** | AS и префиксы; CDN-строки; FQDN; у каждой сущности — **привязка к community** (FK). | +| **Справочник BGP community** | Канонические записи: `standard` (65535:123), `large` (x:y:z) при необходимости, человекочитаемое имя, описание, `tenant_id`. | +| **Привязки community** | Связь «сущность → community»: для **домена**, **ASN**, **префикса/CIDR** (в т.ч. из CDN-листа) — `community_id`; при генерации BIRD маршруты/фильтры получают соответствующий `bgp_community.add()`. | +| **Пиры** | neighbor IP, ASN, пароли/ключи (лучше ссылка на секреты), BGP параметры, привязка к группе узлов. | +| **Узлы** | Идентификатор узла (hostname), роль, теги для «каким пирам/политикам подчиняться». | +| **История (append-only)** | Снимок состояния или дифф после каждого успешного применения; `revision_id`, автор (API key/user), timestamp. | +| **Журнал заданий** | Очередь «пересобрать модуль X», «откатить на revision Y», статус, ошибки. | + + +### Расписание: модуль и отдельно каждый CDN-источник + +- **Уровень модуля:** поля вроде `refresh_interval_seconds` и/или `cron_expr` (если нужны окна по времени); воркер ставит следующий запуск по минимальному интервалу среди дочерних сущностей. +- **Уровень CDN-строки** (таблица `module_cdn_sources` или аналог): для каждого URL/файла — **опционально** свой `refresh_interval_seconds`. Если задан — имеет приоритет над модулем для **fetch** этого списка; если NULL — используется интервал модуля. +- **Домены и AS:** по умолчанию следуют расписанию модуля; при необходимости позже можно добавить переопределение на уровне отдельной записи FQDN (аналогично CDN). + +### DoH для резолва доменов + +- В БД хранится **профиль** (`doh_profiles`): базовый URL DoH, при необходимости заголовки/API-ключ (секрет — вне БД или зашифровано), таймаут, лимит параллельных запросов. +- Модуль типа `DOMAINS` ссылается на `doh_profile_id` (или наследует default из глобальных настроек). +- Воркер выполняет DNS-запросы **только через выбранный DoH** (RFC 8484 JSON или wire-format POST — зафиксировать один поддерживаемый режим в реализации), а не системный stub resolver, чтобы поведение было предсказуемым и привязанным к политике. + +### Справочник community и привязки + +- Таблица `bgp_community` (справочник): имя, тип (`standard` / `large` / `extended` — по потребности), значения полей, уникальность по `(tenant_id, representation)`. +- Для **каждой** привязываемой сущности в контенте модулей — поле `community_id` (nullable: «наследовать от модуля» или «без community» по политике): + - домен → community; + - ASN (в AS-модуле) → community; + - CIDR/префикс (в т.ч. строка из CDN-листа после парсинга) → community. +- **Разрешение конфликтов:** если один префикс попал из двух источников с разными community — в плане заложить явное правило (приоритет модуля, приоритет специфичности CIDR, или запрет дубликата с алертом). Зафиксировать в конфиге по умолчанию: «более специфичный источник wins» + лог предупреждения. + +**Генерация BIRD:** для каждого итогового префикса (или группы) в `filter` экспорта добавляется соответствующий `bgp_community.add(...)` из справочника; именованные community можно сгенерировать как `define` в отдельном include. + +**Откат:** не переписывать текущее состояние «вручную», а хранить **ревизии** (например JSON-снимок или нормализованные строки в history-таблицах). Операция rollback = `INSERT` новой ревизии с содержимым выбранной старой + триггер перегенерации. Так история остаётся линейной и аудируемой. + +**Дополнительно:** мягкие блокировки (`SELECT ... FOR UPDATE` на уровне модуля/узла при применении), чтобы два REST-вызова не портили друг друга. + +### Диаграмма ETL (от источников до BIRD и ревизий) + +```mermaid +flowchart LR + subgraph extract [Extract] + CDN[CDN URL и HTTP fetch] + DOH[DoH A или AAAA] + AS[AS и префиксы из БД] + end + subgraph transform [Transform] + NORM[Нормализация CIDR] + DEDUP[Дедуп и конфликты] + COMM[Подстановка community] + end + subgraph load [Load] + MYSQL[(MySQL материализация)] + REV[revision и snapshot] + ART[Артефакты BIRD] + end + subgraph out [Выход] + BIRD[BIRD на узлах] + end + CDN --> NORM + DOH --> NORM + AS --> NORM + NORM --> DEDUP + DEDUP --> COMM + COMM --> MYSQL + COMM --> REV + REV --> ART + ART --> BIRD +``` + + + +Пояснение: **Extract** разнесён по сервису `evobgp-ingest`; **Transform** частично в ingest, частично в `evobgp-render` (финальное объединение модулей); **Load** — транзакции в MySQL + запись файлов/объектов для деплоя. + +### Диаграмма схемы MySQL (сущности и зависимости FK) + +Схема упрощена; имена таблиц — ориентир для миграций (`golang-migrate` / `goose`). + +```mermaid +erDiagram + tenant ||--o{ module : owns + tenant ||--o{ bgp_community : owns + tenant ||--o{ bgp_peer : owns + doh_profile ||--o{ module : uses + module ||--o{ module_cdn_source : contains + module ||--o{ module_domain_entry : contains + module ||--o{ module_as_entry : contains + bgp_community ||--o{ module_domain_entry : tags + bgp_community ||--o{ module_as_entry : tags + bgp_community ||--o{ module_cdn_source : tags + bgp_node ||--o{ node_peer_binding : has + bgp_peer ||--o{ node_peer_binding : has + module ||--o{ config_revision : produces + config_revision ||--o{ revision_materialized_prefix : snapshot + module ||--o{ job_audit : async_tasks +``` + + + +### Пояснения к таблицам MySQL + +Ниже — **назначение**, **основные поля (логически)** и **кто пишет/читает**. Точные типы и индексы задаются в миграциях. + + +| Таблица | Назначение | Ключевые поля и смысл | Кто использует | +| -------------------------------- | ------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | +| **tenant** | Изоляция клиентов/организаций (multi-tenant). | `id`, `name`, `slug`, статус; все сущности ниже с `tenant_id` при необходимости. | API, все сервисы при фильтрации. | +| **module** | Логический блок политики: тип `AS_PREFIXES` / `CDN_CIDRS` / `DOMAINS`, включён ли, приоритет при конфликтах. | `tenant_id`, `type`, `enabled`, `priority`, `doh_profile_id` (для DOMAINS), `refresh_interval_sec` (и/или `cron_expr`), `default_community_id` (опционально для наследования). | API (CRUD), scheduler (интервалы), ingest/render (содержимое). | +| **doh_profile** | Параметры DNS over HTTPS для резолва доменов. | `url` (HTTPS DoH), таймауты, ссылка на секрет (ID в Vault/K8s, не сам пароль в открытом виде), политика TLS. | Модули DOMAINS, ingest. | +| **module_cdn_source** | Один источник CDN внутри модуля типа `CDN_CIDRS`: URL или встроенный список. | `module_id`, `source_kind` (url / inline), `url`, `etag`, `last_modified`, `refresh_interval_sec` (если NULL — брать из `module`), `community_id` (тег для префиксов из этого источника). | Ingest (fetch), render. | +| **module_domain_entry** | Одна строка FQDN в модуле `DOMAINS`. | `module_id`, `fqdn`, `community_id`, опционально переопределение интервала; материализованные поля после резолва можно хранить в отдельной таблице или здесь (`last_resolved_at`, хэш ответа). | Ingest (DoH), render. | +| **module_as_entry** | Одна строка в модуле `AS_PREFIXES`: ASN и/или явный префикс. | `module_id`, `asn`, `prefix` (nullable если задаётся только ASN), `community_id`. | API, render (если данные не из внешнего IRR — тогда расширить провайдером). | +| **bgp_community** | Справочник BGP community для экспорта в BIRD. | `tenant_id`, `name`, `kind` (standard/large/extended), числовые поля значения, уникальность в рамках tenant. | API, render (генерация `filter` / `define`). | +| **bgp_peer** | Описание BGP-соседа (логический пир). | `tenant_id`, neighbor IP, remote ASN, локальные политики, **ссылка на секрет** (MD5/TC), `group_name`/`tags` для выбора на узлах. | API, render (`peers.conf`), deploy. | +| **bgp_node** | Узел сети, где крутится BIRD и агент. | `tenant_id`, `hostname`, `api_endpoint` или идентификатор для mTLS, `tags`, `last_applied_revision_id`. | deploy, API (статус), мониторинг. | +| **node_peer_binding** | Какие пиры подняты на каком узле (many-to-many). | `bgp_node_id`, `bgp_peer_id`, возможно переопределение при необходимости. | API, render (генерация только релевантных сессий на узел). | +| **config_revision** | Неизменяемая точка истории после успешного применения политики. | `id`, `module_id` или `NULL` (глобальная ревизия), `created_at`, `author`, `hash` префикс-сета, ссылка на артефакт (путь/URL в object storage), `parent_revision_id` (для отката как «новая ревизия со старым содержимым»). | render, deploy, API (rollback, audit). | +| **revision_materialized_prefix** | Снимок итоговых префиксов для ревизии (для быстрого diff и отката без пересчёта из сырья). | `revision_id`, `prefix`, `cidr_len`, `community_id`, `source` (модуль/тип). | render (запись), API (preview, diff), откат. | +| **job_audit** | Журнал асинхронных операций (дополняет брокер, не заменяет его). | `id`, `kind` (refresh / apply / rollback), `module_id`, `status`, `error`, `idempotency_key`, `created_at`. | API, операторы, ретраи. | + + +**Дополнительно (по необходимости):** + +- `**global_settings`** — ключ–значение для дефолтного DoH, лимитов, feature flags; одна строка на tenant или плоская таблица. +- `**module_cdn_fetch_log`** (опционально) — сырые ответы HTTP, размер, время, для отладки CDN; с TTL очистки. + +Очередь в **NATS/Redis** в MySQL не дублируется обязательно; `job_audit` нужен для **идемпотентности**, **аудита** и отображения статуса в UI. + +### Диаграмма зависимостей сервисов и компонентов + +```mermaid +flowchart TB + subgraph clients [Клиенты] + CLI[CLI или CI] + UI[Опционально UI] + end + clients --> LB + LB[Reverse proxy] + LB --> API + API --> MYSQL[(MySQL)] + API --> MQ[Message broker] + SCH[scheduler] --> MYSQL + SCH --> MQ + ING[ingest] --> MQ + ING --> MYSQL + ING --> EXT[Интернет CDN и DoH] + REN[render] --> MQ + REN --> MYSQL + DEP[deploy] --> MQ + DEP --> MYSQL + DEP --> AG[evobgp-agent] + AG --> BIRD[BIRD] + PROM[Prometheus] --> API + PROM --> ING + PROM --> AG +``` + + + +**Зависимости по данным:** все мутирующие сервисы согласуются через **MySQL** и **очередь**; агент не ходит в MySQL напрямую, только к API/deploy или к артефакт-хранилищу (S3/minio + подпись), в зависимости от выбранной реализации `evobgp-deploy`. + +--- + +## 2. Три типа include-модулей + + +| Тип | Ввод | Поведение | +| ----------------- | ---------------------------- | ------------------------------------------------------------------------------------------------------------- | +| **AS / префиксы** | Список AS или явные префиксы | Импорт из ваших таблиц или опционально **pdb/API RIR** (вне scope MVP — заложить интерфейс `PrefixProvider`). | +| **CDN CIDR** | URL или статический список | Периодический fetch + парсинг (plain text, JSON); хранить `etag`/`last_modified` для условных запросов. | +| **Домены** | FQDN | См. ниже: в BIRD **не** попадают строки доменов — только результат резолва. | + + +### FQDN → что именно кладётся в `include` BIRD + +**В конфигурацию BIRD доменные имена не записываются.** В языке BIRD нет встроенного «подставь IP по DNS при reload» для списков анонсируемых префиксов: политика и `filter` оперируют **префиксами** (и community и т.д.), а не FQDN. + +Пайплайн для модуля `DOMAINS`: + +1. **Воркер (control plane)** по расписанию модуля, по TTL или по `POST /modules/{id}/refresh` выполняет DNS lookup (A/AAAA) для каждого FQDN **через DoH-профиль** модуля (см. раздел про DoH в MySQL). +2. Результат нормализуется в префиксы, обычно **хост-префиксы** `/32` (IPv4) и `/128` (IPv6) для каждого полученного адреса; при необходимости политика может задавать агрегацию (редко для «точечных» CDN/host записей). +3. В MySQL хранятся и **исходные FQDN** (для аудита и повторного резолва), и **материализованный набор префиксов** с `resolved_at` / сроком жизни. +4. **Генератор** собирает обычный статический фрагмент, например `include "/etc/bird.d/prefixes_from_domains.conf"`, внутри — объявления вида `route` / набор в `define` / список в `filter` (конкретный синтаксис зависит от выбранной схемы BIRD 2.x), но **только из IP-префиксов**, уже полученных на шаге 2. + +Итог: **include в BIRD — это всегда уже готовые префиксы**; смена IP у DNS обновляет анонс только после следующего успешного резолва и перегенерации конфига + `birdc configure` (или эквивалент). Это совпадает с ограничением из раздела «Риски»: без частого refresh доменный модуль может отставать от реальности. + +Общий пайплайн: модуль → нормализованный список **префиксов + community из справочника по привязкам** → объединение с дедупликацией и политикой конфликтов (более специфичный wins или явный приоритет модулей). + +--- + +## 3. REST API: обновление «отдельного модуля» + +Минимальный набор эндпоинтов: + +- `POST /modules/{id}/refresh` — пересобрать только этот модуль (CDN fetch / DNS refresh / перечитать AS-данные). +- `POST /apply` или `POST /nodes/{id}/apply` — сгенерировать конфиг и применить (см. раздел про узлы). +- `GET /revisions`, `POST /revisions/{id}/rollback`. +- `POST /peers` / `PATCH /peers/{id}` — добавление/изменение пира; опционально `POST /peers/{id}/apply`. +- CRUD для **профилей DoH**, **справочника community**, **расписаний** (если вынесены из PATCH модуля) — по необходимости UI/автоматизации. + +Ответы — **202 Accepted** + `job_id`, если работа асинхронная; **GET /jobs/{id}** для статуса. + +Аутентификация: API keys или mTLS для production. + +--- + +## 4. Генерация BIRD и «на лету» + +- Генерировать **фрагменты** (`/etc/bird.d/*.conf`) и один `bird.conf` с `include`. +- Статические фильтры: префиксы из БД; для экспорта — **community** из справочника по привязкам (отдельный include с `define` или динамика в `filter` по классам префиксов). +- **RPKI** (опционально позже) — отдельный блок. +- **Пиры:** отдельный include `peers.conf`; при изменении — перезапись файла и `birdc configure` (или полный reload по политике безопасности). + +Где возможно, использовать **runtime** команды BIRD для соседей; если версия/политика требует только файл — документировать один поддерживаемый путь (проще сопровождать). + +--- + +## 5. История и быстрый откат + +- Каждое успешное применение создаёт **revision** с хэшем содержимого анонсируемого набора и ссылкой на сгенерированные артефакты (опционально хранить сам `bird` snippet в BLOB для форензики). +- Откат = создание новой ревизии с данными из выбранной + тот же pipeline генерации. +- Индексы по `(module_id, created_at)` и `(revision_id)` для быстрых запросов. + +--- + +## 6. Масштаб до ~20 узлов: предложения по улучшению + +1. **Единый control-plane, много data-plane:** один API+воркер (или небольшой кластер API за балансировщиком), на каждом узле — **агент** (лёгкий daemon), который тянет готовый конфиг/дифф по **mTLS** или получает push через message queue. Так не нужен SSH с центра на 20 хостов. +2. **Идентичность конфигурации:** таблица `node_config_version`; после деплоя агент репортит `applied_revision`. Дашборд «какой узел отстаёт». +3. **Canary / поэтапный rollout:** сначала 1–2 узла, затем остальные — снижает риск массового bad announce. +4. **Очередь и rate-limit:** массовый refresh всех CDN-модулей не должен DDOSить внешние списки; **per-module cooldown** в воркере. +5. **Наблюдаемость:** метрики (Prometheus): время генерации, размер префикс-сета, ошибки DNS/CDN, статус BIRD-сессий (через экспортер или scrape `birdc`). Алерты на **аномальный рост/падение** числа префиксов. +6. **Консистентность БД:** транзакции при записи ревизии + смене «текущего» указателя; миграции через Flyway/Liquibase или аналог. +7. **Секреты:** пароли BGP не в открытом виде в MySQL — **Vault**, Kubernetes secrets, или зашифрованные поля с KMS. +8. **Multi-tenant (если нужно):** `tenant_id` на модулях и пирах с самого начала — дешевле, чем латеральный рефакторинг. +9. **Dry-run:** `POST .../preview` возвращает diff префиксов и фрагмент BIRD без применения — обязателен для операций с 20 узлами. +10. **Резервный путь:** локальный last-known-good конфиг на узле, если центр недоступен (только чтение, без изменения политики до восстановления связи). + +--- + +## Технологический стек + +- **Backend:** **Go** (1.22+): REST на `chi` / `echo` / `fiber`; драйвер MySQL — `database/sql` + `sqlc` или GORM по согласованию команды; DNS DoH — HTTP-клиент с проверкой TLS. +- **Миграции:** `golang-migrate` или `goose`, SQL в репозитории. +- **Контейнеры:** отдельный **multi-stage Dockerfile** на сервис (минимальный образ `distroless` или `alpine`); `docker compose.yaml` для локальной среды: `mysql`, `nats` или `redis`, сервисы `api`, `scheduler`, `ingest`, `render`, `deploy`, опционально `minio` для артефактов. +- **BIRD и агент:** BIRD обычно на хосте или в **privileged** контейнере с `CAP_NET_ADMIN` и доступом к сетевому стеку; образ `evobgp-agent` монтирует volume с конфигом и взаимодействует с сокетом `birdc` (монтирование `bird.ctl`). +- **Наблюдаемость:** OpenTelemetry / Prometheus metrics в каждом Go-сервисе; единый `health` endpoint для оркестратора. + +--- + +## Риски и границы + +- **Домены в BGP:** IP меняются; без короткого TTL и мониторинга возможны утечки/дыры. Заложить политику «максимальный срок жизни записи». +- **DoH:** недоступность выбранного резолвера блокирует обновление доменного модуля; иметь **fallback** (второй профиль или кратковременный отказ в смене префиксов с алертом) — по политике эксплуатации. +- **Согласование «что анонсировать»** с регистрацией в RIR/IRR — отдельная дисциплина; система может лишь **не выходить за заданные в БД границы** (prefix filters). +- **Community:** ошибка в справочнике или привязке ведёт к неверной маркировке трафика у апстримов; обязательны preview/diff перед apply и аудит изменений справочника. + +--- + +## Предлагаемые этапы внедрения + +1. Репозиторий Go (monorepo `cmd/` + `internal/`), MySQL-миграции, `docker compose` с MySQL и брокером. +2. Сервис `evobgp-api` + `evobgp-ingest` (один тип модуля) + очередь; затем `evobgp-render` и генерация BIRD. +3. `evobgp-scheduler` и политики интервалов (модуль + CDN-строка). +4. `evobgp-deploy` + `evobgp-agent`, ревизии и rollback в БД. +5. Наблюдаемость, hardening контейнеров (non-root где возможно, read-only root), пилот на 2–3 узлах, затем шаблон для остальных. +