← Blog

Alexis Β· 2026-10-12

Cloudflare DNS: the ubiquitous layer of global internet infrastructure

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.

Prepare an IaC-ready shell

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

Export Cloudflare DNS configuration and initialize Terraform

Check out the repository and initialize the Terraform workspace:

    terraform init

Then export your Cloudflare DNS configuration:

    sh ./exportZones.sh

Two things will happen:

The 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

Verify that Terraform works correctly after exporting

  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.

Stack

Cloudflare Β· Terraform Β· Git