Ansible Playbook 与变量体系实战

ad-hoc 命令解决”临时干一票”的问题,但真正要重复用的东西必须落成 playbook。我写第一个剧本时的最大困惑不是语法,而是变量:同一个值,写在剧本里、写在清单里、用 -e 传进来,到底谁说了算?这篇把 playbook 的基础和变量体系一起理清楚——变量搞明白了,剧本才谈得上复用。

一、Playbook 是什么

Playbook(剧本)是 Ansible 的编排文件,核心就两个概念:

  • play:定义”在哪批主机上执行”,对应 hosts;
  • task:定义”具体做什么”,一条 task 调一个模块。

一个 playbook 可以包含多个 play,一个 play 可以包含多个 task。和 ad-hoc 的区别:ad-hoc 是一条命今一次执行,playbook 有明确的执行顺序和依赖关系,而且可以持久保存、重复执行。

二、YAML 语法速记

剧本用 YAML 写,语法规则不多,但每条都必须严守,而且错得悄无声息(缩进错了直接语法报错,写错了往往不报错但行为不对):

  1. 只用空格缩进,绝对不能用 Tab;
  2. 同层级左对齐,一层缩进 2 个空格;
  3. 冒号后面必须有一个空格:name: wget,写 name:wget 会解析成字符串;
  4. 大小写敏感,Ansible 的关键字全是小写;
  5. 注释用 #;
  6. 开头 --- 标识 YAML 文档。

两种核心结构:字典(键值对)和列表(- 开头)。写多行内容用 | 保留换行,配配置文件时常用:

1
2
3
4
5
6
7
8
9
- name: 写入 nginx 默认站点配置
ansible.builtin.copy:
content: |
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
}
dest: /etc/nginx/conf.d/default.conf

三、第一个剧本与语法检查

1
2
3
4
5
6
7
8
9
10
11
12
---
- hosts: prod_web # 在哪些主机执行
remote_user: root # 用什么用户执行
tasks:
- name: 安装基础软件包
yum:
name: "{{ yum_items }}"
state: present
vars:
yum_items:
- wget
- lrzsz

写完先别执行,跑一遍语法检查:

1
ansible-playbook --syntax-check install_base.yml

--syntax-check 只检查语法不连远端,顺手还能用 -C(干跑)模拟执行看变更范围。这两个参数我建议当成剧本的”编译检查”来用,尤其是改高危剧本的时候。

四、变量定义在哪里,谁说了算

变量定义的位置有四种,优先级从低到高:

1
Inventory 文件里的变量  <  playbook 里的 vars / vars_files  <  命令行 -e 传入

最常见的是 playbook 里定义:

1
2
3
4
5
6
7
8
9
10
11
# 方式一:列表变量,适合一组软件包
- hosts: web_group
vars:
packages:
- httpd
- mariadb-server
- php
tasks:
- name: 安装 LAMP 组件
yum:
name: "{{ packages }}"
1
2
3
4
5
6
7
8
# 方式二:vars_files 外部文件,多个剧本共享
- hosts: web_group
vars_files:
- ./vars/webserver.yml
tasks:
- name: 安装 web 组件
yum:
name: "{{ web_server }}"

vars_files 是比 vars 更工程化的做法——同一个变量文件可以被多个剧本引用,改版本号只改一处。再进一步就是分层变量:

1
2
3
4
5
6
7
8
9
10
11
# vars_file.yml 里按业务分层
lamp:
framework:
web_package: httpd
db_package: mariadb-server
php_package: php
lnmp:
framework:
web_package: nginx
db_package: mysql
php_package: php
1
2
3
# 引用时两种写法,推荐后者(官方写法,键名有特殊字符也不怕)
name: "{{ lamp.framework.web_package }}"
name: "{{ lamp['framework']['web_package'] }}"

命令行传入变量用于临时覆盖(换环境、换版本):

1
ansible-playbook deploy.yml -e "web_server=nginx" -e "db_server=mariadb-server"

-e 的优先级最高,适合”同一套剧本多环境复用”:剧本里写默认值,执行时按环境覆盖。但要注意它同时也是最容易埋雷的——传错一个变量,剧本会静默用错值跑完,所以我只在明确的场景用,日常变量一律落在文件里。

五、魔法变量:跨主机协作的钥匙

魔法变量和 Inventory 强相关,做批量分组、跨主机协作全靠它们。生产上最常用的五个:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# 1. inventory_hostname:主机在清单里的名字,做唯一标识
- name: 生成带主机标识的备份文件
copy:
content: "config backup"
dest: "/data/backup/{{ inventory_hostname }}_conf_bak.tar.gz"

# 2. group_names:当前主机属于哪些组,一套剧本适配多角色
- name: 只在 web 组装 Nginx
yum:
name: nginx
when: "'prod_web' in group_names"

# 3. groups:所有组和主机列表,自动生成集群配置
# 模板里遍历 web 组,生成负载均衡后端
upstream app_backend {
{% for host in groups['prod_web'] %}
server {{ hostvars[host]['ansible_default_ipv4']['address'] }}:8080;
{% endfor %}
}

# 4. hostvars:跨主机取数据,从库配置主库地址就靠它
- name: 配置主库地址
lineinfile:
path: /etc/my.cnf
regexp: '^master_host'
line: "master_host = {{ hostvars['db01']['ansible_default_ipv4']['address'] }}"

# 5. inventory_dir:清单所在目录,用来拼相对路径
- name: 加载同目录下的业务变量
include_vars:
file: "{{ inventory_dir }}/vars/app_config.yml"

hostvars 有一个使用前提:目标主机的 facts 得先采集过。如果 play 里关了 gather_facts,跨主机取到的值可能是空的——这个坑我第一次用模板生成 upstream 时踩得很实,排查半天才发现是 facts 没采。

六、facts:系统信息自动采集

剧本默认会执行 gather_facts,把被控端信息采集到 ansible_facts 里。巡检、按硬件差异化生成配置,都靠它:

1
2
3
4
5
6
7
8
- debug:
msg: "主机名:{{ ansible_facts['hostname'] }}"
- debug:
msg: "系统:{{ ansible_facts['distribution'] }} {{ ansible_facts['distribution_version'] }}"
- debug:
msg: "内存:{{ ansible_facts['memtotal_mb'] }} MB"
- debug:
var: ansible_facts['mounts']

几个高价值的场景:

1
2
3
4
5
6
7
8
9
10
# 1. 按系统大类分发包管理器(RedHat 系用 yum,Debian 系用 apt)
- name: 安装基础工具(RedHat 系)
yum:
name: [vim, wget, net-tools]
when: ansible_facts['os_family'] == "RedHat"

- name: 安装基础工具(Debian 系)
apt:
name: [vim, wget, net-tools]
when: ansible_facts['os_family'] == "Debian"

这里有个常见误区:Ubuntu 的 os_family 是 Debian,不是 Ubuntu。判断系统家族时先想清楚这层关系,条件写错了,剧本在 Ubuntu 上直接跳过安装步骤。

1
2
3
# 2. 按内存大小生成配置:模板里直接算
[mysqld]
innodb_buffer_pool_size = {{ (ansible_facts['memtotal_mb'] * 0.8) | int }}M

同样一套 MySQL 剧本,撒到 2G、4G、16G 的机器上,配置文件自动适配——这就是”事实驱动”的威力。不用为每种规格写一份剧本。

七、变量注册:把执行结果接出来

模块执行完的返回值想复用,就用 register 接住:

1
2
3
4
5
6
7
8
9
- hosts: web_group
tasks:
- name: 查看根目录
shell: "ls -l /"
register: list_dir

- name: 输出结果
debug:
msg: "{{ list_dir.stdout_lines }}" # 只要标准输出,干净

技巧在于 stdout_lines:直接 msg: "{{ list_dir }}" 会带一堆 return code、耗时之类的杂项,stdout_lines 只取命令输出并按行拆好。之后配合 when: list_dir.rc == 0 之类的判断,就能做出”命令失败则跳过后续”的逻辑。

八、facts 缓存与执行优化

facts 采集需要额外的时间。机器多的时候有两个选择:

1
2
3
4
# 选项一:这个 play 不需要系统信息,直接关掉
- hosts: web_group
gather_facts: no
tasks: ...
1
2
3
4
5
# 选项二:缓存 FACTS,一段时间内复用(生产大环境推荐)
gathering = smart
fact_caching = jsonfile
fact_caching_connection = /tmp/ansible_facts_cache
fact_caching_timeout = 86400

判断标准很简单:剧本里用不到 ansible_facts 就关掉;用得到但机器多,就用缓存。

九、变量优先级的记忆法

Ansible 的完整优先级列表有二十多条,背是不现实的。日常工作记住三个原则就够用:

  1. 就近原则:越”贴近这台主机”的定义优先级越高——单机变量 > 组变量 > 全局变量;
  2. 外部压内部:命令行 -e 永远最高,用来做环境覆盖;
  3. all 就是全局默认值:group_vars/all.yml 是所有机器的兜底。

调试变量时有个必会动作:ansible-inventory -i inventories/prod/hosts --host db01,会把这台机器生效的全部变量、以及从哪层继承的,完整列出来。变量不生效的时候,先跑这条命令,比猜快得多。

小结

Playbook 的入门门槛在 YAML 语法,真正拉开差距的是变量。把”会变的东西”从剧本里赶出去——版本号进变量文件、环境差异进 group_vars、临时覆盖走 -e——剧本才能从”这台机器能跑”变成”哪台机器都能跑”。