Ansible BIND DNS — Deploy and Manage DNS Servers

Introduction

BIND (Berkeley Internet Name Domain) is the most widely used DNS server software. Automating DNS with Ansible ensures consistent zone configurations, proper replication between primary and secondary servers, and reliable record management across your infrastructure.

Install BIND9

---
- name: Deploy BIND DNS Server
  hosts: dns_servers
  become: true
  vars:
    bind_listen_ipv4: "{{ ansible_default_ipv4.address }}"
    bind_forwarders:
      - 8.8.8.8
      - 1.1.1.1
    bind_zones:
      - name: example.com
        type: primary
        file: db.example.com
      - name: 1.168.192.in-addr.arpa
        type: primary
        file: db.192.168.1

  tasks:
    - name: Install BIND9
      ansible.builtin.apt:
        name:
          - bind9
          - bind9utils
          - bind9-doc
          - dnsutils
        state: present
        update_cache: true
      when: ansible_os_family == "Debian"

    - name: Install BIND (RHEL)
      ansible.builtin.dnf:
        name:
          - bind
          - bind-utils
        state: present
      when: ansible_os_family == "RedHat"

    - name: Configure named.conf.options
      ansible.builtin.template:
        src: named.conf.options.j2
        dest: /etc/bind/named.conf.options
        owner: root
        group: bind
        mode: '0640'
      notify: Restart BIND

    - name: Configure named.conf.local
      ansible.builtin.template:
        src: named.conf.local.j2
        dest: /etc/bind/named.conf.local
        owner: root
        group: bind
        mode: '0640'
      notify: Reload BIND

    - name: Deploy zone files
      ansible.builtin.template:
        src: "{{ item.file }}.j2"
        dest: "/etc/bind/zones/{{ item.file }}"
        owner: root
        group: bind
        mode: '0640'
      loop: "{{ bind_zones }}"
      notify: Reload BIND

    - name: Create zones directory
      ansible.builtin.file:
        path: /etc/bind/zones
        state: directory
        owner: root
        group: bind
        mode: '0750'

    - name: Validate BIND configuration
      ansible.builtin.command:
        cmd: named-checkconf
      changed_when: false

    - name: Validate zone files
      ansible.builtin.command:
        cmd: "named-checkzone {{ item.name }} /etc/bind/zones/{{ item.file }}"
      loop: "{{ bind_zones }}"
      changed_when: false

    - name: Start BIND
      ansible.builtin.systemd:
        name: "{{ 'named' if ansible_os_family == 'RedHat' else 'bind9' }}"
        state: started
        enabled: true

  handlers:
    - name: Restart BIND
      ansible.builtin.systemd:
        name: "{{ 'named' if ansible_os_family == 'RedHat' else 'bind9' }}"
        state: restarted

    - name: Reload BIND
      ansible.builtin.command:
        cmd: rndc reload

Named Options Template

; templates/named.conf.options.j2
options {
    directory "/var/cache/bind";

    listen-on { 127.0.0.1; {{ bind_listen_ipv4 }}; };
    listen-on-v6 { ::1; };

    allow-query { localhost; 192.168.0.0/16; 10.0.0.0/8; };
    allow-transfer { none; };

    forwarders {
{% for fwd in bind_forwarders %}
        {{ fwd }};
{% endfor %}
    };

    recursion yes;
    allow-recursion { localhost; 192.168.0.0/16; 10.0.0.0/8; };

    dnssec-validation auto;
    auth-nxdomain no;

    // Logging
    querylog no;
    version "not disclosed";
};

Forward Zone Template

; templates/db.example.com.j2
$TTL    86400
@       IN      SOA     ns1.example.com. admin.example.com. (
                        {{ ansible_date_time.epoch }}  ; Serial
                        3600            ; Refresh
                        1800            ; Retry
                        604800          ; Expire
                        86400 )         ; Negative Cache TTL

; Name Servers
        IN      NS      ns1.example.com.
        IN      NS      ns2.example.com.

; Mail
        IN      MX  10  mail.example.com.

; A Records
ns1     IN      A       192.168.1.10
ns2     IN      A       192.168.1.11
mail    IN      A       192.168.1.20
www     IN      A       192.168.1.30
app     IN      A       192.168.1.31
db      IN      A       192.168.1.40

; CNAME Records
ftp     IN      CNAME   www
webmail IN      CNAME   mail

; Wildcard
*       IN      A       192.168.1.30

Reverse Zone Template

; templates/db.192.168.1.j2
$TTL    86400
@       IN      SOA     ns1.example.com. admin.example.com. (
                        {{ ansible_date_time.epoch }}
                        3600
                        1800
                        604800
                        86400 )

        IN      NS      ns1.example.com.
        IN      NS      ns2.example.com.

10      IN      PTR     ns1.example.com.
11      IN      PTR     ns2.example.com.
20      IN      PTR     mail.example.com.
30      IN      PTR     www.example.com.
40      IN      PTR     db.example.com.

Primary/Secondary Replication

# Primary server configuration
- name: Configure primary DNS
  hosts: dns_primary
  become: true
  tasks:
    - name: Allow zone transfers to secondary
      ansible.builtin.lineinfile:
        path: /etc/bind/named.conf.local
        insertafter: "type master;"
        line: "        allow-transfer { 192.168.1.11; };"
      notify: Reload BIND

# Secondary server configuration
- name: Configure secondary DNS
  hosts: dns_secondary
  become: true
  tasks:
    - name: Configure secondary zone
      ansible.builtin.blockinfile:
        path: /etc/bind/named.conf.local
        block: |
          zone "example.com" {
              type slave;
              masters { 192.168.1.10; };
              file "/var/cache/bind/db.example.com";
          };
      notify: Reload BIND

Dynamic DNS Updates

    - name: Generate TSIG key for dynamic updates
      ansible.builtin.command:
        cmd: tsig-keygen -a hmac-sha256 ansible-key
      register: tsig_key
      changed_when: false

    - name: Add DNS record via nsupdate
      ansible.builtin.shell:
        cmd: |
          nsupdate -k /etc/bind/ansible-key.key << EOF
          server {{ bind_listen_ipv4 }}
          zone example.com
          update add newhost.example.com 3600 A 192.168.1.100
          send
          EOF

Troubleshooting

IssueSolution
Zone not loadingnamed-checkzone example.com /etc/bind/zones/db.example.com
Config syntax errornamed-checkconf — shows line numbers
SERVFAIL responsesCheck forwarders are reachable
Zone transfer failedVerify allow-transfer includes secondary IP
Serial not incrementingEnsure serial increases on every zone change

Best Practices

  1. Increment serial on every change — use epoch or YYYYMMDDNN format
  2. Validate before reload — named-checkconf and named-checkzone
  3. Restrict zone transfers — allow-transfer { secondary_ip; };
  4. Use TSIG keys for secure dynamic updates
  5. Monitor with dig — dig @localhost example.com after changes
  6. Log queries selectively — enable only for troubleshooting

Conclusion

Ansible makes BIND DNS management repeatable and version-controlled. From single-server setups to primary/secondary replication with DNSSEC, every aspect of DNS configuration can be templated, validated, and deployed automatically. No more editing zone files by hand and forgetting to increment the serial.