Playbook 核心
上一章已用 ad-hoc 把常用模块跑通。本章把这些模块编排进 Playbook:掌握 play / task / handler / when / loop / tags / register,写出可读、可部分执行、失败行为可预期的变更剧本。
Playbook 是编排层:描述「对谁、以什么权限、按什么顺序、调用哪些模块」。可复用的安装逻辑将在 Role 章拆出;本章吃透执行模型。
1. Play 的结构
playbooks/baseline.yml:
---
- name: Web 基线信息
hosts: web
gather_facts: true
become: true
serial: "100%" # 本章先全开;滚动在第 6 章细讲
vars:
project: rootwiki
tasks:
- name: 显示发行版
ansible.builtin.debug:
msg: "{{ project }} on {{ inventory_hostname }} => {{ ansible_distribution }} {{ ansible_distribution_version }}"
ansible-playbook playbooks/baseline.yml
ansible-playbook playbooks/baseline.yml -l web1
ansible-playbook playbooks/baseline.yml --check
| 字段 | 含义 |
|---|---|
name | play 名称,日志与 AWX 里都靠它辨认 |
hosts | 组名、主机名、all、模式匹配 |
gather_facts | 是否收集 facts;纯下发静态文件有时可 false 加速 |
become | 本 play 是否提权 |
vars | play 级变量(不如 group_vars 适合长期配置) |
一份 YAML 里可以有多个 play(先 all 基线,再 web,再 db),按顺序执行。
2. Task 编写规范
好的 task
- name: 确保应用目录存在
ansible.builtin.file:
path: /data/{{ project }}
state: directory
owner: root
group: root
mode: "0755"
建议遵守
- 必写
name:失败日志才好看 - 用 FQCN:
ansible.builtin.file而不是裸file - 一件事一条 task:别在一个
shell里串十步 - 能模块化绝不用 shell
常见执行控制
- name: 仅首次创建标记文件
ansible.builtin.file:
path: /var/lib/myapp/bootstrapped
state: touch
modification_time: preserve
access_time: preserve
file 模块不支持 creates;上例在文件首次不存在时创建它,之后因时间设为 preserve 而保持幂等。对于“只在首次创建后执行初始化脚本”的场景,使用下面的 stat + when。
更通用的「先查再改」:
- name: 检查标记
ansible.builtin.stat:
path: /var/lib/myapp/bootstrapped
register: boot_flag
- name: 初始化动作
ansible.builtin.command: /usr/local/bin/bootstrap.sh
when: not boot_flag.stat.exists
notify: Touch bootstrap flag
3. Handler:有变更才重启
错误示范(每次都重启):
- name: 下发配置
ansible.builtin.copy:
src: nginx.conf
dest: /etc/nginx/nginx.conf
- name: 重启
ansible.builtin.service:
name: nginx
state: restarted
正确示范:
tasks:
- name: 下发 nginx 配置
ansible.builtin.template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
mode: "0644"
validate: "/usr/sbin/nginx -t -c %s"
notify: Reload nginx
- name: 下发站点页
ansible.builtin.copy:
content: "<h1>{{ inventory_hostname }}</h1>\n"
dest: /var/www/html/index.html
mode: "0644"
# 静态页变更通常不必 reload;按需 notify
handlers:
- name: Reload nginx
ansible.builtin.service:
name: nginx
state: reloaded
机制要点:
- 只有 task 报告 changed 才会 notify
- 同一 handler 被 notify 多次,默认 play 结束时 只跑一次
listen:可让多个 handler 名映射到同一事件(进阶)meta: flush_handlers可强制提前执行(滚动发布中间验证时有用)
- name: 立刻执行已通知的 handler
ansible.builtin.meta: flush_handlers
- name: 健康检查
ansible.builtin.uri:
url: "http://127.0.0.1:{{ http_port }}/"
status_code: 200
4. 条件 when
- name: Debian 家族用 apt
ansible.builtin.apt:
name: nginx
state: present
update_cache: true
when: ansible_os_family == "Debian"
- name: RedHat 家族用 dnf
ansible.builtin.dnf:
name: nginx
state: present
when: ansible_os_family == "RedHat"
多条件(隐式 AND):
when:
- http_port | int >= 1024
- "'web' in group_names"
- ansible_memtotal_mb | int >= 1024
OR 要用 Jinja:
when: ansible_os_family == "Debian" or ansible_os_family == "RedHat"
对「命令结果」做条件:
- name: 查服务状态
ansible.builtin.command: systemctl is-active nginx
register: nginx_active
changed_when: false
failed_when: false
- name: 仅在未运行时启动
ansible.builtin.service:
name: nginx
state: started
when: nginx_active.stdout != "active"
上面「先 is-active 再 start」只为演示 register。真实场景直接 service: state=started 即可,模块自己保证幂等。
5. 循环 loop
列表
- name: 创建数据子目录
ansible.builtin.file:
path: "/data/app/{{ item }}"
state: directory
mode: "0755"
loop:
- bin
- conf
- logs
- data
字典列表
- name: 创建系统用户
ansible.builtin.user:
name: "{{ item.name }}"
uid: "{{ item.uid | default(omit) }}"
shell: "{{ item.shell | default('/bin/bash') }}"
system: "{{ item.system | default(false) }}"
loop:
- { name: app, system: true, shell: /usr/sbin/nologin }
- { name: deploy, uid: 1100 }
与 until 重试(等待服务就绪)
- name: 等待本地端口
ansible.builtin.wait_for:
port: "{{ http_port }}"
host: 127.0.0.1
delay: 2
timeout: 60
或:
- name: 轮询健康检查
ansible.builtin.uri:
url: "http://127.0.0.1:{{ http_port }}/healthz"
status_code: 200
register: health
until: health.status == 200
retries: 10
delay: 3
6. 标签 tags:只跑一部分
大型 playbook 必须打标签,否则人人都不敢动:
- name: 安装软件包
ansible.builtin.apt:
name: "{{ nginx_packages }}"
state: present
tags: [packages, nginx]
- name: 渲染配置
ansible.builtin.template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
validate: "/usr/sbin/nginx -t -c %s"
tags: [config, nginx]
notify: Reload nginx
- name: 部署静态资源
ansible.builtin.copy:
src: "{{ item }}"
dest: "{{ app_root }}/{{ item | basename }}"
loop: "{{ query('fileglob', 'files/static/*') }}"
tags: [content, nginx]
# 只改配置
ansible-playbook playbooks/web.yml --tags config
# 跳过装包(内网已镜像时)
ansible-playbook playbooks/web.yml --skip-tags packages
# 列出标签
ansible-playbook playbooks/web.yml --list-tags
ansible-playbook playbooks/web.yml --list-tasks
特殊标签:
always:除非--skip-tags always,否则总执行never:除非显式--tags never(或包含它的标签),否则不执行 —— 适合危险任务
- name: 危险:卸载 nginx
ansible.builtin.apt:
name: nginx
state: absent
tags: [never, destroy]
7. register、failed_when、changed_when
- name: 语法检查(示例)
ansible.builtin.command: nginx -t
register: nginx_test
changed_when: false
failed_when:
- nginx_test.rc != 0
- "'syntax is ok' not in nginx_test.stderr"
| 指令 | 用途 |
|---|---|
register | 保存结果给后续 task |
changed_when | 纠正「到底有没有改世界」 |
failed_when | 纠正「什么叫失败」 |
ignore_errors | 粗暴忽略;优先用 failed_when: false + 明确处理 |
8. 块 block / rescue / always
适合「尝试 → 失败补偿 → 收尾」:
- name: 更新配置并验证
block:
- name: 检查当前配置是否存在
ansible.builtin.stat:
path: /etc/myapp/app.conf
register: current_conf
- name: 保存本次变更前的配置
ansible.builtin.copy:
src: /etc/myapp/app.conf
dest: /etc/myapp/app.conf.ansible-before-change
remote_src: true
mode: preserve
when: current_conf.stat.exists
- name: 写配置
ansible.builtin.template:
src: app.conf.j2
dest: /etc/myapp/app.conf
notify: Restart myapp
- name: flush
ansible.builtin.meta: flush_handlers
- name: 健康检查
ansible.builtin.uri:
url: http://127.0.0.1:8080/healthz
status_code: 200
rescue:
- name: 回退到备份配置
ansible.builtin.copy:
src: /etc/myapp/app.conf.ansible-before-change
dest: /etc/myapp/app.conf
remote_src: true
when: current_conf.stat.exists
notify: Restart myapp
- name: 立即应用回退后的配置
ansible.builtin.meta: flush_handlers
- name: 没有旧配置时明确停止
ansible.builtin.fail:
msg: "变更前没有 app.conf,无法自动回退;请按发布流程处理"
when: not current_conf.stat.exists
always:
- name: 留下审计标记
ansible.builtin.debug:
msg: "change window finished on {{ inventory_hostname }}"
这里先复制出固定名称的变更前文件,因此 rescue 有可恢复的目标。backup: true 生成的是带时间戳的文件名,不能假设它叫 .bak。这不能替代 Git 回滚,但能表达本地补偿路径;首次创建配置时没有旧版本,应停止并按 Git/发布流程回退。
9. 完整示例:可标签化的 web 预热 playbook
playbooks/web_intro.yml:
---
- name: Web 预热(无 Role 版)
hosts: web
become: true
vars:
http_port: 80
app_root: /var/www/html
tasks:
- name: 安装 nginx(Debian)
ansible.builtin.apt:
name: nginx
state: present
update_cache: true
when: ansible_os_family == "Debian"
tags: [packages]
- name: 站点目录
ansible.builtin.file:
path: "{{ app_root }}"
state: directory
mode: "0755"
tags: [config, content]
- name: 首页
ansible.builtin.copy:
content: |
<h1>{{ inventory_hostname }}</h1>
<p>port={{ http_port }}</p>
dest: "{{ app_root }}/index.html"
mode: "0644"
tags: [content]
- name: 简单配置(演示,生产用 template)
ansible.builtin.copy:
dest: /etc/nginx/conf.d/demo.conf
mode: "0644"
content: |
server {
listen {{ http_port }};
server_name _;
root {{ app_root }};
location / { try_files $uri $uri/ =404; }
}
notify: Reload nginx
tags: [config]
- name: 确保服务运行
ansible.builtin.service:
name: nginx
state: started
enabled: true
tags: [services]
handlers:
- name: Reload nginx
ansible.builtin.service:
name: nginx
state: reloaded
ansible-playbook playbooks/web_intro.yml --tags content,config
10. 练习与验收
练习 A
把「首页内容」改成来自 group_vars,连续执行两次 playbook,第二次不应再 changed(copy content 相同则 ok)。
练习 B
为 packages/config/content 打标签;练习 --tags 与 --skip-tags。
练习 C
故意把配置写成非法 nginx 语法并加 validate,确认 task 失败且不会落地坏文件(template/copy+validate 行为)。
验收清单
- 理解 changed 与 handler 的关系
- 会用 when/loop/tags
- 会用 register + changed_when 处理只读命令
- 能读懂 block/rescue 适用场景
下一章:Role 与复用 —— 把 playbook 里堆砌的任务拆成可对外复用的角色接口。