1
2
3
4
5
作者:李晓辉

微信联系:lxh_chat

联系邮箱: 939958092@qq.com

第一章 介绍 Ansible

⾃动化与 Linux 系统管理

手工运维的时候,存在以下问题:

  • 低效率

    • 手工操作需要耗费大量时间和人力,尤其是在需要频繁执行重复任务时,效率明显低于自动化工具。
  • 易出错

    • 人为操作容易产生错误,特别是在执行复杂或繁琐的任务时。错误可能导致系统故障、服务中断,甚至造成数据丢失。
  • 难以追踪

    • 手工操作通常缺乏详细的记录和审计功能,难以追踪和回溯问题。自动化工具则能够提供操作日志和审计功能,便于问题排查和责任划分。
  • 不一致性

    • 不同的运维人员执行相同任务时,方法和步骤可能不同,导致环境配置和服务状态不一致。这种不一致性可能导致难以预料的问题和故障。
  • 可扩展性差

    • 手工操作难以在大规模环境中进行有效管理和扩展。在大规模运维场景中,自动化工具能够轻松处理大量节点和复杂操作。
  • 响应时间长

    • 手工操作响应时间较长,尤其是在突发事件和紧急故障处理中。而自动化工具能够快速执行预定义的操作和恢复措施,缩短故障恢复时间。
  • 运维成本高

    • 手工运维需要投入大量人力,导致运维成本高昂。自动化工具能够显著降低人力需求,减少运维成本。
  • 知识转移困难

    • 手工操作依赖于运维人员的经验和技能,新人接手时可能需要较长的培训和适应期。自动化工具则通过脚本和配置文件实现知识的标准化和传递,便于团队协作和人员更替。

使用自动化系统的好处

  1. 高效性
  • 自动化流程:自动化系统可以处理重复性任务,提高工作效率,减少人为错误。

  • 快速部署:自动化工具能够快速部署基础架构和应用程序,缩短产品交付时间。

  1. 一致性
  • 标准化配置:通过代码定义基础架构,确保在不同环境中配置的一致性,避免因手动操作导致的不一致问题。

  • 版本控制:所有配置和代码都可以版本化管理,确保任何更改都有据可查,并且能够快速回滚到之前的版本。

  1. 可扩展性
  • 弹性伸缩:自动化系统能够根据需求自动扩展或缩减资源,优化资源使用,降低成本。

  • 大规模管理:能够有效管理大规模的服务器和应用实例,适应复杂的企业级需求。

  1. 可维护性
  • 易于管理:通过自动化工具,管理员可以轻松地管理和监控系统,简化运维工作。

  • 故障恢复:自动化系统可以快速检测故障并执行预定义的恢复步骤,提高系统的可靠性和可用性。

  1. 基础架构即代码(IaC)
  • 定义基础架构:使用代码定义基础架构(如 Ansible、Terraform),确保基础架构的可重复、可审计和可版本化。

  • 简化变更管理:通过代码进行变更管理,减少手动配置错误,增强变更的可控性和可预测性。

  1. 安全性
  • 权限管理:通过自动化工具,能够精细化管理权限和访问控制,增强系统安全性。

  • 合规性检查:自动化系统可以定期执行合规性检查,确保系统配置符合安全标准和法规要求。

Ansible 概念与安装

什么是 Ansible?

Ansible 是⼀款开源⾃动化平台。它是⼀种简单的⾃动化语⾔,可在 Ansible Playbook 中完美描述 IT应⽤基础架构。它还是⼀个⾃动化引擎,可运⾏ Ansible Playbook。

  • Ansible Playbook 提供⼈类可读的⾃动化,且易于书写和阅读,不需要会编程,Playbook 按顺序执⾏任务

  • Ansible ⽆需代理即可在被管理机器上执行任务,Ansible 通过 OpenSSH 或 WinRM 连接所管理的主机并运⾏任务,⽅法通常是(但不总是)将称为 Ansible 模块的⼩程序推送⾄这些主机。这些程序⽤于将系统置于需要的特定状态。在 Ansible 运⾏完其任务后,推送的所有模块都会被删除。

Ansible 的优点:

  • 跨平台支持:Ansible 提供 Linux、Windows、UNIX 和网络设备的无代理支持,适用于物理、虚拟、云和容器环境。

  • 人类可读的自动化:Ansible Playbook 采用 YAML 文本文件编写,易于阅读,有助于确保所有人都能理解它们的用途。

  • 精准的描述应用:可以通过 Ansible Playbook 进行每一种更改,并描述和记录应用环境的每一个方面。

  • 轻松管理版本控制:Ansible Playbook 和项目是纯文本。它们可以视作源代码,放在您的现有版本控制系统中。

  • 支持动态清单:可以从外部来源动态更新 Ansible 管理的计算机的列表,随时获取所有受管服务器的当前正确列表,不受基础架构或位置的影响。

  • 编排可与其他系统轻松集成:能够利用环境中现有的 HP SA、Puppet、Jenkins、红帽卫星和其他系统,并且集成到您的 Ansible 工作流中。

Ansible 的架构图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
+---------------------+                     +--------------------+
| | | |
| 管理主机 | | 被管理主机 |
| | | |
| +-----------------+ | | +--------------+ |
| | | | SSH 连接 | | | |
| | Ansible |<----------------------> | 被管理主机 1 | |
| | | | | +--------------+ |
| +----+------------+ | +--------------------+
| | | +--------------------+
| | Playbook | | |
| | | | 被管理主机 |
| v | | |
| +-----------------+ | | +--------------+ |
| | | | SSH 连接 | | | |
| | Inventory |<----------------------> | 被管理主机 2 | |
| | | | | +--------------+ |
| +----+------------+ | +--------------------+
| | |
| | Module |
| | |
| v |
| +-----------------+ |
| | | |
| | Ansible CLI | |
| | | |
| +-----------------+ |
+---------------------+

架构说明:

  • Ansible 管理主机

    • 部署和运行 Ansible 的主机,负责执行 Playbook 和管理被管理机。
  • Inventory(清单)

    • 包含被管理机的列表及其分组信息。Ansible 根据 Inventory 中的定义连接和管理这些主机。
  • Playbook(剧本)

    • 使用 YAML 编写的任务配置文件,定义了一系列自动化任务和操作步骤。Ansible 根据 Playbook 执行特定的配置和操作。
  • Module(模块)

    • Ansible 中的功能单元,执行具体的操作任务,如安装软件包、管理服务、复制文件等。Playbook 调用这些模块来完成任务。大部分的模块都具有幂等性,但是shell、command、raw这些模块除外。
  • Ansible CLI(命令行界面)

    • Ansible 的命令行工具,用于执行和管理 Playbook、Inventory 和 Module。
  • 被管理主机

    • Ansible 管理和配置的目标主机,通过 SSH 连接进行通信和操作。

Ansible 设计哲学

  1. 复杂性会破坏效率

越简单越好。Ansible 的设计宗旨是工具易用,自动化易写易读。

  1. 专为易读性优化

Ansible 自动化语言围绕简单易读的声明性文本文件来构建。正确编写的 Ansible Playbook 可以清楚地记录工作流自动化。

  1. 声明式思维

Ansible 是一种要求状态引擎。它通过表达希望系统处于何种状态来解决如何自动化 IT 部署的问题。Ansible 的目标在于仅执行必要更改,将系统置于所需状态。不建议将 Ansible 视为脚本语言。

Ansible能干什么

  • 配置管理

    • 安装和配置软件:自动安装和配置操作系统及应用程序软件。

    • 文件和目录管理:管理文件和目录,包括创建、修改和删除操作。

    • 用户和权限管理:管理用户账户和权限设置,确保系统安全。

  • 应用部署

    • 应用程序部署:自动化应用程序的部署过程,包括拉取代码、编译、打包和部署。

    • 多层应用部署:支持在多台服务器上部署复杂的多层应用程序,例如 Web 应用、数据库和缓存服务。

  • 编排工作流

    • 任务编排:定义和执行复杂的工作流,包括多步骤任务的顺序执行和依赖管理。

    • 跨平台自动化:在多种操作系统和环境中实现一致的自动化操作,包括 Linux、Windows 和云平台。

  • 基础架构即代码(IaC)

    • 云资源管理:管理云资源,例如 AWS、Azure 和 Google Cloud 的虚拟机、存储和网络配置。

    • 网络设备配置:自动配置和管理网络设备,包括路由器、交换机和防火墙。

  • 持续集成和持续部署(CI/CD)

    • 构建和测试:自动化代码构建和测试过程,确保代码质量。

    • 部署流水线:设置和管理持续集成和持续部署流水线,自动化发布和部署过程。

  • 安全和合规

    • 安全补丁管理:自动部署安全补丁和更新,确保系统安全。

    • 合规检查:执行合规性检查,确保系统配置符合安全标准和法规要求。

  • 监控和报告

    • 系统监控:集成监控工具,自动化配置和管理监控服务。

    • 报告生成:生成和分发系统状态和性能报告,便于分析和决策。

安装 Ansible

先选一个Ansible 获取渠道

Ansible和RHAAP

社区 Ansible

上游 Ansible 社区通过两种⽅式开发 Ansible 和分发其版本,不管你选谁,红帽都不给你支持,你只能寻求社区支持或Google去搜。

  1. Ansible Core。这是⼀个简约组件,由解释 Ansible 内容的核⼼运⾏时和⼀组常⽤ Ansible模块(作为 ansible.builtin Ansible 内容集合提供)组成。此运⾏时采取特殊架构,让控制节点充当 Ansible 代码的执⾏环境。

  2. 社区 Ansible。这是 Ansible Core 的发⾏版,外加开源社区选择的其他 Ansible 内容集合,增加了额外的 Ansible 模块和⻆⾊。

红帽版本

这些版本就可以获得支持

  1. 红帽以 RPM 软件包(ansible-core)的形式提供 Ansible Core,该软件包随红帽企业 Linux 9⼀起,通过 AppStream 存储库提供。

  2. 红帽 Ansible 高级⾃动化平台,简称rhaap,这个需要购买订阅才能安装和使用。

RHAAP介绍

本课程中,用的是RHAAP2.2,RHAAP提供了以下几个组件:

  1. Ansible Core,Ansible Core 提供⽤于运⾏ Ansible Playbook 的基本功能。比如剧本和模块的执行。RHAAP提供了ansible-core软件包以及在ee-minimal-rhel8 和 ee-supported-rhel8 ⾃动化执⾏环境中提供 Ansible Core 2.13。

  2. Ansible 内容集合,以前ansible和模块是在一起的,安装了anisble就会带有很多模块,好处是安装即可使用,不好的地方在于,模块的开发者发布新的模块和修复bug都受制于ansible软件是否会更新,软件和模块互相制约,现在Ansible和模块分开发展了,Ansible-core提供了Anible的核心功能,而哪些模块和插件什么的,都单独需要下载才能使用。需要注意的是,Ansible-core 自带了一个名为ansible.builtin的内容集合,这个内容集合提供了一些基本的模块。

  3. ansible-navigator,也称为自动化导航器,这个命令是当前RHAAP版本中的主要命令,用于替换以前的ansible-playbook、ansible-inventory、ansible-config 等,它通过在容器中运⾏ playbook,将运⾏ Ansible 的控制节点与运⾏它的⾃动化执⾏环境分隔开来。这样⼀来,就可以更轻松地为⾃动化代码提供完整的⼯作环境,以部署到⽣产环境中。

  4. ⾃动化执⾏环境,⾃动化执⾏环境是⼀种容器镜像,其包含 Ansible Core、Ansible 内容集合,以及运⾏ playbook 所需的任何 Python 库、可执⾏⽂件或其他依赖项。

使⽤ ansible-navigator 运⾏ playbook 时,你可以选择要用哪个容器镜像

intro-userexperience

  1. 自动化控制器,自动化控制器以前称为红帽 Ansible Tower,是红帽 Ansible 自动化平台的一个组件,提供中央控制点来运行企业自动化代码。此外还提供 Web UI 和 REST API 用于配置、运行和评估自动化作业。

  2. 自动化中心,可通过 console.redhat.com 上的公共服务访问红帽 Ansible 认证内容集合,可以将这些内容集合下载下来与 ansible-galaxy(适用于 ansible-navigator)和自动化控制器结合使用。

安装控制节点

控制节点需要做以下准备:

  1. 安装有podman

  2. 安装有Python3.8或更高版本

  3. 需要有订阅,不过在我们的课程中,订阅内容已经帮我们弄好了,执行环境什么的,都有了。

  4. 生成并分发ssh密钥给所有的被管理机

  5. Windows不能用作控制节点

安装ansible-navigator

1
[student@workstation ~]$ sudo dnf install ansible-navigator -y

看看安装好了没有,有版本号就行

1
2
[student@workstation ~]$ ansible-navigator --version
ansible-navigator 2.1.0

看看Ansible-navigator软件的配置文件,因为在这个配置文件中,控制了用哪个执行环境,输出格式等具体的容器行为

1
2
3
4
5
6
7
[student@workstation ~]$ cat .ansible-navigator.yml
---
ansible-navigator:
execution-environment:
image: utility.lab.example.com/ee-supported-rhel8:latest
pull:
policy: missing

看到了我们的容器镜像之后,我们来下载一下,ansible-navigator images命令会自动下载镜像,如果已经存在,就会展示镜像

1
2
3
[student@workstation ~]$ podman login -u admin -p redhat utility.lab.example.com
Login Succeeded!
[student@workstation ~]$ ansible-navigator images

由于我们的课程中已经提前内置了容器镜像,所以它会自动展示现有镜像,不过它交互式的显示了内容,执行了命令后显示如下内容

  1. 左侧的0 1 2 3 4是一个一个的内容,按下对应的数字即可查看详细内容,超过9之后,可以用:22的方式
  2. 最下面的一行是你可以用的操作方式,比如page up/down翻页,上下键选择,esc返回,或者输入:help查看帮助
1
2
3
4
5
6
  Image                           Tag         Execution environment                Created             Size
0│ee-supported-rhel8 latest True 2 years ago 1.45 GB



^b/PgUp page up ^f/PgDn page down ↑↓ scroll esc back [0-9] goto :help help

准备受管主机

  1. Linux 和 UNIX 受管主机需要安装有 Python 3.8 或更⾼版本

  2. 如果受管主机上启⽤了 SELinux,则要确保已安装 python3-libselinux 软件包

  3. 已经由控制节点分发了ssh密钥

  4. Windows 受管主机安装 PowerShell 3.0 或更⾼版本,安装 .NET Framework 4.0 或更⾼版本。此外,受管主机还需要远程配置 Windows PowerShell。

  5. 网络设备通常使⽤ SSH 上的 CLI、SSH 上的 XML 或 HTTP(S) 上的 API

不过这是红帽的课程,不会讨论如何管理Windows和网络设备

第二章 实施 Ansible Playbook

playbook的运行是在被管理机上的,所以需要先定义被管理机,也就是主机清单文件

构建 Ansible 清单

清单定义 Ansible 管理的主机集合。这些主机也可以分配到组中,以进⾏集中管理。组可以包含⼦组,主机也可以是多个组的成员。清单还可以设置应⽤到它所定义的主机和组的变量。

系统中,/etc/ansible/hosts这个文件是ansible的默认清单问题,如果你没有-i或者在配置文件中指定清单时,默认使用这个清单

清单格式如下:

这里面可以写IP,主机名,FQDN,甚至如果主机名有规则,还可以用[c:d]来表示多个主机

1
2
3
4
5
cat > inventory <<-'EOF'
servera
server[b:c].lab.example.com
172.25.250.13
EOF

列出主机看看效果

我们注意到,有两个组不管我们是否定义,他始终都会存在

  1. all 主机组中含有清单中明确列出的每个主机

  2. ungrouped 主机组中含有清单中明确列出、但不属于任何其他组的每个主机

-i 是手工选择主机清单

-m stdout 是以标准输出的方式显示,而不是交互式

--graph和--list是展示的样式

--graph和--list后面还可以加组的名称,仅列出组中的主机

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
[student@workstation ~]$ ansible-navigator inventory -i inventory -m stdout --graph
@all:
|--@ungrouped:
| |--172.25.250.13
| |--servera
| |--serverb.lab.example.com
| |--serverc.lab.example.com
[student@workstation ~]$ ansible-navigator inventory -i inventory -m stdout --list
{
"_meta": {
"hostvars": {}
},
"all": {
"children": [
"ungrouped"
]
},
"ungrouped": {
"hosts": [
"172.25.250.13",
"servera",
"serverb.lab.example.com",
"serverc.lab.example.com"
]
}
}

主机组的表达格式如下:

  1. 如果要表达不在任何组中的主机,就一定要写在第一个方括号上面

  2. 组名用方括号表示

  3. 一个主机可以属于多个组

  4. course:children的表达是指定义了一个course组,此组下面包括其他组中定义的主机,下面的内容必须是前面定义过的组名

  5. course:vars的表达是指为course组定义了一个变量name,值是lixiaohui

  6. course=rhcsa的表达是指为serverb这一个主机单独定义了一个course变量,值为rhcsa

注意不要定义冲突的变量,如果冲突,主机优先级高于组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
cat > inventory <<-'EOF'
servera
[rhcsa]
servera
serverb course=rhcsa
[rhce]
serverc
serverd
[course:children]
rhcsa
rhce
[course:vars]
name=lixaiohui
EOF

列出主机看看效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
[student@workstation ~]$ ansible-navigator inventory -i inventory -m stdout --graph
@all:
|--@course:
| |--@rhce:
| | |--serverc
| | |--serverd
| |--@rhcsa:
| | |--servera
| | |--serverb
|--@ungrouped:
[student@workstation ~]$ ansible-navigator inventory -i inventory -m stdout --list
{
"_meta": {
"hostvars": {
"servera": {
"name": "lixaiohui"
},
"serverb": {
"course": "rhcsa",
"name": "lixaiohui"
},
"serverc": {
"name": "lixaiohui"
},
"serverd": {
"name": "lixaiohui"
}
}
},
"all": {
"children": [
"course",
"ungrouped"
]
},
"course": {
"children": [
"rhce",
"rhcsa"
]
},
"rhce": {
"hosts": [
"serverc",
"serverd"
]
},
"rhcsa": {
"hosts": [
"servera",
"serverb"
]
}
}

管理 Ansible 配置⽂件

可以在每个 Ansible 项⽬⽬录中创建和编辑两个⽂件,⽤于配置 Ansible 的⾏为和 ansiblenavigator 命令:

  • ansible.cfg,⽤于配置多个 Ansible ⼯具的⾏为。
  • ansible-navigator.yml,⽤于更改 ansible-navigator 命令的默认选项。

在我们的课程中,主要管理ansible.cfg,在RHCA DO374的课程中,将会讲到nsible-navigator.yml

系统中,有一个优先级最低的配置文件文件:/etc/ansible/ansible.cfg

这个ansible.cfg可以放在不同的地方,以下是从高到低的优先级:

  • ANSIBLE_CONFIG 环境变量

    • 如果设置了环境变量 ANSIBLE_CONFIG,Ansible 将首先使用该路径指定的配置文件。
  • 当前目录下的 ansible.cfg 文件

    • 如果没有设置 ANSIBLE_CONFIG 环境变量,Ansible 将查找当前工作目录中的 ansible.cfg 文件。
  • 用户主目录下的 .ansible.cfg 文件

    • 如果当前目录下没有找到 ansible.cfg 文件,Ansible 将查找用户主目录中的 .ansible.cfg 文件。
  • 系统范围的 /etc/ansible/ansible.cfg 文件

    • 如果上述位置都没有找到 ansible.cfg 文件,Ansible 将使用系统范围内的 /etc/ansible/ansible.cfg 文件。

对于Ansible的基础操作,我们可以使用ansible.cfg文件中的以下部分:

  1. [defaults],⽤于设置 Ansible 操作的默认值

  2. [privilege_escalation],⽤于配置 Ansible 如何在受管主机上执⾏特权升级

下面是典型的 ansible.cfg 文件:

1
2
3
4
5
6
7
8
9
10
11
[defaults]
inventory = ./inventory # 清单在哪
remote_user = user # 用哪个用户连接被管理机
ask_pass = false # 如果用密钥认证,就是false
host_key_checking=False # 不校验被管理机器的ssh主机指纹,不然第一次连接会让你输入yes/no

[privilege_escalation]
become = true # 指定连接后是否在受管主机上⾃动切换⽤⼾
become_method = sudo # 切换的方法是什么,有sudo和su
become_user = root # 切换成谁
become_ask_pass = false # sudo的时候,要不要输入密码,需要配置被管理机器的/etc/sudoers

生成配置文件

从默认的配置文件摘取第一条命令即可在当前路径生成配置文件

1
2
3
4
[student@workstation ~]$ cat /etc/ansible/ansible.cfg
# Since Ansible 2.12 (core):
# To generate an example config file (a "disabled" one with all default settings, commented out):
# $ ansible-config init --disabled > ansible.cfg

复制粘贴这个命令,就能生成了

1
[student@workstation ~]$ ansible-config init --disabled > ansible.cfg

查询当前配置

ansible-navigator config 命令会显⽰当前 Ansible 配置,还会告诉你,具体某个设置来自哪里,是否是默认配置,当前值是什么

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[student@workstation ~]$ ansible-navigator config
Name Default Source Current
0│Action warnings True default True ▒
1│Agnostic become prompt True default True ▒
2│Allow world readable tmpfiles True default False ▒
3│Ansible connection path True default None ▒
4│Ansible cow acceptlist True default ['bud-frogs', 'bunny', 'cheese', 'daemon', 'def... ▒
5│Ansible cow path True default None ▒
6│Ansible cow selection True default default
7│Ansible force color True default False
8│Ansible nocolor True default False


^b/PgUp page up ^f/PgDn page down ↑↓ scroll esc back [0-9] goto :help help

ansible-navigator配置文件

它也有优先级,以下是从高到低:

  1. ANSIBLE_NAVIGATOR_CONFIG 环境变量

    • 如果设置了环境变量 ANSIBLE_NAVIGATOR_CONFIGansible-navigator 将首先使用该路径指定的配置文件。
  2. 当前目录下的 ansible-navigator.yml 文件

    • 如果没有设置 ANSIBLE_NAVIGATOR_CONFIG 环境变量,ansible-navigator 将查找当前工作目录中的 ansible-navigator.yml 文件。
  3. 用户主目录下的 .ansible-navigator.yml 文件

    • 如果当前目录下没有找到 ansible-navigator.yml 文件,ansible-navigator 将查找用户主目录中的 .ansible-navigator.yml 文件。
  4. 系统范围的 /etc/ansible-navigator/ansible-navigator.yml 文件

    • 如果上述位置都没有找到 ansible-navigator.yml 文件,ansible-navigator 将使用系统范围内的 /etc/ansible-navigator/ansible-navigator.yml 文件。

我们来解读一下自己现有的配置文件

1
2
3
4
5
6
7
8
9
10
[student@workstation ~]$ cat .ansible-navigator.yml
---
ansible-navigator:
execution-environment:
image: utility.lab.example.com/ee-supported-rhel8:latest
pull:
policy: missing
playbook-artifact:
enable: false
mode: stdout
  1. ansible-navigator

    • 顶层键,表示 ansible-navigator 的配置部分。
  2. execution-environment

    • 定义了执行环境的配置。
  3. image

    • 指定了执行环境的容器镜像。

    • 在此配置中,使用的是 utility.lab.example.com/ee-supported-rhel8:latest 镜像。

  4. pull

    • 配置了镜像的拉取策略。
  5. policy

    • 指定了拉取策略的具体政策。

    • 在此配置中,策略为 missing,表示只有在本地不存在镜像时才会从远程仓库拉取。

  6. playbook-artifact

    • 配置 Playbook 运行工件的记录设置。

    • enable:设置为 false 表示禁用 Playbook 的运行工件记录。

  7. mode

    • 指定运行模式。

    • stdout:设置为 stdout 表示在标准输出模式下运行,而不是交互式模式。

编写和运⾏ Playbook

Ansible Playbook 是一个用 YAML 格式编写的文件,用于定义一组自动化任务和操作步骤。文件后缀一般来说是yml或者yaml,Playbook 是 Ansible 自动化工作的核心,通过它可以描述和执行复杂的操作流程。下面是对 Ansible Playbook 的详细解释:

主要特点

  1. YAML 格式

    • Playbook 使用 YAML(Yet Another Markup Language)编写,这是一种易于阅读和编写的标记语言。
  2. 任务定义

    • Playbook 由一系列任务(Tasks)组成,每个任务定义了要在目标主机上执行的具体操作。
  3. 模块调用

    • 每个任务通常调用一个 Ansible 模块来执行具体的操作,例如安装软件包、管理服务、复制文件等。
  4. 灵活性和可扩展性

    • Playbook 支持变量、条件判断、循环等高级特性,使得自动化工作更具灵活性和可扩展性。
  5. 可重用性

    • Playbook 可以引用其他 Playbook 或角色(Roles),实现代码重用和模块化管理。

Playbook案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
---
- name: Setup web server
hosts: webservers
become: true

tasks:
- name: Install Apache
ansible.builtin.yum:
name: httpd
state: present

- name: Start Apache service
ansible.builtin.service:
name: httpd
state: started
enabled: true

- name: Setup database server
hosts: dbservers
become: true

tasks:
- name: Install MySQL
ansible.builtin.yum:
name: mysql-server
state: present

- name: Start MySQL service
ansible.builtin.service:
name: mysqld
state: started
enabled: true

示例解析

  1. 第一个 Play:Setup web server

    • hosts:指定目标主机组为 webservers

    • become:以特权用户执行任务。

    • tasks:包含两个任务:

      • Install Apache:使用 ansible.builtin.yum 模块安装 Apache 软件包。

      • Start Apache service:使用 ansible.builtin.service 模块启动并启用 Apache 服务。

  2. 第二个 Play:Setup database server

    • hosts:指定目标主机组为 dbservers

    • become:以特权用户执行任务。

    • tasks:包含两个任务:

      • Install MySQL:使用 ansible.builtin.yum 模块安装 MySQL 软件包。

      • Start MySQL service:使用 ansible.builtin.service 模块启动并启用 MySQL 服务。

Playbook 格式

  1. 缩进

    • 使用2个空格缩进,保持每级缩进的一致性,避免使用制表符(tab)。
  2. 列表

    • 列表项用破折号(-)表示,破折号后应有一个空格。
  3. 键值对

    • 键和值用冒号(:)分隔,冒号后应有一个空格。
  4. 字符串

    • 字符串包含特殊字符或需保留格式时,使用引号(单引号或双引号)。
  5. 顶层结构

    • 顶层包含 namehoststasks 等关键字,表示 Play 的结构。
  6. 任务定义

    • 每个任务包含 name 和模块调用的参数,任务名称应描述清晰。
  7. 模块调用

    • 使用命名空间调用模块,如 ansible.builtin.yum
  8. 变量定义

    • 使用 vars 关键字定义变量。
  9. 变量引用

    • 使用 {{ }} 语法引用变量。
  10. 条件判断

    • 使用 when 关键字进行条件判断。
  11. 循环

    • 使用 loop 等关键字进行循环。
  12. 文本多行处理

    • 使用 | 要保留字符串中的换⾏字符。
    • 使用 > 把换⾏字符转换成空格。

还有很多,后面我们学到了再聊,如果你觉得使用TAB习惯了,可以考虑定制一下你的vim,每次输入TAB,自动给你替换成2个空格

1
2
3
cd #回到家目录
vim .vimrc
autocmd FileType yaml setlocal ai ts=2 sw=2 et

以下是每个选项的解释:

  • autocmd FileType yaml:当文件类型为 YAML 时执行自动命令。

  • setlocal:只在当前缓冲区生效的设置。

  • ai (autoindent):自动缩进。

  • ts=2 (tabstop):设置 Tab 宽度为 2 个空格。

  • sw=2 (shiftwidth):设置缩进的宽度为 2 个空格。

  • et (expandtab):将 Tab 替换为空格。

查找模块帮助

刚才的案例中,我们用到了一些模块,那我怎么知道都有哪些模块以及都怎么使用呢?

ansible-core 软件包可提供名为 ansible.builtin 的单个 Ansible 内容集合,在这个集合中带来的模块,我们除了可以用模块的全名之外,还可以用短名称,比如ansible.builtin.copycopy

除了这个builtin集合外,其他所有安装的集合,都必须使用全名(完全限定集合名称 FQCN)

列出所有可用的模块

1
2
3
4
5
6
7
8
9
10
[student@workstation ~]$ ansible-navigator doc -l -m stdout
add_host Add a host (and alternatively a group) to the ans...
amazon.aws.aws_az_info Gather information about availability zones in AW...
amazon.aws.aws_caller_info Get information about the user and account being ...
amazon.aws.aws_s3 manage objects in S3
amazon.aws.cloudformation Create or delete an AWS CloudFormation stack
amazon.aws.cloudformation_info Obtain information about an AWS CloudFormation st...
amazon.aws.ec2 create, terminate, start or stop an instance in e...
amazon.aws.ec2_ami Create or destroy an image (AMI) in ec2
...

有很多模块,初学时,你可以挑选一些内置模块来练手,用以下方法,可以单独列出这些内置模块,默认以短名称显示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[student@workstation ~]$ ansible-doc -l
add_host Add a host (and alternatively a group) to the ansible-playbook in...
apt Manages apt-packages
apt_key Add or remove an apt key
apt_repository Add and remove APT repositories
assemble Assemble configuration files from fragments
assert Asserts given expressions are true
async_status Obtain status of asynchronous task
blockinfile Insert/update/remove a text block surrounded by marker lines
command Execute commands on targets
copy Copy files to remote locations
cron Manage cron.d and crontab entries
debconf Configure a .deb package
debug Print statements during execution
dnf Manages packages with the `dnf' package manager
...

查询copy模块的帮助

我们发现左上角告诉我们模块的全名,然后接着是模块的作用描述,后面是每个参数的含义以及可选值,最后的部分有写EXAMPLE样例,可以辅助我们写playbook,我们只需要复制走,稍微改一下就行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
[student@workstation ~]$ ansible-navigator doc ansible.builtin.copy -m stdout
> ANSIBLE.BUILTIN.COPY (/usr/lib/python3.9/site-packages/ansible/modules/copy.py)

The `copy' module copies a file from the local or remote machine to a location on the
remote machine. 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 in the `content' field will
result in unpredictable output. For Windows targets, use the
[ansible.windows.win_copy] module instead.

ADDED IN: historical

* note: This module has a corresponding action plugin.

OPTIONS (= is mandatory):

- attributes
The attributes the resulting filesystem object should have.
To get supported flags look at the man page for `chattr' on the target system.
This string should contain the attributes in the same order as the one displayed by
`lsattr'.
The `=' operator is assumed as default, otherwise `+' or `-' operators need to be
included in the string.
(Aliases: attr)[Default: (null)]
type: str
added in: version 2.3 of ansible-core


- backup
....
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

- name: Another symbolic mode example, adding some permissions and removing others
ansible.builtin.copy:
src: /srv/myfiles/foo.conf
dest: /etc/foo.conf
owner: foo
group: foo
mode: u+rw,g-wx,o-rwx

- name: Copy a new "ntp.conf" file into place, backing up the original if it differs from the copied version
ansible.builtin.copy:
src: /mine/ntp.conf
dest: /etc/ntp.conf
owner: root
group: root
mode: '0644'

好,我们按照这个EXAMPLE,来写一个,我们把workstaion这个机器上的/etc/hostname文件复制到servera上的/tmp中,以下是我们写的内容

先准备一个清单

1
echo servera > inventory

这些become字段,如果已经在ansible.cfg中申明了,就不再需要写在playbook中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
cat > copy.yml <<-'EOF'
---
- name: Copy /etc/hostname file from workstation to servera
hosts: servera
remote_user: student
become: true
become_method: sudo
become_user: root
tasks:
- name: Copy it by lixiaohui
ansible.builtin.copy:
src: /etc/hostname
dest: /tmp/hostname
owner: root
group: root
mode: '0644'
EOF

运行playbook

写好了之后,我们可以用以下方法做一个语法检查,看看格式什么的有没有什么问题

如果没什么问题,就只输出文件名称

1
2
[student@workstation ~]$ ansible-navigator run copy.yml -m stdout -i inventory --syntax-check
playbook: /home/student/copy.yml

写好了之后,我们得运行测试一下,不过直接对生产环境修改可不行,Ansible提供了一个check mode模式,就是对生产环境做模拟测试,不对机器做任何修改,只测试在这个环境中代码是否能成功运行

1
2
3
4
5
6
7
8
9
10
[student@workstation ~]$ ansible-navigator run copy.yml -m stdout -i inventory --check

PLAY [Copy /etc/hostname file from workstation to servera] *********************

TASK [Gathering Facts] *********************************************************
fatal: [servera]: FAILED! => {"msg": "Missing sudo password"}

PLAY RECAP *********************************************************************
servera : ok=0 changed=0 unreachable=0 failed=1 skipped=0 rescued=0 ignored=0
Please review the log for errors.

在上面的模拟运行中,我们发现有点不对劲,运行失败了,提示Missing sudo password

根据提示的意思,是连接没问题,在本地sudo成root的时候,需要密码,这种就需要去被管理机上做一下免密的时候NOPASSWD设置

虽然student已经加入wheel组,可以给组设置,但是我们演示一下如何单独授权,实现安全最小化原则

1
2
3
[student@workstation ~]$ ssh root@servera
[root@servera ~]# visudo
student ALL=(ALL) NOPASSWD: ALL

退到控制节点上,再模拟运行看看

这次一切成功

1
2
3
4
5
6
7
8
9
10
11
12
[student@workstation ~]$ ansible-navigator run copy.yml -m stdout -i inventory --check

PLAY [Copy /etc/hostname file from workstation to servera] *********************

TASK [Gathering Facts] *********************************************************
ok: [servera]

TASK [Copy it by lixiaohui] ****************************************************
changed: [servera]

PLAY RECAP *********************************************************************
servera : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

模拟成功后,去掉–check就是真的运行

运行命令而非模块

需要注意的是,你应该始终运行相关模块,而非命令,除非你的任务本身没有任何模块能支持,因为模块具有幂等性,而shell、command、raw等不具有此特性

ansible.builtin.command 模块⽆法访问 shell 环境变量,也⽆法执⾏输⼊/输出重定向和管道之类的 shell 操作。当需要执⾏ shell 处理时,可以使⽤ ansible.builtin.shell 模块

我们用shell模块举例创建一个用户user1

1
2
3
4
5
6
7
8
9
10
11
12
cat > user1create.yml <<-EOF
---
- name: Create user1 on servera
hosts: servera
remote_user: student
become: true
become_method: sudo
become_user: root
tasks:
- name: Create user user1
ansible.builtin.shell: "useradd user1"
EOF

第一次运行,一切正常,且全部成功

1
2
3
4
5
6
7
8
9
10
11
12
[student@workstation ~]$ ansible-navigator run user1create.yml -m stdout -i inventory

PLAY [Create user1 on servera] *************************************************

TASK [Gathering Facts] *********************************************************
ok: [servera]

TASK [Create user user1] *******************************************************
changed: [servera]

PLAY RECAP *********************************************************************
servera : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

第二次开始,就无法运行成功了,因为user1已经存在了,这就是没有幂等性的后果,如果有幂等性,playbook不会导致失败

1
2
3
4
5
6
7
8
9
10
11
12
13
[student@workstation ~]$ ansible-navigator run user1create.yml -m stdout -i inventory

PLAY [Create user1 on servera] *************************************************

TASK [Gathering Facts] *********************************************************
ok: [servera]

TASK [Create user user1] *******************************************************
fatal: [servera]: FAILED! => {"changed": true, "cmd": "useradd user1", "delta": "0:00:00.006055", "end": "2024-12-25 02:33:02.358870", "msg": "non-zero return code", "rc": 9, "start": "2024-12-25 02:33:02.352815", "stderr": "useradd: user 'user1' already exists", "stderr_lines": ["useradd: user 'user1' already exists"], "stdout": "", "stdout_lines": []}

PLAY RECAP *********************************************************************
servera : ok=1 changed=0 unreachable=0 failed=1 skipped=0 rescued=0 ignored=0
Please review the log for errors.

第三章 管理变量和事实

Ansible 变量简介

比如上面写的样例playbook,每次安装的软件和复制的文件都是写明的,每次有不同的软件或文件都要改playbook,容易出错,这个时候就可以用变量来表示,以后同样的工作,只需要更新变量文件就行。

变量命名规范

变量的名称必须以字⺟开头,并且只能含有字⺟、数字和下划线。

仔细观察以下表格

无效的变量名称有效的变量名称
web serverweb_server
remote.fileremote_file
1st filefile1
remoteserver$1remote_server_1

变量优先级

可以在多个地方定义变量,以下是优先级从低到高的排序:

  • 在清单中定义的组变量

  • 在清单或 playbook 所在目录的 group_vars 子目录中定义的组变量

  • 在清单中定义的主机变量

  • 在清单或 playbook 所在目录的 host_vars 子目录中定义的主机变量

  • 在运行时中发现的主机事实

  • vars 和 vars_files)playbook 中的 play 变量

  • 任务变量

  • 在命令行中定义的额外变量

这么看来,主机的优先级要高于组,而playbook的变量又高于主机,虽然说可以在这么多地方定义,但是到处定义变量不是个好习惯,从社会上看,大家用host_group和group_vars,以及playbook变量多些,其他的几乎没有人使用

Playbook 中的变量

vars变量

在servera服务器上安装并启动 Apache Web 服务器

这里解释一下在playbook中的变量引用:

  1. 双大括号语法 可以表达变量

  2. 大括号左右两次是否有双引号,却决于变量是否作为参数的第一部分,例如下面的name: 和大括号之间没用内容,那就需要双引号,举例来说

  • 需要双引号

    1
    name: "{{ xxx }}"
  • 不需要双引号

    1
    name: hello lixiaohui {{ xxx }}

根据变量引用规范,我们来写一个playbook

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
---
- name: Setup web server
hosts: servera

vars:
apache_package: httpd
apache_service: httpd

tasks:
- name: Install Apache
ansible.builtin.yum:
name: "{{ apache_package }}"
state: present

- name: Ensure Apache service is running
ansible.builtin.service:
name: "{{ apache_service }}"
state: started
enabled: true

解析

  1. 定义变量

    • vars 部分定义了一些变量,例如 apache_packageapache_service。这些变量将用于后续的任务中,以提高 Playbook 的可读性和可维护性。
  2. 安装 Apache

    • 使用 yum 模块安装 Apache。这里引用了 apache_package 变量,以确保包名称可配置。
  3. 确保 Apache 服务正在运行

    • 使用 service 模块启动并启用 Apache 服务。这里引用了 apache_service 变量。

运行一下并检查看看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[student@workstation ~]$ ansible-navigator run apache.yml -m stdout

PLAY [Setup web server] ********************************************************

TASK [Gathering Facts] *********************************************************
ok: [servera]

TASK [Install Apache] **********************************************************
changed: [servera]

TASK [Ensure Apache service is running] ****************************************
changed: [servera]

PLAY RECAP *********************************************************************
servera : ok=3 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

检查看看是否安装,本次用的是AD-Hoc方法临时查看

1
2
3
4
5
6
7
8
9
[student@workstation ~]$ ansible servera -m shell -a 'rpm -q httpd'
servera | CHANGED | rc=0 >>
httpd-2.4.51-7.el9_0.x86_64
[student@workstation ~]$ ansible servera -m shell -a 'systemctl is-active httpd'
servera | CHANGED | rc=0 >>
active
[student@workstation ~]$ ansible servera -m shell -a 'systemctl is-enabled httpd'
servera | CHANGED | rc=0 >>
enabled

vars_files变量

我们将使用外部变量文件 (vars_files) 来管理配置变量,从而实现更灵活和可维护的Playbook。

  1. 先把变量文件建出来
1
2
3
4
[student@workstation ~]$ mkdir vars
[student@workstation ~]$ vim vars/apache_vars.yml
apache_package: httpd
apache_service: httpd
  1. 根据需求,写一个playbook,并引用变量,我们这次在serverb上运行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
---
- name: Setup web server
hosts: serverb
become: true

vars_files:
- vars/apache_vars.yml

tasks:
- name: Install Apache
ansible.builtin.yum:
name: "{{ apache_package }}"
state: present

- name: Ensure Apache service is running
ansible.builtin.service:
name: "{{ apache_service }}"
state: started
enabled: true

解析

  1. 变量文件

    • vars/apache_vars.yml 文件中定义了一些变量,例如 apache_packageapache_service
  2. Playbook 文件

    • apache.yml 中使用 vars_files 引用外部变量文件 vars/apache_vars.yml

    • tasks 部分包含安装 Apache 包和确保 Apache 服务正在运行的任务。

  3. 运行并检查结果

看上去运行一切成功

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[student@workstation ~]$ ansible-navigator run apache.yml -m stdout

PLAY [Setup web server] ********************************************************

TASK [Gathering Facts] *********************************************************
ok: [serverb]

TASK [Install Apache] **********************************************************
changed: [serverb]

TASK [Ensure Apache service is running] ****************************************
changed: [serverb]

PLAY RECAP *********************************************************************
serverb : ok=3 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

看看服务怎么样了

1
2
3
4
5
6
7
8
9
[student@workstation ~]$ ansible serverb -m shell -a 'rpm -q httpd'
serverb | CHANGED | rc=0 >>
httpd-2.4.51-7.el9_0.x86_64
[student@workstation ~]$ ansible serverb -m shell -a 'systemctl is-active httpd'
serverb | CHANGED | rc=0 >>
active
[student@workstation ~]$ ansible serverb -m shell -a 'systemctl is-enabled httpd'
serverb | CHANGED | rc=0 >>
enabled

主机变量

主机变量又分为清单中定义的主机变量和host_vars变量

主机变量优先于组变量,但 playbook 中定义的变量的优先级⽐这两者更⾼。

清单中的主机变量

不太推荐定义在清单中,不好统一集中管理

首先定制一下清单文件

1
2
3
cat > hostinventory <<-EOF
servera my_variable="Hello, Ansible!"
EOF

好的,我们来写一个playbook,看看是否能识别到这个变量

1
2
3
4
5
6
7
8
9
---
- name: Display inventory variable
hosts: servera
become: true

tasks:
- name: Show variable from inventory
ansible.builtin.debug:
msg: "The value of my_variable is: {{ my_variable }}"

运行看看结果

结果显示,使用默认清单是不行的,必须用我们定制的清单才可以,说明我们在清单中定义的变量已经生效

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
[student@workstation ~]$ ansible-navigator run host.yml -m stdout

PLAY [Display inventory variable] **********************************************

TASK [Gathering Facts] *********************************************************
ok: [servera]

TASK [Show variable from inventory] ********************************************
fatal: [servera]: FAILED! => {"msg": "The task includes an option with an undefined variable. The error was: 'my_variable' is undefined\n\nThe error appears to be in '/home/student/host.yml': line 7, column 7, but may\nbe elsewhere in the file depending on the exact syntax problem.\n\nThe offending line appears to be:\n\n tasks:\n - name: Show variable from inventory\n ^ here\n"}

PLAY RECAP *********************************************************************
servera : ok=1 changed=0 unreachable=0 failed=1 skipped=0 rescued=0 ignored=0
Please review the log for errors.
[student@workstation ~]$
[student@workstation ~]$ ansible-navigator run host.yml -m stdout -i hostinventory

PLAY [Display inventory variable] **********************************************

TASK [Gathering Facts] *********************************************************
ok: [servera]

TASK [Show variable from inventory] ********************************************
ok: [servera] => {
"msg": "The value of my_variable is: Hello, Ansible!"
}

PLAY RECAP *********************************************************************
servera : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

host_vars文件夹变量

在 Ansible 中,host_vars 文件夹用于为特定的主机定义变量。这些变量只适用于定义它们的特定主机,可以用来对每个主机进行细粒度的配置管理。

  • host_vars 文件夹通常位于你的 Ansible 项目根目录下,或者位于你正在使用的角色目录中。

  • host_vars 文件夹中,每个主机都有一个对应的 YAML 文件,文件名与主机名相同。

  • 每个 YAML 文件中可以定义针对该主机的变量。这些变量在该主机执行 Playbook 时会被引用。

我们先来确定一下主机清单,我们现在有两个主机

1
2
3
4
cat > hostinventory <<-EOF
servera
serverb
EOF

根据刚才说的,如果要给他们定义变量,要在host_vars目录中,新建和每个主机同名的文件,然后把变量放入到文件中,我们来制作变量

servera的变量:

1
2
3
[student@workstation ~]$ mkdir host_vars
[student@workstation ~]$ vim host_vars/servera
custom_var: "Hello from servera!"

serverb的变量:

1
2
[student@workstation ~]$ vim host_vars/serverb
custom_var: "Hello from serverb!"

这样定义好了变量之后,这两个机器上都有一个同名的变量了,但是值是不一样的,所以可以处理不同的事

我们写一个debug看看能不能识别到变量

1
2
3
4
5
6
7
8
9
---
- name: Display custom variable
hosts: all
become: true

tasks:
- name: Show custom variable from host_vars
ansible.builtin.debug:
msg: "The value of custom_var is: {{ custom_var }}"

运行后,果然看到变量已经准确的绑定到了不同的主机上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[student@workstation ~]$ ansible-navigator run host.yml -m stdout -i hostinventory

PLAY [Display custom variable] *************************************************

TASK [Gathering Facts] *********************************************************
ok: [serverb]
ok: [servera]

TASK [Show custom variable from host_vars] *************************************
ok: [servera] => {
"msg": "The value of custom_var is: Hello from servera!"
}
ok: [serverb] => {
"msg": "The value of custom_var is: Hello from serverb!"
}

PLAY RECAP *********************************************************************
servera : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
serverb : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

组变量

变量不要和主机的冲突,如果冲突,主机变量优先级更高

清单中的组变量

重新定制一个新的清单

我们单独给webservers组定义了变量,而没有给主机显式的定义

1
2
3
4
5
6
7
8
cat > groupinventory <<-EOF
[webservers]
servera
serverb

[webservers:vars]
my_group_variable="Hello, Web Servers!"
EOF

我们来做一个在webservers组上运行的playbook

1
2
3
4
5
6
7
8
9
---
- name: Display group variable
hosts: webservers
become: true

tasks:
- name: Show group variable from inventory
ansible.builtin.debug:
msg: "The value of my_group_variable is: {{ my_group_variable }}"

运行看看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[student@workstation ~]$ ansible-navigator run group.yml -m stdout -i groupinventory

PLAY [Display group variable] **************************************************

TASK [Gathering Facts] *********************************************************
ok: [servera]
ok: [serverb]

TASK [Show group variable from inventory] **************************************
ok: [servera] => {
"msg": "The value of my_group_variable is: Hello, Web Servers!"
}
ok: [serverb] => {
"msg": "The value of my_group_variable is: Hello, Web Servers!"
}

PLAY RECAP *********************************************************************
servera : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
serverb : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

group_vars 文件夹变量

这个和host_vars是同理的,只是创建的文件要和清单中的组同名

我们先来设计一个清单文件,这个文件要包含不同的组,这样才能看到效果

1
2
3
4
5
6
7
8
cat > groupinventory <<-EOF
[webservers]
servera
serverb
[dbservers]
serverc
serverd
EOF

设计一个变量文件夹,注意新进的文件要和上面方括号的字符串相同

1
2
3
4
5
[student@workstation ~]$ mkdir group_vars
[student@workstation ~]$ vim group_vars/webservers
custom_var: "Hello from webservers group!"
[student@workstation ~]$ vim group_vars/dbservers
custom_var: "Hello from dbservers group!"

变量也准备好了,写一个playbook看看

1
2
3
4
5
6
7
8
9
---
- name: Display group variable
hosts: all
become: true

tasks:
- name: Show group variable from group_vars
ansible.builtin.debug:
msg: "The value of custom_var is: {{ custom_var }}"

运行之前,你需要先删除host_vars文件夹才能看到效果,因为主机的变量优先级高于组变量

以下是不删除和删除的对比

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
[student@workstation ~]$ ansible-navigator run group.yml -m stdout -i groupinventory

PLAY [Display group variable] **************************************************

TASK [Gathering Facts] *********************************************************
ok: [servera]
ok: [serverd]
ok: [serverc]
ok: [serverb]

TASK [Show group variable from group_vars] *************************************
ok: [servera] => {
"msg": "The value of custom_var is: Hello from servera!"
}
ok: [serverb] => {
"msg": "The value of custom_var is: Hello from serverb!"
}
ok: [serverc] => {
"msg": "The value of custom_var is: Hello from dbservers group!"
}
ok: [serverd] => {
"msg": "The value of custom_var is: Hello from dbservers group!"
}

PLAY RECAP *********************************************************************
servera : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
serverb : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
serverc : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
serverd : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0


[student@workstation ~]$ rm -rf host_vars/
[student@workstation ~]$ ansible-navigator run group.yml -m stdout -i groupinventory

PLAY [Display group variable] **************************************************

TASK [Gathering Facts] *********************************************************
ok: [serverd]
ok: [servera]
ok: [serverc]
ok: [serverb]

TASK [Show group variable from group_vars] *************************************
ok: [servera] => {
"msg": "The value of custom_var is: Hello from webservers group!"
}
ok: [serverb] => {
"msg": "The value of custom_var is: Hello from webservers group!"
}
ok: [serverc] => {
"msg": "The value of custom_var is: Hello from dbservers group!"
}
ok: [serverd] => {
"msg": "The value of custom_var is: Hello from dbservers group!"
}

PLAY RECAP *********************************************************************
servera : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
serverb : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
serverc : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
serverd : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

命令行变量

在命令⾏上设置的变量称为额外变量,这个变量可以覆盖清单变量、playbook变量

我们还用刚才的yaml运行看看

1
2
3
4
5
6
7
8
9
---
- name: Display group variable
hosts: all
become: true

tasks:
- name: Show group variable from group_vars
ansible.builtin.debug:
msg: "The value of custom_var is: {{ custom_var }}"

我们运行的时候,主动用-e参数给这个变量赋值,而不是让他采用group_vars变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
[student@workstation ~]$ ansible-navigator run group.yml -m stdout -i groupinventory -e custom_var=lixiaohui

PLAY [Display group variable] **************************************************

TASK [Gathering Facts] *********************************************************
ok: [serverb]
ok: [servera]
ok: [serverc]
ok: [serverd]

TASK [Show group variable from group_vars] *************************************
ok: [servera] => {
"msg": "The value of custom_var is: lixiaohui"
}
ok: [serverb] => {
"msg": "The value of custom_var is: lixiaohui"
}
ok: [serverc] => {
"msg": "The value of custom_var is: lixiaohui"
}
ok: [serverd] => {
"msg": "The value of custom_var is: lixiaohui"
}

PLAY RECAP *********************************************************************
servera : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
serverb : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
serverc : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
serverd : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

将字典⽤作变量

这部分的内容是表达了变量层级的关系和引用规范,将普通变量改为字典的好处:

  • 结构化数据
    使用字典可以更好地组织和结构化相关的数据。例如,上述例子中,用户的名字、姓氏和主目录属于同一个实体(用户),使用字典将这些属性组织在一起,使数据看起来更加有条理。

  • 减少变量数量
    使用字典可以减少顶层变量的数量,避免变量名冲突,并提高可读性。例如,将多个 user1_*user2_* 变量合并成一个 users 字典,可以显著减少变量的数量。

  • 易于扩展
    使用字典可以方便地添加或删除数据,而无需修改多个变量。例如,如果需要为用户添加一个新的属性(如电子邮件地址),只需在字典中添加一行,而不需要定义新的变量。

  • 易于迭代
    字典非常适合在循环中使用。例如,可以轻松地遍历 users 字典来执行批量操作,而不需要手动列出每个用户的变量。

假设我们原来有一些变量如下:

1
2
3
4
5
6
user1_first_name: Bob
user1_last_name: Jones
user1_home_dir: /users/bjones
user2_first_name: Anne
user2_last_name: Cook
user2_home_dir: /users/acook

以上的变量可以改成成为users的字典:

1
2
3
4
5
6
7
8
9
users:
bjones:
first_name: Bob
last_name: Jones
home_dir: /users/bjones
acook:
first_name: Anne
last_name: Cook
home_dir: /users/acook

变量改完之后,我们可以用下面的方法引用:

  1. users.acook.first_name

  2. [‘users’][‘acook’][‘first_name’]

从书写来看,用第一种更好写,如果键名称与 Python ⽅法或属性的名称(如 discard、copy、add 等)相同,点表⽰法可能会造成问题。使⽤⽅括号表⽰法有助于避免冲突和错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
---
- name: Create user acook
hosts: servera
become: true

vars:
users:
bjones:
first_name: Bob
last_name: Jones
home_dir: /users/bjones
acook:
first_name: Anne
last_name: Cook
home_dir: /users/acook

tasks:
- name: Create user acook
ansible.builtin.user:
name: "{{ users.acook.first_name }}"
state: present
home: "{{ users.acook.home_dir }}"
shell: /bin/bash

运行并检查结果

1
2
3
4
5
6
7
8
9
10
11
12
[student@workstation ~]$ ansible-navigator run user.yml -m stdout

PLAY [Create user acook] *******************************************************

TASK [Gathering Facts] *********************************************************
ok: [servera]

TASK [Create user acook] *******************************************************
changed: [servera]

PLAY RECAP *********************************************************************
servera : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

成功创建了此用户

1
2
3
[student@workstation ~]$ ansible servera -m shell -a 'id Anne'
servera | CHANGED | rc=0 >>
uid=1002(Anne) gid=1002(Anne) groups=1002(Anne)

注册变量

可使⽤ register 语句来捕获命令的输出或有关模块执⾏的其他信息。输出会保存到⼀个变量中,稍后在 playbook 中可⽤于调试⽤途或者达成其他⽬的

  • 安装 Apache 包:使用 ansible.builtin.dnf 模块安装 Apache,并使用 register 关键字将任务的输出保存到变量 apache_install_status 中。

  • 确保 Apache 服务正在运行:使用 ansible.builtin.service 模块启动并启用 Apache 服务。

  • 显示 Apache 安装状态:使用 ansible.builtin.debug 模块显示 apache_install_status 变量的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
---
- name: Install and check Apache service
hosts: serverc
become: true

tasks:
- name: Install Apache package
ansible.builtin.dnf:
name: httpd
state: present
register: apache_install_status

- name: Ensure Apache service is running
ansible.builtin.service:
name: httpd
state: started

- name: Display Apache installation status
ansible.builtin.debug:
msg: "Apache installation status: {{ apache_install_status }}"

运行看看,正常来说,debug会输出在register中保存的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[student@workstation ~]$ ansible-navigator run register.yml -m stdout

PLAY [Install and check Apache service] ****************************************

TASK [Gathering Facts] *********************************************************
ok: [serverc]

TASK [Install Apache package] **************************************************
changed: [serverc]

TASK [Ensure Apache service is running] ****************************************
changed: [serverc]

TASK [Display Apache installation status] **************************************
ok: [serverc] => {
"msg": "Apache installation status: {'msg': '', 'changed': True, 'results': ['Installed: apr-util-1.6.1-20.el9.x86_64', 'Installed: redhat-logos-httpd-90.4-1.el9.noarch', 'Installed: apr-util-bdb-1.6.1-20.el9.x86_64', 'Installed: mod_http2-1.15.19-2.el9.x86_64', 'Installed: apr-util-openssl-1.6.1-20.el9.x86_64', 'Installed: mod_lua-2.4.51-7.el9_0.x86_64', 'Installed: httpd-2.4.51-7.el9_0.x86_64', 'Installed: httpd-filesystem-2.4.51-7.el9_0.noarch', 'Installed: httpd-tools-2.4.51-7.el9_0.x86_64', 'Installed: apr-1.7.0-11.el9.x86_64'], 'rc': 0, 'failed': False}"
}

PLAY RECAP *********************************************************************
serverc : ok=4 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

管理机密

使⽤ Ansible Vault 加密敏感变量,Ansible Vault 是 Ansible 提供的一种工具,用于保护和加密敏感数据,例如密码、密钥和其他机密信息。它允许你在代码库中安全地存储这些信息,而无需担心未经授权的访问。以下是 Ansible Vault 的一些关键功能和使用方法:

  • 加密和解密
    Ansible Vault 可以加密和解密整个文件或文件中的特定变量,以确保敏感信息的安全。

  • 密码保护
    通过使用密码来加密和解密文件,只有持有正确密码的人才能访问这些敏感数据。

  • 与 Playbook 兼容
    Ansible Vault 与 Ansible Playbook 完全兼容,允许在 Playbook 中引用加密的变量和文件。

创建加密文件

创建的文件默认会提示加密,cat是看不到的

这将提示你输入一个密码,然后你可以在编辑器中输入要加密的内容。

1
2
3
4
5
6
7
8
9
10
11
[student@workstation ~]$ ansible-vault create secret.yml
New Vault password:
Confirm New Vault password:

[student@workstation ~]$ cat secret.yml
$ANSIBLE_VAULT;1.1;AES256
35333436313332613163323439656130633363306561363763326134306561643862316437366232
3265386163646632653733326332666262356530636430640a303763373038343734326632336239
32386139323534326635393232396366326438306363653930326439646630303438616632363536
3835646631623835660a633933613534626466653861646535336330633233623739633066613734
39633431623633656566653632326436663038613039346139373130663264353366

查看加密文件

看不见可不行,我们来看看里面的内容

1
2
3
[student@workstation ~]$ ansible-vault view secret.yml
Vault password:
lixiaohui vault test

编辑加密文件

1
2
[student@workstation ~]$ ansible-vault edit secret.yml
Vault password:

加密现有文件

先拷贝一个过来,再加密

1
2
3
4
5
6
7
8
9
10
11
12
[student@workstation ~]$ cp /etc/fstab .
[student@workstation ~]$ ansible-vault encrypt fstab
New Vault password:
Confirm New Vault password:
Encryption successful
[student@workstation ~]$ cat fstab
$ANSIBLE_VAULT;1.1;AES256
31343032636665306261303337343133303232373164636233636338303133343539643333363436
6661333635313633396462353635623066363565323636650a393433316135613763613137656130
39333564656362613530336464303034366634643035336636313339393234383834393865353038
3663663266313166310a363066303664636330653238303630373361336261336434313032653666
65373261623339316132333766393463346436646234303834353436346665306232653733396464

重置密码

1
2
3
4
5
[student@workstation ~]$ ansible-vault rekey fstab
Vault password:
New Vault password:
Confirm New Vault password:
Rekey successful

解密文件

1
2
3
4
5
6
7
8
9
10
11
12
[student@workstation ~]$ cat fstab
$ANSIBLE_VAULT;1.1;AES256
32653239336436353834326634303465616530356561393863363566613936323938623565636136
3562366338643764363235383961623630616234363066310a643933353863653864313262373038
...
[student@workstation ~]$ ansible-vault decrypt fstab
Vault password:
Decryption successful
[student@workstation ~]$ cat fstab
UUID=5e75a2b9-1367-4cc8-bb38-4d6abc3964b8 /boot xfs defaults 0 0
UUID=fb535add-9799-4a27-b8bc-e8259f39a767 / xfs defaults 0 0
UUID=7B77-95E7 /boot/efi vfat defaults,uid=0,gid=0,umask=077,shortname=winnt 0 2

自动输入密码

我们发现,不管是创建、查看、编辑、加解密,都需要输入密码,太长了,有没有办法能非交互式的完成工作呢,那就是提前准备好密码文件,每次都指定一下密码文件

先制作一个私密的密码文件

1
2
[student@workstation ~]$ echo woshimima > passfile
[student@workstation ~]$ chmod 600 passfile

现在试试自动输入密码

1
[student@workstation ~]$ ansible-vault create newvaultfile --vault-password-file /home/student/passfile

现在就不再需要输入密码了,但是每次输入这么长也挺麻烦的,这个参数可以放到配置文件中,就会自动引用了

1
2
3
4
5
6
[student@workstation ~]$ cat ansible.cfg
[defaults]
...
vault_password_file=/home/student/passfile

[student@workstation ~]$ ansible-vault create passwordfiletest

现在虽然解决了自动输入密码且不需要你输入密码文件,但是有一个新的问题出现了,重置密码的时候,旧密码和新密码都会自动输入,这就无法重置密码了

这种情况又细分为新密码是字符串还是密码文件

密码是临时的字符串,不打算作为长期的密码文件

  1. 临时注释ansible.cfg中的密码文件
  2. 完成rekey
  3. 恢复注释

新的密码是密码文件,用于替换旧密码文件

1
2
3
[student@workstation ~]$ echo newpass > newpassfile
[student@workstation ~]$ ansible-vault rekey fstab --vault-password-file /home/student/passfile --new-vault-password-file /home/student/newpassfile
Rekey successful

重置后,别忘了在配置文件中,改为新的密码文件

vault与playbook

  1. 如果playbook或变量是加密的,且配置文件中有密码文件,会自动解密运行

  2. 如果playbook或变量是加密的,且配置文件中没有密码文件,会运行识别,提示无法解密

  3. 如果playbook或变量是加密的,且配置文件中没有密码文件,只提供--vault-id @prompt会卡着不动,需要同时加上--playbook-artifact-enable false,才会提示你输入密码

管理fact

Ansible Facts 是 Ansible 自动收集的远程主机的系统信息,这些信息通过 ansible.builtin.setup 模块收集。默认情况下,每次运行 Playbook 时,Ansible 都会自动收集这些 Facts,并将其存储在变量中。这些变量可以在 Playbook 和模板中使用。

获取facts

我们自己也可以导出servera的fact,看看都会收集远程主机的哪些信息供我们稍后使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
[student@workstation ~]$ ansible servera -m ansible.builtin.setup > fact.txt
[student@workstation ~]$ head -n 20 fact.txt
servera | SUCCESS => {
"ansible_facts": {
"ansible_all_ipv4_addresses": [
"172.25.250.10"
],
"ansible_all_ipv6_addresses": [
"fe80::a973:f7b3:7b00:e503"
],
"ansible_apparmor": {
"status": "disabled"
},
"ansible_architecture": "x86_64",
"ansible_bios_date": "04/01/2014",
"ansible_bios_vendor": "SeaBIOS",
"ansible_bios_version": "1.16.1-1.el9",
"ansible_board_asset_tag": "NA",
"ansible_board_name": "RHEL",
"ansible_board_serial": "NA",
"ansible_board_vendor": "Red Hat",
"ansible_board_version": "RHEL-9.2.0 PC (Q35 + ICH9, 2009)",

facts的新旧语法区别

在旧语法中(ansible2.5以前),Ansible Facts 的引用是通过直接访问变量的名称实现的,比如 ansible_ 前缀。

在 Ansible 2.5 之前,事实总是作为以 ansible_ 字符串为前缀的单个变量注⼊,⽽不是作为ansible_facts 变量的⼀部分。例如,ansible_facts[‘distribution’] 事实称为ansible_distribution。

1
2
3
4
5
6
7
8
9
10
11
12
---
- name: Example playbook using old facts syntax
hosts: all

tasks:
- name: Display operating system
ansible.builtin.debug:
msg: "The operating system is: {{ ansible_os_family }}"

- name: Display total memory
ansible.builtin.debug:
msg: "The total memory is: {{ ansible_memtotal_mb }} MB"

在新语法中,Ansible 引入了 ansible_facts 字典,以提供更加一致和模块化的引用方式。

以下方括号也可以用.连接起来

1
2
3
4
5
6
7
8
9
10
11
12
---
- name: Example playbook using new facts syntax
hosts: all

tasks:
- name: Display operating system
ansible.builtin.debug:
msg: "The operating system is: {{ ansible_facts['os_family'] }}"

- name: Display total memory
ansible.builtin.debug:
msg: "The total memory is: {{ ansible_facts['memtotal_mb'] }} MB"

关键区别

  1. 引用方式

    • 旧语法:直接引用 Fact 变量名称,使用 ansible_ 前缀。

    • 新语法:使用 ansible_facts 字典引用 Fact 变量,以提供更加一致和模块化的方式。

  2. 一致性

    • 旧语法:不同类型的 Fact 变量可能在引用方式上不一致。

    • 新语法:所有的 Fact 变量都通过 ansible_facts 字典引用,提供了一致的引用方式。

  3. 可读性

    • 旧语法:在一些复杂的 Playbook 中,直接引用可能会使代码可读性降低。

    • 新语法:使用 ansible_facts 字典引用 Fact 变量,提高了代码的可读性和易维护性。

关闭fact收集

在 Ansible 中,如果你不需要收集 Facts(主机信息),可以在 Playbook 中禁用它们,这样可以提高执行效率。要关闭 Facts 收集,可以在 Playbook 的顶部使用 gather_facts: no

1
2
3
4
5
6
7
8
9
---
- name: Playbook without gathering facts
hosts: all
gather_facts: no

tasks:
- name: Display a message
ansible.builtin.debug:
msg: "This playbook does not gather facts."

创建⾃定义fact

自定义 Facts 允许你在 Ansible 中定义自己的主机信息,以便在 Playbook 中使用。默认情况下,ansible.builtin.setup 模块会从每个受管主机的 /etc/ansible/facts.d 目录中加载自定义 Facts。这些文件和脚本必须以 .fact 结尾,才能被 Ansible 识别和使用,收集到的自定义facts,将保存至ansible_facts.ansible_local

先创建一个custom.fact自定义变量列表

1
2
3
4
cat > custom.fact <<-EOF
[general]
hello=Hello from custom fact file!
EOF

我们把这个custom.fact放到所有机器上去,不过需要注意的是,只有play运行的时候才会收集fact,所以第一个play用于准备,第二个用于重新收集

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
---
- name: Create custom facts file
hosts: all
become: true

tasks:
- name: Create facts directory
ansible.builtin.file:
path: /etc/ansible/facts.d
state: directory
mode: '0755'

- name: Copy custom facts file from current path
ansible.builtin.copy:
src: custom.fact
dest: /etc/ansible/facts.d/custom.fact
mode: '0644'

- name: Use custom facts example
hosts: all
become: true

tasks:
- name: Gather facts
ansible.builtin.setup:

- name: Display custom fact from file
ansible.builtin.debug:
msg: "Custom fact from file is: {{ ansible_facts.ansible_local.custom.general.hello }}"

运行看看结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
[student@workstation ~]$ ansible-navigator run customfact.yml -m stdout

PLAY [Create custom facts file] ************************************************

TASK [Gathering Facts] *********************************************************
ok: [servera]
ok: [serverb]
ok: [serverc]
ok: [serverd]

TASK [Create facts directory] **************************************************
ok: [serverb]
ok: [serverd]
ok: [servera]
ok: [serverc]

TASK [Copy custom facts file from current path] ********************************
changed: [serverb]
changed: [serverd]
changed: [servera]
changed: [serverc]

PLAY [Use custom facts example] ************************************************

TASK [Gathering Facts] *********************************************************
ok: [servera]
ok: [serverc]
ok: [serverd]
ok: [serverb]

TASK [Gather facts] ************************************************************
ok: [servera]
ok: [serverb]
ok: [serverd]
ok: [serverc]

TASK [Display custom fact from file] *******************************************
ok: [servera] => {
"msg": "Custom fact from file is: Hello from custom fact file!"
}
ok: [serverb] => {
"msg": "Custom fact from file is: Hello from custom fact file!"
}
ok: [serverc] => {
"msg": "Custom fact from file is: Hello from custom fact file!"
}
ok: [serverd] => {
"msg": "Custom fact from file is: Hello from custom fact file!"
}

PLAY RECAP *********************************************************************
servera : ok=6 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
serverb : ok=6 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
serverc : ok=6 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
serverd : ok=6 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

使⽤魔法变量

在 Ansible 中,魔法变量(Magic Variables)是一些预定义的特殊变量,用于提供有关运行环境和主机的信息。这些变量可以在 Playbook 和角色中使用,以实现更灵活和动态的配置。

我们的课程中提到了4个魔法变量

  1. hostvars

hostvars 是一个字典,包含所有受管主机的变量和 Facts。可以通过主机名来访问特定主机的变量。

在这个例子中,我们使用 hostvars 获取主机 servera 的默认 IPv4 地址。

1
2
3
- name: Show a variable from another host
ansible.builtin.debug:
msg: "The IP address of servera is {{ hostvars['servera']['ansible_facts']['default_ipv4']['address'] }}"
  1. group_names

group_names 是一个列表,包含当前主机所属的所有组的名称。

在这个例子中,我们显示当前主机所属的所有组。

1
2
3
- name: Show groups this host belongs to
ansible.builtin.debug:
msg: "This host belongs to groups: {{ group_names }}"
  1. groups

groups 是一个字典,包含所有组及其成员主机。可以通过组名来获取该组中的所有主机。

在这个例子中,我们显示 webservers 组中的所有主机。

1
2
3
- name: Show all hosts in the webservers group
ansible.builtin.debug:
msg: "All hosts in the webservers group: {{ groups['webservers'] }}"
  1. inventory_hostname

inventory_hostname 是当前正在执行任务的主机在清单中的名称。

在这个例子中,我们显示当前主机在清单中的名称。

1
2
3
- name: Show the inventory hostname
ansible.builtin.debug:
msg: "The inventory hostname is {{ inventory_hostname }}"
  • hostvars: 访问所有受管主机的变量和 Facts。

  • group_names: 当前主机所属的所有组的名称。

  • groups: 所有组及其成员主机。

  • inventory_hostname: 当前主机在清单中的名称。

第四章 实施任务控制

编写循环和条件任务

loop循环

在 Ansible 中,loop 是一种用来重复执行任务的机制。它允许你在同一个任务中对多个项目进行操作,而无需重复编写任务块。loop 提供了简洁、可读和灵活的方式来处理多个值或项目

多个item可以在loop关键字中用列表来表示,在引用每个loop中的内容时,直接引用{{ item }}变量即可

安装多个软件,你就不需要写多个task,可以用loop来循环多个item给到单个task

1
2
3
4
5
6
7
8
9
10
11
12
13
---
- name: Install multiple packages
hosts: all
become: true

tasks:
- name: Install packages
ansible.builtin.dnf:
name: "{{ item }}"
state: present
loop:
- httpd
- ftp

运行看看结果先

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
[student@workstation ~]$ ansible-navigator run loop.yml -m stdout

PLAY [Install multiple packages] ***********************************************

TASK [Gathering Facts] *********************************************************
ok: [serverd]
ok: [serverb]
ok: [serverc]
ok: [servera]

TASK [Install packages] ********************************************************
ok: [servera] => (item=httpd)
ok: [serverb] => (item=httpd)
ok: [serverc] => (item=httpd)
changed: [serverb] => (item=ftp)
changed: [servera] => (item=ftp)
changed: [serverd] => (item=httpd)
changed: [serverc] => (item=ftp)
changed: [serverd] => (item=ftp)

PLAY RECAP *********************************************************************
servera : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
serverb : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
serverc : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
serverd : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

loop循环在任何需要表达多个项目时,都可以使用:

  1. 创建用户

  2. 安装软件

  3. 复制文件

等等任何需要多个项目时。。。

when条件判断

在 Ansible 中,when 是一个用于条件判断的关键字,它允许你根据特定条件有选择地执行任务。when 语句使 Playbook 更加灵活和动态,可以根据运行时环境的不同情况执行不同的操作。

when 关键字用于在任务、角色或 Playbook 执行时引入条件判断。如果 when 条件表达式的结果为 true,则会执行相应的任务;如果为 false,则跳过该任务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
---
- name: Conditional tasks based on distribution
hosts: all

tasks:
- name: Install Apache on Ubuntu
ansible.builtin.apt:
name: apache2
state: present
when: ansible_facts['distribution'] == "Ubuntu"

- name: Install Apache on RedHat
ansible.builtin.dnf:
name: mysql
state: present
when: ansible_facts['distribution'] == "RedHat"

运行看看结果

可以看到,不满足条件的任务都跳过了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
[student@workstation ~]$ ansible-navigator run when.yml -m stdout

PLAY [Conditional tasks based on distribution] *********************************

TASK [Gathering Facts] *********************************************************
ok: [serverc]
ok: [serverb]
ok: [serverd]
ok: [servera]

TASK [Install Apache on Ubuntu] ************************************************
skipping: [servera]
skipping: [serverb]
skipping: [serverc]
skipping: [serverd]

TASK [Install Apache on RedHat] ************************************************
ok: [serverc]
ok: [serverd]
changed: [servera]
changed: [serverb]

PLAY RECAP *********************************************************************
servera : ok=2 changed=1 unreachable=0 failed=0 skipped=1 rescued=0 ignored=0
serverb : ok=2 changed=1 unreachable=0 failed=0 skipped=1 rescued=0 ignored=0
serverc : ok=2 changed=0 unreachable=0 failed=0 skipped=1 rescued=0 ignored=0
serverd : ok=2 changed=0 unreachable=0 failed=0 skipped=1 rescued=0 ignored=0

when 条件支持的运算符:

运算符说明示例
==等于ansible_facts['distribution'] == "Ubuntu"
!=不等于ansible_facts['distribution'] != "CentOS"
<小于ansible_facts['memtotal_mb'] < 1024
<=小于等于ansible_facts['memtotal_mb'] <= 2048
>大于ansible_facts['memtotal_mb'] > 1024
>=大于等于ansible_facts['memtotal_mb'] >= 2048
and逻辑与(所有条件都为真时为真)ansible_facts['distribution'] == "Ubuntu" and ansible_facts['memtotal_mb'] > 1024
or逻辑或(任一条件为真时为真)ansible_facts['distribution'] == "Ubuntu" or ansible_facts['distribution'] == "CentOS"
not逻辑非(条件为假时为真)not ansible_facts['distribution'] == "Ubuntu"
in成员关系(左操作数在右操作数中)"Ubuntu" in ansible_facts['distributions']
not in非成员关系(左操作数不在右操作数中)"Windows" not in ansible_facts['distributions']

通过使用这些运算符,你可以构建复杂的条件逻辑,以根据不同的条件执行任务。

举个例子来理解一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
---
- name: Check operating system and perform tasks
hosts: all
become: true

vars:
supported_os:
- RedHat
- CentOS
- Fedora

tasks:
- name: Gather facts
ansible.builtin.setup:

- name: Check if OS is RedHat based
ansible.builtin.debug:
msg: "The operating system is RedHat based."
when: ansible_facts['distribution'] in supported_os

- name: Check if OS is not RedHat based
ansible.builtin.debug:
msg: "The operating system is not RedHat based."
when: ansible_facts['distribution'] not in supported_os

测试多个条件

when 关键字支持逻辑运算符 and、or,可以用于组合多个条件表达式。如果所有条件都需要满足,则使用 and;如果任意一个条件满足即可,则使用 or。

  1. 使用 and 运算符

在这个例子中,我们将根据多个条件来决定是否安装 Apache。如果操作系统是 RedHat 且内存大于 500 MB,则安装 Apache:

1
2
3
4
5
6
7
8
9
10
11
---
- name: Install Apache based on multiple conditions
hosts: all
become: true

tasks:
- name: Install Apache on RedHat with sufficient memory
ansible.builtin.dnf:
name: httpd
state: present
when: ansible_facts['distribution'] == "RedHat" and ansible_facts['memtotal_mb'] > 500

如果条件很多,也可以用列表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
---
- name: Perform tasks based on multiple conditions with list and logic
hosts: all
become: true

vars:
supported_os:
- Ubuntu
- Debian
- CentOS
- RedHat
min_memory_mb: 2048

tasks:
- name: Check if OS is supported and memory is sufficient
ansible.builtin.debug:
msg: "This host is supported and has enough memory."
when:
- ansible_facts['distribution'] in supported_os
- ansible_facts['memtotal_mb'] >= min_memory_mb
  1. 使用 or 运算符

在这个例子中,如果操作系统是 Ubuntu 或者 RedHat,我们将安装 Apache:

1
2
3
4
5
6
7
8
9
10
11
---
- name: Install Apache based on multiple conditions
hosts: all
become: true

tasks:
- name: Install Apache on Ubuntu or Debian
ansible.builtin.apt:
name: apache2
state: present
when: ansible_facts['distribution'] == "Ubuntu" or ansible_facts['distribution'] == "RedHat"
  1. 组合使用 and 和 or

在这个例子中,我们结合使用 and 和 or 来决定是否执行任务。如果操作系统是 RedHat 且内存大于 500 MB,或者操作系统是 Ubuntu,我们将安装 Apache:

1
2
3
4
5
6
7
8
9
10
11
---
- name: Install Apache based on complex conditions
hosts: all
become: true

tasks:
- name: Install Apache on RedHat with sufficient memory or on Ubuntu
ansible.builtin.dnf:
name: httpd
state: present
when: (ansible_facts['distribution'] == "RedHat" and ansible_facts['memtotal_mb'] > 1024) or ansible_facts['distribution'] == "Ubuntu"

when只是一个逻辑判断,它可以和任何其他技术和模块使用:

  1. 和loop组合实现对每个loop的item检查

  2. 和register组合,实现判断register变量的值等

notify和handlers

我们先来看个案例,理解一下notify和handlers的必要性

在这种情况下,每次运行任务都会重新启动Nginx服务,即使 nginx 已经是最新版本,不需要重新安装。这不仅浪费资源,还可能导致不必要的服务中断。

1
2
3
4
5
6
7
8
9
- name: Install a package
ansible.builtin.dnf:
name: nginx
state: present

- name: Restart nginx
ansible.builtin.service:
name: nginx
state: restarted

我们再看一下如果有notify和handlers会发生什么

在这里,只有当 nginx 实际被安装或更新时,才会触发 restart nginx 这个 handler,重新启动Nginx服务。

1
2
3
4
5
6
7
8
9
10
11
12
- name: Install a package
ansible.builtin.dnf:
name: nginx
state: present
notify:
- restart nginx

handlers:
- name: restart nginx
ansible.builtin.service:
name: nginx
state: restarted

总结一下:

  • notify: 用于在一个任务发生变化后通知 handler。通常,当一个任务(如安装软件或修改配置文件)状态变为 changed 时,它会触发 notify 来调用指定的 handler

  • handlers: 是一组特殊的任务,只有在被 notify 通知时才会执行。这些任务通常定义在 handlers 部分,并在所有任务完成后执行。即使一个 handler 被多次通知,它也只会执行一次。

简单来说,notifyhandlers 帮助确保在配置发生变化时,自动执行必要的后续操作,如重启服务或重新加载配置。这样可以使你的自动化流程更加高效和可靠。

总结一下关键点:

  1. 处理程序顺序:处理程序按照 handlers 部分指定的顺序运行,而不是按照 notify 语句或任务通知的顺序。

  2. 执行时机:处理程序通常在相关 play 的所有任务完成后运行。特定任务调用的处理程序,会等到 tasks 部分的所有任务处理完毕后才运行,极少有例外。

  3. 命名空间:处理程序名称存在于每个 play 的命名空间中。同名的处理程序,只会运行一个。

  4. 单次运行:即使多个任务通知处理程序,它也只会运行一次。没有任务通知处理程序时,处理程序不会运行。

  5. 任务变化:只有当任务报告 changed 结果(即状态改变)时,处理程序才会被通知。报告 ok 结果(如软件包已安装)时,不会通知处理程序。

处理任务失败

ignore_errors

Ansible 评估各任务的返回代码,从⽽确定任务是成功还是失败。通常⽽⾔,当任务失败
时,Ansible 会⽴即跳过所有后续任务。

这有一个问题,有些任务是成功了更好,不成功也无所谓,这种锦上添花的任务一旦失败,就会取消后续的任务执行,这可不行啊,像这种任务失败,得忽略错误继续执行下面的才行

在这个任务中,Ansible 尝试安装或升级 notapkg 到最新版本。如果任务失败,例如软件包不存在或有其他错误,ignore_errors: yes 将确保任务继续执行后续任务,而不会中断整个 playbook 的执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
---
- name: Test playbook for ignore_errors
hosts: servera
tasks:
- name: Latest version of notapkg is installed
ansible.builtin.dnf:
name: notapkg
state: latest
ignore_errors: yes

- name: Debug message
ansible.builtin.debug:
msg: "hello lixiaohui"

我们来运行看看,会不会把第一个任务的错误忽略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[student@workstation ~]$ ansible-navigator run ignore.yml -i inventory -m stdout

PLAY [Test playbook for ignore_errors] *****************************************

TASK [Gathering Facts] *********************************************************
ok: [servera]

TASK [Latest version of notapkg is installed] **********************************
fatal: [servera]: FAILED! => {"changed": false, "failures": ["No package notapkg available."], "msg": "Failed to install some of the specified packages", "rc": 1, "results": []}
...ignoring

TASK [Debug message] ***********************************************************
ok: [servera] => {
"msg": "hello lixiaohui"
}

PLAY RECAP *********************************************************************
servera : ok=3 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=1

force_handlers

force_handlers: yes 设置在一个play上时,这个play中的所有处理程序一旦被通知到,即使play的某些任务失败,这些处理程序也会在play结束时执行。这意味着即使有任务失败,Ansible仍会运行那些已经被通知的处理程序,确保关键的清理或恢复操作得以执行。

在这个示例中,force_handlers: yes 确保如果 notapkg 任务失败,但 notify 已经通知了 restart nginx 处理程序,那么处理程序仍会在play结束时执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
---
- name: Test playbook for force_handlers
hosts: servera
force_handlers: yes
tasks:
- name: Install ftp for test force_handlers
ansible.builtin.dnf:
name: php
state: present
notify:
- restart psacct
- name: Latest version of notapkg is installed
ansible.builtin.dnf:
name: notapkg
state: latest

handlers:
- name: restart psacct
ansible.builtin.service:
name: psacct
state: restarted

即使设置了 force_handlers: yes,处理程序 (handlers) 只有在相应任务实际发生变化时(即任务报告 changed 状态)才会被通知和执行。如果任务报告 ok 状态(表示任务运行成功但没有实际变化),即使有 force_handlers: yes 也不会触发处理程序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[student@workstation ~]$ ansible-navigator run force_handlers.yml -i inventory -m stdout

PLAY [Test playbook for force_handlers] ****************************************

TASK [Gathering Facts] *********************************************************
ok: [servera]

TASK [Install ftp for test force_handlers] *************************************
changed: [servera]

TASK [Latest version of notapkg is installed] **********************************
fatal: [servera]: FAILED! => {"changed": false, "failures": ["No package notapkg available."], "msg": "Failed to install some of the specified packages", "rc": 1, "results": []}

RUNNING HANDLER [restart psacct] ***********************************************
changed: [servera]

PLAY RECAP *********************************************************************
servera : ok=3 changed=2 unreachable=0 failed=1 skipped=0 rescued=0 ignored=0
Please review the log for errors.

failed_when

failed_when 是 Ansible 中用来定义自定义任务失败条件的选项。它允许你根据任务的输出或其他特定条件来决定任务是否失败,而不仅仅依赖于任务的返回码。这对于需要根据复杂逻辑或特定条件来判断任务成功与否的场景非常有用。

使用场景

  1. 检查命令输出:当你运行一个命令并需要根据输出内容来判断任务是否失败时。例如,检查服务状态并根据特定的关键字来判断服务是否正常运行。

  2. 复杂逻辑判断:当任务的失败条件需要依赖于多个变量或复杂的计算结果时。例如,根据多个文件的存在与否来决定任务是否成功。

  3. API 响应处理:调用外部 API 并根据响应内容来判断任务是否失败。可以解析 API 的响应数据,并基于特定字段的值来决定任务的结果。

在下面这个示例中:

  1. 检查Nginx服务状态:通过 ansible.builtin.command 模块运行 systemctl status nginx 命令,并将结果存储在变量 nginx_status 中。

  2. 定义失败条件:使用 failed_when 选项来定义自定义失败条件。如果 nginx_status.stdout 中包含 inactive 字符串,则任务被认为失败。

  3. 调试信息:如果任务成功(即 nginx 服务处于活动状态),将输出调试信息 “Nginx service is active”。

1
2
3
4
5
6
7
8
9
10
11
---
- name: Test playbook for failed_when
hosts: servera
tasks:
- name: Check if psacct service is active
ansible.builtin.command: systemctl status psacct
register: psacct_status
failed_when: "'active' in psacct_status.stdout"
- name: Debug message
ansible.builtin.debug:
msg: "psacct service is inactive"

根据预期,我们的运行将会失败,下面的失败可以看到我们失败的条件达到了failed_when_result": true

1
2
3
4
5
6
7
8
9
10
11
12
13
[student@workstation ~]$ ansible-navigator run failed_when.yml -i inventory -m stdout

PLAY [Test playbook for failed_when] *******************************************

TASK [Gathering Facts] *********************************************************
ok: [servera]

TASK [Check if psacct service is active] ***************************************
fatal: [servera]: FAILED! => {"changed": true, "cmd": ["systemctl", "status", "psacct"], "delta": "0:00:00.020887", "end": "2024-12-26 21:10:47.169839", "failed_when_result": true, "msg": "", "rc": 0, "start": "2024-12-26 21:10:47.148952", "stderr": "", "stderr_lines": [], "stdout": "● psacct.service - Kernel process accounting\n Loaded: loaded (/usr/lib/systemd/system/psacct.service; disabled; vendor preset: disabled)\n Active: active (exited) since Thu 2024-12-26 21:06:46 EST; 4min 1s ago\n Process: 28352 ExecStartPre=/usr/libexec/psacct/accton-create (code=exited, status=0/SUCCESS)\n Process: 28353 ExecStart=/usr/sbin/accton /var/account/pacct (code=exited, status=0/SUCCESS)\n Main PID: 28353 (code=exited, status=0/SUCCESS)\n CPU: 9ms\n\nDec 26 21:06:46 servera.lab.example.com systemd[1]: Starting Kernel process accounting...\nDec 26 21:06:46 servera.lab.example.com accton[28353]: Turning on process accounting, file set to '/var/account/pacct'.\nDec 26 21:06:46 servera.lab.example.com systemd[1]: Finished Kernel process accounting.", "stdout_lines": ["● psacct.service - Kernel process accounting", " Loaded: loaded (/usr/lib/systemd/system/psacct.service; disabled; vendor preset: disabled)", " Active: active (exited) since Thu 2024-12-26 21:06:46 EST; 4min 1s ago", " Process: 28352 ExecStartPre=/usr/libexec/psacct/accton-create (code=exited, status=0/SUCCESS)", " Process: 28353 ExecStart=/usr/sbin/accton /var/account/pacct (code=exited, status=0/SUCCESS)", " Main PID: 28353 (code=exited, status=0/SUCCESS)", " CPU: 9ms", "", "Dec 26 21:06:46 servera.lab.example.com systemd[1]: Starting Kernel process accounting...", "Dec 26 21:06:46 servera.lab.example.com accton[28353]: Turning on process accounting, file set to '/var/account/pacct'.", "Dec 26 21:06:46 servera.lab.example.com systemd[1]: Finished Kernel process accounting."]}

PLAY RECAP *********************************************************************
servera : ok=1 changed=0 unreachable=0 failed=1 skipped=0 rescued=0 ignored=0
Please review the log for errors.

fail 模块

在Ansible中,fail 模块可以用来故意使任务失败,以便在特定条件下终止playbook的执行或触发错误处理程序。这对于测试、条件检查和确保某些关键步骤不被绕过非常有用。

使用场景

  1. 条件检查失败:确保在某些条件不满足时,强制终止playbook的执行。

  2. 测试:验证错误处理机制是否正确工作,模拟错误场景。

  3. 确保关键步骤完成:在关键任务之前插入检查,确保其前置条件必须满足。

看看我们的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
---
- name: Test playbook for fail
hosts: servera
tasks:
- name: Check if psacct service is active
ansible.builtin.command: systemctl status psacct
register: psacct_status

- name: Fail if psacct service is active
ansible.builtin.fail:
msg: "psacct service is active!"
when: "'active' in psacct_status.stdout"

- name: Debug message
ansible.builtin.debug:
msg: "psacct service is inactive"
when: "'active' not in psacct_status.stdout"
  • 检查 psacct 服务状态:使用 ansible.builtin.command 模块运行 systemctl status psacct 命令,并将结果存储在变量 psacct_status 中。

  • 失败条件:使用 ansible.builtin.fail 模块,在 psacct 服务处于活动状态时终止playbook的执行,并输出错误信息 “psacct service is active!”。

  • 调试信息:如果 psacct 服务处于非活动状态,则输出调试信息 “psacct service is inactive”。

我们运行看看结果

发现fail条件达到后,下面的debug任务不再运行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[student@workstation ~]$ ansible-navigator run fail.yml -i inventory -m stdout

PLAY [Test playbook for fail] **************************************************

TASK [Gathering Facts] *********************************************************
ok: [servera]

TASK [Check if psacct service is active] ***************************************
changed: [servera]

TASK [Fail if psacct service is active] ****************************************
fatal: [servera]: FAILED! => {"changed": false, "msg": "psacct service is active!"}

PLAY RECAP *********************************************************************
servera : ok=2 changed=1 unreachable=0 failed=1 skipped=0 rescued=0 ignored=0
Please review the log for errors.

changed_when

  • changed_when 是 Ansible 中用来定义自定义任务变化条件的选项。它允许你根据任务的输出或特定条件来判断任务是否发生变化。这对需要根据复杂逻辑或特定条件来决定任务状态的场景非常有用。

使用场景

  1. 检查命令输出:当你运行一个命令并需要根据输出内容来判断任务是否发生变化时。如,检查配置文件内容或服务状态。

  2. 复杂逻辑判断:当任务的变化条件需要依赖于多个变量或复杂的计算结果时。例如,根多个文件的内容或状态来决定任务是否发生变化。

  3. API 响应处理:调用外部 API 并根据响应内容来判断任务是否发生变化。可以解析API 的响应数据,并基于特定字段的值来决定任务的状态。

我们看个案例先

1
2
3
4
5
6
7
8
9
10
11
12
13
---
- name: Test playbook for changed_when
hosts: servera
tasks:
- name: Ensure the test file exists
ansible.builtin.copy:
content: lixiaohui
dest: /tmp/testfile.txt

- name: Check if file contains lixiaohui string
ansible.builtin.shell: cat /tmp/testfile.txt
register: file_content
changed_when: "'lixiaohui' in file_content.stdout"

看看运行结果,于其上,我们最后的这个check应该会返回change

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[student@workstation ~]$ ansible-navigator run changed_when.yml -i inventory -m stdout

PLAY [Test playbook for changed_when] ******************************************

TASK [Gathering Facts] *********************************************************
ok: [servera]

TASK [Ensure the test file exists] *********************************************
changed: [servera]

TASK [Check if file contains lixiaohui string] *********************************
changed: [servera]

PLAY RECAP *********************************************************************
servera : ok=3 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

block、rescue 和 always

Ansible的 block、rescue 和 always 提供了一种强大的机制来处理任务中可能出现的错误,并确保在特定条件下执行一些清理或补救操作。

block

block 允许你将多个任务分组在一起,以便它们可以作为一个单元进行管理。这可以用于确保一系列任务一起执行,或是为错误处理提供基础。

rescue

rescue 是在 block 中的任务失败时执行的任务。它通常用于执行一些补救操作或记录错误信息。

always

always 是无论 block 中的任务是否失败,都要执行的任务。这通常用于确保某些清理操作始终执行,比如关闭连接或删除临时文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
---
- name: Test playbook for block, rescue, always
hosts: servera
tasks:
- name: Ansible block, rescue, always example
block:
- name: Task that might fail
ansible.builtin.command: /bin/false # 模拟失败的任务

rescue:
- name: Handle the failure
ansible.builtin.debug:
msg: "Task failed, executing rescue"

always:
- name: Ensure cleanup
ansible.builtin.debug:
msg: "This always runs, even if the task fails"

运行看看block失败会怎么样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
[student@workstation ~]$ ansible-navigator run block.yml -i inventory -m stdout

PLAY [Test playbook for block, rescue, always] *********************************

TASK [Gathering Facts] *********************************************************
ok: [servera]

TASK [Task that might fail] ****************************************************
fatal: [servera]: FAILED! => {"changed": true, "cmd": ["/bin/false"], "delta": "0:00:00.004317", "end": "2024-12-26 22:42:28.384521", "msg": "non-zero return code", "rc": 1, "start": "2024-12-26 22:42:28.380204", "stderr": "", "stderr_lines": [], "stdout": "", "stdout_lines": []}

TASK [Handle the failure] ******************************************************
ok: [servera] => {
"msg": "Task failed, executing rescue"
}

TASK [Ensure cleanup] **********************************************************
ok: [servera] => {
"msg": "This always runs, even if the task fails"
}

PLAY RECAP *********************************************************************
servera : ok=3 changed=0 unreachable=0 failed=0 skipped=0 rescued=1 ignored=0

第五章 将⽂件部署到受管主机

修改⽂件并将其复制到主机

file

file模块要注意state,创建的是文件还是文件夹或链接,都靠state,比如state: absent就是删除文件,state: directory就是创建文件夹,state: touch就是创建文件等

file模块除了能创建出对象外,还可以追加更多的参数修改对象的属性,比如权限,身份,selinux等

1
2
3
4
5
6
7
8
9
10
11
12
---
- name: Example playbook using file module with setype
hosts: servera
tasks:
- name: Ensure /tmp/example_dir exists
ansible.builtin.file:
path: /tmp/example_dir
state: directory
owner: root
group: root
mode: '0755'
setype: httpd_sys_content_t

运行看看

1
2
3
4
5
6
7
8
9
10
11
12
[student@workstation ~]$ ansible-navigator run file.yml -i inventory -m stdout

PLAY [Example playbook using file module with setype] **************************

TASK [Gathering Facts] *********************************************************
ok: [servera]

TASK [Ensure /tmp/example_dir exists] ******************************************
changed: [servera]

PLAY RECAP *********************************************************************
servera : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

权限和selinux都ok了

1
2
3
[student@workstation ~]$ ansible servera -m shell -a 'ls -dlZ /tmp/example_dir' -i inventory
servera | CHANGED | rc=0 >>
drwxr-xr-x. 2 root root unconfined_u:object_r:httpd_sys_content_t:s0 6 Dec 26 22:43 /tmp/example_dir

copy

ansible.builtin.copy 模块是 Ansible 中用来将文件或目录从本地主机复制到远程主机的。它在配置文件分发、模板部署、脚本分发等场景中非常有用。

使用场景

  • 配置文件分发:将配置文件从控制节点复制到目标主机。

  • 脚本分发:将执行脚本从控制节点复制到目标主机。

  • 模板部署:将模板文件替换为目标主机上的配置文件。

在这个案例中,我们把主机名文件复制到servera的tmp

1
2
3
4
5
6
7
8
9
10
11
---
- name: Example playbook using copy module
hosts: servera
tasks:
- name: Copy a file to the remote server
ansible.builtin.copy:
src: /etc/hostname
dest: /tmp/
owner: root
group: root
mode: '0644'

运行看看结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[student@workstation ~]$ ansible-navigator run ignore.yml -i inventory -m stdout

PLAY [Example playbook using copy module] **************************************

TASK [Gathering Facts] *********************************************************
ok: [servera]

TASK [Copy a file to the remote server] ****************************************
changed: [servera]

PLAY RECAP *********************************************************************
servera : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
[student@workstation ~]$ ansible servera -m shell -a 'ls -dlZ /tmp/hostname' -i inventory
servera | CHANGED | rc=0 >>
-rw-r--r--. 1 root root unconfined_u:object_r:admin_home_t:s0 12 Dec 26 23:58 /tmp/hostname

fetch

ansible.builtin.fetch 模块用于从远程主机复制文件到控制节点。它在需要从远程系统收集日志文件、备份配置或获取生成的文件时非常有用。

使用场景

  1. 日志收集:从多个远程服务器收集日志文件进行分析。

  2. 配置备份:将远程主机上的配置文件备份到控制节点。

  3. 生成文件获取:从远程主机获取生成的报告或输出文件。

我们可以使用 ansible.builtin.fetch 模块将 servera 主机上的 /etc/hostname 文件复制到控制节点的 /home/student/ 目录

1
2
3
4
5
6
7
8
---
- name: Fetch /etc/hostname from servera
hosts: servera
tasks:
- name: Fetch /etc/hostname from servera
ansible.builtin.fetch:
src: /etc/hostname
dest: /home/student/

我们发现,获取过来的文件在本地会建立一个主机名的文件夹,然后从根目录开始表达文件,避免了重名问题,也可以方便你知道文件来自哪个服务器的哪里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[student@workstation ~]$ ansible-navigator run fetch.yml -i inventory -m stdout

PLAY [Fetch /etc/hostname from servera] ****************************************

TASK [Gathering Facts] *********************************************************
ok: [servera]

TASK [Fetch /etc/hostname from servera] ****************************************
changed: [servera]

PLAY RECAP *********************************************************************
servera : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
[student@workstation ~]$ tree servera/
servera/
└── etc
└── hostname

1 directory, 1 file

lineinfile

ansible.builtin.lineinfile 模块用于在文件中添加、修改或删除特定行。它在配置文件管理、脚本修改或文本处理方面非常有用。

使用场景

  1. 配置文件管理:在配置文件中添加或修改特定的配置项。

  2. 脚本修改:在脚本文件中添加或修改特定的代码行。

  3. 文本处理:在文本文件中添加或删除特定的行。

假设我们需要在 /etc/hosts文件中添加一条新的主机名映射。如果该映射已经存在,则更新它;如果不存在,则添加它

1
2
3
4
5
6
7
8
9
10
---
- name: Example playbook using lineinfile module
hosts: servera
tasks:
- name: Add or update entry in /etc/hosts
ansible.builtin.lineinfile:
path: /etc/hosts
state: present
regexp: '^192\.168\.1\.100'
line: '192.168.1.100 lixiaohui.lab.example.com'

运行看看是否添加了这一条

非常好,我们已经成功添加了这一条

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[student@workstation ~]$ ansible-navigator run lineinfile.yml -i inventory -m stdout

PLAY [Example playbook using lineinfile module] ********************************

TASK [Gathering Facts] *********************************************************
ok: [servera]

TASK [Add or update entry in /etc/hosts] ***************************************
changed: [servera]

PLAY RECAP *********************************************************************
servera : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
[student@workstation ~]$ ansible servera -m shell -a 'cat /etc/hosts' -i inventory
servera | CHANGED | rc=0 >>
127.0.0.1 localhost localhost.localdomain localhost4 localhost4.localdomain4
::1 localhost localhost.localdomain localhost6 localhost6.localdomain6
192.168.1.100 lixiaohui.lab.example.com

blockinfile

ansible.builtin.blockinfile 模块用于在文件中插入、更新或删除特定块的内容。它在需要添加多行配置、脚本代码或文本块时非常有用。

使用场景

  1. 配置文件管理:在配置文件中插入或更新多个配置项。

  2. 脚本修改:在脚本文件中添加或更新多行代码。

  3. 文本处理:在文本文件中插入或删除多个行的文本块。

1
2
3
4
5
6
7
8
9
10
11
12
13
---
- name: Example playbook using blockinfile module
hosts: servera
tasks:
- name: Add text block to /tmp/blocktext
ansible.builtin.blockinfile:
path: /tmp/blocktext
create: true
block: |
This is an example line 1.
This is an example line 2.
This is an example line 3.
state: present

运行后,看看文件是否主动完成了创建并写入

我们发现blockinfile模块写入的内容会有注释包围起来,提醒我们哪里是ansible管理的,如果不想要注释,可以marker: ""

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[student@workstation ~]$ ansible-navigator run blockinfile.yml -i inventory -m stdout

PLAY [Example playbook using blockinfile module] *******************************

TASK [Gathering Facts] *********************************************************
ok: [servera]

TASK [Add text block to /tmp/blocktext] ****************************************
changed: [servera]

PLAY RECAP *********************************************************************
servera : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
[student@workstation ~]$ ansible servera -m shell -a 'cat /tmp/blocktext' -i inventory
servera | CHANGED | rc=0 >>
# BEGIN ANSIBLE MANAGED BLOCK
This is an example line 1.
This is an example line 2.
This is an example line 3.
# END ANSIBLE MANAGED BLOCK

stat

ansible.builtin.stat 模块是 Ansible 用于获取文件或目录的状态的模块。它可以帮助你检查文件是否存在、文件的权限、所有者、大小以及其他属性。stat 模块返回一个详细的状态信息字典,这对于条件判断、文件操作以及调试非常有用。

使用场景

  1. 检查文件是否存在:在执行特定操作之前,确认文件或目录是否存在。

  2. 获取文件属性:检查文件的权限、所有者、大小、修改时间等属性。

  3. 调试和条件判断:根据文件的状态信息执行不同的任务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
---
- name: Example playbook using stat module
hosts: servera
tasks:
- name: Check if /etc/hostname exists
ansible.builtin.stat:
path: /etc/hostname
checksum_algorithm: md5
register: hostname_stat

- name: Debug file existence
ansible.builtin.debug:
msg: "The file /etc/hostname exists."
when: hostname_stat.stat.exists

- name: Display MD5 checksum of /etc/hostname
ansible.builtin.debug:
msg: "MD5 checksum of /etc/hostname: {{ hostname_stat.stat.checksum }}"
when: hostname_stat.stat.exists

运行后,果然报告了文件存在以及md5

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
[student@workstation ~]$ ansible-navigator run stat.yml -i inventory -m stdout

PLAY [Example playbook using stat module] **************************************

TASK [Gathering Facts] *********************************************************
ok: [servera]

TASK [Check if /etc/hostname exists] *******************************************
ok: [servera]

TASK [Debug file existence] ****************************************************
ok: [servera] => {
"msg": "The file /etc/hostname exists."
}

TASK [Display MD5 checksum of /etc/hostname] ***********************************
ok: [servera] => {
"msg": "MD5 checksum of /etc/hostname: 06d69ef7dd6544b371c0339640ce6e78"
}

PLAY RECAP *********************************************************************
servera : ok=4 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

synchronize

synchronize 模块是 Ansible 用于快速、可靠地在本地或远程主机之间同步文件和目录的模块。它基于 rsync 命令,可以高效地执行数据复制和同步任务,特别适合大文件或大量文件的同步场景。

使用场景

  1. 文件备份:将本地文件或目录备份到远程主机,或将远程文件备份到本地主机。

  2. 目录同步:在多个主机之间同步目录内容,确保数据一致性。

  3. 定期复制:定期复制文件或目录,如日志文件、配置文件等。

假设我们需要将本地主机 /var/log/ 目录中的日志文件同步到远程主机 servera/mnt 目录。

1
2
3
4
5
6
7
8
---
- name: Example playbook using synchronize module
hosts: servera
tasks:
- name: Synchronize logs to remote server
ansible.builtin.synchronize:
src: /var/log/
dest: /mnt/

运行后,日志过去了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[student@workstation ~]$ ansible-navigator run synchronize.yml -i inventory -m stdout

PLAY [Example playbook using synchronize module] *******************************

TASK [Gathering Facts] *********************************************************
ok: [servera]

TASK [Synchronize logs to remote server] ***************************************
changed: [servera]

PLAY RECAP *********************************************************************
servera : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
[student@workstation ~]$ ansible servera -m shell -a 'ls /mnt' -i inventory
servera | CHANGED | rc=0 >>
btmp
lastlog
private
receptor
wtmp

Jinja2 模板

Jinja2 模板基础

虽然说用lineinfile或者blockinfile可以修改配置文件,但是我们在本地并不拥有文件的的完整最新副本,而且有时候需要将配置文件部署到很多机器上,而每个机器上的监听地址需要使用他们各自独特的地址,这就效率很低了,此时就可以用模板,所有涉及到每个机器的ip时,都用变量表示,在把模板部署到远程的时候,自动把变量用对应的值填充即可

基本语法

  1. 变量:使用 {{ variable }} 来输出变量的值。

  2. 表达式:使用 {% expression %} 来执行控制语句或逻辑。

  3. 注释:使用 `` 来添加注释,这种注释不会最终出现在远程的文件中。

  4. 数据:我们的业务数据,需要原封不动拷过去的内容。

  5. 后缀名:虽然没有强烈要求,但是jinja2模板一般采用.j2后缀名。

根据语法,我们来写一个模板:

以下的空行也会作为数据在最终文件呈现

1
2
3
4
5
# 这么写注释,这一行会出现在最终文件中

{# 这么写注释,只在模板中出现,最后不会放到远程 #}

{{ ansible_facts.default_ipv4.address }}

写好后,用下面的方法部署到远程

1
2
3
4
5
6
7
8
9
---
- name: Deploy Jinja2 template to servera
hosts: servera
tasks:
- name: Deploy template.j2 to /mnt/template.txt
ansible.builtin.template:
src: template.j2
dest: /mnt/template.txt
mode: '0644'

运行后看看文件是否按照我们预期写入了内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[student@workstation ~]$ ansible-navigator run jinja.yml -i inventory -m stdout

PLAY [Deploy Jinja2 template to servera] ***************************************

TASK [Gathering Facts] *********************************************************
ok: [servera]

TASK [Deploy template.j2 to /mnt/template.txt] *********************************
changed: [servera]

PLAY RECAP *********************************************************************
servera : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
[student@workstation ~]$ ansible servera -m shell -a 'cat /mnt/template.txt' -i inventory
servera | CHANGED | rc=0 >>
# 这么写注释,这一行会出现在最终文件中


172.25.250.10

最后发现,我们不会出现在文件中的注释没出现,空的两行也作为数据出现了,变量成功处理

Jinja2 循环

在模板里可以用for循环来加快效率,减少书写的内容

我们的需求是生成一个/etc/hosts文件,并放到servera上,hosts文件中,要包含servera..d的内容

  1. 我们在这里用了groups魔法变量来列出所有主机,让host这个变量分别等于每一个主机
  2. 用hostvars魔法变量取出host每个主机的特定值
1
2
3
{% for host in groups['all'] %}
{{ hostvars[host]['ansible_facts']['default_ipv4']['address'] }} {{ hostvars[host]['ansible_facts']['fqdn'] }} {{ hostvars[host]['ansible_facts']['hostname'] }}
{% endfor %}

写好后,用下面的方法部署到远程

需要注意的是,哪怕只生成在一台主机上,你的hosts也必须携程hosts: all,原因是在模板中,你用到了每个主机的ansible_facts,只有你的play才收集facts

1
2
3
4
5
6
7
8
9
10
---
- name: Deploy Jinja2 template to servera
hosts: all
tasks:
- name: Deploy template.j2 to /mnt/template.txt
ansible.builtin.template:
src: template.j2
dest: /mnt/template.txt
mode: '0644'
when: "'servera' in inventory_hostname"

运行后,检查一下效果是否和hosts格式相同

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
[student@workstation ~]$ ansible-navigator run templatefor.yml -i inventory -m stdout

PLAY [Deploy Jinja2 template to servera] ***************************************

TASK [Gathering Facts] *********************************************************
ok: [serverc.lab.example.com]
ok: [serverb.lab.example.com]
ok: [172.25.250.13]
ok: [servera]

TASK [Deploy template.j2 to /mnt/template.txt] *********************************
skipping: [serverb.lab.example.com]
skipping: [serverc.lab.example.com]
skipping: [172.25.250.13]
changed: [servera]

PLAY RECAP *********************************************************************
172.25.250.13 : ok=1 changed=0 unreachable=0 failed=0 skipped=1 rescued=0 ignored=0
servera : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
serverb.lab.example.com : ok=1 changed=0 unreachable=0 failed=0 skipped=1 rescued=0 ignored=0
serverc.lab.example.com : ok=1 changed=0 unreachable=0 failed=0 skipped=1 rescued=0 ignored=0
[student@workstation ~]$ ansible servera -m shell -a 'cat /mnt/template.txt' -i inventory
servera | CHANGED | rc=0 >>
172.25.250.10 servera.lab.example.com servera
172.25.250.11 serverb.lab.example.com serverb
172.25.250.12 serverc.lab.example.com serverc
172.25.250.13 serverd.lab.example.com serverd

Jinja2 判断

Jinja2 使⽤ if 语句来提供条件控制。如果满⾜某些条件,这允许你在已部署的⽂件中放置⼀⾏。

如果1小于2,写入feature_flag = true,不然等于false

1
2
3
4
5
{% if 1 < 2 %}
feature_flag = true
{% else %}
feature_flag = false
{% endif %}

写好后,用下面的方法部署到远程

1
2
3
4
5
6
7
8
9
10
---
- name: Deploy Jinja2 template to servera
hosts: all
tasks:
- name: Deploy template.j2 to /mnt/template.txt
ansible.builtin.template:
src: template.j2
dest: /mnt/template.txt
mode: '0644'
when: "'servera' in inventory_hostname"

运行后,检查一下效果,1的确小于2,结果因为是true才对

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[student@workstation ~]$ ansible-navigator run templateif.yml -i inventory -m stdout

PLAY [Deploy Jinja2 template to servera] ***************************************

TASK [Gathering Facts] *********************************************************
ok: [172.25.250.13]
ok: [serverc.lab.example.com]
ok: [servera]
ok: [serverb.lab.example.com]

TASK [Deploy template.j2 to /mnt/template.txt] *********************************
skipping: [serverb.lab.example.com]
skipping: [serverc.lab.example.com]
skipping: [172.25.250.13]
changed: [servera]

PLAY RECAP *********************************************************************
172.25.250.13 : ok=1 changed=0 unreachable=0 failed=0 skipped=1 rescued=0 ignored=0
servera : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
serverb.lab.example.com : ok=1 changed=0 unreachable=0 failed=0 skipped=1 rescued=0 ignored=0
serverc.lab.example.com : ok=1 changed=0 unreachable=0 failed=0 skipped=1 rescued=0 ignored=0
[student@workstation ~]$ ansible servera -m shell -a 'cat /mnt/template.txt' -i inventory
servera | CHANGED | rc=0 >>
feature_flag = true

第六章 管理复杂的 Play 和 Playbook

利⽤主机模式选择主机

这里主要是聊在playbook里的play部分,hosts:到底怎么写,在开始之前,我们先做一个清单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
web.example.com
data.example.com

[lab]
labhost1.example.com
labhost2.example.com

[test]
test1.example.com
test2.example.com

[datacenter1]
labhost1.example.com
test1.example.com

[datacenter2]
labhost2.example.com
test2.example.com

[datacenter:children]
datacenter1
datacenter2

[new]
192.168.2.1
192.168.2.2

以下是可能的hosts:字段

  1. 所有主机: hosts: all或者hosts: “*”

  2. 特定主机名: hosts: web.example.com

  3. 特定组:hosts: lab

  4. 多个主机组:hosts: lab,test

  5. IP地址:hosts: 192.168.2.1

  6. 主机组中排除特定主机: hosts: lab:!labhost1.example.com

  7. 列表:hosts: 192.168.2.1,lab,web.example.com

  8. 同时存在与lab和datacenter1组:hosts: lab,&datacenter1

inclue和import ⽂件

如果 playbook 很长或很复杂,可以将其拆分成较小的文件以便于管理。你可以将多个 playbook 组合到一个主 playbook 中,或者将文件中的任务列表插入到 play 中。这样可以更轻松地在不同项目中重用 play 或任务序列。

Ansible 支持通过包含(include)和导入(import)两种操作将内容放入 playbook。

包含内容(动态操作)

include 操作是动态的。这意味着在 playbook 运行期间,Ansible 会在执行到 include 指令时处理所包含的内容。使用 include 的好处是,它允许你根据运行时的条件决定是否包含某些任务。

导入内容(静态操作)

import 操作是静态的。在 playbook 开始运行之前,Ansible 会在解析 playbook 时预处理导入的内容。这意味着所有导入的内容在运行前已经确定,并不会在运行时受到条件的影响。

include_tasks

假设我们有一个setup_tasks.yml内容如下:

1
2
3
4
5
---
- name: Setup task - Install package
ansible.builtin.package:
name: httpd
state: present

那么我们的主playbook就这么写

1
2
3
4
5
6
---
- name: Main playbook with include
hosts: servera
tasks:
- name: Include setup tasks
include_tasks: setup_tasks.yml

运行后,将显示我们被包括的文件也运行起来了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[student@workstation ~]$ ansible-navigator run main.yml -i inventory -m stdout

PLAY [Main playbook with include] **********************************************

TASK [Gathering Facts] *********************************************************
ok: [servera]

TASK [Include setup tasks] *****************************************************
included: /home/student/setup_tasks.yml for servera

TASK [Setup task - Install package] ********************************************
ok: [servera]

PLAY RECAP *********************************************************************
servera : ok=3 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

import_tasks

还用刚才的setup_tasks.yml做为被导入的对象

我们的主playbook应该长这样:

1
2
3
4
5
6
---
- name: Main playbook with import
hosts: servera
tasks:
- name: import setup tasks
import_tasks: setup_tasks.yml

运行后,被导入的任务依旧执行成功

1
2
3
4
5
6
7
8
9
10
11
12
[student@workstation ~]$ ansible-navigator run main.yml -i inventory -m stdout

PLAY [Main playbook with import] ***********************************************

TASK [Gathering Facts] *********************************************************
ok: [servera]

TASK [Setup task - Install package] ********************************************
ok: [servera]

PLAY RECAP *********************************************************************
servera : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

import_playbook

我们有一个子playbook长这样 setup_playbook.yml

1
2
3
4
5
6
7
8
---
- name: Setup playbook
hosts: servera
tasks:
- name: Install httpd package
ansible.builtin.package:
name: httpd
state: present

还有一个子playbook长这样 deploy_playbook.yml

1
2
3
4
5
6
7
8
---
- name: restart service playbook
hosts: serverb
tasks:
- name: Restart psacct service
ansible.builtin.service:
name: psacct
state: restarted

我们的主playbook长这样,把这两个playbook导入进来执行

1
2
3
4
5
---
- name: Import setup playbook
import_playbook: setup_playbook.yml
- name: Import deploy playbook
import_playbook: deploy_playbook.yml

运行后,看看是否在两个机器上都运行了预期的任务

完美,两个playbook都被导入了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
[student@workstation ~]$ ansible-navigator run main.yml -i inventory -m stdout

PLAY [Setup playbook] **********************************************************

TASK [Gathering Facts] *********************************************************
ok: [servera]

TASK [Install httpd package] ***************************************************
ok: [servera]

PLAY [restart service playbook] ************************************************

TASK [Gathering Facts] *********************************************************
ok: [serverb]

TASK [Restart psacct service] **************************************************
changed: [serverb]

PLAY RECAP *********************************************************************
servera : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
serverb : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

第七章 使⽤⻆⾊和集合简化playbook

什么是角色?

想象一下你在组织一个烧烤派对。为了确保一切顺利进行,你会定义几个角色,比如厨师、服务员和娱乐人员。每个角色都可以根据不同的变量(如人数、菜单、时间安排等)来调整他们的任务。

举个例子:烧烤派对

厨师角色:

  • 任务:准备和烹饪食物

  • 变量

    • menu(菜单)

    • guests(客人数量)

如果你有一个素食菜单和10位客人,那么厨师的任务可能是:

  • 采购10人份的素食食材

  • 烤蔬菜汉堡和素食热狗

如果你有一个普通菜单和20位客人,那么厨师的任务可能是:

  • 采购20人份的肉类和蔬菜

  • 烤牛排、鸡翅和蔬菜串

服务员角色:

  • 任务:布置场地和服务客人

  • 变量

    • table_count(桌子数量)

    • drink_options(饮品选择)

以上内容,对于每场派对来说,厨师和服务员的工作是不变的,他们只需要根据客人点的菜单、客人数量、喜好稍作调整,整个流程又可以走起来了

这对于我们的IT工作也是一样的,我们有大量的工作是重复的,例如软件安装、重启服务、开通防火墙等,每次只是软件的名称、服务名称、端口的不同,所以我们直接写好重复工作的playbook,那些稍有不同之处,我们用变量表示,我们每次只需要给变量赋予不同的值,即可完成相同的工作。

Ansible 角色本身由众多文件夹构成,不过也不见得每个角色都具有这些所有的文件夹,如果角色是你自己创建的,没用到的建议删除

Ansible 角色子目录

子目录功能
defaults此目录中的 main.yml 文件包含角色变量的默认值,使用角色时可以覆盖这些默认值。这些变量的优先级较低,应该在 play 中更改和自定义。
files此目录包含由角色任务引用的静态文件。
handlers此目录中的 main.yml 文件包含角色的处理程序定义。
meta此目录中的 main.yml 文件包含与角色相关的信息,如作者、许可证、平台和可选的角色依赖项。
tasks此目录中的 main.yml 文件包含角色的任务定义。
templates此目录包含由角色任务引用的 Jinja2 模板。
tests此目录可以包含清单和 test.ymlplaybook,可用于测试角色。
vars此目录中的 main.yml 文件定义角色的变量值。这些变量通常用于角色内部用途。这些变量的优先级较高,在 playbook 中使用时不应更改。

创建角色

创建一个lixiaohui角色

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[student@workstation ~]$ mkdir roles
[student@workstation ~]$ cd roles/
[student@workstation roles]$ ansible-galaxy init lixiaohui
- Role lixiaohui was created successfully
[student@workstation roles]$ tree lixiaohui/
lixiaohui/
├── defaults
│   └── main.yml
├── files
├── handlers
│   └── main.yml
├── meta
│   └── main.yml
├── README.md
├── tasks
│   └── main.yml
├── templates
├── tests
│   ├── inventory
│   └── test.yml
└── vars
└── main.yml

8 directories, 8 files

定义角色功能

要注意,为了灵活,这里的具体值用变量表示,以后人家用我们角色的时候,只需要提供这些变量的值,即可完成这些工作,简化人家的工作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[student@workstation roles]$ vim lixiaohui/tasks/main.yml
---
- name: Install httpd package
ansible.builtin.package:
name: "{{ httpd_package }}"
state: present

- name: Start and enable httpd service
ansible.builtin.service:
name: "{{ httpd_service }}"
state: started
enabled: true

- name: Open port 80 on the firewall
ansible.builtin.firewalld:
port: "{{ http_port }}/tcp"
permanent: true
state: enabled
immediate: yes

我们给这些变量提供一个较低优先级的默认值

1
2
3
4
5
[student@workstation roles]$ vim lixiaohui/defaults/main.yml
---
httpd_package: "httpd"
httpd_service: "httpd"
http_port: 80

使用角色

我们来测试一下角色工作是否正常

需要修改ansible.cfg中的角色路径,这样才能找到我们的角色并引用

冒号隔开,我们在第一个位置加上我们角色所在的路径

1
2
3
vim ansible.cfg
[defaults]
roles_path=/home/student/roles:~/.ansible/roles:/usr/share/ansible/roles:/etc/ansible/roles

列出角色看看,确认一下已经可以识别到我们的角色

1
2
3
[student@workstation ~]$ ansible-galaxy list
# /home/student/roles
- lixiaohui, (unknown version)

写一个playbook,测试一下功能是否正常

1
2
3
4
5
---
- name: Test lixiaohui role
hosts: servera
roles:
- lixiaohui

运行后,发现工作正常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[student@workstation ~]$ ansible-navigator run roletest.yml -i inventory -m stdout

PLAY [Test lixiaohui role] *****************************************************

TASK [Gathering Facts] *********************************************************
ok: [servera]

TASK [lixiaohui : Install httpd package] ***************************************
ok: [servera]

TASK [lixiaohui : Start and enable httpd service] ******************************
changed: [servera]

TASK [lixiaohui : Open port 80 on the firewall] ********************************
changed: [servera]

PLAY RECAP *********************************************************************
servera : ok=4 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

看来变量的默认值已经工作正常,我们来模拟一下别人简化工作的写法,人家想安装一个nginx,并开通防火墙,由于我们的角色提供的是httpd,所以他和我们默认值需求不同时,需要自己提供变量,他提供的变量优先级比defaults高

1
2
3
4
5
6
7
---
- name: Test lixiaohui role
hosts: servera
vars:
- httpd_package: nginx
roles:
- lixiaohui

运行后,发现他的变量比我们的高,成功用我的代码安装了他想要的功能,他只提供了变量,所有功能都来自我的角色

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
[student@workstation ~]$ ansible-navigator run roletest.yml -i inventory -m stdout

PLAY [Test lixiaohui role] *****************************************************

TASK [Gathering Facts] *********************************************************
ok: [servera]

TASK [lixiaohui : Install httpd package] ***************************************
changed: [servera]

TASK [lixiaohui : Start and enable httpd service] ******************************
ok: [servera]

TASK [lixiaohui : Open port 80 on the firewall] ********************************
ok: [servera]

PLAY RECAP *********************************************************************
servera : ok=4 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
[student@workstation ~]$ ansible servera -m shell -a 'rpm -q nginx' -i inventory
servera | CHANGED | rc=0 >>
nginx-1.20.1-10.el9.x86_64

使用角色的注意事项

1
2
3
4
5
6
7
8
9
---
- name: Test lixiaohui role
hosts: servera
vars:
- httpd_package: nginx
roles:
- lixiaohui
tasks:
- name: xxxx

上面这种引用方法,角色中的任务优先级将始终高于tasks下面的内容,如果要在角色之前运行某些东西,得这么做:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
---
- name: Playbook with pre_tasks and post_tasks
hosts: all

pre_tasks:
- name: Ensure the system is updated
ansible.builtin.dnf:
name: '*'
state: latest

- name: Install necessary utilities
ansible.builtin.package:
name: vim
state: present

roles:
- lixiaohui

tasks:
- name: Regular task example
ansible.builtin.command:
cmd: echo "Main tasks running"

post_tasks:
- name: Clean up temporary files
ansible.builtin.file:
path: /tmp/tempfile
state: absent

- name: Notify the admin about completion
ansible.builtin.mail:
to: 'admin@example.com'
subject: 'Playbook execution complete'
body: 'The playbook execution has finished successfully.'

所以说,上面这种角色的引用方法非常简单,但是优先级上不好处理,不过在考试中,没有那么多弯弯绕,直接用上面那种更简单,实际工作可能比较复杂,你可以用下面的ansible.builtin.import_role这种方法消除角色优先级较高的问题,下面的方法就是执行到他的时候,他才运行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
---
- name: Playbook with import_role
hosts: servera
tasks:
- name: Ensure the system is updated
ansible.builtin.dnf:
name: '*'
state: latest

- name: Import and execute the lixiaohui role
ansible.builtin.import_role:
name: lixiaohui

- name: Perform additional setup
ansible.builtin.command:
cmd: echo "Additional setup tasks"

运行看看角色任务是否变成中间运行了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[student@workstation ~]$ ansible-navigator run role.yml -i inventory -m stdout

PLAY [Playbook with import_role] ***********************************************

TASK [Gathering Facts] *********************************************************
ok: [servera]

TASK [Ensure the system is updated] ********************************************
ok: [servera]

TASK [lixiaohui : Install httpd package] ***************************************
ok: [servera]

TASK [lixiaohui : Start and enable httpd service] ******************************
ok: [servera]

TASK [lixiaohui : Open port 80 on the firewall] ********************************
ok: [servera]

TASK [Perform additional setup] ************************************************
changed: [servera]

PLAY RECAP *********************************************************************
servera : ok=6 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

安装外部角色

安装外部Ansible角色可以简化和自动化配置管理流程。通过使用Ansible Galaxy等平台,用户可以轻松搜索、下载并安装他人分享的Ansible角色,从而节省开发时间并提高效率。

常用的外部角色托管位置:

  1. https://console.redhat.com

  2. 自己搭建的私有Hub

  3. https://galaxy.ansible.com

  4. https://github.com

让我们来一起打开浏览器,看看galaxy吧

使⽤要求⽂件安装⻆⾊

可以在项⽬⽬录中创建⼀个 roles/requirements.yml ⽂件来指定所需的⻆⾊。此⽂件充当 playbook 项⽬的依赖项清单,在ansible-navigator run运行的时候,会自动将角色安装到roles目录中,不过最好手工下载,确保一切正常,提高运行速度

1
2
3
4
5
6
7
8
9
10
[student@workstation ~]$ mkdir roles
[student@workstation ~]$ vim roles/requirements.yml
---
- src: http://content.lixiaohui.com/role1.tar
name: role1_name
- src: file:///role2.tar
name: role2_name
- src: http://content.lixiaohui.com/role1.tar
scm: git
version: "1.0"

以上requirements.yml中,示意了http、file、git等不同形式的安装,我们从galaxy下载一个角色,实际安装看看

安装包下载后,上传到workstation的根目录

1
https://github.com/geerlingguy/ansible-role-apache/archive/4.0.0.tar.gz

上传后名字为ansible-role-apache-4.0.0.tar.gz,写一个requirements.yml

1
2
3
4
[student@workstation ~]$ vim roles/requirements.yml
---
- src: file:///ansible-role-apache-4.0.0.tar.gz
name: apacherole

安装一个看看

安装的时候,如果不指定 -p roles 选项,ansible-galaxy 将使⽤默认 roles_path 设置中的第⼀个⽬录来确定⻆⾊的安装位置

1
2
3
4
5
[student@workstation ~]$ ansible-galaxy role install -r roles/requirements.yml
Starting galaxy role install process
- downloading role from file:///ansible-role-apache-4.0.0.tar.gz
- extracting apacherole to /home/student/roles/apacherole
- apacherole was installed successfully

查看一下本地角色中,是否包含此角色,需要注意ansible.cfg中的roles_path

1
2
3
4
5
[student@workstation ~]$ grep ^roles_path ansible.cfg
roles_path=/home/student/roles:~/.ansible/roles:/usr/share/ansible/roles:/etc/ansible/roles
[student@workstation ~]$ ansible-galaxy list
# /home/student/roles
- apacherole, (unknown version)

从内容集合获取⻆⾊和模块

从Ansible内容集合获取角色和模块可以显著扩展Ansible的功能,简化复杂任务的自动化过程。Ansible集合(Collections)是一个打包和分发Ansible角色、模块、插件和其它内容的机制。

借助 Ansible 内容集合,Ansible 代码可以与模块和插件分开更新,Ansible软件包不再自带模块,模块和Ansible不再互相制约,额可以有自己的发布和维护节奏。

集合命名空间

命名空间可以避免不同开发者创建的模块和角色之间的命名冲突。每个开发者或组织可以使用唯一的命名空间,从而确保其集合中的内容与其他集合中的内容不冲突,比如我想让模块叫copy这个名称,你也想,这就冲突了,但是加上不同的命名空间前缀就不会冲突,比如我的模块全称为lixiaohui.copy,你的叫zhangsan.copy,就非常合适。

Ansible中,有一个名为ansible.builtin的命名空间始终存在,其内包括仅有的几个核心模块,此命名空间下的模块使用时,可以不带前缀,例如在playbook中可以使用ansible.builtin.copy,也可以用copy

安装 Ansible 内容集合

必须确保该集合在⾃动化执⾏环境中可⽤,然后 playbook 才可以使⽤ Ansible 内容集合中的内容

和角色一样,我们也必须在ansible.cfg中配置集合路径,需要注意的是,我们需要保留下面这两个路径,不然自动化执行环境中的集合将无法工作

1
collections_path=~/.ansible/collections:/usr/share/ansible/collections

我们可以以冒号隔开的方式,将我们的路径插入到前面

1
collections_path=/home/student/mycollection:~/.ansible/collections:/usr/share/ansible/collections

我们来试试从galaxy中,下载一个集合,将其上传到workstation的根目录,然后再将其安装到我们的系统中

从这里下载它的9.0.0版本

1
https://galaxy.ansible.com/ui/repo/published/community/general/

上传后名为community-general-9.0.0.tar.gz

准备一下我们的集合目录并安装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[student@workstation ~]$ mkdir /home/student/mycollection
[student@workstation ~]$ ansible-galaxy collection install /community-general-9.0.0.tar.gz
Starting galaxy collection install process
Process install dependency map
Starting collection install process
Installing 'community.general:9.0.0' to '/home/student/mycollection/ansible_collections/community/general'
community.general:9.0.0 was installed successfully
[student@workstation ~]$ ansible-galaxy collection list

# /home/student/mycollection/ansible_collections
Collection Version
----------------- -------
community.general 9.0.0

# /usr/share/ansible/collections/ansible_collections
Collection Version
------------------------ -------
redhat.rhel_system_roles 1.16.2

集合的安装也可以用requirements文件

1
2
3
4
5
6
7
---
collections:
- name: community.general
- name: community.crypto # 这种直接写名字的方法,要求机器能连接galaxy互联网
version: 1.2.0
- name: /community-general-9.0.0.tar.gz
- name: http://content.lixiaohui.com/community-general-9.0.0.tar.gz

安装刚才我们的意思,写一个实例

1
[student@workstation ~]$ vim mycollection/requirements.yml
1
2
3
---
collections:
- name: /community-general-9.0.0.tar.gz
1
2
3
4
5
6
7
[student@workstation ~]$ rm -rf mycollection/ansible_collections/community/general/
[student@workstation ~]$ ansible-galaxy collection install -r mycollection/requirements.yml
Starting galaxy collection install process
Process install dependency map
Starting collection install process
Installing 'community.general:9.0.0' to '/home/student/mycollection/ansible_collections/community/general'
community.general:9.0.0 was installed successfully

使用集合来工作

刚才我们安装的集合有lvm相关的模块,我们来试试用vdb创建一个vg,然后创建lv,并格式化挂载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
---
- name: 基于/dev/vdb创建VG和LV,并格式化挂载
hosts: servera
become: yes
tasks:
- name: 创建卷组vg_data
community.general.lvg:
vg: vg_data
pvs: /dev/vdb

- name: 创建逻辑卷lv_data
community.general.lvol:
lv: lv_data
vg: vg_data
size: 100%FREE

- name: 格式化逻辑卷lv_data为ext4
community.general.filesystem:
fstype: ext4
dev: /dev/vg_data/lv_data

- name: 创建挂载点目录
file:
path: /mnt/data
state: directory

- name: 挂载逻辑卷lv_data到/mnt/data
ansible.posix.mount:
path: /mnt/data
src: /dev/vg_data/lv_data
fstype: ext4
state: mounted

运行看看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
[student@workstation ~]$ echo servera > inventory
[student@workstation ~]$ ansible-navigator run lv.yml -m stdout -i inventory

PLAY [基于/dev/vdb创建VG和LV,并格式化挂载] ************************************

TASK [Gathering Facts] *********************************************************
ok: [servera]

TASK [创建卷组vg_data] *********************************************************
changed: [servera]

TASK [创建逻辑卷lv_data] *******************************************************
changed: [servera]

TASK [格式化逻辑卷lv_data为ext4] ***********************************************
changed: [servera]

TASK [创建挂载点目录] **********************************************************
changed: [servera]

TASK [挂载逻辑卷lv_data到/mnt/data] ********************************************
changed: [servera]

PLAY RECAP *********************************************************************
servera : ok=6 changed=5 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

看看是否真的创建好了lv,挂好了没有

1
2
3
4
5
6
[student@workstation ~]$ ssh root@servera
[root@servera ~]# tail -n 1 /etc/fstab
/dev/vg_data/lv_data /mnt/data ext4 defaults 0 0
[root@servera ~]# lvs
LV VG Attr LSize Pool Origin Data% Meta% Move Log Cpy%Sync Convert
lv_data vg_data -wi-ao---- 1020.00m

通过系统⻆⾊重⽤内容

系统角色是定义和描述操作系统或应用程序中特定功能和行为的一组配置和管理任务。它们主要用于自动化和简化复杂系统管理任务,使系统管理员能够更高效地管理和维护系统。

系统角色是从光盘里安装软件包带来的,软件包名字为rhel-system-roles

安装一下角色包,并查询系统角色和集合

1
2
3
4
5
6
7
8
9
[student@workstation ~]$ ansible-galaxy collection list

# /home/student/mycollection/ansible_collections
Collection Version
----------------- -------
community.general 9.0.0
[student@workstation ~]$ sudo dnf install rhel-system-roles -y
...
Complete!

软件包安装后,将释放文件到:

  1. /usr/share/ansible/collections/ansible_collections/redhat/rhel_system_roles

  2. /usr/share/ansible/roles # 这是为了向后兼容而已,优先了解上一个

看到安装后,多了一个集合

1
2
3
4
5
6
7
8
9
10
11
[student@workstation ~]$ ansible-galaxy collection list

# /usr/share/ansible/collections/ansible_collections
Collection Version
------------------------ -------
redhat.rhel_system_roles 1.16.2

# /home/student/mycollection/ansible_collections
Collection Version
----------------- -------
community.general 9.0.0

再直接看看角色

1
2
3
4
5
6
7
8
9
[student@workstation ~]$ ansible-galaxy list
# /home/student/roles
- apacherole, (unknown version)
# /usr/share/ansible/roles
- linux-system-roles.certificate, (unknown version)
- linux-system-roles.cockpit, (unknown version)
- linux-system-roles.crypto_policies, (unknown version)
- linux-system-roles.firewall, (unknown version)
...

软件包也带来了一些playbook样例,我们改改就能用

1
2
3
4
5
6
7
8
9
10
[student@workstation ~]$ rpm -ql rhel-system-roles | grep -i example
/usr/share/ansible/collections/ansible_collections/redhat/rhel_system_roles/docs/sshd/example-accept-env.yml
/usr/share/ansible/collections/ansible_collections/redhat/rhel_system_roles/docs/sshd/example-root-login.yml
/usr/share/ansible/collections/ansible_collections/redhat/rhel_system_roles/docs/vpn/example_cert.yml
/usr/share/ansible/collections/ansible_collections/redhat/rhel_system_roles/docs/vpn/example_psk.yml
/usr/share/ansible/collections/ansible_collections/redhat/rhel_system_roles/tests/tlog/tests_example.yml
/usr/share/ansible/roles/rhel-system-roles.tlog/tests/tests_example.yml
/usr/share/doc/rhel-system-roles/certificate/example-basic_self_signed-playbook.yml
/usr/share/doc/rhel-system-roles/certificate/example-dns_ip_and_email-playbook.yml
...

系统角色之时间同步

用系统角色来完成不同系统的时间同步

先查一下帮助,看看有没有playbook可用

1
2
3
[student@workstation ~]$ rpm -ql rhel-system-roles | grep -i example | grep -i timesync
/usr/share/doc/rhel-system-roles/timesync/example-multiple-ntp-servers-playbook.yml
/usr/share/doc/rhel-system-roles/timesync/example-single-pool-playbook.yml

制作自己的yaml文件

1
2
[student@workstation ~]$ cp /usr/share/doc/rhel-system-roles/timesync/example-single-pool-playbook.yml timesync.yml
[student@workstation ~]$ vim timesync.yml

从classroom上同步时间

1
2
3
4
5
6
7
8
- hosts: servera
vars:
timesync_ntp_servers:
- hostname: classroom.example.com
pool: yes
iburst: yes
roles:
- rhel-system-roles.timesync

运行看看

运行之前,需要将角色复制到我们的角色路径中

1
2
3
[student@workstation ~]$ cp -r /usr/share/ansible/roles/rhel-system-roles.timesync/ roles/
[student@workstation ~]$ ansible-navigator run timesync.yml -m stdout -i inventory
...

第八章 对 Ansible 进⾏故障排除

debug 调试

有时候运行异常需要知道详细情况,而ansible只会返回changed之类的简略内容, 就需要注册变量,然后debug,也在playbook的运行中,可以加更多个-v来提升更多的详细程度

1
2
3
4
5
6
7
8
9
10
11
12
---
- name: 演示register和debug
hosts: localhost
gather_facts: no
tasks:
- name: 检查主机可达性
ansible.builtin.ping:
register: ping_result

- name: 显示ping结果
ansible.builtin.debug:
var: ping_result
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[student@workstation ~]$ ansible-navigator run ping.yml -m stdout -i inventory

PLAY [演示register和debug] *****************************************************

TASK [检查主机可达性] **********************************************************
ok: [localhost]

TASK [显示ping结果] ************************************************************
ok: [localhost] => {
"ping_result": {
"changed": false,
"failed": false,
"ping": "pong"
}
}

PLAY RECAP *********************************************************************
localhost : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

syntax-check

在使⽤ playbook 之前或遇到相关问题时,最好验证其语法。有问题就会输出问题,没问题就会输出文件路径。

1
2
[student@workstation ~]$ ansible-navigator run -m stdout ping.yml --syntax-check
playbook: /home/student/ping.yml

playbook书写最佳实践

  • 使用 play 或任务的用途的简要描述来命名 play 和任务。在执行 playbook 时,play 名称或任务名称会显示出来。这也有助于记录每个 play 或任务应该达到的目标,以及可能需要它的原因。

  • 使用注释添加与任务相关的其他内嵌文档。

  • 有效利用垂直空白。通常,垂直组织任务属性可以使它们更易于阅读。

  • 一致的水平缩进至关重要。使用空格而不是制表符,以避免缩进错误。将文本编辑器设置为当您按下 Tab 键时插入空格,以简化操作。

  • 尽可能使 playbook 简单。仅使用您需要的功能。

检测工具

有一些检测工具,可以辅助你找出playbook中潜在的问题

  1. ansible-lint
1
[student@workstation ~]$ sudo dnf install ansible-lint -y
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
[student@workstation ~]$ cat ping.yml
---
- name: 演示register和debug
hosts: localhost
gather_facts: no
tasks:
- name: 检查主机可达性
ansible.builtin.ping:
register: ping_result

- name: 显示ping结果
ansible.builtin.debug:
var: ping_result


[student@workstation ~]$ ansible-lint ping.yml
WARNING: PATH altered to include /usr/bin
WARNING Overriding detected file kind 'yaml' with 'playbook' for given positional argument: ping.yml
WARNING Listing 1 violation(s) that are fatal
yaml: truthy value should be one of [false, true] (truthy)
ping.yml:4

You can skip specific rules or tags by adding them to your configuration file:
# .config/ansible-lint.yml
warn_list: # or 'skip_list' to silence them completely
- yaml # Violations reported by yamllint.

Finished with 1 failure(s), 0 warning(s) on 1 files.

从错误信息来看,问题出在ping.yml文件的第4行。yaml: truthy value should be one of [false, true] (truthy),这表明gather_facts的值应该是布尔值(truefalse),而不是其他格式。

你可以将gather_facts: no改为gather_facts: false,这样就可以解决这个问题。

不过需要注意的是,以上的运行没有用到自动化执行环境,如果要用自动化执行环境,可以这么做

1
2
3
[student@workstation ~]$ ansible-navigator lint ping.yml -m stdout
yaml: truthy value should be one of [false, true] (truthy)
ping.yml:4

artifact

artifact记录了我们的playbook运行过程中生成的日志,必要时,可以对日志进行replay,重现当时的情况而又不用真的连接服务器

artifact模式保存在playbook运行的当前目录,其文件格式为:

playbook名称-artifact单词-时间戳.json

1
timesync-artifact-2025-01-07T07:30:15.319967+00:00.json

重现当时的情况:

这个重现并不会真的连接服务器执行任务,只是用日志看看运行的情况而已

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[student@workstation ~]$ ansible-navigator replay ping-artifact-2025-01-07T07\:42\:43.857915+00\:00.json -m stdout

PLAY [演示register和debug] *****************************************************

TASK [检查主机可达性] **********************************************************
ok: [localhost]

TASK [显示ping结果] ************************************************************
ok: [localhost] => {
"ping_result": {
"changed": false,
"failed": false,
"ping": "pong"
}
}

PLAY RECAP *********************************************************************
localhost : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

当然,artifact有这种重现的好处,也有弊端,你如果交互式输入任何的密码,都将记录下来,有泄密问题,不需要日志的时候,可以关闭artifact收集

1
[student@workstation ~]$ vim /home/student/.ansible-navigator.yml
1
2
3
4
5
6
7
8
---
ansible-navigator:
execution-environment:
image: utility.lab.example.com/ee-supported-rhel8:latest
pull:
policy: missing
playbook-artifact:
enable: false

除了artifact之外,Ansible也有一个log记录,Ansible 提供了⼀个内置⽇志基础架构,可通过 ansible.cfg 配置⽂件 default 部分中的log_path 参数进⾏配置即可

⾝份验证问题

这种问题一般是以下几种可能性

  1. 你用的什么用户连接的受管主机?密码对不对?是否做了免密?

这个在ansible.cfg中,可以配置:

1
2
3
4
5
6
7
8
9
[student@workstation ~]$ grep -v -e ^$ -e ^';' -e ^# ansible.cfg
[defaults]
remote_user=student
host_key_checking=False
[privilege_escalation]
become=True
become_ask_pass=False
become_method=sudo
become_user=root
  1. 你连接的用户在远程是否有配置sudo特权?

这个就需要在每个受管主机上,确认/etc/sudoers文件配置,是否为student用户授权了无需密码的sudo权限

  1. 连不上主机是否因为清单中指定了错误的解析?

如果你的主机名杂乱无序,可以在清单中写成有序的名称,手工把名称指向IP即可

以下假设我的主机名太乱,我可以让ansible认为我的主机名是servera.lab.example.com

1
servera.lab.example.com ansible_host=172.25.250.10

Check_mode

测试环境毕竟和生产环境还是有点区别,我们可以在playbook中启用检查模式,用生产环境去测试我们的代码是否能成功,但是又不真的修改生产环境,一举两得

这个有两个用法

  1. ansible-navigator run xxx.yaml -m stdout -C/–check

  2. 写进playbook里

这个check_mode可以写在play这里,也可以写到模块上,和模块对齐

1
2
3
4
5
6
7
8
---
- name: lixiaohui checkmode test
hosts: localhost
check_mode: true
tasks:
- name: debug
ansible.builtin.debug:
msg: ok

测试模块

测试网站返回的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
---
- name: 使用FQCN方法的uri模块示例
hosts: localhost
tasks:
- name: 使用uri模块发送GET请求
ansible.builtin.uri:
url: https://api.example.com/data
return_content: yes
register: response

- name: 显示响应内容
ansible.builtin.debug:
var: response.content

运行脚本,并测试脚本运行返回值是否为0,不为0就失败

脚本必须存在于控制节点上,会传输到受管主机并在其上执⾏

1
2
3
4
5
6
7
8
9
10
11
12
---
- name: 使用FQCN方法的script模块示例
hosts: all
tasks:
- name: 运行本地脚本
ansible.builtin.script:
cmd: /path/to/local_script.sh
register: script_output

- name: 显示脚本输出
ansible.builtin.debug:
var: script_output.stdout

ansible.builtin.stat模块用于获取远程文件的状态信息,例如是否存在、文件类型、权限等。

1
2
3
4
5
6
7
8
9
10
11
12
---
- name: 使用FQCN方法的stat模块示例
hosts: all
tasks:
- name: 获取文件状态
ansible.builtin.stat:
path: /path/to/file
register: file_status

- name: 显示文件状态
ansible.builtin.debug:
var: file_status

ansible.builtin.assert模块用于在Playbook中执行判断,以验证特定条件是否成立。如果条件不成立,它将使任务失败,并输出一条错误消息。这对于确保配置和状态符合预期非常有用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
---
- name: 使用FQCN方法的assert模块示例
hosts: localhost
tasks:
- name: 检查变量值是否为预期
ansible.builtin.assert:
that:
- my_variable == "expected_value"
fail_msg: "变量my_variable的值不是预期的expected_value"
success_msg: "变量my_variable的值是预期的expected_value"

- name: 显示成功信息
ansible.builtin.debug:
msg: "断言通过,任务继续执行"
when: my_variable == "expected_value"

ansible.builtin.fail模块用于在Playbook中显式地使任务失败。它通常用于在检测到错误条件或不期望的状态时终止Playbook的执行,并输出一条自定义错误消息。

1
2
3
4
5
6
7
8
---
- name: 使用FQCN方法的fail模块示例
hosts: localhost
tasks:
- name: 检查变量值并失败
ansible.builtin.fail:
msg: "遇到非预期的变量值,任务失败"
when: my_variable != "expected_value"

第九章 ⾃动执⾏ Linux 管理任务

软件安装

rhel7以及以前用yum,7之后用dnf,其他发行版还有用apt的,不过模块可以改为ansible.builtin.package来实现自动检测和处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
---
- name: 使用dnf模块管理软件包和软件包组的示例
hosts: all
become: yes
tasks:
- name: 安装多个软件包
ansible.builtin.dnf:
name:
- httpd
- vim
- git
state: present

- name: 安装开发工具包组
ansible.builtin.dnf:
name: "@Development Tools"
state: present

- name: 更新所有软件包到最新版本
ansible.builtin.dnf:
name: '*'
state: latest

- name: 删除未使用的软件包
ansible.builtin.dnf:
name: some_unused_package
state: absent

需要注意的是是,在支持列表的时候,就不要循环,在第一个模块上,我们安装了多个软件,就等于:

1
dnf install httpd vim git -y

如果是下面这种,效率就比较低了

1
2
3
4
5
6
7
8
9
10
11
12
---
- name: 使用dnf模块管理软件包和软件包组的示例
hosts: all
become: yes
tasks:
- name: 安装多个软件包
ansible.builtin.dnf:
state: present
loop:
- httpd
- vim
- git

这个loop就相当于执行下面的命令

1
2
3
dnf install httpd -y
dnf install vim -y
dnf install git -y

收集已安装软件包信息

ansible.builtin.package_facts模块用于收集关于已安装软件包的信息,并将这些信息作为事实返回。这个模块非常有用,可以帮助你在Playbook中根据系统上已安装的软件包进行条件判断。

1
2
3
4
5
6
7
8
9
10
11
---
- name: 使用package_facts模块收集软件包信息
hosts: localhost
become: yes
tasks:
- name: 收集软件包信息
ansible.builtin.package_facts:

- name: 显示已安装的包信息
ansible.builtin.debug:
var: ansible_facts.packages

运行看看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[student@workstation ~]$ ansible-navigator run -m stdout package.yml
[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all'

PLAY [使用package_facts模块收集软件包信息] *****************************************************************************************

TASK [Gathering Facts] *************************************************************************************************************
ok: [localhost]

TASK [收集软件包信息] **************************************************************************************************************
ok: [localhost]

TASK [显示已安装的包信息] **********************************************************************************************************
ok: [localhost] => {
"ansible_facts.packages": {
"acl": [
{
"arch": "x86_64",
"epoch": null,
"name": "acl",
"release": "1.el8",
"source": "rpm",
"version": "2.2.53"
}
],

部署yum仓库到远程

在远程的系统上准备仓库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
---
- name: Configure the company Yum/DNF repositories
hosts: servera.lab.example.com
become: yes
tasks:
- name: Ensure Example Repo exists
ansible.builtin.yum_repository:
file: example
name: example-internal
description: Example Inc. Internal YUM/DNF repo
baseurl: http://materials.example.com/yum/repository/
enabled: yes
gpgcheck: yes
gpgkey: http://materials.example.com/yum/repository/xxxx.gpgkey
state: present

管理用户和组

创建用户和组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
---
- name: 创建用户组和用户的示例
hosts: all
become: yes
tasks:
- name: 创建用户组
ansible.builtin.group:
name: mygroup
state: present

- name: 创建附加用户组
ansible.builtin.group:
name: secondarygroup
state: present

- name: 创建用户并加入主组,同时生成SSH密钥
ansible.builtin.user:
name: myuser
group: mygroup
generate_ssh_key: true
ssh_key_bits: 2048
ssh_key_file: .ssh/id_rsa
state: present

- name: 添加用户到附加组
ansible.builtin.user:
name: myuser
groups: secondarygroup
append: yes
state: present

- name: 为用户设置密码
ansible.builtin.user:
name: myuser
password: "{{ 'my_password' | password_hash('sha512') }}"
state: present

配置sudo权限

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
---
- name: 修改sudo配置以允许group01组无密码使用sudo
hosts: all
become: yes
tasks:
- name: 确保sudoers.d目录存在
ansible.builtin.file:
path: /etc/sudoers.d
state: directory
mode: '0750'

- name: 修改sudo配置以允许group01组无密码使用sudo
ansible.builtin.lineinfile:
path: /etc/sudoers.d/group01
state: present
create: true
mode: '0440'
line: "%group01 ALL=(ALL) NOPASSWD: ALL"
validate: /usr/sbin/visudo -cf %s

管理系统任务与进程调度

管理系统任务

at 命令可调度作业在特定时间运⾏⼀次。Cron ⼦系统将作业调度为按循环计划运⾏

以下示例中,touch命令将在10分钟后执行

1
2
3
4
5
6
7
8
9
10
---
- name: Schedule a touch command at a specific time
hosts: all
tasks:
- name: Schedule touch command
ansible.posix.at:
command: "touch /path/to/your/file.txt"
count: 10
units: minutes
unique: yes

以下示例指定了任务在每天凌晨2点运行

1
2
3
4
5
6
7
8
9
10
11
---
- name: Schedule a cron job to create a file
hosts: all
tasks:
- name: Create a cron job to touch a file daily at 2 AM
ansible.builtin.cron:
name: "create_file"
minute: "0"
hour: "2"
job: "touch /path/to/your/file.txt"
user: "lixiaohui"

管理服务

管理服务一般来说,有两个模块,分别是

  1. ansible.builtin.service

  2. ansible.builtin.systemd

ansible.builtin.systemd 通常只能管理systemd服务,且具有daemon-reload功能

1
2
3
4
5
6
7
8
9
10
---
- name: Ensure apache is running and enabled
hosts: all
tasks:
- name: Ensure apache is running and enabled
ansible.builtin.systemd:
name: httpd
state: started
enabled: yes
daemon_reload: yes

ansible.builtin.service 这个可以和很多服务管理系统搭配,比如刚才说的systemdupstartsysVinit等等,但是需要注意的是,不具有daemon-reload功能

1
2
3
4
5
6
7
8
9
---
- name: Ensure apache is running and enabled
hosts: all
tasks:
- name: Ensure apache is running and enabled
ansible.builtin.service:
name: httpd
state: started
enabled: yes

重启节点

有时候执行某些任务的时候,需要重启后继续执行,而不是中断,就可以用reboot模块

1
2
3
4
5
6
7
8
9
10
11
12
13
---
- name: Update software and reboot the server
hosts: all
tasks:
- name: Update all packages
ansible.builtin.yum:
name: "*"
state: latest

- name: Reboot the server
ansible.builtin.reboot:
reboot_timeout: 600
msg: "Reboot after updates"

使⽤存储系统⻆⾊配置存储

存储角色只能在未分区、整个磁盘或逻辑卷(LV)上创建文件系统。它不能在分区中创建文件系统。

存储角色配置只会影响在以下变量中列出的文件系统、卷和池:

storage_volumes

在所有要管理的未分区磁盘中的文件系统列表。

storage_volumes 也可以包含 raid 卷。

storage_pools

要管理的池列表。

目前唯一支持的池类型是 LVM。使用 LVM 时,池代表卷组(VG)。每个池中都有一个要由角色管理的卷列表。对于 LVM,每个卷对应一个带文件系统的逻辑卷(LV)。

1
2
3
4
5
6
7
8
9
10
11
12
---
- name: Example of a simple storage device
hosts: all
roles:
- name: redhat.rhel_system_roles.storage
storage_volumes:
- name: extra
type: disk
disks:
- /dev/vdg
fs_type: xfs
mount_point: /opt/extra

配置解释

  • name: Example of a simple storage device 是这次任务的描述。

  • hosts: all 指定任务将在所有主机上执行。

  • roles 部分指定了要使用的 Ansible 角色,这里是 redhat.rhel_system_roles.storage

  • storage_volumes 定义了一个存储卷的配置:

    • name: extra 是存储卷的名称。

    • type: disk 指定了存储卷的类型,这里是一个磁盘。

    • disks: /dev/vdg 列出要管理的物理磁盘,这里是 /dev/vdg

    • fs_type: xfs 指定了要创建的文件系统类型,这里是 xfs

    • mount_point: /opt/extra 指定了文件系统的挂载点。

这个配置将创建一个名为 extra 的存储卷,使用 /dev/vdg 磁盘,并将其格式化为 xfs 文件系统,并挂载到 /opt/extra

再来创建一个lvm的看看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
---
- name: Configure storage on webservers
hosts: webservers
roles:
- name: redhat.rhel_system_roles.storage
storage_pools:
- name: vg01
type: lvm
disks:
- /dev/vdb
storage_volumes:
- name: lvol01
size: 128m
mount_point: /data
fs_type: xfs
state: present

配置解释

  • name: Configure storage on webservers 是该任务的描述。

  • hosts: webservers 指定任务将在 webservers 主机组上执行。

  • roles 部分指定了要使用的 Ansible 角色,这里是 redhat.rhel_system_roles.storage

  • storage_pools 定义了一个存储池的配置:

    • name: vg01 是存储池的名称。

    • type: lvm 指定存储池的类型,这里是 LVM(逻辑卷管理)。

    • disks: /dev/vdb 列出要管理的物理磁盘,这里是 /dev/vdb

  • storage_volumes 定义了一个逻辑卷的配置:

    • name: lvol01 是逻辑卷的名称。

    • size: 128m 指定逻辑卷的大小,这里是 128 MB。

    • mount_point: /data 指定文件系统的挂载点,这里是 /data

    • fs_type: xfs 指定文件系统的类型,这里是 xfs

    • state: present 确保逻辑卷的存在。

这个配置将创建一个名为 vg01 的 LVM 存储池,使用 /dev/vdb 磁盘,并在该存储池中创建一个名为 lvol01 的逻辑卷,将其格式化为 xfs 文件系统,并挂载到 /data

不过以上操作可能比较复杂,也可以考虑用社区的parted模块

1
2
3
4
- name: Ensure that /dev/sda1 exists
ansible.builtin.command:
cmd: parted --script mklabel gpt mkpart primary 1MiB 100%
creates: /dev/sda1

创建了分区后,可以用community.general.filesystem模块格式化,然后ansible.builtin.mount挂载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
---
- name: Ensure filesystem is created and mounted
hosts: all
tasks:
- name: Create an ext4 filesystem on /dev/sda1
community.general.filesystem:
fstype: ext4
dev: /dev/sda1

- name: Mount the filesystem
ansible.builtin.mount:
path: /mnt/data
src: /dev/sda1
fstype: ext4
state: mounted

管理⽹络配置

静态IP配置方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
---
- name: Configure network on webservers
hosts: webservers
vars:
network_connections:
- name: eth0
type: ethernet
ip:
address:
- 192.168.1.100/24
gateway4: 192.168.1.1
dns:
- 8.8.8.8
- 8.8.4.4
state: up
roles:
- role: redhat.rhel_system_roles.network

DHCP 配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
---
- name: Configure DHCP and Auto IPv6 on enp1s0
hosts: all
vars:
network_connections:
- name: enp1s0
type: ethernet
ip:
dhcp4: true
auto6: true
state: up
roles:
- role: redhat.rhel_system_roles.network

完全删除 enp1s0 的配置示例如下:

1
2
3
4
5
6
7
8
9
10
11
---
- name: Configure network on webservers
hosts: all
vars:
network_connections:
- name: enp1s0
type: ethernet
state: down
persistent_state: absent
roles:
- role: redhat.rhel_system_roles.network

设置主机名

1
2
3
4
5
6
7
---
- name: Ensure the hostname is set
hosts: servera
tasks:
- name: Set the hostname to webserver1
ansible.builtin.hostname:
name: webserver1

设置防火墙

1
2
3
4
5
6
7
8
9
10
---
- name: Ensure port 80 is open
hosts: all
tasks:
- name: Open port 80/tcp
ansible.posix.firewalld:
port: 80/tcp
permanent: true
state: enabled
immediate: yes
1
2
3
4
5
6
7
8
9
10
---
- name: Ensure http service is enabled
hosts: all
tasks:
- name: Enable http service
ansible.posix.firewalld:
service: http
permanent: true
state: enabled
immediate: yes