在 RHEL 上构建复杂的 Ansible Playbook

AnsibleAnsibleBeginner
立即练习

💡 本教程由 AI 辅助翻译自英文原版。如需查看原文,您可以 切换至英文原版

引言

在本实验中,你将学习在 RHEL 上构建复杂 Ansible Playbook 的关键技术,以创建更易于管理、可扩展且可重用的自动化。你将从基本概念过渡到高级组织策略,重点关注如何精确控制自动化运行的主机,以及如何将大型 Playbook 拆分成逻辑上独立的模块化组件。

你将首先掌握主机选择,使用基本组名、通配符、排除项和逻辑运算符来定位 inventory 中的特定节点。接下来,你将通过使用 include_tasksimport_tasks 将任务重构到单独的文件中来探索模块化。最后,你将学习使用 import_playbook 组合一个完整的、多 Playbook 的工作流,最终完成对你完全结构化和模块化的 Ansible 项目的执行和验证。

使用基本模式和通配符模式选择主机

在本步骤中,你将学习 Ansible 自动化中定位特定主机的基础知识。其核心是 Ansible inventory 文件,它列出了你管理的服务器,以及 Playbook 中的 hosts 指令,该指令指定一组任务应在哪些服务器上运行。我们将从安装 Ansible、创建基本的 inventory 和 Playbook 开始,然后探索如何使用组名和通配符模式选择主机。

首先,让我们确保你的环境中已安装 Ansible。Ansible 默认未安装,因此你需要使用 DNF 包管理器进行安装。ansible-core 包提供了 Ansible 的核心命令行工具,包括 ansible-playbook。运行以下命令:

sudo dnf install -y ansible-core

你应该会看到类似以下的输出:

...
Installed:
  ansible-core-2.16.x-x.el9.x86_64
  ...
Complete!

接下来,让我们为本次练习创建一个专用目录,以保持文件整洁。本步骤中所有后续操作都将在该新目录中进行:

mkdir -p ~/project/ansible_patterns
cd ~/project/ansible_patterns

现在,创建一个 inventory 文件。Inventory 是一个文本文件,用于定义 Ansible 命令、模块和任务在其上运行的主机和主机组。我们将使用 INI 格式,因为它更简单。

使用 nano 编辑器创建一个名为 inventory 的文件:

nano inventory

inventory 文件添加以下内容。这定义了两个组:webserversdbservers,每个组包含两个主机:

[webservers]
web1.example.com
web2.example.com

[dbservers]
db1.lab.net
db2.lab.net

保存文件并退出 nano,按 Ctrl+X,然后按 Y,最后按 Enter

Inventory 已准备就绪,现在让我们创建一个简单的 Playbook。此 Playbook 将使用 ansible.builtin.debug 模块打印一条消息,确认任务正在哪个主机上运行。这是测试主机模式而无需对系统进行任何实际更改的好方法。

创建一个名为 playbook.yml 的新文件:

nano playbook.yml

添加以下 YAML 内容。最初,它将目标设置为 webservers 组中的所有主机:

---
- name: Test Host Patterns
  hosts: webservers
  gather_facts: false
  tasks:
    - name: Display the inventory hostname
      ansible.builtin.debug:
        msg: "This task is running on {{ inventory_hostname }}"

保存并退出编辑器。现在,使用 ansible-playbook 运行 Playbook。-i 标志用于指定我们的自定义 inventory 文件:

ansible-playbook playbook.yml -i inventory

输出应如下所示:

PLAY [Test Host Patterns] ******************************************************

TASK [Display the inventory hostname] ******************************************
ok: [web1.example.com] => {
    "msg": "This task is running on web1.example.com"
}
ok: [web2.example.com] => {
    "msg": "This task is running on web2.example.com"
}

PLAY RECAP *********************************************************************
web1.example.com           : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
web2.example.com           : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

如你所见,只有 webservers 组中的主机被定位。现在,让我们修改 Playbook 以使用通配符 (*)。通配符允许更灵活的模式匹配。

编辑 playbook.yml 并将 hosts 行更改为 hosts: "*.lab.net"。请记住将包含通配符的模式用引号括起来:

---
- name: Test Host Patterns
  hosts: "*.lab.net"
  gather_facts: false
  tasks:
    - name: Display the inventory hostname
      ansible.builtin.debug:
        msg: "This task is running on {{ inventory_hostname }}"

再次运行 Playbook:

ansible-playbook playbook.yml -i inventory

你应该会看到类似以下的输出:

PLAY [Test Host Patterns] ******************************************************

TASK [Display the inventory hostname] ******************************************
ok: [db1.lab.net] => {
    "msg": "This task is running on db1.lab.net"
}
ok: [db2.lab.net] => {
    "msg": "This task is running on db2.lab.net"
}

PLAY RECAP *********************************************************************
db1.lab.net                : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
db2.lab.net                : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

这次,Play 只在以 .lab.net 结尾的主机上运行。最后,让我们使用特殊关键字 all 来定位 inventory 中定义的所有主机。

最后一次编辑 playbook.yml 并将 hosts 行更改为 hosts: all

---
- name: Test Host Patterns
  hosts: all
  gather_facts: false
  tasks:
    - name: Display the inventory hostname
      ansible.builtin.debug:
        msg: "This task is running on {{ inventory_hostname }}"

运行 Playbook 以查看结果:

ansible-playbook playbook.yml -i inventory

输出将显示所有被定位的主机:

PLAY [Test Host Patterns] ******************************************************

TASK [Display the inventory hostname] ******************************************
ok: [web1.example.com] => {
    "msg": "This task is running on web1.example.com"
}
ok: [web2.example.com] => {
    "msg": "This task is running on web2.example.com"
}
ok: [db1.lab.net] => {
    "msg": "This task is running on db1.lab.net"
}
ok: [db2.lab.net] => {
    "msg": "This task is running on db2.lab.net"
}

PLAY RECAP *********************************************************************
db1.lab.net                : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
db2.lab.net                : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
web1.example.com           : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
web2.example.com           : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

Playbook 现在将在你的 inventory 中的所有四个主机上运行,展示了 all 模式的强大功能。

使用排除项和逻辑运算符精炼主机选择

在本步骤中,你将通过学习如何使用排除项和逻辑运算符来提升你的主机选择技能。这些功能允许进行高度精确的定位,这在管理复杂环境时至关重要。你将学习如何使用 ! (NOT) 运算符排除主机,以及如何使用 & (AND) 运算符组合组。我们将继续使用上一步中的 inventoryplaybook.yml 文件。

首先,请确保你位于正确的工作目录:

cd ~/project/ansible_patterns

为了有效地演示逻辑运算符,我们需要在主机组之间创建一些重叠。让我们编辑 inventory 文件,添加一个名为 production 的新组,该组包含一个 Web 服务器和一个数据库服务器:

nano inventory

[production] 组及其成员添加到文件末尾:

[webservers]
web1.example.com
web2.example.com

[dbservers]
db1.lab.net
db2.lab.net

[production]
web1.example.com
db1.lab.net

保存文件并退出 nano,按 Ctrl+X,然后按 Y,最后按 Enter

现在,让我们练习排除。! 运算符(表示 NOT)允许你从选择中排除主机或组。修改你的 playbook.yml 以定位除 dbservers 组中的主机之外的所有主机:

nano playbook.yml

如下所示更新 hosts 行。模式 all,!dbservers 会选择所有主机,然后移除其中属于 dbservers 组的任何主机:

---
- name: Test Host Patterns
  hosts: all,!dbservers
  gather_facts: false
  tasks:
    - name: Display the inventory hostname
      ansible.builtin.debug:
        msg: "This task is running on {{ inventory_hostname }}"

保存并退出编辑器,然后运行 Playbook:

ansible-playbook playbook.yml -i inventory

你应该只会看到 Web 服务器被定位:

PLAY [Test Host Patterns] ******************************************************

TASK [Display the inventory hostname] ******************************************
ok: [web1.example.com] => {
    "msg": "This task is running on web1.example.com"
}
ok: [web2.example.com] => {
    "msg": "This task is running on web2.example.com"
}

PLAY RECAP *********************************************************************
web1.example.com           : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
web2.example.com           : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

正如预期的那样,只有 webservers 组中的主机被定位。

接下来,让我们探索逻辑 AND 运算符。& 运算符仅选择同时存在于 两个 指定组中的主机(交集)。让我们修改 Playbook 以定位同时属于 webservers 组和 production 组的主机:

nano playbook.yml

hosts 行更改为 webservers,&production

---
- name: Test Host Patterns
  hosts: webservers,&production
  gather_facts: false
  tasks:
    - name: Display the inventory hostname
      ansible.builtin.debug:
        msg: "This task is running on {{ inventory_hostname }}"

保存并运行 Playbook:

ansible-playbook playbook.yml -i inventory

这次,只有两个组的交集将被定位:

PLAY [Test Host Patterns] ******************************************************

TASK [Display the inventory hostname] ******************************************
ok: [web1.example.com] => {
    "msg": "This task is running on web1.example.com"
}

PLAY RECAP *********************************************************************
web1.example.com           : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

输出正确地显示只有 web1.example.com 被定位,因为它同时是 webserversproduction 组的成员。这些运算符让你能够精确控制自动化影响的主机。

使用 include_tasksimport_tasks 模块化 Play

在本步骤中,你将学习如何通过将大型 Ansible 项目分解为更小、可重用的文件来构建它们。随着 Playbook 的增长,将所有任务保存在单个文件中会变得难以管理。Ansible 为此提供了两个主要指令:import_tasksinclude_tasks。两者都允许你从另一个文件中引入任务。

  • import_tasks静态的。它在 Playbook 被 Ansible 首次解析时处理。这最适合 Play 中无条件、结构化的部分。
  • include_tasks动态的。它在 Play 执行期间处理。这使其适用于循环和条件语句。

我们现在将重构我们的 Playbook 以同时使用它们。首先,请确保你位于项目目录中:

cd ~/project/ansible_patterns

在继续之前,让我们更新 inventory 文件,使主机指向 localhost 以便在此实验环境中使用。这将允许 Playbook 成功运行:

nano inventory

用以下配置替换内容,该配置将示例主机映射到 localhost:

[webservers]
web1.example.com ansible_host=localhost ansible_connection=local
web2.example.com ansible_host=localhost ansible_connection=local

[dbservers]
db1.lab.net ansible_host=localhost ansible_connection=local
db2.lab.net ansible_host=localhost ansible_connection=local

保存并退出编辑器。此配置使用 ansible_host=localhost 将连接重定向到本地机器,并使用 ansible_connection=local 来避免 SSH 连接尝试。

一种常见的做法是将可重用的任务文件存储在专用的子目录中。让我们创建一个名为 tasks 的目录:

mkdir tasks

现在,让我们为可能适用于许多服务器的通用设置任务创建一个文件。我们将在此处放置一个安装 httpd Web 服务器包的任务:

nano tasks/web_setup.yml

添加以下内容。请注意,此文件只是一个任务列表;它不包含完整的 Play 结构(如 hosts:name:):

- name: Install the httpd package
  ansible.builtin.dnf:
    name: httpd
    state: present
  become: true

保存并退出 nano。接下来,创建第二个任务文件用于简单的验证步骤:

nano tasks/verify_config.yml

将此调试任务添加到此文件中:

- name: Display a verification message
  ansible.builtin.debug:
    msg: "Configuration tasks applied to {{ inventory_hostname }}"

保存并退出编辑器。现在,让我们修改主 playbook.yml 文件以使用这些新的任务文件。我们将使用 import_tasks 进行静态设置,使用 include_tasks 进行动态验证消息:

nano playbook.yml

用以下内容替换 playbook.yml 的全部内容。此 Playbook 现在将目标设置为 webservers 组,并使用模块化任务文件:

---
- name: Configure Web Servers
  hosts: webservers
  gather_facts: false
  tasks:
    - name: Import web server setup tasks
      import_tasks: tasks/web_setup.yml

    - name: Include verification tasks
      include_tasks: tasks/verify_config.yml

保存文件并运行 Playbook:

ansible-playbook playbook.yml -i inventory

你应该会看到模块化任务正在执行:

PLAY [Configure Web Servers] ***************************************************

TASK [Import web server setup tasks] *******************************************
imported: /home/labex/project/ansible_patterns/tasks/web_setup.yml

TASK [Install the httpd package] ***********************************************
changed: [web1.example.com]
changed: [web2.example.com]

TASK [Include verification tasks] **********************************************
included: /home/labex/project/ansible_patterns/tasks/verify_config.yml for web1.example.com, web2.example.com

TASK [Display a verification message] ******************************************
ok: [web1.example.com] => {
    "msg": "Configuration tasks applied to web1.example.com"
}
ok: [web2.example.com] => {
    "msg": "Configuration tasks applied to web2.example.com"
}

PLAY RECAP *********************************************************************
web1.example.com           : ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
web2.example.com           : ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

请注意输出如何清晰地指示何时从各自的文件导入和包含任务。这种模块化方法使你的自动化更简洁,更易于维护。

使用 import_playbook 组合工作流

在本步骤中,你将学习如何使用 import_playbook 来编排整个 Playbook,以形成复杂的工作流。虽然 import_tasksinclude_tasks 用于在单个 Play 中重用任务列表,但 import_playbook 在更高级别上运行。它允许你创建一个主 Playbook,该 Playbook 按特定顺序执行其他独立的 Playbook。这是管理大规模自动化(例如,配置整个应用程序堆栈)的标准方法。

首先,让我们确保我们位于正确的目录中,并为这种新结构组织我们的项目:

cd ~/project/ansible_patterns

将单个组件 Playbook 存储在专用的子目录中是一种最佳实践。让我们创建一个名为 playbooks 的目录:

mkdir playbooks

现在,将我们在上一步中创建的配置 Web 服务器的 Playbook 移动到这个新目录中。将其重命名以使其更具描述性也是一个好主意:

mv playbook.yml playbooks/web_configure.yml

但是,由于我们将 Playbook 移动到了子目录中,因此我们需要更新任务文件的相对路径。任务文件仍然位于相对于主项目目录的 tasks/ 目录中,因此我们需要调整路径:

nano playbooks/web_configure.yml

在 Playbook 中更新路径,使用 ../tasks/ 而不是 tasks/

---
- name: Configure Web Servers
  hosts: webservers
  gather_facts: false
  tasks:
    - name: Import web server setup tasks
      import_tasks: ../tasks/web_setup.yml

    - name: Include verification tasks
      include_tasks: ../tasks/verify_config.yml

保存并退出编辑器。

让我们测试已更正的 Playbook,以确保路径正常工作:

ansible-playbook playbooks/web_configure.yml -i inventory

你应该会看到 Playbook 使用更正的路径成功执行。

接下来,为配置数据库服务器创建一个新的独立 Playbook。此 Playbook 将定位 dbservers 组并安装 mariadb 包:

nano playbooks/db_setup.yml

将以下内容添加到文件中。这是一个完整、独立的 Play:

---
- name: Configure Database Servers
  hosts: dbservers
  gather_facts: false
  tasks:
    - name: Install mariadb package
      ansible.builtin.dnf:
        name: mariadb
        state: present
      become: true

    - name: Display a confirmation message
      ansible.builtin.debug:
        msg: "Database server {{ inventory_hostname }} configured."

保存并退出编辑器。现在你有了两个组件 Playbook:一个用于 Web 服务器,一个用于数据库服务器。

最后,创建一个顶级的“主”Playbook。此文件本身不包含任何主机或任务。它的唯一工作是按正确的顺序导入其他 Playbook 来定义整体工作流:

nano main.yml

添加以下内容。这创建了一个工作流,该工作流首先配置 Web 服务器,然后配置数据库服务器:

---
- name: Import the web server configuration play
  import_playbook: playbooks/web_configure.yml

- name: Import the database server configuration play
  import_playbook: playbooks/db_setup.yml

保存并退出 nano。现在你可以通过运行 main.yml Playbook 来执行你的整个工作流:

ansible-playbook main.yml -i inventory

输出将显示两个 Playbook 按顺序执行:

PLAY [Configure Web Servers] ***************************************************

TASK [Import web server setup tasks] *******************************************
imported: /home/labex/project/ansible_patterns/playbooks/../tasks/web_setup.yml

TASK [Install the httpd package] ***********************************************
ok: [web1.example.com]
ok: [web2.example.com]

TASK [Include verification tasks] **********************************************
included: /home/labex/project/ansible_patterns/playbooks/../tasks/verify_config.yml for web1.example.com, web2.example.com

TASK [Display a verification message] ******************************************
ok: [web1.example.com] => {
    "msg": "Configuration tasks applied to web1.example.com"
}
ok: [web2.example.com] => {
    "msg": "Configuration tasks applied to web2.example.com"
}

PLAY [Configure Database Servers] **********************************************

TASK [Install mariadb package] *************************************************
changed: [db1.lab.net]
changed: [db2.lab.net]

TASK [Display a confirmation message] ******************************************
ok: [db1.lab.net] => {
    "msg": "Database server db1.lab.net configured."
}
ok: [db2.lab.net] => {
    "msg": "Database server db2.lab.net configured."
}

PLAY RECAP *********************************************************************
db1.lab.net                : ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
db2.lab.net                : ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
web1.example.com           : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
web2.example.com           : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

输出清楚地显示了两个独立的 Play 按顺序执行,演示了 import_playbook 如何有效地从较小的、可管理的部分组合成更大的工作流。

执行和验证完整的模块化 Playbook

在最后一步中,你将执行你构建的完整模块化工作流,更重要的是,你将学习如何验证自动化是否已在目标系统上达到预期状态。一个成功的 Playbook 运行固然好,但确认结果对于可靠的自动化至关重要。

首先,请确保你位于主项目目录中:

cd ~/project/ansible_patterns

在运行最终 Playbook 之前,让我们可视化你创建的完整项目结构。tree 命令非常适合此目的。如果尚未安装,你可以使用 dnf 进行安装:

sudo dnf install -y tree
tree .

你应该会看到类似以下的结构:

.
├── inventory
├── main.yml
├── playbooks
│   ├── db_setup.yml
│   └── web_configure.yml
└── tasks
    ├── verify_config.yml
    └── web_setup.yml

2 directories, 6 files

这种结构,包含一个主入口点 (main.yml)、单独的 Playbook 文件和可重用的任务文件,是管理 Ansible 项目的可扩展且可维护的方式。

现在,通过运行你的顶层 main.yml Playbook 来执行整个工作流:

ansible-playbook main.yml -i inventory

在 Playbook 成功完成后,下一步至关重要的是验证。你需要确认系统处于你期望的状态。我们的 Playbook 设计用于在 Web 服务器上安装 httpd 包,在数据库服务器上安装 mariadb 包。由于此实验中的所有任务都在你的本地机器上运行,我们可以使用 rpm 命令直接验证它们的安装。

首先,检查 httpd 包是否作为 Web 服务器配置的一部分被安装:

rpm -q httpd

你应该会看到确认包已安装的输出:

httpd-2.4.xx-x.el9.x86_64

接下来,验证数据库服务器配置中 mariadb 包的安装:

rpm -q mariadb

同样,你应该会看到 mariadb 已安装的确认信息:

mariadb-10.5.xx-x.el9.x86_64

在输出中看到包名称,即可确认你的 Ansible Playbook 已按预期成功配置了系统。你现在已成功从头到尾构建、执行和验证了一个模块化的 Ansible 项目。

总结

在此次实验中,你学习了在 RHEL 上构建复杂 Ansible Playbook 的基本技术。你从主机选择的基础知识开始,使用基本组名、通配符、排除项和逻辑运算符来精确地定位 inventory 文件中定义的主机。然后,重点转移到模块化,你在此练习了使用 include_tasks 进行动态包含和使用 import_tasks 进行静态包含,将大型 Play 分解为更易于管理和重用的组件。

在此基础上,你学习了如何通过 import_playbook 将单个 Playbook 链接在一起,以组合一个完整的多阶段工作流。实践过程包括安装 Ansible、创建项目结构,以及逐步将一个简单的 Playbook 重构为复杂的多文件结构。本次实验的最终环节是执行最终的复合 Playbook,并验证整个自动化工作流是否成功地针对正确的目标主机运行,展示了一种有组织且可扩展的自动化方法。