This guide provides Ansible playbooks to install and configure BIND with ISC official repository setup, distro-aware package handling, and service management for Debian 10+, Ubuntu LTS, and RHEL 9+ compatible systems.
Latest Version: BIND 9.20.20 (February 2026, ESV). Playbooks are compatible with BIND 9.18.x and 9.20.x.
ansible-galaxy collection install community.general ansible.posix
Create bind-repo-setup.yml to add official ISC repositories:
---
- name: Setup ISC BIND Repository
hosts: bind_servers
become: true
gather_facts: true
vars:
bind_version: "9.20" # Options: 9.18, 9.20, 9.21
tasks:
- name: Setup ISC repository (Debian/Ubuntu)
block:
- name: Install prerequisites
apt:
name:
- gnupg
- curl
- apt-transport-https
- software-properties-common
state: present
update_cache: true
when: ansible_distribution in ['Debian', 'Ubuntu']
- name: Create keyrings directory
file:
path: /etc/apt/keyrings
state: directory
mode: "0755"
when: ansible_distribution in ['Debian', 'Ubuntu']
- name: Import ISC GPG key
ansible.builtin.key:
url: https://downloads.isc.org/isc/bind9/keys/9.18/KSK-2018.key
keyring: /etc/apt/keyrings/isc-archive-keyring.gpg
state: present
when: ansible_distribution in ['Debian', 'Ubuntu']
- name: Add ISC BIND repository (Debian/Ubuntu)
apt_repository:
repo: "deb [signed-by=/etc/apt/keyrings/isc-archive-keyring.gpg] https://downloads.isc.org/isc/bind9/{{ bind_version }}/debian {{ ansible_distribution_release }} main"
state: present
filename: isc-bind
when: ansible_distribution in ['Debian', 'Ubuntu']
- name: Add APT pinning for ISC BIND packages
copy:
dest: /etc/apt/preferences.d/isc-bind
content: |
Package: bind9 bind9-*
Pin: origin downloads.isc.org
Pin-Priority: 600
mode: "0644"
when: ansible_distribution in ['Debian', 'Ubuntu']
when: ansible_os_family == "Debian"
- name: Setup ISC repository (RHEL/CentOS/AlmaLinux/Rocky)
block:
- name: Install EPEL release
package:
name: epel-release
state: present
when: ansible_os_family == "RedHat"
- name: Add ISC BIND repository (Enterprise Linux)
yum_repository:
name: isc-bind
description: ISC BIND {{ bind_version }} Repository
baseurl: "https://downloads.isc.org/isc/bind9/{{ bind_version }}/rhel/{{ ansible_distribution_major_version }}/$basearch"
gpgcheck: yes
gpgkey: "https://downloads.isc.org/isc/bind9/keys/9.18/KSK-2018.key"
enabled: yes
when: ansible_os_family == "RedHat"
when: ansible_os_family == "RedHat"
- name: Update package cache (Debian/Ubuntu)
apt:
update_cache: true
when: ansible_os_family == "Debian"
Create bind-install.yml:
---
- name: Install BIND DNS Server
hosts: bind_servers
become: true
gather_facts: true
vars:
bind_package: "{{ 'bind9' if ansible_os_family == 'Debian' else 'bind' }}"
bind_service: "{{ 'bind9' if ansible_os_family == 'Debian' else 'named' }}"
bind_config_dir: "{{ '/etc/bind' if ansible_os_family == 'Debian' else '/etc' }}"
bind_zone_dir: "{{ '/var/lib/bind' if ansible_os_family == 'Debian' else '/var/named' }}"
tasks:
- name: Update package cache (Debian/Ubuntu)
apt:
update_cache: yes
when: ansible_os_family == "Debian"
- name: Install BIND package
package:
name: "{{ bind_package }}"
state: present
- name: Install BIND utilities
package:
name:
- "{{ bind_package }}-utils"
- dnsutils
state: present
- name: Ensure BIND service is enabled and started
systemd:
name: "{{ bind_service }}"
state: started
enabled: true
daemon_reload: yes
- name: Verify BIND is running
command: named -v
register: bind_version
changed_when: false
- name: Display BIND version
debug:
msg: "BIND version: {{ bind_version.stdout }}"
Create bind-full-config.yml for a complete setup:
---
- name: Install and Configure BIND DNS Server
hosts: bind_servers
become: true
gather_facts: true
vars:
bind_package: "{{ 'bind9' if ansible_os_family == 'Debian' else 'bind' }}"
bind_service: "{{ 'bind9' if ansible_os_family == 'Debian' else 'named' }}"
bind_config_dir: "{{ '/etc/bind' if ansible_os_family == 'Debian' else '/etc' }}"
bind_zone_dir: "{{ '/var/lib/bind' if ansible_os_family == 'Debian' else '/var/named' }}"
# Configuration variables
bind_listen_on: ["127.0.0.1", "{{ ansible_default_ipv4.address }}"]
bind_allow_query: ["localhost", "192.168.0.0/16", "10.0.0.0/8"]
bind_forwarders:
- 8.8.8.8
- 8.8.4.4
bind_zones:
- name: "example.com"
type: "master"
file: "db.example.com"
allow_transfer: ["192.168.1.10", "192.168.1.11"]
tasks:
- name: Update package cache (Debian/Ubuntu)
apt:
update_cache: yes
when: ansible_os_family == "Debian"
- name: Install BIND package
package:
name: "{{ bind_package }}"
state: present
- name: Install BIND utilities (Debian/Ubuntu)
apt:
name:
- "{{ bind_package }}-utils"
- dnsutils
state: present
when: ansible_os_family == "Debian"
- name: Install BIND utilities (RHEL/CentOS)
dnf:
name:
- bind
- bind-utils
- bind-tools
state: present
when: ansible_os_family == "RedHat"
- name: Create zone directory if needed
file:
path: "{{ bind_zone_dir }}"
state: directory
owner: "{{ 'bind' if ansible_os_family == 'Debian' else 'named' }}"
group: "{{ 'bind' if ansible_os_family == 'Debian' else 'named' }}"
mode: '0755'
when: ansible_os_family == "Debian"
- name: Create log directory
file:
path: /var/log/named
state: directory
owner: "{{ 'bind' if ansible_os_family == 'Debian' else 'named' }}"
group: "{{ 'bind' if ansible_os_family == 'Debian' else 'named' }}"
mode: '0755'
- name: Configure BIND options
template:
src: named.conf.options.j2
dest: "{{ bind_config_dir }}/named.conf.options"
owner: root
group: root
mode: '0644'
notify: reload bind
- name: Configure local zones
template:
src: named.conf.local.j2
dest: "{{ bind_config_dir }}/named.conf.local"
owner: root
group: root
mode: '0644'
notify: reload bind
- name: Create zone files
template:
src: db.zone.j2
dest: "{{ bind_zone_dir }}/db.{{ item.name }}"
owner: "{{ 'bind' if ansible_os_family == 'Debian' else 'named' }}"
group: "{{ 'bind' if ansible_os_family == 'Debian' else 'named' }}"
mode: '0644'
loop: "{{ bind_zones }}"
notify: reload bind
- name: Validate BIND configuration
command: named-checkconf {{ bind_config_dir }}/named.conf
changed_when: false
- name: Ensure BIND service is enabled and started
systemd:
name: "{{ bind_service }}"
state: started
enabled: true
daemon_reload: yes
- name: Open DNS firewall ports (UFW)
community.general.ufw:
rule: allow
port: 53
proto: "{{ item }}"
loop:
- udp
- tcp
when: ansible_os_family == "Debian"
- name: Open DNS firewall ports (firewalld)
ansible.posix.firewalld:
service: dns
permanent: yes
state: enabled
immediate: yes
when: ansible_os_family == "RedHat"
- name: Verify BIND is running
command: named -v
register: bind_version
changed_when: false
- name: Display BIND version
debug:
msg: "BIND version: {{ bind_version.stdout }}"
handlers:
- name: reload bind
systemd:
name: "{{ bind_service }}"
state: reloaded
Create templates directory and the following template files:
templates/named.conf.options.j2:
options {
directory "{{ bind_zone_dir }}";
pid-file "/var/run/{{ bind_service }}/{{ bind_service }}.pid";
// Listen on specific interfaces
listen-on port 53 { {% for ip in bind_listen_on %}{{ ip }}; {% endfor %} };
listen-on-v6 port 53 { none; };
// Access controls
allow-query { {% for net in bind_allow_query %}{{ net }}; {% endfor %} };
allow-recursion { {% for net in bind_allow_query %}{{ net }}; {% endfor %} };
allow-query-cache { {% for net in bind_allow_query %}{{ net }}; {% endfor %} };
// Forwarders for recursive queries
forwarders {
{% for forwarder in bind_forwarders %}
{{ forwarder }};
{% endfor %}
};
// DNSSEC
dnssec-enable yes;
dnssec-validation auto;
// Logging
channel default_log {
file "/var/log/named/named.log" versions 10 size 10m;
severity info;
print-time yes;
print-category yes;
};
channel query_log {
file "/var/log/named/query.log" versions 5 size 5m;
severity info;
print-time yes;
};
logging {
category default { default_log; };
category queries { query_log; };
};
// Security
auth-nxdomain no;
recursion yes;
version "not currently available";
};
templates/named.conf.local.j2:
{% for zone in bind_zones %}
zone "{{ zone.name }}" {
type {{ zone.type }};
file "{{ zone.file }}";
{% if zone.allow_transfer %}
allow-transfer { {% for ip in zone.allow_transfer %}{{ ip }}; {% endfor %} };
{% endif %}
};
{% endfor %}
templates/db.zone.j2:
$TTL 86400
@ IN SOA ns1.{{ item.name }}. admin.{{ item.name }}. (
{{ '%Y%m%d01' | strftime }} ; Serial (YYYYMMDDNN format)
3600 ; Refresh
1800 ; Retry
1209600 ; Expire
86400 ) ; Negative Cache TTL
; Name servers
@ IN NS ns1.{{ item.name }}.
@ IN NS ns2.{{ item.name }}.
; A records
@ IN A 192.168.1.10
ns1 IN A 192.168.1.10
ns2 IN A 192.168.1.11
www IN A 192.168.1.20
mail IN A 192.168.1.30
; MX records
@ IN MX 10 mail.{{ item.name }}.
; TXT records
@ IN TXT "v=spf1 mx ~all"
Create an inventory file inventory.ini:
[bind_servers]
dns-primary ansible_host=192.168.1.10
dns-secondary ansible_host=192.168.1.11
[bind_servers:vars]
ansible_user=ubuntu
ansible_become=yes
Run the basic installation:
ansible-playbook -i inventory.ini bind-install.yml
Run the complete configuration:
ansible-playbook -i inventory.ini bind-full-config.yml
You can customize the playbook with additional variables:
# Performance tuning
bind_num_workers: 4
bind_max_cache_size: "256m"
# Security settings
bind_rate_limit:
responses_per_second: 5
window: 10
slip: 2
# Logging settings
bind_logging_level: info
bind_log_versions: 10
bind_log_size: "10m"
Add these tasks to verify the installation:
- name: Test DNS resolution
command: dig @127.0.0.1 google.com
register: dig_result
failed_when: "'NOERROR' not in dig_result.stdout"
- name: Check BIND status
command: rndc status
register: rndc_status
failed_when: rndc_status.rc != 0
when: bind_service == "named"
If BIND fails to start after configuration:
named-checkconfjournalctl -u bind9 or journalctl -u namedWe develop tailored automation solutions for:
Let’s discuss your requirements: office@linux-server-admin.com | Contact