Как все начиналось
Наши пользователи взаимодействуют с DNS-сервисом различными способами. Можно настроить зону и записи в личном кабинете, кто-то предпочитает отправлять запросы напрямую в API. Есть пользователи, у которых десятки зон и тысячи записей, которыми приходится управлять. В таком случае важно иметь возможность автоматизации инфраструктуры и ведения истории изменений. Вот тут и приходят на помощь такие инструменты, как Terraform и OctoDNS.
Средства автоматизации имеют и свои минусы. После того как сервис реализует новые фичи, их поддержка сторонними инструментами имеет некоторую задержку, связанную с временем, необходимым на доработку. В некоторых случаях такие доработки представляют определенную сложность и достаточно затратны по времени реализации.
Таким случаем было добавление поддержки файловера в наш провайдер OctoDNS - octodns-edgecenter. Основная сложность заключалась в том, что команда, разрабатывающая DNS-сервис, пишет на Go. Внести изменения в провайдер Terraform было достаточно просто, так как он тоже написан на Go. Ситуация же с провайдером для OctoDNS обстояла немного иначе. Благодаря обещаниям быстрого ликбеза по стороннему сервису и всяческой поддержке, задача была взята в работу. Но прежде чем рассказывать о процессе доработок и его итоговых результатах, хочется сказать пару слов об этом инструменте и новой фиче DNS-сервиса.
Собственно Failover
DNS Failover — это опция, которая проверяет доступность сайта или сервера, а в случае возникновения проблем выводит неработающие IP-адреса из ответов DNS. Таким образом, трафик будет перенаправлен с неактивного сервера на другой активный, чтобы сервис оставался доступным для пользователей.
При настройке DNS Failover можно выбрать протокол, на основании которого будет осуществляться проверка: TCP, UDP, ICMP, HTTP, а также частоту проверки и таймаут. В зависимости от протокола могут быть добавлены дополнительные поля, которые используются для проверки. Например, для TCP, UDP и HTTP можно настроить порт, по которому будут проверяться записи.
Когда опция включена, проверки будут выполняться автоматически согласно настройкам. В случае если одна из записей не пройдет проверку, то в выдаче её уже не будет. За всю логику работы отвечают сервисы мониторинга, но не те, что обычно представляют, когда слышат слово «мониторинг» (Prometheus, Zabbix и т.д.). Первый сервис — это мониторинг-мастер, который отвечает за взаимодействие с API-сервисом и базами данных. Также мастер вносит информацию о результатах проверок, которые потом можно увидеть в журнале личного кабинета. Второй сервис — это мониторинг-агент, который осуществляет проверки в различных локациях и отправляет результаты мониторинг-мастеру.
OctoDNS - что это и зачем?
Из ридми в репозитории проекта стало понятно, что OctoDNS — это инструмент, основанный на подходе «инфраструктура как код», который позволяет развертывать и управлять DNS-зонами. Инструмент был создан GitHub и написан, неожиданно, на Python. OctoDNS был создан для разделения зон между несколькими поставщиками DNS, а также для управления записями.
Одна из ключевых идей — это поддержка единообразия, благодаря которой можно легко разделять управление между разными поставщиками или мигрировать между ними. Данная информация получена из релизной статьи на GitHub, предоставленной одним из создателей этого инструмента, во время переписки в пул-реквесте.
Инструмент представляет собой отдельную библиотеку octodns — ядро и 30+ отдельных библиотек провайдеров. Наш провайдер — это библиотека octodns-edgecenter, в которую и требовалось добавить новый функционал.
Код большинства провайдеров достаточно типизирован и отличается некоторыми настройками через константы, добавляющими или ограничивающими определённый функционал. Например, типы поддерживаемых записей, поддержку геобалансировки или динамических записей (A, AAAA или CNAME). Такое однообразие, скорее всего, связано с тем, что практически все провайдеры написаны создателями инструмента. Крупные же провайдеры, такие как Microsoft или Amazon, не поленились и переписали всё по-своему.
Любопытным условием для контрибьюторов стоит отметить 100% покрытие тестами. Может быть, это частое требование в опенсорсе, но на практике сталкиваемся с таким впервые. По итогу настолько увлеклись написанием тестов, что, достигнув полного покрытия, оставалось ещё десяток-другой негативных кейсов, которые стоило написать.
Копаемся в коде
Библиотека octodns-edgecenter представляет собой один модуль с 3 классами. На первый взгляд всё просто, но дьявол кроется, как всегда, в деталях. Один класс представляет собой клиента для запросов к API, второй — типичный представитель god object, в котором реализована абсолютно вся логика, а третий — обёртка над god object, упрощающая копипасту между провайдерами. Вся логика написана в функциональном стиле, хоть и на вход методов подаются объекты ядра. Эти объекты практически всегда служат для наполнения каких-либо коллекций, наверное, всех, которые есть в Python, и их последующей обработкой.
Отдельно хочется упомянуть повсеместное использование comprehension. Особенно с несколькими циклами, условиями и вызовом стороннего метода. С первого раза не всегда получалось понять, что же будет в результате. Некоторые даже ушли в нашу копилку, чтобы радовать коллег на очередном ревью.
Основные верхнеуровневые сущности ядра, с которыми пришлось взаимодействовать при добавлении нового функционала, — это классы YamlProvider, Zone, Record. Поэтому остановимся на них немного подробнее.
Record является базовым классом и содержит всю информацию о записи. За каждый тип записи отвечает комбинация миксинов, расширяющая функционал этого класса. Объект записи содержит такие атрибуты, как data, содержащий всю информацию, полученную из конфига, а также другие, по сути представляющие собой конкретный блок конфига, например, dynamic или octodns.
Zone содержит информацию о зоне и является контейнером для объектов записей.
YamlProvider по сути является YAML-парсером с большим количеством проверок на разрешённые параметры конфига, обязательные параметры, а также их порядок. Также класс провайдера представляет собой фабрику, создающую на основе конфига объекты записей.
YamlProvider при инициализации принимает конфиг и выполняет его структурный анализ. Затем в него передаётся зона и выполняется её наполнение записями. В зависимости от типа записи, указанного в конфиге, динамически будет выбран соответствующий класс.
Чтобы понять, что из себя представляет конфиг, рассмотрим его часть, содержащую описание динамической записи A типа:
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. Почти то, что нам нужно, но имеет меньший функционал, чем наш failover.
o00.img:
...
octodns:
healthcheck:
host: my-host-name
path: /dns-health-check
port: 443
protocol: HTTPS
...
Главный результат, который был получен, — это найден параметр octodns, в который можно было добавить что угодно и не нарваться на ошибки. Данный параметр создаёт одноимённый атрибут в объекте записи.
Пример конфига нашего файловера, который прошёл все валидации YAML-провайдера:
o00.img:
...
octodns:
failover:
frequency: 15
timeout: 10
port: 80
protocol: TCP
...
Осталось переписать octodns-edgecenter провайдер, чтобы он из API создавал атрибут, аналогичный YamlProvider, а также на основе атрибута отправлял запросы в API.
Чувство лёгкой эйфории от первого успеха быстро улетучивалось. После внесения изменений и написания юнит-тестов обозначилась следующая проблема. За отслеживание изменений записи отвечал сам класс записи, а точнее её миксины. Например, если в конфиге был изменён вес у какого-либо значения или TTL, то хотелось бы эти изменения увидеть, а также внести их в API. Всё это работало, но не для атрибута octodns. Порядок проверки для динамических записей был следующий и определялся через MRO — dynamic -> geo -> values -> TTL.
Перечитав в очередной раз документацию, был найден крайне сомнительный способ — написать кастомные классы записей и зарегистрировать их для поддержки YAML-провайдером в виде EdgeCenter/A, EdgeCenter/AAAA, EdgeCenter/CNAME. Имея свою кастомную запись, можно было добавить метод валидации изменений в атрибуте octodns.
Сказано — сделано. Правда, большим минусом данного подхода было отсутствие обратной совместимости со стандартными динамическими записями и, как следствие, совместимости с другими провайдерами. Но было решено отправить PR в надежде получить советы, как сделать лучше, так как других решений не находилось.
Контрибьютор Росс оказался человеком понимающим и в замысловатой форме истолковал, как надо правильно, часто ссылаясь на крупных провайдеров. Их мы, конечно, и до этого смотрели, но не досконально. Тратить время на чтение тысяч строк отборного кода Route53 — та ещё задачка. Собственно говоря, как раз в обсуждении PR и было выяснено, что одной из основных целей OctoDNS является объединение поставщиков DNS, чтобы пользователи могли использовать такие вещи, как разделение полномочий. Второстепенной задачей является обеспечение лёгкой миграции. В рамках этого основное внимание уделяется единообразию, в противном случае ничего из вышеперечисленного невозможно.
То есть добавление кастомных записей добавляло бы лишние действия по переписыванию типов записей в конфиге, если пользователь хотел бы перевести их на другого провайдера. Так же обстояли дела и с настройками файловера, пересекающимися с настройками параметра healthcheck.
Уход от кастомных записей решался довольно просто. В базовом классе провайдера есть метод _extra_changes(), который принимает на вход коллекцию различий между записями зоны — объекте changes, а также оба объекта зоны, между которыми проводилось сравнение. Зона, конечно, одна, но это 2 разных объекта, наполненных через разные классы — YamlProvider и EdgeCenterProvider. Оставалось добавить сравнение атрибутов octodns и внести их в changes.
Пересечение настроек решалось тем, что базовые настройки оставались бы в параметре healthcheck, а те, которые есть только в octodns-edgecenter провайдере, необходимо было вынести в параметр failover. Была только небольшая загвоздка — ядро OctoDNS поддерживало не все необходимые нам протоколы, а точнее ICMP и UDP.
Повод внести изменения в ядро — это когда какой-либо функционал востребован несколькими провайдерами. Росс, исследовав поддерживаемые протоколы у провайдеров, которые используют healthcheck, выяснил, что Azure и Route53 используют только стандартные протоколы, а NS1, в дополнение к стандартным, — ICMP. Поэтому поддержка ICMP была добавлена в ядро, как и поддержка UDP, но в качестве исключения. Для этого пришлось сделать issue на добавление валидации протоколов в провайдерах Amazon, Microsoft и IBM.
В итоге конфиг стал выглядеть таким образом:
o00.img:
...
octodns:
edgecenter:
failover:
frequency: 15
timeout: 10
healthcheck:
port: 80
protocol: TCP
...
Дополнительные настройки вынесены в параметр edgecenter согласно стайлгайду, который нигде не описан. Будем иметь в виду на будущее.
Так как ядро стало поддерживать дополнительные протоколы, его минимальная версия должна быть 1.9.0, чтобы использовать настройки файловера в провайдере octodns-edgecenter. Обновив requirements через скрипт (вручную не рекомендовалось), обновились и другие библиотеки. Оставалось ждать запуска тестов на совместимость с разными версиями языка, но эту часть работы брал на себя Росс.
Что получилось
Настройки файловера распространяются на всю запись, но есть и такие, которые влияют на конкретные значения в записи. Такие настройки задаются через пулы и правила.
До добавления поддержки файловера поддерживались только два пула, не отвечающих за геобалансировку, — 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.
Для понимания, при описании метаданных и фильтров использовалась терминология сервиса DNS, несколько отличающаяся от принятой в OctoDNS. Ресурс — это запись, а записи — это значения.
Гайд по установке, настройке и использованию OctoDNS
Перед установкой OctoDNS необходимо убедиться, что у вас установлен Python версии не ниже 3.9.
-
Создайте новую папку, например, octodns, и перейдите в нее:
mkdir octodns && cd octodns
-
Создайте виртуальное окружение и активируйте его:
python -m venv env && source env/bin/activate
-
При помощи пакетного менеджера pip установите octodns:
pip install octodns
-
При помощи pip установите последнюю актуальную версию провайдера EdgeЦентр (octodns-edgecenter):
pip install -e git+https://github.com/octodns/octodns-edgecenter.git#egg=octodns-edgecenter
-
Далее необходимо создать следующую структуру минимально необходимых файлов конфига:
. ├──config │ ├── config.yaml │ ├── zones │ │ └── example4.com.yaml
config.yaml - будет содержать настройки провайдера EdgeЦентр и ямл провайдера
example4.com.yaml - будет содержать настройки одноименной зоны
-
Заполняем файл config.yaml:
providers: ec: class: octodns_edgecenter.EdgeCenterProvider # Your API key token: env/EC_TOKEN token_type: APIKey auth_url: https://api.edgecenter.ru/iam url: https://api.edgecenter.ru/dns/v2 config: class: octodns.provider.yaml.YamlProvider directory: ./config/zones zones: example4.com.: sources: - config targets: - ec
В данном конфиге необходимо настроить провайдеров, а также зону. В зоне указывается, откуда мы будем брать данные и куда применять. Обратите внимание, название зон указываются с корневым доменом(FQDN), а название файла yaml в папке zones должно совпадать с именем вашей зоны.
Первым провайдером будет EdgeCenterProvider. Для краткости обозначим его ec. Он будет выступать в роли цели для создания записи. Для него необходимо указать следующие данные:
class - имя класса из библиотеки octodns-edgecenter
token - API KEY токен, который будем брать из переменных окружения
token_type: тип токена
auth_url - url сервиса аутентификации EdgeЦентр
-
url - url DNS API сервиса EdgeЦентр
Вторым провайдером будет YamlProvider. Для краткости обозначим его config. Он будет выступать в роли источника данных для наполнения зоны. Для него необходимо указать следующие данные:
class - имя класса из библиотеки octodns
directory - путь до папки с конфигами зон
-
Заполняем файл example4.com.yaml:
--- '': # weight and failover dynamic: pools: # pool that adds weight to resource records meta weight: 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: 10 timeout: 10 healthcheck: port: 80 protocol: TCP ttl: 60 type: A # values not from pools are added without meta values: - 5.5.5.5 - 6.6.6.6 - 7.7.7.7
В нем мы указываем необходимые настройки, которые нужно применить к нашей зоне в EdgeЦентр. Например, указываем ресурсные записи, параметры их балансировки, настройку сервиса проверки доступности и т.д. (Примеры оформления и заполнения можно найти здесь:https://github.com/octodns/octodnsedgecenter/blob/main/tests/config/failover.test.yaml
-
Экспортируем наш API KEY в переменные окружения:
export EC_TOKEN='YOUR API KEY'
API KEY можно выписать в личном кабинете клиента EdgeЦентр.
После всех настроек можно приступать к использованию OctoDNS.
-
Для валидации заполненного вами конфига можно вызвать следующую команду (путь до config-file может отличаться, в зависимости от того, как вы назвали папку и файл конфигурации):
octodns-validate --config-file=config/config.yaml
Если после выполнения данной команды не были выведены ошибки, то валидация прошла успешна.
-
Для предпросмотра планируемых изменений в вашей зоне выполните команду (путь до config-file может отличаться):
octodns-sync --config-file=config/config.yaml
При успешном выполнении команды вы увидите информацию о планируемой операции, например:
******************************************************************************** * example4.com. ******************************************************************************** * ec (EdgeCenterProvider) * Create <ARecord A 60, example4.com., ['5.5.5.5', '6.6.6.6', '7.7.7.7'], {'weight': {'fallback': None, 'values': [{'value': '5.5.5.5', 'weight': 25, 'status': 'obey'}, {'value': '6.6.6.6', 'weight': 50, 'status': 'obey'}, {'value': '7.7.7.7', 'weight': 75, 'status': 'obey'}]}}, [{'pool': 'weight'}]> (config) ******************************************************************************** * Summary: Creates=1, Updates=0, Deletes=0, Existing Records=0
-
Для применения изменений к вашей зоне следует выполнить следующую команду (путь до config-file может отличаться):
octodns-sync --config-file=config/config.yaml --doit
При успешном выполнении команды вы увидите информацию о внесенных изменениях, например:
INFO EdgeCenterProvider[ec] apply: making 1 changes to example4.com. INFO EdgeCenterProvider[ec] creating: Create <ARecord A 60, example4.com., ['5.5.5.5', '6.6.6.6', '7.7.7.7'], {'weight': {'fallback': None, 'values': [{'value': '5.5.5.5', 'weight': 25, 'status': 'obey'}, {'value': '6.6.6.6', 'weight': 50, 'status': 'obey'}, {'value': '7.7.7.7', 'weight': 75, 'status': 'obey'}]}}, [{'pool': 'weight'}]> (config) INFO Manager sync: 1 total changes
Теперь можно перейти в личный кабинет клиента в EdgeЦентр, перейти к настраиваемой через OctoDNS зоне и убедиться, что изменения были применены к данной зоне успешно.
Содержимое зоны example4.com до внесения изменений через OctoDNS:
Содержимое зоны example4.com после внесения изменений через OctoDNS (Добавился RRSET Type A):
А вот и само содержимое RRSET Type A:
Как итог
В целом, опыт контрибьюции в опенсорсный проект был увлекательным. Получилось сделать даже немного больше, чем планировалось, переписав часть кода, отвечающую за пулы и правила. В итоге, самой большой проблемой оказалось не незнание предметной области, а изучение работы библиотек, сопоставимое по затраченному времени с изучением фреймворка.