Hey,
I'm Parker.

Use Git & GitHub to manage your DNS with OctoDNS

DNS is one of those services that still requires logging into some web portal and clicking buttons and entering data. Heaven forbid you enter a bad IP address and take down your service – there’s no “rollback to previous value” option. Want to track your configurations over time? Sorry, not something your web portal can often do. DNS configuration should be simple, trackable, and automatable.

Good news! You can take charge of your DNS configuration with Git, GitHub and OctoDNS.

OctoDNS is a library for managing your DNS, which ships with a series of commands for syncing, dumping, and reporting. OctoDNS was originally developed at GitHub to aid in tracking DNS across our domains and in helping integrate DNS management into our normal tools, like Git, our deployment stack, and so on.

OctoDNS operates kind of like rsync: you specify a source (usually YAML files) and specify a destination or destinations (DNS providers) and ask it to sync. Any configurations present in the source configuration (YAML) will sync to the destination (DNS providers). I haven’t tried, but theoretically you can even sync from one DNS provider to another and skip YAML altogether! But the point of this blog post is to share how we manage DNS with Git & GitHub so we’ll be using YAML.

Getting Started

1. Configuration

We’ll create our repository and dump a zone to get started. Create a repository on GitHub, initialize with a readme, and clone:

~$ git clone https://github.com/YOURUSERNAME/dns
~$ cd dns

Now, make a config directory and specify the source & destination for your configurations. At GitHub we name this file after its environment. We’ll be fancy and call our environment “production.”

In this example, I’ll use Cloudflare as my DNS provider but you can use any other supported providers. The records they support depend on their API capabilities, so make sure to check that your provider supports creating the types of records you want. In this file, we’ll use the built-in environment variable expansion. This means any octodns-* commands will need to be run with these variables set.

~/dns$ mkdir config
~/dns$ cat > config/production.yaml <<EOF
---
providers:
  config:
    class: octodns.provider.yaml.YamlProvider
    directory: ./config
  cloudflare:
    class: octodns.provider.cloudflare.CloudflareProvider
    email: env/CLOUDFLARE_EMAIL
    token: env/CLOUDFLARE_TOKEN

zones:
  example.com.:
    sources:
      - config
    targets:
      - cloudflare
EOF

Ok, so the configuration specifies two things: the providers, and the zones.

The providers are your sources and destinations for the DNS records. Here, we’re specifying a provider called config which reads the YAML from ./config. We also specify a provider called cloudflare which uses Cloudflare’s API and requires the CLOUDFLARE_EMAIL and CLOUDFLARE_TOKEN environment variables.

The zones are your domains! You can specify unique sources and targets for each. “Target” is our destination, and you can specify as many as you wish! They must be configured in the providers dictionary to use them.

2. Dump your zone

Now that you have your configuration file in place, you can run OctoDNS! First, we’ll dump the zone you added to your configuration file. OctoDNS will create a new YAML file with all the records it could find in the target you specify.

We’ll use Docker to make installation a little simpler, but if you’re into managing your own Python virtualenv and such, feel free to do that. The OctoDNS repository’s readme has great documentation on running it straight on your host.

$ docker run -it --rm \
    -e CLOUDFLARE_EMAIL=yourlogin@gmail.com \
    -e CLOUDFLARE_TOKEN=yourtoken \
    -v $(pwd):/opt/dns \
    parkr/octodns:v0.9.4 \
    octodns-dump \
    --config-file /opt/dns/config/production.yaml \
    --output-dir /opt/dns/config \
    "example.com." \
    cloudflare

This will print out a YAML file in config/example.com.yaml, containing your DNS entries (comments my own):

---
# These are the top-level DNS records for example.com.
# This specifies:
#    1. ALIAS example.com to subdomainarecords.example.com
#    2. MX example.com to mxa.emailprovider.com at priority 10 and mxb.emailprovider.com at priority 10
#    3. TXT example.com with "v=spf1..."
? ''
: - ttl: 300
    type: ALIAS
    value: subdomainarecords.example.com.
  - ttl: 300
    type: MX
    values:
    - exchange: mxa.emailprovider.com.
      preference: 10
    - exchange: mxb.emailprovider.com.
      preference: 10
  - ttl: 300
    type: TXT
    value: v=spf1 include:emailprovider.com ~all
# Here is an example of a CNAME record, subdomaincname.example.com.
# It points back to example.com.
subdomaincname:
  ttl: 300
  type: CNAME
  value: example.com.
# And here is an example of an A record with corresponding IPv6 AAAA record,
# subdomainarecords.example.com.
subdomainarecords:
- ttl: 300
  type: A
  value: 100.100.100.100
- ttl: 300
  type: AAAA
  value: 2001:2001:2001:2001:2001:2001:2001:2001

Whew! This is great! Now we can commit this file and propose a change.

3. Syncing your zones

Let’s make a change. Let’s modify the A records for subdomainarecords.example.com. To do this, we’ll update the YAML:

subdomainarecords:
- ttl: 300
  type: A
  value: 300.300.300.300
- ttl: 300
  type: AAAA
  value: 4001:4001:4001:4001:4001:4001:4001:4001

Now we’ll test our changes with octodns-sync:

$ docker run -it --rm \
    -e CLOUDFLARE_EMAIL=yourlogin@gmail.com \
    -e CLOUDFLARE_TOKEN=yourtoken \
    -v $(pwd):/opt/dns \
    parkr/octodns:v0.9.4 \
    octodns-sync --config-file /opt/dns/config/production.yaml

It will print out some beautiful output showing the before and after picture (it will NOT apply the changes yet):

********************************************************************************
* example.com.
********************************************************************************
* cloudflare (CloudflareProvider)
*   Update
*     <AaaaRecord AAAA 300, subdomainarecords.example.com., ['2001:2001:2001:2001:2001:2001:2001:2001']> ->
*     <AaaaRecord AAAA 300, subdomainarecords.example.com., ['4001:4001:4001:4001:4001:4001:4001:4001']> (config)
*   Update
*     <ARecord A 300, subdomainarecords.example.com., ['100.100.100.100']> ->
*     <ARecord A 300, subdomainarecords.example.com., ['300.300.300.300']> (config)
*   Summary: Creates=0, Updates=2, Deletes=0, Existing Records=6
********************************************************************************

Now, let’s apply our changes! To do this, add the --doit flag to our octodns-sync command:

$ docker run -it --rm \
    -e CLOUDFLARE_EMAIL=yourlogin@gmail.com \
    -e CLOUDFLARE_TOKEN=yourtoken \
    -v $(pwd):/opt/dns \
    parkr/octodns:v0.9.4 \
    octodns-sync --config-file /opt/dns/config/production.yaml --doit

You’ll see CloudflareProvider[cloudflare] apply: making changes when it’s making your changes.

Tada! Wait for the records to expire (300 seconds) and dig for your new records!

4. Make it go with GitHub Flow

Now, let’s propose a change using a PR workflow. This is especially important when you’re managing the DNS

  1. Create a new branch and check it out
  2. Modify a record and commit the changes
  3. Push the changes and submit a PR
  4. Run octodns-sync without --doit and leave a comment with the output to show the changes you’re making
  5. Get approval
  6. Run octodns-sync with --doit to apply the changes
  7. Verify the records are present in DNS and merge the PR

:tada: GitHub Flow helps make managing your DNS much easier.

See it in action

Want to see it in action? I am managing my own DNS at parkr/dns, as well as the DNS for Jekyll at jekyll/dns.

I hope this helps illustrate how OctoDNS can take the pain away from managing DNS manually in a web portal.