Ansible 剧本精粹 - 编写你的第一个 Playbook
如果说 Ansible Ad-Hoc 命令像是你对厨房里的助手发出的零散口头指令(“切个洋葱”、“烧开水”),那么 Playbook 就是一份完整、详细、写在纸上的菜谱。它列明了所有需要的“食材”(变量),详细的“烹饪步骤”(任务),甚至还有一些“特殊处理程序”(处理器),可以指导任何人(或 Ansible)精确地烹制出一道(甚至一桌)美味佳肴(即完成复杂的系统配置)。
Playbook 是 Ansible 实现“配置即代码 (Configuration as Code)”的核心。
Playbook 基础结构
Playbook 使用 YAML (YAML Ain't Markup Language) 格式编写。YAML 以其简洁、人类可读性强而著称。你需要了解一些基本的 YAML 语法:
- 缩进: YAML 使用空格(通常是 2 个或 4 个,但必须在同一层级保持一致)来表示层级关系,非常重要!
- 列表/数组: 以
-
(短横线加空格) 开头。 - 字典/映射: 以
key: value
(冒号后有空格) 的形式表示。
下面是一个最小化的 Playbook 结构示例,用于确保 webservers
组中的服务器都安装并启动了 Nginx:
# my_nginx_playbook.yaml
--- # YAML 文档开始的标志 (可选,但推荐)
- name: Configure Nginx Web Servers # 这是第一个 "Play" (剧目) 的名称
hosts: webservers # 此 Play 针对 Inventory 中的 'webservers' 组
become: true # 表示需要提权 (例如 sudo) 来执行任务
tasks: # 此 Play 包含的任务列表
- name: Ensure nginx package is present and updated # 第一个任务的名称
ansible.builtin.apt: # 使用的模块 (推荐使用 FQCN - 完全限定集合名称)
name: nginx
state: present # 确保 nginx 包是 'present'(已安装) 状态
update_cache: yes # 执行前更新 apt 缓存
- name: Ensure nginx service is started and enabled # 第二个任务的名称
ansible.builtin.service:
name: nginx
state: started # 确保服务是 'started'(已启动) 状态
enabled: yes # 确保服务是 'enabled'(开机自启) 状态
解读核心组件:
---
: YAML 文档的开始标记(一个 Playbook 文件可以包含多个 YAML 文档,每个文档以---
开始,以...
结束,但通常一个文件就是一个 Playbook,只包含一个---
)。-
(短横线): YAML 中列表项的开始。最外层的列表项代表一个 Play (剧目)。
- Play (剧目): Playbook 中的一个逻辑执行单元。它将一组任务映射到 Inventory 中的一组主机。一个 Playbook 可以包含多个 Play,它们会按顺序执行。
name
: 对这个 Play 的描述性名称,方便阅读和理解。hosts
: 指定这个 Play 作用于 Inventory 中的哪些主机或主机组。可以是all
,某个组名,某个主机名,或者更复杂的模式。become: true/false
: 是否需要提升权限来执行 Play 中的任务。可以设在 Play 级别(对所有任务生效)或 Task 级别(只对该任务生效)。tasks
: 一个列表,包含了在此 Play 中要按顺序执行的任务 (Task)。
- Task (任务): Playbook 中的最小执行单元,定义了一个要完成的动作。
name
: 对这个 Task 的描述性名称。module_name: {arguments}
: 指定要调用的 Ansible 模块 (Module) 及其参数。例如,ansible.builtin.apt
是用于 Debian/Ubuntu 系统包管理的模块。推荐使用 FQCN (Fully Qualified Collection Name) 如ansible.builtin.apt
而非简单的apt
,这能提高可读性和避免潜在的模块名冲突,尤其是在使用 Ansible Collections 时。
模块再探 (Modules Revisited)
我们之前提到,模块是 Ansible 完成实际工作的“工具”。Ansible 拥有庞大的模块库,覆盖了系统管理的方方面面:包管理、服务控制、文件操作、模板渲染、用户管理、命令执行、云资源管理等等。你可以在 Ansible 官方文档中查找和学习具体模块的用法。
变量的使用 (Using Variables)
硬编码值(如包名、用户名、端口号)会使 Playbook 难以复用和维护。变量 (Variables) 允许我们将这些值参数化。
- 定义变量的位置:
- Playbook 的
vars:
部分 (Play 级别)。 - 通过
vars_files:
导入外部变量文件。 - Inventory 文件中 (主机变量
host_vars/hostname.yml
,组变量group_vars/groupname.yml
)。 - 命令行通过
-e "var_name=value"
传递。 - 通过
register
关键字将一个任务的输出注册为一个变量。
- 使用变量: 在 Playbook 中,使用 Jinja2 模板语法
{{ variable_name }}
来引用变量。 - 示例:
---
- name: Install packages and create user using variables
hosts: all
become: true
vars: # 在 Play 级别定义变量
install_packages:
- vim
- htop
- curl
user_to_create: alice
tasks:
- name: Install specified packages
ansible.builtin.apt:
name: "{{ item }}" # 使用 item 变量,通常与 loop 结合
state: present
loop: "{{ install_packages }}" # 循环遍历 install_packages 列表
- name: Create user {{ user_to_create }}
ansible.builtin.user:
name: "{{ user_to_create }}" # 使用 user_to_create 变量
state: present
shell: /bin/bash
Handlers 与通知 (Handlers and Notifications)
有些任务(比如重启服务)我们不希望每次运行 Playbook 都执行,而只希望在相关的配置文件发生改变时才执行,并且即使有多个任务都请求重启,也只重启一次。这就是 Handlers (处理器) 的作用。
- 定义 Handlers: 在 Playbook 中与
tasks:
同一级,使用handlers:
块定义。Handler 本身也是一个 Task。 - 触发 Handlers (通知): 在一个 Task 执行后,如果该 Task 的状态是
changed
(即它确实对系统做了修改),并且该 Task 定义了notify:
指令,那么对应的 Handler 就会被“通知”。 - 执行时机: 被通知的 Handler 不会立即执行,而是在当前 Play 中所有常规 Task 执行完毕后,才会按照它们被定义的顺序执行一次(即使被多次通知也只执行一次)。
- 示例:
---
- name: Configure web server and restart if needed
hosts: webservers
become: true
tasks:
- name: Ensure nginx is installed
ansible.builtin.apt:
name: nginx
state: present
- name: Copy nginx configuration file
ansible.builtin.template: # 通常配置文件用 template 模块
src: my_nginx_config.conf.j2 # 源模板文件 (下一节讲)
dest: /etc/nginx/sites-available/my_app
notify: # 如果这个任务导致文件内容改变 (changed),则通知下面的 handler
- Restart nginx service
- name: Enable nginx site
ansible.builtin.file:
src: /etc/nginx/sites-available/my_app
dest: /etc/nginx/sites-enabled/my_app
state: link
notify: Restart nginx service # 如果符号链接状态改变,也通知重启
handlers: # 定义处理器
- name: Restart nginx service
ansible.builtin.service:
name: nginx
state: restarted
listen: "Restart nginx service" # 可选,让 notify 更明确地指向 handler 的 name
模板 (Templates - Jinja2)
当我们需要根据变量动态生成配置文件内容时,就需要使用模板 (Templates)。Ansible 使用 Jinja2 作为其模板引擎(与 Python Flask/Django Web 框架中使用的类似)。
- 工作方式: 你创建一个模板文件(通常以
.j2
结尾,如nginx.conf.j2
),在其中使用 Jinja2 语法嵌入变量、循环、条件判断等。ansible.builtin.template
模块会读取这个模板文件,用实际的变量值替换掉占位符,然后将渲染后的文件内容复制到目标节点上。 - Jinja2 模板示例 (
my_nginx_config.conf.j2
):
# my_nginx_config.conf.j2
server {
listen {{ http_port | default(80) }}; # 使用变量 http_port,如果未定义则默认为 80
server_name {{ ansible_fqdn }}; # ansible_fqdn 是 Ansible 自动收集的 Fact 变量
root /var/www/{{ app_name | default('default_app') }}/html;
index index.html index.htm;
location / {
try_files $uri $uri/ =404;
}
# 如果定义了 php_fpm_socket 变量,则包含 PHP 配置
{% if php_fpm_socket is defined %}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:{{ php_fpm_socket }};
}
{% endif %}
}
- 解释:
{{ variable_name }}
: 引用变量。| default(value)
: Jinja2 过滤器,如果变量未定义,则使用默认值。ansible_fqdn
: Ansible 在执行 Playbook 前会自动收集目标主机的很多信息(称为 Facts),这些 Facts 可以作为变量直接使用。{% if ... %}
...{% endif %}
: 条件判断。
运行 Playbook
编写好 Playbook 后,使用 ansible-playbook
命令来执行它。
- 命令 (输入):
ansible-playbook -i myinventory.ini my_nginx_playbook.yaml [-u remote_user] [--become] [-K] [-v]
- 常用选项:
-i <inventory_file>
: 指定 Inventory 文件。-v
,-vv
,-vvv
,-vvvv
: 增加输出的详细程度 (verbose)。--check
或-C
: 演练模式 (Dry Run)。模拟执行,不会实际修改目标主机,但会报告可能发生的变更。强烈建议在实际执行前使用!--limit <host-pattern>
: 只在 Inventory 中匹配此模式的主机上运行。--list-hosts
: 列出 Playbook 将要执行的目标主机,不实际运行。--list-tasks
: 列出 Playbook 中将要执行的所有任务。-K
或--ask-become-pass
: 提示输入提权密码(如 sudo 密码)。
- 输出解读 (概念性): Ansible Playbook 执行完毕后,会输出一个 PLAY RECAP,总结了每个主机的执行情况:
PLAY RECAP *********************************************************************
web1.example.com : ok=3 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
web2.example.com : ok=3 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
ok
: 成功执行且未对系统做出更改的任务数。changed
: 成功执行且已对系统做出更改的任务数。unreachable
: 无法连接的主机数。failed
: 执行失败的任务数。skipped
: 因条件不满足而被跳过的任务数。
总结
Playbook 是 Ansible 自动化的核心,它使用人类可读的 YAML 格式,通过组织有序的 Play 和 Task,结合强大的模块、变量、处理器和模板功能,使我们能够定义和执行复杂、可重复且版本可控的配置管理流程。这真正体现了“配置即代码”的思想。
掌握了 Playbook 的基本编写,你就拥有了自动化大部分日常运维任务的能力。但是,当配置逻辑变得越来越复杂,Playbook 文件越来越长时,如何有效地组织和复用我们的 Ansible 代码呢?答案就是 Ansible Roles。
在下一篇博客中,我们将深入探讨 Ansible Roles 的概念、目录结构和使用方法,以及更高级的 Inventory 管理技巧,让你的 Ansible 技能更上一层楼。敬请期待!