Abstracting DNS Record Management with Ansible and Jinja 2
Published: 2024-01-08 05:00:40

---
title: Abstracting DNS Record Management with Ansible and Jinja 2
published: true
date: 2024-01-06 09:00:00 UTC
tags: 
canonical_url: https://blog.engyak.co/2024/01/dns-automation/
---

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).

<style>
     
    @import url('https://cdn.rawgit.com/lonekorean/gist-syntax-themes/d49b91b3/stylesheets/idle-fingers.css');
    
    @import url('https://fonts.googleapis.com/css?family=Open+Sans');
    body {
      font: 16px 'Open Sans', sans-serif;
    }
    body .gist .gist-file {
      border-color: #555 #555 #444
    }
    body .gist .gist-data {
      border-color: #555
    }
    body .gist .gist-meta {
      color: #ffffff;
      background: #373737; 
    }
    body .gist .gist-meta a {
      color: #ffffff
    }
</style>[GitHub Link](https://gist.github.com/ngschmidt/d0862985e382b052fd3f42bbc4082af3)<script type="application/javascript" src="https://gist.github.com/ngschmidt/d0862985e382b052fd3f42bbc4082af3.js"></script>