loop 与 when — 增加的是数据,不是任务
一句话总结
不要复制同一个 task 生成十份,而应只保留一个 task,把数据写成十行。
为什么需要它
如果通过复制粘贴编写创建三个用户的 playbook,增加第四个用户时就会再复制一个 task。只要其中一个漏掉权限参数,该用户的配置就会不同,而代码看起来几乎完全一致,很难凭肉眼发现。
loop 消除了这种结构性风险。task 只有一个,遗漏参数的位置也只有一个;数据像表格一样排列,缺失字段一眼可见。
如何工作
loop 接收列表,把每个元素依次放入 item 后重复执行 task。列表元素若是字典,可用 item.name 访问字段。
when 表示条件。同时使用 loop 与 when 时,条件会对每个循环项分别求值。 因此,只处理满足条件的项,形成“条件循环”。
有几种组合需要留意。
| 工具 | 用途 | 陷阱 |
|---|---|---|
loop_control.label |
指定日志中的简短名称 | 未设置时,整个字典会暴露在日志中 |
until / retries / delay |
重试直到条件为真 | until 与 loop 一起使用较复杂 |
product 过滤器 |
两个列表的所有组合 | 组合数量按乘法增长 |
fileglob 查询 |
匹配模式的文件列表 | 不保证顺序 |
loop_control.label 也关系到安全。字典包含密码时,默认日志会原样输出。指定 label 或使用 no_log 更安全。
until 表达“等待直到成功”,常用于等待服务启动。但重试只会延后失败,不会消除失败。 设计次数与间隔时,必须同时判断“超过这个时间仍未成功,就是真正的问题”。
在实际项目中
第一,循环内的幂等性。 被循环的 task 也必须对每个项幂等。循环十次,非幂等造成的损害也会放大十倍。
第二,组合爆炸。 环境 3 × 组件 2 = 6 尚可,再乘以 5 个区域就变为 30。组合循环虽然方便,但执行时间与日志长度也会相乘。
第三,不依赖顺序。 文件模式查询不保证结果顺序。顺序重要时,应显式排序或使用固定列表。
loop 的形式与代价
# 목록을 그대로
- name: 패키지 설치
apt: {name: "{{ item }}", state: present}
loop: [git, curl, jq] # ❌ 태스크가 세 번 돈다
# 모듈이 목록을 받으면 한 번에
- name: 패키지 설치
apt: {name: [git, curl, jq], state: present} # ✅ apt 한 번
模块支持列表时,不要使用 loop。 apt、yum、pip 可以一次处理列表;用 loop 就会为每个软件包重新调用 apt,一百个包就是一百次。
遍历复杂数据时,用 loop_control 让输出更易读。
- name: 사용자 만들기
user: {name: "{{ item.name }}", groups: "{{ item.groups }}"}
loop: "{{ users }}"
loop_control:
label: "{{ item.name }}" # 로그에 딕셔너리 전체 대신 이름만
pause: 1 # 항목 사이 1초 — API 속도 제한이 있을 때
没有 label,日志会被整个字典填满;若包含 secret,还会原样泄露,因此这也是安全问题,通常应配合 no_log: true。
when 的求值时机
when 会对每个项分别求值,与 loop 一起使用时按项过滤。
- name: 프로덕션 호스트에만
copy: {src: prod.conf, dest: /etc/app.conf}
when: env == 'prod' # 호스트마다 평가
when 内不要使用 {{ }}。 它本身已经处于 Jinja2 表达式上下文,再包一层会出现警告,或被当成字符串求值。
when: env == 'prod' # ✅
when: "{{ env == 'prod' }}" # ❌ 되기는 하지만 경고
处理未定义变量时,先写 is defined。它像 Python 一样短路求值,后续条件因此安全。
when: extra_port is defined and extra_port | int > 1024
处理失败的三种方式
- command: /opt/check.sh
register: result
failed_when: result.rc not in [0, 2] # 2도 성공으로 본다
changed_when: false # 상태를 안 바꾸는 조회
ignore_errors: true # 실패해도 계속 (최후의 수단)
ignore_errors 很容易被滥用。忽略失败后,后续 task 会在错误前提上继续运行。 通常更合适的做法是用 failed_when 定义“什么才是真正失败”。
使用 block/rescue/always 可以像异常处理一样组织。
- block:
- command: /opt/migrate.sh
rescue:
- command: /opt/rollback.sh
- fail: {msg: "마이그레이션 실패 — 되돌렸습니다"}
always:
- command: /opt/unlock.sh
下一实验要做什么
遍历普通列表与字典列表创建目录,用 when 只处理符合条件的项;通过 loop_control.label 整理日志,并用 until 构建等待逻辑。再用两个列表的组合生成六个配置文件,通过模式重新收集,最后制作汇总报告。