Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
9.5 KiB
DNS Autoresolver — дизайн
Дата: 2026-07-03 Статус: черновик на ревью
Контекст и цель
Утилита для автонастройки и проверки DNS-зон у внешних провайдеров. Пользователь заранее готовит шаблоны базовых настроек зоны. Утилита подключается к учётной записи провайдера, получает список доменов (их может быть несколько), сверяет текущие настройки каждого домена с привязанным шаблоном, подсвечивает отклонения (diff) и предлагает привести зону в соответствие после ручного подтверждения.
Провайдеры расширяемы; первый — Selectel (DNS API v2 "actual", https://docs.selectel.ru/api/dns-actual/).
Проектируется как мультитенантный веб-сервис: UI/UX-сайт + Go API, авторизация, разделение по пользователям и их проектам, в перспективе — периодические проверки, уведомления и метрики для алертов.
Общая архитектура
┌─────────────────┐
React SPA ◄────►│ Go API (REST) │
├─────────────────┤
│ Domain core │ диф-движок шаблон↔зона (чистые функции)
│ Provider layer │ интерфейс Provider (Selectel — первый)
│ Repository │ PostgreSQL, multi-tenant
└────────┬────────┘
│
┌────────▼────────┐
│ PostgreSQL │ users/projects/provider_accounts/
│ │ templates/domains/check_runs
└─────────────────┘
Каждый модуль имеет одну ответственность и общается через явные интерфейсы:
- Provider layer — единственный, кто знает про конкретный API провайдера. Мапит его
ответы в нейтральную модель
Record. Домен-ядро не знает про Selectel. - Domain core — диф-движок: чистые функции без сайд-эффектов, тестируются изолированно.
- Repository — доступ к PostgreSQL, все запросы размечены
user_id/project_id. - Go API — REST/JSON, транспорт; оркестрирует provider + core + repository.
- React SPA — весь UI; общается только через REST API.
Ключевая абстракция — Provider
Точка расширяемости. Новый провайдер = новая реализация интерфейса + маппинг его API в нейтральную модель.
type Provider interface {
Name() string
ListZones(ctx context.Context, creds Credentials) ([]Zone, error)
GetRecords(ctx context.Context, creds Credentials, zoneID string) ([]Record, error)
ApplyChanges(ctx context.Context, creds Credentials, zoneID string, cs Changeset) error
}
Нейтральная модель:
type Record struct {
Type string // A, AAAA, CNAME, MX, TXT, SRV, NS, SOA
Name string // относительное или FQDN имя
Values []string // значения (RRset)
TTL int
Priority *int // MX/SRV
// SRV: разбирается из Values (priority weight port target)
}
Диф-движок
Чистая функция без сайд-эффектов:
func Diff(template []Record, actual []Record) Changeset
// Changeset{ ToAdd, ToUpdate, ToDelete, InSync []RecordDiff }
- Сравнение по ключу
(Type, Name), затем по нормализованным значениям/TTL. - Каждое отклонение помечается для подсветки в UI/API.
- Применение (
ApplyChanges) — только после ручного подтверждения, гранулярно по домену/записи.
Управляемые типы записей
| Тип | Режим | Комментарий |
|---|---|---|
| A, AAAA, CNAME, MX, TXT | Управляемые (diff + apply) | ядро шаблонов |
| SRV | Управляемые (diff + apply) | почтовый автодискавери: _autodiscover._tcp, _submission._tcp, _imaps._tcp, _pop3s._tcp и т.п. — задаются в шаблонах |
| NS, SOA | Read-only | отклонения показываются, но не применяются автоматически |
Модель данных (multi-tenant с первого дня)
users
└─ projects
├─ provider_accounts (provider, зашифрованные креды)
├─ templates (набор Record, versioned)
└─ domains (zone, provider_account_id, template_id)
└─ check_runs (история сверок: результат diff, timestamp)
- Креды провайдера шифруются в БД (например, AES-GCM с ключом из env/секрет-менеджера).
- Даже в Фазе 1 без логина строки размечены
user_id/project_id(единый системный владелец) — авторизация «включается» в Фазе 2 без миграции данных. - Шаблоны версионируются, чтобы
check_runsссылались на конкретную версию.
Фазы
Проект большой — режется на подсистемы, каждая со своим spec → план → реализация.
- Фаза 1 (MVP-ядро): Provider-интерфейс + реализация Selectel, диф-движок, PostgreSQL-схема и repository, REST API, ручной apply. Мультитенантная схема, но единый владелец (без регистрации). React-UI тонкий; ядро покрыто тестами.
- Фаза 2: Авторизация (регистрация/логин, сессии/JWT), полноценный React UI с визуальным дифом и управлением шаблонами/учётками.
- Фаза 3: Периодические проверки по расписанию, уведомления, метрики для алертов.
Обработка ошибок
- Provider layer оборачивает ошибки API (таймауты, 4xx/5xx, rate limit) в типизированные ошибки; retries с backoff для идемпотентных операций.
ApplyChangesне должен оставлять зону в промежуточном состоянии: применять changeset атомарно, где API позволяет; иначе — фиксировать частичный результат вcheck_runs.- API возвращает структурированные ошибки (code + message) для UI.
Тестирование
- Диф-движок — модульные тесты на все ветки (add/update/delete/in-sync, SRV, NS/SOA RO).
- Provider Selectel — тесты против замоканного HTTP (записанные ответы API).
- Repository — интеграционные тесты на PostgreSQL (testcontainers или локальная БД).
- API — тесты хендлеров с мок-провайдером.
Решённые развилки
- Язык: Go (backend), React SPA (frontend).
- БД: PostgreSQL.
- Применение: diff + ручное подтверждение.
- Хранение: всё в БД, разделение по пользователям и проектам.
- Типы: A/AAAA/CNAME/MX/TXT/SRV управляемые, NS/SOA read-only.
- Git: репозиторий инициализирован.
- Scope Фазы 1: надёжное API + диф-движок, минимальный UI.
- Аутентификация Selectel: статический API-ключ. При добавлении учётки в UI показываем инструкцию и ссылку, где и как его получить (панель Selectel).
- Шаблоны в БД: JSONB — набор
Recordхранится как JSONB-документ (versioned). - Шифрование кредов: симметричный ключ из env (AES-GCM) в Фазе 1; внешний секрет-менеджер — возможное улучшение позже.
Заметки по UI (для будущих фаз)
- Экран добавления provider-учётки Selectel содержит: поле API-ключа, краткую инструкцию и ссылку на страницу получения ключа в панели Selectel.