DNS давно перестал быть набором вручную поправленных записей в панели регистратора. Когда у компании десятки зон, десятки окружений и сотни записей, править их в UI — путь к дрейфу конфигурации и человеческим ошибкам. Подход Infrastructure as Code позволяет описывать зоны и записи декларативно, хранить историю в Git, запускать ревью и откатывать изменения одной командой. В этой статье разбираю, как организовать управление DNS у регистратора через Terraform: какие есть провайдеры, как строить репозиторий и state, как импортировать существующие зоны без простоя, какие шаблоны и приёмы облегчают жизнь, и как встроить всё это в GitOps-процесс.
Зачем переводить DNS в Terraform
Основной мотив — воспроизводимость и контроль. Любую правку в зоне можно превратить в PR, получить plan-вывод в комментариях, обсудить TTL, убедиться, что не удаляем нужные записи, и применить через CI. История в Git фиксирует, кто и что менял, а откат становится делом revert-коммита. Плюс Terraform обнаруживает дрейф: если запись поправили вручную в панели, на следующем планировании это станет видно.
Второй мотив — повторяемость. Типовые шаблоны MX, SPF, DKIM, DMARC, SRV, CAA можно оформить в модули и переиспользовать в десятках зон. Меняется политика — меняем модуль, а не бегаем по интерфейсам. Подробный разбор почтовых записей и их назначения смотрите в материале «Полная шпаргалка по SPF, DKIM и DMARC» по ссылке DNS-записи для почтовой аутентификации.
Третий мотив — автоматизация. В GitOps-подходе изменения в основной ветке автоматически запускают план и применяются под присмотром политик. Это сокращает TTM для новых доменов и снижает риск ручных ошибок.
Провайдеры и доступы: регистратор vs DNS-хостинг
Чтобы управлять DNS из Terraform, нужен провайдер. Есть два сценария:
- Управляем DNS у регистратора (когда он же хостит вашу зону на своих NS). Тогда пригодится провайдер, поддерживающий API конкретного регистратора.
- Держим зону у внешнего DNS-хостинга. Тогда используем провайдер этой платформы, а у регистратора только NS указываем на нужные сервера.
Выбор зависит от ваших требований к функциям (например, поддержка ALIAS/ANAME на апексе, геотаргетинг, API-лимиты, доступность DNSSEC-операций). Важно заранее проверить:
- Какие типы записей поддерживает провайдер и Terraform-ресурсы для них.
- Нужен ли отдельный ресурс для самой зоны (часто да) и как создаются/удаляются зоны.
- Есть ли ограничения при массовых изменениях (rate limit) и как настраивать ретраи/таймауты.
- Как хранить и передавать токены/ключи в CI (переменные окружения, отдельные секрет-хранилища).
Совет: заведите отдельный технический доступ с минимально необходимыми правами только на нужные зоны. Не используйте личную учетку с MFA в автоматизации.
Структура репозитория: зоны как модули, окружения как рабочие пространства
Удобная практика — вынести каждую зону в модуль и иметь слой-оркестратор, который подключает модули с нужными переменными. Это позволяет переиспользовать шаблоны по проектам и обеспечить единообразие. Окружения (prod, stage, dev) удобно разделять рабочими пространствами Terraform или каталогами с отдельным состоянием.
Пример структуры:
dns/
modules/
zone/
main.tf
variables.tf
outputs.tf
envs/
prod/
main.tf
versions.tf
terraform.tfvars
stage/
main.tf
versions.tf
terraform.tfvars
Где модуль zone описывает создание самой зоны и набор типовых записей, а файлы в envs/<env> перечисляют, какие домены обслуживаем и какими переменными наполняем шаблоны (почта, SPF-провайдеры, CAA-политика и пр.).
Базовые элементы HCL: зона и записи
Ниже — условные имена ресурсов. В вашем провайдере они будут другими (например, отдельные ресурсы на A, AAAA, CNAME, MX, TXT, SRV, CAA и т.д.). Суть в том, что зона — это один ресурс, а записи — другие, обычно с полем zone_id/zone.
# Провайдер и креды
provider "exampledns" {
api_token = var.api_token
}
# Зона (example.com)
resource "exampledns_zone" "root" {
name = var.domain
}
# А-записи
resource "exampledns_record_a" "www" {
zone_id = exampledns_zone.root.id
name = "www"
value = "203.0.113.10"
ttl = 300
}
# AAAA
resource "exampledns_record_aaaa" "www6" {
zone_id = exampledns_zone.root.id
name = "www"
value = "2001:db8::10"
ttl = 300
}
# MX
resource "exampledns_record_mx" "mx" {
zone_id = exampledns_zone.root.id
name = "@"
priority = 10
value = "mail.example.com."
ttl = 600
}
# TXT (SPF)
resource "exampledns_record_txt" "spf" {
zone_id = exampledns_zone.root.id
name = "@"
value = "v=spf1 include:_spf.provider.example -all"
ttl = 300
}
Важные тонкости:
- У многих провайдеров имя корня зоны обозначают как
@или пустую строку. Проверяйте документацию. - Хвостовая точка у значений типа FQDN (например, для MX, CNAME) может требоваться или добавляться автоматически. Лучше задавать явно.
- Для апекса (
@) часто нельзя использовать CNAME. Если требуется редирект/резолв через CNAME-подобную семантику, ищите поддержку ALIAS/ANAME у провайдера.
Жизненный цикл и защита от удаления
Для базовых записей и самой зоны полезно ставить защиту:
lifecycle {
prevent_destroy = true
}
Это спасает от случайного удаления зоны с десятками записей. Снимаем защиту явным PR и осознанно.
Импорт существующих зон без простоя
Если зоны уже крутятся у регистратора, разумнее сначала «подтянуть» инфраструктуру в состояние Terraform, не меняя реальность, и только потом начать вносить правки. Алгоритм:
- Опишите каркас ресурсов для зоны и ключевых записей так, чтобы соответствовать текущему факту.
- Сделайте
terraform importдля зоны и записей, используя идентификаторы из панели или API. - Запустите
terraform planи добейтесь нулевого diff. Возможно, потребуется поправить имена, TTL или точки в FQDN. - Зафиксируйте состояние в удалённом бекенде и включите GitOps-процесс.
Пока вы не меняете NS у домена и не трогаете содержимое записей, пользователи ничего не замечают. Если планируется перенос на другой DNS-хостинг, готовим зону зеркально, снижаем TTL заранее (например, до 300), проверяем соответствие контента и уже потом переключаем NS у регистратора. Если вы только заводите новый домен, удобно совместить это с регистрация доменов у одного провайдера, чтобы управлять NS и DS из одного места.
Перед переключением NS важно дать время в 1–2 TTL после снижения, чтобы кеши выветрились. И сразу после смены NS проверьте критичные записи: A/AAAA сайта, MX почты, TXT для SPF/DKIM/DMARC.

Моделируем типовые записи и политику
Хорошая практика — завернуть повторяющиеся паттерны в модульные блоки и переменные:
- SPF как строка, собранная из include-списка провайдеров рассылок, с ограничением на 10 DNS-lookup. При необходимости — «сплющивание» include.
- DKIM: по домену и селекторам, часто несколько TXT-ключей для ротации.
- DMARC:
_dmarcс политикойp=quarantine|reject, отчетами и правильным TTL. Как читать отчеты — в статье Парсинг DMARC-отчетов и доставляемость. - CAA: определяем, кто может выпускать сертификаты, плюс указание e-mail для отчетов.
- SRV: для VoIP, XMPP и прочих сервисов — аккуратность в приоритете, весе, порте и цели.
- NS для делегирования подзон (split-horizon, delegated sub-zones для отдельных команд).
Часть записей может быть «автоуправляемой» внешними инструментами. Например, валидационные TXT для разных сервисов или временные записи для ACME DNS-01. Чтобы Terraform не «дрался» с ними, используйте lifecycle { ignore_changes = [...] } на соответствующих ресурсах или вынесите такие записи в отдельную, неуправляемую Terraform зону или подзону. Для выпуска сертификатов через DNS-01 проверьте политику CAA и наличие валидных SSL-сертификаты в вашем процессе.
Переменные, locals и генерация записей
Зачастую удобнее описывать данные декларативно в виде карт, а записи генерировать в циклах:
variable "a_records" {
type = map(string)
}
locals {
default_ttl = 300
}
resource "exampledns_record_a" "bulk" {
for_each = var.a_records
zone_id = exampledns_zone.root.id
name = each.key
value = each.value
ttl = local.default_ttl
}
Аналогично можно описать массив MX (с приоритетом), набор TXT, SRV и т.д. Такой подход уменьшает объём кода и упрощает ревью: видно только дельту данных.
State и GitOps: план, ревью, применение
Чтобы команда могла одновременно работать над зонами, нужен удалённый бекенд состояния с блокировками и версионированием. Так вы избежите конкурентных применений. Дальше строим простую цепочку:
- PR запускает
terraform initиterraform plan, артефакт плана публикуется в комментарии. - После одобрения и слияния запускается
terraform applyот имени сервис-аккаунта. - Права на apply ограничены: только основной pipeline с подписанными артефактами.
Рекомендуется добавлять защитные флаги и таймауты в CI: -lock-timeout, линтер HCL, форматирование terraform fmt -check и валидацию terraform validate. Для критических зон используйте «двухключевой» процесс: отдельное подтверждение применений или ручной hold в CI.

Безопасность и доступы
Ключи доступа к API провайдера храним в секретах CI/CD или системном секрет-хранилище, передаём провайдеру через переменные окружения или Terraform variables с пометкой sensitive. Не коммитим *.tfvars с секретами, добавляем их в .gitignore. Разделяем роли: чтение для планов и запись только для применений.
Отдельно следите за лимитами API. Массовые изменения могут попасть под rate limit. Помогают батчинг, искусственные задержки между ресурсами (если провайдер поддерживает), а также разбивка применений по зонам.
Отладка и проверки: dig — лучший друг
После каждого изменения выполняйте серию проверок. На локальном резолвере и с авторитативных:
# Проверить A/AAAA
dig +short www.example.com A
dig +short www.example.com AAAA
# Проверить NS и SOA
dig NS example.com
dig SOA example.com
# Проверить TXT (SPF/DMARC/DKIM)
dig +short TXT example.com
dig +short TXT _dmarc.example.com
# С учетом авторазрешения с авторитетных серверов
dig @ns1.provider.example example.com A
Не забудьте про TTL и кеши: планирование и проведение переключений делайте с учетом того, что пользователи могут видеть запись ещё в течение прежнего TTL. Для критичных сценариев заранее снижайте TTL за 24–48 часов.
Типичные грабли и как их обходить
- Аpex и CNAME: используйте ALIAS/ANAME, если провайдер поддерживает, либо держите A/AAAA.
- Хвостовые точки в значениях FQDN: лучше указывать явно, чтобы избежать нежданных суффиксов.
- SPF превышает лимит 10 DNS-lookup: сплющивайте
includeили пересматривайте политику. - DKIM и ротация: храните несколько селекторов и TTL не ниже 1 часа, планируйте смену ключей.
- Drift из-за ручных правок в панели: не правьте вручную; если пришлось — делайте импорт и синхронизируйте код.
- Удаления при рефакторинге: ставьте
prevent_destroyи проверяйте план целиком, а не точечно.
Мультизоны и мультиаккаунты
Если зон много и они разбросаны по нескольким регистраторам, используйте алиасы провайдера и карты зон с привязкой к провайдеру. Описывайте зоны данными, а не кодом: один модуль — множество инстансов через for_each. Так вы избежите дублирования и получите единый формат.
DNSSEC и DS-записи
Часть регистраторов позволяет управлять DNSSEC через API. Процесс обычно двухшаговый: включаем подпись в хостинге зоны (генерируется ключ и DS), после чего публикуем DS у регистратора. Держите это в одном месте кода и документируйте порядок действий. При ротации ключей не забывайте обновлять DS и выдерживать время на распространение. Если у вас в конвейере также автоматизирован выпуск и продление SSL-сертификаты, увязывайте сроки ротации ключей DNSSEC и CAA-политику.
Чек-лист внедрения
- Определите провайдера и проверьте поддержку нужных типов записей и возможностей.
- Соберите каркас модулей для зоны и типовых записей, определите переменные и locals.
- Настройте удалённый state с блокировками и доступами по ролям.
- Импортируйте существующие зоны и добейтесь нулевого плана.
- Постройте GitOps: план в PR, обязательное ревью, контролируемый apply.
- Опишите процедуры TTL-менеджмента и переключений NS.
- Защитите критические ресурсы
prevent_destroy, документируйте ручные исключения. - Автоматизируйте проверки dig и бизнес-критичных записей после каждого apply.
В результате вы получите воспроизводимый, проверяемый и безопасный процесс управления DNS, где любое изменение проходит через код, ревью и автоматические проверки. Это ускоряет выпуск новых доменов, снижает риск ошибок и делает инфраструктуру прозрачной для команды.


