Chief Tools Logo
Guides

Manage DNS with Terraform

The Domain Chief provider for Terraform and OpenTofu manages the hosted DNS records, web redirects and mail forwards of your domains as code. DNS changes go through the same review as the rest of your infrastructure, and a plan shows every change before it is made.

The provider manages domains that are already in Domain Chief. It does not register, transfer, renew or delete domains. Do that in Domain Chief or through the API.

This guide covers setting up the provider, managing records and importing a zone you already have. The provider documentation on the Terraform Registry describes every resource, data source and attribute.

Before you start

Domain requirements

The provider manages DNS records, web redirects and mail forwards of domains that use Domain Chief hosted DNS. Mail forwards also need mail forwarding to be enabled for the domain. Mail forwarding is a paid feature, so the provider does not enable it. Enable it in Domain Chief first.

Create a token

Create a team access token for the team that owns the domains. Keep only the scopes of the resources you use:

Resource or data sourceScope
domainchief_dns_record, domainchief_dns_recordsdomainchief:dns:write
domainchief_web_redirectdomainchief:redirects_web:write
domainchief_mail_forwarddomainchief:redirects_mail:write
domainchief_domain, domainchief_domainsdomainchief:domains:read

A write scope also allows reading, so domainchief:dns:write covers listing records with domainchief_dns_records. See scopes for what each scope allows.

The token can change every DNS record of every domain in the team. Store it in the secret store of your CI system or Terraform runner, never in your configuration or state backend files.

Configure the provider

Add the provider to your configuration:

Code
terraform { required_providers { domainchief = { source = "chieftools/domainchief" } } } provider "domainchief" {}

Set the token in the DOMAINCHIEF_API_TOKEN environment variable and initialize the working directory:

TerminalCode
export DOMAINCHIEF_API_TOKEN=$TOKEN terraform init

OpenTofu uses the same configuration. Run tofu init instead.

Manage records

DNS records

Each domainchief_dns_record is one record. Leave out name for the domain itself:

Code
resource "domainchief_dns_record" "apex" { domain = "example.com" type = "A" content = "192.0.2.10" } resource "domainchief_dns_record" "www" { domain = "example.com" type = "CNAME" name = "www" content = "example.com" } resource "domainchief_dns_record" "mx" { domain = "example.com" type = "MX" content = "mail.example.net" priority = 10 } resource "domainchief_dns_record" "spf" { domain = "example.com" type = "TXT" content = "v=spf1 include:example.net -all" ttl = 300 }

Write records the way you are used to. @ for the domain itself, uppercase names, hostnames with a trailing dot and TXT content with or without surrounding quotes are all accepted and do not show up as changes in later plans.

The TTL must be one of the values hosted DNS supports, such as 300 or 3600. A plan fails for other values instead of rounding them.

Web redirects

A domainchief_web_redirect redirects the domain itself or one of its subdomains. Domain Chief creates the DNS records the redirect needs, so do not add domainchief_dns_record resources for the same names:

Code
resource "domainchief_web_redirect" "root" { domain = "example.com" target = "https://www.example.net" } resource "domainchief_web_redirect" "docs" { domain = "example.com" subdomain = "docs" target = "https://docs.example.net" include_path = true }

A redirect of the domain itself also redirects www unless you set include_www = false.

Mail forwards

A domainchief_mail_forward forwards one address, or every address without its own mail forward when the alias is *:

Code
resource "domainchief_mail_forward" "info" { domain = "example.com" alias = "info" target = "team@example.net" }

Like web redirects, mail forwarding creates its own MX, SPF and DKIM records. Do not manage those records as domainchief_dns_record resources.

Import an existing zone

Records you created in Domain Chief or through the API are not in your Terraform state. Import them, so Terraform manages the existing records instead of creating duplicates next to them. Importing needs Terraform 1.5 or OpenTofu 1.6 or later.

Generate import blocks

Each import needs the ID of the record. The domainchief_dns_records data source lists every record of a domain, and a temporary discover.tf file turns that list into an import block for each record:

Code
data "domainchief_dns_records" "existing" { domain = "example.com" } locals { # Records managed by web redirects, mail forwarding or ACME challenges are skipped. importable = [for record in data.domainchief_dns_records.existing.records : record if record.managed_by == null] # Group records by type and name to give each resource a readable, unique name. grouped = { for record in local.importable : "${lower(record.type)}_${record.name == "" ? "apex" : replace(replace(record.name, "*", "wildcard"), "/[^0-9A-Za-z_]/", "_")}" => record... } } output "import_blocks" { value = join("\n", flatten([ for key, records in local.grouped : [ for index, record in records : <<-EOT import { to = domainchief_dns_record.${index == 0 ? key : "${key}_${index + 1}"} id = "example.com/${record.id}" } EOT ] ])) }

Read the records and write the import blocks to a file:

TerminalCode
terraform apply terraform output -raw import_blocks > imports.tf rm discover.tf

The apply only reads the records and changes nothing. imports.tf now contains an import block for every record you manage yourself:

Code
import { to = domainchief_dns_record.cname_www id = "example.com/01hx4m2z8q3v7k5n9t6r1w0ybc" }

The data source marks records that Domain Chief manages with managed_by: web_redirect, mail_forwarding or acme_challenge. Those are skipped because Domain Chief maintains them for you. Manage the web redirects and mail forwards themselves with their own resources instead. Import those with the IDs from list web redirects and list mail forwards, in the form example.com/$ID.

Generate the configuration

Let Terraform write the resource configuration for the imported records:

TerminalCode
terraform plan -generate-config-out=dns.tf

Review dns.tf. The generated resources also list attributes with their default values, such as comment = null, metadata = null and priority = 0, which you can remove. Then import the records:

TerminalCode
terraform apply rm imports.tf terraform plan

The apply imports the records without changing them. The final plan reports no changes, because Terraform now manages the zone.

Keep Terraform and Domain Chief in sync

Once Terraform manages a zone, make changes in your configuration:

  • A record changed in Domain Chief or through the API shows up as a change in the next plan, and the next apply reverts it.
  • A record deleted outside Terraform, for example by clearing or resetting the zone, is created again by the next apply.
  • Records with a managed_by value belong to their feature. Change them through the web redirect, mail forwarding or ACME client instead.
  • Metadata keys starting with domainchief: are reserved for Domain Chief. You cannot set them and Terraform does not track them.

The provider applies changes to one domain one at a time. When another change to the same domain is running, for example from a second Terraform workspace, the provider waits and retries.