Intro

Hey Everybody,

Happy New Year! This is my first post of 2026, and I’m excited to share how I currently manage DNS.

If you’re like me, then nothing is worse than needing to do things manually. Especially the tedious task of clicking my way through Cloudflare’s portal and updating DNS by hand.

Luckily, I found a better way to manage it with Terraform, Cloudflare, and GitHub Actions.

Why Cloudflare?

Why did I pick Cloudflare in the first place?

I really enjoy their free tier program, and I’m a big user of 1.1.1.1. It only made sense to start using Cloudflare to host DNS.

I do run a local DNS server, but that is mainly for local resolution and performance. Cloudflare is still the source of truth for my public DNS records. Another bonus is it provides me an API that I can use to programmatically create DNS records.

Terraform

I’m currently using Cloudflare’s Terraform provider to programmatically create DNS records. I chose Terraform for simplicity and because it’s easy to track my changes.

Terraform state is stored remotely in S3. This lets GitHub Actions run Terraform consistently without relying on local state from my laptop.

CI/CD

Finally, I use CI/CD to tie it all together. My CI/CD pipeline is just a modest GitHub runner workflow. This enables me to run a few trivial checks (mainly linting) as well as retrieving my Cloudflare API token supplied as a secret in GitHub.

Terraform then utilizes this secret to access Cloudflare and update DNS on my behalf!

Here is a high-level diagram of the flow:

alt text

While I won’t go into all the details of my Terraform code, I created a module that leverages Cloudflare’s DNS Provider. I can easily update the main.tf on a per zone basis and provision records for each zone/domain hosted on Cloudflare.

alt text

An additional benefit is that everything is tracked in git. Even though I’m the only one managing this at home, every DNS change still goes through a repeatable workflow: format, validate, plan, and apply.

For my homelab, I’m comfortable applying changes automatically after a successful plan. In production, I’d add pull requests, code review, and manual approval before applying DNS changes.

GitHub Actions workflow for Terraform DNS deployment

Here is the GitHub Actions workflow that ties everything together:

name: Terraform CI/CD

on:
  push:
    branches:
      - main
      - master

env:
  TF_IN_AUTOMATION: true

jobs:
  terraform:
    name: Terraform Fmt → Plan → Apply
    runs-on: ubuntu-latest

    env:
      TF_VAR_api_token: ${{ secrets.CLOUDFLARE_API_TOKEN }}
      AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
      AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: 1.9.8

      - name: Terraform Init
        working-directory: dns/zone
        run: terraform init -backend-config="access_key=${{ secrets.AWS_ACCESS_KEY_ID }}" -backend-config="secret_key=${{ secrets.AWS_SECRET_ACCESS_KEY }}"

      - name: Terraform Fmt Check
        working-directory: dns/zone
        run: terraform fmt -check -recursive

      - name: Terraform Validate
        working-directory: dns/zone
        run: terraform validate

      - name: Terraform Plan
        working-directory: dns/zone
        env:
          CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
        run: terraform plan -out=tfplan

      - name: Terraform Apply
        working-directory: dns/zone
        env:
          CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
        run: terraform apply -auto-approve tfplan

Summary

At the end of the day, managing DNS is a huge PITA. That being said, with the help of some automation, we can make our lives a lot easier!