Skip to main content

第16章 使用 Ansible 服务实现自动化运维

章节简述

Ansible 是近年来备受瞩目的一款开源运维自动化工具,堪称运维领域的翘楚。它极大地提升了运维人员的工作效率,同时减少了人为错误的发生,可谓事半功倍。Ansible 包含数千个功能丰富且实用的模块,并附有详尽的帮助信息,即使是初学者也能迅速上手, 如鱼得水。

本章首先介绍了 Ansible 的起源、关键术语以及主机清单的配置,带领大家深入学习ping、yum、firewalld、service、template、setup、lvol、lvg、copy、file、debug 等十多个常用的 Ansible 模块,以满足日常运维的需求。接着,本章通过动手实操, 详细介绍从系统中加载角色、从外部获取角色以及自行创建角色的方法,旨在让读者在生产环境中能够得心应手地管理和控制任务流程。此外,本章还通过精心编写的 playbook(剧本) 文件,演示了创建逻辑卷设备、依据主机改写文件以及管理文件属性的方法。最后,本章以使用 Ansible 的 vault 功能对变量和 playbook 文件进行加密来收尾。

本章全面涵盖了 Ansible 的使用细节,内容环环相扣,相信读者在学完本章内容之后会有酣畅淋漓之感。

16.1 Ansible介绍与安装

Ansible 是当前运维自动化工具中简便易用的典范,堪称运维人员的得力助手。它能够高效地管理各种资源,并通过自动部署应用程序,实现 IT 基础设施的全面自动化。借助 Ansible, 我们可以轻松完成服务器的初始化配置、安全基线配置,以及系统更新和补丁管理等操作。与 Chef、Puppet、SaltStack 等采用 C/S(客户端/服务器)架构的自动化工具相比,尽管 Ansible 在性能上并非最佳,但由于其基于 SSH 协议,不需要安装客户端程序,只需掌握受控主机的账号和密码,便可直接通过 SSH 协议进行远程控制。因此,Ansible 在使用便捷性上占据了显著优势,称得上是运维领域的“利器”。

2012 年 2 月,程序员 Michael DeHaan 发布了 Ansible 的第一个版本。Michael DeHaan 在配置管理和架构设计领域拥有丰富的经验,他在红帽公司任职期间,曾研发了 Cobbler 自动化系统安装工具。然而,面对现有自动化工具的复杂性和局限性,他深感不满,遂决心打造一款集众家所长的自动化工具。Ansible 应运而生。凭借其卓越的易用性和强大功能,Ansible 迅速得到了运维界的广泛认可,其在 GitHub 上的star 和fork 数量远超SaltStack,足以见其受欢迎程度。2015 年,Ansible 正式被红帽公司收购(其 Logo 变为图 16-1 所示),这为其未来发展增添了无限潜力。

图 16-1 Ansible 的 Logo

使用自动化运维工具,能够肉眼可见地提高运维人员的工作效率,并减少人为错误。Ansible 服务本身并没有批量部署的功能,它仅仅是一个框架,真正具有批量部署能力的是其运行的模块。Ansible 内置了上千个模块,这些模块会在安装 Ansible 时一并安装,之后通过调用指定的模块,就能实现特定的功能。Ansible 内置的模块非常丰富,几乎可以满足一切需求,使用起来也非常简单,一条命令甚至影响上千台主机。如果需要更高级的功能,也可以使用Python 语言对Ansible 进行二次开发。

如今,Ansible 已被 Amazon、Google、Microsoft、Cisco、HP、VMware、Twitter(即现在的 X 公司)等大科技公司接纳并投入使用,并在实践中展现出非凡的性能和灵活性。红帽公司更是对自家产品进行了不遗余力的支持。从 2020 年 8 月 1 日起,RHCE 考试的内容由配置多款服务转变为 Ansible 专项考题内容。现在,要想顺利拿到 RHCE 认证,真的有必要好好学习一下本章了。

在正式介绍 Ansible 之前,我们先来熟悉一下相关的专用术语,好让大家对术语有统一的理解,以便在后续实验时能直奔主题。这里整理的与 Ansible 相关的专用术语如表 16-1 所示。

表 16-1 Ansible 服务的专用术语对照表

术语中文称呼含义
control node控制节点安装了 Ansible 服务的主机,用于发布运行任务、调用模块,以及对其 他主机进行批量控制
managed node受控节点由 Ansible 服务管理的主机,是模块命令的执行对象,也称为受控主机 或客户端
inventory主机清单受控节点的列表,可以是 IP 地址、主机名称或域名
module模块特定功能代码;Ansible 默认自带上千款模块,也可以在 Ansible Galaxy 中选择其他模块
task任务要在 Ansible 客户端上执行的操作
playbook剧本用 YAML 语言编写的可重复执行的任务列表;将常用操作写入剧本文 件后,下次可直接执行
role角色用于结构化地组织 playbook,通过调用角色实现一连串功能

由于受控节点不需要安装客户端,加之 SSH 协议是 Linux 系统的标配,因此可以直接通过 SSH 协议进行远程控制。在控制节点上,也无须每次都重复启动服务程序,只需使用 ansible 命令直接调用模块即可进行控制。

RHEL 10 系统的镜像文件默认不包含 Ansible 服务程序,用户需要从 Extra Packages for Enterprise Linux(EPEL)扩展软件包仓库获取。EPEL 扩展软件包仓库由红帽公司提供,是一个用于创建、维护和管理企业版 Linux 的高质量软件扩展仓库,通用于 RHEL、CentOS、Oracle Linux 等多种红帽系企业版系统,目的是对于默认系统仓库软件包进行扩展。

下面准备在系统上部署 Ansible 服务程序。

第 1 步:在“虚拟机设置”界面中,将“网络适配器”的“网络连接”选项调整为“桥接模式”,并将系统的网卡设置成 Automatic(DHCP)模式,如图 16-2 和图 16-3 所示。

图 16-2 将“网络连接”设置为“桥接模式”

图 16-3 将网卡设置为 Automatic(DHCP)模式

在大多数情况下,只要把虚拟机设置成桥接模式,且 Linux 系统的网卡信息与物理机的网卡信息相同,然后再重启网络服务,就可以连接外部网络了。如果不放心,可以通过 ping 命令测试一下。

root@linuxprobe:~# nmcli connection up ens160 
Connection successfully activated (D-Bus active path: /org/freedesktop/NetworkManager/ActiveConnection/3)
root@linuxprobe:~# ping -c 4 www.linuxprobe.com
PING www.linuxprobe.com.w.kunlunno.com (124.95.157.160) 56(84) bytes of data.
64 bytes from www.linuxprobe.com (124.95.157.160): icmp_seq=1 ttl=53 time=17.1 ms
64 bytes from www.linuxprobe.com (124.95.157.160): icmp_seq=2 ttl=53 time=15.6 ms
64 bytes from www.linuxprobe.com (124.95.157.160): icmp_seq=3 ttl=53 time=16.8 ms
64 bytes from www.linuxprobe.com (124.95.157.160): icmp_seq=4 ttl=53 time=17.5 ms

--- www.linuxprobe.com.w.kunlunno.com ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 10ms
rtt min/avg/max/mdev = 15.598/16.732/17.452/0.708 ms

第 2 步:在原有软件仓库配置的下方,追加 EPEL 扩展软件包安装源的信息。

root@linuxprobe:~# vim /etc/yum.repos.d/rhel10.repo
[BaseOS]
name=BaseOS
baseurl=file:///media/cdrom/BaseOS
enabled=1
gpgcheck=0

[AppStream]
name=AppStream
baseurl=file:///media/cdrom/AppStream
enabled=1
gpgcheck=0

[EPEL]
name=EPEL
baseurl=https://dl.fedoraproject.org/pub/epel/10/Everything/x86_64/
enabled=1
gpgcheck=0

第 3 步:安装!

ansible-core 是 Ansible 服务程序的软件包名,而 sshpass 软件包的功能是让服务器可以远程管理受控节点。

root@linuxprobe:~# dnf install ansible-core sshpass
Updating Subscription Management repositories.
BaseOS 2.7 MB/s | 2.7 kB 00:00
AppStream 2.7 MB/s | 2.8 kB 00:00
EPEL 1.3 MB/s | 3.8 MB 00:03
Dependencies resolved.
========================================================================================================
Package Architecture Version Repository Size
========================================================================================================
Installing:
ansible-core noarch 1:2.16.3-3.el10 AppStream 3.9 M
sshpass x86_64 1.09-10.el10_1 EPEL 27 k
Installing dependencies:
python3-argcomplete noarch 3.2.2-3.el10 AppStream 90 k
python3-cffi x86_64 1.16.0-5.el10 BaseOS 312 k
python3-cryptography x86_64 43.0.0-2.el10 BaseOS 1.4 M
python3-jinja2 noarch 3.1.4-2.el10 AppStream 330 k
python3-markupsafe x86_64 2.1.3-5.el10 AppStream 36 k
python3-ply noarch 3.11-24.el10 BaseOS 139 k
python3-pycparser noarch 2.20-15.el10 BaseOS 162 k
python3-resolvelib noarch 1.0.1-5.el10 AppStream 49 k
………………省略部分输出信息………………
Installed:
ansible-core-1:2.16.3-3.el10.noarch python3-argcomplete-3.2.2-3.el10.noarch
python3-cffi-1.16.0-5.el10.x86_64 python3-cryptography-43.0.0-2.el10.x86_64
python3-jinja2-3.1.4-2.el10.noarch python3-markupsafe-2.1.3-5.el10.x86_64
python3-ply-3.11-24.el10.noarch python3-pycparser-2.20-15.el10.noarch
python3-resolvelib-1.0.1-5.el10.noarch sshpass-1.09-10.el10_1.x86_64

Complete!

由于 ansible-core 默认只安装了核心模块,而 lvg、parted、lvol、mount 等模块属于 community.general 集合(collection),需要手动安装:

root@linuxprobe:~# ansible-galaxy collection install community.general
Starting galaxy collection install process
Process install dependency map
Starting collection install process
Downloading https://galaxy.ansible.com/api/v3/plugin/ansible/content/published/collections/artifacts/community-general-10.5.0.tar.gz to /root/.ansible/tmp/ansible-local-3064xaqti5li/tmpt0je0dr4/community-general-10.5.0-z14ni88o
Installing 'community.general:10.5.0' to '/root/.ansible/collections/ansible_collections/community/general'
community.general:10.5.0 was installed successfully

安装完毕后,Ansible 服务便默认已经启动。使用--version 参数可以看到 Ansible 服务的版本及配置信息。

root@linuxprobe:~# ansible --version
ansible [core 2.16.3]
config file = /etc/ansible/ansible.cfg
configured module search path = ['/root/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
ansible python module location = /usr/lib/python3.12/site-packages/ansible
ansible collection location = /root/.ansible/collections:/usr/share/ansible/collections
executable location = /usr/bin/ansible
python version = 3.12.5 (main, Aug 23 2024, 00:00:00) [GCC 14.2.1 20240801 (Red Hat 14.2.1-1)] (/usr/bin/python3)
jinja version = 3.1.4
libyaml = True

16.2 设置主机清单

在初次使用 Ansible 服务时,可能会遇到这样的问题:参数明明已经修改,但却未生效。这通常是 Ansible 主配置文件的优先级顺序所导致的。Ansible 的主配置文件通常存放在/etc/ansible 目录中,但其优先级最低。如果在当前目录或用户家目录中存在另一份主配置文件,则 Ansible 会优先使用这份文件。当多个主配置文件同时存在时,具体优先级顺序如表 16-2 所示。

表 16-2 Ansible 服务主配置文件的优先级顺序

优先级文件位置
./ansible.cfg
~/.ansible.cfg
/etc/ansible/ansible.cfg

既然 Ansible 服务是用于实现主机批量自动化控制的管理工具,受管的主机一定不是一两台,而是数十台甚至成百上千台,那么主机清单(inventory)在生产环境中就能帮上大忙了。用户可以把要管理的主机 IP 地址预先写入/etc/ansible/hosts 文件,这样后续在通过执行 ansible 命令来执行任务时就自动包含这些主机了,也就不需要每次都重复输入受管主机的地址了。例如,假设需要管理 5 台主机,它们的 IP 地址和用途如表 16-3 所示。

表 16-3 受管主机的信息

操作系统IP地址功能用途
RHEL 10192.168.10.20dev
RHEL 10192.168.10.21test
RHEL 10192.168.10.22prod
RHEL 10192.168.10.23prod
RHEL 10192.168.10.24balancers

首先需要说明的是,受管主机的系统默认使用 RHEL 10,这是为了避免大家在准备实验机阶段产生歧义而给出的建议值,也可以用其他 Linux 系统。主机清单文件/etc/ansible/ hosts 中默认存在大量的注释信息,建议全部删除,然后替换成实验信息。

root@linuxprobe:~# vim /etc/ansible/hosts
192.168.10.20
192.168.10.21
192.168.10.22
192.168.10.23
192.168.10.24

为了增加实验难度,“通吃”生产环境中的常见需求,我们又为这 5 台主机分别规划了功能用途,有开发机(dev)、测试机(test)、产品机(prod)(两台)和负载均衡机(balancers)。在对主机进行分组标注后,后期在管理时就方便多了。

root@linuxprobe:~# vim /etc/ansible/hosts
[dev]
192.168.10.20
[test]
192.168.10.21
[prod]
192.168.10.22
192.168.10.23
[balancers]
192.168.10.24

主机清单文件在修改后会立即生效,一般使用 ansible-inventory --graph 命令以结构化的方式显示出受管主机的信息。因为对受管主机进行了分组,因此这种方式非常方便阅读。

root@linuxprobe:~# ansible-inventory --graph
@all:
|--@ungrouped:
|--@dev:
| |--192.168.10.20
|--@test:
| |--192.168.10.21
|--@prod:
| |--192.168.10.22
| |--192.168.10.23
|--@balancers:
| |--192.168.10.24

等等!先不要着急开始后面的实验。前文讲过,Ansible 服务是基于 SSH 协议进行自动化控制的,这是开展后面实验的前提条件。第 9 章曾经讲到,sshd 服务在初次连接时会要求用户接受一次对方主机的指纹信息。准备输入受管主机的账号和密码。例如,正常的第一次SSH 远程连接过程是这样的:

root@linuxprobe:~# ssh 192.168.10.10
The authenticity of host '192.168.10.10 (192.168.10.10)' can't be established.
ED25519 key fingerprint is SHA256:0R7Kuk/yCTlJ+E4G9y9iX/A/hAklHkALm5ZUgnJ01cc.
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '192.168.10.10' (ED25519) to the list of known hosts.
root@192.168.10.10's password:此处应输入管理员密码后回车确认
………………省略部分输出信息………………
Last login: Mon Mar 31 13:20:15 2025
root@linuxprobe:~#

众所周知,自动化运维的一个好处就是能提高工作效率。但是,如果每次执行操作都要输入受管主机的密码,也是比较麻烦的事情。好在 Ansible 服务已经对此有了解决办法,那就是使用如表 16-4 所示的变量。

表 16-4 Ansible 常用变量汇总

参数作用
ansible_ssh_host受管主机名
ansible_ssh_port端口号
ansible_ssh_user默认账号
ansible_password默认密码
ansible_shell_typeShell 终端类型

用户只需将对应的变量及信息填写到主机清单文件中,在执行任务时便会自动对账号和密码进行匹配,而不用每次重复输入它们。继续修改主机清单文件:

root@linuxprobe:~# vim /etc/ansible/hosts
[dev]
192.168.10.20
[test]
192.168.10.21
[prod]
192.168.10.22
192.168.10.23
[balancers]
192.168.10.24
[all:vars]
ansible_user=root
ansible_password=redhat

还剩最后一步。Ansible 的主配置文件默认会注释掉一切参数,我们可以根据实际需要逐一开启。我们把第 221 行配置为默认执行剧本时使用的管理员名称 root,以及将第 320 行设置成默认不需要 SSH 协议的指纹验证,方便直接管理受控节点:

root@linuxprobe:~# cd /etc/ansible/
root@linuxprobe:/etc/ansible# ansible-config init --disabled > ansible.cfg
root@linuxprobe:/etc/ansible# vim /etc/ansible/ansible.cfg
219 # (string) Sets the login user for the target machines
220 # When blank it uses the connection plugin's default, normally the user curr ently executing Ansible.
221 remote_user=root
………………省略部分输出信息………………
319 # (boolean) Set this to "False" if you want to avoid host key checking by the underlying tools Ansible uses to connect to the host
320 host_key_checking=False

无须重启服务,在以上操作完全搞定后就可以开始后面的实验了。由于刚才将 Ansible 服务器设置为桥接及 DHCP 模式,现在请同学们自行将网络适配器修改回“仅主机模式”(见图 16-4)以及 192.168.10.10/24 的 IP 地址。在修改完成后重启网卡,然后自行在主机之间执行 ping 操作。保证主机之间的网络能够互通是后续实验的基石。

root@linuxprobe:/etc/ansible# ifconfig
ens160: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 192.168.10.10 netmask 255.255.255.0 broadcast 192.168.10.255
inet6 fe80::20c:29ff:fee5:e733 prefixlen 64 scopeid 0x20<link>
ether 00:0c:29:e5:e7:33 txqueuelen 1000 (Ethernet)
RX packets 1132 bytes 107455 (104.9 KiB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 152 bytes 18695 (18.2 KiB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
………………省略部分输出信息………………

图 16-4 将虚拟机网卡改回“仅主机模式”

16.3 运行临时命令

Ansible 服务的强大之处在于只需要一条命令,便能够操控成千上万台的主机节点,而ansible 命令便是最得力的工具之一。前文提到,Ansible 服务实际上只是一个框架,实际完成工作的是模块化功能代码。Ansible 的常用模块大致有 20 多个(见表 16-5),本书将会在后面的实验中逐一详解。

偶尔遇到书中没有提及的模块,大家可以使用“ansible-doc 模块名称”的命令格式自行查询,或是使用 ansible-doc -l 命令列出所有的模块信息以供选择,浏览完毕后按 q 键退出。

表 16-5 Ansible 服务的常用模块名称及作用

模块名称模块作用
ping检查受管主机的网络是否能够连通
yum安装、更新及卸载软件包
yum_repository管理主机的软件仓库配置文件
template复制模板文件到受管主机
copy新建、修改及复制文件
user创建、修改及删除用户
group创建、修改及删除用户组
service启动、关闭及查看服务状态
get_url从网络中下载文件
file设置文件权限及创建快捷方式
cron添加、修改及删除计划任务
command直接执行用户指定的命令
shell直接执行用户指定的命令(支持特殊字符)
debug输出调试或报错信息
mount挂载磁盘设备文件
filesystem格式化磁盘设备文件
lineinfile通过正则表达式修改文件内容
setup收集受管主机上的系统及变量信息
firewalld添加、修改及删除防火墙策略
lvg管理主机的物理卷及卷组设备
lvol管理主机的逻辑卷设备

在 Ansible 服务中,ansible 是用于执行临时任务的命令,也就是执行后即结束(与剧本文件的可重复执行不同)。在使用 ansible 命令时,必须指明受管主机的信息,如果已经设置过主机清单文件(/etc/ansible/hosts),则可以使用 all 参数来指代全体受管主机,或是用 dev、test 等主机组名称来指代某一组的主机。

ansible 命令常用的语法格式为“ansible 受管主机节点 –m 模块名称 [-a 模块参数]”,常见的参数如表 16-6 所示。其中,-a 是要传递给模块的参数,只有功能极其简单的模块才不需要额外参数,所以大多情况下-m 与-a 参数都会同时出现。

表 16-6 ansible 命令的常用参数

参数作用
-k手动输入 SSH 协议的密码
-i指定主机清单文件
-m指定要使用的模块名
-M指定要使用的模块路径
-S使用 su 命令
-T设置 SSH 协议连接超时时间
-a设置传递给模块的参数
--version查看版本信息
-h帮助信息

如果想实现某个功能,但是却不知道用什么模块,抑或知道模块的名称,但不清楚模块具体的作用,则建议使用 ansible-doc 命令进行查找。例如,列举出当前 Ansible 服务所支持的所有模块信息:

root@linuxprobe:/etc/ansible# ansible-doc -l 
ansible.builtin.add_host Add a host (and alternatively a grou...
ansible.builtin.apt Manages apt-packages
ansible.builtin.apt_key Add or remove an apt key
ansible.builtin.apt_repository Add and remove APT repositories
ansible.builtin.assemble Assemble configuration files from fr...
ansible.builtin.assert Asserts given expressions are true
ansible.builtin.async_status Obtain status of asynchronous task
………………省略部分输出信息………………

一般情况下,很难通过名称来判别一个模块的作用,要么是参考模块后面的介绍信息, 要么是平时多学多练,进行积累。例如,接下来随机查看一个模块的详细信息。ansible-doc 命令会在屏幕上显示出这个模块的作用、可用参数及实例等信息:

root@linuxprobe:/etc/ansible# ansible-doc add_host
> ANSIBLE.BUILTIN.ADD_HOST (/usr/lib/python3.12/site-packages/ansible/module>

Use variables to create new hosts and groups in inventory for
use in later plays of the same playbook. Takes variables so
you can define the new hosts more fully. This module is also
supported for Windows targets.
………………省略部分输出信息………………

在 16.2 节,已经成功地将受管主机的 IP 地址填写到主机清单文件中,接下来小试牛刀, 检查一下这些主机的网络连通性。ping 模块用于进行简单的网络测试(类似于常用的 ping 命令)。可以使用 ansible 命令直接针对所有主机调用ping 模块,不需要增加额外的参数, 返回值若为 SUCCESS,则表示主机当前在线。

root@linuxprobe:/etc/ansible# ansible all -m ping
192.168.10.20 | SUCCESS => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python3"
},
"changed": false,
"ping": "pong"
}
192.168.10.21 | SUCCESS => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python3"
},
"changed": false,
"ping": "pong"
}
192.168.10.22 | SUCCESS => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python3"
},
"changed": false,
"ping": "pong"
}
192.168.10.23 | SUCCESS => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python3"
},
"changed": false,
"ping": "pong"
}
192.168.10.24 | SUCCESS => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python3"
},
"changed": false,
"ping": "pong"
}

Tips :

由于 5 台受控主机的输出信息大致相同,因此为了提升读者的阅读体验,本章后续的输出结果默认仅保留 192.168.10.20 主机的输出值,其余相同的输出信息将会被省略。

是不是感觉很方便呢?!一次就能知道所有主机的在线情况。除了使用-m 参数直接指定模块名称之外,还可以用-a 参数将参数传递给模块,让模块的功能更高级,更好地满足当前生产的需求。例如,yum_repository 模块的作用是管理主机的软件仓库,能够添加、修改及删除软件仓库的配置信息,参数相对比较复杂。遇到这种情况时,建议先用 ansible-doc 命令对其进行了解。尤其是下面的 EXAMPLES 结构段会有该模块的实例,对用户来说有非常高的参考价值。

root@linuxprobe:/etc/ansible# ansible-doc yum_repository
> ANSIBLE.BUILTIN.YUM_REPOSITORY (/usr/lib/python3.12/site-packages/ansible/>

Add or remove YUM repositories in RPM-based Linux
distributions. If you wish to update an existing repository
definition use [community.general.ini_file] instead.
………………省略部分输出信息………………
EXAMPLES:
- name: Add repository
ansible.builtin.yum_repository:
name: epel
description: EPEL YUM repo
baseurl: https://download.fedoraproject.org/pub/epel/$releasever/$basearch/

- name: Add multiple repositories into the same file (1/2)
ansible.builtin.yum_repository:
name: epel
description: EPEL YUM repo
file: external_repos
baseurl: https://download.fedoraproject.org/pub/epel/$releasever/$basearch/
gpgcheck: no

- name: Add multiple repositories into the same file (2/2)
ansible.builtin.yum_repository:
name: rpmforge
description: RPMforge YUM repo
file: external_repos
baseurl: http://apt.sw.be/redhat/el7/en/$basearch/rpmforge
mirrorlist: http://mirrorlist.repoforge.org/el7/mirrors-rpmforge
enabled: no

还好,参数并不是很多,而且与此前学过的/etc/yum.repos.d 目录中的配置文件基本相似。现在,想为主机清单中的所有服务器新增一个如表 16-7 所示的软件仓库,该怎么操作呢?

表 16-7 新增软件仓库信息

参数项参数值
仓库名称EX294_BASE
仓库描述EX294 base software
仓库地址file:///media/cdrom/BaseOS
GPG 签名启用
GPG 密钥文件file:///media/cdrom/RPM-GPG-KEY-redhat-release

我们可以对照 EXAMPLE 实例段,逐一对应填写需求值和参数,其标准格式是在-a 参数后接整体参数(用单引号圈起),而各个参数字段的值则用双引号圈起。这是最严谨的写法。在执行下述命令后如果出现 CHANGED 字样,则表示修改已经成功:

root@linuxprobe:/etc/ansible# ansible all -m yum_repository -a 'name="EX294_BASE" description="EX294 base software" baseurl="file:///media/cdrom/BaseOS" gpgcheck=yes enabled=1 gpgkey="file:///media/cdrom/RPM-GPG-KEY-redhat-release"'

192.168.10.20 | CHANGED => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python3"
},
"changed": true,
"repo": "EX294_BASE",
"state": "present"
}

在命令执行成功后,可以到主机清单中的任意机器上查看新建成功的软件仓库配置文件。尽管这个实验的参数很多,但是并不难。

root@linuxprobe:~# cat /etc/yum.repos.d/EX294_BASE.repo 
[EX294_BASE]
baseurl = file:///media/cdrom/BaseOS
enabled = 1
gpgcheck = 1
gpgkey = file:///media/cdrom/RPM-GPG-KEY-redhat-release
name = EX294 base software

16.4 剧本文件实战

在很多情况下,仅仅执行单个命令或调用某一个模块,根本无法满足复杂工作的需要。Ansible 服务允许用户根据需求,在类似于 Shell 脚本的模式下编写自动化运维脚本,然后由程序自动、重复地执行,从而大大提高工作效率。

Ansible 服务的剧本(playbook)文件采用 YAML 语言编写,具有强制性的格式规范,它通过空格将不同信息分组,因此有时会因一两个空格错位而导致报错。大家在使用时要万分小心。YAML 文件的开头需要先写 3 个减号(---),多个分组的信息需要间隔一致才能执行,而且上下也要对齐,后缀名一般为.yml。剧本文件在执行后,会在屏幕上输出运行界面,内容会根据工作的不同而变化。在运行界面中,绿色表示成功,黄色表示执行成功并进行了修改,而红色则表示执行失败。

剧本文件的结构由 4 部分组成,分别是 target、variable、task、handler,其各自的作用如下。

target:用于定义要执行剧本的主机范围。

variable:用于定义剧本执行时要用到的变量。

task:用于定义将在远程主机上执行的任务列表。

handler:用于定义执行完成后需要调用的后续任务。

YAML 语言编写的 Ansible 剧本文件会按照从上到下的顺序自动运行,其形式类似于第 4 章介绍的 Shell 脚本,但格式有严格的要求。例如,创建一个名为 packages.yml 的剧本,让 dev、test 和 prod 组的主机可以自动安装数据库软件,并且将 dev 组主机的软件更新至最新。

安装和更新软件需要使用 yum 模块。先看一下帮助信息中的示例吧:

root@linuxprobe:/etc/ansible# ansible-doc yum
> ANSIBLE.BUILTIN.YUM (/usr/lib/python3.12/site-packages/ansible/modules/yum>

Installs, upgrade, downgrades, removes, and lists packages and
groups with the `yum' package manager. This module only works
on Python 2. If you require Python 3 support see the
[ansible.builtin.dnf] module.
………………省略部分输出信息………………
EXAMPLES:

- name: Install the latest version of Apache
ansible.builtin.yum:
name: httpd
state: latest

在配置 Ansible 剧本文件时,ansible-doc 命令提供的帮助信息真是好用。在知道 yum 模块的使用方法和格式后,就可以开始编写剧本了。初次编写剧本文件时,请务必看准格式, 模块及 play(动作)格式也要上下对齐,否则会出现“参数完全一样,但不能执行”的情况。

综上,一个剧本正确的写法应该是:

root@linuxprobe:/etc/ansible# vim packages.yml
---
- name: 安装软件包
hosts: dev,test,prod
tasks:
- name: one
yum:
name: mariadb
state: latest

其中,name 字段表示此项 play(动作)的名字,用于在执行过程中提示用户执行到了哪一步,以及帮助管理员在日后阅读时能想起这段代码的作用。大家可以在 name 字段自行命名,没有任何限制。hosts 字段表示要在哪些主机上执行该剧本,多个主机组之间用逗号间隔;如果需要对全部主机进行操作,则使用 all 参数。tasks 字段用于定义要执行的任务, 每个任务都要有一个独立的 name 字段进行命名,并且每个任务的 name 字段和模块名称都要严格上下对齐,参数要单独缩进。

而错误的剧本文件是下面这样的:

root@linuxprobe:/etc/ansible# vim packages.yml
---
- name: 安装软件包
hosts: dev,test,prod
tasks:
- name: one
yum:
name: mariadb
state: latest

大家可以感受到 YAML 语言对格式要求有多严格吧。

在编写 Ansible 剧本文件时,RHEL 10 系统自带的 Vim 编辑器具有自动缩进功能,能给我们提供很多帮助。在确认无误后就可以用 ansible-playbook 命令运行这个剧本文件了。

root@linuxprobe:/etc/ansible# ansible-playbook packages.yml 
PLAY [安装软件包] **************************************************************

TASK [Gathering Facts] ********************************************************
ok: [192.168.10.20]
ok: [192.168.10.21]
ok: [192.168.10.22]
ok: [192.168.10.23]

TASK [one] ********************************************************************
changed: [192.168.10.20]
changed: [192.168.10.21]
changed: [192.168.10.22]
changed: [192.168.10.23]

PLAY RECAP ********************************************************************
192.168.10.20 : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
192.168.10.21 : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
192.168.10.22 : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
192.168.10.23 : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

在执行成功后,我们主要观察最下方的输出信息。其中,ok 和changed 表示执行及修改成功。如遇到unreachable 或failed 大于 0 的情况,建议手动检查剧本是否在所有主机中都正确运行了,以及有无安装失败的情况。在正确执行过 packages.yml 文件后,随机切换到 dev、test、prod 组中的任意一台主机上,再次安装 mariadb 软件包,此时会提示该服务已经存在。这说明刚才的操作一切顺利!

root@linuxprobe:~# dnf install mariadb
Updating Subscription Management repositories.
Last metadata expiration check: 0:00:38 ago on Tue 01 Apr 2025 06:52:29 PM CST.
Package mariadb-3:10.11.9-3.el10.x86_64 is already installed.
Dependencies resolved.
Nothing to do.
Complete!

16.5 创建及使用角色

在编写 Ansible 剧本时,随着内容的不断增加,代码可能变得冗长、复杂,难以维护,且其他剧本也难以灵活调用其中的功能。为了解决这些问题,Ansible 自 1.2 版本起引入了“角色”(role)功能,用于层次化、结构化地组织剧本。角色功能将变量、文件、任务、模块及处理器分别放置在独立的目录中,能够方便地进行加载和复用。简单来说,角色功能将常用的功能模块化,以便在需要时方便调用。

Ansible 的角色功能类似于编程中的封装技术—将具体功能封装起来,用户不仅可以方便地调用,而且不必深入理解其内部原理。就像普通消费者在驾驶汽车时,不需要理解刹车系统的复杂构造,如制动总泵、刹车分泵、真空助力器、刹车盘、刹车鼓、刹车片或 ABS 泵的工作原理,只需轻踩刹车踏板即可实现制动。这便是技术封装带来的好处。

角色的优势在于将剧本组织成一个简洁且可重复调用的抽象对象,使用户能够将注意力集中在剧本的宏观设计上,统筹各个关键任务,只有在需要时才需深入了解细节。这种结构化的组织方式极大地提升了剧本的可维护性和可扩展性。

获取和使用角色有 3 种主要方法:加载系统内置角色、从外部环境获取角色以及自行创建角色。通过这些方法,用户可以根据需求灵活地使用角色功能,从而实现高效、简洁的自动化运维管理。

16.5.1 加载系统内置角色

在使用 RHEL 系统的内置角色时,我们不需要联网就能实现。用户只需要配置好软件仓库的配置文件,然后安装包含系统角色的软件包rhel-system-roles,随后便可以在系统中找到它们了,然后就可以使用剧本文件调用角色了。

root@linuxprobe:/etc/ansible# dnf install rhel-system-roles
Updating Subscription Management repositories.
Last metadata expiration check: 0:27:07 ago on Tue 01 Apr 2025 06:27:45 PM CST.
Dependencies resolved.
================================================================================
Package Arch Version Repository Size
================================================================================
Installing:
rhel-system-roles noarch 1.88.9-0.1.el10 AppStream 3.9 M
………………省略部分输出信息………………
Installed:
rhel-system-roles-1.88.9-0.1.el10.noarch

Complete!

安装完毕后,使用 ansible-galaxy list 命令查看RHEL 10 系统中有哪些自带的角色可用:

root@linuxprobe:/etc/ansible# ansible-galaxy list
# /usr/share/ansible/roles
- rhel-system-roles.network, (unknown version)
- rhel-system-roles.podman, (unknown version)
- rhel-system-roles.postfix, (unknown version)
- rhel-system-roles.postgresql, (unknown version)
- rhel-system-roles.rhc, (unknown version)
- rhel-system-roles.selinux, (unknown version)
- rhel-system-roles.snapshot, (unknown version)
- rhel-system-roles.ssh, (unknown version)
- rhel-system-roles.sshd, (unknown version)
- rhel-system-roles.storage, (unknown version)
- rhel-system-roles.sudo, (unknown version)
- rhel-system-roles.systemd, (unknown version)
- rhel-system-roles.timesync, (unknown version)
………………省略部分输出信息………………

大家千万不要低估这些由系统镜像自带的角色,它们在日常的工作中能派上大用场。这些角色的主要功能如表 16-8 所示。

表 16-8 RHEL 系统自带的角色

角色名称作用
rhel-system-roles.kdump配置 KDUMP 崩溃恢复服务
rhel-system-roles.network配置网络接口
rhel-system-roles.selinux配置 SELinux 策略及模式
rhel-system-roles.timesync配置网络时间协议
rhel-system-roles.postfix配置邮件传输服务
rhel-system-roles.firewall配置防火墙服务
rhel-system-roles.tuned配置系统调优选项

以rhel-system-roles.timesync 角色为例,它用于设置系统的时间和 NTP 服务, 让主机能够同步准确的时间信息。剧本模板文件存放在/usr/share/doc/rhel-system- roles/目录中,可以复制过来修改使用:

root@linuxprobe:/etc/ansible# cp /usr/share/doc/rhel-system-roles/timesync/example-single-pool-playbook.yml timesync.yml

NTP 服务器主要用于同步计算机的时间,可以提供高精度的时间校准服务,帮助计算机校对系统时钟。在复制过来的剧本模板文件中,删除掉多余的代码,将 NTP 服务器的地址填写到timesync_ntp_servers 变量的hostname 字段中即可。该变量的参数含义如表 16-9 所示。稍后 timesync 角色就会自动为用户配置参数信息了。

表 16-9 timesync_ntp_servers 变量的参数含义

参数含义
hostnameNTP 服务器的主机名
iburst启用快速同步
root@linuxprobe:/etc/ansible# vim timesync.yml 
---
- name: Example with single pool
hosts: all
vars:
timesync_ntp_servers:
- hostname: 2.pool.ntp.org
pool: true
iburst: true
roles:
- rhel-system-roles.timesync

16.5.2 从外部获取角色

Ansible Galaxy 是Ansible 的一个官方社区(地址为https://galaxy.ansible.com),用于共享角色和功能代码,用户可以在该社区自由地共享和下载 Ansible 角色。该社区是管理和使用角色的不二之选。

在图 16-5 所示的 Ansible Galaxy 官网首页中,左侧有 5 个功能选项,分别是首页搜索(Search)、集合(Collections)、角色(Roles)、文档(Documentation)和使用条款(Terms of Use)。单击 Roles 按钮进入角色搜索页面,这里以 nginx 服务为例进行搜索,即可找到 Nginx官方发布的角色信息,如图 16-6 所示。

图 16-5 Ansible Galaxy 官网首页

图 16-6 在搜索界面中找到 nginx 角色信息

Tips :
Ansible Galaxy 官网首页:https://galaxy.ansible.com

当单击 nginxinc.nginx 角色进入到详情页面后,会显示这个项目的软件版本、评分、下载次数等信息。在 Installation 字段能看到相应的安装方式,如图 16-7 所示。在保持虚拟机正常连接外网的前提下,可以按这个页面提示的命令进行安装。

这时,如果需要使用这个角色,可以在虚拟机连网的状态下直接按照“ansible-galaxy install 角色名称”的命令格式自动获取:

图 16-7 nginx 角色的详情页

root@linuxprobe:/etc/ansible# ansible-galaxy role install nginxinc.nginx
Starting galaxy role install process
- downloading role 'nginx', owned by nginxinc
- downloading role from https://github.com/nginxinc/ansible-role-nginx/archive/0.25.0.tar.gz
- extracting nginxinc.nginx to /etc/ansible/roles/nginxinc.nginx
- nginxinc.nginx (0.25.0) was installed successfully

执行完毕后,再次查看系统中已有的角色,便可找到 nginx 角色信息了:

root@linuxprobe:/etc/ansible# ansible-galaxy list
# /etc/ansible/roles
- nginx-core, (unknown version)
# /usr/share/ansible/roles
- rhel-system-roles.network, (unknown version)
- rhel-system-roles.podman, (unknown version)
- rhel-system-roles.postfix, (unknown version)
- rhel-system-roles.postgresql, (unknown version)
- rhel-system-roles.rhc, (unknown version)
- rhel-system-roles.selinux, (unknown version)
- rhel-system-roles.snapshot, (unknown version)
- rhel-system-roles.ssh, (unknown version)
- rhel-system-roles.sshd, (unknown version)
- rhel-system-roles.storage, (unknown version)
- rhel-system-roles.sudo, (unknown version)
- rhel-system-roles.systemd, (unknown version)
- rhel-system-roles.timesync, (unknown version)
………………省略部分输出信息………………

这里还存在两种特殊情况:

在国内访问 Ansible Galaxy 官网时可能存在不稳定的情况,导致访问不了或者网速较慢;

某位作者是将作品上传到了自己的网站,或者除 Ansible Galaxy 官网以外的其他平台。

在这两种情况下,就不能再用“ansible-galaxy install 角色名称”的命令直接加载了,而是需要手动编写一个 YAML 语言格式的文件,指明网址链接和角色名称,然后再用-r 参数进行加载。

例如,刘遄老师在本书的配套网站上传了一个名为 nginx_core 的角色软件包(一个用于对 Nginx 网站进行保护的插件)。这时需要编写如下所示的一个 yml 配置文件:

root@linuxprobe:/etc/ansible# vim nginx.yml
---
- src: https://www.linuxprobe.com/Software/ansible-role-nginx-0.25.0.tar.gz
name: nginx-core

随后使用 ansible-galaxy 命令的-r 参数加载这个文件,即可查看到新角色信息了:

root@linuxprobe:/etc/ansible# ansible-galaxy role install -r nginx.yml
Starting galaxy role install process
- downloading role from https://www.linuxprobe.com/Software/ansible-role-nginx-0.25.0.tar.gz
- extracting nginx-core to /root/.ansible/roles/nginx-core
- nginx-core was installed successfully
root@linuxprobe:/etc/ansible# ansible-galaxy list
# /root/.ansible/roles
- nginx-core, (unknown version)
# /usr/share/ansible/roles
- rhel-system-roles.network, (unknown version)
- rhel-system-roles.podman, (unknown version)
- rhel-system-roles.postfix, (unknown version)
- rhel-system-roles.postgresql, (unknown version)
- rhel-system-roles.rhc, (unknown version)
- rhel-system-roles.selinux, (unknown version)
- rhel-system-roles.snapshot, (unknown version)
- rhel-system-roles.ssh, (unknown version)
- rhel-system-roles.sshd, (unknown version)
- rhel-system-roles.storage, (unknown version)
- rhel-system-roles.sudo, (unknown version)
- rhel-system-roles.systemd, (unknown version)
- rhel-system-roles.timesync, (unknown version)
………………省略部分输出信息………………

16.5.3 创建新的角色

除了使用系统自带的角色和从 Ansible Galaxy 获取的角色外,用户还可以根据实际工作需求,自行创建符合生产环境的定制化角色。虽然这种定制化编写的角色能够更好地贴合实际需求,但难度也相对较高,需要具备一定的 Ansible 知识和实践经验。

接下来,我们将创建一个名为 apache 的新角色,该角色将自动安装和配置 httpd 网站服务,设置防火墙规则,并根据每台主机生成独立的 index.html 主页文件。通过调用这个角色,用户可享受到“一条龙”式的网站部署服务,极大地简化运维工作。

在Ansible 的主配置文件中,第 224 行定义了角色的保存路径。如果用户创建的新角色不在这个指定目录内,则无法通过 ansible-galaxy list 命令找到该角色。因此,用户需要手动填写新角色的目录路径,或是在进入/etc/ansible/roles 目录后再创建角色。为了避免角色信息过于分散,导致管理上的困难,建议在默认目录下创建角色,以便后期的统一管理。

root@linuxprobe:/etc/ansible# vim ansible.cfg
223 # (pathspec) Colon separated paths in which Ansible will search for Roles.
224 roles_path=/usr/share/ansible/roles:/etc/ansible/roles

在 ansible-galaxy 命令后面跟一个 init 参数,创建一个新的角色信息,且建立成功后便会在当前目录下生成出一个新的目录:

root@linuxprobe:/etc/ansible# cd roles
root@linuxprobe:/etc/ansible/roles# ansible-galaxy init apache
- Role apache was created successfully
root@linuxprobe:/etc/ansible/roles# ls
apache

此时的 apache 既是角色名称,也是用于存放角色信息的目录名称。切换到该目录下, 查看它的结构:

root@linuxprobe:/etc/ansible/roles# cd apache/
root@linuxprobe:/etc/ansible/roles/apache# ls
defaults files handlers meta README.md tasks templates tests vars

在创建新角色时,最关键的便是要正确理解目录结构。通俗来说,就是要把正确的信息放入正确的目录中,这样在调用角色时才能有正确的效果。角色信息对应的目录结构及含义如表 16-10 所示。

表 16-10 角色信息对应的目录结构及含义

目录结构含义
defaults包含角色变量的默认值(优先级低)
files包含角色执行任务时所引用的静态文件
handlers包含角色的处理程序定义
meta包含角色的作者、许可证、平台和依赖关系等信息
tasks包含角色所执行的任务
templates包含角色任务所使用的 Jinja2 模板
tests包含用于测试角色的剧本文件
vars包含角色变量的默认值(优先级高)

下面准备创建新角色。

第 1 步:打开用于定义角色任务的 tasks/main.yml 文件。在该文件中不需要定义要执行的主机组列表,因为后面会单独编写剧本进行调用,此时应先对 apache 角色能做的事情(任务)有一个明确的思路,在调用角色后 yml 文件会按照从上到下的顺序自动执行。

任务 1:安装 httpd 网站服务。

任务 2:运行 httpd 网站服务,并加入到开机启动项中。

任务 3:配置防火墙,使其放行 HTTP。

任务 4:根据每台主机的变量值,生成不同的主页文件。

先写出第一个任务。使用 yum 模块安装httpd 网站服务程序(注意格式):

root@linuxprobe:/etc/ansible/roles/apache# vim tasks/main.yml
---
- name: one
yum:
name: httpd
state: latest

第 2 步:使用 service 模块启动 httpd 网站服务程序,并加入到启动项中,使其能够一直为用户提供服务。在初次使用模块前,先用 ansible-doc 命令查看一下帮助和实例信息。由于篇幅的限制,这里对信息进行了删减,仅保留了有用的内容。

root@linuxprobe:/etc/ansible/roles/apache# ansible-doc service
> ANSIBLE.BUILTIN.SERVICE (/usr/lib/python3.12/site-packages/ansible/modules/service.py)

Controls services on remote hosts. Supported init systems include BSD init,
OpenRC, SysV, Solaris SMF, systemd, upstart. This module acts as a proxy to
the underlying service manager module. While all arguments will be passed
to the underlying module, not all modules support the same arguments. This
documentation only covers the minimum intersection of module arguments that
all service manager modules support. This module is a proxy for multiple
more specific service manager modules (such as [ansible.builtin.systemd]
and [ansible.builtin.sysvinit]). This allows management of a heterogeneous
environment of machines without creating a specific task for each service
manager. The module to be executed is determined by the `use' option, which
defaults to the service manager discovered by [ansible.builtin.setup]. If
[ansible.builtin.setup] was not yet run, this module may run it. For
Windows targets, use the [ansible.windows.win_service] module instead.
………………省略部分输出信息………………
EXAMPLES:

- name: Start service httpd, if not started
ansible.builtin.service:
name: httpd
state: started

- name: Stop service httpd, if started
ansible.builtin.service:
name: httpd
state: stopped

真幸运,默认的 EXAMPLES 示例使用的就是 httpd 网站服务。通过输出信息可得知, 使用state: started 参数来启动服务,而加入开机启动项使用的参数是 enabled: yes。继续编写:

root@linuxprobe:/etc/ansible/roles/apache# vim tasks/main.yml
---
- name: one
yum:
name: httpd
state: latest
- name: two
service:
name: httpd
state: started
enabled: yes

第 3 步:配置防火墙的允许策略,让其他主机能正常访问。在配置防火墙时,需要使用firewalld 模块。在 RHEL 10 系统中,firewalld 模块属于 ansible.posix 这个集合(collection),需要先安装:

root@linuxprobe:/etc/ansible/roles/apache# ansible-galaxy collection install ansible.posix
Starting galaxy collection install process
Process install dependency map
Starting collection install process
Downloading https://galaxy.ansible.com/api/v3/plugin/ansible/content/published/collections/artifacts/ansible-posix-2.0.0.tar.gz to /root/.ansible/tmp/ansible-local-3580_c2zd0z_/tmpfmch8wdd/ansible-posix-2.0.0-vf36mg2m
Installing 'ansible.posix:2.0.0' to '/root/.ansible/collections/ansible_collections/ansible/posix'
ansible.posix:2.0.0 was installed successfully

然后再查看一下帮助示例:

root@linuxprobe:/etc/ansible/roles/apache# ansible-doc firewalld
> ANSIBLE.POSIX.FIREWALLD (/root/.ansible/collections/ansible_collections/ansible/posix/plugins/modu>

This module allows for addition or deletion of services and ports (either
TCP or UDP) in either running or permanent firewalld rules.
………………省略部分输出信息………………
EXAMPLES:

- name: Permanently enable https service, also enable it immediately if possible
ansible.posix.firewalld:
service: https
state: enabled
permanent: true
immediate: true
offline: true

- name: Permit traffic in default zone for https service
ansible.posix.firewalld:
service: https
permanent: true
state: enabled

依据输出信息可得知,在firewalld 模块设置防火墙策略时,指定协议名称为service: http 参数,放行该协议为 state: enabled 参数,设置为永久生效为 permanent: yes 参数,当前立即生效为 immediate: yes 参数。参数虽然多了一些,但是基本与在第 8 章学习的一致,并不需要担心。继续编写:

root@linuxprobe:/etc/ansible/roles/apache# vim tasks/main.yml
---
- name: one
yum:
name: httpd
state: latest
- name: two
service:
name: httpd
state: started
enabled: yes
- name: three
firewalld:
service: http
permanent: yes
state: enabled
immediate: yes

第 4 步:让每台主机显示的主页文件均不相同。在使用 Ansible 的常规模块时,都是采用“查询帮助示例并模仿”的方式搞定的,这里为了增加难度,我们再提出一个新需求,即能否让每台主机上运行的httpd 网站服务都显示不同的内容呢?例如显示当前服务器的主机名及IP 地址。这就要用到template 模块及Jinja2 技术了。

依旧先使用 ansible-doc 命令查询 template 模块的使用方法。EXAMPLES 部分大有帮助:

root@linuxprobe:/etc/ansible/roles/apache# ansible-doc template
> ANSIBLE.BUILTIN.TEMPLATE (/usr/lib/python3.12/site-packages/ansible/modules/template.py)

Templates are processed by the Jinja2 templating language
<http://jinja.pocoo.org/docs/>. Documentation on the template formatting
can be found in the Template Designer Documentation
<http://jinja.pocoo.org/docs/templates/>. Additional variables listed below
can be used in templates. `ansible_managed' (configurable via the
`defaults' section of `ansible.cfg') contains a string which can be used to
describe the template name, host, modification time of the template file
and the owner uid. `template_host' contains the node name of the template's
machine. `template_uid' is the numeric user id of the owner.
`template_path' is the path of the template. `template_fullpath' is the
absolute path of the template. `template_destpath' is the path of the
template on the remote system (added in 2.8). `template_run_date' is the
date that the template was rendered.
………………省略部分输出信息………………
EXAMPLES:

- name: Template a file to /etc/file.conf
ansible.builtin.template:
src: /mytemplates/foo.j2
dest: /etc/file.conf
owner: bin
group: wheel
mode: '0644'

从template 模块的输出信息中可得知,这是一个用于复制文件模板的模块,能够把文件从 Ansible 服务器复制到受管主机上。其中,src 参数用于定义本地文件的路径,dest 参数用于定义复制到受管主机的文件路径,而 owner、group、mode 参数可选择性地设置文件归属及权限信息。

正常来说,我们可以直接进行复制文件操作,受管主机上会获取到一个与 Ansible 服务器上的文件完全一样的文件。但要是想让每台客户端根据自身系统的情况产生不同的文件信息, 就需要用到 Jinja2 技术了,Jinja2 格式的模板文件的后缀是.j2。继续编写:

root@linuxprobe:/etc/ansible/roles/apache# vim tasks/main.yml
---
- name: one
yum:
name: httpd
state: latest
- name: two
service:
name: httpd
state: started
enabled: yes
- name: three
firewalld:
service: http
permanent: yes
state: enabled
immediate: yes
- name: four
template:
src: index.html.j2
dest: /var/www/html/index.html
JINJA

Jinja2 是Python 语言中一个广泛使用的模板引擎,其设计灵感源自Django 的模板引擎。Jinja2 在此基础上发展出了更为强大的语法和功能,能够根据受管主机的变量动态生成不同的文件内容。简而言之,正常情况下的文件复制操作会使新旧文件完全一致,而使用 Jinja2 技术时,文件中并不会直接写入固定内容,而是写入一系列变量名称。

在使用 template 模块进行文件复制时,Ansible 服务会在受管主机上收集这些变量的对应值,并将其动态地填入模板文件中。最终生成的文件将根据每台主机的具体情况量身定制。这种方式极大地提升了自动化运维的灵活性和智能化,使得同一模板能适应多种环境和需求。

例如,想要让每个网站的输出信息值为“Welcome to 主机名 on 主机地址”,也就是用每个主机自己独有的名称和 IP 地址来替换文本中的内容,这样就有趣太多了。这个实验的难点在于查询到对应的变量名称、主机名及地址所对应的值保存在哪里?可以用 setup 模块进行查询。

root@linuxprobe:/etc/ansible/roles/apache# ansible-doc setup
> ANSIBLE.BUILTIN.SETUP (/usr/lib/python3.12/site-packages/ansible/modules/setup.py)

This module is automatically called by playbooks to gather useful variables
about remote hosts that can be used in playbooks. It can also be executed
directly by `/usr/bin/ansible' to check what variables are available to a
host. Ansible provides many `facts' about the system, automatically. This
module is also supported for Windows targets.
………………省略部分输出信息………………

setup 模块的作用是自动收集受管主机上的变量信息,使用-a 参数外加 filter 命令能够对收集来的信息进行二次过滤。相应的语法格式为 ansible all -m setup -a 'filter="关键词"',其中*号是第 3 章节讲到的通配符,用于进行关键词查询。例如,如果想搜索各个主机的名称,则可以使用通配符搜索所有包含 fqdn 关键词的变量值信息。

FQDN(Fully Qualified Domain Name,全限定域名)用于在逻辑上准确表示出主机的位置。FQDN 常常被作为主机名的完全表达形式,比/etc/hostname 文件中定义的主机名更加严谨和准确。通过输出信息可得知,ansible_fqdn 变量保存有主机名称。随后进行下一步操作:

root@linuxprobe:/etc/ansible/roles/apache# ansible all -m setup -a 'filter="*fqdn*"'
192.168.10.20 | SUCCESS => {
"ansible_facts": {
"ansible_fqdn": "linuxprobe.com",
"discovered_interpreter_python": "/usr/bin/python3"
},
"changed": false
}
………………省略部分输出信息………………

用于指定主机地址的变量可以用 ip 作为关键词进行检索。在下面的输出信息中, ansible_all_ipv4_addresses 变量中的值是我们想要的信息。如果想输出 IPv6 形式的地址,则可用 ansible_all_ipv6_addresses 变量。

root@linuxprobe:/etc/ansible/roles/apache# ansible all -m setup -a 'filter="*ip*"'
192.168.10.20 | SUCCESS => {
"ansible_facts": {
"ansible_all_ipv4_addresses": [
"192.168.10.20"
],
"ansible_all_ipv6_addresses": [
"fe80::20c:29ff:fed3:80d8"
],
"ansible_default_ipv4": {},
"ansible_default_ipv6": {},
"ansible_fips": false,
"ansible_locally_reachable_ips": {
"ipv4": [
"127.0.0.0/8",
"127.0.0.1",
"192.168.10.20"
],
"ipv6": [
"::1",
"fe80::20c:29ff:fed3:80d8"
]
},
"discovered_interpreter_python": "/usr/bin/python3"
},
"changed": false
}
………………省略部分输出信息………………

在确认了主机名与 IP 地址所对应的具体变量名称后,在角色所对应的 templates 目录内新建一个与上面的 template 模块参数相同的文件名称(index.html.j2)。Jinja2 在调用变量值时,格式为在变量名称的两侧各加两个大括号:

root@linuxprobe:/etc/ansible/roles/apache# vim templates/index.html.j2
Welcome to {{ ansible_fqdn }} on {{ ansible_all_ipv4_addresses }}

进行到这里,任务基本就算完成了。最后要做的就是编写一个用于调用 apache 角色的yml 文件,以及执行这个文件。

root@linuxprobe:/etc/ansible/roles/apache# cd ../../
root@linuxprobe:/etc/ansible# vim roles.yml
---
- name: 调用自建角色
hosts: all
roles:
- apache
root@linuxprobe:~# ansible-playbook roles.yml
PLAY [调用自建角色] **************************************************************************

TASK [Gathering Facts] **********************************************************************
ok: [192.168.10.20]
ok: [192.168.10.21]
ok: [192.168.10.22]
ok: [192.168.10.23]
ok: [192.168.10.24]

TASK [apache : one] *************************************************************************
changed: [192.168.10.20]
changed: [192.168.10.21]
changed: [192.168.10.22]
changed: [192.168.10.23]
changed: [192.168.10.24]

TASK [apache : two] *************************************************************************
changed: [192.168.10.20]
changed: [192.168.10.21]
changed: [192.168.10.22]
changed: [192.168.10.23]
changed: [192.168.10.24]

TASK [apache : three] ***********************************************************************
changed: [192.168.10.20]
changed: [192.168.10.21]
changed: [192.168.10.22]
changed: [192.168.10.23]
changed: [192.168.10.24]

TASK [apache : four] ***********************************************************************
changed: [192.168.10.20]
changed: [192.168.10.21]
changed: [192.168.10.22]
changed: [192.168.10.23]
changed: [192.168.10.24]

PLAY RECAP **********************************************************************************
192.168.10.20 : ok=5 changed=4 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
192.168.10.21 : ok=5 changed=4 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
192.168.10.22 : ok=5 changed=4 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
192.168.10.23 : ok=5 changed=4 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
192.168.10.24 : ok=4 changed=4 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

执行完毕后,在浏览器中随机输入几台主机的 IP 地址,即可访问包含主机 FQDN 和 IP地址的网页了,如图 16-8 到图 16-10 所示。

图 16-8 随机访问一台主机的网站主页

图 16-9 随机访问一台主机的网站主页

图 16-10 随机访问一台主机的网站主页

实验相当成功!

16.6 创建和使用逻辑卷

创建一个能批量、自动管理逻辑卷设备的剧本,不但能大大提高磁盘设备的管理效率, 而且还能避免手动创建带来的错误。例如,我们想在每台受管主机上都创建出一个名为 data 的逻辑卷设备,大小为 150MB,归属于 research 卷组。如果创建成功,则进一步用 Ext4 文件系统进行格式化操作;如果创建失败,则给用户输出一条报错提醒,以便排查原因。

在这种情况下,使用 Ansible 剧本要比使用 Shell 脚本的优势大,原因主要有下面两点。

Ansible 模块化的功能让操作更标准,只要在执行过程中无报错,那么便会依据远程主机的系统版本及配置自动做出判断和操作,不用担心因系统变化而导致命令失效的问题。

Ansible 服务在执行剧本文件时会进行判断:如果该文件或该设备已经被创建过,或是某个动作(play)已经被执行过,则绝对不会再重复执行;而使用 Shell 脚本有可能导致设备被重复格式化,导致数据丢失。

首先在 prod 组的两台主机上分别添加一块磁盘设备,大小为 20GB,类型为 SCSI,其余选项选择默认值,如图 16-11 到图 16-13 所示。

图 16-11 添加一块新磁盘

图 16-12 选择磁盘类型

图 16-13 新磁盘添加完毕

通过回忆第 7 章学习过的逻辑卷的知识,我们应该让剧本文件依次创建物理卷(PV)、卷组(VG)及逻辑卷(LV)。需要先使用 lvg 模块让设备支持逻辑卷技术,然后创建一个名为 research 的卷组。lvg 模块的帮助信息如下:

root@linuxprobe:/etc/ansible# ansible-doc lvg
> COMMUNITY.GENERAL.LVG (/root/.ansible/collections/ansible_collections/community/general/plugins/mo>

This module creates, removes or resizes volume groups.
………………省略部分输出信息………………
EXAMPLES:

- name: Create a volume group on top of /dev/sda1 with physical extent size = 32MB
community.general.lvg:
vg: vg.services
pvs: /dev/sda1
pesize: 32

- name: Create a volume group on top of /dev/sdb with physical extent size = 128KiB
community.general.lvg:
vg: vg.services
pvs: /dev/sdb
pesize: 128K

通过输出信息可知,创建 PV 和 VG 的lvg 模块总共有 3 个必备参数。其中,vg 参数用于定义卷组的名称,pvs 参数用于指定磁盘设备的名称,pesize 参数用于确定最终卷组的容量大小(可以用 PE 个数或容量值进行指定)。这样一来,我们先创建出一个由/dev/sdb 设备组成的名称为 research、大小为 150MB 的卷组设备。

root@linuxprobe:/etc/ansible# vim lv.yml
---
- name: 创建和使用逻辑卷
hosts: all
tasks:
- name: one
lvg:
vg: research
pvs: /dev/sdb
pesize: 150M

由于刚才只在 prod 组的两台主机上添加了新磁盘设备文件,因此在执行上述操作时其余 3 台主机会提示未创建成功,这属于正常情况。接下来使用 lvol 模块创建出逻辑卷设备。还是按照惯例,先查看模块的帮助信息:

root@linuxprobe:/etc/ansible# ansible-doc lvol
> COMMUNITY.GENERAL.LVOL (/root/.ansible/collections/ansible_collections/community/general/plugins/m>

This module creates, removes or resizes logical volumes.
………………省略部分输出信息………………
EXAMPLES:

- name: Create a logical volume of 512m
community.general.lvol:
vg: firefly
lv: test
size: 512

- name: Create a logical volume of 512m with disks /dev/sda and /dev/sdb
community.general.lvol:
vg: firefly
lv: test
size: 512
pvs:
- /dev/sda
- /dev/sdb

通过输出信息可知,lvol 是用于创建逻辑卷设备的模块。其中,vg 参数用于指定卷组名称,lv 参数用于指定逻辑卷名称,size 参数则用于指定最终逻辑卷设备的容量大小(不用加单位,默认为 MB)。填写好参数,创建出一个大小为 150MB、归属于 research 卷组且名称为 data 的逻辑卷设备:

root@linuxprobe:/etc/ansible# vim lv.yml
---
- name: 创建和使用逻辑卷
hosts: all
tasks:
- name: one
lvg:
vg: research
pvs: /dev/sdb
pesize: 150M
- name: two
lvol:
vg: research
lv: data
size: 150M

这样还不够好,如果还能将创建出的/dev/research/data 逻辑卷设备自动用Ext4 文件系统进行格式化操作,就可以进一步减少运维管理员的一些工作量。可使用 filesystem 模块来完成设备的文件系统格式化操作。该模块的帮助信息如下:

root@linuxprobe:/etc/ansible# ansible-doc filesystem
> COMMUNITY.GENERAL.FILESYSTEM (/root/.ansible/collections/ansible_collections/community/general/plu>

This module creates a filesystem.
………………省略部分输出信息………………
EXAMPLES:

- name: Create a ext2 filesystem on /dev/sdb1
community.general.filesystem:
fstype: ext2
dev: /dev/sdb1

filesystem 模块的参数真是简练,fstype 参数用于指定文件系统的格式化类型,dev 参数用于指定要格式化的设备文件路径。继续编写:

root@linuxprobe:/etc/ansible# vim lv.yml
---
- name: 创建和使用逻辑卷
hosts: all
tasks:
- name: one
lvg:
vg: research
pvs: /dev/sdb
pesize: 150M
- name: two
lvol:
vg: research
lv: data
size: 150M
- name: three
filesystem:
fstype: ext4
dev: /dev/research/data

这样按照顺序执行下来,逻辑卷设备就能自动创建好了。等一下,还有个问题没有解决。现在只有 prod 组的主机上添加了新的磁盘设备文件,其余主机是无法按照既定模块顺利完成操作的。这时就要使用类似于第 4 章学习的 if 条件语句的方式进行判断—如果失败……,则……。

首先用 block 操作符将上述的 3 个模块命令作为一个整体(相当于对这 3 个模块的执行结果作为一个整体进行判断),然后使用 rescue 操作符进行救援,且只有 block 块中的模块执行失败后才会调用 rescue 中的救援模块。其中,debug 模块的 msg 参数的作用是, 如果block 中的模块执行失败,则输出一条信息到屏幕,用于提醒用户。编写完成后的剧本是下面这个样子:

root@linuxprobe:/etc/ansible# vim lv.yml
---
- name: 创建和使用逻辑卷
hosts: all
tasks:
- block:
- name: one
lvg:
vg: research
pvs: /dev/sdb
pesize: 150M
- name: two
lvol:
vg: research
lv: data
size: 150M
- name: three
filesystem:
fstype: ext4
dev: /dev/research/data
rescue:
- debug:
msg: "Could not create logical volume of that size"

YAML 语言对格式有着硬性的要求,既然 rescue 是对 block 内的模块进行救援的功能代码,因此 rescue 和 block 两个操作符必须严格对齐,错开一个空格都会导致剧本执行失败。确认无误后,执行 lv.yml 剧本文件检阅一下效果:

root@linuxprobe:/etc/ansible# ansible-playbook lv.yml 

PLAY [创建和使用逻辑卷] *********************************************************

TASK [Gathering Facts] *********************************************************
ok: [192.168.10.20]
ok: [192.168.10.21]
ok: [192.168.10.22]
ok: [192.168.10.23]
ok: [192.168.10.24]

TASK [one] *********************************************************************
fatal: [192.168.10.20]: FAILED! => {"changed": false, "msg": "Device /dev/sdb not found."}
fatal: [192.168.10.21]: FAILED! => {"changed": false, "msg": "Device /dev/sdb not found."}
changed: [192.168.10.22]
changed: [192.168.10.23]
fatal: [192.168.10.24]: FAILED! => {"changed": false, "msg": "Device /dev/sdb not found."}

TASK [two] *********************************************************************
changed: [192.168.10.22]
changed: [192.168.10.23]

TASK [three] *********************************************************************
changed: [192.168.10.22]
changed: [192.168.10.23]

TASK [debug] *******************************************************************
ok: [192.168.10.20] => {
"msg": "Could not create logical volume of that size"
}
ok: [192.168.10.21] => {
"msg": "Could not create logical volume of that size"
}
ok: [192.168.10.24] => {
"msg": "Could not create logical volume of that size"
}

PLAY RECAP *********************************************************************
192.168.10.20 : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=1 ignored=0
192.168.10.21 : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=1 ignored=0
192.168.10.22 : ok=4 changed=3 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
192.168.10.23 : ok=4 changed=3 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
192.168.10.24 : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=1 ignored=0

在剧本运行完毕后的执行记录(PLAY RECAP)中可很清晰地看到只有 192.168.10.22 及 192.168.10.23 这两台 prod 组中的主机执行成功了,其余 3 台主机均触发了 rescue 功能。登录到任意一台 prod 组的主机上,找到新建的逻辑卷设备信息:

root@linuxprobe:/etc/ansible# lvdisplay 
--- Logical volume ---
LV Path /dev/research/data
LV Name data
VG Name research
LV UUID jI3PoD-az0v-oTRZ-w8Ve-FI08-LvDk-2oISvZ
LV Write Access read/write
LV Creation host, time linuxprobe.com, 2025-04-03 20:28:41 +0800
LV Status available
# open 0
LV Size 150.00 MiB
Current LE 1
Segments 1
Allocation inherit
Read ahead sectors auto
- currently set to 8192
Block device 253:2
………………省略部分输出信息………………

16.7 判断主机组名

在上面的剧本实验中,我们可以让不同的主机根据自身不同的变量信息而生成独特的网站主页文件,但却无法对某个主机组进行针对性的操作。其实,在每个客户端中都会有一个名为 inventory_hostname 的变量,用于定义每台主机所对应的 Ansible 服务的主机组名称,也就是/etc/ansible/hosts 文件中所对应的分组信息,例如 dev、test、prod、balancers。

inventory_hostname 是 Ansible 服务中的魔法变量,这意味着无法使用 setup 模块直接进行查询,诸如 ansible all -m setup -a 'filter="关键词"'这样的命令将对它失效。魔法变量需要在执行剧本文件时的 Gathering Facts 阶段进行搜集,直接查询是看不到的,只能在剧本文件中进行调用。

在获得了存储主机组名称的变量名称后,接下来开始实战。这里的需求如下:

若主机在 dev 分组中,则修改/etc/issue 文件内容为 Development;

若主机在 test 分组中,则修改/etc/issue 文件内容为 Test;

若主机在 prod 分组中,则修改/etc/issue 文件内容为 Production。

根据表 16-5 所提及的 Ansible 常用模块名称及作用,可以看到copy 模块的主要作用是新建、修改及复制文件,更符合当前的需要,此时它便派上了用场。先查询 copy 模块的帮助信息:

root@linuxprobe:/etc/ansible# ansible-doc copy
> ANSIBLE.BUILTIN.COPY (/usr/lib/python3.12/site-packages/ansible/modules/copy.py)

The [ansible.builtin.copy] module copies a file or a directory structure
from the local or remote machine to a location on the remote machine. File
system meta-information (permissions, ownership, etc.) may be set, even
when the file or directory already exists on the target system. Some meta-
information may be copied on request. Get meta-information with the
[ansible.builtin.stat] module. Set meta-information with the
[ansible.builtin.file] module. Use the [ansible.builtin.fetch] module to
copy files from remote locations to the local box. If you need variable
interpolation in copied files, use the [ansible.builtin.template] module.
Using a variable with the `content' parameter produces unpredictable
results. For Windows targets, use the [ansible.windows.win_copy] module
instead.
………………省略部分输出信息………………
EXAMPLES:
- name: Copy file with owner and permissions
ansible.builtin.copy:
src: /srv/myfiles/foo.conf
dest: /etc/foo.conf
owner: foo
group: foo
mode: '0644'

- name: Copy file with owner and permission, using symbolic representation
ansible.builtin.copy:
src: /srv/myfiles/foo.conf
dest: /etc/foo.conf
owner: foo
group: foo
mode: u=rw,g=r,o=r

在输出信息中列举了两种管理文件内容的示例。第一种用于文件的复制行为,第二种是通过content 参数定义内容,通过 dest 参数指定新建文件的名称。显然,第二种更加符合当前的实验场景。编写剧本文件如下:

root@linuxprobe:/etc/ansible# vim issue.yml
---
- name: 修改文件内容
hosts: all
tasks:
- name: one
copy:
content: 'Development'
dest: /etc/issue
- name: two
copy:
content: 'Test'
dest: /etc/issue
- name: three
copy:
content: 'Production'
dest: /etc/issue

但是,如果按照这种顺序执行下去,每一台主机的/etc/issue 文件都会被重复修改 3 次,最终定格在 Production 字样,这显然缺少了一些东西。我们应该依据 inventory_ hostname 变量中的值进行判断。若主机为 dev 组,则执行第一个动作;若主机为 test 组, 则执行第二个动作;若主机为 prod 组,则执行第三个动作。因此,要进行 3 次判断。

when 是用于判断的语法,我们将其用在每个动作的下方进行判断,使得只有在满足条件才会执行:

root@linuxprobe:/etc/ansible# vim issue.yml
---
- name: 修改文件内容
hosts: all
tasks:
- name: one
copy:
content: 'Development'
dest: /etc/issue
when: "inventory_hostname in groups.dev"
- name: two
copy:
content: 'Test'
dest: /etc/issue
when: "inventory_hostname in groups.test"
- name: three
copy:
content: 'Production'
dest: /etc/issue
when: "inventory_hostname in groups.prod"

执行剧本文件,在过程中可清晰地看到由于 when 语法的作用,未在指定主机组中的主机将被跳过(skipping):

root@linuxprobe:/etc/ansible# ansible-playbook issue.yml 

PLAY [修改文件内容] ************************************************************************

TASK [Gathering Facts] ********************************************************************
ok: [192.168.10.20]
ok: [192.168.10.21]
ok: [192.168.10.22]
ok: [192.168.10.23]
ok: [192.168.10.24]

TASK [one] ********************************************************************************
changed: [192.168.10.20]
skipping: [192.168.10.21]
skipping: [192.168.10.22]
skipping: [192.168.10.23]
skipping: [192.168.10.24]

TASK [two] ********************************************************************************
skipping: [192.168.10.20]
changed: [192.168.10.21]
skipping: [192.168.10.23]
skipping: [192.168.10.24]
skipping: [192.168.10.25]

TASK [three] ******************************************************************************
skipping: [192.168.10.20]
skipping: [192.168.10.21]
changed: [192.168.10.22]
changed: [192.168.10.23]
skipping: [192.168.10.24]

PLAY RECAP ********************************************************************************
192.168.10.20 : ok=2 changed=1 unreachable=0 failed=0 skipped=2 rescued=0 ignored=0
192.168.10.21 : ok=2 changed=1 unreachable=0 failed=0 skipped=2 rescued=0 ignored=0
192.168.10.22 : ok=2 changed=1 unreachable=0 failed=0 skipped=2 rescued=0 ignored=0
192.168.10.23 : ok=2 changed=1 unreachable=0 failed=0 skipped=2 rescued=0 ignored=0
192.168.10.24 : ok=1 changed=0 unreachable=0 failed=0 skipped=3 rescued=0 ignored=0

登录到 dev 组的 192.168.10.20 主机上,查看文件内容:

root@linuxprobe:~# cat /etc/issue 
Development

登录到 test 组的 192.168.10.21 主机上,查看文件内容:

root@linuxprobe:~# cat /etc/issue 
Test

登录到 prod 组的 192.168.10.22/23 主机上,查看文件内容:

root@linuxprobe:~# cat /etc/issue 
Production

16.8 管理文件属性

我们学习剧本的目的是满足日常的工作需求,把重复的事情写入脚本中,然后再批量执行下去,从而提高运维工作的效率。其中,创建文件、管理权限以及设置快捷方式几乎是每天都要用到的技能。尤其是在第 5 章学习文件的一般权限、特殊权限、隐藏权限时,往往还会因命令的格式问题而导致出错。这么多命令该怎么记呢?

Ansible 服务将常用的文件管理功能都合并到了 file 模块中,大家不用再为了寻找模块而“东奔西跑”了。先来看一下 file 模块的帮助信息:

root@linuxprobe:/etc/ansible# ansible-doc file
> ANSIBLE.BUILTIN.FILE (/usr/lib/python3.12/site-packages/ansible/modules/file.py)

Set attributes of files, directories, or symlinks and their targets.
Alternatively, remove files, symlinks or directories. Many other modules
support the same options as the [ansible.builtin.file] module - including
[ansible.builtin.copy], [ansible.builtin.template], and
[ansible.builtin.assemble]. For Windows targets, use the
[ansible.windows.win_file] module instead.
………………省略部分输出信息………………
EXAMPLES:

- name: Change file ownership, group and permissions
ansible.builtin.file:
path: /etc/foo.conf
owner: foo
group: foo
mode: '0644'

- name: Give insecure permissions to an existing file
ansible.builtin.file:
path: /work
owner: root
group: root
mode: '1777'

- name: Create a symbolic link
ansible.builtin.file:
src: /file/to/link/to
dest: /path/to/symlink
owner: foo
group: foo
state: link

通过上面的输出示例,大家已经能够了解 file 模块的基本参数了。其中,path 参数定义了文件的路径,owner 参数定义了文件所有者,group 参数定义了文件所属组,mode 参数定义了文件权限,src 参数定义了源文件的路径,dest 参数定义了目标文件的路径,state参数则定义了文件类型。

可见,file 模块基本上把第 5 章学习过的管理文件权限的功能都包含在内了。我们现在来挑战下面的实验吧:

请创建出一个名为/linuxprobe 的新目录,所有者及所属组均为root 管理员身份;

设置所有者和所属组拥有对文件的完全控制权,而其他人则只有可读和可执行权限;

给予SGID 特殊权限;

仅在dev 主机组的主机上实施。

第二条要求是算术题,即将权限描述转换为数字表示法,即可读为 4、可写为 2、可执行为 1。大家先自行默默计算一下答案。此前在编写剧本文件时,hosts 参数对应的一直是all,即全体主机,这次需要修改为仅对 dev 主机组成员生效,请小心谨慎。编写模块代码如下:

root@linuxprobe:/etc/ansible# vim chmod.yml
---
- name: 管理文件属性
hosts: dev
tasks:
- name: one
file:
path: /linuxprobe
state: directory
owner: root
group: root
mode: '2775'

一不小心把题目出简单了,这里没能完全展示出 file 模块的强大之处。我们临时添加一个需求:请再创建一个名称为/linuxcool 的快捷方式文件,指向刚刚建立的/linuxprobe 目录。这样用户在访问两个目录时就能有相同的内容了。在使用 file 模块设置快捷方式时,不需要再单独创建目标文件,Ansible 服务会帮我们完成。

root@linuxprobe:/etc/ansible# vim chmod.yml
---
- name: 管理文件属性
hosts: dev
tasks:
- name: one
file:
path: /linuxprobe
state: directory
owner: root
group: root
mode: '2775'
- name: two
file:
src: /linuxprobe
dest: /linuxcool
state: link

剧本文件的执行过程如下所示:

root@linuxprobe:/etc/ansible# ansible-playbook chmod.yml 

PLAY [管理文件属性] ***************************************************************

TASK [Gathering Facts] ***********************************************************
ok: [192.168.10.20]
ok: [192.168.10.21]
ok: [192.168.10.22]
ok: [192.168.10.23]
ok: [192.168.10.24]

TASK [one] ***********************************************************************
changed: [192.168.10.20]
skipping: [192.168.10.21]
skipping: [192.168.10.22]
skipping: [192.168.10.23]
skipping: [192.168.10.24]

TASK [two] ***********************************************************************
changed: [192.168.10.20]
skipping: [192.168.10.21]
skipping: [192.168.10.22]
skipping: [192.168.10.23]
skipping: [192.168.10.24]

PLAY RECAP ***********************************************************************
192.168.10.20 : ok=3 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
192.168.10.22 : ok=1 changed=0 unreachable=0 failed=0 skipped=3 rescued=0 ignored=0
192.168.10.22 : ok=1 changed=0 unreachable=0 failed=0 skipped=3 rescued=0 ignored=0
192.168.10.22 : ok=1 changed=0 unreachable=0 failed=0 skipped=3 rescued=0 ignored=0
192.168.10.22 : ok=1 changed=0 unreachable=0 failed=0 skipped=3 rescued=0 ignored=0

进入dev 组的主机中,看到/linuxprobe 目录及/linuxcool 的快捷方式均已经被顺利创建:

root@linuxprobe:~# ls -ld /linuxprobe
drwxrwsr-x. 2 root root 6 Apr 3 20:35 /linuxprobe
root@linuxprobe:~# ls -ld /linuxcool
lrwxrwxrwx. 1 root root 11 Apr 3 20:35 /linuxcool -> /linuxprobe

16.9 管理密码库文件

自Ansible 1.5 版本发布后,vault 作为一项新功能进入到了运维人员的视野。它不仅能对密码、剧本等敏感信息进行加密,而且还能加密变量名称和变量值,从而确保数据不会被他人轻易阅读。使用 ansible-vault 命令可以实现内容的创建(create)、加密(encrypt)、解密(decrypt)、修改密码(rekey)及查看(view)等功能。

下面通过示例来学习 vault 的具体用法。

第 1 步:创建一个名为 locker.yml 的配置文件,其中保存了两个变量值。

root@linuxprobe:/etc/ansible# vim locker.yml
---
pw_developer: Imadev
pw_manager: Imamgr

第 2 步:使用 ansible-vault 命令对文件进行加密。由于每次加密使时都需要输入密码,比较麻烦,因此还应新建一个用于保存密码值的文本文件,以便让ansible-vault 命令自动调用。为了保证数据的安全性,在新建密码文件后将该文件的权限设置为 600,确保仅管理员可读可写。

root@linuxprobe:/etc/ansible# vim /root/secret.txt
whenyouwishuponastar
root@linuxprobe:/etc/ansible# chmod 600 /root/secret.txt

在Ansible 服务的主配置文件中,在第 278 行的vault_password_file 参数后指定保存密码值的文本文件的路径,准备进行调用:

root@linuxprobe:/etc/ansible# vim /etc/ansible/ansible.cfg
137
138 # If set, configures the path to the Vault password file as an alternative to
139 # specifying --vault-password-file on the command line.
140 vault_password_file = /root/secret.txt
141

第 3 步:在设置好密码文件的路径后,Ansible 服务便会自动进行加载。用户也就不用在每次加密或解密时都重复输入密码了。例如,在加密刚刚创建的 locker.yml 文件时,只需要使用 encrypt 参数即可。

root@linuxprobe:/etc/ansible# ansible-vault encrypt locker.yml
Encryption successful

文件将使用 AES 256 加密方式进行加密,也就是意味着密钥有 2256 种可能。查看到加密后的内容为:

root@linuxprobe:/etc/ansible# cat locker.yml 
$ANSIBLE_VAULT;1.1;AES256
38666539353062343930633135306633613736386366633665353464343964613838356135616137
3538616139303438316636613564313266353833336337640a313963316664303431383132386362
30326430643234653336363130393238633266386636333666633932613937326135373766656539
3637386562646539610a333761333039323565353761353636623762623435616163333031376333
36316334386562326337363730353463323462316131383064333234626561366338356261383333
3235626134393832323436386232646562396133666435386361

如果不想使用原始密码了呢?也可以使用rekey 参数手动对文件进行改密操作,同时应结合--ask-vault-pass 参数进行修改,否则 Ansible 服务会因接收不到用户输入的旧密码而拒绝新的密码变更请求:

root@linuxprobe:/etc/ansible# ansible-vault rekey --ask-vault-pass locker.yml 
Vault password: 输入旧的密码
New Vault password: 输入新的密码
Confirm New Vault password: 再输入新的密码
Rekey successful

第 4 步:如果想查看和修改加密文件中的内容,该怎么操作呢?对于已经加密过的文件, 需要使用 ansible-vault 命令的 edit 参数进行修改,随后用 view 参数即可查看到修改后的内容。ansible-vault 命令对加密文件的编辑操作默认使用的是 Vim 编辑器,在修改完毕后请记得执行 wq 操作保存后退出。

root@linuxprobe:/etc/ansible# ansible-vault edit locker.yml
---
pw_developer: Imadev
pw_manager: Imamgr
pw_production: Imaprod

最后,再用 view 参数进行查看,便是最新的内容了:

root@linuxprobe:/etc/ansible# ansible-vault view locker.yml
Vault password: 输入密码后敲击回车确认
---
pw_developer: Imadev
pw_manager: Imamgr
pw_production: Imaprod

复习题

  1. 当前已经搭建好软件仓库,但却不能用 dnf 命令安装 Ansible 服务。这有可能是什么原因呢?

    答:RHEL 10 系统中默认的BaseOS 和AppStream 软件仓库不包含 Ansible 服务软件包,需要额外配置 EPEL 安装源。

  2. 当/etc/ansible/ansible.cfg 与~/.ansible.cfg 两个主配置文件都同时存在时, 以哪个为准?

    答:个人家目录中的主配置文件的优先级更高。

  3. 使用Ansible 的哪个模块能启动服务,使用 Ansbile 的哪个模块能挂载磁盘设备文件?

    答:使用 service 模块可启动服务,使用 mount 模块可挂载磁盘设备文件。

  4. 我们想了解一个模块的作用,可以使用什么命令来查询它的帮助信息?

    答:可以使用ansible-doc 命令。

  5. Ansible 角色有几种获取方法?

    答:有 3 种,分别是加载系统内置角色、从外部环境获取角色以及自行创建角色。

  6. 在执行剧本文件时,出现了黄色显示的changed 字样,这表示什么意思?

    答:表示剧本文件执行成功并进行了修改。

  7. Jinja2 与copy 复制模块有什么区别?

    答:使用 copy 模块时,是进行 1:1 的复制,源文件与目标文件完全相同。而使用 Jinja2时,则是将源文件中的变量名替换成受控主机上的实际变量值。

  8. 想要让剧本内容对主机清单中的所有受控主机都生效,应如何操作?

    答:需在剧本文件中使用 hosts: all 参数,确保主机清单文件中的所有主机均执行相应操作。

  9. 如果只记得某个变量的部分名称,想通过关键词搜索该变量的相关信息,该执行什么命令?

    答:可执行命令 ansible all -m setup-a 'filter="关键词"'。

  10. 在使用ansible-vault 命令进行加密时,默认使用的是哪种加密方式?

    答:AES 256。