DNS as Code
Background
I was frustrated with having to visit my DNS provider every single time I needed to add a subdomain and configure it. I realized it’s a pain: you log into the Cloudflare dashboard and click through dozens of settings. Why do all that by hand when I could just write some code instead of navigating menus?
And once again, laziness became the mother of invention. Right after diving into Ansible, I discovered Terraform. Terraform is ==an infrastructure-as-code (IaC) tool created by HashiCorp== (later acquired by IBM). It lets you use code to define the desired state of your cloud infrastructure, and it then makes sure the actual state matches your definition.
Cloudflare even provides official Terraform documentation. Each cloud provider ships its own Terraform provider and docs. The language itself is simple — just follow the examples and you’ll be up and running.
Installing Terraform
I’m on a Mac, so:
brew tap hashicorp/tap
brew install hashicorp/tap/terraform
For other operating systems, check the Cloudflare Terraform tutorial.
Setting Up Terraform
Inside my IaC repo, I added a new folder called dns, and inside it I created a main.tf file (.tf for Terraform). I wrote the following in that file:
terraform {
required_version = ">= 1.6"
required_providers {
cloudflare = {
source = "cloudflare/cloudflare"
version = "~> 5.0"
}
}
}
provider "cloudflare" {}
Then run terraform init.
What this command does is read main.tf, see that I required the ==cloudflare/cloudflare ~> 5.0== provider, and download that provider from the Terraform registry into my local ==.terraform/== folder.
It also writes/checks ==.terraform.lock.hcl==. This file pins the exact provider version and its checksums. On init, Terraform verifies that the downloaded provider matches the lock: if it matches, nothing changes; if the lock allows a newer 5.x, it may update the lock.
Since I need to manage my main domain, my subdomains, and my VPS, I added this to main.tf:
variable "zone_id" {
description = "Cloudflare Zone ID for DOMAIN_NAME (Dashboard -> domain -> Overview)."
type = string
}
variable "server_ip" {
description = "VPS public IP that the A records point to."
type = string
default = "YOUR_IP_ADDRESS"
}
variable "subdomains" {
description = "Subdomains that get an A record pointing at the VPS. Add one to create it."
type = set(string)
default = ["SUB_DOMAIN_NAME"]
}
As you can see, we can give each variable a default value — but we can also put those values in a separate file called ==terraform.tfvars==, like this:
zone_id = "ZONE_ID"
Then I add my DNS configuration by appending this to main.tf:
resource "cloudflare_dns_record" "app" {
for_each = var.subdomains
zone_id = var.zone_id
name = "${each.value}.yassineafaila.net"
type = "A"
content = var.server_ip
ttl = 1 # 1 = automatic
proxied = false # DNS-only so Caddy's Let's Encrypt HTTP challenge works
comment = "Managed by Terraform"
}
One A record per subdomain, all pointing at the VPS.
None of this works without a Cloudflare API key, so you’ll need to generate a new one and export it into your variables.
You can add as many subdomains as you want to var.subdomains, then run terraform apply in your terminal and watch the magic happen.
Summary
I’ll always be a fan of tools that make things easy. Cloudflare is a great platform with a nice service, but doing this configuration by hand every single time is — technically speaking — very, very slow for me. With Terraform I define my DNS once in code, add a subdomain to a list, run terraform apply, and it’s done.
That’s the end of this DNS as Code experiment with Terraform.
Links
These are the resources I checked for the Terraform configuration, in order: