EdgeЦентр 22 октября 2024

Как внедрить OctoDNS — опыт разработчиков EdgeCenter

Материал о результатах межкомандного взаимодействия специалистов EdgeCenter по внедрению OctoDNS: заодно модернизировали опенсорс-библиотеку

Игорь Замараев
Разработчик сети доставки контента EdgeCenter

Занимается разработкой, в EdgeCenter отвечает за сеть доставки

DNS — это инфраструктура DNS-хостинга, куда пользователь загружает информацию о своих доменах. Можно взаимодействовать с ней разными способами. Например, настраивать зону и записи в личном кабинете или отправлять запросы в API. Но если у вас десятки зон и тысячи записей — управлять ими вручную будет сложно. В этом случае на помощь приходят инструменты автоматизации и ведения истории изменений — например, Terraform и OctoDNS.

У средств автоматизации есть один серьезный минус — если появляются новые функции, то для их поддержки сторонними инструментами требуется доработка. В некоторых случаях она вызывает определенные сложности и требует времени на реализацию. 

Именно это случилось при добавлении файловера в провайдер OctoDNS — octodns-edgecenter. Дело в том, что команда разработки DNS пишет на Go. При работе с провайдером Terraform проблем не возникало — он тоже написан на Go. Но OctoDNS написан на Python. Для решения этой проблемы был привлечен специалист из другой команды, который знает Python, но не разбирается в тонкостях работы DNS. 

Рассказываем о результатах межкомандного взаимодействия специалистов EdgeЦентр по внедрению OctoDNS.

Немного о Failover

DNS Failover — это опция, которая проверяет доступность сайта или сервера. Если возникают какие-то проблемы, то она выводит неработающие IP-адреса из ответов DNS. В результате трафик перенаправляется с неактивного сервера на активный, а сервис остается доступным для пользователей. 

При настройке DNS Failover можно выбрать протокол для проверки (TCP, UDP, ICMP, HTTP), частоту проверки и таймаут. В зависимости от выбранного протокола можно добавить дополнительные поля, которые используются для проверки. Например, для TCP, UDP и HTTP можно настроить порт для проверки записей. 

Когда Failover работает, проверки выполняются автоматически в соответствии с настройками. Если какая-то запись не проходит проверку — она не попадает в выдачу. За логику работы отвечают сервисы мониторинга:

  • Мониторинг мастер — отвечает за взаимодействие с API и базами данных, вносит информацию о результатах проверок (их можно увидеть в журнале в личном кабинете);
  • Мониторинг агент — делает проверки в разных локациях и отправляет результаты мониторинг мастеру.

OctoDNS — что это и зачем нужно?

OctoDNS — это инструмент для развертывания и управления DNS-зонами. Он создан для разделения зон между несколькими поставщиками DNS и управления записями. Проект основан на IaC-подходе («инфраструктура как код»), написан на Python и размещен на Github, мейнтейнер — Росс МакФарланд.  

Ключевая идея OctoDNS — поддержка единообразия. Благодаря этому можно легко разделять управление между разными поставщиками или мигрировать между ними. 

Инструмент представляет собой отдельную библиотеку octodns — ядро и 30+ отдельных библиотек провайдеров. Одна из таких библиотек — octodns-edgecenter. В нее и требовалось добавить новый функционал. 

У большинства провайдеров код достаточно типизирован и отличается некоторыми настройками через константы, которые добавляют или ограничивают определенный функционал — например, типы поддерживаемых записей, поддержка геобалансировки или динамических записей (А, АААА или CNAME). Подобное единообразие связано с тем, что большинство провайдеров написано создателями инструмента. Крупные провайдеры — например, Microsoft и Amazon — переписали все по-своему. 

Копаемся в коде

Библиотека octodns-edgecenter представляет собой один модуль с 3 классами:

  • Клиент для запросов к API;
  • Типичный god object, в котором реализована вся логика;
  • Обертка над god object, упрощающая копипасту между провайдерами.

Вся логика написана в функциональном стиле, хоть и на вход методов попадаются объекты ядра. Они практически всегда служат для наполнения каких-либо коллекций, которые есть в Python, и их последующей обработки. Также повсеместно используется comprehension (особенно с несколькими циклами, условиями и вызовом стороннего метода). 

Основные верхнеуровневые сущности ядра, с которыми нужно взаимодействовать при добавлении нового функционала — это классы:

  • Record — базовый класс, который содержит всю информацию о записи. За каждый тип записи отвечает комбинация миксинов, которая расширяет функционал этого класса. Объект записи содержит атрибуты — data (содержит всю информацию, полученную из конфига) и другие (представляют конкретный блок конфига — dynamic или octodns);
  • Zone — содержит информацию о зоне и является контейнером для объектов записей;
  • YamlProvider — yaml-парсер с большим количеством проверок на разрешенные параметры конфига, обязательные параметры и их порядок.

YamlProvider при инициализации принимает конфиг и выполняет его структурный анализ. Затем в него передается зона и выполняется ее наполнение записями. В зависимости от типа записи, который указан в конфиге, динамически выбирается соответствующий класс. 

Чтобы понять, что из себя представляет конфиг, рассмотрим его часть, которая содержит описание динамической записи А-типа:

o00.img:
  dynamic:
    pools:
      weight:
        values:
        — value: 1.1.1.1
        weight: 25
        — value: 2.2.2.2
        weight: 75
      rules:
    — pool: weight
  ttl: 300
  type: A
  values:
  — 1.1.1.1
  — 2.2.2.2

Сначала мы попробовали добавить какой-нибудь кастомный параметр — например, failover. Чаще всего это это заканчивалось ошибкой валидации или его игнорированием — в объект записи он не попадал. 

После этого мы более подробно изучили документацию в репозитории ядра и нашли упоминание healthcheck параметра. Это почти то, что нужно, но имеет меньший функционал, чем файловер:

o00.img:
  ...
    octodns:
      healthcheck:
        host: my-host-name
        path: /dns-health-check
        port: 443
        protocol: HTTPS
  ...

Благодаря этому мы нашли параметр octodns, в который можно было добавить что угодно и не нарваться на ошибки. Этот параметр создает одноименный атрибут в объекте записи. 

Пример конфига файловера, который прошел все валидации YamlProvider:

o00.img:
  ...
  octodns:
    failover:
      frequency: 15
      timeout: 10
      port: 80
      protocol: TCP
  ...

Осталось переписать octodns-edgecenter провайдер, чтобы он из API создавал атрибут, аналогичный YamlProvider, а также на основе атрибута отправлял запросы в API.

Однако после внесения изменений и написания unit-тестов появилась новая проблема. За отслеживание изменений записи отвечает сам класс записи, а точнее его миксины. Например, если в конфиге изменить вес у какого-либо значения или ttl, то хотелось бы эти изменения увидеть и внести их в API. И это работает. Но не для атрибута octodns. Порядок проверки для динамических записей был следующий и определялся через mro — dymamic -> geo -> values -> ttl.

В результате мы попробовали другой способ — написать кастомные классы записей и зарегистрировать их для поддержки YamlProvider в виде EdgeCenter/A, EdgeCenter/AAAA, EdgeCenter/CNAME. При наличии своей кастомной записи можно добавить метод валидации изменений в атрибуте octodns.

У этого способа есть большой минус — отсутствие обратной совместимости со стандартными динамическими записями и, как следствие, с другими провайдерами. 

На этой стадии мы решили спросить совета у контрибьютора Росса МакФарланда. В результате мы пришли к пониманию того, что одна из основных целей OctoDNS — объединение поставщиков DNS. Благодаря этому пользователям доступны разделение полномочий и легкая миграция. Поэтому необходимо уделять внимание единообразию. 

Добавление кастомных записей приводит к лишним действиям по переписыванию типов записей в конфиге, если пользователь хочет перевести их на другого провайдера. То же самое с настройками файловера, которые пересекаются с настройками healthcheck параметра. 

Уйти от кастомных записей довольно просто. В базовом классе провайдера есть метод _extra_changes(). Он принимает на вход коллекцию различий между записями зоны — объекте changes, а также двумя объектами зоны, между которыми проводилось сравнение. Зона одна, но это 2 разных объекта, наполненных через разные классы — YamlProvider и EdgeCenterProvider. Поэтому нужно добавить сравнение атрибутов octodns и внести их в changes.

Пересечение настроек решается тем, что базовые настройки остаются в healthcheck параметре, а те, что есть в octodns-edgecenter провайдере, нужно вынести в failover параметр. Правда, ядро octodns поддерживает не все необходимые протоколы, в частности — ICMP и UDP.

Соответственно, требуется внести изменения в ядро. Но это обосновано лишь в том случае, если функционал востребован несколькими провайдерами. Росс МакФарланд исследовал поддерживаемые протоколы провайдеров, использующих healthcheck, и выяснил, что Asure и Route53 используют только стандартные протоколы, а NS1, в дополнение к стандартным — ICMP. Поэтому поддержка ICMP была добавлена в ядро, как и поддержка UDP (но в качестве исключения). Для этого пришлось сделать issue на добавление валидации протоколов в провайдерах Amazon, Microsoft и IBM. 

В результате конфиг стал выглядеть так:

o00.img:
  ...
  octodns:
    edgecenter:
      failover:
        frequency: 15
        timeout: 10
  healthcheck:
    port: 80
    protocol: TCP
  ...

Т.к. ядро поддерживает дополнительные протоколы, его минимальная версия должны быть 1.9.0, чтобы использовать настройки файловера в провайдере octodns-edgecenter. После апгрейда requirements через скрипт обновились и другие библиотеки. Осталось лишь дождаться тестов на совместимость с разными версиями языка, но эту часть работы взял на себя Росс МакФарланд.

Что получилось

Настройки файловера распространяются на всю запись. Но есть ряд настроек, которые влияют на конкретные значения в записи — они задаются через пулы и правила. 

До добавления поддержки файловера поддерживались только 2 пула, не отвечающих за геобалансировку — other и weight. При этом можно было использовать только один, т.к. в правилах можно указать только один дефолтный пул. Мы добавили поддержку дополнительного backup-пула и сделали возможным использование всех трех пулов одновременно через fallback ссылки. 

Использование пулов и правил для балансировки довольно сложное и может отличаться конфигурацией для разных провайдеров. Добавляя использование новых пулов, мы сохранили идеи OctoDNS, экстраполировав балансировку DNS сервиса на описание конфигурации.

Для более простого понимания использования пулов продемонстрируем, как конфиг записи конвертируется в json при отправке запросом в API DNS сервиса:

example.dynamic:
  dynamic:
    pools:
      # pool that adds backup to resource records meta
      backup:
        fallback: other
        values:
          — value: 5.5.5.5
          — value: 8.8.8.8
          — value: 9.9.9.9
      # pool that adds default to resource records meta
      other:
        values:
         — value: 1.1.1.1
         — value: 2.2.2.2
         — value: 3.3.3.3
      # pool that adds weight to resource records meta
      weight:
        fallback: backup
        values:
         — value: 5.5.5.5
           weight: 25
         — value: 6.6.6.6
           weight: 50
         — value: 7.7.7.7
           weight: 75
    rules:
      — pool: weight
  # failover configuration
  octodns:
    edgecenter:
      failover:
        frequency: 15
        timeout: 10
    healthcheck:
      port: 80
      protocol: TCP
  ttl: 60
  type: A
  # values not from pools are added without meta
  values:
  — 1.1.1.1
  — 2.2.2.2
  — 3.3.3.3
  — 4.4.4.4
  — 5.5.5.5
  — 6.6.6.6
  — 7.7.7.7
  — 8.8.8.8
  — 9.9.9.9

В качестве дефолтного пула в правилах указывается тот, у которого самый высокий приоритет. Приоритет пулов по убыванию выглядит так: weight -> backup -> other.

Значения в пулах weight и backup могут пересекаться. В параметре values можно добавлять значения, не указанные в пулах.

{
"rrsets": [
{
"name": example.dynamic.failover.test",
"type": "A",
"ttl": 60,
"meta": {
"failover": {
"frequency": 15,
"port": 80,
"protocol": "TCP",
"timeout": 10
}
},
"filters": [
{
"type": "weighted_shuffle"
},
{
"limit": 1,
"type": "first_n"
},
{
"type": "is_healthy",
"strict": false
}
],
"resource_records": [
{
"content": [
"1.1.1.1"
],
"meta": {
"default": true
}
},
{
"content": [
"2.2.2.2"
],
"meta": {
"default": true
}
},
{
"content": [
"3.3.3.3"
],
"meta": {
"default": true
}
},
{
"content": [
"4.4.4.4"
]
},
{
"content": [
"5.5.5.5"
],
"meta": {
"weight": 25,
"backup": true
}
},
{
"content": [
"6.6.6.6"
],
"meta": {
"weight": 50
}
},
{
"content": [
"7.7.7.7"
],
"meta": {
"weight": 75
}
},
{
"content": [
"8.8.8.8"
],
"meta": {
"backup": true
}
},
{
"content": [
"9.9.9.9"
],
"meta": {
"backup": true
}
}
]
}
]
}

Для упрощения корневая запись failover.test не описана в конфиге. 

Как видно из json:

  • weight отвечает за установку веса в метаданные;
  • backup отвечает за установку параметра, определяющего, следует ли включать запись ресурса в ответ только в том случае, если DNS Failover обнаруживает сбой всех нерезервных записей;
  • other отвечает за установку параметра, определяющего, следует ли использовать запись ресурса по умолчанию, когда другие записи не выбраны на основе их метаданных;
  • values содержит все значения записи, а также те значения, в которых не будет добавлена какая-либо информация в метаданные.

В зависимости от того, какие пулы использованы, в json автоматически добавляются соответствующие фильтры с дефолтными значениями. Например, для пула weight — это weighted_shuffle, который отвечает за частоту попадания записи ресурса в выборку, и first_n, который определяет количество записей ресурса в ответе. Для backup — это is_healthy, благодаря которому записи ресурсов будут включены в ответ на основе результатов мониторинга отказоустойчивости DNS.

Подведем итоги

В результате у нас получилось сделать даже немного больше, чем планировалось. Мы не только внедрили OctoDNS, но и переписали часть кода, отвечающего за пулы и правила и модернизировали опенсорс-библиотеку.