# My Infrastructure as Code for DNS

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:

- `terraform`/`opentofu`
- `pulumi`
- `octodns`
- `dnscontrol`

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.

- All of these need some storage backend either an encrypted state committed to your repo (not the best practice, with the risk of passphrase leaks) or an `S3` compatible storage bucket and credentials for that (Cloudflare's `R2` was a tempting choice with its generous 10GB free tier. Not to forget mentioning the UI is just way more pretty than `S3` which pleases me somewhat every time I need to look at it).
- Furthermore, being stateful means its easy for these tools to go out of sync if you don't respect the "no manual configuration after setting this up" rule. While you can configure it to ignore parts of your infra, I'm one person and I don't want to spend time resolving these kinds of issues if in the future I lack the needed self-control.

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.

- `octodns` was the main contender, but was also quickly rejected after a few local experiments. I am no expert, but while seemingly fully capable, it also at the time seemed that its Cloudflare support was not as well refined. So that left me with only one reasonable choice.

Initial setup of [dnscontrol](https://docs.dnscontrol.org/getting-started/examples) 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`

- > Please be careful when you run the following. `dnscontrol` assumes it has FULL ownership of your dns management. If any records differ from your current domain configuration, they will be updated or removed when you run `dnscontrol push`

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

```javascript
// 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.

```yaml
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!
