If the traditional internet is a vast system of physical wires and data centers, Cloudflare is the smart operating system built on top of it. It handles security, traffic routing, performance, and computation at the global edge. It is nearly impossible to avoid today β most of the web servers I manage are exposed first through Cloudflare DNS, and immediately benefit from its security and performance capabilities.
Managing DNS at Cloudflare can be done manually, and until this post that was exactly how I managed all my different domains.
First, on NixOS, enable cf-terraforming and terraform. This can be slow as it appears to require compilation, at least on my NixOS box.
{ pkgs ? import <nixpkgs> { config.allowUnfree = true; } }:
pkgs.mkShell {
buildInputs = with pkgs; [
pkgs.terraform
pkgs.cf-terraforming
];
shellHook = ''
echo "Cloudflare IaC environment loaded!"
'';
}
A quicker option is to fetch the script directly
Check out the repository and initialize the Terraform workspace:
terraform init
Then export your Cloudflare DNS configuration:
sh ./exportZones.sh
Two things will happen:
zones.tf will be created at the root folder along with imports.tfdns_<domain>.tf file and an import_dns_<domain>.tf file will be generated per domainThe following is the content of the exportZones.sh script:
#!/bin/sh
if [ -f ".env.local" ]; then
CLOUDFLARE_API_TOKEN=$(grep -E '^CLOUDFLARE_API_TOKEN=' .env.local | cut -d '=' -f 2- | tr -d '"' | tr -d "'")
CLOUDFLARE_ACCOUNT_ID=$(grep -E '^CLOUDFLARE_ACCOUNT_ID=' .env.local | cut -d '=' -f 2- | tr -d '"' | tr -d "'")
echo "Loaded API token from .env.local"
fi
if [ -z "$CLOUDFLARE_API_TOKEN" ]; then
echo "ERROR: CLOUDFLARE_API_TOKEN is missing from .env.local and environment." >&2
exit 1
fi
if [ -z "$CLOUDFLARE_ACCOUNT_ID" ]; then
echo "ERROR: CLOUDFLARE_ACCOUNT_ID is missing from .env.local and environment." >&2
exit 1
fi
export CLOUDFLARE_API_TOKEN
export CLOUDFLARE_ACCOUNT_ID
echo "1. Generating zones file..."
cf-terraforming generate --resource-type cloudflare_zone > zones.tf
echo "1.1. Generating root-level zone imports..."
awk '
/resource "cloudflare_zone"/ {
if (match($0, /terraform_managed_resource_[a-f0-9]{32}_[0-9]+/)) {
resource_name = substr($0, RSTART, RLENGTH)
token = substr($0, RSTART + 27, RLENGTH - 27)
split(token, parts, "_")
zone_id = parts[1]
print "import {"
print " to = cloudflare_zone." resource_name
print " id = \"" zone_id "\""
print "}"
print ""
}
}
' "zones.tf" > "import_zones.tf"
echo "2. Processing zones and exporting DNS records to root..."
awk '
/resource "cloudflare_zone"/ {
if (match($0, /terraform_managed_resource_[a-f0-9]+/)) {
id = substr($0, RSTART + 27, RLENGTH - 27)
}
}
/^[ \t]*zone[ \t]*=/ {
if (match($0, /"[^"]+"/)) {
domain = substr($0, RSTART + 1, RLENGTH - 2)
}
if (id != "" && domain != "") {
print id "|" domain
id = ""
domain = ""
}
}
' "zones.tf" | while IFS='|' read -r zone_id domain_name; do
if [ -n "$zone_id" ] && [ -n "$domain_name" ]; then
DNS_FILE="dns_${domain_name}.tf"
IMPORTS_FILE="import_dns_${domain_name}.tf"
echo "Exporting DNS for: $domain_name -> $DNS_FILE"
CLOUDFLARE_ACCOUNT_ID="" cf-terraforming generate --resource-type cloudflare_record --zone "$zone_id" > "$DNS_FILE"
echo "Calculating imports for $domain_name (Zone ID: $zone_id)"
awk -v zid="$zone_id" '
/resource "cloudflare_record"/ {
if (match($0, /terraform_managed_resource_[a-f0-9]{32}_[0-9]+/)) {
resource_name = substr($0, RSTART, RLENGTH)
token = substr($0, RSTART + 27, RLENGTH - 27)
split(token, parts, "_")
rec_id = parts[1]
print "import {"
print " to = cloudflare_record." resource_name
print " id = \"" zid "/" rec_id "\""
print "}"
print ""
}
}
' "$DNS_FILE" > "$IMPORTS_FILE"
fi
done
echo "Done! Zones, import_zones.tf, and individual dns/import files are saved at the root."
Review your zone files and DNS export.
Your repository should look something like this β import files and state are not committed:
.
βββ backend.tf --> Optional: your backend configuration
βββ domains --> The domains you configure in your Cloudflare account
β βββ dns_alcanzatuproposito.com.tf
β βββ dns_comunidadsana.com.tf
β βββ dns_emely.es.tf
β βββ dns_jugandoalacomidita.com.tf
β βββ dns_miespazio.pe.tf
β βββ versions.tf
βββ exportZones.sh
βββ LICENSE
βββ modules.tf
βββ README.md
βββ shell.nix
βββ versions.tf
βββ zones.tf --> Zone files
terraform plan -out "plan.tfplan"
We exported the imports, zones, and DNS files from Cloudflare. Because the read and write formats differ slightly, Terraform will plan with imports and changes, but no additions or deletions.
...
Plan: 41 to import, 0 to add, 36 to change, 0 to destroy.
Saved the plan to: plan.tfplan
...
Now run apply to persist the state to your backend:
terraform apply "plan.tfplan"
After this, running terraform plan should show:
No changes. Your infrastructure matches the configuration.
Terraform has compared your real infrastructure against your configuration and found no differences, so no changes are needed.
This allows you to manage your DNS configuration, plan and apply changes via Terraform, or connect a GitHub Action to deploy your DNS configuration automatically. In the next post, I will cover how to manage Terraform state using Forgejo Packages and link a Terraform backend to it.
Cloudflare Β· Terraform Β· Git