Ansible - Discover and Backup PowerDNS
Refreshed June 2026: the original did this with two playbooks, a shell wrapper, and a chain of
curl,jq,grep,replace, andlineinfileto turn JSON into a YAML list by hand. None of that is needed now. Theansible.builtin.urimodule calls the PowerDNS API and hands back parsed JSON directly, so the whole thing collapses to one playbook with no text munging. The API path also gained an/api/v1prefix since 2015. Playbook syntax-checked on ansible-core 2.19.
Background
I wanted Ansible to back up the zones and records from a PowerDNS server through its HTTP API. The 2015 version of this post worked, but it was held together with shell commands: curl to hit the API, jq to format it, then grep and three replace passes and a lineinfile to carve a usable zone list out of the JSON. It also needed two separate playbooks because there was no clean way to pass the discovered zones from one step to the next without writing an intermediate file.
The uri module removes all of that. It calls the API, parses the JSON response into a variable, and you loop over it.
The Playbook
One playbook. Discover the zones, pull each zone’s records, write each one to disk.
---
- name: Discover and back up PowerDNS zones and records
hosts: localhost
connection: local
gather_facts: false
vars:
pdns_api_key: changeme
pdns_api_url: http://127.0.0.1:8081/api/v1
zones_dir: pdns_zone_backups
tasks:
- name: Ensure the backup directory exists
ansible.builtin.file:
path: "{{ zones_dir }}"
state: directory
mode: "0750"
- name: Discover all zones
ansible.builtin.uri:
url: "{{ pdns_api_url }}/servers/localhost/zones"
method: GET
headers:
X-API-Key: "{{ pdns_api_key }}"
register: pdns_zones
- name: Pull the full record set for each zone
ansible.builtin.uri:
url: "{{ pdns_api_url }}/servers/localhost/zones/{{ item.id }}"
method: GET
headers:
X-API-Key: "{{ pdns_api_key }}"
register: pdns_zone_detail
loop: "{{ pdns_zones.json }}"
loop_control:
label: "{{ item.name }}"
- name: Write each zone backup to disk
ansible.builtin.copy:
content: "{{ item.json | to_nice_json }}\n"
dest: "{{ zones_dir }}/{{ item.json.name }}.json"
loop: "{{ pdns_zone_detail.results }}"
loop_control:
label: "{{ item.json.name }}"
A few notes on why this is so much shorter than the original:
urireturns parsed JSON.pdns_zones.jsonis already a list of zone dictionaries. Nocurl, nojq, no parsing the text yourself.- The zone list passes straight into a loop.
loop: "{{ pdns_zones.json }}"uses the discovery result directly, so there is no intermediatepdns_zones_query_names.ymlfile and no second playbook. - Reference each zone by
id. The API gives every zone anid, which is the correct value to use in the per-zone URL (it handles the trailing dot and any escaping for you). - Back up as
.json. The original saved JSON content into files named.yml, which was misleading. These are JSON, so they get a.jsonextension.
What The Zones API Returns
The discovery call returns a JSON array of zone objects. Trimmed to a couple of entries:
[
{
"id": "vagrant.local.",
"url": "/api/v1/servers/localhost/zones/vagrant.local.",
"name": "vagrant.local",
"kind": "Native",
"dnssec": false,
"serial": 2015101001
},
{
"id": "1.168.192.in-addr.arpa.",
"url": "/api/v1/servers/localhost/zones/1.168.192.in-addr.arpa.",
"name": "1.168.192.in-addr.arpa",
"kind": "Native",
"dnssec": false,
"serial": 2015101001
}
]
The per-zone call returns the full record set. For example, pdns_zone_backups/vagrant.local.json:
{
"id": "vagrant.local.",
"name": "vagrant.local",
"kind": "Native",
"dnssec": false,
"serial": 2015101001,
"records": [
{
"content": "192.168.70.241",
"disabled": false,
"ttl": 3600,
"type": "A",
"name": "dns.vagrant.local"
},
{
"content": "node-1.vagrant.local hostmaster.vagrant.local 2015101001 10800 3600 604800 3600",
"disabled": false,
"ttl": 3600,
"type": "SOA",
"name": "vagrant.local"
}
]
}
Automating It
These tasks are a natural fit for a scheduled run: a cron job, a CI pipeline, or a systemd timer. Point the dest at a directory under version control and commit after each run, and you have a dated history of every zone change. Because the playbook is idempotent on the directory and overwrites the backups in place, a git diff after each run shows exactly what changed in DNS.
Conclusion
Same goal as a decade ago, a fraction of the moving parts. Let uri talk to the API and parse the JSON, loop over the result, and write it out. No shell pipeline to babysit.
Enjoy!
Comments