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

    Multi-Provider DNS with Terraform & DNSControl

    Deploy DNS zones across DNScale and Hetzner DNS for redundancy using Terraform or DNSControl.

    Running your DNS through a single provider means a single point of failure. If that provider goes down β€” whether from an outage, a DDoS attack, or a misconfiguration β€” your domains go dark. Multi-provider DNS eliminates that risk by serving the same zone from two independent providers simultaneously.

    This guide walks through setting up DNScale and Hetzner DNS as dual providers using either Terraform or DNSControl. Both tools let you define records once and push them to both providers, keeping everything in sync without manual duplication.

    Why Multi-Provider DNS

    DNS is the foundation of every internet service you run. A provider outage doesn't just mean your website is unreachable β€” it means email stops flowing, APIs become inaccessible, and services that depend on SRV or TXT records break.

    Multi-provider DNS works by delegating your domain to nameservers from both providers. Resolvers worldwide will query whichever set of nameservers responds, so if one provider is down, the other keeps serving answers.

    When does multi-provider make sense?

    • Production domains where downtime has real cost
    • Compliance environments that require redundancy
    • Global services where provider coverage varies by region

    When is it overkill? For development domains, internal tooling, or domains where a few minutes of downtime is acceptable, a single provider with good uptime is usually enough.

    How It Works

    The concept is straightforward:

    1. Define your records once in Terraform or DNSControl
    2. Push identical records to both DNScale and Hetzner DNS
    3. Set NS records at your registrar pointing to nameservers from both providers
    4. Resolvers query either provider β€” whichever responds first wins
    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    β”‚   Registrar  β”‚
    β”‚              β”‚
    β”‚ NS records:  β”‚
    β”‚  ns1.dnscale β”‚
    β”‚  ns2.dnscale β”‚
    β”‚  hydrogen.ns β”‚  ← Hetzner
    β”‚  oxygen.ns   β”‚  ← Hetzner
    β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜
           β”‚
           β–Ό
    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    β”‚   DNScale    β”‚     β”‚  Hetzner DNS β”‚
    β”‚              β”‚     β”‚              β”‚
    β”‚ example.com  β”‚     β”‚ example.com  β”‚
    β”‚  A, MX, TXT  β”‚     β”‚  A, MX, TXT  β”‚
    β”‚ (identical)  β”‚     β”‚ (identical)  β”‚
    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
           β–²                    β–²
           β”‚    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”‚
           └────│ Resolver β”‚β”€β”€β”€β”€β”˜
                β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
             Queries either one

    Both providers hold the same records. Your IaC tool is the single source of truth, and both providers are just mirrors of that truth.

    Prerequisites

    You'll need:

    1. A DNScale account with an API key β€” Get your API key
    2. A Hetzner DNS account with an API token β€” Hetzner DNS Console
    3. Terraform (v1.0+) or DNSControl (v4.0+) installed
    4. A domain you control with access to set NS records at the registrar

    Terraform: Multi-Provider Setup

    Terraform makes multi-provider DNS clean with its native support for multiple providers and for_each loops. You define your records once in a locals block and create them in both providers.

    Provider Configuration

    # providers.tf
    terraform {
      required_providers {
        dnscale = {
          source  = "dnscaleou/dnscale"
          version = "~> 1.0"
        }
        hetznerdns = {
          source  = "timohirt/hetznerdns"
          version = "~> 2.2"
        }
      }
    }
     
    provider "dnscale" {
      api_key = var.dnscale_api_key
    }
     
    provider "hetznerdns" {
      apitoken = var.hetzner_dns_token
    }
    # variables.tf
    variable "dnscale_api_key" {
      description = "DNScale API key"
      type        = string
      sensitive   = true
    }
     
    variable "hetzner_dns_token" {
      description = "Hetzner DNS API token"
      type        = string
      sensitive   = true
    }
     
    variable "domain" {
      description = "Domain to manage"
      type        = string
      default     = "example.com"
    }

    Define Records Once

    The key to multi-provider DNS with Terraform is defining your records in a single place. Use locals to create a shared record set:

    # records.tf
    locals {
      dns_records = {
        # A records
        "root-a" = {
          name  = "@"
          type  = "A"
          value = "203.0.113.10"
          ttl   = 3600
        }
        "www-a" = {
          name  = "www"
          type  = "CNAME"
          value = "example.com."
          ttl   = 3600
        }
        # Mail
        "mx-primary" = {
          name     = "@"
          type     = "MX"
          value    = "mail.example.com."
          ttl      = 3600
          priority = 10
        }
        "mail-a" = {
          name  = "mail"
          type  = "A"
          value = "203.0.113.20"
          ttl   = 3600
        }
        # SPF
        "spf" = {
          name  = "@"
          type  = "TXT"
          value = "v=spf1 mx -all"
          ttl   = 3600
        }
        # IPv6
        "root-aaaa" = {
          name  = "@"
          type  = "AAAA"
          value = "2001:db8::1"
          ttl   = 3600
        }
      }
    }

    Create in Both Providers

    # dnscale.tf
    resource "dnscale_zone" "primary" {
      name = var.domain
    }
     
    resource "dnscale_record" "all" {
      for_each = local.dns_records
     
      zone_id  = dnscale_zone.primary.id
      name     = each.value.name
      type     = each.value.type
      content  = each.value.value
      ttl      = each.value.ttl
      priority = lookup(each.value, "priority", null)
    }
    # hetzner.tf
    resource "hetznerdns_zone" "secondary" {
      name = var.domain
      ttl  = 3600
    }
     
    resource "hetznerdns_record" "all" {
      for_each = local.dns_records
     
      zone_id = hetznerdns_zone.secondary.id
      name    = each.value.name
      type    = each.value.type
      value   = each.value.value
      ttl     = each.value.ttl
    }

    For MX records on Hetzner DNS, the priority is typically included in the value field (e.g., "10 mail.example.com."). You may need to adjust formatting depending on the provider version. Check the Hetzner DNS Terraform provider docs for specifics.

    Apply

    export TF_VAR_dnscale_api_key="your-dnscale-key"
    export TF_VAR_hetzner_dns_token="your-hetzner-token"
     
    terraform init
    terraform plan    # Review changes for both providers
    terraform apply   # Push to both simultaneously

    Terraform creates resources in both providers in parallel, so a single apply updates everything. For a deeper dive on the DNScale provider specifically, see the Terraform provider guide.

    DNSControl: Multi-Provider Setup

    DNSControl has native multi-provider support built in. You can attach multiple DNS providers to a single domain, and DNSControl pushes identical records to all of them in one command.

    Credentials

    // creds.json
    {
      "dnscale": {
        "TYPE": "DNSCALE",
        "api_key": "your-dnscale-key"
      },
      "hetzner": {
        "TYPE": "HETZNER",
        "api_token": "your-hetzner-token"
      }
    }

    Keep creds.json out of version control:

    echo "creds.json" >> .gitignore

    Configuration

    DNSControl's DnsProvider() function accepts multiple providers for a single domain. Records are automatically pushed to all attached providers:

    // dnsconfig.js
    var REG_NONE = NewRegistrar("none");
    var DSP_DNSCALE = NewDnsProvider("dnscale");
    var DSP_HETZNER = NewDnsProvider("hetzner");
     
    D("example.com", REG_NONE,
      DnsProvider(DSP_DNSCALE),
      DnsProvider(DSP_HETZNER),
     
      // A records
      A("@", "203.0.113.10", TTL(3600)),
      A("mail", "203.0.113.20", TTL(3600)),
     
      // IPv6
      AAAA("@", "2001:db8::1", TTL(3600)),
     
      // CNAME
      CNAME("www", "example.com.", TTL(3600)),
     
      // Mail
      MX("@", 10, "mail.example.com.", TTL(3600)),
     
      // SPF
      TXT("@", "v=spf1 mx -all", TTL(3600)),
     
    END);

    That's it. Both providers are listed on the same domain, and DNSControl handles the rest.

    Preview and Push

    # See what would change on both providers
    dnscontrol preview
     
    # Apply changes to both providers
    dnscontrol push

    The preview output shows changes per provider, so you can verify both DNScale and Hetzner will receive the correct records before pushing.

    For more details on DNSControl with DNScale, see the DNSControl guide.

    Keeping Records in Sync

    The biggest risk with multi-provider DNS is drift β€” records getting out of sync between providers. Your IaC tool solves this by being the single source of truth.

    IaC as the Source of Truth

    Never edit records directly in the DNScale or Hetzner dashboards. All changes go through your Terraform or DNSControl configuration. This ensures both providers always have identical records.

    CI/CD Pipeline

    Automate deployments so records are pushed to both providers on every merge to main:

    # .github/workflows/dns.yml
    name: DNS Deploy
    on:
      push:
        branches: [main]
        paths: ["dns/**"]
     
    jobs:
      deploy:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v4
     
          - name: Setup Terraform
            uses: hashicorp/setup-terraform@v3
     
          - name: Apply DNS changes
            working-directory: dns/
            env:
              TF_VAR_dnscale_api_key: ${{ secrets.DNSCALE_API_KEY }}
              TF_VAR_hetzner_dns_token: ${{ secrets.HETZNER_DNS_TOKEN }}
            run: |
              terraform init
              terraform apply -auto-approve

    Drift Detection

    Run periodic checks to catch any manual changes or API inconsistencies:

    # Terraform: detect drift
    terraform plan -detailed-exitcode
    # Exit code 2 means drift detected
     
    # DNSControl: preview will show any differences
    dnscontrol preview

    Schedule this in CI (e.g., a daily cron job) and alert on unexpected differences.

    NS Delegation at Your Registrar

    After deploying records to both providers, update the NS records at your domain registrar to include nameservers from both DNScale and Hetzner.

    A typical NS set looks like:

    ns1.dnscale.eu
    ns2.dnscale.eu
    hydrogen.ns.hetzner.com
    oxygen.ns.hetzner.com
    helium.ns.hetzner.de

    The exact DNScale nameserver names depend on your account. Check your zone details in the DNScale dashboard for the assigned nameservers.

    Set all of these as NS records at your registrar. Most registrars let you add as many nameservers as you need.

    Verify Delegation

    After updating NS records (allow up to 48 hours for propagation), verify with:

    dig NS example.com +short

    You should see nameservers from both providers in the response. You can also verify each provider is responding correctly:

    # Query DNScale directly
    dig @ns1.dnscale.eu example.com A +short
     
    # Query Hetzner directly
    dig @hydrogen.ns.hetzner.com example.com A +short

    Both should return the same IP address.

    Limitations & Considerations

    DNSSEC

    Multi-provider DNSSEC is complex. Each provider signs the zone with its own keys, so you either need:

    • Both providers to support multi-signer DNSSEC (RFC 8901) β€” not widely supported yet
    • One provider to be the signer and transfer signed zones to the other
    • Skip DNSSEC on multi-provider zones until multi-signer support improves

    For most setups, skipping DNSSEC on multi-provider zones is the pragmatic choice. If DNSSEC is a hard requirement, consider using a single provider with strong uptime guarantees instead. See the DNSSEC guide for single-provider DNSSEC setup.

    TTL Alignment

    Keep TTLs identical across both providers. Mismatched TTLs mean resolvers cache records for different durations depending on which provider they queried, leading to inconsistent behavior.

    Propagation Delays

    When you push changes, both providers process them independently. There's a brief window (usually seconds, sometimes minutes) where one provider has the new records and the other still serves the old ones. For most use cases this is fine β€” just avoid making changes that would break if partially applied.

    Provider-Specific Record Types

    Stick to standard record types that both providers support: A, AAAA, CNAME, MX, TXT, SRV, CAA, NS. Provider-specific extensions or proprietary record types won't be portable.

    Next Steps