Introducing PostScale, email API for transactional, inbound, and masked addresses. PostScale

    Automation

    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 publishes

    Examples:

    Declared stateLive stateRisk
    www.example.com A 192.0.2.10 ttl=300www.example.com A 192.0.2.55 ttl=300Web 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 missingCertificate issuance policy weakened
    Terraform state tracks a recordRecord was deleted in dashboardNext apply may recreate it unexpectedly
    DNSControl expects one TXT valueLive provider has duplicate TXT valuesVerification 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 script

    Good:

    www.example.com A          -> Terraform
    _dmarc.example.com TXT     -> DNSControl
    _acme-challenge.example.com TXT -> certificate automation

    The 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 hours

    The 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=false

    Interpret the result:

    exit 0 -> no changes
    exit 1 -> command or provider error
    exit 2 -> changes are present; review as drift unless a PR explains them

    For a focused refresh review:

    terraform plan -refresh-only -input=false

    This 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=false

    Only 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, and terraform 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 preview

    dnscontrol 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 preview

    If 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 preview

    When 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 -u

    Compare it with a reviewed baseline:

    ./export-live-records.sh > live-records.tsv
    diff -u expected-records.tsv live-records.tsv

    This 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.

    DecisionUse whenExample
    AcceptLive DNS is correct and code is staleEmergency A record fix worked; update Terraform to match
    RevertLive DNS is wrong or unauthorizedDashboard changed MX target by mistake; restore declared value
    ImportLive DNS should become code-managedMigration created records manually; import into Terraform state
    Split ownershipOne tool should not own the whole surfaceTerraform 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 tfplan

    DNSControl:

    dnscontrol preview
    dnscontrol push

    Before 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=false

    For 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 only

    Document 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=false

    Then verify authoritative DNS:

    dig +short A example.com @ns1.dnscale.eu
    dig +short A example.com @ns2.dnscale.eu

    Check recursive resolvers after the relevant TTL:

    dig +short A example.com @1.1.1.1
    dig +short A example.com @8.8.8.8

    DNSControl rollback

    git revert <bad-commit>
    dnscontrol check
    dnscontrol preview
    dnscontrol push

    If 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:

    1. Export the current live records.
    2. Change only the affected record set.
    3. Capture who approved it and why.
    4. Verify authoritative DNS.
    5. 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 preview

    Adapt 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

    1. One routine writer per record set.
    2. Pull requests preview; protected branches apply.
    3. Scheduled drift checks alert instead of auto-fixing.
    4. Emergency dashboard edits are allowed but must be backfilled.
    5. Terraform imports happen before Terraform applies.
    6. DNSControl previews must be reviewed before push.
    7. API inventory jobs stay read-only unless they are a separate reviewed apply workflow.
    8. Audit logs are part of the drift review, not optional evidence.
    9. Rollback uses the normal source of truth whenever possible.
    10. 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.

    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

    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