Ansible Playbook 进阶:when 条件、loop 循环、handlers 触发器落地

本文系统讲解 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 末尾执行。

拆开看有三个关键信息:

  1. 只有 changed 才触发:配置文件没变化,handler 不跑,服务不会重启。
  2. 在 play 末尾执行:不是 task 一变就立刻重启,而是等所有 task 跑完统一执行。
  3. 通过 notify 关联 :task 里 notify handler 的名字,建立触发关系。

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

运行流程:

  1. Ansible 对比模板和目标文件,内容一样 → 输出 ok,不触发 notify。
  2. 内容变了 → 输出 changed,标记 "restart nginx" 需要执行。
  3. 整个 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,服务不中断

五、常见坑点速查

  1. when 里不要写 {``{ }}:写了反而报错,Ansible 会自动求值。
  2. loop 里的 item 是模板变量 :字符串直接用 {``{ item }},字典用 {``{ item.key }}。
  3. handlers 只在 play 末尾跑 :中间需要立即执行就加 meta: flush_handlers。
  4. handler 名字要和 notify 完全一致:大小写、空格、标点都不能差,差了就静默不触发。
  5. register + loop 的结果是列表 :取每个元素要用 .results,不是直接 .stdout。
  6. when 列表是 AND 不是 OR :想表达"满足其一"得自己用 or 拼。
  7. handlers 失败会中断 play:重启服务失败后面的 task 都不跑了,排查时注意。

六、结语

when、loop、handlers 这三个东西,语法半天就能学会,但真正用得顺手需要踩几个月坑。

我的建议:

  • 先把 when 的系统判断、变量判断用熟,所有 playbook 起步就加 OS 分支。
  • 再把 loop 用起来,能循环的绝不写第二个 task。
  • 最后把所有"改配置 → 重启服务"的模式全部改成 notify + handlers,这是生产环境稳定性的底线。

Ansible 的哲学是"简单、可预测、幂等",这三个关键字正是这个哲学的载体。用熟了之后你会发现,playbook 写起来越来越像写代码,而不是堆命令。


如果觉得有用,欢迎点赞收藏,后续会继续更新 Ansible 进阶系列(roles 拆分、变量优先级、动态 inventory)。

相关推荐
无足鸟ICT2 小时前
【RHCA+】type命令(判断命令类型)
linux·ssh·bash
咸鱼梦想家π2 小时前
Linux开发工具(下)
linux·运维·服务器
LHICOD2 小时前
Linux-1
linux·ip·vmware·虚拟机·cmd·ping
丰锋ff4 小时前
用户管理命令
linux·运维·服务器
刚入门的大一新生4 小时前
Linux-线程概念
linux·jvm
H.莓飛4 小时前
【数据结构】排序总结与规模实测
linux·服务器·开发语言·数据结构·visual studio·1024程序员节
Zenova EdgeOS5 小时前
Linux 日志实战:用 journalctl 精准检索与治理工业边缘日志
linux·运维·chrome
Dxy12393102165 小时前
Linux 压缩解压相关命令介绍
linux·运维·服务器
eleven_4u5 小时前
QPS如何计算,如何部署架构
java·linux·运维·jvm·中间件