Using IaC to Manage DNS Records

Organizations like to manage their DNS records using Infrastructure as Code (IaC) tools. This approach helps streamline approval processes, makes changes easy to track, and simplifies rollbacks in case of failure. I came across this idea[1] recently and wanted to try it out for the DNS records of my domain maheshrijal.com. This blog outlines the process.

NS records for my domain are in Cloudflare. They already have a terraform provider. It was quite easy to get started.

Initialize the provider

First step was initializing the Cloudflare provider for Terraform and creating a Cloudflare API token with edit access to the DNS zone.

// providers.tf

terraform {
  required_providers {
    cloudflare = {
      source  = "cloudflare/cloudflare"
      version = "4.33.0"
    }
  }
}

provider "cloudflare" {
  api_token = var.cloudflare_api_token
}

Next, I defined the variables.tf file and created terraform.tfvars file in which the actual API token will be assigned. This file will not be comitted to GitHub.

I then initialized the provider with terraform init.

What to do with the state file?

Terraform uses a state file to keep track of the current state of the infrastructure. I initially thought storing the state file locally would be a good idea and relatively simple. But, if I stored the state file locally, I would not be able to edit my DNS records if I did not have the device that stored the state. This was a no-go. I looked at other options.

Store the state file in Cloudflare R2? ❌

R2 is is a object storage service privded by Cloudflare. It is compatible with S3 which means it can use the terraform provider for S3 it also has a generous free tier.

I was already using the Cloudflare provider for terraform anyway, so one would think using R2 would be straightforward right?

Welp, initializing the provider for R2 is not straightforward because terraform does not support R2 officially. But, given that R2 is compatible with S3, I could use the S3 backend to configure R2.

However, another issue I ran into was that the backend block(backend.tf) does not support variables. This is unlikely to be ever fixed in the terraform core repository given the incentive hashicorp has with terraform cloud. But, there is still hope as a similar issue is open in the recently forked OpenTofu. There is a workaround to the issue using -backend-config but, I was worried about credential leak

Store the state file in Terraform Cloud ✅

I could use AWS S3, but my free tier on AWS just ended, and I didn’t want to pay for bucket storage. So, I settled on using terraform cloud. I defined the backend and logged in with terraform login.

// backend.tf

terraform {
  cloud {
    organization = "xyz"

    workspaces {
      name = "abc"
    }
  }
}

Importing existing DNS records

Since I already have existing DNS records for my domain in Cloudflare, it is not ideal to start creating new records. First, I will have to import these records into my terraform state. There is a CLI utility called cf-terraforming that does just this.

I installed the tool and exported the Cloudflare API token & Zone ID.

export CLOUDFLARE_API_TOKEN='abc'
export CLOUDFLARE_ZONE_ID='xyz

Then, I ran the tool to generate the main.tf file. This will create the terraform code for all the existing DNS records and write it to main.tf.

cf-terraforming generate --resource-type "cloudflare_record" > main.tf

Next, we can get to importing the current state into our state file. Without this, terraform would think these are new DNS records.

cf-terraforming import --resource-type "cloudflare_record" --modern-import-block > import.tf

Together, the above 2 files will generate the state file by importing existing resources rather than creating new ones.

terraform-dns-import

In our current main.tf file the zone_id is hard-coded. I want to store it in a variable so that it is not pushed to Github. I converted the zone_id assignment to a variable.

var.zone_id

Then, created a variable for zone_id. The zone_id variable also needs a value which will be passed from terraform.tfvars.

// variables.tf

variable "cloudflare_api_token" {
  description = "The API token for Cloudflare"
  type        = string
}

variable "zone_id" {
  description = "The zone ID for the Cloudflare domain"
  type        = string
}

Before completing the import, I wanted to have a clear naming scheme for all the records. To achieve this, I renamed the records in main.tf and changed the import destination accordingly in import.tf.

Now, I was ready to apply the changes. Just to be safe, I exported my DNS records from the Cloudflare dashboard incase something went wrong.

terraform-dns-apply-complete

Yay! 🎉 I’m able to see the state in terraform cloud, and no records were changed in Cloudflare. Which is a success. Now, to make it work with Github actions.

Integrate with Github actions

Since the code is on Github, I wanted a workflow to run terraform plan whenever a pull request is created and terraform apply to run when change merged to main branch. To do this, I used Github actions.

To authenticate with Terraform workspace, we need to create a secret for our terraform workspace called TF_API_TOKEN & and add it to github repository as actions secret.

Until now, I was running the terraform plan & apply locally so, the variables defined in terraform.tfvars were being passed to terraform cloud on each run. Now, the plan is to run Terrafrom straight from github so, I added the zone_id and cloudflare_api_token as sensitive variables in terraform workspace.

I also created a personal access token called GH_TOKEN and added it to the github repo. This token has the permission to comment on a pull request. This will be required to output changes of the terraform plan command.

After these changes are pushed, once a pull request is created, github actions should run the terraform plan workflow which runs terrafrom plan in terraform cloud using the API. The output of the plan is shown as a comment. When merged into main, the terraform apply workflow runs, which creates the DNS records.

With this approach, it is critical to make sure the branch protection rules are strong to prevent accidental pushes into main branch.

With this, the project is ready. Now, whenever I need to create/change a DNS record I create a pull request on the repo and I can easily track changes or rollback in case of failure. This also helps me keep track of provider documentation that I might have referred for DNS configuration.

Future Scope

Few things in this project are still not ideal. For starters, the Terraform organization and workspace name are public in the GitHub repo, which is probably a concern. Also, it would be great to have the ability to query DNS records after they are configured to verify if they are set correctly.

Overall, that’s where I feel like leaving this project now. I might get back to it in the future.

If you’re interested, the code is public on my github repository.


  1. I started working on this after reading through Alex’s blog. ↩︎