This commit is contained in:
Denozordec
2026-04-03 21:36:21 +07:00
commit cbf3aaa485
@@ -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/<service>` + `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 узлах, затем шаблон для остальных.