playbook 跑完显示绿色,WireGuard unit 处于 active,握手正常,新的 peer 却依然连不上任何东西。看着 playbook 报告成功、服务器却仍在用昨天的配置,是一种特别的挫败,因为输出里没有任何一处是错的。Ansible 做的正是被要求的事:确保 unit 处于启动状态,而它本来就已启动。应用一份重写后的配置文件是另一件事,只有某些东西让服务重新读取它时才会发生。

看第二次运行到底报告了什么

保留改动后的配置再跑一遍 playbook,对比 summary。第二次运行时,unit 相关任务应显示为 ok 而不是 changed,因为已在运行的服务按设计不会被触碰。这就是幂等性在正确工作,也正是新文件永远到不了内核的原因。

加一次带 diff 输出的检查运行,看看究竟哪些任务会碰到东西。

ansible-playbook -i inventory site.yml --check --diff
systemctl show wg-quick@wg0 -p ActiveEnterTimestamp -p NRestarts
stat -c '%y %n' /etc/wireguard/wg0.conf

三个时间戳在你读第一行日志之前就把故事讲完了。如果配置文件比 unit 变为 active 的时刻更新,运行中的接口就是用旧文件构建的,之后所有症状都由此而来。

这不是 Ansible 的缺陷。任何把「确保它在运行」与「确保它在运行这一份」分开的配置管理工具都有同样表现。这个区分看似吹毛求疵,直到它让你搭进一个下午。

已启动是状态,不是重载

state: started 断言的是 unit 的状态。它并不承诺服务会重新读取输入,也无从得知一个它从未打开过的文件已经改变。在 wg-quick 部署中,输入包括接口配置、地址、路由、DNS 记录以及任何 post-up 钩子。

所以更新后的文件可以包含新的 peer、不同的地址或改动的路由指令,而运行中的接口仍在使用旧集合。握手依然成功,因为已加载的密钥与端点仍然有效。正是这一点让故障难以察觉,也是人们先追一小时 DNS 才想到去看时间戳的原因。

另外两个模块选项常被误当成解决办法。daemon_reload 让 systemd 重新读取 unit 文件,这与 WireGuard 配置文件无关。state: reloaded 请求 unit 执行它的 reload 动作,而它是否做到你需要的,完全取决于该 unit。两者都代替不了弄清运行中的进程究竟读了哪些输入。

先安装文件,再通知 handler

既保持幂等又能应用改动的写法很普通:写文件的任务通知 handler,只有文件真的变了 handler 才运行。没有改动的第二次运行什么也不做,这正是你要的。

- name: write the interface configuration
  ansible.builtin.template:
    src: wg0.conf.j2
    dest: /etc/wireguard/wg0.conf
    owner: root
    mode: "0600"
  notify: apply wg0

- name: ensure the interface unit is enabled
  ansible.builtin.systemd_service:
    name: wg-quick@wg0
    enabled: true
    state: started

- name: apply wg0
  listen: apply wg0
  ansible.builtin.systemd_service:
    name: wg-quick@wg0
    state: restarted

handler 在 play 结尾、通知它的任务之后运行,所以同一 play 中靠后的任务可能仍看到旧接口。如果顺序重要,就在依赖新配置的任务之前用 meta: flush_handlers 显式清空 handler。

除非已经验证该 unit 的 reload 动作,否则用 restart 而不是 reload。上游 wg-quick unit 确实支持 reload,并会同步 WireGuard 配置,但它不会重新执行地址、路由、DNS 或钩子的设置。因此在新增路由指令后执行 reload 看似成功,却什么都没变。

把运行中的设备与文件做对比

真正终结这类缺陷的检查,是把内核里的状态与文件所写的内容做对比。两条命令给出可比较的输出,差异就是漂移。

sudo wg showconf wg0
sudo wg-quick strip wg0
sudo diff <(sudo wg showconf wg0) <(sudo wg-quick strip wg0)

wg showconf 会打印运行中的设备,包含私钥,所以要把这份输出当作机密,不要贴进工单。wg-quick strip 打印的是去掉 wg-quick 专有键之后的文件,这正是两者可以比较的原因。

peer 列表有差异,说明文件从未被应用。差异只出现在你预期会变的密钥或端点上,说明有东西只应用了一部分。完全没有差异,说明配置是最新的、故障在别处,这本身就是有用的结果。

有意识地支付重启的代价

重启接口会断开隧道,而如果这条隧道承载着你的管理会话,重启就会结束那个请求它的会话。把 handler 放在变更窗口之后,或者从不依赖接口重启的管理路径运行 playbook。

代价正是人们转向 reload 的原因。这个直觉合理,一旦你知道 reload 真正应用哪些改动,它就是对的。对上游 wg-quick unit 来说,peer 与密钥改动足够;对地址、路由和钩子则不够。把这一点弄清楚一次,比在变更窗口里才发现要便宜。

systemd_service 模块文档 讲清了 started、restarted 与 reloaded 的区别,值得读一次,而不是从绿色 summary 里推断。本文的一切都来自那一个区分。

相关文章