Managing DNS records with dnscontrol and GitHub Actions

Managing DNS records with dnscontrol and GitHub Actions

guide

Manage your DNS records across multiple providers with a single file and streamlined deployment with GitHub actions.

Like any respectable tech geek, I have dozens of domains spread across multiple registrars and DNS hosts. A few weeks ago, I asked a group of friends what DNS hosting platform were they using lately. My friend Benni mentioned that he was using dnscontrol and that he only used providers that were compatible with it. Curious, I dug in to see what it was all about; little did I know this tool would change how I manage my domain's DNS records.

As their website puts it "DNSControl is an opinionated platform for seamlessly managing your DNS configuration across any number of DNS hosts, both in the cloud or in your own infrastructure". It does this by communicating with the API of the DNS hosts to interact with the zones.

Because all the config is in two files, it allows you to follow infrastructure-as-code principles and integrate it into version control and CI/CD systems. I am storing mine in a private GitHub repo and integrated with GitHub Actions so that it deploys my zone changes when I push to main.

You can learn how to get started with dnscontrol by following their instructions here. If you have any questions, feel free to ping me - I'd be happy to help! Since setting it up to work with GitHub Actions was undocumented, that's what I'll show you here.

Unlike locally run instances, where you store the API tokens in the creds.json file, I am using environment variables for the provider API token as I am actually storing the secret in GitHub for it to run via Actions (it's also a very bad idea to store the credentials unencrypted in the repo).

 // Example creds.json
 {
    "linode": {
      "token": "$LINODE_API_TOKEN"
    }
  }

To add a token into GitHub Secrets:

  1. Go to the repo settings
  2. Go to Secrets -> Actions on the right hand nav
  3. Click the New repository secret button
  4. Name = the variable name you defined in creds.json _(eg: LINODE_APITOKEN)
  5. Value = the API token from your provider
  6. Add secret

Your secret is now stored safely and available to the repo for when the action runs.

Now we're going to setup the actions that will be run when certain parameters are met. I setup two, preview and push.

preview will show you a preview of the changes that will happen when a PR is opened. push will actually push the changes to the DNS providers via the API.

In the repo folder on your local machine, create a .github/workflows folder. This folder will store the actions.

mkdir -p .github/workflows

Inside the workflows folder, we're going to add two files for the two actions. Let's start with preview.yml

This file will run the action with the 'preview' argument (same as running dnscontrol preview locally) and give you a summary of what changes will be made. It prints out what DNS records are expected to be created, modified or deleted. This action requires the secrets for the specified DNS providers, so we'll call our previously created secret. It will also add the output of the 'preview' command as a comment to the pull request. This saves you several clicks through the menus to get to the output logs for the preview job.

Here's how the file looks:

name: Preview

on: pull_request

jobs:
  preview:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2

      - name: DNSControl preview
        uses: koenrh/dnscontrol-action@v3.18.1
        id: dnscontrol_preview
        env:
          CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          LINODE_API_TOKEN: ${{ secrets.LINODE_API_TOKEN }}
        with:
          args: preview
      - name: Preview pull request comment
        uses: unsplash/comment-on-pr@v1.3.0
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          msg: |
          *** Ticks escaped due to markdown syntax - remove slash and this line in prod file ***
           \``` 
            ${{ steps.dnscontrol_preview.outputs.preview_comment }}
           \```
          check_for_duplicate_msg: true

Now every time a pull request is opened, GitHub Actions will run the action, and show you a preview of the changes to be made to the zone.

dnscontrol-gha-preview

If everything looks good, merge the request. When you merge the request, push.yml will be executed, which is what we're going to define next. As you'll see, I'm setting up the action to only trigger if dnsconfig.js is modified, and only when a push to main happens, which will save the action from running when not needed, like when updating creds.json or any other file, or when pushing to a branch.

Don't forget to add your secrets in the env section!

name: Push

on:
  push:
    branches:
      - main
    paths:
      - 'dnsconfig.js'
jobs:
  push:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2

      - name: DNSControl push
        uses: koenrh/dnscontrol-action@v3.18.1
        env:
          CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          LINODE_API_TOKEN: ${{ secrets.LINODE_API_TOKEN }}
        with:
          args: push

Lastly, to keep your action updated with the latest updates, you can use dependabot.

To enable Dependabot in your GitHub repository, add a .github/dependabot.yml file with the following contents:

version: 2
updates:
  # Maintain dependencies for GitHub Actions
  - package-ecosystem: "github-actions"
    directory: "/"
    schedule:
      interval: "daily"

That's pretty much it. You can now commit to a branch and open a PR to see a preview and then merge the PR to master to push the changes to the DNS providers.

If you ran dnscontrol preview locally and are satisfied with the outcome, you can push directly to main for the push action to run, without opening a PR. You can always check the action log in the Actions tab to review what the action did.

Now that we're done with our GitHub actions setup, I want to show you a quick tip on how to use macros to reuse sets of records across zones.

Because the main config file is a javascript file, we can do some neat things like using macros to reuse the same DNS records across zones (and only needing to update it once!).

Because macros are really just variables, we can define them like any other variable like so and just call the variable name in the record list

var GOOGLE_MX = [
    MX('@', 1, 'aspmx.l.google.com.'),
    MX('@', 5, 'alt1.aspmx.l.google.com.'),
    MX('@', 5, 'alt2.aspmx.l.google.com.'),
    MX('@', 10, 'alt3.aspmx.l.google.com.'),
    MX('@', 10, 'alt4.aspmx.l.google.com.'),
]

D('example.com', REG, DnsProvider('LINODE'),
   GOOGLE_MX,
   A('@', '1.2.3.4')
)

D('example.net', REG, DnsProvider('LINODE'),
   GOOGLE_MX,
   A('@', '1.2.3.4')
)

You can see how dnscontrol makes managing multiple zones across various hosts easy. You define the domain, define the provider, and add the records. If you ever need to change DNS hosts, all you need to do is change provider definition and dnscontrol will upload the zone to the new provider.

If you have any questions while getting started, feel free to shoot me an email - happy to help you get started!

Previous Post Next Post