DNS Drift Detection for Terraform, DNSControl, and Manual Edits
Detect DNS drift across Terraform, DNSControl, manual dashboard edits, and API scripts with safe review, import, rollback, and CI patterns.
Answer snapshot
DNS drift is any difference between the DNS records your source of truth declares and the records your authoritative provider currently publishes. Treat drift as an operational signal, not an automatic apply target: identify the writer, decide whether to accept or revert it, then reconcile Terraform, DNSControl, API scripts, or manual edits through review.
What you'll learn
- Define DNS drift and the common causes across IaC, APIs, and manual changes
- Build safe drift checks for Terraform, DNSControl, and read-only API inventory jobs
- Decide when to accept, revert, import, or split record ownership
- Add alerting, audit review, rollback, and CI guardrails around DNS drift
DNS drift is what happens when production DNS and the declared source of truth stop matching.
That sounds like an infrastructure-as-code housekeeping problem. It is not. DNS drift can move traffic, break email, block certificate issuance, invalidate DNSSEC, or hide an emergency change that kept production online.
Use this guide with the DNS Automation hub, DNS as code best practices, the DNScale Terraform provider guide, and the DNScale DNSControl guide.
Quick Drift Review Checklist
Copy this into a pull request, incident ticket, or scheduled drift alert:
## DNS drift review
- [ ] Zone:
- [ ] Record set: name + type + value + TTL:
- [ ] Source of truth: Terraform / DNSControl / API script / dashboard / other:
- [ ] Expected owner:
- [ ] Detected by: terraform plan / dnscontrol preview / API inventory / audit log:
- [ ] Last known good commit or export:
- [ ] Was this an emergency change?
- [ ] Does it affect apex, CNAME/ALIAS, MX, SPF, DKIM, DMARC, CAA, NS, DS, or DNSKEY?
- [ ] Decision: accept / revert / import / split ownership:
- [ ] Rollback command or commit:
- [ ] Authoritative DNS verified:
- [ ] Recursive resolver behavior checked after TTL:
- [ ] Source of truth updated:
- [ ] Audit log attached:Do not skip the decision line. A drift alert is not a command to overwrite production.
What DNS Drift Means
DNS drift is a difference between:
declared state -> what your repo, state file, runbook, or inventory says should exist
live state -> what the authoritative DNS provider currently publishesExamples:
| Declared state | Live state | Risk |
|---|---|---|
www.example.com A 192.0.2.10 ttl=300 | www.example.com A 192.0.2.55 ttl=300 | Web traffic moved outside review |
_dmarc.example.com TXT "v=DMARC1; p=none" | _dmarc.example.com TXT "v=DMARC1; p=reject" | Mail policy changed without mail-owner approval |
example.com CAA 0 issue "letsencrypt.org" | CAA record missing | Certificate issuance policy weakened |
| Terraform state tracks a record | Record was deleted in dashboard | Next apply may recreate it unexpectedly |
| DNSControl expects one TXT value | Live provider has duplicate TXT values | Verification or SPF behavior may be wrong |
Drift is not automatically bad. During an incident, a manual dashboard edit may be the fastest safe fix. The problem is letting that change remain outside the source of truth after the incident.
Common Causes
Most drift comes from one of these patterns.
Emergency dashboard edits
Someone fixes production in the provider dashboard during an outage, then forgets to backfill Terraform, DNSControl, or the DNS repository.
This is acceptable as break-glass behavior. It becomes unsafe when the next scheduled apply silently reverts the fix.
Multiple writers
Do not let several systems manage the same record set.
Bad:
www.example.com A -> Terraform
www.example.com A -> DNSControl
www.example.com A -> dashboard
www.example.com A -> deployment scriptGood:
www.example.com A -> Terraform
_dmarc.example.com TXT -> DNSControl
_acme-challenge.example.com TXT -> certificate automationThe boundary must be by zone, delegated subdomain, or record set. A "record set" means the same owner name and type, such as all TXT values at _acme-challenge.example.com.
Migration imports
Imports often create temporary mismatch:
- records exist at the provider but not in code
- code exists but resources have not been imported into Terraform state
- DNSControl normalizes names or TTLs differently from the old provider
- multi-value TXT records are split or joined differently
During migration, treat the live provider export as evidence. Do not let the new tool delete unknown records until every owner has signed off.
Stale Terraform state
Terraform refreshes remote objects during normal plans, but state can still mislead people when resources were changed outside Terraform, imported incorrectly, moved between modules, or removed from configuration without being removed from state.
If state and provider disagree, review both. Do not repair state until you know whether live DNS or code is the intended truth.
Deleted remote records
A dashboard user may delete a record that still exists in Terraform or DNSControl. The next apply may recreate it.
That is good if the deletion was accidental. It is bad if the deletion was a valid emergency mitigation and the source of truth was never updated.
Duplicated TXT records
TXT records are common drift sources because many systems write them:
- SPF includes
- DKIM selectors
- DMARC policy
- SaaS ownership verification
- ACME DNS-01 challenges
- MTA-STS and TLS-RPT
Some TXT names are multi-value by design. Others must have one effective value. Review duplicate TXT drift with the application owner before deleting anything.
The Safe Source-of-Truth Model
Pick one routine writer for each production record set.
Use this ownership table in the DNS repository:
example.com
apex A/AAAA/ALIAS: Terraform, platform team
www CNAME: Terraform, platform team
MX: DNSControl, email team
SPF/DKIM/DMARC TXT: DNSControl, email team
CAA: DNSControl, security team
_acme-challenge TXT: certificate automation, scoped to _acme-challenge only
emergency edits: dashboard, must be backfilled within 24 hoursThe key rule:
If two systems can write the same name and type, drift detection will eventually become incident response.
For DNScale, use scoped credentials so each path can only touch the zone or records it owns. Read the product context in zone-scoped API keys and the API permission details in API authentication.
Terraform Drift Workflow
Terraform is strongest when DNS is part of broader infrastructure state. It is also strict about ownership: if a dnscale_record resource exists in configuration, Terraform expects that record to match configuration.
Use this workflow for routine drift checks:
cd terraform/dns
terraform init -input=false
terraform validate
terraform plan -detailed-exitcode -input=falseInterpret the result:
exit 0 -> no changes
exit 1 -> command or provider error
exit 2 -> changes are present; review as drift unless a PR explains themFor a focused refresh review:
terraform plan -refresh-only -input=falseThis shows how Terraform state would change after reading the provider. It does not prove the live change is desired. It only proves the remote object no longer matches the state Terraform last knew.
Terraform example: imported record drift
Suppose www.example.com was created manually during a migration and should now be owned by Terraform.
First declare the intended resource:
resource "dnscale_record" "www" {
zone_id = dnscale_zone.example.id
name = "www.example.com."
type = "A"
content = "192.0.2.10"
ttl = 300
}Then import the existing record instead of recreating it:
terraform import dnscale_record.www <zone-id>/<record-id>
terraform plan -input=falseOnly apply after the plan shows the expected result. If Terraform wants to delete or replace records you did not intend to change, stop and fix the HCL or import mapping first.
Terraform protected apply
Use this minimum policy:
- pull requests run
terraform fmt,terraform validate, andterraform plan - scheduled jobs run
terraform plan -detailed-exitcode - production applies run only from a protected branch or environment
- write credentials are zone-scoped
- drift alerts open a review ticket instead of applying automatically
For CI examples, see GitHub Actions DNS Workflows and GitLab CI DNS Workflows.
DNSControl Drift Workflow
DNSControl is strongest when DNS itself is the workflow: readable zone declarations, macros, many zones, and multi-provider sync.
Use this routine check:
cd dns
dnscontrol check
dnscontrol previewdnscontrol preview compares dnsconfig.js with live provider records and prints the changes DNSControl would make.
DNSControl example: review a live manual edit
Declared state:
var REG_NONE = NewRegistrar("none");
var DSP_DNSCALE = NewDnsProvider("dnscale");
D("example.com", REG_NONE, DnsProvider(DSP_DNSCALE),
A("@", "192.0.2.10", TTL(300)),
CNAME("www", "example.com."),
TXT("_dmarc", "v=DMARC1; p=none; rua=mailto:dmarc@example.com")
);Run:
dnscontrol previewIf the provider has _dmarc set to p=reject, the preview should show a correction back to p=none. Do not run dnscontrol push until the mail owner confirms which value is intended.
If p=reject was a deliberate rollout, accept the drift by editing dnsconfig.js:
TXT("_dmarc", "v=DMARC1; p=reject; rua=mailto:dmarc@example.com")Then run:
dnscontrol check
dnscontrol previewWhen the preview is clean or shows only the intended changes, merge the update and apply from the protected path.
DNSControl multi-provider drift
For multi-provider DNS, drift can mean:
- provider A differs from
dnsconfig.js - provider B differs from
dnsconfig.js - provider A and provider B differ from each other
- registrar delegation does not match the intended nameserver set
DNSControl helps because the same declaration can preview against every configured provider. Still, provider-specific features such as ALIAS flattening or DNSSEC behavior may need explicit review. See multi-provider DNS deployment before using drift automation as a failover mechanism.
API Inventory Workflow
Not every drift check needs write access or a full IaC engine. A read-only inventory job is useful when:
- the source of truth is a database, CMDB, or internal service
- application teams create short-lived records
- you want an independent audit of Terraform or DNSControl output
- you need to compare provider state with an exported baseline
For DNScale, start with API Overview, Records API, and Activity API. Record inventory requires zones:read plus records:read.
Generic API inventory pattern
This example exports all records from one zone into a stable TSV file.
#!/usr/bin/env bash
set -euo pipefail
: "${DNSCALE_API_KEY:?set DNSCALE_API_KEY}"
: "${ZONE_ID:?set ZONE_ID}"
base_url="https://api.dnscale.eu/v1"
limit=1000
offset=0
while :; do
response="$(
curl -fsS "${base_url}/zones/${ZONE_ID}/records?limit=${limit}&offset=${offset}" \
-H "Authorization: Bearer ${DNSCALE_API_KEY}"
)"
printf "%s" "$response" | jq -r '
.data.records[]
| [
.name,
.type,
.content,
(.ttl | tostring),
((.disabled // false) | tostring)
]
| @tsv
'
has_more="$(printf "%s" "$response" | jq -r '.data.pagination.has_more')"
if [ "$has_more" != "true" ]; then
break
fi
offset=$((offset + limit))
done | sort -uCompare it with a reviewed baseline:
./export-live-records.sh > live-records.tsv
diff -u expected-records.tsv live-records.tsvThis pattern is intentionally read-only. If the diff is non-empty, open a review. Do not bolt an automatic DELETE or PUT loop onto the inventory job.
Handling Drift: Accept, Revert, Import, or Split Ownership
Every drift review should end in one of four decisions.
| Decision | Use when | Example |
|---|---|---|
| Accept | Live DNS is correct and code is stale | Emergency A record fix worked; update Terraform to match |
| Revert | Live DNS is wrong or unauthorized | Dashboard changed MX target by mistake; restore declared value |
| Import | Live DNS should become code-managed | Migration created records manually; import into Terraform state |
| Split ownership | One tool should not own the whole surface | Terraform owns app records; DNSControl owns email records |
Avoid the fifth non-decision: "ignore." Ignored drift becomes tribal knowledge, then an outage.
Accepting drift
Accept drift by changing the source of truth.
Terraform:
# Edit .tf files to match the intended live record.
terraform plan -input=false
git add terraform/dns
git commit -m "fix: backfill emergency DNS record"DNSControl:
# Edit dnsconfig.js to match the intended live record.
dnscontrol check
dnscontrol preview
git add dns/dnsconfig.js
git commit -m "fix: backfill emergency DNS record"Reverting drift
Revert drift by applying the reviewed source of truth, not by guessing in the dashboard.
Terraform:
terraform plan -out=tfplan -input=false
terraform apply tfplanDNSControl:
dnscontrol preview
dnscontrol pushBefore reverting, confirm the live value was not an active incident mitigation.
Importing drift
Import when the live resource should be adopted by Terraform:
terraform import dnscale_record.api <zone-id>/<record-id>
terraform plan -input=falseFor DNSControl, there is no state import step. You update dnsconfig.js until dnscontrol preview shows no unwanted correction.
Splitting ownership
Split ownership when one tool is too broad.
Example:
Terraform workspace:
api.example.com A
app.example.com CNAME
internal.example.com A
DNSControl:
MX
SPF/DKIM/DMARC
CAA
static verification TXT records
Certificate automation:
_acme-challenge.example.com TXT onlyDocument the boundary in the repo and in CI path filters. The goal is to prevent two jobs from trying to correct each other.
Alerting and Audit Log Requirements
A drift check should produce enough context for a human to decide.
Alert on:
- any change to apex A, AAAA, ALIAS, or CNAME records
- MX, SPF, DKIM, DMARC, CAA, NS, DS, DNSKEY, and wildcard changes
- unexpected record deletion
- new records created outside the expected tool
- TTL changes before or during a migration window
- drift that persists for more than one scheduled check
Include in the alert:
- zone
- record name and type
- old declared value
- live value
- detector name and run URL
- last successful apply
- recent audit log entries
- owner and escalation target
Review audit logs before deciding. In DNScale, the Activity API can support compliance exports, configuration drift investigation, and internal tooling. Use it to answer "who changed this?" before automation overwrites "what changed?"
Rollback Examples
Rollback should use the same path that normally manages the record.
Terraform rollback
If a bad DNS PR changed the apex address:
git revert <bad-commit>
terraform plan -input=false
terraform apply -input=falseThen verify authoritative DNS:
dig +short A example.com @ns1.dnscale.eu
dig +short A example.com @ns2.dnscale.euCheck recursive resolvers after the relevant TTL:
dig +short A example.com @1.1.1.1
dig +short A example.com @8.8.8.8DNSControl rollback
git revert <bad-commit>
dnscontrol check
dnscontrol preview
dnscontrol pushIf DNSControl shows changes beyond the rollback, stop and review for independent drift.
Manual emergency rollback
If the normal pipeline is unavailable, use break-glass access:
- Export the current live records.
- Change only the affected record set.
- Capture who approved it and why.
- Verify authoritative DNS.
- Open a backfill PR before closing the incident.
Manual rollback is a valid emergency path. It should not become the permanent source of truth.
CI Template
This GitHub Actions template separates pull-request preview from scheduled drift detection. Keep the Terraform pair or the DNSControl pair for a given record set, not both.
name: dns-drift
on:
pull_request:
paths:
- "terraform/dns/**"
- "dns/**"
- ".github/workflows/dns-drift.yml"
schedule:
- cron: "23 6 * * *"
workflow_dispatch:
jobs:
# Use this pair when Terraform owns the DNS record set.
terraform-preview:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
defaults:
run:
working-directory: terraform/dns
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
- run: terraform init -input=false
- run: terraform validate
- name: Plan DNS changes
env:
DNSCALE_API_KEY: ${{ secrets.DNSCALE_READ_TOKEN }}
run: terraform plan -input=false
terraform-drift:
if: github.event_name != 'pull_request'
runs-on: ubuntu-latest
defaults:
run:
working-directory: terraform/dns
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
- run: terraform init -input=false
- name: Detect Terraform drift
env:
DNSCALE_API_KEY: ${{ secrets.DNSCALE_READ_TOKEN }}
run: terraform plan -detailed-exitcode -input=false
# Use this pair when DNSControl owns the DNS record set.
dnscontrol-preview:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: "1.25"
- run: go install github.com/StackExchange/dnscontrol/v4@latest
- name: Preview DNSControl changes
working-directory: dns
env:
DNSCALE_API_KEY: ${{ secrets.DNSCALE_READ_TOKEN }}
run: |
dnscontrol check
dnscontrol preview
dnscontrol-drift:
if: github.event_name != 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: "1.25"
- run: go install github.com/StackExchange/dnscontrol/v4@latest
- name: Detect DNSControl drift
working-directory: dns
env:
DNSCALE_API_KEY: ${{ secrets.DNSCALE_READ_TOKEN }}
run: |
dnscontrol check
dnscontrol previewAdapt the path filters to your repository. For production, send non-empty scheduled output to your alerting system or issue tracker. Do not give this workflow a production write token.
For complete CI examples, use GitHub Actions DNS Workflows or GitLab CI DNS Workflows.
Final Operating Rules
- One routine writer per record set.
- Pull requests preview; protected branches apply.
- Scheduled drift checks alert instead of auto-fixing.
- Emergency dashboard edits are allowed but must be backfilled.
- Terraform imports happen before Terraform applies.
- DNSControl previews must be reviewed before
push. - API inventory jobs stay read-only unless they are a separate reviewed apply workflow.
- Audit logs are part of the drift review, not optional evidence.
- Rollback uses the normal source of truth whenever possible.
- High-risk records get owner approval before accept or revert.
Good DNS automation does not mean nobody can make an emergency edit. It means emergency edits are visible, reviewed, and reconciled before the next routine change surprises production.
Related Guides
Frequently asked questions
- What is DNS drift?
- DNS drift is a difference between the declared DNS source of truth and the records currently published by the provider. It usually appears after manual emergency edits, stale Terraform state, imports, deleted remote records, or another automation path changing the same record set.
- Should a drift job automatically fix DNS?
- No. A drift job should alert and preserve evidence. The live change may be a valid incident fix. Review the diff, identify the writer, then accept, revert, import, or split ownership through the normal change path.
- Can Terraform and DNSControl manage the same zone?
- They can coexist only with clear boundaries. Do not let two tools manage the same name and type. For example, Terraform can own infrastructure A records while DNSControl owns email and policy records, but the boundary must be documented and enforced.
- How often should DNS drift detection run?
- Daily is enough for many teams. Run it more often for production zones with emergency dashboard access, multiple automation paths, or customer-facing records that change frequently.
- What API permissions are needed for inventory drift checks?
- Use read-only access where possible. For DNScale, record inventory uses zones:read plus records:read. Write keys should be reserved for reviewed apply paths.
- What is the safest response to drift after an incident?
- Freeze automatic applies, capture the live records, confirm the incident owner, and backfill the emergency fix into the source of truth before resuming routine automation.
Related guides
Automation
DNS Automation
A practical guide to automating DNS changes with APIs, CI/CD, DNS as code, scoped keys, previews, drift detection, and rollback workflows.
Automation
GitLab CI DNS Workflows
Practical GitLab CI patterns for DNSControl and Terraform DNS changes, including merge-request previews, protected deploys, scoped variables, and drift checks.
Automation
DNS as Code Best Practices
A practical guide to managing DNS as code with ownership, review, previews, drift detection, scoped credentials, and safe migration patterns.
Automation
DNS in CI/CD Pipelines
How to manage DNS changes through CI/CD with checks, previews, approvals, scoped secrets, drift detection, and safe rollback.
Ready to manage your DNS with confidence?
DNScale provides anycast DNS hosting with a global network, real-time analytics, and an easy-to-use API.
Start free