Всем привет! Меня зовут Максим, я руководитель одной из групп эксплуатации инфраструктурных сервисов в Ozon. Наша команда занимается поддержкой и развитием нескольких базовых сервисов компании, одним из которых, по историческим причинам, является сервис разрешения доменных имен (DNS). В Ozon много различных сервисов и систем. Они общаются друг с другом и внешним миром по доменным именам. DNS — центральное звено, без которого не обходится почти ни одна инфраструктура. Понятно, что когда DNS отдаёт некорректные данные, то это неприятно, когда таймаутит — плохо, когда прилёг — очень плохо, когда прилёг надолго — в принципе, можно расходиться. Значит, одна из основных задач команды инфраструктуры — обеспечить сервисам надёжное и, желательно, быстрое разрешение доменных имён. Об этом мы и поговорим. Также затронем вопросы управления ресурсными записями, жизнь в Multi DC-среде, обслуживание DNS, кеширование, журналирование запросов и возможные проблемы. Статья может быть полезна коллегам, интересующимся эксплуатацией, архитектурой и высокой доступностью сервисов, да и просто может быть любопытна как история построения инфраструктурной единицы в крупной компании. Небольшое примечание: потребителями нашего инфраструктурного DNS будут внутренние клиенты компании (любой сервис во внутренней сети). Внешние зоны у нас, конечно же, есть, но, исторически, ими занимается команда NOC (сетевые инженеры), и передавать их в инфру пока не стремится. Поэтому мы ориентируемся только на внутренних клиентов. По этой же причине в статье не будем затрагивать DNSSEC и IPv6. Любые совпадения имен машин и IP-адресов с реальными именами и ip случайны. Так, а зачем нам вообще что-то менять, разве наш текущий инфраструктурный DNS ненадёжен? У меня вот есть три DNS-сервера в трёх ЦОДах, все они прописаны в resolv.conf каждого клиента, то есть если основной сервер прилёг, системный резолвер пойдёт во второй, а затем и в третий. Дело в том, что в самом неудачном случае (когда остался доступен только третий ЦОД) и при дефолтных настройках резолвера (option timeout:5), последний nameserver ответит на запрос только по прошествии десяти секунд, что в микросервисной среде, когда один сервис взаимодействует с десятками других микросервисов и в коде нет кеширования DNS-запросов, просто недопустимо. Можно поиграть с уменьшенным таймаутом и другими опциями системного резолвера, но это не изменит картину принципиально. Тот же options rotate будет отправлять запросы на серверы из nameserver-списка по алгоритму round-robin (то есть по очереди), и 33 % запросов будут обработаны без таймаутов… ТОЛЬКО 33 %! Нам это не подходит. Зафиксируем первое требование: Не хотим полагаться на системный resolver.       Идём дальше. Думаю, многие слышали и, возможно, нередко используют “smart DNS resolver”. По сути, это легковесный DNS forwarder, который ставится на каждый хост. Его ключевой особенностью является функциональность отправки параллельных запросов сразу в несколько DNS-серверов и обработка ответа от самого быстрого из них. Достаточно интересно — получаем и отказоустойчивость, и максимальную скорость разрешения имён. (Возможно, кому-то трафик x2,3,N может быть критичен, поэтому следует это учитывать в своей архитектуре.) Для гомогенной среды (например, везде Linux) с единым центром управления (DNS контролирует одна команда инженеров) это вполне себе хорошее решение, и, что немаловажно, — простое. Но, у нас не гомогенная среда (встречается Windows, немного, но есть), а также много инфраструктурных команд; придётся уговаривать каждую команду. К тому же при изменении адреса хотя бы одного DNS-сервера (например, при переезде машины в другой ЦОД), конфигурацию «smart DNS» нужно будет раскатить на тысячи машин… Тоже не подходит. В общем, хочется более гибкого и универсального решения. Универсальное — это значит, что IP-адрес DNS-сервиса не должен меняться. Второе требование: DNS-сервис должен быть отказоустойчив на сетевом (IP не меняется) уровне (L3 OSI). Получается, за одним IP-адресом нашего DNS-сервиса должно скрываться два и более реальных DNS-сервера. Давайте для краткости такой IP называть VIP-адресом (Virtual IP), реальный DNS-сервер — real (сервером), а пачку реальных DNS-серверов за VIP — пулом (pool) Также в Ozon есть общее требование: Сервис должен переживать полную недоступность одного ЦОД на любой период времени (мы называем это требование DC-1). Что делать, если за VIP один из real-серверов «задумался» или «приказал долго жить»? По сути, мы попадём в ту же ситуацию, что и с системным resolver, когда некая доля DNS-запросов будет гарантировано не обслужена. Именно поэтому необходим механизм проверок (healthchecks) real-серверов с выкидыванием из пула «больных» и закидыванием обратно «здоровых». Следующее требование: Проверки (healthchecks) real-серверов со стороны VIP. Мы будем успевать обрабатывать входящие запросы? Нагрузка на наш текущий DNS сервис весьма незначительна, всего 15-20 тыс. RPS, которую легко вывозит одна виртуалка, поэтому в эту сторону сильно не копаем. А с горизонтальным масштабированием и подавно вопрос «успевания» полностью закрыт. Оно у нас, скорее, для высокой доступности и возможности локализации трафика. Пятое требование: Уметь горизонтально масштабироваться. Раз уж у нас несколько ЦОД, стоит подумать и о сетевых задержках. Странно ходить в DNS из одного ЦОД в другой, когда под боком есть рабочий экземпляр DNS. Мы хотим, чтобы при наличии сервера в том же ЦОД сервисы ходили именно в него и получали ответ быстрее. Шестое требование: Уметь локализовывать трафик в пределах ЦОД. А что если мне захочется выполнить регламентное обслуживание сервера (обновить ОС, железо, другой деструктив)? Я могу остановить локальную DNS-службу, дождаться неудачных healthchecks, дождаться исключения сервера из пула и заняться обслуживанием. Но тогда все запросы после остановки локального DNS и вплоть до исключения сервера из пула завершатся ошибкой. Такое себе. Поэтому: Седьмое требование: Плавно выводить трафик с сервера, на котором проводим регламентные работы. DNS-сервер должен поддерживать динамическое обновление зон по протоколу DNS или API. Это нужно для различных систем наливки железных и виртуальных серверов (Foreman, MaaS, Terraform и т.д.). Восьмое требование: Поддержка динамически обновляемых зон. Далее, что насчёт IP-адреса клиента? Очень желательно, я бы даже сказал, обязательно для поддержки и расследования инцидентов. А ещё это может пригодиться нам в будущем, например geo DNS прикрутить. Девятое требование: Должны видеть source IP источника запроса. Логирование DNS-запросов? Да, отдел информационной безопасности очень хочет. К тому же с логами debug куда проще и приятнее. Десятое требование: Уметь логировать DNS-запросы. В современном мире без мониторинга никуда, поэтому у DNS-сервиса должны иметься встроенные метрики. Прекрасно, если это будет формат Prometheus, потому что именно эту систему мониторинга мы используем в Ozon. Последнее требование: Наличие встроенных в DNS-сервис метрик. По задумке путь прохождения клиентского DNS-запроса должен выглядеть так: Client (DNS-query) → LoadBalancer → Caching DNS → Authoritative DNS  Конечно, все компоненты DNS (LoadBalancer, Caching DNS, Authoritative DNS) — должны быть зарезервированы, поэтому немного усложним схему дублирующими узлами. Машины LoadBalancer (LB) являются точками входа клиентского трафика. Напомню, что у них должен быть VIP — единый IP-адрес, на который LB будет откликаться.  На ум приходят две технологии: CARP/VRRP и динамическая маршрутизация (BGP). Давайте сравним: Если мы озаботились вопросом о healthchecks, то должен быть компонент в нашей инфраструктуре, который эти проверки запускает. Обычно таким компонентом является балансировщик (LoadBalancer, или LB). Из названия понятно, что его главная задача — балансировка, то есть распределение трафика между real-серверами. Кроме того, он, как правило, проверяет доступность этих real-серверов, и при необходимости убирает и добавляет их из пула (балансировки).  Тут мы выбирали из следующих вариантов: Три ЦОД. В качестве физической сети, или underlay, выступает Layer 3 Network, в качестве overlay  — BGP EVPN VXLAN. VLAN между ЦОД стараемся не тянуть, чтобы не увеличивать сложность обслуживания сети. IPVS NAT mode обяжет нас возвращать ответ через тот же IPVS-шлюз, что уже непросто для real-сервера, если таких шлюзов больше одного. Усложняется debug. IPVS DR mode завязан на L2-сегмент сети. И если не тянуть эти сегменты через VLAN, то «пошарить» real-серверы между ЦОД не получится. IPVS IPIP — самый подходящий: благодаря GRE-туннелю трафик может быть проброшен в любую L3-сеть и отдан клиенту напрямую, минуя IPVS. Но, опять же, усложняется конфигурирование сети, debug и мониторинг. Вывод: выбираем Userspace Layer7 LB как самый функциональный по возможностям и простой с точки зрения конфигурации сети. Обсудим способы, которыми можем выполнить требования записи IP-адреса каждого клиента в логи системы. Принцип работы: модифицируем (если есть, и добавляем, если отсутствует) поле ECS в DNS-запросе, прописывая там source IP клиента. Поскольку записывается подсеть, для того, чтобы получить конкретный IP, нам нужна маска /32 (для IPv4). Добавлением поля ECS будет заниматься L7 LoadBalancer. Недостаток в том, что это решение требует анализа и редактирования исходного запроса: балансировщик принимает DNS-запрос, разбирает его на Layer7 (OSI), принимает решение о модификации или добавлении поля ECS в запрос.  По сути, мета-RR (resource record), которая добавляется к запросу. Имеет экспериментальный статус, с истёкшим драфтом. Не рассматриваем это решение, потому что нам нужно надёжное и проверенное временем.  Информация о начальных адресах и портах источника и получателя добавляется в header в начале UDP-датаграммы или TCP-соединения, аналогично тому, как это реализовано в nginx, Haproxy и т.д.  Недостаток в том, что требуется поддержки в lb, resursor и auth. Поддержка в PowerDNS: Dnsdist начиная с 1.5.0, recursor — с 4.4.0, auth — с 4.6.0. В результате на момент выбора механизма передачи source IP клиента самым универсальным решением показался ECS. И так как проблем с производительностью на наших небольших объемах нет (15-20 тыс. RPS), мы остановились на нём.  Proxy protocol, честно говоря, пощупать тогда не удалось, не было нужных версий бинарей. Как бы там ни было, этот вариант выглядит вполне рабочим. Недавно руки дошли, протестировали, работает. Оставили его как вариант на будущее, если возможностей ECS перестанет хватать. Подробнее про source ip от Dnsdist здесь и здесь.  Для хранения RR раньше мы использовали PowerDNS + Generic PostgreSQL backend.  Но после аварии (о которой я расскажу ниже) решили пересмотреть подход к хранению данных. Для динамически обновляемых зон (например, по протоколу DNS update) можно оставить SQL-совместимый backend (тот же PostgreSQL), а для относительно редко меняющихся RR взять что-нибудь более статичное, например, BIND zone backend.  Для редактирования RR удобно пользоваться какой-нибудь готовой админкой, например, PowerDNS-Admin. Тяп-ляп и в production. К сожалению, использование таких инструментов несёт серьёзные риски, которые незаметны на первый взгляд. Мы начали с такого решения, что привело к плачевным последствиям: потере данных и простою на время восстановления. Подробнее об этом ниже. Так или иначе, необходимо проработать более отказоустойчивое решение.  В нашей компании для доставки кода мы используем Gitlab. Почему бы не доставлять через него статику bind-zone? Подумаем.  При обслуживании любого компонента нашей архитектуры мы хотим плавно убирать с него нагрузку без влияния на клиентские запросы, и потом так же плавно возвращать. Другими словами, switchover.  Нам нужен признак, по наличию которого auth, recursor и сервис BGP-маршрутизации поймут, что им следует снять нагрузку. По опыту, самым простым признаком может быть наличие обычного файла: если он отсутствует — нормальный режим работы, присутствует — режим обслуживания (убираем нагрузку). Тут всё просто. Anycast BGP перенаправляет клиента на ближайший LoadBalancer. который всегда находится в том же ЦОД, что и клиент (если анонс VIP поднят и нет специфичных настроек маршрутизации). Далее LoadBalancer отправляет DNS-запрос на локальный, заранее прописанный в конфиге Caching/Authoritative DNS, и только в случае его недоступности — на Caching/Authoritative DNS, находящийся в одном из соседних ЦОД. Клиентский трафик заводится в три ЦОД через BGP anycast. Через ECMP попадает на несколько Layer7 LoadBalancer, далее перенаправляется на Caching DNS, и затем — на Authoritative DNS.  LoadBalancer живут на отдельных машинах (в нашем случае — виртуальных). Caching DNS и Authoritative DNS живут на одной виртуалке (VM).  С транспортным цехом (NOC, network operation center) договорились, что там, где будет настроена BGP-маршрутизация, будут жить только балансировщики.  Caching DNS и Authoritative DNS на одной машине — такая конструкция достаточно автономна, легко горизонтально масштабируется, нет лишних сетевых хопов.  Авторитативный DNS должен иметь два бэкенда: статичный, в котором нельзя удалить RR легким кликом мышки, и динамичный — для всякого рода DNS update (Foreman), редактирование через API (Terraform, MaaS).  Также должно быть управляющее ПО, через которое народ сможет вносить изменения (в RR) в бэкенды, описанные выше.  Нагрузка с машины LB снимается прекращением BGP-анонса VIP-адреса, с машины Caching/Authoritative DNS — неудачным healthcheck со стороны LB.  Failover: в случае проблем на одной из LB-машин BGP-анонс VIP-адреса прекращается, таблицы маршрутизации перестраиваются, клиенты идут на оставшиеся LB-машины.  В случае проблем с виртуальной машиной с Caching/Authoritative DNS балансировщик через healthchecks понимает это и переключает трафик на виртуалку Caching/Authoritative DNS в другом ЦОД.  Switchover: аналогично failover, но без отказа в обслуживании для клиента — нагрузка с машины LB снимается с помощью прекращения BGP-анонса VIP-адреса, с машины Caching/Authoritative DNS — неудачным healthcheck со стороны LB.  Мониторинг через Prometheus.  По сути, DNS — это такой же сервис, и перед выкаткой в production новая конфигурация должна быть протестирована на более терпимом к сбоям окружении. Таким окружением для PowerDNS, как и для наших микросервисов, является staging. Для максимального приближения к условиям в prod на PowerDNS кластер в staging-окружении также подаётся нагрузка. Это запросы сервисов из development- и staging-окружений. Таким образом, и по конфигу, и по данным, и по наличию трафика кластеры в production и staging почти идентичны. Разница только в конфигурации «железной» зоны (h.lan). Исторически сложилось, что у нас нет отдельной зоны для production, отдельной для staging и т.д. Все реальные имена машин изо всех окружений находятся в h.lan. Очевидно, что вносить изменения в «железную» зону мы обязаны через единый эндпоинт, которым является production. При этом изменения, прилетевшие в production, мы должны видеть и в staging PowerDNS. Связывать эти окружения БД-репликацией не очень хорошая идея, потому что реплика из staging легко может повлиять на prod, а вот обычная master → slave DNS-репликация (через AXFR) — то, что нужно.  В качестве authoritative и caching DNS у нас уже использовался PowerDNS, поэтому в первую очередь смотрели на него. PowerDNS Authoritative Server (или просто pdns auth) поддерживает различные SQL-базы, файлы bind zone, кучу других бэкендов, Lua-записи (это нам чуть позже пригодится), встроенные метрики для Prometheus и SNMP, имеет API для менеджмента RR и управления демоном.  PowerDNS Caching Server (или просто pdns recursor) — высокопроизводительный кеширующий DNS-сервер со встроенной поддержкой Lua-сценариев. Метрики тоже имеются.  Долго искать не пришлось: оказывается, в PowerDNS обо всём уже подумали и завезли Dnsdist — балансировщик с защитой от DNS- и DoS-атак.  Справедливости ради попытались нагуглить другие альтернативы, но ничего даже отдалённо напоминающее Dnsdist не нашлось.  Кроме, собственно, BGP-сервиса нам нужны healthcheks, чтобы снять или вернуть нагрузку (убрать или вернуть анонс /32 префикса VIP), если что-то не так с интерфейсом, dnsdist, или если мы решили вывести машину на обслуживание.  Выбирали между:  ExaBGP — демоном на Python с поддержкой healthchecks, но без BFD. BIRD — можно сказать, стандартом де-факто в индустрии. Есть встроенный BFD, но нет healthchecks, то есть все проверки придётся костылить сбоку.  Из-за большой применимости и наличия встроенного BFD остановились на BIRD.  Для редактирования RR PowerDNS требует SQL-совместимую БД. В нашей компании большая экспертиза в PostgreSQL, поэтому выбрали её. Также в качестве оркестратора поверх PostgreSQL мы используем Patroni, здесь выбор тоже очевиден.  Если взять всю цепочку прохождения DNS-запроса — Dnsdist → PDNS Recursor → PDNS Auth, — то в сумме мы получим пять уровней, на которых запрос может быть закеширован. Пять, Карл!!! Понятно, что если мы включим их все, то навряд ли debug окажется лёгкой прогулкой.  Dnsdist: packet cache.  PDNS Recursor: packet cache, query cache. PDNS Auth: packet cache, query cache. Разница между кешами пакетов и запросов следующая. В кеше пакетов лежат записи в виде уже сформированных ответов. То есть на идентичные запросы без какой-либо дополнительной обработки отдаём готовые ответы. Получается вроде кеша запросов в MySQL.  Query cache или record cache, — кеш внутренних запросов, сюда попадают отдельные записи, то есть ответы от бэкенда. Некоторые запросы приводят к ряду внутренних запросов, которые также попадут в query cache. Самый очевидный пример — когда запрос A-типа приводит к дополнительному запросу CNAME-типа.  TTL в ответе из packet cache не будет меняться, в то время как из query cache TTL честно будет уменьшаться на 1 с каждой секундой.  Что еще… Packet cache для всех ответов проставляет одинаковый TTL (packetcache-ttl), в то время как query cache ориентируется на TTL записи и может быть разным, но не более max-cache-ttl. Если используются бэкенды на основе ОЗУ (не требующие переключений контекста), то packet cache может быть даже вреден и лучше его отключить.  Оставляем максимум два кеша. Какие именно?  Dnsdist для нас — проксирующий узел, который должен просто передать пакет дальше, поэтому кеш здесь не используем.  PDNS Recursor — кеш запросов — да, кеш пакетов — нет. Простой синтетический тест показал, что packet cache на бэкенде bind-zone профита не дает. Поэтому выключим его в pdns, но включим с небольшим TTL query cache. Он нам нужен, чтобы не сильно мучить тяжелый SQL-бэкенд, но и слишком длительным по времени он тоже быть не должен (по крайней мере в нашем случае), чтобы healthcheck сквозь SQL-бэкенд был достаточно актуальным.  /etc/dnsdist/dnsdist.conf     Если настроить webserver, то можно посмотреть статистику по http://127.0.0.1:8080:  Что ещё, кроме балансировки, умеет dnsdist?  Отбрасывать неугодные по содержанию запросы, кешировать запросы, лимитировать клиентов по DNS query и по IP, использовать в качестве фильтра eBPF для максимальной производительности.   Кеш включается на каждый пул. “” — пул по умолчанию. А с помощью setStaleCacheEntriesTTL() можно временно отдавать клиенту устаревший кеш, пока бэкенд не придёт в норму.  Сообщаем наш VIP-адрес, а точнее маршрут к нему (обязательно с префиксом /32). Сам VIP будет висеть на dummy-интерфейсе, а анонс префикса /32 будет убираться и добавляться в BGP через удаление и добавление маршрута /32. Всё это будем делать скриптами под управлением monit.   Давайте настроим систему так, чтобы трафик снимался с машины, если мы не можем получить успешный ответ от локального Dnsdist, или когда инженер выполняет регламентные работы.  Алгоритм простой:  Выполняем healthcheck (обычный DNS-запрос к локальному Dnsdist) и проверяем наличие файла /var/lib/vip0.down. Снимаем анонс (удаляем маршрут к vip0), если результат healthcheck неуспешен или существует /var/lib/vip0.down. Добавляем анонс (добавляем маршрут к vip0), если healthcheck успешен и отсутствует файл /var/lib/vip0.down. Скрипт проверки живости:    Снимаем анонс маршрута /32:   Возвращаем анонс маршрута /32:   Осталось передать это всё под управление какого-нибудь супервизора. Старый добрый monit должен с этим справиться.   Конфигурация рекурсора:  Для удобства объединяем все Lua-вставки в один файл:  Далее рассмотрим код процедуры maintenance. Наличие файла включает режим обслуживания, отсутствие — выключает. Физически это заключается в проверке наличия файла и присвоении булевой переменной maintenanceState значения true или false.    Файл форварда зон:   Конфигурация авторитетного DNS:  Подключение плагинов bind и gpgsql:   Здесь мы подключили плагины bind и gpgsql.  Может показаться хорошей идеей прописать одну и ту же зону с разным набором записей в разных бэкендах и получить выгоду от использования обоих. Например, нам было бы удобно работать с простым и понятным всем по формату bind-файлом, но в некоторых случаях всё-таки подключать записи из GeoIP-бэка (то есть отдавать разный набор записей в зависимости от source IP клиента, просто потому что там это делать очень удобно.  Так вот, забудьте про это. Архитектура бэкендов на это не рассчитана. Да, технически вы можете разместить одну и ту же зону с разным или идентичным набором записей в разных бэкендах, и PDNS Auth это проглотит без ошибок, но порядок просмотра бэкендов в этом случае неочевиден. Зону из какого бэка PDNS Auth возьмёт в качестве эталонной? Ответ знает только сам PDNS Auth.  Может поиграться с опцией launch=bind в gpgsql, меняя очерёдность подгрузки бэкендов. В этом случае бэки действительно загрузятся в том порядке, в котором они перечислены в опции launch, но, опять же, это никак не повлияет на то, в какой бэк PDNS Auth пойдёт за зоной.  Пример:  берём bind- и geoip-бэки; заполняем одинаковым по количеству набором записей, но с немного отличающимся содержимым (чтобы понимать, из какого бэка пришёл ответ); делаем запрос; получаем содержимое из geoip-бэка; меняем местами бэки в launch; опять получаем содержимое из geoip-бэка; меняем serial в большую и в меньшую сторону; снова получаем контент из geoip-бэка; добавляем новую запись только в bind-бэк; ищем по ней получаем NXDOMAIN; удаляем SOA из geoip; получаем REFUSED.  А вот с парой bind + gpgsql было чуть иначе: при именовании конфигурационных файлов 40_gpgsql.conf и 20_bind.conf (которые подключались через include-dir=/etc/powersns/pdns.d). Вывод: каждая зона должна быть определена только один раз в одном из бэкендов PDNS Auth.  Теперь донастроим оба бэка и добавим пару ресурсных записей. Настроим кластер PostgreSQL. Процесс настройки связки master –> replica тривиален, поэтому останавливаться здесь не будем. Гораздо интереснее подъём полноценного HighAvailability (HA)решения, например Patroni. В Ozon по нему отличная компетенция, поэтому сюрпризов в эксплуатации быть не должно. Однако есть небольшое отличие от стандартных кластеров под микросервисы: все необходимые для HA компоненты (PostgreSQL, Patroni, etcd) будут располагаться на тех же машинах, что и PDNS Auth. Сделано это для того, чтобы запросы к БД были максимально быстрыми, а отдельные компоненты, такие как etcd, не использовались совместно с другими сервисами, никак не связанными с DNS, дабы избежать влияния от них. Небольшой оффтоп. Наш текущий кластер etcd-Patroni (тот, который под микросервисы, а не тот, который под DNS), обслуживает уже 3,7 тыс. Patroni-кластеров и 11 тыс. отдельных экземпляров PostgreSQL в production. Конфиг Patroni будет примерно такой:  После сетапа БД.. Импортируем схему данных (таблицы, индексы и т.д.). Для PostgreSQL: https://doc.powerdns.com/authoritative/backends/generic-postgresql.html#default-schema  Теперь можно создать пару зон и наполнить их записями. Зона powerdns.lan Нужна для правильной работы механизма healthchecks. Dnsdist постоянно запрашивает у бэкенда status.backend.powerdns.lan текстовую запись и тем самым прозрачно проверяет работоспособность базы данных на каждом PowerDNS узле. Чтобы healthcheck был более-менее актуален, отключаем paсket cache на рекурсоре, выставляем TTL записи в 1 секунду и кеш запроса в 3 секунды (чтобы не сильно мучить PostgreSQL). Поскольку в зону powerdns.lan мы никогда никакие изменения не вносим, то вероятность накосячить, удалив или изменив ключевую запись status.backend.powerdns.lan, существенно снижается. Создаём зону в БД: pdnsutil create-zone powerdns.lan ns1.powerdns.lan. Заполняем: pdnsutil edit-zone powerdns.lan. Смотрим: pdnsutil list-zone powerdns.lan.  Зона h.lan — основная («железная») зона. Создаём, заполняем и смотрим: pdnsutil create-zone h.lan ns1.powerdns.lan pdnsutil edit-zone h.lan pdnsutil list-zone h.lan  Всё, теперь резолв h.lan должен заработать: Для production зону h.lan мы настроили, теперь её нужно затащить в staging-окружение. Навскидку это можно организовать через репликацию PostgreSQL или DNS. DNS-репликация видится более гибким решением, потому что позволяет для того же staging-окружения применить отличную от production «конфигурацию». Например, можно на лету модифицировать получаемую по AXFR зону  или настроить DNSSEC, что в доступной только для чтения базе PostgreSQL было бы невозможно. Соответственно, для staging-окружения поднимаем свой экземпляр кластера Patroni и всего остального (PDNS Auth, PDNS Recursor, Dnsdist). И далее у нас есть два пути.  Создаём в БД вторичную зону h.lan: pdnsutil create-secondary-zone h.lan 10.40.0.11:5300 10.40.0.12:5300 и т.д. Заполняем pdnsutil edit-zone h.lan. Чтобы DNS-репликации заработала в конфигурации PDNS Auth (/etc/powerdns/pdns.conf), мы должны прописать secondary=yes. И только на той PowerDNS-машине, на которой PostgreSQL в данный момент является primary! Если это сделать и на остальных vm, то при попытке получить зону (AXFR-запрос) процесс pdns_server (PDNS Auth) будет постоянно падать, потому что БД находиться в режиме только для чтения. Ну и остался bind-бэкенд. Тут всё максимально просто: обычный файл bind zone, который раскатываем сначала в staging, потом в production: /etc/powerdns/backend_bind/zone_bind.conf. Описываем тип зоны/зон: master, slave или native.  Вспоминаем, что PDNS Auth (бэкенд gpgsql) смотрит на локальный экземпляр PostgreSQL, и только на одной из машин находится master, на остальных — реплики только на чтение. Создаём в БД зону h.lan: pdnsutil create-zone h.lan Заполняем: pdnsutil edit-zone h.lan На всех PowerDNS-машинах staging-окружения прописываем в конфигурации pdns secondary=yes. А зону h.lan (AXFR-запрос) получаем через периодический запуск команды: В чем тут соль? Через secondary=yes мы говорим PDNS Auth, чтобы он был готов выполнять функции вторичного (slave) DNS, но будет ли он по факту брать зону с первичного , определяется настройками (типом) зоны. В первом примере (Путь 1) мы явно задали тип зоны как SECONDARY. И в соответствии с TTL из SOA-записи PDNS Auth начал периодические попытки «стянуть» зону. Во втором примере (Путь 2) тип зоны получился NATIVE, что отключает логику PRIMARY (отсылка notify всем NS) при обновлении зоны и отключает логику SECONDARY (периодический опрос PRIMARY-машин), но отсутствие функциональности SECONDARY нивелируется периодическим выполнением pdns_control retrieve.  Второй путь кажется красивше. Все узлы PowerDNS в staging получаются идентичны по конфигурации: на всех узлах включен secondary=yes; на всех узлах есть cron-задача pdns_control retrieve, которая успешно запускается, только если локальный PostgreSQL является первичным. Ну и остался bind-бэкенд. Тут всё максимально просто: обычный файл bind zone, который раскатываем сначала в staging, потом в production: /etc/powerdns/backend_bind/zone_bind.conf. Описываем тип зоны/зон: master, slave или native.   Обновить состояние зоны можно без рестарта pdns_server (PDNS Auth) службы: Пару лет назад у нас случился «неплохой» такой инцидент, который привёл к даунтайму ozon.ru на час с лишним и убытку в огромную сумму. Просто из PowerDNS пропала бОльшая часть записей.  Почему? Всё дело в той самой админке, о которой я писал выше, PowerDNS-Admin — WebUI для управления ресурсными записями в PowerDNS. И оказалось, что если при редактировании внести синтаксически, с точки зрения формата RR, ошибочную запись, то этот UI типа этого не замечает. Он отбрасывает пачку текущих записей, но добавить новые не может, так как в одной из записей синтаксическая ошибка. В общем, в первый раз табличка PostgreSQL records потёрлась из-за неверно внесённой записи, во второй раз — ну, надо было проверить, что мы не ошиблись в своих суждениях, и повторили предыдущую операцию, а в третий раз — просто дрогнула рука.  Проблема известна и на тот момент не была решена. В итоге, админку отключили совсем и временно перешли на pdnsutil edit-zone <zone>.  Теперь подумаем, как можно уменьшить влияние подобных эксцессов, сохранив приемлемый уровень контроля и комфорта при внесении правок в статичные зоны.  Первое, что приходит в голову — разделить имеющийся набор SQL-записей на две части: Редко меняющуюся. Положить её в какой-нибудь статичный бэкенд типа bind Часто меняющуюся — динамический бэк, типа SQL.  Так и сделаем: сервисные записи (s.lan) выносим в bind-бэкенд, а железные записи (h.lan) — в gpgsql.  С gpgsql всё понятно: заходим на машину и редактируем зону через CLI pdnsutil edit-zone <zone>, благо не приходится это делать достаточно часто, ведь обычно за нас эту работу выполняет Terraform или другой наливатор железных или виртуальных машин.  Проблем со сменой IP-адресов мы, как правило, не имеем, потому что никогда их не меняем (исключением может быть первоначально неудачная настройка виртуальных машин или оборудования). Подход такой: если хочешь перенести виртуальную машину или железный сервер в другое место — как правило, другой ЦОД, — то полностью перенастрой: задай новое имя, IP-адрес и т.д.  А как быть со статикой? Может, в Git положить?  В нашей компании в качестве основной системы CI/CD используется gitlab (локальная версия. конечно). На первый взгляд, правильное место для интеграции, давайте на нём и остановимся.  Алгоритм выкатки такой:   Создать задачу. Внести изменения в данные зоны. Закоммитить и запушить в новую ветку. Создать MR. Попросить коллег проверить и заапрувить. После удачного завершения задачи zones check мержим MR. Ждём, пока запустится новый pipeline и успешно отработают стадии diff staging и diff production. Проверяем diff -зоны (достаточно по одной job из diff staging/diff production).  Если diff соответствует нашим ожиданиям, то запускаем задачу stgpowerdns1.lan. После успешного прогона stgpowerdns1.lan прогоняем оставшиеся задачи в Deploy staging и Deploy production. Gitlab pipeline выглядит следующим образом:   В итоге deploy заходит по SSH на каждый узел PowerDNS, берёт master-ветку из Gitlab и подгружает в PDNS Auth новые данные зоны. Загрузка данных сделана через reload, простоя в обслуживании нет. Pull-модель выглядит более безопасной по сравнению с push, выбрали её.   Но через некоторое время выяснили «на практике», что Gitlab может быть недоступен, а изменения в DNS нас могут попросить выкатить в любое время. Сделали процедуру аварийной выкатки нужных изменений в рамках Ansible (это наш основной инструмент управления конфигурацией серверов). Выглядит так:  Берём репозиторий с данными зон. Берём Ansible (как правило, у каждого инженера Ansible репозиторий уже есть и актуален). Вносим правки в файлы зон. Катим: playbooks/powerdns/disaster_zones_deploy/main.yml -l 	powerdns_multidc -v.  Сам код:    «Железную» зону редактируем через API PowerDNS: например, добавляем RR при настройке виртуальной машины через Terraform или настройке железного хоста через MaaS. Вручную исправляем редко, но если нужно, то выполняем pdnsutil edit-zone <zone> на узле с PostgreSQL-мастером.   Хорошо, а если всё вышеописанное работает слишком медленно, громоздко, или мне не нравится катить DNS через Gitlab или другой CI/CD, да и вообще я смелый?! Берите PowerDNS-Admin. Ошибка с отбрасыванием данных исправлена, пользоваться можно. Какие есть аналоги для редактирования RR:  Pdns-gui не поддерживается (последний релиз — 25 апреля 2010). Poweradmin (478 звёзд). Имеет неприятную особенность: в текстовых записях не сохраняет двойные кавычки (“), что напрочь ломает доступ к этим записям через API, который необходим для хранения текстовыми полями информации внутри экранированного кавычками (”) блока. Хотя на функционирование самого сервера это не влияет. Также Poweradmin добавляет несколько своих таблиц и полей в исходную БД, не ведет никаких логов работы в веб-интерфейсе.   PDNS manager (160 звёзд) — два года не обновлялся.  Ну и сам PowerDNS-Admin (1,8 тыс. звёзд). Самая популярная админка, достаточно гибкая, есть аудит, разграничение прав, разные методы аутентификации, двухфакторка… но удаляет данные. Тут весело. Оказывается, Lua record, та, что реализует проверки и формирует на их основе ответ, начинает работать только после первого совершённого к ней DNS-запроса. Для обхода этой проблемы существует прекрасный костыль:  Как-то раз случился инцидент, не связанный с PowerDNS, который, в итоге, привёл к сильному замедлению задач, выполнявшихся на gitlab runners. Одним из шагов по решению проблемы была задача поправить несколько A-записей. Конечно же, через Gitlab нам этого сделать не удалось, джобы могли висеть в статусе ожидания ресурсов по 10-20 минут. В итоге зону поправили вручную и позже написали процедуру раскатки статичных зон в случае форс-мажора. У нас Kubernetes, и его записи имеют достаточно малый TTL, порядка пяти секунд. Иногда замечали, что при перевыкатке некоторых проектов их сервисные DNS-имена, которые указывают на service_ip в Kubernetes, вели в никуда (NXDOMAIN). Настройка кеша подобных записей стояла 10 секунд (max-negative-ttl=10), но почему-то они висели в кеше минуты и часы. Если сбросить кеш рекурсора или перезапустить сам рекурсор, то всё приходило в норму. Может, ошибка была в предыдущей версии рекурсора (4.4.), или нет, но после обновления на 4.5. такое поведение прекратилось.  Есть подозрения в отношении обработки ECS или DNSSEC, потому как после включения опций edns0 и/или trust-ad в /etc/resolv.conf системный резолвер начинал видеть желанный IP-адрес. Также, если попытаться разрезолвить имя через nslookup, получали NXDOMAIN, в то время как dig всё прекрасно видел. Разница в том, что dig по умолчанию выставляет query опции +adflag +edns. А вот так они уравниваются: dig +noadflag +noedns == nslookup.  В самом PDNS Auth её нет (https://github.com/powerdns/pdns/issues/63, https://github.com/powerdns/pdns/issues/10816), но можно накостылить:   использовать GeoIP-бэкенд; использовать Lua-записи; использовать Dnsdist и несколько PDNS Auth.  Самый гибкий вариант. Недостаток в том, что если исходная зона находится в другом бэке, то придётся перенести её в GeoIP-бэк полностью.  Файл bind zone:  Самый простой для внедрения вариант. Недостаток в отсутствии гибкости, один if и два варианта содержимого. Если захочется что-то более сложное, возможно, придётся писать свою Lua-функцию. В SQL-совместимом бэкенде также будет работать.  Два экземпляра PDNS Auth с разным содержимым для внутренних и внешних потребителей.  Если сюда добавить логику выбора следующего сервера при недоступности основного, то получится достаточно громоздко.  Нужно как минимум в двух случаях: для отдела ИБ и для отладки.  Решили логировать на уровне рекурсора, потому что запрос обязан через него проходить. Dnsdist можно обойти, зная реальные адреса PowerDNS-машин (закрыть не проблема), а прямой доступ в PDNS Auth есть только у локального PDNS Recursor (специально).  Каких-то специфичных требований к логированию запросов не было, поэтому сделали просто:    Если packetcache включен и ответ из него отдаётся, то в логе увидим:  Но существует более зрелый подход — DNStap (https://dnstap.info). Это гибкий, структурированный бинарный формат лога на основе protobuf. Dnsdist поддерживает его с версии 1.3 (https://dnsdist.org/reference/dnstap.html), pdns recursor — с версии 4.2 (https://docs.powerdns.com/recursor/lua-config/protobuf.html)  Кроме его поддержки на стороне рекурсора и/или балансировщика, нам понадобится DNStap-коллектор, ведь сгенерированный лог нужно где-то хранить и анализировать (желательно централизованно). В качестве коллектора можно использовать go-dns-collector. Много звёзд, поддерживается. Является одновременно агрегатором, анализатором и транспортом в другие системы. Может экспортировать логи в Elastic, Fluentd, Syslog и т.д., предоставляет метрики и REST API. Запустим коллектор: ./go-dnscollector -config config.yml   И натравим на него рекурсор: ​​cat /etc/powerdns/resursor.conf  cat log.lua  Важные моменты:  Для логирования в формате DNStap по TCP PDNS Recursor должен быть скомпилирован с параметром --enable-dnstap. Также должна быть обновлена библиотека libfstrm, иначе передача логов будет возможна только через unix-socket.  Передача данных на удалённый узел осуществляется по протоколу TCP, поэтому в 	случае недоступности коллектора неотправленные сообщения будут копиться, что в конечном итоге приведёт к исчерпанию оперативной памяти. Смотрим на параметры inputQueueSize и outputQueueSize, подбираем под себя размеры очередей, после переполнения которых DNStap логи начнут теряться.  Если логи нужны прям обязательно, да ещё чтобы не терялись, поднимаем несколько коллекторов и прописываем директиву DNStapFrameStreamServer необходимое количество раз. Ну и dnsdist давайте настроим. Тут всё аналогично:  Пример лога.   Вот что увидим в Grafana: С помощью утилиты rec_control и команды trace-regex можно получить детальный разбор запроса при прохождении его через рекурсор.  Вот с таким кодом на рекурсоре и соответствующим txt-запросом можно понять, на какие бэкенды в данный момент могут уходить DNS-запросы с того или иного хоста:     А вот так мы понимаем, где находится первичный узел кластера PostgreSQL, на который настроен PowerDNS:   На 127.0.0.1:8005 висит Patroni. Команда backends выводит список всех узлов Patroni-кластера. Тут пусть будут одни картинки.  Эта статья должна была выйти год назад. Отлежалась, дополнилась некоторыми разделами. Приведённая конфигурация работает в бою уже 1,5 года. Инцидентов, кроме описанного, за это время не было. Что нас ждёт впереди? Можно задуматься о написании собственной админки: ключевые моменты — уменьшение длительности выкатки статичных зон и уход от рисков, связанных с использованием стороннего ПО (вспоминаем PowerDNS-Admin и 2022 год в целом (https://habr.com/ru/news/t/655929/)).  Кардинально менять архитектуру не планируем, пока устраивает. Возможно, подумаем над выселением кластера PostgreSQL с узлов PowerDNS. Из рекомендаций: в инфраструктурах меньшего масштаба, или когда нет возможности настроить динамическую маршрутизацию на LB, самым подходящим решением для VIP будет использование протокола CARP/VRRP. Например, пара LB машин с Keepalived на борту в каждом ЦОД, а в resolv.conf клиента перечислены все VIP.  Вроде всё. Спасибо.