Ansible02-Playbook
Ansible Playbook 详解:从 ad-hoc 到自动化剧本
目录
- 为什么要用 Playbook
- 模块与幂等性
- 模块速查:常用模块一览
- 怎么查模块文档:ansible-doc
- Playbook 基础结构
- 条件、循环、标签与处理器
- 任务复用与错误处理
- 写一个完整的 Playbook
- 总结
- 参考资料
0. 为什么要用 Playbook
第一篇以 ad-hoc 跑通了逐条命令,但 ad-hoc 只适合「一条命令干一件事」。真实场景往往是多步、有逻辑、要反复执行:
- 先装软件 → 再下发配置 → 再启动服务 → 再验证;
- 不同系统(RHEL/Debian)要装不同的包、走不同的路径;
- 只有配置变化时才重启服务,而不是每次都重启;
- 这套流程要能被团队复用、被 CI/CD 调用。
Playbook(剧本) 就是把这些写成一整套 YAML 任务编排。它可读、可复用、幂等(重复跑结果一致),是 Ansible 自动化的「主脚本」。其常用要素包括条件、循环、标签、处理器与错误处理。
一、模块与幂等性
1.1 什么是模块
模块(module) 是 Ansible 执行的最小操作单元,封装了具体的操作逻辑(复制文件、安装软件、管理服务、创建用户等)。每个模块接收参数、在被管端执行、返回结果。
模块按命名空间组织:
ansible.builtin.*:内置模块(装 ansible-core 就有);community.general.*、ansible.posix.*等:集合里的扩展模块(需安装集合)。
绝大多数模块需要被管端有 Python 才能执行;少数不需要(如
ansible.builtin.raw,直接裸跑命令)。
1.2 幂等性:Ansible 最核心的设计
幂等性(idempotency):同一个任务反复执行,结果始终一致。也就是说,「已经处于目标状态」时再执行,不会重复操作、不会破坏现状。
例如:copy 复制文件时,如果目标文件内容和源一致,Ansible 会报告 ok(未做任何改动),而不是再次复制。
幂等性为什么重要? 可以安全地反复运行 Playbook(补跑、重试、定期校准),不用担心重复安装、重复改配置导致问题。这是 Ansible 模块与裸 shell 命令的本质区别。
1.3 模块怎么「判断」要不要改?
每个模块内置了目标状态(desired state)比对:先检查被管端当前状态,只有不一致时才动手改。模块通过返回值里的 changed 字段告诉 Ansible「这次有没有真的改动」。
| 模块 | 如何判断是否 changed |
|---|---|
copy |
对比本地源与目标文件的哈希(checksum),一致则 ok,不一致才复制 |
file |
检查文件/目录/链接是否存在、权限、属主、链接目标是否匹配 |
yum / apt |
查询软件包是否已安装、版本是否满足 |
service / systemd |
查询服务当前 state 与 enabled 是否已符合 |
user / group |
检查用户/组是否存在、属组、shell 是否匹配 |
这就是为什么同一条命令反复跑,输出会从
changed变成ok:第一次真的改了,第二次发现已符合目标状态,就只报 ok、不做事。
1.4 幂等 vs 非幂等:模块与裸命令的区别
command / shell 模块不幂等——它们只是把命令原样扔到被管端执行,无法判断「是否需要做」,每次执行都会报 changed,且可能重复造成副作用:
# 非幂等:每次运行都会执行,哪怕软件已装好
- name: 用 command 装软件(不推荐)
command: dnf install -y nginx
# 幂等:软件已装则跳过,返回 ok
- name: 用 yum 装软件(推荐)
yum:
name: nginx
state: present
原则:凡是能抽象成「目标状态」的操作,都用专用模块(yum/apt/copy/service/user…)获得幂等性;实在没有对应模块、必须跑命令时,才用 command/shell,并尽量让它自身可重复执行。
二、模块速查:常用模块一览
2.1 核心模块速查表
| 模块 | 作用 | 关键参数 |
|---|---|---|
ping |
测试被管端连通性与 SSH+Python 是否就绪 | 无 |
command |
执行命令(不支持管道/通配符/重定向) | cmd |
shell |
通过 shell 执行命令(支持管道、通配符、() 等) |
cmd |
copy |
把本地文件复制到被管端,或写入内容 | src/dest/content/mode/owner |
fetch |
从被管端把文件拉到本地 | src/dest/flat |
file |
管理文件/目录/链接的属性 | path/state/mode/owner |
template |
用 Jinja2 模板渲染后写到被管端(变量化渲染,见变量与 Roles 篇) | src/dest/mode |
lineinfile |
确保文件中某一行存在/修改 | path/line/regexp |
blockinfile |
在文件中插入/更新一块文本 | path/block/marker |
service |
管理系统服务(RHEL/Debian 通用) | name/state/enabled |
systemd |
用 systemd 管理服务(更细粒度) | name/state/enabled/daemon_reload |
yum/dnf |
用 yum/dnf 管理软件包(RHEL 系) | name/state/update_cache |
apt |
用 apt 管理软件包(Debian 系) | name/state/update_cache |
package |
跨平台包管理,自动选 yum/apt/dnf | name/state |
pip |
管理 Python 库 | name/state/virtualenv |
user |
管理系统用户 | name/state/groups/shell/password |
group |
管理系统用户组 | name/state |
git |
从 Git 仓库拉取/更新代码 | repo/dest/version |
get_url |
从 URL 下载文件 | url/dest/checksum |
uri |
发 HTTP 请求(GET/POST 等) | url/method/return_content |
unarchive |
解压归档(自动处理 tar.gz/zip 等) | src/dest/remote_src |
archive |
创建归档(tar.gz 等) | path/dest/format |
cron |
管理定时任务 | name/minute/hour/job |
mount |
管理挂载(nfs 等) | src/path/fstype/state |
selinux |
设置 SELinux 模式 | policy/state |
debug |
打印变量/调试信息 | msg/var |
set_fact |
设置自定义变量 | key=value |
setup |
收集被管端 facts(系统信息) | filter |
此处列出的是最常用的模块。模块数量庞大,不必逐一记忆,关键是掌握用
ansible-doc查询文档的方法(见下一节),需要时现查现用。
2.2 软件包模块:yum / apt / package 的区别
| 模块 | 适用系统 | 说明 |
|---|---|---|
yum / dnf |
RHEL 系 | yum 会自动调用 dnf |
apt |
Debian 系 | 装包前常需 update_cache: true |
package |
跨平台 | 自动根据被管端系统选 yum/apt,适合写「通用」剧本 |
一个 Playbook 同时兼容两系的标准做法,是配合
when: ansible_os_family == 'RedHat'/== 'Debian'分别写 yum 和 apt 两个任务(见第 5 章)。
三、怎么查模块文档:ansible-doc
模块数量很多,逐一记忆并不现实。掌握文档查询方法比背诵参数更重要,ansible-doc 正是内置的模块文档工具。
3.1 列出所有模块
# 列出所有可用模块(可能很长,用 grep 过滤)
ansible-doc -l
ansible-doc -l | grep -E '^(copy|service|user|yum|apt)'
3.2 查看某个模块的完整文档
ansible-doc copy
关键段落:
OPTIONS(可用选项):每个选项有type(类型)和default(默认值)两个关键属性。例如copy的mode是str,backup是bool且默认no。EXAMPLES(例子):给出可直接抄的用法,不会用时先搜这里的案例(用/EXAMPLE在分页里搜索)。
查看 OPTIONS 时重点关注两处:
type(list/str/bool,决定传列表还是单个值)和default(默认取值,不写时的行为)。
3.3 查看模块的参数速览
# -s 只列参数,不列长篇说明,快速看参数名
ansible-doc -s copy
3.4 按类型列模块
# 列出所有缓存类插件/模块
ansible-doc -t cache -l
实战习惯:写 Playbook 前,
ansible-doc <模块>扫一眼 OPTIONS 的 type/default 和 EXAMPLES,能避免大部分「参数写错类型」「漏了必填项」的问题。
四、Playbook 基础结构
有了模块的基础,可将其串成 Playbook。本章先介绍骨架:Playbook 的结构、运行方式与输出解读。
4.1 一个最小的 Playbook
---
- name: 部署应用 # Play 的名字(可省略,但建议写)
hosts: webservers # 作用于哪些主机(模式)
become: true # 是否提权
tasks: # 任务列表
- name: 安装 nginx # 任务名
yum:
name: nginx
state: present
- name: 启动服务
service:
name: nginx
state: started
enabled: true
4.2 YAML 缩进规则(新手最容易错)
- YAML 用空格缩进表示层级,绝不能用 Tab;
- name、hosts、become、tasks是同一层级(都在 play 下),缩进对齐;tasks下的每个- name是一个任务(列表项),它的子项(模块名、参数)再多缩进两格;- 缩进以「第一个字母」算,不考虑开头的
-。
报
Syntax Error while loading YAML时,先查:是不是用了 Tab、冒号后是不是少了空格、引号有没有闭合。关于 YAML 本身(列表、字典、缩进),可回看《YAML 配置语言基础》。
4.3 Playbook 与 ad-hoc 的对应关系
| ad-hoc | Playbook |
|---|---|
ansible -m yum -a 'name=nginx state=present' |
tasks: - yum: name: nginx state: present |
-b 提权 |
become: true |
--limit node1 |
命令行 --limit |
-i inventory.ini |
ansible.cfg 的 inventory 或 -i |
4.4 运行 Playbook
# 语法检查(不执行)
ansible-playbook -i inventory.ini playbook.yml --syntax-check
# 试运行(只模拟,不真正改动;对支持 check 的模块有效)
ansible-playbook -i inventory.ini playbook.yml --check
# 正式运行
ansible-playbook -i inventory.ini playbook.yml
# 只跑某几台(灰度)
ansible-playbook -i inventory.ini playbook.yml --limit node1
# 提高输出详细度(排错用)
ansible-playbook -i inventory.ini playbook.yml -v
ansible-playbook -i inventory.ini playbook.yml -vvv
-v/-vv/-vvv/-vvvv逐渐增加详细度:-v给任务和主机状态,-vv加变量值,-vvv更多调试,-vvvv连 HTTP 请求都显示。
4.5 运行输出解读
| 状态 | 含义 |
|---|---|
ok |
任务已满足(幂等,无改动) |
changed |
任务执行并产生了变更 |
failed |
任务失败 |
skipping |
因 when 条件不满足而跳过 |
unreachable |
主机连不上 |
PLAY [部署应用] ************************
TASK [安装 nginx] **********************
ok: [192.168.1.21]
changed: [192.168.1.22]
再次运行同一 Playbook,第二次大多显示
ok(幂等)——这正是第 1 章讲的幂等性带来的好处。
五、条件、循环、标签与处理器
骨架会了,但真实任务很少是「一条直线跑到底」。这一章讲让任务变聪明的四个要素:处理器(变了才触发)、条件(when)、标签(按需执行)、循环(对列表逐项做)。
5.1 handlers(处理器):配置变了才触发
handlers 是一组特殊的任务,只有被 notify 触发时才执行,且只在某个任务真正产生变更(changed)后才触发。典型场景:配置文件变了才重启服务。
---
- name: 配置 nginx
hosts: webservers
become: true
tasks:
- name: 下发配置
copy:
src: nginx.conf
dest: /etc/nginx/nginx.conf
notify: restart nginx # 变更后触发名为 restart nginx 的 handler
handlers:
- name: restart nginx # handler 的名字
service:
name: nginx
state: restarted
handler 的几个关键行为:
- 只在 changed 时触发:配置没变(
ok),handler 不执行; - 最后统一执行一次:即使多个任务 notify 同一个 handler,也只在所有任务执行完后执行一次(「改多个配置、最后统一重启一次」是自然写法);
notify的名字必须和handlers:里的name一字不差(大小写、空格都一样)。对不上会报错The requested handler ... was not found,或静默不触发。
提醒:
reload(平滑重载,如nginx -s reload,不断服务)与restart(彻底重启)语义不同。日常改配置用reload更稳妥;改了影响进程本身的设置才需要restart。
多个 handler / listen(监听):可以用 listen 给多个 handler 起一个共同的「监听名」,一个 notify 触发多个 handler。日常用 name 直配即可,listen 是进阶用法。
5.2 when(条件):满足条件才执行
- name: 仅在 RedHat 系执行
yum:
name: nginx
state: present
when: ansible_os_family == 'RedHat'
- name: 仅在 Debian 系执行
apt:
name: nginx
state: present
when: ansible_os_family == 'Debian'
ansible_os_family是自动收集的事实(fact):RedHat(含 RHEL/CentOS/Rocky/Fedora)或 Debian(含 Ubuntu)。用when可让一个 Playbook 同时兼容两系。这是发行版差异落到代码里的最典型写法。
when 还支持多种判断:is defined(变量存在)、in(在列表里)、逻辑组合 and/or、not 等。更全的判断类型在第 3 篇变量章节讲。
5.3 tags(标签):按需选择性执行
一个 Playbook 有几十个任务,有时只想「只重启服务」或「只部署配置」。给任务打上 tags,就能用 --tags/--skip-tags 精确挑选:
tasks:
- name: 安装软件
yum:
name: nginx
state: present
tags: [install]
- name: 部署配置
copy:
src: nginx.conf
dest: /etc/nginx/nginx.conf
notify: restart nginx
tags: [config]
- name: 重启服务
service:
name: nginx
state: restarted
tags: [restart]
ansible-playbook site.yml --tags restart # 只执行 restart 标签的任务
ansible-playbook site.yml --skip-tags install # 跳过 install 类
ansible-playbook site.yml --list-tags # 查看有哪些标签
tags还支持always(始终执行,如打印最终结果)和never(默认不执行,需显式--tags never才跑)。生产做灰度、定点操作时非常有用。
5.4 loop(循环):对列表逐项执行
- name: 安装多个软件包
yum:
name: '{{ item }}'
state: present
loop:
- nginx
- git
- vim
{{ item }} 每次循环取当前项。旧写法 with_items 等价,新版本推荐 loop。
loop 还能遍历字典(批量创建用户/目录时很常用),用 item.key/item.value 取值:
- name: 按字典批量创建用户(用户名 -> shell)
user:
name: "{{ item.key }}"
shell: "{{ item.value }}"
state: present
loop:
alice: /bin/bash
bob: /bin/bash
carol: /usr/sbin/nologin
需要循环序号时可用 loop_control: { index_var: idx }。日常最常用的是列表、字典两种基本形态。关于 item 里嵌入更复杂的数据结构(如「每个元素是带多个字段的字典」),在变量章节补充。
六、任务复用与错误处理
Playbook 写大了,就要考虑「怎么复用、怎么容错」。这一章讲两个主题:把任务拆到多个文件复用,以及出错时的处理机制。
6.1 用 import_tasks / include_tasks 拆分任务文件
Playbook 变大后,把任务拆到多个文件里复用:
# tasks/deploy_app.yml
---
- name: 安装依赖
yum:
name: [git, curl]
state: present
- name: 拉取代码
git:
repo: https://example.com/app.git
dest: /opt/app
# site.yml 主 Playbook 中引入
---
- name: 部署应用
hosts: webservers
become: true
tasks:
- name: 执行公共初始化任务
import_tasks: tasks/common.yml
- name: 执行部署任务
include_tasks: tasks/deploy_app.yml
import_tasks 与 include_tasks 的区别(新手常混淆):
import_tasks(静态导入) |
include_tasks(动态包含) |
|
|---|---|---|
| 解析时机 | 解析阶段就展开进 Playbook | 运行阶段才读取执行 |
支持 tags/被 handlers 引用 |
√ | × |
支持 when/loop 动态决定 |
× | √ |
| 直观性/可调试 | 更直观 | 适合按条件动态加载 |
简化记忆:内容在解析期就定死的用
import_tasks;需要按运行时条件/循环加载的用include_tasks。日常静态拆分用import_tasks更直观。
6.2 用 block / rescue / always 做错误处理
block 把多个任务包成一个组,配合 rescue(出错时执行)与 always(无论成败都执行),类似 try/catch/finally:
- name: 尝试重启并回滚
block:
- name: 重启服务
service:
name: myapp
state: restarted
- name: 验证服务正常
command: curl -fsS http://127.0.0.1/health
rescue:
- name: 服务启动失败,回滚到旧版本
shell: /opt/app/rollback.sh
always:
- name: 无论成败都记录日志
debug:
msg: 本次操作结束
执行逻辑:先跑
block;其中任何一步失败就跳进rescue补救/回滚;always无论成败都执行(收尾、记录)。这是生产部署处理失败回滚的标准写法。
block也可以只用来给一组任务统一加when/become(把公共条件提出来)。
6.3 忽略错误:ignore_errors 与 force_handlers
ignore_errors: true:任务失败也不中断,继续执行后续任务(慎用,用于「失败了也无妨」的探测类任务);force_handlers: true(play 级):即使前面的任务有失败,已触发的 handlers 也照常执行(默认是「某主机某 play 报错后,该主机的后续任务包括 handler 都不执行」)。
- name: 部署
hosts: all
force_handlers: true # 有任务失败也保证 handlers 执行
tasks:
- name: 装软件
yum:
name: httpd
state: present
notify: restart httpd
handlers:
- name: restart httpd
service:
name: httpd
state: restarted
Ansible 的默认机制是:某主机的某个 play 执行错误,该主机就从执行队列移除,后续所有 play(含 handler)都不再执行。
force_handlers用来保证该执行的 handler 不被「连坐」。
七、写一个完整的 Playbook
将上述知识点整合为一个「兼容 RHEL/Debian、带 handler、带循环」的完整 Playbook:批量部署 nginx + 下发配置 + 启动服务。此例刻意不使用模板(模板渲染依赖变量体系),以便聚焦 Playbook 自身的要素。
目录结构
ansible-lab/
├── inventory.ini
├── playbook.yml
└── files/
└── nginx.conf # 一份静态的主配置(不加变量,用 copy 原样下发)
inventory.ini
[webservers]
192.168.1.21
192.168.1.22
[all:vars]
ansible_user=centos
ansible_ssh_private_key_file=/home/ops/.ssh/id_ed25519
playbook.yml
---
- name: 批量部署 Nginx
hosts: webservers
become: true
tasks:
- name: 安装基础软件包(loop 循环)
package:
name: '{{ item }}'
state: present
loop:
- nginx
- curl
- name: 安装 nginx(RedHat,用 yum)
yum:
name: nginx
state: present
when: ansible_os_family == 'RedHat'
- name: 安装 nginx(Debian,用 apt)
apt:
name: nginx
state: present
update_cache: true
when: ansible_os_family == 'Debian'
- name: 下发主配置
copy:
src: files/nginx.conf
dest: /etc/nginx/nginx.conf
notify: restart nginx
- name: 启动并开机自启
service:
name: nginx
state: started
enabled: true
handlers:
- name: restart nginx
service:
name: nginx
state: restarted
说明:
- 这里用
package+loop装 nginx 和 curl(package会自动按被管端系统选 yum/apt,适合两系通用);再用两个分别的 yum/apt 任务演示「按发行版走不同包名」的写法。实际项目中往往取其一,这里演示两种思路。copy把files/nginx.conf原样下发,配置一变(文件内容与目标不同)就触发restart nginx。这体现了「配置变了才重启」的 handler 用法。
运行
ansible-playbook -i inventory.ini playbook.yml --syntax-check
ansible-playbook -i inventory.ini playbook.yml --check
ansible-playbook -i inventory.ini playbook.yml
ansible-playbook -i inventory.ini playbook.yml --limit 192.168.1.21 # 灰度先跑一台
这个 Playbook 使用了:
when(跨发行版)、notify/handlers(配置变了才重启)、loop(循环装多个包)、copy(下发静态配置)、become(提权)。理解并改造它,即可掌握 Playbook 的核心。待第 3 篇引入变量与模板后,这份静态nginx.conf可升级为「按主机渲染出不同内容」的模板版。
八、总结
8.1 本篇要点回顾
- 幂等性:Ansible 的核心设计,让 Playbook 可安全反复执行;
command/shell不幂等,能用专用模块就用。 - 模块速查:copy/file/service/yum/apt/user/git 等是主力;跨平台用
package,跨发行版用when。 - ansible-doc:
-l列全部、-s看参数、重点看 OPTIONS 的 type/default 和 EXAMPLES。 - Playbook 关键字:
handlers/notify(变了才触发)、when(条件)、tags(按需执行)、loop(循环)。 - 任务复用与错误处理:
import_tasks(静态)/include_tasks(动态)、block/rescue/always、ignore_errors/force_handlers。
8.2 下一步
到此可写出可复用、带逻辑的 Playbook,但仍有两点待补充:一是 {{ item }}、when 中使用的变量来源与种类;二是如何将配置渲染为按主机区分的模板。这两点涵盖变量体系(变量来源与种类、事实、魔法变量、优先级)与Jinja2 模板渲染(将配置按主机区分的模板),在《Ansible 变量与 Roles》中展开。
参考资料
- Ansible Playbook 介绍:https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_intro.html
- Ansible 内置模块总览:https://docs.ansible.com/ansible/latest/collections/ansible/builtin/index.html
- ansible-doc 用法:https://docs.ansible.com/ansible/latest/cli/ansible-doc.html
- Playbook 处理器 handlers:https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_handlers.html
- Playbook 条件 when:https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_conditionals.html
- Playbook 循环 loop:https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_loops.html
- Playbook 标签 tags:https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_tags.html
- Ansible 模板(Jinja2):https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_templating.html
