Abstracting DNS Record Management with Ansible and Jinja 2

Synchronizing properly implemented DNS zones is, to put it lightly, a real chore:

  • Creating forward DNS entries, e.g. A, AAAA, CNAME. These names are used to resolve to resources.
  • Creating reverse DNS entries, e.g. PTR.
  • Creating DNS entries that define the zone, e.g. SOA, NS

For a system to behave properly, your forward and reverse entries need to be identical, but software like BIND/Unbound rely on zonefiles that don't connect the two. Many information systems / DNS zones exist with improperly implemented reverse DNS, or partially implemented forward DNS asymptomatically for a time. Certain events (e.g. CA validation, discovery, implementing IPv6) can bring things to the forefront if ordinary network management practice doesn't.

For this post, we'll first work on abstracting the DNS zonefile - ensuring that a user can deploy zonefiles conformant to a standard - and then we'll illustrate how that can be used with Netbox to automatically populate DNS entries from Netbox.

Abstracting the zonefile here will achieve a few goals - but the file size is guaranteed to be longer than if you simply managed the zone files from source. Here are some advantages:

  • This pipeline ABSOLUTELY MUST establish forward and reverse records from the same data!
  • This pipeline must test zonefiles, and avoid installing them if they aren't good (prevents outages)
  • This pipeline must establish documentation standards for a DNS zone (abstract the standard)
  • This pipeline must scale to support large quantities of DNS zones / records
  • This pipeline must be easy to use, even with inexperienced DNS administrators (we can't have it all be on the shoulders of that one guy who can safely make DNS changes)

To achieve this, we'll first establish a YAML schema and Jinja2 template to structure the data. Here's the YAML schema:

 1zones:
 2    - name: filename
 3      zonename:
 4      soa:
 5      settings:
 6        ttl:
 7        serial:
 8        refresh:
 9        retry:
10        expiry:
11      nameservers: []
12      reverse_zones:
13        ip4:
14        ip6:
15      records: [{ "name": "", "type": "", "addr": ""}]

There are also some subtle differences between IPv4 and IPv6 reverse zones, so in this case, we're going to use three Jinja2 templates (in the Gist below).

It also assumes that there's a dedicated classful prefix for each DNS zone. This isn't always true for more complex deployments, but they can also do stuff like buy Infoblox.

I have also included a GitHub Action in the gist, because it provides a good place to demostrate best practices (e.g. using venv) in one compact place. If you want to install generated zone files on-premises, you can run this on a self-hosted runner with an Ansible inventory group (e.g. nameservers).

It's still a little clunky, the next step should help with that (harvesting DDI information from Netbox IPAM data).

GitHub Link