本文系统讲解 Ansible Playbook 进阶三大核心:when 条件判断、loop 循环迭代、handlers 触发器机制。从生产实战角度出发,覆盖按 OS 分支安装、批量创建用户、配置变更后智能重启等高频场景,指出 when 列表 AND/OR 误区、register+loop 结果结构、handler 名字匹配等常见坑点,并给出完整可运行的 Nginx 部署实战工程。掌握这三者,playbook 才能从"能跑"升级为"高效、幂等、可维护"的工程化实践。
前言
刚入门 Ansible 的时候,大家都在学什么?ping、yum、template,一次性把所有 task 堆起来跑完。
但真正到生产环境,你会立刻撞上三个现实问题:
- 不同系统命令不一样 :CentOS 用
yum,Ubuntu 用apt,总不能写两份 playbook 吧? - 重复操作太多:批量加 20 个用户、下发 10 个配置文件,难道要写 20 个 task?
- 改了配置就要重启服务:但如果配置没变化,重启不就是白白浪费时间吗?
这三个问题,对应的正是 when、loop、handlers 这三件套。
说明:本文基于 Ansible 2.14+ 稳定版,语法规则官方长期稳定。
一、when:让 task 学会"看情况办事"
1.1 基本语法
when 接收一个布尔表达式,为 true 时执行该 task,为 false 时跳过。
新手最容易犯的错 :写 when 的时候不要加 {``{ }}。Ansible 会自动把表达式当 Jinja2 求值,加了花括号反而报错。
看个最基础的例子:
yaml
- name: 在 CentOS 上安装 nginx
yum:
name: nginx
state: present
when: ansible_distribution == "CentOS"
注意看:when 后面直接写表达式,不需要花括号。
1.2 最常用的五种判断场景
场景一:按操作系统分支
生产环境最常见的需求------一套 playbook 同时跑 CentOS 和 Ubuntu:
yaml
- name: Debian/Ubuntu 系安装 nginx
apt:
name: nginx
update_cache: yes
when: ansible_distribution in ["Debian", "Ubuntu"]
- name: RHEL/CentOS 系安装 nginx
yum:
name: nginx
when: ansible_distribution in ["CentOS", "RedHat", "Rocky"]
这里用了 in 关键字判断成员关系,比写三个 or 干净得多。
场景二:判断变量是否定义
yaml
- name: 如果定义了 deploy_user 才创建用户
user:
name: "{{ deploy_user }}"
state: present
when: deploy_user is defined
反过来判断"未定义"用 is undefined,或者 is not defined。
场景三:根据上一步结果决定下一步
register 注册变量 + when 条件判断,是 Ansible 里最常见的组合拳:
yaml
- name: 检测配置文件是否存在
stat:
path: /etc/myapp/config.yaml
register: config_file
- name: 文件不存在时才执行初始化
debug:
msg: "首次部署,执行初始化流程"
when: not config_file.stat.exists
✅ 结论:stat 模块的结果在 .stat.exists 里,取的时候不要漏了 .stat。
场景四:按主机名或组过滤
yaml
- name: 只在 db 组的机器上执行数据库备份
command: /opt/backup_db.sh
when: "'db' in group_names"
group_names 是内置变量,存了当前主机所属的所有组名。
场景五:带条件的变量默认值
简单分支用 Jinja2 内联表达式就够了:
yaml
- name: 根据系统版本设置包名
set_fact:
web_package: >-
{{ 'httpd' if ansible_distribution_major_version | int >= 8 else 'nginx' }}
一句话口诀 :简单分支用内联 if-else,复杂分支老老实实写两个 task 加 when。
1.3 多条件组合:列表是 AND,不是 OR
yaml
- name: CentOS 7 且是 x86_64 才执行
shell: echo "old system"
when:
- ansible_distribution == "CentOS"
- ansible_distribution_major_version == "7"
- ansible_architecture == "x86_64"
坑点提醒 :当 when 后面接一个列表时,列表里的多个条件是 AND 关系(全部满足才执行)。这是官方推荐写法,可读性比 and 拼一长串好很多。
如果要表达 OR,就得自己写:
yaml
when: (ansible_distribution == "CentOS") or (ansible_distribution == "Ubuntu")
二、loop:告别复制粘贴
2.1 基本循环(loop 替代 with_items)
Ansible 2.5 之后官方推荐用 loop 替代老的 with_items,写法非常直观:
yaml
- name: 批量创建多个用户
user:
name: "{{ item }}"
state: present
groups: "sudo"
loop:
- alice
- bob
- charlie
item 是循环内置变量,每轮自动取列表里的一个元素。
2.2 字典列表:实际工作中 90% 的场景
光循环字符串太简单了,真实场景每个元素都带一堆属性:
yaml
- name: 批量创建带不同属性的用户
user:
name: "{{ item.name }}"
uid: "{{ item.uid }}"
shell: "{{ item.shell }}"
groups: "{{ item.groups | default('users') }}"
loop:
- name: alice
uid: 1001
shell: /bin/bash
groups: sudo
- name: bob
uid: 1002
shell: /bin/zsh
- name: charlie
uid: 1003
shell: /bin/bash
groups: www-data
注意这里用了 item.groups | default('users'),如果某个字典没写 groups,就用默认值兜底,避免报错。
2.3 register + loop:结果是列表,不是单个
这是进阶技巧,经常和 command/shell 模块配合:
yaml
- name: 批量执行命令并收集结果
shell: "cat /etc/{{ item }}"
register: file_contents
loop:
- hostname
- hosts
- os-release
- name: 打印每个文件的内容
debug:
msg: "{{ item.item }} => {{ item.stdout }}"
loop: "{{ file_contents.results }}"
坑点提醒 :这里有两个 item------
- 外层
item.item:原始循环输入 - 内层
item.stdout:该轮命令的输出
register 配合循环时,结果会被包成一个 results 列表,这个结构新手经常搞混。
2.4 until:带重试的循环
某些操作(比如等待端口起来、等待接口就绪)不能一次成功,要轮询:
yaml
- name: 轮询直到接口返回 200
uri:
url: http://localhost:8080/health
status_code: 200
register: health_check
until: health_check.status == 200
retries: 10
delay: 5
until 的逻辑:执行 task → 判断表达式是否为真 → 为假就 delay 秒后重试 → 最多 retries 次。
2.5 label:别让循环输出刷屏
循环跑 50 个用户,Ansible 默认打印 50 行 ok: [host] => (item=alice),根本没法看。加个 label:
yaml
- name: 批量创建用户(精简输出)
user:
name: "{{ item.name }}"
state: present
loop:
- { name: alice, uid: 1001 }
- { name: bob, uid: 1002 }
loop_control:
label: "用户 {{ item.name }} (uid={{ item.uid }})"
输出就变成 ok: [web1] => 用户 alice (uid=1001),一眼能看懂。
三、handlers:只在需要的时候重启服务
3.1 核心思想
handlers 是 Ansible 里最优雅的设计之一:只有 task 报告 changed 状态时,对应的 handler 才会在 play 末尾执行。
拆开看有三个关键信息:
- 只有 changed 才触发:配置文件没变化,handler 不跑,服务不会重启。
- 在 play 末尾执行:不是 task 一变就立刻重启,而是等所有 task 跑完统一执行。
- 通过 notify 关联 :task 里
notifyhandler 的名字,建立触发关系。
3.2 最基础的例子
yaml
- name: 下发 nginx 配置文件
template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
notify: restart nginx
handlers:
- name: restart nginx
service:
name: nginx
state: restarted
运行流程:
- Ansible 对比模板和目标文件,内容一样 → 输出
ok,不触发 notify。 - 内容变了 → 输出
changed,标记 "restart nginx" 需要执行。 - 整个 play 所有 task 跑完后,统一执行被标记的 handler。
3.3 一个 handler 被 notify 多次怎么办?
假设你有两个 task 都 notify: restart nginx,而且两个都 changed 了,handler 会执行几次?
✅ 结论:一次。
Ansible 会自动去重。哪怕同一个 handler 在一个 play 里被 notify 了 5 次,它也只在最后执行一次。这是设计好的行为------你不需要担心重复重启。
3.4 listen:给 handler 起别名
handler 默认靠名字匹配,名字改了所有 notify 都得改。用 listen 给它起组名:
yaml
handlers:
- name: restart nginx service
listen: "web service changed"
service:
name: nginx
state: restarted
- name: reload nginx config
listen: "web service changed"
service:
name: nginx
state: reloaded
然后 task 里只需要:
yaml
- name: 下发配置
template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
notify: "web service changed"
✅ 结论:不管 handler 内部叫什么名字、以后是重启还是 reload,task 都不用改。适合团队协作。
3.5 强制立即执行:meta: flush_handlers
默认 handler 在 play 末尾执行,但有时候你改完配置必须立刻重启,后面的 task 还依赖这个服务已经起来了:
yaml
- name: 下发配置
template:
src: app.conf.j2
dest: /etc/app.conf
notify: restart app
- name: 立刻执行所有已 notify 的 handler
meta: flush_handlers
- name: 后续依赖 app 已重启的操作
uri:
url: http://localhost:8080/health
meta: flush_handlers 会在这个位置立即执行所有被 notify 但还没跑的 handler,执行完再继续往下走。
四、综合实战:用三件套部署一台 Nginx
光看片段不够,来一个完整可跑的例子,把三个关键字全部串起来。
yaml
---
- name: 部署 Nginx Web 服务
hosts: web_servers
become: yes
vars:
nginx_work_connections: 1024
nginx_server_name: "demo.example.com"
allowed_users:
- alice
- bob
tasks:
- name: 按系统安装 nginx
package:
name: nginx
state: present
when: ansible_os_family in ["RedHat", "Debian"]
- name: 批量创建运维账号
user:
name: "{{ item }}"
shell: /bin/bash
groups: nginx
loop: "{{ allowed_users }}"
loop_control:
label: "创建账号 {{ item }}"
- name: 下发 nginx 主配置
template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
owner: root
group: root
mode: "0644"
notify: reload nginx
- name: 下发站点配置
template:
src: site.conf.j2
dest: /etc/nginx/conf.d/demo.conf
notify: reload nginx
- name: 确保 nginx 开机自启
service:
name: nginx
enabled: yes
state: started
handlers:
- name: reload nginx
service:
name: nginx
state: reloaded
这个 playbook 干了什么:
| 关键字 | 用在哪 | 解决什么问题 |
|---|---|---|
when |
安装包那一步 | CentOS/Ubuntu 都能跑,不用写两份 |
loop |
创建用户那一步 | 加人只改 allowed_users 列表,不动 task |
handlers |
配置下发那一步 | 配置没变就不 reload,服务不中断 |
五、常见坑点速查
- when 里不要写
{``{ }}:写了反而报错,Ansible 会自动求值。 - loop 里的 item 是模板变量 :字符串直接用
{``{ item }},字典用{``{ item.key }}。 - handlers 只在 play 末尾跑 :中间需要立即执行就加
meta: flush_handlers。 - handler 名字要和 notify 完全一致:大小写、空格、标点都不能差,差了就静默不触发。
- register + loop 的结果是列表 :取每个元素要用
.results,不是直接.stdout。 - when 列表是 AND 不是 OR :想表达"满足其一"得自己用
or拼。 - handlers 失败会中断 play:重启服务失败后面的 task 都不跑了,排查时注意。
六、结语
when、loop、handlers 这三个东西,语法半天就能学会,但真正用得顺手需要踩几个月坑。
我的建议:
- 先把
when的系统判断、变量判断用熟,所有 playbook 起步就加 OS 分支。 - 再把
loop用起来,能循环的绝不写第二个 task。 - 最后把所有"改配置 → 重启服务"的模式全部改成
notify+handlers,这是生产环境稳定性的底线。
Ansible 的哲学是"简单、可预测、幂等",这三个关键字正是这个哲学的载体。用熟了之后你会发现,playbook 写起来越来越像写代码,而不是堆命令。
如果觉得有用,欢迎点赞收藏,后续会继续更新 Ansible 进阶系列(roles 拆分、变量优先级、动态 inventory)。