Over the years, I’ve inevitably grown my collection of unneeded domains, domains which I really don’t have much use for, but also don’t have the heart to abandon and let squatters do their thing.

Management has not necessarily become more complex, never quite needing to change anything after initial setup, nor needing anything more complex than an A or MX record. Rather, I’m practically OCD and want a nice birds-eye-view of everything.

To solve this problem, there were a few choices:

First of all, terraform/opentofu/pulumi were very tempting choices because they meant I could manage my full Cloudflare infrastructure, not only DNS records. However, one downside for a simple setup is that they aren’t stateless, what you see in your configs might not represent what is actually rolled out in the real world.

Now on to the stateless tools. The advantage of these is what you see is what you get, there are no unexpected differences or chances for divergence. When these tools run the state is diff’d and synchronised with the configuration.

Initial setup of dnscontrol was easy. Assuming you’re wanting to manage DNS records for cloudflare, just create a new CLOUDFLARE_API_TOKEN and a corresponding creds.json to define reading this from the environment, finally create your configuration with dnsconfig.js and run dnscontrol preview

// creds.json
{
  "cloudflare": {
    "TYPE": "CLOUDFLAREAPI",
    "apitoken": "$CLOUDFLARE_API_TOKEN"
  }
}
// dnsconfig.js

var DSP_CLOUDFLARE = NewDnsProvider("cloudflare");

// * "none" means only DNS management, not domain registration.
// * aliases to "none" are for documentation purposes only
var REG_NONE = NewRegistrar("none");
var REG_CLOUDFLARE = REG_NONE;

// configure your domain(s)
D("my.cool.domain", REG_CLOUDFLARE,
	DnsProvider(DSP_CLOUDFLARE),
	CNAME("www", "michlo.dev.", CF_PROXY_ON),
END);

The last thing you need to do to make this process painless is setup your CI to automate the check/preview/push process. I’m using github actions, so here’s that config. Don’t forget to set your secret for CLOUDFLARE_API_TOKEN in your repo.

name: dnscontrol
on:
  pull_request:
  push:
    branches: [main]
jobs:
  dnscontrol:
    runs-on: ubuntu-latest
    container:
      image: ghcr.io/stackexchange/dnscontrol:latest
    steps:
      - uses: actions/checkout@v6
      - run: dnscontrol --version
      - env:
          CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
        run: dnscontrol check
      - env:
          CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
        run: dnscontrol ${{ (github.ref == 'refs/heads/main' && github.event_name == 'push') && 'push' || 'preview' }}

Now you’re all set! Open a new PR with your changes, preview them, merge, and watch it all roll out!