第 4-1 章 Linux Ansible 入门知识

作者: admin 分类: ARM64 Linux CICD 研发 发布时间: 2022-07-19 12:11

4.1、Ansible 简介

Ansible 是一个基于 Python 开发的配置管理和应用部署工具,现在也在自动化管理领域大放异彩。它融合了众多老牌运维工具的优点,Pubbet 和 Saltstack 能实现的功能,Ansible 基本上都可以实现。

Ansible 能批量配置、部署、管理一大堆的主机。比如以前需要切换到每个主机上执行的一或多个操作,使用Ansible 只需在固定的一台 Ansible 控制节点上去完成所有主机的操作。

Ansible 是基于模块工作的,它只是提供了一种运行框架,它本身没有完成任务的能力,真正执行操作的是 Ansible 的模块,比如 copy 模块用于拷贝文件到远程主机上,service 模块用于管理服务的启动、停止、重启等。

ansible_arch

Ansible 其中一个比较鲜明的特性是 Agentless,即无 Agent 的存在,它就像普通命令一样,并非 C/S 软件,也只需在某个作为控制节点的主机上安装一次 Ansible 即可,通常它基于 ssh 连接来控制远程主机,远程主机上不需要安装 Ansible 或其它额外的服务。

Ansible 的另一个比较鲜明的特性是它的绝大多数模块都具备幂等性(idempotence)。重复执行某个任务绝大多数时候不会产生任何副作用。

4.2、Ansible 体验

4.2.1、Ansible 环境准备

Ansible 的作用是批量控制其它远程主机,并指挥远程主机节点做一些操作、完成一些任务。所以在这个结构中,分为控制节点和被控制节点。Ansible 是 Agentless 的软件,只需在控制节点安装 Ansible,被控制节点一般不需额外安装任何程序。

注意:Ansible 的模块是用 Python 来执行的,且默认远程连接的方式是 ssh,所以控制端和被控制端都需要有 Python 环境,并且被控制端需要启动 sshd 服务,但通常这两个条件在安装 Linux 系统时就已经具备了。所以使用 Ansible 的安装过程只有一个:在控制端安装 Ansible。

在本文中,将配置如下测试环境:包括一个 Ansible 控制节点和 7 个被控制节点。后面的文章中如果没有特别说明,也都使用此处的主机环境。

注意:该 Ansible 环境是基于 x86 架构,与 ARM64 架构使用方式相同。

主机 IP 地址 主机名 操作系统 Ansible 版本
controller 192.168.95.88 controller Centos79 v2.9.27
node1 192.168.95.131 node1 Centos79
node2 192.168.95.132 node2 Centos79
node3 192.168.95.133 node3 Centos79
node4 192.168.95.134 node4 Centos79
node5 192.168.95.135 node5 Centos79
node6 192.168.95.136 node6 Centos79
node7 192.168.95.137 node7 Centos79

所有主机上都已启动 sshd 服务并监听在 22 端口上。因为有时候也会使用主机名去控制目标节点,所以这里也在 controller 节点上配置了其余 7 个节点的 DNS 解析,可在 controller 节点的 /etc/hosts 文件中加入如下内容:

[root@controller ~]# cat >>/etc/hosts<<EOF
192.168.95.131 node1
192.168.95.132 node2
192.168.95.133 node3
192.168.95.134 node4
192.168.95.135 node5
192.168.95.136 node6
192.168.95.137 node7
EOF

4.2.2、安装 Ansible

官方文档:https://docs.ansible.com/ansible/latest/

对于 RHEL 系列的系统来说,配置好 epel 镜像即可安装最新版的 Ansible。使用阿里云的 epel 源。

[root@controller ~]# wget -O /etc/yum.repos.d/epel.repo http://mirrors.aliyun.com/repo/epel-7.repo
[root@controller ~]# yum info ansible
Loaded plugins: fastestmirror
Loading mirror speeds from cached hostfile
 * base: mirrors.aliyun.com
 * extras: mirrors.njupt.edu.cn
 * updates: mirrors.aliyun.com
Available Packages
Name        : ansible
Arch        : noarch
Version     : 2.9.27
Release     : 1.el7
Size        : 17 M
Repo        : epel/x86_64
Summary     : SSH-based configuration management, deployment, and task execution system
URL         : http://ansible.com
License     : GPLv3+
Description : Ansible is a radically simple model-driven configuration management,
            : multi-node deployment, and remote task execution system. Ansible works
            : over SSH and does not require any software or daemons to be installed
            : on remote nodes. Extension modules can be written in any language and
            : are transferred to managed machines automatically.

然后安装即可:

[root@controller ~]# yum install ansible -y

Ansible 每个版本释放出来之后,都首先提交到 Pypi,所以任何操作系统,都可以使用 pip 工具来安装最新版的 Ansible。

[root@controller ~]# pip3 install ansible
  • 注意:python2.x 的环境和 python3.x 的环境是不兼容的,需要注意区分。
  • 注意:使用各系统的包管理工具(如 yum)安装 Ansible 时会自动提供一些配置文件,如 /etc/ansible/ansible.cfg。而使用 pip 安装的 Ansible 默认不提供配置文件。

4.2.3、Ansible 命令补全

从 Ansible 2.9 版本开始,它支持命令的选项补全功能,它依赖于 python 的 argcomplete 插件。

安装 argcomplete:

# CentOS/RHEL
yum -y install python-argcomplete

# 任何系统都可以使用pip工具安装argcomplete
pip3 install argcomplete

安装完成后,还需激活该插件:

# 要求bash版本大于等于4.2
sudo activate-global-python-argcomplete

# 如果bash版本低于4.2,则单独为每个ansible命令注册补全功能
eval $(register-python-argcomplete ansible)
eval $(register-python-argcomplete ansible-config)
eval $(register-python-argcomplete ansible-console)
eval $(register-python-argcomplete ansible-doc)
eval $(register-python-argcomplete ansible-galaxy)
eval $(register-python-argcomplete ansible-inventory)
eval $(register-python-argcomplete ansible-playbook)
eval $(register-python-argcomplete ansible-pull)
eval $(register-python-argcomplete ansible-vault)

最后,退出当前 Shell 重新进入,或者简单的直接执行如下命令即可:

exec $SHELL

然后就可以按 tab 一次或两次补全参数或提示参数。例如,下面选项输入到一半的时候,按一下 tab 键就会补全得到 ansible --syntax-check。

ansible --syn

4.2.4、配置主机互信

因为 Ansible 默认是基于 ssh 连接的,所以要控制其它节点首先需要建立好 ssh 连接,而建立 ssh 连接要么需要提供密码,要么需要配置好认证方式。为了方便后文的测试,这里先配置好 controller 和其它被控节点之间的主机互信。

为了避免配置主机互信过程中的交互式询问,这里使用 ssh-keyscan 工具添加主机认证信息以及 sshpass 工具(安装 Ansible 时会自动安装 sshpass,也可以 yum -y install sshpass 安装)直接指定 ssh 连接密码。

  1. 在 controller 节点上生成密钥对
ssh-keygen -t rsa -f ~/.ssh/id_rsa -N ''
  1. 将各节点的主机信息(host key)写入 controller 的 ~/.ssh/known_hosts 文件
for host in node{1..7};do
  ssh-keyscan $host >>~/.ssh/known_hosts 2>/dev/null
done
  1. 将 controller 上的 ssh 公钥分发给各节点
# sshpass -p 选项指定的是密码
for host in node{1..7};do
  sshpass -p'123456' ssh-copy-id root@$host &>/dev/null
done

注意:通过上面三步可以完成 controller 到其它 7 个主机的免密认证,但是反过来其它主机到 controller 不是免密认证。

4.2.5、Ansible 体验

前面我们提到过,Ansible 的功能之所以强大,是因为 Ansible 提供了成千上万个模块,每个模块都对应一个功能。其中 100 多个核心模块是 Ansible 团队自己维护的,剩余的模块都是 Ansible 社区维护。另外,Ansible 也允许用户自定义模块,解决某些个性化的问题 。

在控制节点上执行:

[root@controller ~]# ansible localhost -m copy -a 'src=/etc/passwd dest=/tmp'
localhost | CHANGED => {
    "changed": true,
    "checksum": "f46e74616780e28c950837c16571665b058c3233",
    "dest": "/tmp/passwd",
    "gid": 0,
    "group": "root",
    "md5sum": "388824fe0e2029fba5ef752f0e0fab2c",
    "mode": "0644",
    "owner": "root",
    "size": 798,
    "src": "/root/.ansible/tmp/ansible-tmp-1657750978.04-11732-253772452173510/source",
    "state": "file",
    "uid": 0
}
  • 该命令的作用是拷贝本机的 /etc/passwd 文件到本机的 /tmp 目录下。
  • 其中的 ansible 是一个命令,这个自不需要多做解释。除 ansible 命令外,后面还会用到 ansible-playbook 命令。
  • localhost 参数表示 ansible 要控制的节点,即 ansible 将指挥本机执行任务。
  • 执行任务主要是执行模块,模块的执行还可以依赖一些模块参数。在 ansible 命令行中,使用 -m Module 来指定要执行哪个模块,即执行什么任务,使用 -a ARGS 来指定模块运行时的参数。
  • 本示例中的模块为 copy 模块,传递给 copy 模块的参数包含两项:
    • src=/etc/passwd 指定源文件
    • dest=/tmp 指定拷贝的目标路径

初学之时,只需学习一些常用的模块,在本文以及后面的文章中会介绍一些常见模块的功能以及用法。熟悉了 Ansible 之后,再按需求到官方手册中去寻找:https://docs.ansible.com/ansible/latest/modules/modules_by_category.html

有时候为了方便快速寻找模块,可以使用 ansible-doc -l | grep 'xxx' 命令来筛选模块。例如,想要筛选具有拷贝功能的模块:

[root@controller ~]# ansible-doc -l | grep "copy"
vsphere_copy                                                  Copy a file to a VMware datastore
win_copy                                                      Copies files to remote locations on windows ho...
bigip_file_copy                                               Manage files in datastores on a BIG-IP
ec2_ami_copy                                                  copies AMI between AWS regions, return new ima...
win_robocopy                                                  Synchronizes the contents of two directories u...
copy                                                          Copy files to remote locations
na_ontap_lun_copy                                             NetApp ONTAP copy LUNs
icx_copy                                                      Transfer files from or to remote Ruckus ICX 70...
unarchive                                                     Unpacks an archive after (optionally) copying ...
ce_file_copy                                                  Copy a file to a remote cloudengine device ove...
postgresql_copy                                               Copy data between a file/program and a Postgre...
ec2_snapshot_copy                                             copies an EC2 snapshot and returns the new Sna...
nxos_file_copy                                                Copy a file to a remote NXOS device
netapp_e_volume_copy                                          NetApp E-Series create volume copy pairs

根据描述,大概找出是否有想要的模块。找到模块后,想要看看它的功能描述以及用法,可以继续使用 ansible-doc 命令。

# 详细的模块描述手册
ansible-doc copy

# 只包含模块参数用法的模块描述手册
ansible-doc -s copy

再来一个示例,通过 Ansible 删除本地文件 /tmp/passwd。需要使用的模块是 file 模块,file 模块的主要作用是创建或删除文件/目录。

ansible localhost -m file -a 'path=/tmp/passwd state=absent'
  • 参数 path= 指定要操作的文件路径,state= 参数指定执行何种操作,此处指定为 absent 表示删除操作。

Ansible 的很多模块都提供了一个 state 参数,它是一个非常重要的参数。它的值一般都会包含 present 和 absent 两种状态值(并非一定),不同模块的 present 和 absent 状态表示的含义不同,但通常来说,present 状态表示肯定、存在、会、成功等含义,absent 则相反,表示否定、不存在、不会、失败等含义。

例如这里的 file 模块,absent 状态表示递归删除文件 / 目录,类似于 rm -r 命令,touch 状态和 touch 命令的功能一样,directory 状态表示递归创建目录,类似于 mkdir -p 命令。

所以,在本地创建文件、创建目录的命令如下:

# 创建文件
ansible localhost -m file -a 'path=/tmp/a.log state=touch'

# 创建目录
ansible localhost -m file -a 'path=/tmp/dir1/dir2 state=directory'

再说一个 debug 模块,顾名思义,用于输出或调试一些数据。debug 模块的用法非常简单,就两个常用参数:msg 参数和 var 参数。这两个参数是互斥的,所以只能使用其中一个。msg 参数可以输出字符串,也可以输出变量的值;var 参数只能输出变量的值。

例如,输出 ”hello world”,需要使用 msg 参数:

[root@controller ~]# ansible localhost -m debug -a 'msg="hello world"'
localhost | SUCCESS => {
    "msg": "hello world"
}

Ansible 中也支持使用变量,这里仅演示最简单的设置变量和引用变量的方式。ansible 命令的 -e 选项或 --extra-vars 选项可以设置变量,设置的方式为 -e 'var1="aaa" var2="bbb"'

例如,设置变量后使用 debug 的 msg 参数输出:

[root@controller ~]# ansible localhost -e 'str=world' -m debug -a 'msg="hello {{str}}"'
localhost | SUCCESS => {
    "msg": "hello world"
}
  • 注意上面示例中的 msg="hello {{str}}",Ansible 的字符串是可以不用引号去包围的,例如 msg=hello 是允许的,但如果字符串中包含了特殊符号,则可能需要使用引号去包围,例如此处的示例出现了会产生歧义的空格。
  • 此外,要区分变量名和普通的字符串,需要在变量名上加一点标注:用 {} 包围 Ansible 的变量,这其实是 Jinja2 模板的语法。其实不难理解,它的用法和 Shell 下引用变量使用 $ 符号或 ${} 是一样的,例如 echo "hello ${var}"

debug 模块除了 msg 参数,还有一个 var 参数,它只能用来输出变量(还包括以后要介绍的 Jinja2 表达式),而且 var 参数引用变量的时候,不能使用 {} 包围,因为 var 参数已经隐式地包围了一层 {}。例如:

[root@controller ~]# ansible localhost -e 'str="hello world"' -m debug -a 'var=str'
localhost | SUCCESS => {
    "str": "hello world"
}

4.3、Ansible 配置文件

通过操作系统自带的包管理器(比如 yum、dnf、apt)安装的 Ansible 一般都会提供好 Ansible 的配置文件 /etc/ansible/ansible.cfg。

这是 Ansible 默认的全局配置文件。实际上,Ansible 支持 4 种方式指定配置文件,它们的解析顺序从上到下:

  • ANSIBLE_CFG:环境变量指定的配置文件。
  • ansible.cfg:当前目录下的 ansible.cfg。
  • ~/.ansible.cfg:家目录下的 ansible.cfg。
  • /etc/ansible/ansible.cfg:默认的全局配置文件。

Ansible 配置文件采用 ini 风格进行配置,每一项配置都使用 key=value 的方式进行配置。

例如,下面是我从默认的 /etc/ansible/ansible.cfg 中截取的 [defaults] 段落和 [inventory] 段落的部分配置信息。

[defaults]
# some basic default values...
#inventory      = /etc/ansible/hosts
#library        = /usr/share/my_modules/
#module_utils   = /usr/share/my_module_utils/
#remote_tmp     = ~/.ansible/tmp
#local_tmp      = ~/.ansible/tmp
#plugin_filters_cfg = /etc/ansible/plugin_filters.yml
#forks          = 5

[inventory]
#enable_plugins = host_list, virtualbox, yaml, constructed
#ignore_extensions = .pyc, .pyo, .swp, .bak, ~, .rpm, .md, .txt, ~, .orig, .ini, .cfg, .retry
#ignore_patterns=
#unparsed_is_failed=False

4.4、Inventory

让 Ansible 发挥强大作用的第一步是配置 inventory。inventory 表示清单的意思,在计算机领域里往往表示的资源清单,在 Ansible 中它表示主机节点清单,也是资源的一种。通过配置 inventory,就可以定义哪些目标主机是可以被控制的。

4.4.1、Inventory 文件路径

默认的 inventory 文件是 /etc/ansible/hosts,可以通过 Ansible 配置文件的 inventory 配置指令去修改路径。

grep '/etc/ansible/hosts' /etc/ansible/ansible.cfg 
#inventory      = /etc/ansible/hosts

但通常不会去修改这个配置项,如果在其它地方定义了 inventory 文件,可以直接在 ansible 的命令行中使用 -i 选项去指定自定义的 inventory 文件。

ansible -i /tmp/myinv.ini ...
ansible-playbook -i /tmp/myinv.ini ...

4.4.2、配置 Inventory

Ansible inventory 文件的书写格式遵循 ini 配置格式。从 Ansible 2.4 开始支持其它格式,比如 yaml 格式的 inventory。此处以 ini 格式为例,循序渐进地介绍 inventory 的规则。假设所有的规则都定义在 /etc/ansible/hosts 文件中。

4.4.2.1、一行一主机的定义方式

Ansible 默认是基于 ssh 连接的,所以一般情况下 inventory 中的每个目标节点都配置主机名或 IP 地址、sshd 监听的端口号、连接的用户名和密码、ssh 连接时的参数等等。当然,很多参数有默认值,所以最简单的是直接指定主机名或 IP 地址即可。

例如,在默认的 inventory 文件 /etc/ansible/hosts 添加几个目标主机:

node1
node2 ansible_host=192.168.95.132
192.168.95.133
192.168.95.134:22
192.168.95.13[5:6] ansible_port=22
  • 第一行通过主机名定义,在 ansible 连接该节点时会进行主机名 DNS 解析。
  • 第二行也是通过主机名定义,但是使用了一个主机变量 ansible_host=IP,此时该 ansible 去连接该主机时将直接通过 IP 地址进行连接,而不会进行 DNS 解析,此时 node2 相当于主机别名并且可以命名为任何其它名称。
  • 第三行通过 IP 地址定义主机节点。
  • 第四行通过 IP 地址和端口号定义主机节点。
  • 最后一行通过范围的方式展开成了两个主机节点 192.168.95.135 和 192.168.95.136,同时还定义了这两个节点的主机变量 ansible_port=22 表示连接这两个主机时的端口号为 22。

范围展开的方式还支持字母范围。下面都是有效的:

范围表示      展开结果
--------------------
a[1:3]  -->  a1,a2,a3
[08:12] -->  08,09,10,11,12
a[a:c]  -->  aa,ab,ac

上面示例中使用了两个主机变量 ansible_port 和 ansible_host,它们直接定义在主机的后面,这些变量都是连接目标主机时的行为控制变量,通常它们都能见名知意。Ansible 支持很多个连接时的行为控制变量,而且不同版本的 Ansible 的行为控制变量名称可能还不同,比如在以前版本中指定端口号的行为变量是 ansible_ssh_port。

完整的连接行为控制变量参见官方手册

这样定义之后,Ansible 就可以控制任何一个目标主机了:

ansible node1 -m copy -a 'src=/etc/passwd dest=/tmp'
ansible 192.168.95.133 -m copy -a 'src=/etc/passwd dest=/tmp'

4.4.2.2、Inventory 中的普通变量

在定义 inventory 时,除了可以指定连接的行为控制变量,也可以指定 Ansible 的普通变量,以便在 ansible 执行任务时使用。

node1 node1_var="hello world"
node2 ansible_host=192.168.95.132

在 ansible 执行任务时可以引用普通变量:

$ ansible node1 -m debug -a 'var=node1_var'
node1 | SUCCESS => {
    "node1_var": "hello world"
}

4.4.2.3、主机分组

上面的示例中是每行单独定义一个主机,这样的方式虽然简单,但是极其不方便管理多个节点。为此,Inventory 支持对主机进行分组,每个组内可以定义多个主机,每个主机都可以定义在任何一个或多个主机组内。

node1 node1_var="hello world"
node2 ansible_host=192.168.95.132
192.168.95.133
192.168.95.134:22
192.168.95.13[5:6] ansible_port=22

[nginx]
192.168.95.131
192.168.95.132 ansible_password='123456'
192.168.95.133

[apache]
192.168.95.13[4:7]

[mysql]
192.168.95.131
192.168.95.132
  • 这里定义了 3 个主机组:nginx 主机组、apache 主机组和 mysql 主机组。nginx 组包含 3 个节点,apache 主机组包含 4 个节点,mysql 主机组包含 2 个节点。
  • Ansible 默认预定义了两个主机组:all 分组和 ungrouped 分组。
    • all 分组中包含所有分组内的节点
    • ungrouped 分组包含所有不在分组内的节点
    • 这两个分组都不包含 localhost 这个特殊的节点

需要注意的是,mysql 组中的节点也同时存在于 nginx 组内,一个主机同时存在于多个组内是允许也是必要的功能,只有这样才能更为灵活的对各个节点进行分类管理。

有了主机组,就可以让 ansible 控制一个组,从而让该组内所有主机执行任务:

ansible apache -m copy -a 'src=/etc/passwd dest=/tmp'

定义了 inventory 之后,可以使用 ansible --list 或 ansible--playbook --list 命令来查看主机组的信息,还可以使用更为专业的 ansible-inventory 命令来查看主机组信息。

# 使用ansible或ansible-playbook列出所有主机
[root@controller ~]# ansible -i /etc/ansible/hosts nginx --list
  hosts (3):
    192.168.95.131
    192.168.95.132
    192.168.95.133

# 使用ansible-inventory列出nginx组中的主机
[root@controller ~]# ansible-inventory -i /etc/ansible/hosts nginx --graph
@nginx:
  |--192.168.95.131
  |--192.168.95.132
  |--192.168.95.133

# 使用ansible-inventory列出nginx组中的主机,同时带上变量
[root@controller ~]# ansible-inventory nginx --graph --vars
@nginx:
  |--192.168.95.131
  |--192.168.95.132
  |  |--{ansible_password = 123456}
  |--192.168.95.133

# 使用ansible-inventory列出all组内的主机
[root@controller ~]# ansible-inventory --graph all
@all:
  |--@apache:
  |  |--192.168.95.134
  |  |--192.168.95.135
  |  |--192.168.95.136
  |  |--192.168.95.137
  |--@mysql:
  |  |--192.168.95.131
  |  |--192.168.95.132
  |--@nginx:
  |  |--192.168.95.131
  |  |--192.168.95.132
  |  |--192.168.95.133
  |--@ungrouped:
  |  |--node1
  |  |--node2

# 使用ansible-inventory以json格式列出所有主机的信息
[root@controller ~]# ansible-inventory --list

4.4.2.4、主机组变量

有了主机组之后,可以直接为主机组定义变量,这样组内的所有主机都具有该变量。

[nginx]
192.168.95.131
192.168.95.132 ansible_password='123456'
192.168.95.133

[nginx:vars]
ansible_password='123456'

[all:vars]
ansible_port=22

[ungrouped:vars]
ansible_port=22
  • 上面 [nginx:vars] 表示为 nginx 组内所有主机定义变量 ansible_password='123456'。而 [all:vars][ungrouped:vars] 分别表示为 all 和 ungrouped 这两个特殊的主机组内的所有主机定义变量。

4.4.2.5、组嵌套

Inventory 还支持主机组的分组嵌套,可以通过 [GROUP:children] 的方式定义一个主机组,并在其中包含子组。

[nginx]
192.168.95.131
192.168.95.132 ansible_password='123456'
192.168.95.133

[apache]
192.168.95.13[4:7]

[mysql]
192.168.95.131
192.168.95.132

[webservers:children]
nginx
apache

[centos7:children]
webservers  # 递归嵌套
mysql

4.4.2.6、Multi Inventory 文件

当 Ansible 要管理的节点非常多时,仅靠分组的逻辑可能也不足够方便管理,这个时候可以定义多个 inventory 文件并放在一个目录下,并按一定的命名规则为每个 inventory 命名,以便见名知意。

例如,创建一个名为 /etc/ansible/inventorys 的目录,在其中定义 web 和 database 两个 inventory 文件:

/etc/ansible/inventorys/
├── web
└── database

内容分别如下:

# /etc/ansible/inventorys/web的内容:
[nginx]
192.168.95.131
192.168.95.132 ansible_password='123456'
192.168.95.133

[apache]
192.168.95.13[4:7]

[web:children]
apache
nginx

# /etc/ansible/inventorys/database的内容:
[mysql]
192.168.95.131
192.168.95.132

现在要使用多个 inventory 的功能,需要将 inventory 指定为目录路径。

例如,Ansible 配置文件将 inventory 指令设置为对应的目录:

inventory      = /etc/ansible/inventorys

或者,ansible 或 ansible-playbook 命令使用 -i INVENTORY 选项指定的路径应当为目录。

执行下面的命令将列出所有主机:

ansible-inventory -i /etc/ansible/inventorys --graph all

inventory 指定为目录时,inventory 文件最好不要带有后缀,就像示例中的 web 和 database 文件。因为 Ansible 当使用目录作为 inventory 时,默认将忽略一些后缀的文件,不去解析。需要修改配置文件中的 inventory_ignore_extensions 项来禁止忽略指定后缀(如 ini 后缀)的文件。

#inventory_ignore_extensions = ~, .orig, .bak, .ini, .cfg, .retry, .pyc, .pyo
inventory_ignore_extensions = ~, .orig, .bak, .cfg, .retry, .pyc, .pyo

4.4.3、实验 Inventory 文件

下面是本文使用的 inventory 文件 /etc/ansible/hosts 的内容,在后面的文章中如果没有特别说明,也将使用此处的 inventory 配置。

[nginx]
192.168.95.131
192.168.95.132
192.168.95.133

[apache]
192.168.95.13[4:7]

[webservers:children]
nginx
apache

[mysql]
192.168.95.137

4.5、Playbook

ansible 命令每次只能执行一个任务,这种运行方式称为 Ad-hoc(点对点模式);ansible playbook 可以集成多个任务,编排好任务的执行顺序,像电影剧本一样一直演下去直到杀青。

4.5.1、playbook、play 和 task 的关系

playbook 相当于一整套电视剧剧本,play 相当于一集片段,tasks 相当于一集片段当中的某一节剧情。

  1. playbook 中可以定义一个或多个 play。
  2. 每个 play 中可以定义一个或多个 task。
    • 其中还可以定义两类特殊的 task:pre_tasks 和 post_tasks。
      • pre_tasks 表示执行执行普通任务之前执行的任务列表。
      • post_tasks 表示普通任务执行完之后执行的任务列表。
  3. 每个 play 都需要通过 hosts 指令指定要执行该 play 的目标主机。
  4. 每个 play 都可以设置一些该 play 的环境控制行为,比如定义 play 级别的变量。

例如,下面是一个 playbook 示例,文件名为 first.yml,内容如下:

---
- name: play 1
  hosts: nginx
  gather_facts: false
  tasks: 
    - name: task1 in play1
      debug: 
        msg: "output task1 in play1"

    - name: task2 in play1
      debug: 
        msg: "output task2 in play1"

- name: play 2
  hosts: apache
  gather_facts: false
  tasks: 
    - name: task1 in play2
      debug: 
        msg: "output task1 in play2"

    - name: task2 in play2
      debug: 
        msg: "output task2 in play2"
  • playbook 中包含两个 play:”play 1” 和 ”play 2”,每个 play 中又包含了两个 task。且执行 ”play 1” 的是 nginx 主机组中的主机节点,执行 ”play 2” 的是 apache 主机组中的主机节点。

使用 ansible-playbook 命令执行这个 playbook:

[root@controller ~]# ansible-playbook first.yml

PLAY [play 1] ***************************************************************************************************

TASK [task1 in play1] *******************************************************************************************
ok: [192.168.95.131] => {
    "msg": "output task1 in play1"
}
ok: [192.168.95.132] => {
    "msg": "output task1 in play1"
}
ok: [192.168.95.133] => {
    "msg": "output task1 in play1"
}
......
PLAY RECAP ******************************************************************************************************
192.168.95.131             : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
192.168.95.132             : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
192.168.95.133             : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
192.168.95.134             : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
192.168.95.135             : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
192.168.95.136             : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
192.168.95.137             : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

对照输出结果和 first.yml 中的定义,可以很容易对应上执行的流程。

4.5.2、YAML 语法

ansible 的 playbook 采用 yaml 语法,它以非常简洁的方式实现了 json 格式的事件描述。yaml 之于 json 就像 markdown 之于 html 一样,极度简化了 json 的书写。

YAML 文件后缀通常为 .yaml.yml

YAML 在不少工具里都使用,学习它是”一次学习、终生受益”的,所以很有必要把 yaml 的语法格式做个梳理,系统性地去学一学。

YAML 的基本语法规则如下:

  • 使用缩进表示层级关系。
  • 缩进时不允许使用 Tab 键,只允许使用空格。
  • 缩进的空格数目不重要,只要相同层级的元素左对齐即可。
  • yaml 文件以 "---" 作为文档的开始,以表明这是一个 yaml 文件。当然 "---" 可以省略。
  • # 表示注释,从这个字符开始到结尾,都会被解析器忽略。
  • 字符串不用加引号,但在可能产生歧义时,加引号(单双引号都可以)。
  • 布尔值非常灵活,不区分大小写的 true/false、yes/no、on/off、y/n、0 和 1 都允许。

YAML 支持三种数据结构:

  • 对象:key/value 格式,也称为哈希结构、字典结构或关联数组。
  • 数组:也称为列表。
  • 标量(scalars):单个值。

可以去找一些在线 YAML 转换 JSON 网站,比如http://yaml-online-parser.appspot.com,通过在线转换可以验证或查看自己所写的 YAML 是否出错以及哪里出错。也可以安装 yq(yaml query) 命令将 yaml 数据转换成 json 格式数据。

yum -y install jq
pip3 install yq
cat a.yml | yq .

4.5.2.1、对象

一组键值对,使用冒号隔开 key 和 value。注意,冒号后必须至少一个空格。

name: junmajinlong

等价于 json:

{
  "name": "junmajinlong"
}

4.5.2.2、数组

---
- Shell
- Perl
- Python

等价于 json:

["Shell","Perl","Python"]

也可以使用行内数组(内联语法)的写法:

---
["Shell","Perl","Python"]

再例如:

---
- lang1: Shell
- lang2: Perl
- lang3: Python

等价于 json:

[
  {"lang1": "Shell"}, 
  {"lang2": "Perl"}, 
  {"lang3": "Python"}
]

将对象和数组混合:

---
languages:
  - Shell
  - Perl
  - Python

等价于 json:

{
  "languages": ["Shell","Perl","Python"]
}

4.5.2.3、字典

---
person1:
  name: junmajinlong
  age: 18
  gender: male

person2:
  name: xiaofanggao
  age: 19
  gender: female

等价于 json:

{
  "person2": {
    "gender": "female", 
    "age": 19, 
    "name": "xiaofanggao"
  }, 
  "person1": {
    "gender": "male", 
    "age": 18, 
    "name": "junmajinlong"
  }
}

也可以使用行内对象的写法:

---
person1: {name: junmajinlong, age: 18, gender: male}

4.5.2.4、复合结构

---
- person1:
  name: junmajinlong
  age: 18
  langs:
    - Perl
    - Ruby
    - Shell

- person2:
  name: xiaofanggao
  age: 19
  langs:
    - Python
    - Javascript

等价于 json:

[
  {
    "langs": [
      "Perl", 
      "Ruby", 
      "Shell"
    ], 
    "person1": null, 
    "age": 18, 
    "name": "junmajinlong"
  }, 
  {
    "person2": null, 
    "age": 19, 
    "langs": [
      "Python", 
      "Javascript"
    ], 
    "name": "xiaofanggao"
  }
]

4.5.2.5、字符串续行

字符串可以写成多行,从第二行开始,必须至少有一个单空格缩进。换行符会被转为空格。

str: hello
  world
  hello world

等价于 json:

{
  "str": "hello world hello world"
}

也可以使用 > 换行,它类似于上面的多层缩进写法。此外,还可以使用 | 在换行时保留换行符。

this: |
  Foo
  Bar
that: >
  Foo
  Bar

等价于 json:

{'that': 'Foo Bar', 'this': 'Foo\nBar\n'}

4.5.2.6、空值

YAML 中某个 key 有时候不想为其赋值,可以直接写 key 但不写 value,另一种方式是直接写 null,还有一种比较少为人知的方式:波浪号 ~

例如,下面几种方式全是等价的:

key1: 
key2: null
key3: Null
key4: NULL
key5: ~

4.5.2.7、单双引号和转义

YAML 中的字符串是可以不用使用引号包围的,但是如果包含了特殊符号,则需要使用引号包围。

单引号包围字符串时,会将特殊符号保留。

双引号包围字符串时,反斜线需要额外进行转义。

例如,下面几对书写方式是等价的:

- key1: "~"
- key2: '~'

- key3: '\.php$'
- key4: "\\.php$"
- key5: \.php$

- key6: \n
- key7: '\n'
- key8: "\\n"

等价于 json:

[
  { "key1": "~" },
  { "key2": "~" },
  { "key3": "\\.php$" },
  { "key4": "\\.php$" },
  { "key5": "\\.php$" },
  { "key6": "\\n" },
  { "key7": "\\n" },
  { "key8": "\\n" }
]

4.5.3、playbook 的写法

了解 YAML 写法之后,就可以来写 Ansible 的 playbook 了。

回顾一下前文对 playbook、play 和 task 关系的描述,playbook 可以包含一个或多个 play,每个 play 可以包含一个或多个任务,且每个 play 都需要指定要执行该 play 的目标主机。

于是,将下面这个 ad-hoc 模式的 ansible 任务改成等价的 playbook 模式:

$ ansible nginx -m copy -a 'src=/etc/passwd dest=/tmp'

假设这个 playbook 的文件名为 copy.yml,其内容如下:

---
- hosts: nginx
  gather_facts: false

  tasks: 
    - copy: src=/etc/passwd dest=/tmp

然后使用 ansible-playbook 命令执行该 playbook。

$ ansible-playbook copy.yml

再来解释一下这个 playbook 文件的含义。

playbook 中,每个 play 都需要放在数组中,所以在 playbook 的顶层使用列表的方式 - xxx: 来表示这是一个 play(此处是 - hosts:)。

每个 play 都必须包含 hoststasks 指令。

hosts 指令用来指定要执行该 play 的目标主机,可以是主机名,也可以是主机组,还支持其它方式来更灵活的指定目标主机。具体的规则后文再做介绍。

tasks 指令用来指定这个 play 中包含的任务,可以是一个或多个任务,任务也需要放在 play 的数组中,所以 tasks 指令内使用 - xxx: 的方式来表示每一个任务(此处是 - copy:)。

gather_facts 是一个 play 级别的指令设置,它是一个负责收集目标主机信息的任务,由 setup 模块提供。默认情况下,每个 play 都会先执行这个特殊的任务,收集完信息之后才开始执行其它任务。

但是,收集目标主机信息的效率很低,如果能够确保 playbook 中不会使用到所收集的信息,可以显式指定 gather_facts: no 来禁止这个默认执行的收集任务,这对效率的提升是非常可观的。

此外每个 play 和每个 task 都可以使用 name 指令来命名,也建议尽量为每个 play 和每个 task 都命名,且名称具有唯一性。

所以,将上面的 playbook 改写:

---
- name: first play
  hosts: nginx
  gather_facts: false

  tasks: 
    - name: copy /etc/passwd to /tmp
      copy: src=/etc/passwd dest=/tmp

4.5.4、playbook 模块参数的传递方式

在刚才的示例中,copy 模块的参数传递方式如下:

tasks: 
  - name: copy /etc/passwd to /tmp
    copy: src=/etc/passwd dest=/tmp

这是标准的 yaml 语法,参数部分 src=/etc/passwd dest=/tmp 是一个字符串,当作 copy 对应的值。

根据前面介绍的 yaml 语法,还可以换行书写。有以下几种方式:

---
- name: first play
  hosts: nginx
  gather_facts: false
  tasks: 
    - copy: 
        src=/etc/passwd dest=/tmp

    - copy: 
        src=/etc/passwd
        dest=/tmp

    - copy: >
        src=/etc/passwd
        dest=/tmp

    - copy: |
        src=/etc/passwd
        dest=/tmp

除此之外,Ansible 还提供了另外两种传递参数的方式:

  • 将参数和参数值写成 key: value 的方式
  • 使用 args 参数声明接下来的是参数

通过示例便可对其用法一目了然:

---
- name: first play
  hosts: nginx
  gather_facts: false
  tasks: 
    - name: copy1
      copy: 
        src: /etc/passwd
        dest: /tmp

    - name: copy2
      copy: 
      args:
        src: /etc/passwd
        dest: /tmp

大多数时候,使用何种方式传递参数并无关紧要,只要个人觉得可读性高、方便、美观即可。

4.5.5、play 的目标主机

每一个 play 都包含 hosts 指令,它用来指示在解析 inventory 之后选择哪些主机执行该 play 中的 tasks。

hosts 指令通过 pattern 的方式来筛选节点,pattern 的指定方式有以下几种规则:

  1. 直接指定 inventory 中定义的主机名
    • hosts: localhost
  2. 直接指定 inventory 中的主机组名
    • hosts: nginx
    • hosts: all
  3. 使用组名时,可以使用数值索引的方式表示组中的第几个主机
    • hosts: nginx[1]:mysql[0]
  4. 可使用冒号或逗号隔开多个 pattern
    • hosts: nginx:localhost
  5. 可以使用范围表示法
    • hosts: 192.168.200.3[0:3]
    • hosts: web[A:D]
  6. 可以使用通配符 *
    • hosts: *.example.com
    • hosts: *,这等价于 hosts: all
  7. 可以使用正则表达式,需使用 ~ 开头
    • hosts: ~(web|db)\.example\.com

此外:

  1. 所有 pattern 选中的主机都是包含性的,第一个 pattern 选中的主机会添加到下一个 pattern 的范围内,直到最后一个 pattern 筛选完,于是取得了所有 pattern 匹配的主机。
  2. pattern 前面加一个 & 符号表示取交集
    • pattern1:&pattern2 要求同时存在于 pattern1 和 pattern2 中的主机
  3. pattern 前面加一个 ! 符号表示排除
    • pattern1:!pattern2 要求出现在 pattern1 中但未出现在 pattern2 中

4.6、默认的任务执行策略

假设有 10 个目标节点要执行某个 play 中的 3 个任务:tA、tB、tC。

默认情况下,会从 10 个目标节点中选择 5 个节点作为第一批次的节点执行任务 tA,第一批次的 5 个节点都执行 tA 完成后,将选择剩下的 5 个节点作为第二批次执行任务 tA。

所有节点都执行完任务 tA 后,第一批次的 5 节点开始执行任务 tB,然后第二批次的 5 个节点执行任务 tB。

所有节点都执行完任务 tB 后,第一批次的 5 节点开始执行任务 tC,然后第二批次的 5 个节点执行任务 tC。

整个过程如下:

ansible_forks

这个流程图虽然简单形象,但是不严谨,稍后会解释为何不严谨。

这里提到的 5 个节点的数量 5,是由配置文件中 forks 指令的值决定的,默认值为 5。

$ grep 'fork' /etc/ansible/ansible.cfg
#forks          = 5

forks 指令用来指定 Ansible 最多要创建几个子进程来执行任务,每个节点默认对应一个 ansible-playbook 进程和 ssh 进程,例如 forks=5 表示最多创建 5 个 ansible-playbook 子进程。所以,forks 的值也代表了最多有几个节点同时执行任务。

例如,将 hosts 指令指定为 all,并将 gather_facts 指令取消注释,因为这个任务执行比较慢,方便观察进程列表。

---
- name: first play
  hosts: all 
  #gather_facts: false

执行该 playbook。然后在另外一个终端上去查看进程列表:

[root@controller ~]# ansible-playbook first.yml
[root@controller ~]# pstree -c | grep 'ansible'
        |-sshd-+-sshd---bash---ansible-playboo-+-ansible-playboo---ssh
        |      |                               |-ansible-playboo---ssh
        |      |                               |-ansible-playboo---ssh
        |      |                               |-ansible-playboo---ssh
        |      |                               |-ansible-playboo
        |      |                               `-{ansible-playboo}
  • 结果表明 forks=5 时共有 6 个 Ansible 进程,其中父进程是 Ansible 主控进程,负责监控节点执行任务的状态以及创建子进程来执行任务。
  • 如果某个节点连接失败或执行某个任务失败,则该节点将不再执行该 play 中的后续任务(但会执行后续的 play)。

根据上面对 forks 指令的效果描述,前面的执行策略流程图并不严谨。因为 forks 的效果并不是选中一批节点,本批节点执行完任务才选下一批节点。

forks 是保证最多有 N 个节点同时执行任务,但有的节点可能执行任务较慢。比如有 10 个节点,且 forks=5 时,第一批选中 5 个节点执行任务,假如第 1 个节点先执行完任务,Ansible 主控进程不会等待本批中其它 4 个节点执行完任务,而是直接创建一个新的 Ansible 进程,让第 6 个节点执行任务。

4.7、综合案例

在生产环境中,有大量的集群需要进行初始化工作,这里使用 playbook 写一个批量初始化服务器的案例。

参见批量初始化服务器

标签云