02. Ansible - Playbook
Playbook
在之前使用 Ansible 的时候都是采用命令行的方式执行,这样的方式存在几个问题:
- 如果某个操作需要多次执行,如何保存命令。
- 生产中的操作往往不是一个模块能完成的,如何整合这些命令。
对于上面的需求,Playbook(剧本)的作用就在于能够通过声明配置的方式,对操作流程进行有序的编排,并支持同步或者异步的方式发起任务。
Playbook 采用 YAML
语言编写,由一个或多个 play 组成。同时,每个 play 又由一个或者多个 task(任务)组成。
基础示例
以一个添加组合用户为例,使用命令行操作:
# 先添加用户组
ansible '192.168.2.201' -b -m group -a 'gid=10001 name=ops state=present'
# 再添加用户
ansible '192.168.2.201' -b -m user -a 'uid=10001 name=opuser01 password="123456" group="ops" create_home=no shell=/sbin/nologin state=present'
这样就需要使用两条命令来完成,改写成 playbook:
---
- hosts: 192.168.2.202
tasks:
- name: "1. Add group"
group: gid=10001 name=ops state=present
- name: "2. Add user"
user: uid=10001 name=opuser01 password="123456" group="ops" create_home=no shell=/sbin/nologin state=presen
只需要将 -a 的参数作为模块名称的 Key 的 Value 即可。写完后可以检测是否正常:
ansible-playbook -b 01.add-group-user.yaml --check
通过 --check
或者 -C
参数,只会检测,不会真实执行。
ansible-playbook -b 01.add-group-user.yaml
如图所示:
可以看到执行过程有 1 个 Play,3 个 TASK。其中 Gathering Facts
任务的作用在于收集远程客户端的信息,类似于 setup 模块的用途。属于默认的 TASK,如果主机多,会非常耗时,不过如果不需要用到它返回的 facts 变量,可以关掉它。剩下的 2 个 TASK 就是用户自己定义操作。
将 -a
参数的值直接放到模块后面,虽然书写简单,但是不便于维护。建议按照 YAML 格式继续拆解:
---
- hosts: 192.168.2.202
tasks:
- name: "1. Add group"
group:
gid: 10001
name: ops
state: present
- name: "2. Add user"
user:
uid: 10001
name: opuser01
password: 123456
group: ops
create_home: no
shell: /sbin/nologin
state: present
变量定义
在 Playbook 中创建变量的方式主要包含以下几种:
- 在 Playbook 文件中,通过
vars
定义。 - 在 Playbook 文件中,通过
vars_files
引入外部变量文件。 - 在 Playbook 所在目录中创建
group_vars
(能自动识别)目录,在下面为不同组设置对应的变量文件。 - ansible 内置的变量,比如 facts 的变量。
- 在 Playbook 文件中,通过
register
将返回注册成变量。
变量定义(vars)
通过在 Playbook 中定义变量可以直接被后面使用:
---
- hosts: 192.168.2.201
vars:
source_file: /tmp/demo.sh
remote_dir: /tmp
tasks:
- name: "Send file"
copy:
src: "{{ source_file }}"
dest: "{{ remote_dir }}/abc.sh"
mode: 0755
可以使用 {{}}
调用变量,但是需要注意,如果变量在内容的开头,需要使用引号括起来,否则可能会报错。
变量定义(vars_files)
通过引入外部定义变量文件然后使用里面的变量。
vars.yaml
:
source_file: /tmp/demo.sh
remote_dir: /tmp
Playbook:
---
- hosts: 192.168.2.201
vars_files:
- "./vars.yaml"
tasks:
- name: "Send file"
copy:
src: "{{ source_file }}"
dest: "{{ remote_dir }}/aaa.sh"
mode: 0755
变量定义(group_vars)
通过在 Playbook 目录创建 group_vars 目录来定义变量:
mkdir -p group_vars/{all,master,client,linux}
在指定的组下面创建对应的变量配置文件:group_vars/master/vars.yaml
dir_name: /tmp
Playbook:
---
- hosts: master
tasks:
- name: "测试变量"
debug:
msg: "变量的值为:{{ dir_name }}"
变量定义(facts)
Ansible 在执行 Playbook 的时候有 Gathering Facts
的操作,该操作用于手机远程机器的配置信息。这些信息也正是 setup 模块输出的信息。在 Playbook 中,可以利用这一点,动态获取到客户端的配置信息,以此作为变量使用。
ansible master -m setup
其返回的内容其实际是一个 JSON,在 Playbook 能够对该 JSON 进行解析,其中主要一些值如下:
{
"ansible_facts": {
"ansible_all_ipv4_addresses": [], // IPV4 列表
"ansible_all_ipv6_addresses": [], // IPV6 列表
"ansible_default_ipv4": {
"address": "192.168.2.201", // 默认 IPV4 地址
"interface": "ens33", // 默认网卡名称
},
"ansible_distribution": "CentOS", // 系统
"ansible_distribution_version": "7.9", // 系统版本
"ansible_dns": {
"nameservers": [] // DNS 地址
},
"ansible_hostname": "node-01", // 主机名
"ansible_kernel": "3.10.0-1160.el7.x86_64", // 内核版本
"ansible_memfree_mb": 2779, // 空闲内存
"ansible_memtotal_mb": 3770, // 总内存
"ansible_processor_cores": 2, // CPU 核心数
"ansible_processor_vcpus": 4, // 总 CPU 数
},
}
Playbook:
---
- hosts: 192.168.2.201
tasks:
- name: "测试 facts 变量"
debug:
msg: "主机名称:{{ ansible_hostname }}
IPV4地址:{{ ansible_default_ipv4.address }}
系统版本:{{ ansible_distribution }} {{ ansible_distribution_version }}"
如果在 Playbook 中不需要使用 facts 变量,可以配置 gather_facts: false
去掉 Gathering Facts
这个 TASK。
---
- hosts: 192.168.2.201
gather_facts: false
tasks:
...
变量定义(register)
有时候想要把命令的输出结果当成变量让后面的 TASK 使用,就可以使用 register:
---
- hosts: 192.168.2.201
tasks:
- name: "1. Get time"
shell: "date +%F"
register: time_now
- name: "2. Use time"
debug:
msg: "{{ time_now }}"
但是这样写存在一个问题,输出的结果不是命令执行的结果:
{
"msg": {
"changed": true,
"cmd": "date +%F",
"delta": "0:00:00.002554",
"end": "2023-03-18 21:11:30.448565",
"failed": false,
"rc": 0,
"start": "2023-03-18 21:11:30.446011",
"stderr": "",
"stderr_lines": [],
"stdout": "2023-03-18",
"stdout_lines": [
"2023-03-18"
]
}
}
真正想要的内容其实是 stdout
里面的内容,所以第二步的变量得变成:{{ time_now.stdout }}
除此之外,还有几个值是很总要的:
- rc:如果是 0,则表示命令执行正确,否则错误
- stderr:如果出错,这里会有错误信息
- stdout:真正输出的内容
应用示例(登录提示)
在 /etc/motd
文件中可以定义一些内容,用户在 ssh 成功后会在终端输出这些内容。通过这个功能,可以在初始化的时候将这个文件嵌入一些系统相关信息,让用户登录服务器的时候能够第一时间看到。
创建 motd 的模板文件:motd.j2
######################################################################################
主机名称:{{ ansible_hostname }}
IPV4地址:{{ ansible_default_ipv4.address }}
系统信息:{{ ansible_distribution }} {{ ansible_distribution_version }}
内核版本:{{ ansible_kernel }}
内存容量:{{ ansible_memtotal_mb }}
######################################################################################
Playbook:
---
- hosts: 192.168.2.201
tasks:
- name: "Send file"
template:
src: ./motd.j2
dest: /etc/motd
backup: yes
执行后登录指定机器查看:
特别注意:
templates
模块的用法和 copy 几乎是一模一样,但是 template 能够处理模板文件中的变量,而 copy 不行。
流程控制(处理 handler)
在之前使用的时候会存在一个问题,上一步不管是否执行,只要没有报错,下一步都会执行。这在某些场景是没必要的。
比如更新 nginx 配置,然后重新加载 nginx,如果第一步更新配置文件,发现文件内容并没有变,文件是不会被分发过去的,但是第二步重新加载 nginx 还是会执行。这有时候并不符合用户的预期设想。为了解决这个问题,就需要用到 handler。
在没使用 handler 之前的 Playbook:
---
- hosts: 192.168.2.201
gather_facts: false
tasks:
- name: "Send file"
copy:
src: /tmp/demo.sh
dest: /tmp/abc.sh
backup: yes
- name: "Debug"
debug:
msg: "到执行啦"
无论如何都会执行 Debug,此时修改使用 handler:
---
- hosts: 192.168.2.201
gather_facts: false
tasks:
- name: "Send file"
copy:
src: /tmp/demo.sh
dest: /tmp/abc.sh
backup: yes
notify:
- Debug
handlers:
- name: "Debug"
debug:
msg: "到执行啦"
此时再执行,就不会执行 Debug,因为加了 notify
对当前 TASK 的内容进行了监控,如果有变化,才会执行 handlers 中对应的 TASK。
此时的 Debug 就不再是 TASK,而是 Handler 了。
流程控制(判断 when)
when 一般配合 facts 变量或者 register 变量一起使用,通过对 facts 变量进行判断,进而确定是否执行。
when 的常用配置方式:
when: ( ansible_hostname == "node-01" )
when: ( ansible_hostname is match("node.*|n9e.*") )
when: ( ansible_hostname is not match("node.*|n9e.*") )
使用示例:
---
- hosts: all
tasks:
- name: "Debug"
debug:
msg: "主机 {{ ansible_hostname }} 执行啦"
when: ( ansible_hostname == "node-01" )
如图所示:
流程控制(循环 with-items)
with-items 用于列表的循环,用于重复执行某个 TASK。
Playbook:
---
- hosts: 192.168.2.201
tasks:
- name: "Debug"
debug:
msg: "开始学习:{{ item }}"
with_items:
- "Java"
- "Python"
- "Golang"
如图所示:
在模块中使用 item
变量可以调用到 with_items
列表中定义的值。
有的时候每次循环传递值可能不止一个,此时的 Playbook 就需要改成:
---
- hosts: 192.168.2.201
tasks:
- name: "Debug"
debug:
msg: "{{ item.language }} 已经学习 {{ item.years }} 年啦!"
with_items:
- { language: "Java", years: 3 }
- { language: "Python", years: 5 }
- { language: "Golang", years: 8 }
如图所示:
一般情况下 with_items 已经够用了,如果想要更强大的循环功能,可以使用 loops。
标签设置(tags)
给某些 TASK 打上标签,一般用于调试的时候执行或跳过执行标签的 TASK。
Playbook:
---
- hosts: 192.168.2.201
tasks:
- name: "Debug1"
debug:
msg: "步骤1"
tags:
- step1
- step
- name: "Debug2"
debug:
msg: "步骤2"
tags:
- step2
- step
在执行的时候就可以通过 -t
或者 --tags
指定运行的标签,也可以使用 --skip-tags
跳过某些标签。
ansible-playbook -b 11.tags.yaml -t step1,step2
ansible-playbook -b 11.tags.yaml --skip-tags step1
忽略错误(ignore errors)
在检测语法文件语法的时候,比如 register 设置变量的时候,如果使用 --check
有时会报错,比如:
---
- hosts: 192.168.2.201
tasks:
- name: "Get time"
shell: "date +%F"
register: "time_now"
- name: "Use time"
debug:
msg: "当期时间:{{ time_now.stdout }}"
此时如果执行的时候 -C
或者 --check
检测文件,就会提示因为 time_now 变量没有生成,所有获取 stdout 出错。
为了顺利检测,可以加上忽略错误:
---
- hosts: 192.168.2.201
tasks:
- name: "Get time"
shell: "date +%F"
register: "time_now"
- name: "Use time"
debug:
msg: "当期时间:{{ time_now.stdout }}"
ignore_errors: yes
此时再测试,虽然还是会报错,但是不会影响接下来的流程。
剧本嵌套(include)
在日常使用中,某些剧本是很复杂的,如果都写在一个 yaml 中,会不好维护,所有可以将某些 TASK 拆除出来。然后嵌套回去。
step1.yaml
:
- name: "Step 1"
debug:
msg: "This is step 1"
step2.yaml
- name: "Step 2"
debug:
msg: "This is step 2"
main.yaml
---
- hosts: 192.168.2.201
tasks:
- include_tasks: step1.yaml
- include_tasks: step2.yaml
如图所示:
include_tasks 能满足一定的需求,但是如果某些 task 中需要对主机进行筛选,那就意味着需要写很多 when 对主机进行判断,不是很方便。可以通过 role 的方式解决。
角色(roles)
roles 与其说是一种配置,不如说是一种目录规范,其作用在于让剧本的内容更细的进行更细的分门别类。
基本的目录结构:
demo/
├── basic # 自定义,但需要有意义,而且 role 中会指定该目录
│ ├── files # 存放没有变量的文件,例如安装配置文件,安装包等
│ ├── handlers
│ │ └── main.yml # 用于存放 handler
│ ├── tasks
│ │ └── main.yml # 存放该 role 下的 tasks
│ └── templates # 放置使用了变量的模板,推荐以 .j2 作为后缀
├── hosts # 如果有需要,还可以单独弄一个主机清单,执行的时候通过 -i 指定该文件
└── top.yml # 入口文件
测试 roles 项目:
demo/basic/files/demo.sh
:
#!/bin/bash
echo $date
demo/basic/templates/demo.j2
:
主机名:{{ ansible_hostname }}
demo/basic/handlers/main.yml
:
- name: "Handler1"
debug:
msg: "This is handler 1"
- name: "Handler2"
debug:
msg: "This is handler 2"
demo/basic/tasks/main.yml
:
- name: "Send normal file"
copy:
src: demo.sh
dest: /tmp/a.sh
mode: 0755
backup: yes
notify:
- Handler1
- name: "Send template file"
template:
src: demo.j2
dest: /tmp/
backup: yes
notify:
- Handler2
这里文件和模板都不需要目录名称,ansible 会自己去对应的目录下面找。
demo/top.yml
:
---
- hosts: 192.168.2.201
roles:
- role: basic
对于复杂的需求,一般都会有多个 hosts 或者 role。结果如图所示:
安全设置(vault)
有些时候某些配置文件中可能包含了密码等敏感内容,如果直接明文写,可能会有风险,此时就需要用到 vault 给这个文件加密。
config.yml
:
password: 123456
vault.yaml
:
---
- hosts: 192.168.2.201
vars_files:
- "config.yaml"
tasks:
- name: echo password
debug:
msg: "Password is {{ password }}"
给文件加密:
ansible-vault encrypt config.yaml
此时会让输入密码,按照提示配置就行。完成之后查看 config.yaml 内容就是加密的了。
这时候执行 Playbook 会出现如下错误提示:
ERROR! Attempting to decrypt but no vault secrets found
如果想要使用它,可以跟上询问密码的参数:
ansible-playbook -b vault.yaml --ask-vault-pass
此时就能正常使用了: