Всем привет!
Я Саша Краснов, CTO контейнерной платформы «Штурвал». В апреле прошла юбилейная DevOpsConf 2025, на которой мне посчастливилось выступать с докладом. Рассказывал я про хаки, которые позволяют автоматизировать использование DNS.
Эта статья построена на базе моего доклада и трех реальных историй:
управление DNS из git;
собственный nip.io;
как и зачем писать плагины для CoreDNS.
Добро пожаловать под кат!

История № 1. Как управлять миром DNS‑зоной из git
В течение нескольких лет я заведовал лабораторией DevSecOps. В ней команды пилили различные инструменты AppSec, DevSec, SecOps и DevOps — начиная со сканеров и фаззеров и заканчивая GitOps. И пытались все это между собой интегрировать.
Если у кого-то есть файлик или чатик с IP-адресами каких-то систем для непродуктивных окружений, то вы поймете мою проблему.
В какой-то момент мне надоело лазить по Confluence, смотреть на IP-адреса стендов и прописывать себе в hosts. Мы же DevOps-ы, в конце концов, возьмем и автоматизируем этот процесс!
Однако у этой задачи был ряд нюансов. С одной стороны, изменять записи в зоне необходимо значительному количеству инженеров — но у них не должно быть возможности сломать все безвозвратно. А с другой стороны, сервис должен работать внутри компании, например у пресейлов, которые не понимают, как все реализовано с точки зрения инфраструктуры, но проводят демонстрации.
Нужна была простейшая конфигурация, и вот что я сделал.
Во внутреннем корпоративном домене я запросил делегирование зоны DevSecOps (dso) на мои серверы. Далее в ней прикрутил механизм, который создает, обновляет и ротирует все записи, приходящие с двух name-серверов, которые я поднял под эту зону.
Я взял DNSControl — маленькую утилиту, умеющую работать в разных форматах и с различными провайдерами. Реализация максимально простая, по формату RFC 1035. DNSControl работает с публичными облачными провайдерами и имеет понятную структуру описания.
Конфигурация выглядит так:
require("vars.js");
// Domains:
D('dso.domain.corp', REG_NONE, DnsProvider(BIND),
A('@', '10.1.2.3'),
A('ns1','10.1.2.4'),
A('ns2','10.1.2.5')
);
// subzones
require("./subzones/poc.js");
// reverse zone
require("./revzones/rev-10-2-2.js");
dnsconfig.js — это основная конфигурация зоны, также есть reverse zone, subzones и другие дополнительные конфигурации. SOA-запись и делегирование зоны изменить невозможно без рутового доступа на серверы — это сокращает поверхность ошибки.
К ней идет простой пайплайн:
test_job:
stage: test
script:
- dnscontrol check
build_job:
stage: build
before_script:
- cp /etc/coredns/origins/* zones/
script:
- dnscontrol push
after_script:
- cp zones/*.zone /etc/coredns/origins/
Проверяем, что мы нигде не ошиблись с запятыми, и заливаем конфигурацию.
На DNS-серверах была простая конфигурация CoreDNS, которая сначала стала временным решением, а по итогу используется вот уже пять лет.
Концепция следующая:

Любой корпоративный пользователь, который подключился к сети компании, может получить доступ к стенду.
Доступ на ns-серверы зоны есть у ограниченного числа администраторов.
Даже если что-то сломалось, можно вернуть все в исходное состояние в Git или провести профилактическую беседу. Простой Rollback как залог успеха.
История №2. Понять, простить и поднять свой nip.io
Проводя анализ пилотных внедрений «Штурвала», мы поняли, что заметное количество нашего времени уходит на согласование ЗНИ/RFC для создания DNS-записей в корпоративных зонах заказчиков. Решить эту проблему можно было, выбрав один из сценариев:
Менять локальную конфигурацию hosts;
Отказаться от ingress;
Минимизировать конфигурацию.
Первый вариант не подошел, потому что в пилот очень быстро залетают новые люди, а это значит, что им на своих машинах надо конфигурировать, и работать это будет через пень-колоду. Второй вариант противоречил конструкции платформы. Оставалось создать сервис, который будет работать везде, но при этом не требовать дополнительных настроек и поддерживать как минимум IPv4. И лучший выход — NoIP, когда в сам FQDN зашивается IPv4-адрес.
Но стоило учитывать два момента:
Должна быть возможность поднять зону в закрытом окружении, так как у некоторых наших заказчиков даже DNS-запросы не должны выходить наружу. Да и наши тестовые зоны также полностью изолированы;
Не хотелось зависеть от внешнего сервиса.
Давайте посмотрим на пример запроса:
dig 2025.devopsconf.ip-20-25-4-7.shturval.link @8.8.8.8
; <<>> DiG 9.16.1-Ubuntu <<>> 2025.devopsconf.ip-20-25-4-7.shturval.link @8.8.8.8
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 45138
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 512
;; QUESTION SECTION:
;2025.devopsconf.ip-20-25-4-7.shturval.link. IN A
;; ANSWER SECTION:
2025.devopsconf.ip-20-25-4-7.shturval.link. 3600 IN A 20.25.4.7
;; Query time: 109 msec
;; SERVER: 8.8.8.8#53(8.8.8.8)
Как видите, IP вшит в сам адрес.
Как это работает:
Резолвинг имени происходит на серверах зоны shturval.link на основе регулярного выражения (см. ниже). Запрос на них доставляется штатным образом через любой рекурсор.
Легко поднимается локально без доступа в интернет. В корпоративной сети достаточно настроить локальный форвардинг запросов.

Не требует конфигурирования, если нет ограничений на резолвинг DNS.
Необходимо минимальное количество ресурсов для развертывания в закрытом окружении.
Конфигурация зоны выглядит так:
shturval.link:53 {
ready
template IN A shturval.link {
match ^\S+[.]ip-(?P<a>[0-9]*)-(?P<b>[0-9]*)-(?P<c>[0-9]*)-(?P<d>[0
-9]*)[.]shturval[.]link[.]$
answer "{{ .Name }} 3600 IN A {{ .Group.a }}.{{ .Group.b }}.{{
.Group.c }}.{{ .Group.d }}"
fallthrough
}
rewrite stop type AAAA A
rewrite stop type MX A
rewrite stop type HTTPS A
file /etc/coredns/shturval.link.zone
cache 3600
reload
prometheus localhost:9253
}
$TTL 300
$ORIGIN shturval.link.
@ IN SOA ns1.shturval.link. alex.shturval.tech. (
2023032100 ; Serial
604800 ; Refresh
86400 ; Retry
2419200 ; Expire
3600 ) ; Negative Cache TTL
IN A 77.95.135.109
ns1 IN A 77.95.135.109
Обращаю ваше внимание на такие слова, как template, rewrite, file, cache, reload — все это плагины CoreDNS. Каждый имеет свою конфигурацию, которая может быть как легкой, так и сложной. К примеру, для template — это целый объект.
История №3. Написать свой плагин для CoreDNS
Все началось с инцидента во время плановых работ на платформе виртуализации у заказчика. В один момент что-то пошло не так, часть узлов просто перестала отвечать. Наша платформа отреагировала штатно: отработали health check, пересоздались узлы, завелись поды. Данные с системного мониторинга, да и прикладники тоже подтвердили, что все гладко.
Но не тут-то было. Прибежала команда эксплуатации с криками: «Почему у вас на новых ingress нет трафика, только на старые прилетает?».

Пошли разбираться с балансировщиком. Оказалось, что в HAProxy гвоздями были прибиты IP. А так как IP узлов поменялись, конечно, ничего не работало. Начали предлагать варианты, что можно сделать:
Развернуть ingress на hostNetwork — но безопасники ударили по рукам;
Конфигурировать HAProxy через API — не дали доступ.
А еще нужно было учитывать, что HAProxy будет в будущем заменен на другое решение.
Итого, нам требовалось: автодискавери на любых балансировщиках (HAProxy, Nginx, Envoy, F5), и при этом отсутствие доступа к управлению балансировщиком.
Логичным решением был бы Consul, к которому балансировщик будет отправлять запросы.

Как это происходит на примере HAProxy:
# конфигурация резолвера для кластера
resolvers shturval-sht-capov
nameserver ns1 api.shturval-sht-capov.domain.corp:1053
accepted_payload_size 8192
# конфигурация https-бэкенда для балансировки трафика на ingress
backend ingress-https-shturval-sht-capov
balance source
mode tcp
server-template ingress 3 default-nginx.ingress.shturval-sht-
capov.coreha.shturval:30443 check resolvers shturval-sht-capov init-addr none
# конфигурация http-бэкенда для балансировки трафика на ingress
backend ingress-http-shturval-sht-capov
balance source
mode tcp
server-template ingress 3 default-nginx.ingress.shturval-sht-
capov.coreha.shturval:30080 check resolvers shturval-sht-capov init-addr none
Мы настраиваем резолвер и шаблон бэкэндов. HAProxy идет к резолверу, который возвращает IP и автоматически создает в бэкэндах записи.
Но зачем нам Consul, если у нас есть Kubernetes и его сервисы, CoreDNS и etcd? Проблема в том, что у нас изначально не было такого плагина. Поэтому мы решили написать его.
Конструкция получается очень похожей на Consul. На сontrol planе у нас висит CoreDNS, который смотрит за подами внутри кластера с определенной аннотацией.
Вот пример запроса:
$ dig @K.A.P.I -p 1053 default-nginx.ingress.shturval-sht-
capov.coreha.shturval
; <<>> DiG 9.16.1-Ubuntu <<>> @10.X.Y.Z -p 1053
default-nginx.ingress.shturval-sht-capov.coreha.shturval
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 35620
;; flags: qr aa rd; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;default-nginx.ingress.shturval-sht-capov.coreha.shturval. IN A
;; ANSWER SECTION:
default-nginx.ingress.shturval-sht-capov.coreha.shturval. 15 IN A 10.D.E.V
default-nginx.ingress.shturval-sht-capov.coreha.shturval. 15 IN A 10.O.P.S
;; Query time: 79 msec
;; SERVER: K.A.P.I#1053(K.A.P.I)
То есть мы запрашиваем конкретные поды в namespace. Первая часть, default-nginx, — это значение лейбла, далее идет namespace. Это позволяет разносить несколько ingress-контроллеров разных классов по трафику. По итогу он выдает список IP. С точки зрения нагрузки: порядка 60 тыс. запросов в секунду на одном ядре (и это — не напрягаясь!).
Вообще написать свой плагин так же просто, как нарисовать сову :) Нам нужно его зарегистрировать, инициализировать и реализовать функцию ServeDNS.

Что делает Init? Когда у нас запускается CoreDNS, он читает пользовательскую конфигурацию с описанными параметрами и плагинами. На основе этой конфигурации CoreDNS инициализирует плагины и передает им параметры.
Например, в нашем плагине мы используем kubeapi — публичный плагин, который конструирует Kubernetes Client. Либо берет файл, либо, если он внутри кубера, берет сервис-аккаунт, токен, серты и дальше лепит сам куб конфиг.

После инициализации запускается watch. Это то же самое, что происходит внутри команды kubectl get по label selector с –w и -A. Результат, который у нас обновляется постоянно из куба, мы пишем в кеш. Запросы в куб в этом случае идут на GET, только если у нас изменился под в кластере.
Как это все выглядит в коде? На самом деле не так страшно.
func init() {
plugin.Register(pluginName, setup) }
func setup(c *caddy.Controller) error {
k, err := parse(c)
if err != nil {/*...*/}
k.setWatch(context.Background())
c.OnStartup(startWatch(k,
dnsserver.GetConfig(c)))
c.OnShutdown(stopWatch(k))
dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler
{
k.Next = next
return k
})
return nil
}
func (k *KubeHostport) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg)
(int, error) {
/*...*/
writeResponse(w, r, records, nil, nil, dns.RcodeSuccess)
return dns.RcodeSuccess, nil
}
func writeResponse(w dns.ResponseWriter, r *dns.Msg, answer, extra, ns []dns.RR, rcode int)
{
m := new(dns.Msg)
m.SetReply(r)
m.Rcode = rcode
m.Authoritative = true
m.Answer = answer
m.Extra = extra
m.Ns = ns
w.WriteMsg(m)
В первую часть (init) входит регистрация плагина, ее парсинг, запуск setup и watch.
Функция ServeDNS тоже крайне прозаична (cмотрите код на Github!).
К примеру, этот плагин написан за один вечер. Можете попробовать сгенерировать код с помощью ChatGPT и Deepseek. Скорее всего, с первого раза работать не будет, поэтому не поленитесь залезть в дебаггер (см. конфигурацию здесь), все будет понятно.
Лирическое авторское послесловие
Мне очень нравится CoreDNS за гибкость конфигурации и возможность достаточно легкого расширения функционала.
Он имеет много полезного «из коробки» и позволяет легко расширяться. В официальной сборке есть 54 in-tree плагина (в версии v1.12.0), которые постоянно обновляются. Добавить или убрать плагин, а также сделать свою сборку очень просто. Если необходимо, можно что-то сверху дописать. На выходе мы получаем бинарь, который не имеет внешних зависимостей.
Материалы к статье выложены на Github. Пулл-реквесты приветствуются ^_^
Приходите в наше Kubernetes-сообщество с обратной связью, идеями и вопросами!