DNS解析服务看起来简单,实际维护起来却相当琐碎。每新增一台服务器要加A记录,每个业务域名要配CNAME,改完还得手动检查语法、重启服务、验证解析结果。一旦机器多了、环境多了,手工操作几乎必然出现配置不一致的情况。把DNS配置交给Ansible管理,是解决这类问题比较成熟的思路:配置以文件形式存放,变更通过剧本下发,执行结果幂等可重复,整个过程天然留有审计记录。

为什么DNS配置适合用Ansible管理
DNS配置本质上是纯文本文件的集合,zone文件、named.conf、resolv.conf,全部都是可以模板化的内容,这正是Ansible最擅长的领域。用copy或template模块下发文件,用service模块控制named服务,用command模块调用named-checkzone做语法校验,整条链路都能被playbook覆盖。
另一个重要原因是幂等性。手工改配置时,重复执行同一次修改可能导致重复的记录条目;而Ansible的任务在设计良好的情况下,无论执行多少遍,最终状态都一致。这对于DNS这种基础服务尤为关键——一条重复或错乱的记录可能影响整个业务的域名解析。
此外,playbook本身可以作为配置的历史档案。每次变更走Git提交,出问题时能快速定位是哪次提交引入的,回滚也只是checkout到上一个版本再执行一次,比手工改回配置可靠得多。
基础实现:批量分发zone文件并自动reload
先看一个最直接的场景:管理BIND的正向解析zone文件。我们用template模块渲染zone内容,并在文件变化时触发reload。目录结构建议按role组织:
roles/
dns/
tasks/main.yml
handlers/main.yml
templates/db.ippipp.com.j2
files/named.conf.optionstasks中的核心任务如下:
- name: 安装BIND
ansible.builtin.package:
name: bind9
state: present
- name: 下发zone配置文件
ansible.builtin.template:
src: db.ippipp.com.j2
dest: /etc/bind/db.ippipp.com
owner: root
group: bind
mode: '0644'
notify: reload bind
- name: 确保named运行中
ansible.builtin.service:
name: named
state: started
enabled: truehandler写在handlers/main.yml中,注意handler只在被notify且任务实际产生变更时才执行:
- name: reload bind
ansible.builtin.service:
name: named
state: reloaded这里有个容易踩的坑:不要用state: restarted代替reloaded。BIND重启会清空缓存,reload则只重新加载配置,对线上服务更友好。另外,模板文件中的序列号必须每次变更时递增,否则主从同步不会生效,后面会讲如何用Jinja2自动处理。
模板化与环境变量分离
不同环境的解析记录通常有差异,比如测试环境的api域名指向测试机房,生产环境指向另一组IP。如果为每个环境维护一份独立的zone文件,后续合并维护成本很高。正确做法是用一个模板加多份变量文件。
模板文件db.ippipp.com.j2中,解析记录从变量读取:
$TTL 3600
@ IN SOA ns1.ippipp.com. admin.ippipp.com. (
{{ serial }} ; Serial
3600 ; Refresh
900 ; Retry
604800 ; Expire
86400 ) ; Minimum
@ IN NS ns1.ippipp.com.
ns1 IN A {{ nameserver_ip }}
{% for host in web_servers %}
{{ host.name }} IN A {{ host.ip }}
{% endfor %}变量按环境存放:
# group_vars/prod.yml
nameserver_ip: 10.0.1.53
web_servers:
- { name: web01, ip: 10.0.1.11 }
- { name: web02, ip: 10.0.1.12 }
# group_vars/test.yml
nameserver_ip: 192.168.1.53
web_servers:
- { name: web01, ip: 192.168.1.11 }序列号推荐用Ansible在渲染时自动生成,例如用执行时间戳:
- name: 设置序列号
ansible.builtin.set_fact:
serial: "{{ lookup('pipe', 'date +%Y%m%d01') }}"这样每次发版变更,序列号自然变化,从节点同步就能正常触发,不需要人工记得改数字。
变更前校验:避免把错误配置推上线
DNS配置出错影响面大,语法校验必须在reload之前完成。BIND自带两个很好用的工具:named-checkconf检查主配置,named-checkzone检查zone文件。可以在playbook中加入校验步骤,校验失败则中止执行:
- name: 校验zone文件语法
ansible.builtin.command: named-checkzone ippipp.com /etc/bind/db.ippipp.com
register: zone_check
changed_when: false
- name: 校验失败则中止
ansible.builtin.fail:
msg: "zone文件语法错误,请检查"
when: zone_check.rc != 0更优雅的做法是利用template模块的validate参数,在文件落地前就完成校验,校验失败文件不会写入目标路径:
- name: 下发并校验zone文件
ansible.builtin.template:
src: db.ippipp.com.j2
dest: /etc/bind/db.ippipp.com
validate: "named-checkzone ippipp.com %s"注意validate中的%s代表临时文件路径,这个机制能保证线上永远不存在一份语法错误的配置文件。对于客户端的resolv.conf,如果只是简单场景,也可以直接用template分发;如果需要保留手工添加的条目,则要考虑用lineinfile做增量修改,避免覆盖管理员临时加的排查用记录。
轻量场景:用dnsmasq做内网解析
并非所有场景都需要上BIND。内网几十台机器、只需要一些自定义域名映射时,dnsmasq更轻便。它的配置同样适合Ansible管理,核心是维护一份hosts条目列表:
- name: 渲染dnsmasq自定义解析
ansible.builtin.template:
src: dnsmasq.addresses.conf.j2
dest: /etc/dnsmasq.d/addresses.conf
notify: restart dnsmasq模板中循环生成address记录:
{% for item in dns_entries %}
address=/{{ item.domain }}/{{ item.ip }}
{% endfor %}dnsmasq改配置后需要restart而非reload,这一点和BIND不同,写handler时别搞混。另外dnsmasq可以作为缓存服务器,把上游DNS指向公共解析,内网查询延迟能明显下降,这部分配置也可以一并用Ansible统一分发。
小结与实践建议
把DNS配置纳入Ansible管理后,日常加一条解析记录的流程变成:修改变量文件、提交Git、执行playbook、观察变更报告,全程几十秒完成且可追溯。实践中有几点经验值得注意:第一,zone文件权限要收紧,避免被普通用户读取到内网拓扑;第二,主从架构下先推从节点再推主节点,可以减少解析抖动;第三,重要变更前用--check模式做 dry run,先看diff再实际执行。
如果团队规模较大,还可以在此基础上接入CI流水线,把playbook的执行交给流水线触发,变更走审批流程,DNS这个基础设施组件就真正实现了代码化、可审计的运维方式。