Ansible: Deploy DNS/DHCP Failover Pair

Table of Contents

I’ve been working on getting the individual pieces put together on this, and I finally have the entire thing working. I’ve got a playbook workflow that will deploy two containers, install BIND and ISC DHCPD, and start them.

What it will not do is update DNS automatically. I’m able to update the DNS manually with nsupdate, but it appears as though DHCPD isn’t even trying. For now, I’m moving on, and I will revisit with fresh eyes.

You can find this git commit here

Container deployment

In my previous articles, I deployed a single machine, but I want to be able to call a group in inventory, and deploy many containers/VMs at a time. To that aim:

Flesh out hostvars

Here’s an example entry:

fqdn: ''
ip_address: ''
mac_address: '52:54:00:07:60:08'
services: [ 'dns', 'dhcp' ]
vmid: '201'
node: 'gold'

I don’t have any particular use at this moment for the services block, but I plan to.

Tweak how the playbook manages vars

The original playbook was making a manually coded dict. Most of my hosts are going to be default configuration, so I fleshed out sane defaults;

  balloon: '1024'
  cores: '1'
  cpus: '1'
    ct: '2'
    vm: '10'
  format: 'qcow2'
    ct: '1024'
    vm: '2048'
  nameserver: ''
  net: '{"net0":"virtio,bridge=vmbr0"}'
  netif: '{"net0":"name=eth0,ip=dhcp,ip6=dhcp,bridge=vmbr0"}'
# and so on

And then call the host group and pull facts directly in the deployment playbook and fall back to defaults.

    deploy_cts: "{{ groups['nameservers']}}"
  - name: 'create containers'
      password: '{{ root_password }}'
      vmid: '{{ hostvars[item].vmid | default([])}}'
      api_user: "{{ api_user }}"
      api_password: "{{ api_password }}"
      api_host: "{{ api_host }}"
      hostname: '{{ hostvars[item].inventory_hostname }}'
      node: '{{ hostvars[item].node }}'
      cores: '{{ hostvars[item].cores | default(defaults.cores) }}'
      cpus: '{{ hostvars[item].cpus | default(defaults.cpus) }}'
      netif: "{{ '{\"net0\":\"name=eth0,type=veth,bridge=vmbr0,ip6=auto,gw=,hwaddr=' + hostvars[item].mac_address + ',ip=' + hostvars[item].ip_address + '/25\"}' | default(defaults.netif) }}"
      memory: '{{ hostvars[item].memory | default(defaults.memory.ct) }}'
      swap: '{{ hostvars[item].swap | default(defaults.swap.ct) }}'
      disk: '{{ hostvars[item].disk | default(defaults.disk.ct) }}'
      storage: '{{ hostvars[item].storage | default( }}'
      onboot: '{{ hostvars[item].onboot | default(defaults.onboot) }}'
      pubkey: '{{ id_ed25519_pub }}'
      searchdomain: '{{ hostvars[item].searchdomain | default(defaults.searchdomain) }}'
      nameserver: '{{ hostvars[item].nameserver | default(defaults.nameserver) }}'
      ostemplate: '{{ ostemplate }}'
      state: 'present'
    with_items: "{{ deploy_cts }}"
      pause: 5
      - 'sleep'
    register: 'created_cts_pve'

Pulling the variables from the register in the next step took me way too long.


Similarly, I wanted this to be as modular as possible. It still needs some work, but it’s most of the way there.

The zone files are dynamically filled out:

$TTL 604800	; 1 week
{{ domain }}	IN SOA	{{ ansible_fqdn }}. admin.{{ domain }}. (
				8          ; serial
				604800     ; refresh (1 week)
				86400      ; retry (1 day)
				2419200    ; expire (4 weeks)
				604800     ; minimum (1 week)
			NS	{{ ansible_fqdn }}.
{% for host in hostlist %}
{{ host }}  IN  A  {{ hostvars[host].ansible_host }}
{% endfor %}


And templates use conditionals whether deploying on master or slave. From named.conf:

	recursion yes;
  recursive-clients 50;
	allow-recursion{ lan; };
	allow-query { lan; };
{% if ansible_hostname in groups['ns-masters'] %}
	allow-transfer { localhost;; };
{% else %}
	allow-transfer { none; };
{% endif %}

Vault secrets

My rndc.key template pulls a secret I generated and stored in my vault:

key "rndc-key" {
	algorithm hmac-md5;
	secret "{{ rndc_key }}";


By the time I got BIND up, I was rolling, and so there’s not too much to say here. It’s committed to git, so you can check it there.

What’s left to do

Like I opened with, DNS updates aren’t working, and I can’t figure out why. I tested with some straight configs that were reported to work, but something is up. I’m currently running with the firewall down and no SELinux/AppArmor, and I will be tightening security as I move forward.

For now, I call three playbooks individually. When I make a little more headway, and get some error handling built into my site, I’m going to put together coherent ways to call large multi-stage operations.

For now, take a look at this git commit if you’re interested