DNS as code: providers and Terraform

Managing a hosted zone from code, writing records in Terraform, importing an existing zone safely, and reviewing DNS changes like any other change.

Why records belong in version control

A zone edited by hand has no history, no review and no way to answer "what changed last Tuesday". When the same records are declared in code, a change becomes a pull request with a diff, and a rollback becomes a revert.

ConcernManual zoneRecords as code
HistoryProvider audit log, if anyGit history with authors and reasons
ReviewWhoever has console accessPull request, at least one reviewer
ReproducibilityNoneThe zone can be rebuilt from the repository
Drift detectionNoticed when something breaksNext plan run shows the difference
Bulk changeRisky and slowA loop or a variable
Secrets in recordsUnavoidable in some casesMarked sensitive and kept out of the diff
# the zone itself. Do not let a state-managed zone delete records it
# does not know about, or an import mistake removes live DNS.
resource "aws_route53_zone" "main" {
  name = "example.com"

  lifecycle {
    prevent_destroy = true
  }
}

locals {
  records = {
    "www" = { type = "A", values = ["203.0.113.10"] }
    "api" = { type = "A", values = ["203.0.113.30"] }
    "cdn" = { type = "CNAME", values = ["example.cdn-provider.net"] }
  }
}

resource "aws_route53_record" "app" {
  for_each = local.records

  zone_id = aws_route53_zone.main.zone_id
  name    = each.key
  type    = each.value.type
  ttl     = 300
  records = each.value.values
}

Importing an existing zone

  1. Export the live zone as a BIND file from the provider. That export is the source of truth, not the console view.
  2. Generate configuration from the export with a script. Hand-writing 200 records guarantees a mistake.
  3. Import the zone resource first, then each record, verifying that plan reports no changes after each batch.
  4. A plan that wants to destroy records you did not import is the classic failure. Import them before the first apply.
  5. Keep the provider's zone-level setting that governs record deletion turned off during the migration window.
# generate records from an exported zone file, then review the output
terraform import aws_route53_zone.main Z0123456789ABCDEF

# import one record at a time, checking the plan after every batch
terraform import 'aws_route53_record.app["www"]' Z0123456789ABCDEF_www_A

terraform plan    # must show no changes once the batch is complete
  • Wrap every zone in prevent_destroy. A mistyped resource name should never be able to delete a production zone.
  • Records with long TTLs and records referenced by external systems - MX, verification TXT, domain verification for SaaS - deserve an explicit comment in the code so nobody removes them.
  • Keep the state file remote and locked. A lost state file turns the next apply into a full re-creation.

A safe change workflow

Every DNS change

  1. branch, edit, run plan in CI, paste the plan into the pull request
  2. reviewer checks the diff against the reason for the change
  3. merge, apply, and note the time
  4. verify from two public resolvers and one authoritative server
  5. if it is wrong, revert the commit and apply - never edit in the console

Rate limits worth knowing
  most providers throttle API writes; a loop over 500 records fails
  apply in batches with a short pause, and retry with backoff
  a partial apply leaves the zone in a mixed state - re-run until clean
⚠️
Never make a manual change in the provider console on a zone that is managed by code. The next apply will revert it, usually at the worst possible moment, and nobody will understand why the fix they made yesterday disappeared. If you must make an emergency edit, make it in the repository immediately afterwards.

FAQ

Terraform or a purpose-built DNS tool?
Terraform if the zone lives alongside the rest of your infrastructure. Tools like octodns are better when the zone is large, spans several providers, or must be generated from a data source.
Should DNS live in the same state as the application?
Usually a separate state and a separate apply. DNS is shared infrastructure with a different blast radius, and coupling it to an application deploy means an app rollback can move a nameserver.

Migrating DNS providers without downtime TTL, propagation and common mistakes

Last refreshed 2026-09-18.