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
| Issue | Solution |
|---|---|
| Zone not loading | named-checkzone example.com /etc/bind/zones/db.example.com |
| Config syntax error | named-checkconf — shows line numbers |
| SERVFAIL responses | Check forwarders are reachable |
| Zone transfer failed | Verify allow-transfer includes secondary IP |
| Serial not incrementing | Ensure serial increases on every zone change |
Best Practices
- Increment serial on every change — use epoch or YYYYMMDDNN format
- Validate before reload —
named-checkconfandnamed-checkzone - Restrict zone transfers —
allow-transfer { secondary_ip; }; - Use TSIG keys for secure dynamic updates
- Monitor with
dig—dig @localhost example.comafter changes - 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.