Ansible – Discover and Backup PowerDNS Zones/Records

Ansible – Discover and Backup PowerDNS Zones/Records

While working on a solution that requires PowerDNS, I have come to a point in which I would like to include backup and recovery options for this solution. So I figured I would share these options and possibly help out others. I will keep this post short as there really is not anything to it. Another post will be forthcoming which will cover recovering from a disaster. Again, we will be leveraging Ansible to provide all of this

There are actually two Ansible playbooks that are required to be ran but I will wrap this up in a shell script for simplicity. I have created these playbooks to run locally from where they are executed and leverage the PowerDNS API.

The first playbook connects to the PowerDNS API and discovers all zones created on the server.

---
- hosts: localhost
  connection: local
  sudo: false
  vars:
    - pdns_api_key: changeme
    - pdns_api_web_url: 127.0.0.1
    - pdns_webserver_port: 8081
    - zones_dir: pdns_zone_backups
  tasks:
    - name: checking for zones_dir
      stat: path={{ zones_dir }}
      register: zones_dir_check

    - name: creating zones_dir if not exist
      file: path={{ zones_dir }} state=directory
      when: not zones_dir_check.stat.exists

    - name: gathering zones
      shell: "curl -H 'X-API-Key: {{ pdns_api_key }}' http://{{ pdns_api_web_url }}:{{ pdns_webserver_port }}/servers/localhost/zones | jq . > {{ zones_dir }}/pdns_zones_query.yml"

    - name: parsing zone names
      shell: "cat {{ zones_dir }}/pdns_zones_query.yml | grep name > {{ zones_dir }}/pdns_zones_query_names.yml"

    - name: cleaning up zone names
      replace: dest={{ zones_dir }}/pdns_zones_query_names.yml regexp="^    \"name\"{{ ':' }} \"" replace="  - "

    - name: cleaning up zones names
      replace: dest={{ zones_dir }}/pdns_zones_query_names.yml regexp="," replace=""

    - name: cleaning up zones names
      replace: dest={{ zones_dir }}/pdns_zones_query_names.yml regexp="\"" replace=""

    - name: making file yaml with vars
      lineinfile: dest={{ zones_dir }}/pdns_zones_query_names.yml line={{ item }} insertafter=BOF
      with_items:
        - "pdns_zones_query_names:"
        - "---"

The above will create a folder pdns_zone_backups with two files initially (pdns_zones_query.yml and pdns_zones_query_names.yml). Examples are below.
pdns_zones_query.yml

[
  {
    "last_check": 0,
    "notified_serial": 0,
    "id": "=5Fmsdcs.vagrant.local.",
    "url": "/servers/localhost/zones/=5Fmsdcs.vagrant.local.",
    "name": "_msdcs.vagrant.local",
    "kind": "Native",
    "dnssec": false,
    "account": "",
    "masters": [],
    "serial": 2015101001
  },
  {
    "last_check": 0,
    "notified_serial": 0,
    "id": "=5Fsites.vagrant.local.",
    "url": "/servers/localhost/zones/=5Fsites.vagrant.local.",
    "name": "_sites.vagrant.local",
    "kind": "Native",
    "dnssec": false,
    "account": "",
    "masters": [],
    "serial": 2015101001
  },
  {
    "last_check": 0,
    "notified_serial": 0,
    "id": "=5Ftcp.vagrant.local.",
    "url": "/servers/localhost/zones/=5Ftcp.vagrant.local.",
    "name": "_tcp.vagrant.local",
    "kind": "Native",
    "dnssec": false,
    "account": "",
    "masters": [],
    "serial": 2015101001
  },
  {
    "last_check": 0,
    "notified_serial": 0,
    "id": "=5Fudp.vagrant.local.",
    "url": "/servers/localhost/zones/=5Fudp.vagrant.local.",
    "name": "_udp.vagrant.local",
    "kind": "Native",
    "dnssec": false,
    "account": "",
    "masters": [],
    "serial": 2015101001
  },
  {
    "last_check": 0,
    "notified_serial": 0,
    "id": "vagrant.local.",
    "url": "/servers/localhost/zones/vagrant.local.",
    "name": "vagrant.local",
    "kind": "Native",
    "dnssec": false,
    "account": "",
    "masters": [],
    "serial": 2015101001
  },
  {
    "last_check": 0,
    "notified_serial": 0,
    "id": "0.0.10.in-addr.arpa.",
    "url": "/servers/localhost/zones/0.0.10.in-addr.arpa.",
    "name": "0.0.10.in-addr.arpa",
    "kind": "Native",
    "dnssec": false,
    "account": "",
    "masters": [],
    "serial": 2015101001
  },
  {
    "last_check": 0,
    "notified_serial": 0,
    "id": "2.0.10.in-addr.arpa.",
    "url": "/servers/localhost/zones/2.0.10.in-addr.arpa.",
    "name": "2.0.10.in-addr.arpa",
    "kind": "Native",
    "dnssec": false,
    "account": "",
    "masters": [],
    "serial": 2015101001
  },
  {
    "last_check": 0,
    "notified_serial": 0,
    "id": "101.0.10.in-addr.arpa.",
    "url": "/servers/localhost/zones/101.0.10.in-addr.arpa.",
    "name": "101.0.10.in-addr.arpa",
    "kind": "Native",
    "dnssec": false,
    "account": "",
    "masters": [],
    "serial": 2015101001
  },
  {
    "last_check": 0,
    "notified_serial": 0,
    "id": "106.0.10.in-addr.arpa.",
    "url": "/servers/localhost/zones/106.0.10.in-addr.arpa.",
    "name": "106.0.10.in-addr.arpa",
    "kind": "Native",
    "dnssec": false,
    "account": "",
    "masters": [],
    "serial": 2015101001
  },
  {
    "last_check": 0,
    "notified_serial": 0,
    "id": "107.0.10.in-addr.arpa.",
    "url": "/servers/localhost/zones/107.0.10.in-addr.arpa.",
    "name": "107.0.10.in-addr.arpa",
    "kind": "Native",
    "dnssec": false,
    "account": "",
    "masters": [],
    "serial": 2015101001
  },
  {
    "last_check": 0,
    "notified_serial": 0,
    "id": "110.0.10.in-addr.arpa.",
    "url": "/servers/localhost/zones/110.0.10.in-addr.arpa.",
    "name": "110.0.10.in-addr.arpa",
    "kind": "Native",
    "dnssec": false,
    "account": "",
    "masters": [],
    "serial": 2015101001
  },
  {
    "last_check": 0,
    "notified_serial": 0,
    "id": "125.0.10.in-addr.arpa.",
    "url": "/servers/localhost/zones/125.0.10.in-addr.arpa.",
    "name": "125.0.10.in-addr.arpa",
    "kind": "Native",
    "dnssec": false,
    "account": "",
    "masters": [],
    "serial": 2015101001
  },
  {
    "last_check": 0,
    "notified_serial": 0,
    "id": "10.10.10.in-addr.arpa.",
    "url": "/servers/localhost/zones/10.10.10.in-addr.arpa.",
    "name": "10.10.10.in-addr.arpa",
    "kind": "Native",
    "dnssec": false,
    "account": "",
    "masters": [],
    "serial": 2015101001
  },
  {
    "last_check": 0,
    "notified_serial": 0,
    "id": "24.16.172.in-addr.arpa.",
    "url": "/servers/localhost/zones/24.16.172.in-addr.arpa.",
    "name": "24.16.172.in-addr.arpa",
    "kind": "Native",
    "dnssec": false,
    "account": "",
    "masters": [],
    "serial": 2015101001
  },
  {
    "last_check": 0,
    "notified_serial": 0,
    "id": "1.168.192.in-addr.arpa.",
    "url": "/servers/localhost/zones/1.168.192.in-addr.arpa.",
    "name": "1.168.192.in-addr.arpa",
    "kind": "Native",
    "dnssec": false,
    "account": "",
    "masters": [],
    "serial": 2015101001
  },
  {
    "last_check": 0,
    "notified_serial": 0,
    "id": "70.168.192.in-addr.arpa.",
    "url": "/servers/localhost/zones/70.168.192.in-addr.arpa.",
    "name": "70.168.192.in-addr.arpa",
    "kind": "Native",
    "dnssec": false,
    "account": "",
    "masters": [],
    "serial": 2015101001
  },
  {
    "last_check": 0,
    "notified_serial": 0,
    "id": "200.168.192.in-addr.arpa.",
    "url": "/servers/localhost/zones/200.168.192.in-addr.arpa.",
    "name": "200.168.192.in-addr.arpa",
    "kind": "Native",
    "dnssec": false,
    "account": "",
    "masters": [],
    "serial": 2015101001
  }
]

From the above above information after running through Ansible we end up with a usable list to use as a variable for our next Ansible playbook. Example below.

pdns_query_zones_names.yml

---
pdns_zones_query_names:
  - _msdcs.vagrant.local
  - _sites.vagrant.local
  - _tcp.vagrant.local
  - _udp.vagrant.local
  - vagrant.local
  - 0.0.10.in-addr.arpa
  - 2.0.10.in-addr.arpa
  - 101.0.10.in-addr.arpa
  - 106.0.10.in-addr.arpa
  - 107.0.10.in-addr.arpa
  - 110.0.10.in-addr.arpa
  - 125.0.10.in-addr.arpa
  - 10.10.10.in-addr.arpa
  - 24.16.172.in-addr.arpa
  - 1.168.192.in-addr.arpa
  - 70.168.192.in-addr.arpa
  - 200.168.192.in-addr.arpa

Now we are ready to use the above variable list for our next Ansible playbook which will connect back to the PowerDNS API, query all records for each zone and back them up. Our next playbook looks like below.

---
- hosts: localhost
  connection: local
  sudo: false
  vars:
    - pdns_api_key: changeme
    - pdns_api_web_url: 127.0.0.1
    - pdns_webserver_port: 8081
    - zones_dir: pdns_zone_backups
  vars_files:
    - "{{ zones_dir }}/pdns_zones_query_names.yml"
  tasks:
    - name: creating zone folders
      file: path={{ zones_dir }}/{{ item }} state=directory
      with_items: pdns_zones_query_names

    - name: pulling records for zones
      shell: "curl -H 'X-API-Key: {{ pdns_api_key }}' http://{{ pdns_api_web_url }}:{{ pdns_webserver_port }}/servers/localhost/zones/{{ item }} | jq . > {{ zones_dir }}/{{ item }}/{{ item }}.yml"
      with_items: pdns_zones_query_names

And once the above runs we will now have a folder structure that looks like below, which includes all of our zones created as folders and then all of our records created as a file for each zone.

|-- pdns_zone_backups
|   |-- 0.0.10.in-addr.arpa
|   |   `-- 0.0.10.in-addr.arpa.yml
|   |-- 10.10.10.in-addr.arpa
|   |   `-- 10.10.10.in-addr.arpa.yml
|   |-- 101.0.10.in-addr.arpa
|   |   `-- 101.0.10.in-addr.arpa.yml
|   |-- 106.0.10.in-addr.arpa
|   |   `-- 106.0.10.in-addr.arpa.yml
|   |-- 107.0.10.in-addr.arpa
|   |   `-- 107.0.10.in-addr.arpa.yml
|   |-- 110.0.10.in-addr.arpa
|   |   `-- 110.0.10.in-addr.arpa.yml
|   |-- 1.168.192.in-addr.arpa
|   |   `-- 1.168.192.in-addr.arpa.yml
|   |-- 125.0.10.in-addr.arpa
|   |   `-- 125.0.10.in-addr.arpa.yml
|   |-- 200.168.192.in-addr.arpa
|   |   `-- 200.168.192.in-addr.arpa.yml
|   |-- 2.0.10.in-addr.arpa
|   |   `-- 2.0.10.in-addr.arpa.yml
|   |-- 24.16.172.in-addr.arpa
|   |   `-- 24.16.172.in-addr.arpa.yml
|   |-- 70.168.192.in-addr.arpa
|   |   `-- 70.168.192.in-addr.arpa.yml
|   |-- _msdcs.vagrant.local
|   |   `-- _msdcs.vagrant.local.yml
|   |-- pdns_zones_query_names.yml
|   |-- pdns_zones_query.yml
|   |-- _sites.vagrant.local
|   |   `-- _sites.vagrant.local.yml
|   |-- _tcp.vagrant.local
|   |   `-- _tcp.vagrant.local.yml
|   |-- _udp.vagrant.local
|   |   `-- _udp.vagrant.local.yml
|   `-- vagrant.local
|       `-- vagrant.local.yml

And if we take a look at our vagrant.local/vagrant.local.yml file for example.

{
  "comments": [],
  "records": [
    {
      "content": "192.168.70.241",
      "disabled": false,
      "ttl": 3600,
      "type": "A",
      "name": "dns.vagrant.local"
    },
    {
      "content": "10.0.101.60",
      "disabled": false,
      "ttl": 3600,
      "type": "A",
      "name": "logstash.vagrant.local"
    },
    {
      "content": "dns.vagrant.local",
      "disabled": false,
      "ttl": 3600,
      "type": "CNAME",
      "name": "ntp1.vagrant.local"
    },
    {
      "content": "ns1.vagrant.local",
      "disabled": false,
      "ttl": 3600,
      "type": "NS",
      "name": "vagrant.local"
    },
    {
      "content": "ns2.vagrant.local",
      "disabled": false,
      "ttl": 3600,
      "type": "NS",
      "name": "vagrant.local"
    },
    {
      "content": "node-1.vagrant.local hostmaster.vagrant.local 2015101001 10800 3600 604800 3600",
      "disabled": false,
      "ttl": 3600,
      "type": "SOA",
      "name": "vagrant.local"
    },
    {
      "content": "10.0.101.40",
      "disabled": false,
      "ttl": 3600,
      "type": "A",
      "name": "vcsa.vagrant.local"
    }
  ],
  "soa_edit": "",
  "soa_edit_api": "",
  "last_check": 0,
  "notified_serial": 0,
  "id": "vagrant.local.",
  "url": "/servers/localhost/zones/vagrant.local.",
  "name": "vagrant.local",
  "kind": "Native",
  "dnssec": false,
  "account": "",
  "masters": [],
  "serial": 2015101001
}

As you can see from everything above it is actually quite simple to accomplish our backups of zones/records from our PowerDNS server(s). Now these tasks could be scheduled to run as a cron job, Jenkins job or any other method. We can also incorporate these backups into our version control utilizing Git. Again, all of these tasks we would want to automate (more on this in a later post).

Looking for an Ansible playbook to install PowerDNS? Go here.

Up next will be how we can restore our zones/records from these backups.

Enjoy!

About Larry Smith Jr.

vExpert 2013-2016 | Old-School coder coming back around to my roots #DevOPS and #automation | #Ansible junky!

2 thoughts on “Ansible – Discover and Backup PowerDNS Zones/Records

  1. Hey Larry,

    I’m back! I wonder if it would be better to have variables of (something like) web_hostname: 127.0.0.1:8081 and web_url: /servers/localhost/zones – these are more accurate and descriptive. 127.0.0.1 is not a URL.

    Double word here: “From the above above <<< information after running through Ansible…"

    It won't be clear to everyone what jq is and does, perhaps worth a brief sentence or two?

    Anyway, great stuff and nice to see some example of modules I've not seen before (replace and lineinfile)

Leave a Reply

Your email address will not be published. Required fields are marked *

*