Всем привет!

Я Саша Краснов, CTO контейнерной платформы «Штурвал». В апреле прошла юбилейная DevOpsConf 2025, на которой мне посчастливилось выступать с докладом. Рассказывал я про хаки, которые позволяют автоматизировать использование DNS.

Эта статья построена на базе моего доклада и трех реальных историй:

  1. управление DNS из git;

  2. собственный nip.io;

  3. как и зачем писать плагины для 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-записей в корпоративных зонах заказчиков. Решить эту проблему можно было, выбрав один из сценариев:

  1. Менять локальную конфигурацию hosts;

  2. Отказаться от ingress;

  3. Минимизировать конфигурацию.

Первый вариант не подошел, потому что в пилот очень быстро залетают новые люди, а это значит, что им на своих машинах надо конфигурировать, и работать это будет через пень-колоду. Второй вариант противоречил конструкции платформы. Оставалось создать сервис, который будет работать везде, но при этом не требовать дополнительных настроек и поддерживать как минимум IPv4. И лучший выход — NoIP, когда в сам FQDN зашивается IPv4-адрес.

 Но стоило учитывать два момента:

  1. Должна быть возможность поднять зону в закрытом окружении, так как у некоторых наших заказчиков даже DNS-запросы не должны выходить наружу. Да и наши тестовые зоны также полностью изолированы;

  2. Не хотелось зависеть от внешнего сервиса.

Давайте посмотрим на пример запроса:

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 узлов поменялись, конечно, ничего не работало. Начали предлагать варианты, что можно сделать:

  1. Развернуть ingress на hostNetwork — но безопасники ударили по рукам;

  2. Конфигурировать HAProxy через API — не дали доступ.

А еще нужно было учитывать, что HAProxy будет в будущем заменен на другое решение.

Итого, нам требовалось: автодискавери на любых балансировщиках (HAProxy, Nginx, Envoy, F5), и при этом отсутствие доступа к управлению балансировщиком.

Логичным решением был бы Consul, к которому балансировщик будет отправлять запросы.

Load Balancing with HAProxy Service Discovery Integration | Consul | HashiCorp Developer

Как это происходит на примере 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-сообщество с обратной связью, идеями и вопросами!

Комментарии (0)