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
+36
View File
@@ -0,0 +1,36 @@
# Конфиг демона traefik-selectel-dns. Секреты Selectel здесь НЕ хранятся:
# SELECTEL_USERNAME / SELECTEL_PASSWORD / SELECTEL_ACCOUNT_ID / SELECTEL_PROJECT_ID
# задаются только через env (или *_FILE). Любой параметр можно переопределить
# env-переменной TSD_* (см. README).
# Разрешённые зоны (opt-in по зоне): записи создаются только для хостов,
# равных зоне или являющихся её поддоменом. Зоны должны существовать в проекте Selectel.
zones:
- example.com
# IPv4 публичного адреса ВМ с Traefik — значение всех создаваемых A-записей.
ip: 203.0.113.10
ttl: 300 # 60..604800
interval: 30s # период опроса Traefik API
dryRun: false # true — только логировать, ничего не менять в DNS
traefik:
apiURL: http://traefik:8080 # внутренний адрес API, наружу не публиковать
timeout: 10s
docker:
enabled: true # подписка на события контейнеров (триггер немедленного reconcile)
host: tcp://docker-proxy:2375 # или unix:///var/run/docker.sock; недоступность не фатальна
delete:
enabled: false # по умолчанию записи не удаляются
gracePeriod: 10m # удалять только после непрерывного отсутствия роута
# selectel: # обычно не нужно
# baseURL: https://api.selectel.ru/domains/v2
# authURL: https://cloud.api.selcloud.ru/identity/v3
# timeout: 30s
listen: ":9090" # /healthz
logLevel: info # debug|info|warn|error
logFormat: json # json|text
+97
View File
@@ -0,0 +1,97 @@
# Пример: Traefik + traefik-selectel-dns на одной ВМ.
# Перед запуском создайте файлы секретов (права 0400/0440, не в git):
# mkdir -p secrets && umask 077
# printf '%s' 'service-user' > secrets/selectel_username
# printf '%s' 'password' > secrets/selectel_password
# printf '%s' '123456' > secrets/selectel_account_id
# printf '%s' 'project-uuid' > secrets/selectel_project_id
# Пользователь Selectel — сервисный, с минимальной ролью на DNS в нужном проекте.
services:
traefik:
image: traefik:v3 # в проде закрепите конкретную версию/digest
restart: unless-stopped
command: ["--configFile=/etc/traefik/traefik.yml"]
ports: # публикуются ТОЛЬКО 80/443; 8080 (API) не публикуется
- "80:80"
- "443:443"
environment:
# Имена переменных lego (selectelv2); суффикс _FILE — чтение значения из файла
SELECTELV2_USERNAME_FILE: /run/secrets/selectel_username
SELECTELV2_PASSWORD_FILE: /run/secrets/selectel_password
SELECTELV2_ACCOUNT_ID_FILE: /run/secrets/selectel_account_id
SELECTELV2_PROJECT_ID_FILE: /run/secrets/selectel_project_id
secrets:
- selectel_username
- selectel_password
- selectel_account_id
- selectel_project_id
volumes:
- ./traefik.example.yml:/etc/traefik/traefik.yml:ro
- ./dynamic:/etc/traefik/dynamic:ro
- letsencrypt:/letsencrypt
- /var/run/docker.sock:/var/run/docker.sock:ro
networks:
- proxy # сюда подключаются пользовательские контейнеры
- control # только Traefik <-> демон
docker-proxy:
# Демону нужен только поток событий: даём ему read-only прокси вместо docker.sock.
image: tecnativa/docker-socket-proxy:latest # закрепите версию
restart: unless-stopped
environment:
EVENTS: 1
CONTAINERS: 0
POST: 0
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
networks:
- control
traefik-selectel-dns:
image: git.realmanual.ru/pub/traefik-selectel:latest # или build: ..
# build:
# context: ..
restart: unless-stopped
depends_on:
- traefik
environment:
SELECTEL_USERNAME_FILE: /run/secrets/selectel_username
SELECTEL_PASSWORD_FILE: /run/secrets/selectel_password
SELECTEL_ACCOUNT_ID_FILE: /run/secrets/selectel_account_id
SELECTEL_PROJECT_ID_FILE: /run/secrets/selectel_project_id
secrets:
- selectel_username
- selectel_password
- selectel_account_id
- selectel_project_id
volumes:
- ./config.example.yaml:/etc/traefik-selectel-dns/config.yaml:ro
read_only: true
cap_drop: [ALL]
security_opt:
- no-new-privileges:true
networks:
- control # доступ к Traefik API и docker-proxy; исходящий интернет — к Selectel
secrets:
selectel_username:
file: ./secrets/selectel_username
selectel_password:
file: ./secrets/selectel_password
selectel_account_id:
file: ./secrets/selectel_account_id
selectel_project_id:
file: ./secrets/selectel_project_id
volumes:
letsencrypt:
networks:
proxy:
name: proxy
control:
name: traefik-control # НЕ подключайте к ней пользовательские контейнеры
ipam:
config:
- subnet: 172.30.250.0/24 # должна совпадать с sourceRange в dynamic/api-internal.yml
+14
View File
@@ -0,0 +1,14 @@
# Traefik API только для демона: entrypoint "traefik" (:8080) + разрешён только источник
# из подсети сети control. Пользовательские контейнеры (сеть proxy) получат 403.
http:
routers:
api-internal:
rule: PathPrefix(`/api`)
entryPoints: [traefik]
service: api@internal
middlewares: [api-internal-allow]
middlewares:
api-internal-allow:
ipAllowList:
sourceRange:
- 172.30.250.0/24
+13
View File
@@ -0,0 +1,13 @@
# Пример роута через file-provider: демон увидит его в Traefik API так же, как docker-роуты.
http:
routers:
legacy-app:
rule: Host(`legacy.example.com`)
entryPoints: [websecure]
service: legacy-app
tls: {} # используется wildcard-сертификат с entrypoint websecure
services:
legacy-app:
loadBalancer:
servers:
- url: http://10.0.0.5:8080
+56
View File
@@ -0,0 +1,56 @@
# Статический конфиг Traefik v3.
# Креды Selectel для dns-challenge (lego, провайдер selectelv2) передаются через env
# SELECTELV2_*_FILE (см. docker-compose.example.yml), в этом файле секретов нет.
entryPoints:
web:
address: ":80"
http:
redirections:
entryPoint:
to: websecure
scheme: https
websecure:
address: ":443"
http:
tls:
certResolver: letsencrypt
# Общий wildcard-сертификат: Traefik выпустит его при старте, роуты с tls: {}
# на этом entrypoint будут использовать его без отдельных запросов в ACME.
domains:
- main: example.com
sans:
- "*.example.com"
# Внутренний entrypoint для API. Порт НЕ публикуется (ports:), а роутер api@internal
# ограничен по IP подсетью сети control (dynamic/api-internal.yml), т.к. Traefik также
# подключён к сети proxy с пользовательскими контейнерами.
traefik:
address: ":8080"
api:
insecure: false # не открываем API автоматически — только через ограниченный роутер
dashboard: false # демону нужен только /api/http/routers
certificatesResolvers:
letsencrypt:
acme:
email: admin@example.com
storage: /letsencrypt/acme.json
# Для отладки используйте staging:
# caServer: https://acme-staging-v02.api.letsencrypt.org/directory
dnsChallenge:
provider: selectelv2
resolvers:
- "1.1.1.1:53"
- "8.8.8.8:53"
providers:
docker:
exposedByDefault: false
network: proxy
file:
directory: /etc/traefik/dynamic
watch: true
log:
level: INFO
+17
View File
@@ -0,0 +1,17 @@
# Пример пользовательского контейнера: достаточно обычных labels Traefik,
# DNS-запись для whoami.example.com демон создаст сам (зона example.com разрешена в конфиге).
services:
whoami:
image: traefik/whoami
restart: unless-stopped
labels:
traefik.enable: "true"
traefik.http.routers.whoami.rule: Host(`whoami.example.com`)
traefik.http.routers.whoami.entrypoints: websecure
traefik.http.routers.whoami.tls: "true"
networks:
- proxy
networks:
proxy:
external: true
name: proxy