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:
- Define your records once in Terraform or DNSControl
- Push identical records to both DNScale and Hetzner DNS
- Set NS records at your registrar pointing to nameservers from both providers
- 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 oneBoth 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:
- A DNScale account with an API key β Get your API key
- A Hetzner DNS account with an API token β Hetzner DNS Console
- Terraform (v1.0+) or DNSControl (v4.0+) installed
- 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
valuefield (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 simultaneouslyTerraform 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" >> .gitignoreConfiguration
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 pushThe 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-approveDrift 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 previewSchedule 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.deThe 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 +shortYou 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 +shortBoth 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
- Managing DNS with Terraform β deeper dive into the DNScale Terraform provider
- Managing DNS with DNSControl β full DNSControl setup and configuration
- DNSSEC Configuration β set up DNSSEC for single-provider zones
- Global DNS Resolution Balancing β geographic traffic distribution across DNS nodes