Всем привет!
Я Саша Краснов, 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-сообщество с обратной связью, идеями и вопросами!
ссылка на оригинал статьи https://habr.com/ru/articles/913946/
Добавить комментарий