feat: traefik-selectel-dns — A-записи Selectel по роутам Traefik + CI сборка образа
Build / Tests (push) Successful in 13s
Build / Build image (push) Failing after 15s
Build / Notify on failure (push) Failing after 0s

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-21 21:50:07 +07:00
co-authored by Claude Sonnet 5
commit 273cd54415
35 changed files with 3722 additions and 0 deletions
+67
View File
@@ -0,0 +1,67 @@
# traefik-selectel-dns
Демон, который создаёт A-записи в Selectel DNS (API v2) для хостов из роутеров Traefik.
Сертификаты выписывает сам Traefik (встроенный `dnsChallenge.provider: selectelv2`) — кода для этого не нужно.
## Почему не плагин Traefik
Плагин (Yaegi) работает внутри цепочки обработки запросов: он не видит создание/удаление роутов
и не должен хранить токен с правом записи в DNS. Отдельный демон читает Traefik API
(`/api/http/routers`), поэтому одинаково покрывает Docker labels и file-provider.
## Как работает
1. Раз в `interval` (и сразу после событий Docker) читает `GET /api/http/routers`.
2. Из `rule` берёт все `Host(...)` у роутеров со статусом `enabled` (`HostRegexp`, `!Host(...)` игнорируются, `*.x` пропускаются с warning).
3. Хосты, попадающие в `zones` из конфига (равны зоне или её поддомен; при вложенных зонах — самая длинная), получают A-запись `ip`/`ttl`.
4. Записи, созданные демоном, помечаются `comment: managed-by=traefik-selectel-dns`. Меняются/удаляются только они. Существующая A-запись без метки (и имя с CNAME) не трогается — только warning.
5. Удаление выключено по умолчанию. При `delete.enabled: true` запись удаляется после того, как роут отсутствует непрерывно `delete.gracePeriod` (состояние в памяти; после рестарта отсчёт начинается заново). Роутеры со статусом `warning` считаются присутствующими. При ошибке Traefik API или пустом списке (после непустого) цикл пропускается и ничего не удаляется.
## Настройка
Пример полного стенда: `deploy/` (`docker-compose.example.yml`, `traefik.example.yml`, `config.example.yaml`, `dynamic/`).
1. Создайте в Selectel сервисного пользователя с минимальной ролью на DNS проекта.
2. Положите креды в файлы секретов (`deploy/secrets/*`, не коммитить) — см. комментарий в compose.
3. Поправьте `zones`, `ip`, `traefik.apiURL` в `config.example.yaml`; wildcard `tls.domains` — в `traefik.example.yml`.
4. Для первого запуска включите `dryRun: true` и проверьте логи.
Сборка: `docker build -t traefik-selectel-dns .` (distroless, non-root, HEALTHCHECK через `-healthcheck`).
## Конфигурация
YAML (`-config` или `TSD_CONFIG`), неизвестные поля — ошибка. Переопределение через env:
| YAML | env | По умолчанию |
|---|---|---|
| `zones` | `TSD_ZONES` (через запятую) | обязательно |
| `ip` | `TSD_IP` | обязательно, IPv4 |
| `ttl` | `TSD_TTL` | 300 (60..604800) |
| `interval` | `TSD_INTERVAL` | 30s |
| `dryRun` | `TSD_DRY_RUN` | false |
| `traefik.apiURL` / `.timeout` | `TSD_TRAEFIK_API_URL` / `TSD_TRAEFIK_TIMEOUT` | http://traefik:8080 / 10s |
| `docker.enabled` / `.host` | `TSD_DOCKER_ENABLED` / `TSD_DOCKER_HOST` | true / unix:///var/run/docker.sock (`tcp://` тоже) |
| `delete.enabled` / `.gracePeriod` | `TSD_DELETE_ENABLED` / `TSD_DELETE_GRACE_PERIOD` | false / 10m |
| `selectel.baseURL` / `.authURL` / `.timeout` | `TSD_SELECTEL_BASE_URL` / `_AUTH_URL` / `_TIMEOUT` | api.selectel.ru/domains/v2, cloud.api.selcloud.ru/identity/v3, 30s |
| `listen` | `TSD_LISTEN` | :9090 (`/healthz`) |
| `logLevel` / `logFormat` | `TSD_LOG_LEVEL` / `TSD_LOG_FORMAT` | info / json |
Секреты — только env (каждая поддерживает суффикс `_FILE`; одновременно обе — ошибка):
`SELECTEL_USERNAME`, `SELECTEL_PASSWORD`, `SELECTEL_ACCOUNT_ID`, `SELECTEL_PROJECT_ID`.
Для Traefik (lego) те же значения передаются как `SELECTELV2_USERNAME_FILE`, `SELECTELV2_PASSWORD_FILE`, `SELECTELV2_ACCOUNT_ID_FILE`, `SELECTELV2_PROJECT_ID_FILE`.
## Безопасность
- Секреты не попадают в конфиг, образ и логи; IAM-токен (Keystone) кэшируется в памяти и обновляется заранее.
- Traefik API не публикуется наружу и ограничен `ipAllowList` подсетью сети `control`; пользовательские контейнеры в неё не подключать.
- Docker socket = root на хосте. Демону нужны только события, поэтому в примере используется `docker-socket-proxy` (`EVENTS=1`), а не прямой сокет. Без Docker events демон работает по опросу.
- Чужие записи не перезаписываются; удаление выключено по умолчанию.
## Ограничения
- Только A-записи, один фиксированный IPv4 для всех хостов; AAAA нет.
- Только `Host(...)` с ASCII-именами (IDN — в punycode); `HostRegexp` и wildcard-хосты не поддерживаются.
- Метка владения — exact-match комментария; если её вручную изменить/стереть, запись считается чужой.
- Grace-состояние в памяти; несколько реплик демона одновременно не поддерживаются.
- Поток Docker events без общего таймаута (переподключается при обрыве; страховка — периодический опрос).
- Реальный Selectel API не проверялся (нет кредов) — см. отчёт в `swarm-report/`.