🧩
Domains

DNS as Code: Managing Records with Terraform and OctoDNS

10.11.2026
← All articles

Most people add DNS records by hand in a web panel: an A record, then MX, then SPF and DKIM for email. For a single site that is fine. Once you have more than ten domains, several people on the team, or a need to know exactly who changed what and why, manual editing becomes a source of confusion. "DNS as code" means keeping the zone in text files, versioning them in Git and letting a tool apply them to the DNS provider.

Why keep DNS in files

The first benefit is history. Every change is a commit with an author, a date and a message. If email breaks in the morning, you can immediately see who touched MX or SPF yesterday and revert it with one command. The second benefit is review: a change arrives as a pull request, a colleague checks it, and the tool shows in advance which records will be added, changed or deleted. The third is consistency, so a chain of ten branch sites never ends up with a forgotten DMARC record or a stale IP.

Who needs it

A small Tashkent coffee shop with one domain and five records does not need Terraform. An online store with several brands, staging and production, and dozens of subdomains, or the IT team of a clinic network, will get value quickly. The key requirement is an API at your DNS provider, or your own DNS server. If your domain is registered with sayt.uz, you can point its nameservers to any DNS service from the cabinet, keeping billing there while the zone lives on a code-managed server.

DNS records in Terraform

Terraform is declarative: you describe the desired state and it applies the difference. The example uses the generic hashicorp/dns provider, which talks RFC 2136 dynamic updates to your own server such as BIND, authenticated with a TSIG key.

terraform {
  required_providers {
    dns = {
      source  = "hashicorp/dns"
      version = "~> 3.4"
    }
  }
}

provider "dns" {
  update {
    server        = "192.0.2.53"
    key_name      = "terraform-key."
    key_algorithm = "hmac-sha256"
    key_secret    = var.tsig_secret
  }
}

resource "dns_a_record_set" "www" {
  zone      = "example.uz."
  name      = "www"
  addresses = ["203.0.113.10"]
  ttl       = 3600
}

Run terraform init, then terraform plan -out=dns.plan, then terraform apply dns.plan. Read every plan carefully: an unexpected destroy line means someone added a record by hand or a name in the file has a typo.

OctoDNS: the zone as YAML

OctoDNS is built specifically for DNS. It keeps the zone in a YAML file and syncs it to one or more providers, which makes running the same zone on two providers for redundancy easy.

providers:
  config:
    class: octodns.provider.yaml.YamlProvider
    directory: ./zones
  cloudflare:
    class: octodns_cloudflare.CloudflareProvider
    token: env/CLOUDFLARE_TOKEN

zones:
  example.uz.:
    sources:
      - config
    targets:
      - cloudflare

The zone file zones/example.uz.yaml holds records, with an empty key for the apex. octodns-sync --config-file=config/production.yaml runs as a dry run by default; you add --doit to actually apply changes, which protects against accidental deletions.

Migrating an existing zone

The riskiest step is the initial import. Any live record missing from your files may be treated as extra and deleted. Export the current zone first, copy every record, and do not apply anything until the first plan or dry run shows zero changes. Terraform uses terraform import or import blocks; OctoDNS offers octodns-dump. A training centre in Samarkand moved a 40-record zone this way and only made its first real change after the plan reported "0 to add, 0 to change, 0 to destroy", with no downtime for the site or email.

Secrets, state and manual edits

Never commit API tokens or TSIG keys; pass them through environment variables or CI secrets. The terraform.tfstate file can contain sensitive values in plain text, so keep it out of the repository and in protected storage. Agree as a team not to edit a code-managed zone by hand, because the next apply will silently revert the edit. Before a big migration, lower TTLs on key records to 300 seconds a day in advance so you can roll back quickly, then raise them again afterwards.

Automating with CI

The most comfortable setup runs plan or dry run on every pull request, posts the result as a comment, and applies after merge to the main branch. Nobody touches the live zone directly, every change gets a second pair of eyes, and a rollback is just a revert of the previous commit.

Related articles

🤖 Why .ai domains are expensive and which restrictions to know before you register 🌐 www or No www: Which Version of Your Domain Is Right 🤝 Becoming a Domain Reseller: A Business Model for Uzbekistan 🕳️ Subdomain takeover: the risk hiding in forgotten DNS records
🌐 Language
🇺🇿 O'zbek 🇺🇿 Ўзбек 🇷🇺 Русский 🇬🇧 English ✓