Linux-企业级自动化实用指南-全-

Linux 企业级自动化实用指南(全)

原文:annas-archive.org/md5/d8264d0c86b84dbcbeee64b1cb1b61b1

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

欢迎来到 Linux 企业自动化实操,这本书将是你了解一系列最有价值的流程、方法论和工具的指南,帮助你在企业级规模上精简并高效管理你的 Linux 部署。本书将提供你标准化 Linux 环境并进行大规模管理所需的知识和技能,使用包括 Ansible、AWX(Ansible Tower)、Pulp、Katello 和 OpenSCAP 在内的开源工具。你将学习如何创建标准化操作环境,以及如何使用 Ansible 定义、文档化、管理和维护这些标准。此外,你还将掌握安全加固标准,如 CIS 基准。在整个书中,将提供实际的操作示例供你自己动手尝试,帮助你编写自己的代码,并演示所讲解的原则。

本书适用对象

本书适用于任何需要设计、实施和维护 Linux 环境的人。它旨在吸引各种开源专业人士,从基础设施架构师到系统管理员,包括 C 级别的专业人士。假设读者已经具备实施和维护 Linux 服务器的能力,并且熟悉构建、修补和维护 Linux 服务器基础设施的相关概念。对 Ansible 及其他自动化工具的 prior 知识并非必需,但有一定帮助。

本书涵盖内容

第一章,在 Linux 上构建标准化操作环境,详细介绍了标准化操作环境的概念,这是本书中贯穿始终的核心概念,也是你开始本次学习旅程的必要理解。

第二章,使用 Ansible 自动化 IT 基础设施,提供了 Ansible 剧本的详细实操分解,包括清单、角色、变量和开发与维护剧本的最佳实践;这是一个速成课程,让你能学习到足够的 Ansible 知识,开始你的自动化之旅。

第三章,使用 AWX 精简基础设施管理,通过实际示例,探讨如何安装和使用 AWX(也可作为 Ansible Tower),从而围绕你的 Ansible 自动化基础设施构建良好的业务流程。

第四章,部署方法论,帮助你理解与 Linux 环境中大规模部署相关的各种方法,以及如何最大化这些方法的企业优势。

第五章,使用 Ansible 构建虚拟机模板进行部署,探讨了通过构建虚拟机模板在超管中以实际且动手的方式进行大规模部署 Linux 的最佳实践。

第六章,使用 PXE 启动进行自定义构建,介绍了 PXE 启动的过程,适用于服务器构建的模板化方法无法使用的情况(例如,仍在使用裸机服务器的场景),以及如何编写脚本通过网络构建标准的服务器镜像。

第七章,使用 Ansible 进行配置管理,提供了如何在服务投入使用后管理构建的实际示例,以确保一致性始终如一而不限制创新。

第八章,使用 Pulp 进行企业存储库管理,介绍了如何以受控方式执行修补,防止即使是最精心标准化的环境中也会重新引入不一致性,利用 Pulp 工具来实现这一点。

第九章,使用 Katello 进行修补,在介绍了 Pulp 工具的工作基础上,引入了 Katello,提供了更多对存储库的控制,同时提供了一个用户友好的图形用户界面。

第十章,在 Linux 上管理用户,提供了详细的用户账户管理方法,使用 Ansible 作为编排工具,并结合使用如 LDAP 目录等集中认证系统。

第十一章,数据库管理,介绍了如何使用 Ansible 自动化数据库的部署,并在 Linux 服务器上执行常规数据库管理任务。

第十二章,使用 Ansible 执行常规维护,探讨了 Ansible 在 Linux 服务器环境中可以执行的一些更高级的持续维护工作。

第十三章,使用 CIS 基准,深入探讨了 CIS 服务器加固基准以及如何在 Linux 服务器上应用它们。

第十四章,使用 Ansible 进行 CIS 加固,介绍了如何通过 Ansible 高效、可重复地在整个 Linux 服务器环境中部署安全加固策略。

第十五章,使用 OpenSCAP 审计安全策略,提供了如何安装和使用 OpenSCAP 持续审计 Linux 服务器的实操示例,因为安全标准可能会被恶意或其他良意的终端用户逆转。

第十六章,技巧与窍门,探讨了一些技巧和窍门,以帮助你在企业不断变化的需求面前,保持 Linux 自动化过程的顺畅运行。

为了最大程度地利用本书

为了跟随本书中的示例,建议你至少拥有两台 Linux 机器用于测试,尽管更多的机器会更有利于更全面地开发示例。这些可以是物理机或虚拟机——所有示例都是在一组 Linux 虚拟机上开发的,但同样适用于物理机器。在第五章,使用 Ansible 构建虚拟机模板进行部署中,我们利用 KVM 虚拟机上的嵌套虚拟化来构建 Linux 镜像。此操作的硬件要求在本章开始时列出。这将需要访问一台具有适当 CPU 的物理机器,或者一台支持嵌套虚拟化的虚拟化管理程序(例如,VMware 或 Linux KVM)。

请注意,本书中的某些示例可能会干扰你网络上的其他服务;如果存在此类风险,会在每章的开头进行说明。我建议你在一个隔离的测试网络中尝试这些示例,除非/直到你确信它们不会对你的操作产生任何影响。

尽管书中提到其他 Linux 发行版,但我们专注于两个关键的 Linux 发行版——CentOS 7.6(不过如果你有 Red Hat Enterprise Linux 7.6 的访问权限,也可以使用,它在大多数示例中也应该同样适用),以及 Ubuntu Server 18.04。所有测试机器都从官方 ISO 镜像构建,使用最小安装配置。

因此,当需要额外的软件时,我们将带你了解安装所需软件的步骤,以便你能够完成示例。如果你选择完成所有示例,你将安装如 AWX、Pulp、Katello 和 OpenSCAP 等软件。唯一的例外是 FreeIPA,它在第十章,在 Linux 上管理用户中提到。为你的企业安装目录服务器是一个庞大的主题,不幸的是需要的篇幅超出了本书的范围——因此,你可能需要自行探讨这个话题。

该文本假设你将在你的 Linux 测试机器之一上运行 Ansible,但实际上 Ansible 可以在任何安装了 Python 2.7 或 Python 3(版本 3.5 及以上)的机器上运行(Windows 可以作为控制机运行,但仅通过在较新版本的 Windows 上运行Windows 子系统 LinuxWSL)层来实现)。Ansible 支持的操作系统包括(但不限于)Red Hat、Debian、Ubuntu、CentOS、macOS 和 FreeBSD。

本书使用的是 Ansible 2.8.x.x 系列版本,尽管部分示例是针对在编写过程中发布的 Ansible 2.9.x.x 版本。Ansible 的安装说明可以在 https:/​/​docs.​ansible.​com/​ansible/​intro_​installation.​html 找到。

下载示例代码文件

你可以从你的帐户在www.packt.com下载本书的示例代码文件。如果你在其他地方购买了本书,可以访问www.packtpub.com/support,注册后,文件将直接通过电子邮件发送给你。

你可以按照以下步骤下载代码文件:

  1. www.packt.com登录或注册。

  2. 选择支持标签。

  3. 点击“代码下载”。

  4. 在搜索框中输入书名,并按照屏幕上的指示操作。

下载文件后,请确保使用最新版本解压文件或提取文件夹:

  • WinRAR/7-Zip for Windows

  • Zipeg/iZip/UnRarX for Mac

  • 7-Zip/PeaZip for Linux

本书的代码包也托管在 GitHub 上,地址是github.com/PacktPublishing/Hands-On-Enterprise-Automation-on-Linux。如果代码有更新,它将会在现有的 GitHub 仓库中更新。

我们还提供其他来自我们丰富书籍和视频目录的代码包,访问github.com/PacktPublishing/,查看它们吧!

下载彩色图片

我们还提供了一份 PDF 文件,里面有本书使用的截图/图表的彩色图片。你可以在此下载: static.packt-cdn.com/downloads/9781789131611_ColorImages.pdf

使用的约定

本书中使用了多种文本约定。

CodeInText:表示文本中的代码词汇、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟网址、用户输入和 Twitter 账号。例如:“首先,让我们创建一个名为loadmariadb的角色。”

代码块设置如下:

- name: Ensure PostgreSQL service is installed and started at boot time
  service:
    name: postgresql
    state: started
    enabled: yes

任何命令行输入或输出都以如下形式编写:

$ mkdir /var/lib/tftpboot/EFIx64/centos7

粗体:表示新术语、重要词汇或屏幕上显示的词汇。例如,菜单或对话框中的词语会以这种方式出现在文本中。举个例子:“从管理面板中选择系统信息。”

警告或重要说明以这种方式显示。

提示和技巧以这种方式显示。

联系我们

我们始终欢迎读者的反馈。

常见反馈:如果你对本书的任何部分有疑问,请在邮件主题中提及书名,并通过电子邮件联系我们:customercare@packtpub.com

勘误:虽然我们已尽力确保内容的准确性,但错误有时会发生。如果您在本书中发现了错误,我们将非常感激您能向我们报告。请访问 www.packtpub.com/support/errata,选择您的书籍,点击“勘误提交表单”链接,并填写相关信息。

盗版:如果您在互联网上发现我们作品的任何非法复制品,我们将非常感激您能提供该内容的地址或网站名称。请通过 copyright@packt.com 与我们联系,并附上该资料的链接。

如果您有兴趣成为作者:如果您在某个领域有专长,并且有兴趣编写或参与编写一本书,请访问 authors.packtpub.com

评论

请留下评论。在您阅读并使用本书后,为什么不在您购买本书的网站上留下评论呢?潜在读者可以看到并利用您的公正意见来做出购买决策,我们在 Packt 能了解您对我们产品的看法,而我们的作者也能看到您对他们书籍的反馈。谢谢!

如需了解更多关于 Packt 的信息,请访问 packt.com

第一部分:核心概念

本节的目标是理解本书将要介绍的系统管理基础和技巧。首先,我们将通过动手实践介绍 Ansible,这是本书中用于自动化以及诸如软件包管理和大规模高级系统管理等任务的工具。

本节包括以下章节:

  • 第一章,在 Linux 上构建标准操作环境

  • 第二章,使用 Ansible 自动化您的 IT 基础设施

  • 第三章,通过 AWX 简化基础设施管理

第一章:在 Linux 上构建标准操作环境

本章详细探讨了 Linux 中的标准操作环境(以下简称SOE)概念。虽然我们稍后会详细讨论,但简而言之,SOE 是指所有事物都以标准化的方式进行创建和修改的环境。例如,这意味着所有 Linux 服务器都以相同的方式构建,使用相同的软件版本。这个概念很重要,因为它使得管理环境变得更容易,并减少了管理人员的工作量。尽管本章的内容较为理论,但它为本书的其余部分奠定了基础。

我们将从探讨这种环境的基本定义开始,然后继续研究为什么希望创建这种环境是有利的。从这里出发,我们将探讨一些 SOE 的陷阱,为你提供如何在这种环境中保持正确平衡的视角,最后讨论如何将 SOE 融入到日常维护流程中。有效应用这一概念,可以在非常大的规模下高效且有效地管理 Linux 环境。

在本章中,我们将涵盖以下主题:

  • 理解 Linux 环境扩展的挑战

  • 什么是 SOE?

  • 探索 SOE 的好处

  • 知道何时偏离标准

  • SOE 的持续维护

理解 Linux 环境扩展的挑战

在深入探讨 SOE 的定义之前,让我们先来探索没有标准的 Linux 环境扩展所面临的挑战。对这一问题的探讨将帮助我们理解 SOE 的定义,同时也帮助我们为特定场景定义适当的标准。

非标准环境的挑战

需要考虑的是,许多企业在拥有技术资源(无论是 Linux 还是其他)的过程中所遇到的挑战,并非一开始就显现出来。事实上,在增长的早期阶段,许多系统和流程完全是可持续的,在下一部分中,我们将探讨环境增长的这一早期阶段,以便为理解大规模增长相关的挑战做铺垫。

非标准环境的早期增长

在许多公司中,Linux 环境的初始阶段往往没有任何标准化形式。通常,它们随着时间的推移而自然增长。最初的部署可能很小,可能仅覆盖一些核心功能,随着时间的推移和需求的增长,环境也随之扩展。熟练的系统管理员通常根据每台服务器手动进行更改,部署新服务,并根据业务需求扩展服务器规模。

这种有机增长是大多数公司选择的最小阻力路径——项目的截止日期通常很紧,而且预算和资源也很紧张。因此,当有一位熟练的 Linux 人员时,这位人员几乎可以协助完成所有需要的任务,从简单的维护任务到复杂应用栈的调试与部署。这节省了大量在架构设计上花费的时间和金钱,并且能够充分利用现有员工的技能,因为他们可以用来处理紧急问题和部署,而不是在架构设计上浪费时间。因此,简而言之,这是有道理的,作者在几家公司,甚至一些知名的跨国公司中都经历过这种情况。

非标准环境的影响

从技术角度更深入地看这个问题。Linux 有多种版本,也有多种应用程序执行(在高层次上)相同的功能,并且有多种方式解决给定的问题。例如,如果你想写一个任务脚本,你是写一个 shell 脚本、Perl、Python 还是 Ruby?对于某些任务,所有这些方式都能实现预期的最终结果。不同的人在解决问题时有不同的偏好方式和技术解决方案,通常会发现一个 Linux 环境是使用一种当时“月度风味”的技术或负责此环境的人最喜欢的技术构建的。就其本身而言,这没有什么问题,最初也不会引发任何问题。

如果有机增长带来一个根本性的问题,那就是:规模。当环境的规模相对较小时,手动进行更改并始终使用最新、最先进的技术是非常不错的,通常也会带来有趣的挑战,因此能够保持技术人员的积极性和价值感。对从事技术工作的人来说,保持技能的更新至关重要,因此,能够在日常工作中运用最新技术,常常是一个激励因素。

扩展非标准环境

当服务器数量达到数百,甚至数千(或更多!)时,这个有机过程就会崩溃。曾经有趣的挑战变成了繁重且单调的任务,甚至带来压力。新团队成员的学习曲线陡峭。新员工可能会发现自己面对一个不同技术堆栈的环境,需要学习大量不同的技术,可能还需要一段时间的培训才能真正发挥作用。长期服务的团队成员可能成为知识孤岛,如果他们离开公司,知识的流失可能会导致连续性问题。随着非标准环境的无序扩展,问题和故障会变得更多,故障排除也会变得漫长——当你试图实现 99.99%的服务正常运行时间协议时,每一秒的停机时间都至关重要,这显然不是理想的。因此,在下一节中,我们将探讨如何通过 SOE 来解决这些挑战。

解决挑战

从中我们意识到对标准化的需求。构建合适的 SOE 的关键在于以下几点:

  • 实现规模经济

  • 在日常操作中高效工作

  • 使所有参与者能够快速轻松地掌握并适应

  • 与企业不断增长的需求保持一致

毕竟,如果一个环境在定义上简洁明了,那么所有参与其中的人都更容易理解和使用它。反过来,这意味着任务能更快完成,且更加轻松。简而言之,标准化可以带来成本节约并提高可靠性。

必须强调,这只是一个概念,而非绝对的标准。虽然构建这种环境没有绝对对错之分,但有一些最佳实践。在本章中,我们将进一步探讨这一概念,并帮助你识别与 SOE 相关的核心最佳实践,以便在定义自己的环境时做出明智的决策。

让我们继续深入探讨这个问题。每个企业对其 IT 环境都有一定的需求,无论是基于 Linux、Windows、FreeBSD 还是其他技术。有时,这些需求是清晰且有文档支持的,而有时它们只是隐性的——也就是说,大家假设环境已经符合这些标准,但没有正式定义。这些需求通常包括以下几个方面:

  • 安全性

  • 可靠性

  • 可扩展性

  • 长期性

  • 支持性

  • 易用性

这些当然都是高层次的要求,且它们往往彼此交织。让我们更详细地探讨这些要求。

安全性

安全性由多个因素共同决定。让我们通过一些问题来了解其中涉及的因素:

  • 配置是否安全?

  • 我们是否允许使用弱密码?

  • 超级用户 root 是否允许远程登录?

  • 我们是否记录并审计所有连接?

在非标准环境下,你如何真正说这些要求在所有的 Linux 服务器上都得到了强制执行?要做到这一点,需要相当大的信任,假设所有服务器的构建方式相同,应用了相同的安全参数,并且没有人曾经重新审视该环境进行更改。简而言之,这需要相当频繁的审计以确保合规性。

然而,当环境已标准化,并且所有服务器都从相同的源构建或使用相同的自动化工具(我们将在本书后续内容中展示这一点)时,就更容易有信心地说你的 Linux 系统是安全的。

基于标准的环境当然并不意味着自动安全——如果存在导致构建过程中出现漏洞的问题,自动化意味着这种漏洞将会在整个环境中被复制!因此,了解你环境的安全要求,并小心实施这些要求,持续维护和审计环境,以确保安全水平得到保持,是非常重要的。

安全性还通过补丁得到强制执行,补丁确保你不会运行任何有漏洞的软件,这些漏洞可能允许攻击者侵入你的服务器。一些 Linux 发行版的生命周期比其他的要长。例如,Red Hat Enterprise Linux(以及 CentOS 等衍生版)和 Ubuntu LTS 版本都具有较长、可预测的生命周期,是你的 Linux 系统的不错选择。

因此,它们应该成为你标准的一部分。相反,如果使用了如 Fedora 这样的前沿 Linux 发行版,可能是因为它当时提供了所需的最新软件包,你可以确信它的生命周期会很短,并且不久后更新会停止,这样就会使你暴露于潜在的未修补漏洞中,并且需要升级到 Fedora 的新版本。

即使升级到 Fedora 的新版本,有时也会出现软件包被遗弃的情况——也就是说,这些包没有包含在新的发行版本中。这可能是因为它们被其他包取代了。无论原因如何,升级一个发行版到另一个可能会导致虚假的安全感,除非经过彻底研究,否则应避免这种做法。通过这种方式,标准化有助于确保良好的安全实践。

可靠性

许多企业希望其 IT 运维能够保持 99.99%(或更高)的正常运行时间。实现这一目标的一部分途径是使用稳健的软件,应用相关的 bug 修复以及明确定义的故障排除流程。这确保了在最坏的情况下发生停机时,停机时间最小化。

标准化在这里同样起到帮助作用——正如我们在上一节关于安全的讨论中所提到的,一个合适的操作系统选择能够确保你持续获得漏洞修复和更新,而且如果你知道企业需要供应商备份来确保业务连续性,那么选择一个有支持合同的 Linux 操作系统(例如 Red Hat 或 Canonical 提供的)是明智的选择。

同样地,当所有服务器都按照一个明确定义且被充分理解的标准构建时,对其进行修改应该能产生可预测的结果,因为每个人都知道自己在处理什么。如果所有的服务器构建方式略有不同,那么即便是出于好意的修改或更新,也可能会产生意想不到的后果,从而导致昂贵的停机时间。

再次强调标准化,即使发生最坏的情况,所有参与者应该知道如何解决问题,因为他们知道所有的服务器都基于某个基础镜像并拥有相同的配置。这种知识和信心能减少故障排除的时间,最终也能减少停机时间。

可扩展性

所有企业都希望自己的业务增长,而大多数时候,这意味着 IT 环境需要扩展以应对不断增加的需求。在一个服务器构建方式非标准化的环境中,扩展环境就成了一个更大的挑战。

例如,如果进行横向扩展(向现有服务添加更多相同的服务器),新服务器应该与现有服务器具有相同的配置。如果没有标准化,第一步是弄清楚初始服务器集群是如何构建的,然后克隆这些服务器并进行必要的更改,创建更多独特的服务器。

这个过程有些繁琐,而在标准化环境下,调查步骤完全不必要,水平扩展也变成了一个可预测、可重复的日常任务。它还确保了更高的可靠性,因为在新服务器中如果漏掉了非标准配置项,应该不会产生意外的结果。人类是令人难以置信的智能生物,能够把人类送上月球,但同样也能忽视配置文件中的一行。标准化的目的是减少这种风险,从而使得在使用经过深思熟虑的操作系统模板进行环境扩展时,无论是纵向扩展还是横向扩展,都变得既快速又高效,我们将在本章中进一步探讨这一概念。

长期可用性

有时在部署某个服务时,需要特定的软件版本。比如,我们以一个运行在 PHP 上的 Web 应用为例。假设你的企业因为历史原因,标准化使用的是 CentOS 6(或 RHEL 6)。该操作系统默认提供 PHP 5.3 版本,这意味着如果你突然需要运行一个仅支持 PHP 7.0 及以上版本的应用,你就得想办法如何托管它。

一个显而易见的解决方案可能是推出 Fedora 虚拟机镜像。毕竟,它与 CentOS 和 RHEL 共享类似的技术,并且包含了更新的库。作者在多个角色中都有直接使用这种解决方案的经验!不过,让我们从更大的视角来看待这个问题。

RHEL(以及基于此的 CentOS)的生命周期大约为 10 年,具体取决于你购买的时间点。在企业环境中,这是一个宝贵的选择——这意味着你可以确保任何构建的服务器在构建后的 10 年内(以及可能更长时间的延长生命周期支持)都能获得补丁和支持。这与我们之前提到的安全性、可靠性和可支持性(在下文中)相契合。

然而,在 Fedora 上构建的任何服务器的生命周期大约是 12-18 个月(取决于 Fedora 发布周期)——在企业环境中,必须在例如 12-18 个月后重新部署服务器,显然是一个不必要的麻烦。

这并不是说在 Fedora 或任何其他快速发展的 Linux 平台上部署没有必要——只是想说明,在安全性和可靠性至关重要的企业环境中,你不太可能想要一个生命周期短的 Linux 平台,因为短期的收益(更新的库支持)将在 12-18 个月后被缺少更新和需要重新构建/升级平台的痛苦所取代。

当然,这在很大程度上取决于你对基础设施的处理方式——有些企业采用类似容器的方式管理服务器,并在每次新的软件发布或应用部署时重新部署它们。当你的基础设施和构建标准由代码定义(如 Ansible)时,完全可以做到这一点,且对日常操作的影响最小,且不太可能有任何单一的服务器存在足够长的时间,导致操作系统过时或不再受支持。

归根结底,选择权在你手中,你必须确定哪条路径能为你提供最大的商业利益,而又不会使你的运营面临风险。标准化的一部分是做出合理的、理智的技术决策,并在可行的情况下采纳这些决策,你的标准可能包括频繁的重建,从而可以使用像 Fedora 这样的快速发展操作系统。同样,你也可能决定标准是服务器将有较长的生命周期,并在原地升级,在这种情况下,你最好选择像 Ubuntu LTS 版本或 RHEL/CentOS 这样的操作系统。

在下一个部分中,我们将更详细地探讨 SOE 如何促进支持性这一概念。

可支持性

正如我们已经讨论过的,拥有标准化的环境带来了两个好处。第一个好处是,精心选择的平台意味着较长的供应商支持生命周期。反过来,这意味着无论是来自供应商(例如 RHEL 产品)的长期支持,还是来自社区(例如 CentOS)的长期支持。某些操作系统,如 Ubuntu Server,可以选择通过社区支持或直接从 Canonical 获得付费合同支持。

然而,可支持性不仅仅意味着来自供应商或 Linux 社区的支持。请记住,在企业中,你的员工是前线支持,外部人员介入之前,员工就已经在处理问题了。现在,想象一下你有一支优秀的 Linux 团队,并且他们面对的是由 Debian、SuSe、CentOS、Fedora、Ubuntu 和 Manjaro 组成的服务器环境。它们之间有相似之处,但也有大量的差异。它们之间有四种不同的软件包管理器来安装和管理软件包,这只是其中的一个例子。

虽然完全可以支持,但这对你的员工提出了更大的挑战,这意味着对于任何加入公司的人,你需要一套广泛且深入的 Linux 经验——或者需要一个广泛的入职过程来帮助他们快速上手。

在一个标准化的环境中,你可能会使用多个操作系统,但如果你能够通过选择例如 CentOS 7 和 Ubuntu Server 18.04 LTS 来满足所有的需求,并且知道你在未来几年内选择是有保障的,那么你立刻就能减少 Linux 团队的工作负担,让他们有更多时间去创造性地解决问题(例如,通过 Ansible 自动化解决方案!),而不是花时间去琢磨操作系统之间的细微差别。正如我们所讨论的,在出现问题时,他们会更熟悉每个操作系统,因此需要花费更少的时间来调试,从而减少停机时间。

这引出了一个关于大规模易用性的话题,我们将在下一节中对此进行概述。

易用性

这一最终类别与前两个类别有很大重叠——也就是说,更标准化的环境使得给定的员工更容易掌握。这样就自动促进了我们之前讨论的所有好处,包括减少停机时间、简化员工招聘和入职等。

在列出了 SOE 帮助解决的挑战之后,我们将在下一节中继续探讨这种环境的结构,从技术角度理解它。

什么是 SOE?

现在我们已经探讨了 SOE 对企业重要性的原因,并且在高层次上了解了这些问题的解决方案,让我们详细了解 SOE。我们将从定义 SOE 本身开始。

定义 SOE

让我们从一个更实际的角度来看一下这个问题。正如我们已经提到的,SOE 是一个概念,而非绝对的标准。从最简单的层面来看,它是一个在公司多个服务器上部署的通用服务器镜像或构建标准。在这里,所有必需的任务都以一种已知且文档化的方式完成。

首先是基础操作系统——正如我们讨论的那样,选择 Linux 发行版有成百上千种。有些在系统管理方面非常相似(例如,Debian 和 Ubuntu),而有些则有显著不同(例如,Fedora 和 Manjaro)。举个简单的例子,假设你想在 Ubuntu 18.04 LTS 上安装 Apache Web 服务器——你需要输入以下命令:

# sudo apt-get update
# sudo apt-get install apache2

现在,如果你想在 CentOS 7 上做相同的事情,你需要输入以下命令:

# sudo yum install httpd

如你所见,这些命令之间没有任何共同之处——即便最终结果在两种情况下都是安装 Apache,命令的包名也完全不同。在小规模环境下这不是问题,但当服务器数量增多时,管理这样一个环境的复杂度也会随之增加。

基础操作系统只是开始。我们上面的例子是安装 Apache,但我们也可以安装 nginx 或者 lighttpd。毕竟,它们也是网页服务器。

然后是配置。你是否希望用户能够通过 SSH 以 root 身份登录?你是否需要某种级别的日志记录以便审计或调试?你需要本地认证还是集中式认证?这些问题不胜枚举,正如你所看到的,如果不加以控制,它们可能会发展成一场巨大的头痛。

这就是 SOE 的作用所在。它实际上是一种规范,从高层来看,它可能包含以下内容:

  • 我们的标准基础操作系统是 Ubuntu 18.04 LTS。

  • 我们的标准网页服务器将是 Apache 2.4。

  • 启用了 SSH 登录,但仅限于具有 SSH 密钥的用户,并且禁止 root 登录。

  • 所有用户登录必须进行记录并归档,以供审计使用。

  • 除了一些本地的紧急账户外,所有账户必须集中管理(例如,通过 LDAP 或 Active Directory)。

  • 我们的企业监控解决方案必须集成(例如,必须安装并配置 Nagios NCPA 代理,以便与我们的 Nagios 服务器进行通信)。

  • 所有系统日志必须发送到企业的中央日志管理系统。

  • 必须对系统进行安全加固。

上述只是一个示例,绝非完整的内容;然而,它应该能让你初步了解 SOE 的高层次定义。随着本章的推进,我们将深入探讨这一主题,并提供更多示例,以帮助清晰地定义 SOE。

知道需要包含什么内容

在我们继续之前,让我们更详细地了解一下环境中需要包含的内容。在前面的章节中,我们概述了 SOE 的一个非常简化的定义。任何良好的 SOE 操作流程的一部分就是拥有一个预定义的操作系统构建,可以在任何时刻进行部署。有多种方法可以实现这一点,我们将在本书后续章节讨论这些方法——然而,暂时假设我们之前提到的 Ubuntu 18.04 LTS 基础镜像已经构建完成。我们在这个标准构建中应该集成哪些内容呢?

例如,我们知道我们的登录策略将在整个组织中实施——因此,在创建镜像时,/etc/ssh/sshd_config必须定制为包含PermitRootLogin noPasswordAuthentication no。在后期部署配置中执行这个步骤没有意义,因为每次部署都需要执行这一操作。简单来说,这样做效率低下。

我们的操作系统镜像还需要考虑一些重要的自动化问题。我们知道 Ansible 本身是通过 SSH 进行通信的,因此我们知道需要某种类型的凭证(很可能是基于 SSH 密钥)才能让 Ansible 在所有已部署的服务器上运行。在你能够执行任何自动化操作之前,如果需要手动将 Ansible 凭证分发到每一台机器上,是没有意义的,因此考虑 Ansible 使用的认证方式非常重要(例如,基于密码或 SSH 密钥),并且在构建镜像时创建相应的账户和凭证。实现这一点的具体方法将取决于你们公司安全标准,但我建议可以考虑以下解决方案:

  • 在标准镜像上创建一个本地账户,供 Ansible 进行身份验证

  • 为该账户授予适当的 sudo 权限,确保能够执行所有预定的自动化任务

  • 为这个账户设置本地密码,或者将 Ansible 密钥对中的 SSH 公钥添加到你创建的本地 Ansible 账户的authorized_keys文件中

这样做当然会带来一些安全风险。最有可能的是,Ansible 需要完全访问服务器上的 root 权限,才能有效地执行你要求的所有自动化任务,因此,如果凭证被泄露,这个 Ansible 帐户可能会成为一个后门。建议尽量让尽可能少的人访问凭证,并使用像 AWX 或 Ansible Tower 这样的工具(我们将在第三章,通过 AWX 简化基础设施管理中进行探讨)来管理凭证,从而防止不当人员获取凭证。你几乎肯定还会希望启用对 Ansible 帐户执行的所有活动进行审计,并将这些活动记录到某个中央服务器上,这样你就可以检查是否有任何可疑活动,并根据需要进行审计。

从用户帐户和身份验证转移开,考虑一下Nagios 跨平台代理NCPA)。我们在示例中知道,所有部署的服务器都需要进行监控,因此可以认为 NCPA 代理必须安装,并且令牌必须被定义,以便它可以与 Nagios 服务器进行通信。同样,在标准映像部署后,没必要在每台服务器上都执行这个操作。

那么,关于 Web 服务器呢?拥有一个标准是明智的,因为这意味着所有负责环境的人都能熟悉这项技术。这使得管理变得更容易,尤其对自动化非常有利,正如我们将在下一节中看到的。然而,除非你只部署运行 Linux 的 Web 服务器,否则这不应该作为标准构建的一部分。

作为一种基本原则,标准构建应尽可能简单且轻量。没有必要在它们上运行额外的服务,这些服务占用内存和 CPU 周期,尤其是在它们是冗余的情况下。同样,未配置的服务会增加潜在攻击者的攻击面,因此出于安全原因,建议将它们排除在外。

简而言之,标准构建应该只包含那些将对每台部署的服务器都通用的配置和/或服务。这种方法有时被称为恰到好处的操作系统,简称JeOS,它是 SOE 的最佳起点。

理解了 SOE 的基本原则后,我们将在下一节中更详细地探讨 SOE 为企业带来的好处。

探索 SOE 的好处

到目前为止,你应该已经对 SOE 有了一些了解,并且知道它如何为 Linux 环境带来规模经济和更高的效率。现在,让我们在此基础上,详细了解标准化的重要性示例。

在 Linux 环境中,SOE 的示例好处

说 Linux 环境中存在共性,就是说组成它的服务器都有一些共享的属性和特征。例如,它们可能都建立在 Ubuntu Linux 之上,或者它们可能都使用 Apache 作为 Web 服务器。

我们可以通过一个例子来探索这个概念。假设你有 10 台 Linux Web 服务器,它们都在一个负载均衡器后面,且它们都在提供简单的静态内容。所有一切运行正常,但随后需要进行配置更改。也许这是为了更改每台 Web 服务器的文档根目录,以指向由其他团队部署到这些服务器的新代码版本。

作为负责人,你知道因为整体解决方案是负载均衡的,所以所有服务器应该提供相同的内容。因此,配置更改将在每台服务器上都需要进行。这意味着如果你手动操作,就需要做 10 次配置更改。

当然,你可以手动完成这个操作,但这将是一个繁琐的过程,显然对于一个熟练的 Linux 管理员来说,这并不是最好的时间利用方式。它也容易出错——可能在 10 台服务器中的某一台上输入了错误的内容,且没有被发现。或者管理员可能被其他地方的故障打断,只对部分服务器的配置进行了更改。

更好的解决方案是编写一个脚本来进行更改。这正是自动化的基础,而且几乎可以肯定,运行一个脚本一次以更改 10 台服务器的配置,比手动重复更改 10 次要更高效。不仅效率更高,而且如果一个月后需要做相同的更改,这个脚本可以在仅做最小调整的情况下重复使用。

现在,让我们再增加一点复杂性。如果,出于某些未知原因,五台 Web 服务器使用的是基于 CentOS 7 的 Apache,另外五台则使用的是基于 Ubuntu 18.04 LTS 的 nginx,会发生什么呢?最终结果其实是相同的——从基本层面来看,它们都是 Web 服务器。然而,如果你想在 CentOS 7 上的 Apache 中更改文档根目录,你需要做如下操作:

  1. 找到/etc/httpd/conf.d中的适当配置文件。

  2. DocumentRoot参数进行所需的更改。

  3. 使用systemctl reload httpd.service重新加载 Web 服务器。

如果你需要在 Ubuntu 18.04 LTS 上的 nginx 中做相同的更改,你需要做如下操作:

  1. 找到/etc/nginx/sites-available中的正确配置文件。

  2. root参数进行所需的更改。

  3. 确保使用a2ensite命令启用站点配置文件——否则,Apache 将不会实际看到配置文件。

  4. 使用systemctl reload apache2.service重新加载 Web 服务器。

从这个相当简单(尽管是人为设定的)例子中可以看出,缺乏共性是自动化的敌人。为了应对这种情况,你需要做如下操作:

  1. 检测每台服务器上的操作系统。这本身并不简单——没有一种方法可以检测 Linux 操作系统,因此你的脚本需要依次进行一系列检查,包括以下内容:

    1. /etc/os-release 的内容(如果存在)

    2. lsb_release 的输出(如果已安装)

    3. /etc/redhat-release 的内容(如果存在)

    4. /etc/debian_version 的内容(如果存在)

    5. 根据需要的其他操作系统特定文件,如果前面的检查没有产生有意义的结果

  2. 在不同的目录中运行不同的修改命令,以实现之前讨论的更改。

  3. 运行不同的命令来重新加载 Web 服务器,正如之前所详细描述的那样。

因此,脚本变得复杂,编写和维护变得更加困难,显然也更难以保证其可靠性。

虽然这个特定的例子在现实生活中不太可能发生,但它确实突出了一个重要的观点——当环境被构建为符合某个标准时,自动化的实现要容易得多。如果决定所有的 Web 服务器都基于 CentOS 7,运行 Apache 2,并且站点配置文件以服务名称命名,那么我们的自动化就变得更加简单。事实上,你甚至可以运行一个简单的sed命令来完成这个更改;例如,假设新的 Web 应用程序部署到了/var/www/newapp

# sed -i 's!DocumentRoot.*!DocumentRoot /var/www/newapp!g' /etc/httpd/conf.d/webservice.conf
# systemctl reload httpd.service

完全不需要环境检测——只需要两个简单的 Shell 命令。这可以成为一个非常简单的自动化脚本的基础,既可以在 10 台服务器中依次运行,也可以通过 SSH 远程运行。无论哪种方式,我们的自动化任务现在变得非常简单,充分展示了公共性的的重要性。重要的是,SOE 本身就提供了这种公共性。然而,缺乏公共性不仅使得自动化变得困难——它还会妨碍测试,通常会扭曲测试结果,因为如果环境不同,测试结果可能不具代表性。

在本章的下一部分,我们将基于这些知识,展示 SOE 如何有利于软件测试过程。

SOE 对软件测试的好处

我在许多环境中看到的一个常见问题是:一个新的软件部署在隔离的预生产环境中经过成功测试,但在发布到生产环境后却不能正常工作。这个问题往往可以追溯到生产环境和预生产环境之间的根本差异,因此很明显,要使测试有效,两个环境必须尽可能相似。

实际上,像 Docker 这样的容器平台旨在解决的问题之一就是这个,因此可移植性是容器环境的核心特性。在 Docker 上部署的代码是建立在一个容器镜像之上的,简单来说,这是一个精简的操作系统镜像(记得 JeOS 吗?)。实际上,这只是在容器中运行而不是在裸金属服务器或虚拟机上运行的一个非常小的 SOE。然而,值得考虑的是,如果通过环境标准化实现可移植性是容器技术的一个关键特性,那么我们不应该在不考虑基础设施的情况下努力实现这一点吗?

毕竟,如果生产服务器的配置与预生产服务器不同,那么测试的有效性又有多大保证呢?如果预生产环境是建立在 CentOS 7.6 上,而生产环境却落后于 CentOS 7.4,那么你真的能确保在一个环境中的成功测试结果在另一个环境中也能保证吗?在理论上,应该可以,但由于环境中软件和库版本的根本差异,这永远无法保证。这甚至还没有考虑到配置文件和安装软件可能存在的差异。

因此,SOE 在这里可以起到帮助的作用——如果所有环境都按照相同的标准构建,那么理论上它们都应该是相同的。那些眼尖的人会注意到前面一句中使用“应该”这个词,这是有充分理由的。SOE 在定义测试失败解决方案方面迈出了重要一步,但它们并非全部内容。

只有当没有人修改它时,环境才能保持标准化,如果所有用户都拥有管理级别的权限,那么某人(无论出于善意还是其他原因)登录并进行更改,使环境偏离标准就非常容易了。

解决这个问题的答案是自动化——SOE 不仅促进和实现自动化,它们也依赖于它来维持最初所需的标准化水平。这两者直接支持彼此,并且理想情况下应该成为不可分割的伙伴——SOE 是环境本身的定义,而自动化则提供标准的实施、执行和审计。事实上,这本书的核心前提就是——环境应尽可能地标准化,并且尽可能多的变更应该是自动化的。

本书的重点将放在这个方程式的自动化方面,除了遵循本章概述的原则之外,采用的标准将对每个环境都是独特的,本书并不旨在低级别确定它们。与之前的例子一起工作,Apache 和 nginx 都有各自的好处,适合一个用例的可能并不适合另一个用例。

操作系统也是如此——有些组织可能依赖于 Red Hat Enterprise Linux 所提供的支持包,而其他组织则不需要此支持包,但需要例如 Fedora 提供的前沿技术。定义标准并没有绝对对错,只要它满足所支撑服务的需求即可。到目前为止,我们非常注重共同性和标准化;然而,总会有一些特殊情况需要采用替代方案。在下一节中,我们将讨论如何判断何时应该偏离标准。

知道何时偏离标准

标准化的好处很容易被过度宣扬,尽管它们无疑是自动化有效性的必要条件。然而,像任何事物一样,标准化也可以被推得过头。例如,完全没有必要在 2019 年仅仅因为某个时间点曾被定义为标准而在 Red Hat Enterprise Linux 5.7 上构建服务器(该版本现在已到达生命周期终点,不再支持或更新)。同样,软件供应商有时会在某些特定的 Linux 发行版或应用堆栈上对其产品进行认证,除非软件运行在该生态系统内,否则不会提供支持。

这些都是需要偏离 SOE 的情况,但必须以受控的方式进行。例如,如果企业已经在 Ubuntu 18.04 LTS 上构建了其 Linux 服务器环境,然后购买了一个仅在 RHEL 7 上经过认证的新软件堆栈,那么显然需要构建 RHEL 7。但如果可能的话,这些应该成为新标准的一部分,并成为一个次要 SOE。

例如,如果将 CIS 安全加固基准应用于 Ubuntu SOE,那么同等的基准也应该应用于 RHEL。同样,如果企业已经在 nginx 上实现了标准化,那么在环境中应该继续使用它,除非有令人信服的理由不使用(提示:令人信服的理由不是它“新”和“炫酷”,而是它解决了一个实际问题或以某种方式在可衡量的方面改进了某些东西)。

这导致企业从一个 Linux SOE(标准操作环境)转向两个,虽然这仍然完全可以管理,并且无疑比回到那些阻碍有效自动化的有机增长方法要好。

简而言之,要预期会有偏离,并且不要害怕它们。相反,应该处理它们,并利用这些需求来扩展你的标准,但在可能的情况下坚持标准。SOE 对于每个人来说都是一种平衡——一方面,它们带来了规模优势,使得自动化变得更容易,并减少了对新员工的培训时间(因为所有服务器的构建和配置大致相同);但如果过于严格地应用,它们也可能阻碍创新。它们不能作为以“因为一直都是这么做的”为理由的借口。

总会有充分的理由偏离标准;只需要寻找它所带来的业务利益,无论是供应商支持、较低的资源需求(从而节省电力和金钱)、更长的支持周期,还是其他原因。尽量避免仅仅因为一种新技术看起来光鲜亮丽而去偏离标准。只要你牢记这一点,你就能在偏离标准时做出明智的决策。在本章的下一部分,我们将探讨 SOE 的持续维护。

SOE 的持续维护

虽然本书后面会更详细地讨论补丁和维护,但在此提及它是因为它与常规性和偏差的讨论密切相关。

如果没有其他原因,你至少需要为你的 Linux 环境打补丁。仅仅出于安全考虑,这也是一项理所当然且良好的实践,即使是在隔离环境中也是如此。假设你的环境完全由虚拟机组成,而你早些时候决定将 CentOS 7.2 作为标准。你创建了一个虚拟机,执行了所有必要的配置步骤将其变成 SOE 镜像,然后将其转换为虚拟化环境中的模板。这就成为了你的黄金构建。到目前为止,一切顺利。

然而,CentOS 7.2 发布于 2015 年 12 月,距今已经快四年了。如果你今天部署这样的镜像,首先需要做的就是为其打补丁。根据构建定义(以及包含的包数量),这可能意味着你需要下载一 GB 或更多的包,以使其符合最新标准,并确保修补所有已发现的漏洞,并完成所有必要的 bug 修复。

显然,如果你在大规模操作中进行这种操作,这是低效的——每台新服务器都需要通过网络(或者更糟,如果没有内部镜像,还需要通过互联网)下载所有数据,并且在应用补丁时会消耗大量的 I/O 时间和 CPU 时间,而在此期间服务器无法用于任何有意义的任务。如果你每隔几个月才部署一台服务器,你可能还能忍受这个过程。如果你更频繁地部署服务器,这将浪费大量宝贵的时间和资源。

因此,除了对你的环境本身进行持续维护外,持续维护你的标准同样重要。在 2019 年,更新你的 CentOS 构建到 7.6 是有意义的。至少,你的持续维护计划应该包括定期更新黄金构建

本书稍后将详细讨论如何执行这一过程。然而,对于那些急于了解的人来说,这可能简单到只需启动虚拟机镜像,执行更新,清理它(例如,删除克隆模板时可能重复的 SSH 主机密钥),然后从中创建一个新模板。显然,如果自上次维护周期以来对 SOE 进行了任何更改,那么这些更改也可以纳入其中。

您应该预期您的 SOE 会随着时间的推移而发展——或许在这个问题上会花费很多篇幅,但创建和维护标准与过于僵化之间需要有一个重要的平衡。您必须接受有时您需要偏离这些标准,如我们在前一节中讨论的那样,并且随着时间推移,这些标准会不断演变。

总而言之,SOE 应该成为您常规 IT 流程的一部分;如果正确使用,它们不会妨碍创新——相反,它们通过将时间返还给使用它们的人,积极支持创新,确保他们花更少的时间在繁琐、重复的任务上,从而有更多的时间评估新技术和寻找更好的工作方式。这毕竟是自动化的关键好处之一,而 SOE 正是直接支持这一点。

总结

SOE(标准化操作环境)是几乎所有环境中技术流程的宝贵补充。它们需要在设计工作和定义标准方面花费一些前期时间,但随着它支持环境的高效自动化,这段时间在后期得到了充分的回报,从而真正为负责环境管理的人们节省了时间,让他们有更多时间评估新技术、寻找更高效的工作方式,并在总体上进行创新。

在本章中,您了解了 SOE 的基本定义。您探讨了它们为几乎所有对规模要求较高的 Linux 环境带来的好处,它们如何支持自动化,以及如何在确保不会过于僵化并妨碍发展的情况下,何时以及如何偏离标准。最后,您了解了持续维护的重要性,包括作为持续维护周期一部分的标准维护。

在下一章中,我们将探讨如何将 Ansible 作为一个有效的自动化框架应用于您的 Linux 环境。

问题

  1. SOE 的缩写代表什么?

  2. 为什么您会选择支持周期长的操作系统,如 CentOS,而不是支持周期较短、发布频率较高的操作系统,如 Fedora?

  3. 如果您偏离了为环境定义的标准,应该怎么办?

  4. 列出三个将 Linux 环境扩展到企业规模时面临的挑战。

  5. 列举三个 SOE 为企业中的 Linux 带来的好处。

  6. SOE 如何帮助减少企业中的培训需求?

  7. 为什么 SOE 有助于提高 Linux 环境的安全性?

进一步阅读

第二章:使用 Ansible 自动化你的 IT 基础设施

虽然在 Linux 上自动化任务有很多方法,但有一种技术在大规模自动化方面脱颖而出,那就是 Ansible。虽然完全可以通过 shell 脚本轻松地自动化某些任务,但这种方法有许多缺点,其中最重要的一点是,shell 脚本在大规模环境中的扩展性较差。应该指出,虽然还有其他自动化工具,但 Ansible 利用本地通信协议(例如,Linux 上的 SSH 和 Windows 上的 WinRM),因此它是完全无代理的!这使得将其部署到现有环境中变得简单。虽然 Ansible 的自动化是一个庞大且深入的主题,但本章旨在涵盖基础知识并快速启动,让你即使没有任何经验,也能够跟随本书中的自动化示例进行操作。事实上,这也是 Ansible 在过去几年内快速普及的原因之一——尽管它功能强大,但入门和自动化你的第一个任务非常简单。

在本章中,我们将介绍以下 Ansible 主题:

  • 探索 Ansible 剧本结构

  • 探索 Ansible 中的清单

  • 了解 Ansible 中的角色

  • 了解 Ansible 变量

  • 了解 Ansible 模板

  • 将 Ansible 与 SOE(标准操作环境)结合起来

技术要求

本章包含基于以下技术的示例:

  • Ubuntu Server 18.04 LTS

  • CentOS 7.6

  • Ansible 2.8

要运行这些示例,你需要访问一台运行本章列出的操作系统之一的服务器或虚拟机,并且还需要能够访问 Ansible。请注意,本章中给出的示例可能具有破坏性(例如,它们涉及安装文件和软件包),如果按原样运行,建议仅在隔离的测试环境中执行。

一旦确认你有一个安全的环境可以进行操作,我们就开始查看如何使用 Ansible 安装新软件包。

本章讨论的所有示例代码都可以从 GitHub 获取:github.com/PacktPublishing/Hands-On-Enterprise-Automation-on-Linux/tree/master/chapter02

探索 Ansible 剧本结构

启动并运行 Ansible 是一个简单的过程,且大多数主要 Linux 发行版、FreeBSD 以及几乎所有支持 Python 的平台都有相应的安装包。如果你安装了支持 Windows 子系统 Linux (WSL) 的最新版本 Microsoft Windows,Ansible 甚至可以在这个环境中安装并运行。

请注意,在写作时没有本地的 Windows 包。

官方 Ansible 文档为所有主要平台提供了安装文档。请参考 docs.ansible.com/ansible/latest/installation_guide/intro_installation.html

本章中的示例将在 Ubuntu Server 18.04.2 上运行。尽管 Ansible 可以跨多个不同平台工作,但大多数示例也应该可以在其他操作系统上运行(或者最多只需做最小的适配)。

根据官方安装文档,执行以下命令以在我们的演示系统上安装 Ansible 的最新版本:

$ sudo apt-get update
$ sudo apt-get install software-properties-common
$ sudo apt-add-repository --yes --update ppa:ansible/ansible
$ sudo apt-get install ansible

如果一切顺利,您应该能够通过运行以下命令查询 Ansible 二进制文件的版本:

$ ansible --version

输出应该类似于以下内容:

恭喜!现在 Ansible 已经安装完成,让我们来看看运行您的第一组 Ansible 任务的基本操作,这些任务被称为 Playbook。为了运行这些任务,您需要具备以下三个条件:

  1. 一个配置文件

  2. 一个清单

  3. Playbook 本身

当 Ansible 安装时,通常会在 /etc/ansible/ansible.cfg 路径下安装一个默认的配置文件。通过这个文件,您可以更改许多高级功能,并且可以通过多种方式覆盖该文件。对于本书,我们几乎将完全使用默认设置,这意味着现在只需了解这个文件的存在即可。

要了解更多关于 Ansible 配置文件的信息,可以参考这篇文档,这是一个很好的起点,链接地址为 docs.ansible.com/ansible/latest/installation_guide/intro_configuration.html

没有清单,Ansible 是无法工作的。清单是一个文本文件(或脚本),为 Ansible 二进制文件提供要操作的主机名列表,即使只是本地主机。我们将在本章的下一部分详细介绍清单,因为它们在我们的自动化旅程中将变得非常重要。现在,您会发现,在大多数 Linux 平台上,作为 Ansible 安装的一部分,通常会在 /etc/ansible/hosts 路径下安装一个示例清单文件。当清单文件为空(或仅包含注释,例如示例文件中那样)时,Ansible 默认只操作本地主机。

最后,但绝对不容忽视的是,你必须有一个 playbook 来执行服务器(或多个服务器)。现在,让我们通过一个简单的例子来实现一个可以在 Ansible 中运行的 playbook。Ansible playbook 是用 YAML 编写的(YAML 是一个递归首字母缩略词,表示 YAML Ain't Markup Language),由于 YAML 易于阅读——事实上,这也是 Ansible 的核心优势之一——因此,playbook 可以很容易地被有最少 Ansible 技能的人理解,并且能够轻松地应用或修改。

如果你不习惯用 Python 或 YAML 编写代码,那么你需要了解的一点是:YAML 文件在编写 playbook 时非常讲究缩进。与许多高级语言使用括号或大括号来定义代码块,并用分号标识行结束不同,YAML 使用缩进级别本身来决定你在代码中的位置,以及它如何与周围的代码相关联。缩进总是通过空格来实现——绝对不要使用制表符(tab)。即使缩进在肉眼看起来是相同的,YAML 解析器也不会认为它们相同。

考虑以下这段代码块:

---
- name: Simple playbook
  hosts: localhost
  become: false

这是一份 Ansible playbook 的开头。Ansible 的 YAML 文件总是以三个短横线(---)开始,并且没有缩进。接下来,我们有一行定义了 play 的开始,用一个短横线(-)表示,也没有缩进。需要注意的是,Ansible 的 playbook 可以包含一个或多个 plays,每个 play 都是(从基础层面来看)要在一组给定主机上执行的任务集合。这个 playbook 中的这一行指定了 play 的 name。虽然 name 关键字在大多数情况下是可选的,可以省略,但强烈建议在所有 play 定义中都包含它(就像我们在这里做的一样),并且每个任务也应如此。这样做可以显著提高 playbook 的可读性,并帮助新手快速理解,从而提升效率并降低新手的入门门槛,正如我们在前一章所讨论的那样。

这段代码的第三行告诉 Ansible 应该在哪些 hosts 上执行任务。在这个例子中,我们只会在 localhost 上执行。第四行告诉 Ansible 不需要以超级用户(root)身份执行任务,因为该任务不需要管理员权限。有些任务,例如重启系统服务,必须以超级用户身份执行,在这种情况下,你会指定 become: true。请注意前两行缩进的两个空格——这告诉 YAML 解析器这些行属于第二行定义的 play。

现在,让我们在前一段代码的下面添加两项任务:

  tasks:
    - name: Show a message
      debug:
        msg: "Hello world!"

    - name: Touch a file
      file:
        path: /tmp/foo
        state: touch

tasks关键字定义了 play 的结束以及我们希望执行的实际任务的开始。注意,它仍然被缩进了两个空格,这告诉解析器它仍然是我们之前定义的 play 的一部分。然后,我们再次增加缩进,表示下一行是tasks块的一部分。

到现在,你会看到一个熟悉的模式开始形成。每当一行代码成为前一个语句的一部分时,我们会增加两个空格的缩进。每个新项都以一个破折号(-)开始,因此我们之前的代码块包含了两个任务。

第一个任务使用name关键字,值为Show a message,作为文档说明(可以类比其他编程语言中的注释),并使用一个叫做Ansible 模块的东西。模块是 Ansible 用来执行特定任务的预定义代码块。这里包含的debug模块主要用于显示消息或变量内容,从而用于 playbook 调试。我们通过进一步缩进msg参数两格,将我们希望在运行 playbook 时打印的消息传递给debug模块。

第二个任务使用了nameTouch a file关键字,并使用file模块去触摸位于/tmp/foo的文件。当我们运行这个 playbook 时,输出应该类似于这样:

对于大多数简单的 playbook,任务是按顺序从上到下执行的,这使得执行顺序可预测且易于管理。这就是全部!你已经编写并执行了第一个 Ansible playbook。你会发现它非常简单,且将它与单一测试系统集成几乎不需要什么工作。现在,对于这样一个简单的示例,一个有效的问题是:既然两行 shell 脚本就能实现同样的功能,为什么还要花那么大力气使用 Ansible? 以下代码块展示了一个 shell 脚本的示例:

echo "Hello World!"
touch /tmp/foo

使用 Ansible 的第一个原因是,虽然这个例子非常简单且易于理解,但随着脚本所需任务的复杂化,它们变得更加难以阅读,并且需要懂得 shell 脚本的人来调试或修改它们。使用 Ansible playbook,你可以看到代码非常易于阅读,每一部分都有一个相关的 name。强制缩进也使得代码更易于阅读,虽然 shell 脚本中也支持注释和缩进,但它们没有强制要求,且常常被省略。除此之外,所有模块必须有文档才能被接受到核心的 Ansible 发布中——因此,你可以保证手头有高质量的文档用于你的 playbook。模块文档可以在官方的 Ansible 网站上找到,或者作为已安装的 Ansible 包的一部分。如果我们想学习如何使用我们之前用过的 file 模块,只需在系统的 shell 中输入以下命令:

$ ansible-doc file

当执行此命令时,它会提供文件模块的完整文档,而这些文档恰好与官方 Ansible 网站上的文档相同。因此,即使你工作的系统与互联网断开连接,你仍然能够随时访问 Ansible 模块文档。以下截图显示了我们刚刚运行的命令的输出页面:

下一个原因是,Ansible 模块(大多数情况下)提供对幂等性更改的支持。这意味着,如果一个更改已经执行过,我们就不会再次执行。这对于一些可能具有破坏性的更改尤其重要。它还节省了时间和计算资源,甚至有助于审计系统。除此之外,Ansible 提供了流程控制和强大的错误处理,而在 shell 脚本中,即使发生错误,脚本也会继续执行,除非你集成自己的错误处理代码(可能会导致不可预测或不希望的结果),而 Ansible 会停止所有进一步的执行,要求你在重新运行 playbook 之前修复问题。

值得一提的是,虽然模块构成了 Ansible 强大功能的核心部分,但有时你可能会遇到需要的功能无法通过现有模块来处理的情况。Ansible 作为开源软件OSS)的一个优点是,你可以编写并集成自己的模块。这超出了本书的范围,但随着你不断提升 Ansible 技能,它绝对值得深入探索。在现有模块无法满足需求,并且你没有时间或资源编写自己模块的情况下,Ansible 也可以向被自动化的系统发送原始 Shell 命令。事实上,有两个模块——shellcommand——可以向远程系统发送原始命令。因此,如果有需要,你甚至可以将 Shell 脚本与 Ansible 结合使用,尽管在 resort 使用shellcommand之前,你应该始终优先使用原生的 Ansible 模块。Ansible 在这方面非常灵活——内置的功能非常强大,但如果它无法满足需求,扩展功能也极其容易。

这些好处只是冰山一角,我们将在本章接下来的部分探讨其中的一些其他优点。如前所述,本章的目的并不是详尽无遗,而是作为 Ansible 的入门指南,帮助你入门并理解书中的示例。

在下一节中,我们将探讨使用 Ansible 而非简单 Shell 脚本的一个重要原因。

探索 Ansible 中的库存

正如我们之前提到的,Ansible 快速普及的一个关键原因是,它可以在不使用代理的情况下,集成到大多数主要操作系统中。例如,一个单一的 Ansible 主机可以通过 SSH 连接,自动执行几乎任何其他 Linux(或 BSD)主机上的命令。它甚至可以自动化启用了远程 WinRM 的 Windows 主机上的任务,正是在这里,我们开始揭示 Ansible 的真正强大之处。

在本章前面的部分,我们仅查看了 Ansible 在隐式 localhost 上的运行,而没有使用 SSH。Ansible 支持两种不同类型的库存:静态库存和动态库存。在本书中,我们将主要使用静态库存,因为它适合我们当前的示例。事实上,静态库存非常适合小型环境,在这些环境中,维护待自动化服务器列表(本质上就是 Ansible 库存的内容)的工作量很小。然而,随着库存的规模扩大,或库存虽然保持小但变化迅速(例如,云计算资源或 Docker 容器),保持 Ansible 库存文件更新所需的工作量会变得非常大,并且容易出错。

Ansible 提供了许多现成的动态清单解决方案,可以与流行的公共云平台(如 Microsoft Azure 和 Amazon Web Services)、本地计算平台(如 OpenStack 和 VMware)以及基础设施管理解决方案(如 Katello)集成。甚至可以编写自己的动态清单脚本,随着环境的扩展,你很可能会走上这条道路。

现在,让我们聚焦于静态清单。假设我们想要将之前章节中的示例剧本,运行到两个远程主机上,而不是本地主机。首先,让我们创建一个包含这两个主机名称/地址的清单文件。静态清单采用 INI 格式编写(与剧本中使用的 YAML 格式不同),在最简单的形式下,每一行包含一个主机。注意,主机可以通过 DNS 条目或 IP 地址指定。

这是我们演示环境的清单文件:

[test]
testhost1
testhost2

如你所见,文件非常简单。第一行带有方括号的是一个组的名称,下面的服务器被放置在这个组中。服务器可以属于多个组,这对日常管理服务器非常有帮助。例如,如果你有一个剧本是为了对所有 Linux 服务器应用安全更新,那么你可能会想要一个名为[linux-servers]的组,其中包含所有这类服务器的地址。如果你接下来有一个部署 Web 应用的剧本,你可能会想要将所有的 Web 服务器放入一个名为[web-servers]的组中。这使得在运行特定剧本时,能够轻松地选择正确的服务器集——还记得之前例子中的hosts:那一行吗?

组可以是其他组的子组。因此,如果你知道你的网络服务器都基于 Linux,你可以将web-servers组指定为linux-servers组的子组,从而将所有的网络服务器纳入安全补丁更新的范围,而无需在清单中进行重复。

我们需要对之前的剧本做一些小的修改。前四行现在应该包含如下内容:

---
- name: Simple playbook
  hosts: all
  become: false

如你所见,我们已经将hosts参数从localhost改为allall是一个特殊的关键字,表示清单中的所有主机,不论其属于哪个组)。如果我们只想指定test组,我们可以写成hosts: test,或者甚至写成hosts: testhost1,让剧本仅在单个主机上运行。

现在,我们知道 Ansible 使用 SSH 连接到清单中的远程 Linux 主机,并且在此阶段我们还没有设置基于密钥的 SSH 身份验证。因此,我们需要告诉 Ansible 提示输入 SSH 密码(默认情况下,它不会提示,这意味着如果没有设置基于密钥的身份验证,它将失败)。类似于 SSH 命令行工具,除非你告诉 Ansible 其他要求,否则它会启动一个 SSH 连接到远程系统,使用本地机器当前会话用户的用户名。因此,在我的示例中,用户 james 存在于我的 Ansible 服务器和我的两个测试系统上,所有任务都是以该用户身份执行的。我可以运行以下命令,在我的两个远程系统上执行我的 playbook:

$ ansible-playbook -i hosts --ask-pass simple.yml

这次运行的效果与上次有所不同——请注意以下新的参数:

  • -i hosts:告诉 Ansible 使用当前工作目录下名为 hosts 的文件作为清单

  • --ask-pass:告诉 Ansible 停止并提示输入 SSH 密码,以便访问远程系统(假设所有系统的密码相同)

  • simple.yml:告诉 Ansible 要运行的 playbook 的名称

让我们看一下实际操作,如下所示:

在这里,你可以看到我们在本章前面创建的两个任务已经运行——只不过这次它们是在一对远程系统上运行的,使用的是本地的 SSH 通信协议。由于 SSH 通常在大多数 Linux 服务器上启用,这立即为我们扩展自动化提供了巨大的空间——这个示例在仅包含两个主机的清单上执行,但它同样可以包含 200 个或更多的主机。

请注意,任务仍然是按顺序执行的——只不过这次,每个任务都会在所有清单中的主机上执行完毕后,再尝试下一个任务,这使得我们的 playbook 流程变得非常可预测且易于管理。

如果我们为远程主机设置了 SSH 密钥,那么 --ask-pass 参数就不再需要,playbook 会在没有用户交互的情况下运行,这在许多自动化场景中是最理想的:

SSH 密钥虽然比密码更安全,但也有其风险,特别是如果密钥没有用密码加密的话。在这种情况下,任何获得未加密私钥的人都可以在没有任何提示或挑战的情况下,利用匹配的公钥远程访问任何系统。如果你选择设置 SSH 密钥,一定要理解其安全隐患。

让我们通过一个简单的过程来生成 SSH 密钥,并在我们的测试系统上配置它,以便 Ansible 可以进行身份验证:

  1. 为了在我们的测试主机上设置一个非常简单的基于 SSH 密钥的访问,我们可以从 Ansible 主机运行以下命令来创建密钥对(如果你已经有了密钥对,请不要执行此操作,因为你可能会覆盖它!):
$ ssh-keygen -b 2048 -t rsa -f ~/.ssh/id_rsa -q -N ''
  1. 该命令会静默地在 ~/.ssh/id_rsa 文件中创建一个 2048 位的 RSA 密钥,且不设置密码(因此没有加密)。对应的公钥文件将创建为 ~/.ssh/id_rsa.pub(即与 -f 指定的相同文件名和路径,并附加 .pub 后缀)。现在,使用以下命令将其复制到两个远程主机(你将在两次操作时被提示输入 SSH 密码):
$ ssh-copy-id testhost1
$ ssh-copy-id testhost2
  1. 最后,我们可以像之前一样运行我们的 playbook,但无需使用 --ask-pass 标志,以下截图展示了这一点:

如你所见,这个区别虽小,但却非常重要——没有需要用户干预,这意味着我们的简单 playbook 突然间可以在几乎任何规模的环境中实现大规模扩展。

尽管在这里我们利用了 Ansible 会默认读取位于 .ssh 目录下的 SSH 私钥文件的这一事实,但是你并不局限于使用这些密钥。你可以通过在清单中使用 ansible_ssh_private_key_file 主机变量手动指定一个私钥文件,或者你也可以使用 ssh-agent 在当前的 shell 会话中为 Ansible 提供不同的私钥。

这一过程留给你作为练习来完成,官方 Ansible 文档中的以下页面将帮助你实现这一目标:

当然,你不必以当前用户身份在远程系统上执行所有任务——你可以使用 ansible-playbook 命令中的 --user(或 -u)标志,指定一个将用于清单中所有主机的用户,或者你甚至可以在清单中使用 ansible_user 主机变量,按每个主机来指定用户帐户。显然,你应该尽量避免这种情况,因为它违背了我们在 第一章《在 Linux 上构建标准操作环境》一章中讨论的共性原则,但需要注意的重点是,Ansible 提供了巨大的灵活性和自定义机会。它在 SOE 中的扩展能力非常强,但如果有偏离的地方,也很容易让 Ansible 适应。

我们稍后将在本章中更详细地讨论变量,但此时值得提及的是,清单也可以包含变量。这些变量可以是用户创建的变量,也可以是一些特殊变量,例如前面提到的 ansible_user。扩展本章中的简单清单,如果我们想将 SSH 用户设置为 bob 并创建一个名为 http_port 的新用户定义变量,以便在剧本中稍后使用,我们的清单可能如下所示:

[test]
testhost1
testhost2

[test:vars]
ansible_user=bob
http_port=8080

这涵盖了你开始使用 Ansible 并继续阅读本书时需要了解的清单基础知识。希望你已经开始意识到 Ansible 为新用户提供的低门槛,这也是它如此受欢迎的原因之一。

理解 Ansible 中的角色

尽管 Ansible 非常容易上手,而且当剧本较短时也非常易读,但随着需求的增加,它确实变得更加复杂。此外,某些功能可能需要在不同的场景中反复使用。例如,你可能需要在环境中将 MariaDB 数据库服务器作为一个常见任务进行部署。一个名为 apt 的模块用于管理 Ubuntu 服务器上的软件包,因此,如果我们想在测试系统上安装 mariadb-server 包,执行此任务的剧本可能如下所示:

---
- name: Install MariaDB Server
  hosts: localhost
  become: true

  tasks:
    - name: Install mariadb-server package
      apt:
        name: mariadb-server
        update_cache: yes

请注意,这一次我们将 become 设置为 true,因为安装软件包需要 root 权限。当然,这是一个非常简单的例子,因为安装数据库服务器通常需要更多的配置工作,但它作为一个起点是足够的。我们可以在测试系统上运行这个剧本,并得到期望的结果,结果如下所示:

到目前为止,一切都很好。但是,如果你需要在不同的剧本中为不同的主机反复执行这些操作,你是否真的想一次又一次地编写(或者复制粘贴)这个任务块?此外,这个例子过于简化,实际上,数据库部署代码会复杂得多。如果有人对代码进行了修复或改进,你如何确保这个新的修订版本的代码能够传播到所有正确的位置?

这就是角色的作用,Ansible 角色本质上不过是一个结构化的目录集合和 YAML 文件,它使代码的高效复用成为可能。它还使初始的剧本更易于阅读,正如我们稍后所看到的那样。一旦角色创建完成,它们可以存储在一个中央位置,例如版本控制仓库(例如 GitHub),然后,只要需要安装 MariaDB 的剧本,始终可以访问到最新版本。

默认情况下,角色是从名为roles/的子目录运行的,与您的剧本文件在同一目录中。在本书中,我们将使用这种约定,尽管必须指出,Ansible 还会在/etc/ansible/roles和 Ansible 配置文件中指定的roles_path参数所指定的路径中搜索角色(默认情况下,可以在/etc/ansible/ansible.cfg中找到该文件,虽然有方法可以覆盖此设置)。然后,每个角色在此目录下都有其自己的子目录,并且该目录名称形成角色的名称。让我们通过一个简单的示例来探讨这一点,如下所示:

  1. 我们将首先创建一个roles/目录,并在其下创建一个install-mariadb/目录,作为我们的第一个角色:
$ mkdir -p roles/install-mariadb
  1. 每个角色都有其固定的目录结构;然而,在我们的简单示例中,我们只关心其中一个:tasks/。角色的tasks/子目录包含在调用角色时将运行的主要任务列表,存储在名为main.yml的文件中。现在让我们创建该目录,如下所示:
$ cd roles/install-mariadb
$ mkdir tasks
$ vi tasks/main.yml
  1. 当然,你可以使用你喜欢的编辑器替代vi。在main.yml文件中,输入以下代码——请注意,这本质上是原始剧本中的任务块,但缩进级别现在已更改:
---
- name: Install mariadb-server package
  apt:
    name: mariadb-server
    update_cache: yes
  1. 创建了这个文件后,我们接着编辑我们的原始install-db.yml剧本,使其如下所示:
---
- name: Install MariaDB Server
  hosts: localhost
  become: true

  roles:
    - install-mariadb

注意现在剧本的紧凑程度!它也更容易阅读,然而如果我们运行它,我们可以看到它执行了相同的功能。请注意上次运行时 MariaDB 服务器安装任务的状态是changed,但现在是ok。这意味着 Ansible 检测到mariadb-server包已经安装,因此不需要进一步操作。这是之前提到的幂等性变更的实际示例,如下截图所示:

干得好!你已经创建并执行了你的第一个角色。如果你想进一步了解角色和所需的目录结构,请参阅docs.ansible.com/ansible/latest/user_guide/playbooks_reuse_roles.html

角色不仅仅是这样——它们不仅在结构化 playbook 和代码重用方面非常宝贵;还有一个用于社区贡献角色的中央仓库,叫做 Ansible Galaxy。如果你在 Ansible Galaxy 中搜索与 MariaDB 相关的角色,你会发现(截至目前)有 277 个不同的角色,所有这些角色都旨在执行各种数据库安装任务。这意味着你甚至不需要为常见任务编写自己的角色——你可以利用社区贡献的角色,或者将它们 fork 出来,按自己的需求修改。大多数常见的服务器自动化任务已经被 Ansible 社区在某个地方解决了,所以你很可能能找到完全符合你需求的角色。

现在我们来测试一下,如下所示:

  1. 首先,从 Ansible Galaxy 安装一个在 Ubuntu 上安装 MariaDB 服务器的角色:
$ ansible-galaxy install -p roles/ mrlesmithjr.mariadb-mysql
  1. 现在,我们将修改我们的 playbook 来引用这个角色:
---
- name: Install MariaDB Server
  hosts: localhost
  become: true

  roles:
    - mrlesmithjr.mariadb-mysql
  1. 这就是所需的全部—如果我们运行它,我们可以看到这个 playbook 执行了比我们简单的 playbook 更多的任务,包括许多安装新数据库时的安全设置,这些设置是良好实践,正如以下截图所示:

最终结果是,mariadb-server 包已经安装到我们的测试系统上——而这一次,我们几乎没有编写任何代码!当然,建议在盲目运行 Ansible Galaxy 中的角色之前,先检查它会做些什么,以防它做出你没有预料到(或不希望)的更改!尽管如此,角色结合 Ansible Galaxy,构成了 Ansible 提供的强大附加功能。

了解角色之后,在下一部分,我们将讨论一个重要的概念,帮助你通过使内容动态化,充分发挥 playbook 和角色的最大效用:Ansible 变量。

理解 Ansible 变量

到目前为止,我们看到的大多数示例都是静态的。这对于最简单的 playbook 示例是可以的,但在很多情况下,更希望能够存储或轻松在一个中央位置定义值,而不是在 playbook(及其角色树)中到处寻找某个硬编码的值。和其他语言一样,也希望能以某种方式捕获值,以便以后重用。

在 Ansible 中有许多不同类型的变量,并且需要注意它们有严格的优先级顺序。虽然在本书中我们不会遇到太多这个问题,但了解这一点是非常重要的,因为否则你可能会从变量中得到意想不到的结果。

变量优先级的更多细节可以参考 docs.ansible.com/ansible/latest/user_guide/playbooks_variables.html#variable-precedence-where-should-i-put-a-variable

简而言之,变量可以在多个位置定义,特定场景下的正确位置将由剧本的目标驱动。例如,如果某个变量对一组服务器都通用,那么将其作为组变量定义在清单中是合乎逻辑的。如果它适用于每个特定剧本运行的主机,那么你几乎肯定会在剧本中定义它。让我们快速看一下,通过修改我们在本章前面使用的simple.yml剧本,在其中定义一个名为message的剧本变量,以便在剧本运行时通过debug语句显示,示例如下:

---
- name: Simple playbook
  hosts: localhost
  become: false

  vars:
    message: "Life is beautiful!"

  tasks:
    - name: Show a message
      debug:
        msg: "{{ message }}"
    - name: Touch a file
      file:
        path: /tmp/foo
        state: touch

请注意,我们现在在tasks部分之前定义了一个vars部分,并且通过将变量放在一对大括号中来访问它。运行此剧本会得到以下结果:

如果你参考变量优先级顺序列表,你会注意到传递给ansible-playbook命令行二进制文件的变量位于列表顶部,并且会覆盖所有其他变量。因此,如果我们希望在不编辑剧本的情况下覆盖消息变量的内容,我们可以如下操作:

$ ansible-playbook simple.yml -e "message=\"Hello from the CLI\""

注意处理变量内容中空格所需的特殊引用和转义,以及这对剧本操作的影响:

变量也可以传递给角色,这是创建通用角色的一种简单而强大的方式,这些角色可以在多种场景中使用,而无需使用相同的配置数据。例如,在前面的部分中,我们探讨了安装 MariaDB 服务器。虽然这是一个适合做成角色的好例子,但你肯定不希望在每个服务器上配置相同的 root 数据库密码。因此,定义一个密码变量,并将其从调用的剧本(或其他适当来源,如主机或组变量)传递给角色是很有意义的。

除了用户定义的变量,Ansible 还拥有一些内置变量,这些变量被称为特殊变量。可以在剧本的任何地方访问这些变量,它们对于获取与剧本状态相关的某些细节非常有用。

例如,如果你需要知道当前正在执行特定任务的主机名,可以通过inventory_hostname变量获取。有关这些变量的完整列表,请访问docs.ansible.com/ansible/latest/reference_appendices/special_variables.html

许多读者现在应该已经注意到,我们所有示例剧本的输出中都包含一行文字:“Gathering Facts”。虽然可以关闭这一功能,但它实际上非常有用,会填充许多包含有用系统数据的变量。为了了解在此阶段收集到的数据类型,请从命令行运行以下代码:

$ ansible -m setup localhost

这个命令并不是运行剧本,而是指示 Ansible 直接在localhost上运行setup模块——setup模块是在Gathering Facts阶段后台运行的模块。输出看起来像这样,并且会显示很多内容——这里只是前几行:

我们可以立即看到这里有一些非常有用的信息,例如主机的 IP 地址、根卷等等。记得我们在第一章中讨论过的共同点吗?在 Linux 上构建标准操作环境,以及检测你所运行的操作系统的难度?好了,Ansible 使得这一点变得简单,因为这些数据都可以在收集到的事实中轻松获取。我们只需通过指定相应的事实,便可以从上一条命令的输出中访问,修改我们的debug语句来显示我们所运行的 Linux 发行版,如下所示:

    - name: Show a message
      debug:
        msg: "{{ ansible_distribution }}"

现在,当我们运行剧本时,我们可以很容易地看出我们正在运行的是 Ubuntu,正如以下截图所示:

Ansible 使得你可以有条件地执行单个任务、角色,甚至整个任务块,因此,访问这些事实数据使得编写可以在多个平台上运行的健壮剧本变得非常容易,并且能在每个平台上执行正确的操作。

同样值得注意的是,变量不必存储在未加密的文本中。偶尔,可能需要将密码存储在变量中(如前所述——可能是我们的 MariaDB 服务器安装的 root 密码)。以明文格式存储这些细节存在很大的安全风险,但幸运的是,Ansible 包括了一种名为Vault的技术,能够使用 AES256 加密存储变量数据。这些加密的 Vault 可以被任何剧本引用,只要在运行剧本时提供 Vault 密码即可。Vault 的内容超出了本章的范围,但如果你想了解更多,可以查看docs.ansible.com/ansible/latest/user_guide/playbooks_vault.html。在本书中,我们不会广泛使用 Vault,仅仅是为了保持示例代码简洁。然而,强烈建议在生产环境中,尽可能在需要存储剧本敏感数据的地方使用 Vault。

现在我们已经介绍了 Ansible 中变量的概念,以及各种类型的变量,让我们来看看 Ansible 中管理配置文件的重要方式——使用模板。

理解 Ansible 模板

一个常见的自动化需求是根据某些给定的参数在配置文件中设置值,或者甚至部署一个新的配置文件。Ansible 提供了可以执行类似于传统sedawk工具功能的模块,当然,这些也是修改现有配置文件的有效方式。假设我们有一个小型的 Apache 虚拟主机配置文件,包含如下代码:

<VirtualHost *:80>
    DocumentRoot "/var/www/automation"
    ServerName www.example.com
</VirtualHost>

我们希望部署这个配置,但为每个主机定制DocumentRoot参数。当然,我们可以像以前那样将文件直接部署到每个主机,然后使用正则表达式,结合 Ansible 的replace模块,找到DocumentRoot所在的行并修改它(类似于使用sed命令行工具)。最终的 playbook 可能如下所示:

---
- name: Deploy and customize an Apache configuration
  hosts: localhost
  become: true

  vars:
    docroot: "/var/www/myexample"

  tasks:
    - name: Copy static configuration file to remote host
      copy:
        src: files/vhost.conf
        dest: /etc/apache2/sites-available/my-vhost.conf

    - name: Replace static DocumentRoot with variable contents
      replace:
        path: /etc/apache2/sites-available/my-vhost.conf
        regexp: '^(\s+DocumentRoot)\s+.*$'
        replace: '\1 {{ docroot }}'

如果我们创建一个名为files/vhost.conf的静态虚拟主机配置文件,内容如前所述,并运行这个 playbook,我们可以看到它工作正常,如下所示:

然而,这并不是一个优雅的解决方案。首先,我们使用了两个任务,如果还想定制ServerName,我们还需要更多的任务。其次,那些熟悉正则表达式的人会知道,简单的正则表达式很容易出错。编写用于这类任务的健壮的正则表达式本身就是一门艺术。

幸运的是,Ansible 继承了它所写的 Python 中的技术,叫做 Jinja2 模板。这对于像这样的场景(以及许多其他与部署相关的自动化场景)非常适用。我们现在不需要像之前那样的繁琐多步骤的方法,而是将我们的起始虚拟主机配置文件定义为templates/vhost.conf.j2模板,如下所示:

<VirtualHost *:80>
    DocumentRoot {{ docroot }}
    ServerName www.example.com
</VirtualHost>

如你所见,这几乎与我们原始的配置文件相同,唯一不同的是我们已经将其中一个静态值替换成了我们的变量,变量用一对大括号括起来,就像我们在 playbook 中做的一样。在继续这个例子之前,值得一提的是,Jinja2 是一个功能非常强大的模板系统,远远超出了简单的变量替换,它支持条件语句,如if...elsefor循环,还包括许多可以用来处理内容的过滤器(例如,将字符串转换为大写,或者将列表中的成员连接起来形成一个字符串)。

话虽如此,本书并不是 Ansible 或 Jinja2 的完整语言参考——它更像是一本实用指南,向您展示如何使用 Ansible 构建 SOE。请参考本章末尾的进一步阅读部分,里面有一些参考资料,可以为您提供 Ansible 和 Jinja2 更全面的概述。

返回到我们的示例,我们将修改 playbook 来部署此示例,如下所示:

---
- name: Deploy and customize an Apache configuration
  hosts: localhost
  become: true

  vars:
    docroot: "/var/www/myexample"

  tasks:
    - name: Copy across and populate the template configuration
      template:
        src: templates/vhost.conf.j2
        dest: /etc/apache2/sites-available/my-vhost.conf

请注意,这个 playbook 更加简洁优雅——template 模块将配置模板复制到远程主机,就像在前面的示例中 copy 模块所做的那样,并且还填充了我们指定的任何变量。这是一种极其强大的方式,以可重复、通用的方式部署配置文件,并且强烈建议在可能的情况下采用这种方法。当人类编辑文件时,他们往往会以不一致的方式进行,这可能会成为自动化的敌人,因为您必须构建一个非常稳健的正则表达式,以确保捕捉所有可能的边界情况。通过 Ansible 从模板部署可以创建可重复、可靠的结果,且能够在生产环境中轻松验证。运行这个 playbook 会得到与我们之前更复杂示例相同的结果,如下所示:

目前我们已经结束了对变量的讨论,也完成了 Ansible 的速成课程。在接下来的章节中,我们将把所学的内容整合起来,之后结束这一章。

将 Ansible 和 SOE 结合起来

我们已经通过 Ansible 完成了多个端到端的示例。虽然这些示例很简单,但它们展示了本书所基于的 Ansible 自动化的基本构建模块。在大规模 Linux 环境中实现自动化的一个重要部分是拥有良好的标准和稳健的流程。因此,您的操作环境不仅应当标准化,您的部署和配置流程也应当如此。

正如上一章所讨论的那样,尽管定义良好的 SOE 在部署时是统一的,但如果管理员可以随意更改,使用他们偏好的任何方法,这种一致性很快就会丧失。就像部署 SOE 是实现自动化成功的关键一样,尽可能将自动化作为您进行大多数(理想情况下是所有)管理任务的首选方法也是十分理想的。

理想情况下,playbook 应该有一个单一的真实来源(例如,中央 Git 仓库),清单也应有一个单一的真实来源(这可以是一个集中存储的静态清单,或者使用动态清单)。

任何编写良好的 Ansible playbook(或角色)的目标是确保运行它的结果是可重复且可预测的。例如,我们在上一节末尾运行的 playbook,我们通过该 playbook 部署了一个简单的 Apache vhost.conf 文件。每次你在任何服务器上运行此 playbook 时,/etc/apache2/sites-available/my-vhost.conf 的内容都会相同,因为该 playbook 使用模板部署此文件,并且如果目标文件存在,则会覆盖该文件。

当然,这只是标准操作环境的一个缩影,但这样的环境将由成百上千—如果不是更多—这样的微小构件块组成。毕竟,如果你无法确保你的 Apache 配置在整个基础设施中保持一致,那么你如何能确信它的其他部分也都符合你的标准呢?

编写良好的 playbook 的可重复性也很重要—仅仅因为你部署了一个一致的 Apache 配置,并不意味着它会保持一致。在你部署配置后的五分钟内,拥有相应权限的人可能会登录服务器并更改配置。因此,你的环境可能会几乎立即偏离你的 SOE 定义。实际上,反复运行你的 Ansible playbook 来管理基础设施是你持续过程中的一个重要部分,因为这些 playbook 的目的是将配置恢复到你原来的标准。因此,Ansible playbook 不仅在定义和部署 SOE 时至关重要,而且在持续执行标准方面也起着重要作用。

如果可能的话,不应手动部署任何修复措施。假设有人手动调整了 /etc/apache2/sites-available/my-vhost.conf 中的配置以解决某个问题。单独来看,这并不构成问题,但重要的是,这些更改必须被添加回 playbook、角色或模板中。如果通过 Ansible 部署或强制执行你的 SOE 以某种方式破坏了它,那么你的流程可能存在问题。

实际上,通过实施我们到目前为止讨论的,并将在本书中继续探索的过程,可以实现企业范围内的成功自动化。本章中简要介绍的 Ansible 自动化,虽然简短,却是这些建议流程的一部分。

关于 Ansible 还有很多内容需要学习,简而言之,我想提出一个大胆的观点:如果你能将其构思为服务器部署或配置任务,Ansible 都能提供帮助。得益于其开源特性,Ansible 非常具有扩展性,并且其广泛的应用意味着许多常见的自动化挑战已经得到解决,并且相关功能已经被包含在内。希望本章已经为你进入 Linux 自动化和 Ansible 打下了良好的基础。

总结

Ansible 是一款强大、可靠的开源工具,一旦你掌握了几个简单的概念,它就能帮助你在 Linux 环境中实现大规模的自动化。Ansible 无需代理,因此在 Linux 客户端机器上无需配置,你就可以开始自动化之旅,而且项目背后有一个强大的社区,这意味着你可以轻松找到解决你希望用 Ansible 解决的大多数问题的答案。

在本章中,你学习了 playbook 结构的基础知识以及运行简单 playbook 所需的一些关键文件。你了解了清单的重要性以及如何使用它们,如何通过角色(以及如何利用社区的代码来节省时间和精力)高效地重用代码。你学习了变量和 facts,以及如何在 playbook 中引用它们,如何使用 Jinja2 模板来帮助你的自动化旅程。在这一过程中,你构建并运行了多个完整的 playbook,展示了 Ansible 的使用。

在下一章中,你将发现如何简化基础设施管理,并进一步优化你的自动化流程,使用 AWX 来完善管理。

问题

  1. 什么是 Ansible,它与运行简单的 shell 脚本有何不同?

  2. 什么是 Ansible 清单?

  3. 为什么将任务编写为角色而不是单个大型 playbook 通常是有益的?

  4. Ansible 使用哪种模板语言?

  5. 你可以覆盖 Ansible 中的变量吗?

  6. 为什么你会使用 Ansible 模板模块而不是简单的查找和替换操作?

  7. 你如何利用 Ansible facts 来改善你的 playbook 流程?

深入阅读

第三章:使用 AWX 简化基础设施管理

正如本书迄今为止所讨论的,Linux 上的有效企业自动化涉及几个关键要素,包括工具和技术的标准化,以及实现使环境管理更加高效的流程和工具。Ansible 是这条旅程的第一步,可以通过一种名为 AWX 的互补技术来进一步简化其应用。

AWX 简而言之是一个用于管理 Ansible 任务的图形界面驱动工具。它并不替代 Ansible 的功能,而是通过提供一个多用户图形界面的前端来补充 Ansible,从而简化了剧本的管理和编排。当管理像企业环境中那样的大型 Linux 环境时,AWX 是 Ansible 自动化的完美补充,是实现高效管理的重要一步。在本章中,我们将涵盖以下主题:

  • AWX 简介

  • 安装 AWX

  • 从 AWX 运行你的剧本

  • 使用 AWX 自动化常规任务

技术要求

本章包括基于以下技术的示例:

  • Ubuntu Server 18.04 LTS

  • CentOS 7.6

  • Ansible 2.8

要运行这些示例,你需要访问运行上述操作系统之一和 Ansible 的服务器或虚拟机。请注意,本章给出的示例可能具有破坏性(例如,它们涉及在服务器上安装 Docker 并运行服务),如果按照原样运行,仅适合在隔离的测试环境中运行。

一旦确认你有一个安全的操作环境,我们就可以开始看看如何使用 Ansible 安装新的软件包。

本书中讨论的所有示例代码可从 GitHub 获取:github.com/PacktPublishing/Hands-On-Enterprise-Automation-on-Linux

AWX 简介

AWX 旨在解决在企业环境中使用 Ansible 自动化所面临的问题。为了保持我们的实践焦点,让我们考虑一下我们在第一章中讨论的有机增长场景,在 Linux 上构建标准操作环境。在一个已经实现了 Ansible 的小型环境中,可能只有一到两个人负责编写和运行剧本。在这个小型场景中,了解谁运行了哪些剧本以及最新版本是什么相对容易,而且 Ansible 的培训需求较低,因为只有少数关键人员负责使用它。

随着环境规模的扩大,Ansible 操作员的数量也会增加。如果所有负责运行 Ansible 的人都在自己的机器上安装了它,并且每个人都有本地的 playbook 副本,突然之间,管理这个环境就变成了一场噩梦!你如何确保每个人都在使用最新版本的 playbook?你怎么知道是谁运行了什么,结果是什么?如果需要在非工作时间执行某个变更怎么办?你能将 Ansible 作业交给 网络运营中心NOC)团队吗?还是说不行,因为他们需要接受 Ansible 使用培训?

AWX 旨在解决所有这些挑战,正如我们接下来将看到的那样,从下一节开始,我们将讨论 AWX 如何帮助降低员工培训成本。

AWX 降低了培训需求

Ansible 非常容易上手。不过,它仍然需要一些培训。例如,未接受过培训的 IT 管理员和操作员可能不习惯在命令行上运行 playbook。以下示例演示了这一点。虽然从 Ansible 的角度来看这个过程相当简单,但任何不熟悉这个工具的人都会发现,它并不十分友好:

$ ansible-playbook -i hosts --ask-pass simple.yml

尽管这不是一个复杂的命令,但那些不熟悉它的人可能会因为害怕对生产系统造成损害而不愿意执行,更不用说解读一个大型 playbook 可能产生的输出内容了。

为了缓解这一问题,AWX 提供了一个基于 Web GUI 的界面,字面意思就是点击即可使用。虽然熟悉该工具的用户可以使用许多高级功能,但通过鼠标点击几下就可以运行 playbook,结果将通过一个简单的 交通信号灯 系统展示(红色表示 playbook 运行失败,而绿色表示成功)。通过这种方式,AWX 提供了一个接口,使得即使是没有 Ansible 使用经验的人也能够启动 playbook,并将结果传递给其他团队进行分析。

AWX 也为安全团队和管理者提供了好处,通过记录所有执行的操作和作业的详细结果,下一节中我们将提供对此的概述。

AWX 使审计变得可能

尽管 Ansible 命令行工具提供了日志选项,但这些选项默认是禁用的,因此,一旦终端会话关闭,playbook 的运行输出可能会丢失。在企业环境中,这并不理想,特别是在出现问题或故障时,进行根本原因分析是必需的。

AWX 通过两种方式解决了这个问题。首先,所有用户在执行任何操作之前必须登录 GUI。AWX 可以与集中式会计系统(如 LDAP 或 Active Directory)集成,或者用户可以在 AWX 主机上本地定义。然后,所有 UI 中的操作都会被跟踪,因此,可以将剧本运行追溯到特定用户,甚至配置更改。在企业环境中,这种级别的问责制和审计追踪是必不可少的。

此外,AWX 会捕获每次剧本运行的所有输出,以及一些关键信息,例如剧本运行的清单、传递给它的变量(如果有的话)以及运行的日期和时间。这意味着如果发生问题,AWX 可以提供完整的审计追踪,帮助你找出发生了什么以及发生的时间。

AWX 不仅可以帮助审计你的自动化,还可以帮助确保剧本的版本控制,我们将在下一节中详细讨论这一点。

AWX 支持版本控制

在企业场景中,个人将剧本存储在本地可能会导致潜在的问题。例如,如果用户 A 更新了包含关键修复的剧本,如何确保用户 B 能够访问到该代码?理想情况下,代码应存储在版本控制系统中(例如,GitHub),并且每次运行时本地副本都会更新。

良好的流程是 Linux 企业自动化的重要组成部分,尽管用户 B 在运行剧本之前应更新本地剧本,但你无法强制执行这一点。同样,AWX 通过允许剧本从版本控制仓库中获取来解决这个问题,AWX 服务器上的本地剧本副本会自动更新。

尽管 AWX 可以帮助你,特别是在确保从仓库拉取最新版本的代码时,但它无法帮助解决其他错误行为,例如有人根本没有提交代码。然而,强制使用 AWX 来执行 Ansible 剧本的目的是:任何做出更改的人都必须提交这些更改,AWX 才能运行它们。应严格限制本地访问 AWX 服务器,以防止人们在本地文件系统上修改代码,通过这种方式,你可以确信每个人都在积极有效地使用版本控制系统。

这些更新可以是事件驱动的,例如,当从该存储库运行剧本时,本地剧本每次都会更新。它们也可以按照 AWX 管理员的决策,定期更新或手动更新。

AWX 还可以帮助你确保自动化的安全性。我们将在下一节中通过查看 AWX 中的凭证管理来探讨这一点。

AWX 有助于凭证管理

为了让 Ansible 有效地管理企业 Linux 环境,它必须具有某种形式的凭证来访问它所管理的所有服务器。SSH 认证通常使用 SSH 密钥或密码进行保护,在一个大型的 Ansible 操作员团队中,这意味着每个人都可能访问这些密码和 SSH 私钥,因为它们是运行 Ansible 所必需的。不言而喻,这带来了安全风险!

如前所述,从安全角度来看,这种做法并不理想,因为别人可以轻易地复制并粘贴凭证,将其用于未经授权的方式。AWX 通过将所需的凭证存储在数据库中,并使用安装时选择的密码短语加密,来处理这个问题。GUI 使用可逆加密存储所有凭证,以便在稍后运行 playbook 时将它们传递给 Ansible。然而,GUI 不允许你查看任何以前输入的敏感数据(例如密码或 SSH 私钥)——也就是说,凭证可以输入和更改,但你无法在 GUI 中显示密码或 SSH 密钥,因此操作员无法轻松通过 AWX 前端获取凭证信息以供其他用途。通过这种方式,AWX 帮助企业将凭证锁定保管,并确保它们仅用于 Ansible 部署,避免泄露或被用于其他非预期目的。

Ansible Vault 是一个出色的工具,用于加密 playbook 操作时需要的任何敏感数据,无论是作为变量形式的 playbook 数据,还是存储服务器凭证本身,例如 SSH 私钥。尽管 Vault 非常安全,但如果你拥有 Vault 密码(在这里,你需要运行使用 Vault 的 playbook),仍然可以查看 Vault 内容。因此,AWX 提供了独特的功能,以补充 Ansible 并确保企业环境中的安全性。

通过这些方式,AWX 帮助解决企业在大规模环境中部署 Ansible 时面临的许多挑战。在我们完成本章的这一部分之前,我们将简要讨论 AWX 如何帮助你与其他服务进行集成。

将 AWX 与其他服务集成

AWX 可以与各种工具集成——例如,Red Hat 的 Satellite 6 和 CloudForms 产品(及其开源版本 Katello 和 ManageIQ)都提供与 AWX 和 Ansible Tower 的原生集成。这只是两个示例,而所有这些都可能实现,因为我们将在本章中探索的所有内容也可以通过 API 和命令行界面访问。

这使得 AWX 可以与各种服务进行集成,或者您甚至可以编写自己的服务,通过调用 API 来运行 AWX 的 playbook,从而响应某些其他操作。命令行界面(称为 tower-cli,来源于商业产品 Ansible Tower)也非常有用,尤其是在程序化地向 AWX 填充数据时。例如,如果您想将一个主机添加到静态清单中,您可以通过 Web 用户界面(稍后我们会演示)、API 或使用 CLI 来完成。后两种方法非常适合与其他服务集成——例如,配置管理数据库CMDB)可以通过 API 将新主机推送到清单中,无需用户手动操作。

若要进一步探索这两个集成点,您可以参考以下官方文档来源:

鉴于这类集成的广泛性和多样性,超出了本书的讨论范围——然而,提到这些集成点很重要,因为希望在阅读本章时,您会看到与其他服务集成的机会,从而能够进一步探索这一主题。本章的下一部分,我们将实际操作 AWX,并查看一个简单的部署。稍后本章将通过一些示例用例进行补充。

安装 AWX

一旦设置了正确的前提条件,安装 AWX 就非常简单。事实上,AWX 的一个前提条件是 Ansible,证明了这项技术的互补性。大多数 AWX 代码运行在一组 Docker 容器中,这使得它在大多数 Linux 环境中都能轻松部署。

使用 Docker 容器意味着可以在 OpenShift 或其他 Kubernetes 环境中运行 AWX——然而,为了简便起见,我们将在单一 Docker 主机上开始安装。在继续之前,您应该确保所选主机具备以下条件:

  • Docker,完全安装并正常工作

  • 您版本的 Python 的 docker-py 模块

  • 访问 Docker Hub(需要互联网访问)

  • Ansible 2.4 或更高版本

  • Git 1.8.4 或更高版本

  • Docker Compose

这些前提条件通常在大多数 Linux 系统中都可以轻松获得。现在,我们将执行以下步骤开始我们的安装:

  1. 继续我们在上一章中使用的 Ubuntu 系统示例,我们将运行以下命令来安装 AWX 所需的依赖:
$ sudo apt-get install git docker.io python-docker docker-compose
  1. 安装这些之后,接下来的任务是从 GitHub 上的仓库克隆 AWX 代码:
$ git clone https://github.com/ansible/awx.git 

Git 工具将忠实地克隆 AWX 源代码的最新版本—请注意,该项目正在积极开发中,最新发布版本可能存在错误。

如果您想克隆稳定的 AWX 发行版之一,请浏览存储库的 Releases 部分并检出所需版本:https:/​/​github.​com/ansible/​awx/​releases.

  1. 我们已经克隆了存储库,现在是时候为我们的 AWX 安装定义配置了,特别是安全细节如密码。要开始此过程,请切换到克隆存储库下的 installer 目录:
$ cd awx/installer 

希望在阅读前一章节后,您对此目录的内容已经感到熟悉。这里有一个 inventory 文件,一个我们要运行的 playbook 叫做 install.yml,还有一个 roles/ 目录。但在运行 install.yml playbook 之前,请注意清单文件中还有一些变量需要设置。

如果您查看清单文件,您会看到其中有大量的配置可供设置。一些变量被注释掉,而其他变量则设置为默认值。在安装 AWX 之前,我建议您至少设置六个变量,具体如下:

变量名 推荐值
admin_password

这是管理员用户的默认密码—第一次登录时需要它,所以请务必将其设置为易于记忆且安全的内容!

|

pg_password

这是后端 PostgreSQL 数据库的密码—请确保将其设置为独特且安全的内容。

|

postgres_data_dir

这是本地文件系统上 PostgreSQL 容器存储其数据的目录—它默认位于 /tmp 下的一个目录,在大多数系统上会定期自动清理。这通常会销毁 PostgreSQL 数据库,因此请将其设置为 AWX 特定的目录(例如,/var/lib/awx/pgdocker)。

|

project_data_dir

若要手动上传 Playbooks 到 AWX 而无需使用版本控制系统,则 Playbooks 必须位于文件系统的某个位置。为了避免复制到容器中,此变量将本地指定的文件夹映射到容器内所需的文件夹。在本书的示例中,我们将使用默认的 (/var/lib/awx/projects 文件夹)。

|

rabbitmq_password

这是后端 RabbitMQ 服务的密码—请确保将其设置为独特且安全的内容。

|

secret_key

这是用于加密 PostgreSQL 数据库凭据的秘钥。在 AWX 升级之间必须保持一致,所以请确保将其存储在安全的地方,因为将来需要在 AWX 的清单中设置它。请设置一个又长又安全的内容。

|

  1. 你会发现,在这个清单文件中有大量的秘密信息以明文形式显示。虽然在安装过程中我们可以容忍这一点,但安装完成后,不应该把这个文件留在文件系统上,因为它可能为潜在攻击者提供获取系统详细信息的途径,进而轻易地破坏你的系统。安装阶段完成后,务必将此文件复制到某种密码管理器中,或者将单独的密码存储起来——无论哪种方式,都不要将文件以未加密形式存放!

  2. 一旦清单被自定义,就可以开始执行安装过程——通过运行以下命令启动安装:

$ sudo ansible-playbook -i inventory install.yml

从我们在上一章关于 Ansible 的工作中,你会认识到这个命令——它使用ansible-playbook命令来运行install.yml剧本,同时也使用我们在步骤 1中编辑的名为inventory的清单文件。终端中将会输出很多内容,如果安装成功,你应该能看到类似这样的信息:

  1. 安装完成后,Docker 容器需要几分钟才能启动,后台数据库也需要一些时间来创建。然而,一旦完成,你应该能够在浏览器中导航到所选 AWX 主机的 IP 地址,并看到登录页面,下面的截图展示了一个示例:

  1. 使用你之前在清单文件中的admin_password变量设置的密码,以管理员身份登录。登录后,你将被带到 AWX 的仪表板页面:

就这样——你已经成功安装并登录到 AWX 了!当然,你可以定义更多的高级安装参数,而且在企业环境中,你也不应该只依赖单个 AWX 主机而没有备份(或高可用性)。

请注意,当你登录 AWX 时,连接没有使用 SSL 加密,这可能导致敏感数据(如机器凭证)以明文形式通过网络传输。

没有一种适合所有企业的现成高可用性和 SSL 解决方案,因此我们将实际解决方案留给你作为练习。例如,如果你有一个包含多个主机的 OpenShift 环境,那么在该环境中安装 AWX 可以确保即使运行 AWX 的主机发生故障,它也能继续运行。当然,也有方法可以在没有 OpenShift 的情况下实现高可用性。

将安全 HTTP 应用于 AWX 的解决方法会因不同的环境而异。大多数 Docker 环境前面会有某种负载均衡器来帮助处理其多主机特性,因此,SSL 加密可能会卸载到负载均衡器上。也可以通过安装能够进行反向代理的软件(例如 nginx)并配置它来处理 SSL 加密,从而保护单个 Docker 主机,例如我们在这里构建的那个。

总之,这并没有统一的解决方案,但建议你根据自己企业的需求采取最合适的方法。因此,我们在此不再进一步讨论它们,只建议你在为生产环境部署 AWX 时考虑这些问题。

现在你已经有了一个运行中的 AWX 实例,我们必须对其进行配置,以便我们能够成功地复现上一章中从命令行运行剧本的方式。例如,我们必须像之前那样定义一个库存,并确保我们已设置 SSH 身份验证,以便 Ansible 可以在远程计算机上执行自动化任务。在本章的下一部分,我们将演示运行第一个剧本所需的所有设置。

从 AWX 运行剧本

当我们从命令行运行示例剧本时,我们创建了库存文件,然后是剧本,并使用ansible-playbook命令运行它。所有这些,当然,假设我们已经通过交互式输入密码或设置 SSH 密钥的方式与远程系统建立了连接。

尽管 AWX 中的最终结果非常相似——剧本是针对库存运行的——但术语和命名方式却有很大不同。在本章的这一部分,我们将演示如何通过 AWX 启动并运行第一个剧本。虽然本书没有足够的篇幅详细介绍 AWX 的每个功能,但本节旨在让你具备足够的知识和信心,从 AWX 管理剧本,并进一步探索。

在你能够从 AWX 运行第一个剧本之前,必须完成几个前置设置步骤。在接下来的部分中,我们将完成其中的第一个——创建将用于通过 SSH 与目标机器进行身份验证的凭据。

在 AWX 中设置凭据

当你登录到 AWX 时,你会注意到屏幕左侧有一个菜单栏。为了定义一组新的凭据,我们将用它来让 Ansible 登录到我们的目标机器,执行以下步骤:

  1. 点击左侧菜单栏中的 Credentials。

  2. 点击绿色的 + 图标以创建新的凭据。

  3. 给凭据起个名字,并从 CREDENTIAL TYPE 字段中选择 Machine。AWX 支持许多类型的凭据,可以与各种服务交互,但目前我们只关注这一类型。

  4. 还有许多其他字段可以用于指定更高级的参数,但对于我们的演示来说,这些已经足够。

你的最终结果应类似于下面的截图。请注意,我已经为我的示范机器指定了登录密码,但你也可以在屏幕上的较大文本框中指定 SSH 私钥。你还会看到 Prompt on launch 复选框的存在—AWX 有许多选项,可以在运行 playbook 时提示用户,这能提供丰富的交互式用户体验。不过,在本示例中,我们不会使用这个选项,因为我们希望演示无需用户干预即可运行 playbook:

定义凭据后,下一步是定义库存来运行我们的 playbook。我们将在下一节中详细探讨。

在 AWX 中创建库存

就像在命令行一样,AWX 需要创建一个库存,才能执行 playbook。在这里,我们将使用一个官方的、公开可用的 Ansible 示例 playbook,它需要一个包含两个组的库存。在更大的设置中,我们会为每个组指定不同的服务器,但在这个小型示例中,我们可以重复使用同一台服务器作为两个角色。

相关代码用于在 RHEL 或 CentOS 7 机器上安装一个简单的 LAMP 堆栈,可以在这里查看:github.com/ansible/ansible-examples/tree/master/lamp_simple_rhel7

要运行这个示例,你需要一台 CentOS 7 机器。我的示例主机叫做 centos-testhost,如果我在命令行定义一个库存文件,它看起来会是这样的:

[webservers]
centos-testhost

[dbservers]
centos-testhost

要在 AWX 图形界面中复制此操作,请按照以下步骤执行:

  1. 在左侧菜单栏点击 Inventories。

  2. 点击绿色的 + 图标来创建一个新的库存。

  3. 从下拉菜单中选择 Inventory。

  4. 给库存起个合适的名字,然后点击 SAVE。

完成此过程后,屏幕应显示如下所示:

完成后,我们可以创建第一个组,并将我们的测试主机加入其中。按照以下步骤操作:

  1. 点击窗格顶部的 GROUPS 按钮。

  2. 点击绿色的 + 图标来创建一个新的组。

  3. 在 NAME 字段中输入名称 webservers

  4. 点击绿色的 SAVE 按钮。

  5. 点击顶部的 HOSTS 按钮。

  6. 点击绿色的 + 图标按钮来添加一个新主机。

    1. 从下拉列表中选择 New Host。
  7. 在 HOST NAME 字段中输入名称 centos-testhost

  8. 点击绿色的 SAVE 按钮。

一旦你完成了这些步骤,屏幕应该像下面的截图一样:

重复这个过程来定义 dbservers 组。小心不要把这个组创建为 webservers 组的子组,这是很容易犯的错误。你会注意到前面截图顶部的面包屑路径——通过点击 Hands on Inventory (或你的名字,如果你选择了不同的名称),你可以用它来返回到新库存的顶级页面。

从这里开始,过程几乎相同,唯一的不同是,当你开始将主机添加到新创建的组时(从前面的步骤第6 步开始),选择 Existing Host,因为我们在这个示例中为两个组复用了同一台主机。你最终的屏幕应该像下面的截图一样:

完成这些步骤后,我们的库存和分组就已经在 AWX 中完成了,我们可以继续下一步,定义我们的配置——创建 AWX 项目。我们将在本章的下一节中详细介绍这部分内容。

在 AWX 中创建项目

如果你在命令行中使用 Ansible,可能不太会将所有的 playbook 和角色存储在一个目录下太长时间,因为这样会变得难以管理,并且很难判断哪个文件是哪个。这就是 AWX 中项目的目的——它简单来说就是 playbook 的逻辑分组,用来让组织变得更加简单和清晰。

尽管我们在本书中不会深入讨论 基于角色的访问控制 (RBAC),项目在这方面也起着重要作用。在迄今为止提供的截图中,你可能注意到在多个面板的顶部有一个 PERMISSIONS 按钮。这些按钮遍布整个 UI,用于定义哪些用户可以访问哪些配置项。例如,如果你有一个 数据库管理员 (DBA) 团队,他们只需要有权运行与数据库服务器相关的 playbooks,你可以创建一个数据库服务器的库存,并仅允许 DBAs 访问它。同样,你可以将所有与 DBA 相关的 playbook 放入一个项目中,并且再次仅给该团队访问该项目的权限。通过这种方式,AWX 作为企业内部良好流程的一部分,既让 Ansible 更加易于访问,又确保了只有正确的人可以访问到正确的项目。

为了继续我们的简单示例,让我们创建一个新项目来引用我们的示例 Ansible 代码:

  1. 点击左侧菜单栏中的 Projects。

  2. 点击绿色的 + 图标 以创建一个新项目。

  3. 给项目起一个合适的名称。

  4. 从 SCM TYPE 下拉列表中选择 Git。

  5. 在 SCM URL 字段中输入以下 URL: github.com/ansible/ansible-examples.git

  6. 可选地,如果你只想在仓库中的特定提交或分支上工作,也可以填写 SCM BRANCH/TAG/COMMIT 字段。在这个简单的示例中,我们将使用最新的提交,即在 Git 中被称为HEAD

  7. 由于这是一个公开可用的 GitHub 示例,因此不需要其他凭据——然而,如果你使用的是受密码保护的仓库,你需要为我们在本章 设置 AWX 中的凭据 部分创建一个 SCM 凭据。

  8. 勾选 UPDATE REVISION ON LAUNCH 复选框——这会导致 AWX 在每次运行该项目的 playbook 时,从我们的 SCM URL 拉取最新的代码版本。如果未勾选此选项,则必须在 AWX 看到最新版本之前手动更新本地代码副本。

  9. 点击绿色的 SAVE 按钮。

完成后,结果页面应该类似于以下截图:

在我们进入配置 playbook 以进行首次运行的最后一步之前,我们需要手动从 GitHub 仓库拉取内容。为此,点击新创建项目右侧的两个半圆形箭头——这将强制从上游仓库手动同步项目。以下截图展示了这个操作,供你参考:

项目标题左侧的绿色圆点(如前面的截图所示)将在同步过程中闪烁。当同步成功完成时,它会变为静态绿色;如果出现问题,则会变为红色。假设一切正常,我们可以继续进行准备运行 playbook 的最后一步。

在 AWX 中定义好项目后,接下来的任务是创建模板,为从该项目运行我们的第一个 playbook 做准备,接下来我们将正好完成这一任务。

在 AWX 中创建模板

AWX 中的模板汇集了你目前已创建的所有其他配置项——本质上,模板是 AWX 对你在命令行中执行 ansible-playbook 命令时所指定的所有参数的定义。

让我们一步步走过创建模板的过程,以便能够运行我们的 playbook:

  1. 在左侧菜单栏中点击 Templates。

  2. 点击绿色的 + 图标创建一个新模板。

  3. 从下拉列表中选择 Job Template。

  4. 给模板起一个合适的名称。

  5. 在 INVENTORY 字段中,选择我们在本章中创建的清单。

  6. 在 PROJECT 字段中,选择我们之前创建的项目。

  7. 在 PLAYBOOK 字段中,注意下拉列表已自动填充了我们在 PROJECT 定义中指定的 GitHub 仓库中所有可用的 playbook 列表。从中选择 lamp_simple_rhel7/site.yml

  8. 最后,在 CREDENTIAL 字段中选择我们之前定义的凭据。

  9. 点击绿色的 SAVE 按钮。

最终结果应该像下图所示,展示了所有字段都已填写的情况:

完成这些步骤后,我们已经完成了运行第一个 AWX 作业所需的一切。因此,我们将在下一节继续执行这一操作并观察结果。

从 AWX 运行 playbook

当我们从 AWX 运行 playbook 时,实际上是在运行一个模板。因此,为了交互式操作,我们需要返回到模板屏幕,在这里可以看到可用模板的列表。请注意,当你使用基于角色的访问控制时,你只能看到你有权限查看的模板(以及清单和其他配置项)——如果你没有权限,它们将不可见。这有助于在不同团队间使用 AWX 时使其更加易于管理。

我们使用的是管理员账户,所以可以看到所有内容。要启动我们新创建的模板,请按照以下说明操作:

  1. 点击模板名称右侧的火箭图标,如下图所示,展示了我们新创建的模板,并高亮显示了执行该模板的选项:

当你执行此操作时,屏幕会自动重新加载,并且你将看到运行的详细信息。不要担心如果你离开此页面——你始终可以稍后通过点击左侧菜单栏上的“Jobs”来再次找到它。由于我们已经定义了这个作业,第一次运行时它会失败。幸运的是,作业面板会显示你在命令行运行 Ansible 时看到的所有相同的详细信息和输出,只是在 AWX 中,这些信息被存档在数据库中,你可以随时回溯它,或者其他用户可以通过登录 AWX(假设他们有相应的权限)来分析它。

  1. 从作业输出中,我们可以看到问题是某种权限问题,下面显示了一张截图,供您参考:

查看 GitHub 上的 playbook 源代码,我们可以看到原作者在此 playbook 中硬编码了使用 root 用户账户(请注意 site.yml 中的 remote_user: root 语句)。通常情况下,你不会这样做——更好的做法是让 Ansible 使用非特权账户登录,然后在需要时通过在 play 头部添加 become: true 语句来使用 sudo(稍后我们会在本书中看到这个操作)。

  1. 为了绕过这个问题,目前我们将允许在 CentOS 7 服务器上通过 SSH 进行 root 登录,然后在 AWX 中修改凭证以使用 root 账户。请注意,你也可以定义一个新凭证并更改与模板相关联的凭证——这两种方案都可以接受。更改凭证后,再次运行模板——这次,输出应该看起来有所不同,正如我们在以下截图中看到的,这显示了 playbook 的成功运行:

正如我们从前面的截图中看到的,我们已经成功运行了 playbook,并且获得了所有相关的细节信息,包括哪个用户启动了它、使用了 GitHub 上的哪个修订版本、使用了哪些凭证、使用了哪个库存等等。向下滚动此面板可以看到我们在之前错误截图中看到的 ansible-playbook 输出;如果我们愿意,还可以进一步分析 playbook 的运行情况,查看是否有任何警告、哪些内容被更改等。因此,通过 AWX,我们真正实现了一个简洁的 Ansible 用户界面,集成了自动化 Linux 的企业环境中应该具备的所有最佳实践,如安全性、可审计性和 Ansible 的集中控制(通过源代码控制集成,甚至是 playbook 代码的集中控制)。

我们已经看到 AWX 如何帮助我们手动运行任务——但是如果我们想要真正的无人干预的任务自动化呢?我们将在本章的下一节探讨任务调度。

使用 AWX 自动化例行任务

虽然 AWX 具有许多方面需要更大的篇幅来讨论,但其中有一项特别突出——例行任务的自动化。Ansible 可以处理的例行任务可能包括服务器的打补丁、运行某种合规性检查或审计,或者执行安全策略。

例如,你可以编写一个 Ansible playbook,确保 SSH 守护进程不允许远程 root 登录,因为这是一个良好的安全实践。当然,任何具有 root 权限的系统管理员都可以登录并重新启用这一功能;然而,定期运行 Ansible playbook 来关闭这个功能,可以强制执行这一策略,并确保没有人(无论动机如何)会重新启用它。Ansible 的幂等性意味着,如果配置已经到位,Ansible 不会做任何更改,因此运行 playbook 是安全的,且对系统资源消耗小,不会中断系统。

如果你想在命令行中使用 Ansible 做到这一点,你需要创建一个 cron 任务,定期运行 ansible-playbook 命令,并且包含所有必需的参数。这意味着需要在处理自动化的服务器上安装 SSH 私钥,并且需要跟踪哪些服务器定期运行 Ansible。这对于一个以良好实践为自动化标杆并确保一切顺利运行的企业来说并不理想。

幸运的是,AWX 也能在这里帮上忙。为了简洁起见,我们将重用本章前面部分中的 LAMP 堆栈示例。在这种情况下,我们可能希望在安静时段安排一次性安装 LAMP 堆栈,而对于常规任务,则会选择持续性计划。

要为此模板设置计划,请按照以下步骤操作:

  1. 点击左侧菜单栏中的 Templates。

  2. 点击我们之前创建的模板。

  3. 点击窗格顶部的 SCHEDULES 按钮。

  4. 点击绿色的 + 图标 以添加一个新的计划。

  5. 设置适当的开始日期和时间——为了演示,我将把时间设置为几分钟后。

  6. 同时,设置适当的时区。

  7. 最后,选择 REPEAT FREQUENCY——在这个例子中,我将选择 None(运行一次),但请注意,从下拉列表中还可以选择其他持续性的选项。

  8. 点击绿色的 SAVE 按钮以激活该计划。

当你完成上述步骤时,生成的配置屏幕应该类似于以下内容:

现在,如果你观察作业窗格,你应该能看到模板在预定时间开始运行。当你分析已完成(或正在运行的)作业时,你应该会看到它是由你之前创建的计划名称启动的,而不是由像 admin 这样的用户帐户启动的(就像我们手动启动时看到的那样)。以下截图展示了一个已完成的作业示例,它是由我们在本节中创建的计划安装计划启动的:

如果你想查看所有即将到来的计划任务,可以点击左侧菜单栏中的 Schedules 菜单项,屏幕将加载并列出所有在 AWX 实例中配置的计划任务。如果你熟悉 Linux 管理,这类似于列出 cron 任务。以下截图展示了这样一个屏幕的示例:

这为你提供了一个简洁的概述,展示了你创建的所有计划,而无需进入各个配置项进行编辑。

通过这种方式,AWX 不仅支持交互式自动化 Linux 环境,还支持无人值守的定时自动化任务,从而增强了自动化解决方案的能力和灵活性。

希望这个概述能让你了解 AWX 或 Ansible Tower 这样的工具能为你的企业带来哪些好处,以及为什么将 Ansible 自动化与这些工具结合使用是有益的。

小结

Ansible 只需少量学习就能提供强大的功能,但当在企业中大规模部署时,管理所有内容变得更加困难,尤其是要追踪哪些用户拥有最新版本的 playbook 代码,谁在何时运行了什么 playbook。AWX 通过带来一些关键功能,如基于角色的访问控制、可审计性、playbook 代码的集成源代码管理、安全的凭据管理和作业调度,来补充 Ansible 在企业中的应用。它在提供易于使用的点选界面的同时,还进一步降低了所有负责 Linux 环境的员工的入门门槛。

在这一章中,你学习了 AWX 对企业 Linux 环境的重要性以及如何利用其一些关键功能。接着,你进行了单个 AWX 节点的实际安装,并完成了从 GitHub 直接运行 playbook 在 CentOS 7 服务器上安装 LAMP 堆栈的实践端到端示例。最后,你学习了作业调度,以便使用 Ansible 自动化日常维护任务。

在下一章,我们将探讨与企业 Linux 环境相关的不同部署方法,以及如何利用这些方法。

问题

  1. 使用 AWX 存储凭据相较于命令行方法的一个关键优势是什么?

  2. 为什么充分利用版本控制系统(如 Git)存储 playbooks 变得如此重要?

  3. 在动态清单方面,AWX 相对于命令行的 Ansible 有哪些优势?

  4. AWX 中的项目是什么?

  5. AWX 中的模板在命令行上类似于什么?

  6. AWX 如何告诉你,playbook 运行时是对 Git 仓库的哪个提交进行的?

  7. 为什么建议限制对托管 AWX 的服务器的访问,特别是对 shell 和本地文件系统的访问?

  8. 如果你需要通过编程方式启动 playbook 运行,AWX 如何帮助你?

深入阅读

第二部分:标准化你的 Linux 服务器

本节通过实际操作展示如何确保一致性和可重复性仍然是你的 Linux 服务器环境的核心特征,促进最佳实践,如可扩展性、可重现性和效率。

本节包括以下章节:

  • 第四章,部署方法论

  • 第五章,使用 Ansible 构建虚拟机模板进行部署

  • 第六章,使用 PXE 启动进行自定义构建

  • 第七章,使用 Ansible 进行配置管理

第四章:部署方法

到目前为止,在本书中,我们已经为您的企业 Linux 环境打下了一个稳定的基础。我们详细讨论了如何通过标准化确保您的 Linux 环境能够很好地支持自动化,以及如何利用 Ansible 和 AWX 来支持您的自动化之旅。在本章深入探讨更详细的技术工作之前,我们必须审视最后一个细节——您的部署方法。

我们已经确定了为您的环境建立少量一致的 Linux 构建的需求。现在,您需要通过一个决策过程——如何将这些构建部署到您的企业中。大多数企业有几种选择,从最简单的——下载公开可用的模板镜像——到构建自己的模板,再到可能最复杂的——使用预启动环境从零开始构建。或者,最好的方法可能是这些方法的某种混合。在本章中,我们将探索这些选项,并了解如何确保您选择最适合您的企业、支持您自动化之旅的高效且易于实施的方法。在后续章节中,我们将深入探讨每种方法的技术细节。

本章将涵盖以下主题:

  • 了解您的环境

  • 保持构建高效

  • 确保 Linux 镜像的一致性

技术要求

本章假设您可以访问运行 Ubuntu 18.04 LTS 的虚拟化环境。一些示例也在 CentOS 7 上执行。在这两种情况下,示例可以在运行上述操作系统之一的物理机器(或笔记本电脑)上运行,该机器启用了虚拟化扩展,或者在启用了嵌套虚拟化的虚拟机上运行。

本章后面还将使用 Ansible 2.8,假设您已在正在使用的 Linux 主机上安装了该版本。

本书中讨论的所有示例代码都可以在 GitHub 上获取:github.com/PacktPublishing/Hands-On-Enterprise-Automation-on-Linux

了解您的环境

没有两个企业环境是相同的。一些企业仍然在很大程度上依赖裸金属服务器,而其他企业则依赖于各种虚拟化或云服务提供商(无论是私有还是公有)。了解可用的环境是决策过程中的关键部分。

让我们探索各种环境以及每种环境的相关构建策略。

部署到裸金属环境

裸金属环境无疑是所有企业环境的“祖父”。在 21 世纪虚拟化和云技术革命之前,构建环境的唯一方式就是在裸金属上构建。

现在,找到一个完全运行在裸机上的环境已经不常见,但通常会发现某些关键组件运行在物理硬件上,特别是数据库或需要某些物理硬件辅助的计算任务(例如,GPU 加速或硬件随机数生成)。

在从裸机构建服务器时,大多数环境下适用两种基本方法。第一种是手动使用光盘或现在更常见的 USB 驱动器构建服务器。这是一个缓慢的交互式过程,无法在大规模上重复,因此不推荐用于除少数物理服务器之外的任何环境,在这些环境中,构建新机器的需求很小且不频繁。

构建大规模、以我们至今在本书中所提倡的一致且可重复的方式的另一个可行选项,是通过网络启动物理服务器,使用 预执行环境PXE)。这涉及从网络服务器加载一个小型的启动环境,然后使用它加载 Linux 内核和相关数据。通过这种方式,可以在没有任何物理介质的情况下启动安装环境。一旦环境启动,我们将使用无人值守安装方法,使安装在无需用户干预的情况下完成。

本书稍后将详细介绍这些方法,以及一旦构建好服务器后配置服务器的可重复技术。与此同时,简而言之,对于在企业中构建物理 Linux 服务器,结合 PXE 启动和无人值守安装的方法是最容易自动化并且能产生最可重复结果的途径。

部署到传统的虚拟化环境

传统虚拟化环境是指早于我们今天所知的云环境的那些——也就是说,它们是运行操作系统的简单虚拟机监控程序。像 VMware 这样的商业实例很常见,还有像 Xen 和 KVM 这样的开源实例(以及基于这些的框架,如 oVirt)。

由于这些技术最初是为补充传统物理环境而构建的,因此它们为构建您的企业 Linux 系统提供了几种可能的选项。例如,这些平台大多数支持与裸机相同的网络启动功能,因此我们实际上可以假装它们是裸机,并继续使用网络启动方法。

然而,虚拟化环境引入了一些在物理环境中很难实现的功能,因为它们运行的裸金属设备之间存在硬件差异——模板。模板化虚拟机可以简单理解为一个可部署的预配置虚拟机快照。因此,你可以为企业构建完美的 CentOS 7 镜像,集成监控平台,执行所有所需的安全加固,然后使用虚拟化平台本身内置的工具将其转化为模板。以下是作者实验室环境中 CentOS 7 模板的截图:

这些模板都是完全配置好的 CentOS 7 基础镜像,准备好进行部署,并且所有部署前的工作(如删除 SSH 主机密钥)都已经完成。因此,管理员只需选择合适的模板并点击“新建虚拟机”按钮——在除 RHV 外的大多数平台上,这个过程都类似,因为大多数主流虚拟化解决方案都会提供类似的功能。

请注意,为了保持示例的可访问性,我使用了图形用户界面(GUI)作为创建新虚拟机的主要过程。几乎所有虚拟化和云平台都提供 API、命令行接口,甚至 Ansible 模块,可以用来部署虚拟机,而在企业环境中,这些方法比 GUI 更适合大规模应用。鉴于环境的多样性,这部分内容留给你自行探索。

这个过程本身相对简单,但需要一些细心和注意。例如,几乎所有的 Linux 服务器现在都启用了 SSH,每台服务器上的 SSH 守护进程都有一个唯一的主机标识密钥,用来防止(除其他之外)中间人攻击。如果你将一个预配置的操作系统模板化,你也将模板化这些密钥,这就意味着在环境中很可能会出现密钥重复的情况,这会显著降低安全性。因此,在将虚拟机转化为模板之前,执行一些准备步骤非常重要,而其中一个常见的步骤就是删除 SSH 主机密钥。

使用 PXE 方法创建的服务器不会遇到这个问题,因为它们都是从头开始安装的,因此没有历史日志需要清理,也没有重复的 SSH 密钥。

在第五章中,使用 Ansible 构建虚拟机模板进行部署,我们将详细讲解如何创建适合使用 Ansible 模板的虚拟机模板。尽管 PXE 启动和模板部署方法在虚拟化环境中都同样有效,大多数人认为模板化的方式更高效、更易于管理,因此我也推荐这种方法(例如,大多数 PXE 启动环境需要知道部署的物理或虚拟服务器所使用的网络接口的 MAC 地址——但在模板部署中,这不是必须的步骤)。

部署到云环境

最近对企业 Linux 架构的最新采用者(当然,容器是另一个完全不同的话题)是云资源配置环境。这可能通过一个公共云解决方案来实现,例如Amazon Web ServicesAWS)、Microsoft Azure、Google Cloud PlatformGCP)或近年来涌现出的无数小型服务提供商。也可能是通过一个本地部署的解决方案,如 OpenStack 项目的某个变体或专有平台。

这些云环境彻底改变了 Linux 机器在企业中的生命周期。在裸机或传统虚拟化架构下,Linux 机器需要被照顾、培育和修复(如果它们发生故障),而云架构的前提是每台机器基本上都是可替代的,如果它失败了,只需要在其位置部署一台新的机器。

因此,PXE 部署方法在这样的环境中甚至不可行,它们依赖于预构建的操作系统镜像。这些本质上只是一个由第三方供应商创建的模板,或者由企业准备的模板。

无论你选择商业云提供商,还是搭建本地部署的 OpenStack 架构,你都会找到一个可供选择的操作系统镜像目录。通常,云服务提供商自己提供的镜像是值得信赖的,尽管根据你的安全需求,你也可能发现由外部方提供的镜像同样合适。

例如,下面是 OpenStack 可用的推荐操作系统镜像的截图:

正如你从目录中看到的,几乎所有主要的 Linux 发行版都在此列出,这立即为你节省了构建基本操作系统的任务。AWS 也是如此:

简而言之,如果你正在使用云环境,你将有很多基础操作系统镜像可供选择,以此开始工作。尽管如此,这种选择不太可能满足所有企业的需求。例如,使用预构建的云环境镜像并不意味着可以忽视企业安全标准、监控或日志转发代理集成等要求,以及许多其他对于企业至关重要的事项。在继续之前,值得注意的是,当然你可以为所选的云平台创建自己的镜像。然而,出于效率考虑,为什么要重新发明轮子?如果有人已经为你完成了这一步,你可以有效地将这项工作委托给别人。

尽管大多数现成的操作系统镜像是可信的,但在选择一个新镜像时你应该始终保持谨慎,特别是当它是由你不熟悉的作者创建时。我们无法确定该镜像具体包含了什么内容,因此在选择要使用的镜像时,你应该始终进行尽职调查。

假设你确实选择继续使用一个预制的云环境镜像,那么后安装配置工作可以通过 Ansible 整理完成。事实上,所需的步骤与构建传统虚拟化平台模板的步骤几乎相同,我们将在本书稍后的部分再次详细介绍这个过程。

Docker 部署

Docker 部署在我们关于 Linux 环境的讨论中是一个特殊案例。从实际操作来看,它与云环境有很多共同点——Docker 镜像是基于现有的最小化操作系统镜像构建的,通常使用原生 Docker 工具链构建,尽管完全可以使用 Ansible 实现自动化。

由于 Docker 是一个特殊案例,我们不会在本书中深入探讨它,尽管值得注意的是,Docker 作为 Linux 在企业中最近的“新成员”,实际上是围绕本书中已经考虑的许多原则设计的。让我们简要地考虑用于创建官方 nginx 容器的 Dockerfile。

对于那些不熟悉 Docker 的人来说,Dockerfile 是一个纯文本文件,包含了构建容器镜像所需的所有指令和命令,用于部署。

截至写作时,该文件包含以下内容:

#
# Nginx Dockerfile
#
# https://github.com/dockerfile/nginx
#

# Pull base image.
FROM ubuntu:bionic

# Install Nginx.
RUN \
  add-apt-repository -y ppa:nginx/stable && \
  apt-get update && \
  apt-get install -y nginx && \
  rm -rf /var/lib/apt/lists/* && \
  echo -e "\ndaemon off;" >> /etc/nginx/nginx.conf && \
  chown -R www-data:www-data /var/lib/nginx

尽管不是基于 Ansible,我们可以在前面的代码块中看到以下内容:

  1. 顶部的 FROM 行定义了一个最小的 Ubuntu 基础镜像,用于执行其余配置——这可以看作是我们在其他平台讨论的 SOE Linux 镜像。

  2. RUN 命令执行安装 nginx 包所需的步骤,并进行一些清理工作,以保持镜像的整洁和简洁(减少空间需求和杂乱)。

代码继续如下:

# Define mountable directories.
VOLUME ["/etc/nginx/sites-enabled", "/etc/nginx/certs", "/etc/nginx/conf.d", "/var/log/nginx", "/var/www/html"]

# Define working directory.
WORKDIR /etc/nginx

# Define default command.
CMD ["nginx"]

# Expose ports.
EXPOSE 80
EXPOSE 443

继续分析这个文件,我们可以看到以下内容:

  1. VOLUME 行定义了哪些来自主机文件系统的目录可以挂载到容器内。

  2. WORKDIR 指令告诉 Docker 容器在哪个目录下运行接下来的 CMD ——可以把它看作是启动时的配置。

  3. CMD 行定义了容器启动时要运行的命令——这与在完整的 Linux 系统镜像中定义在启动时哪些服务会启动的过程是一个缩影。

  4. 最后,EXPOSE 行定义了容器应该向网络暴露的端口——这或许有点像防火墙可能允许某些端口通过。

简而言之,构建 Docker 容器的原生过程与我们为企业 Linux 环境定义的构建过程非常契合——因此,我们可以自信地继续这个过程。考虑到这一点,我们将探讨确保构建尽可能简洁高效的过程。

保持构建高效

如我们在上一节中讨论的,了解你 Linux 环境的基本知识对制定部署方法至关重要。尽管构建过程本身(尤其是传统的虚拟化环境与云环境之间)存在一些相似之处,但了解这些差异能帮助你在全企业范围内做出有关如何部署 Linux 的明智决策。

一旦你选择了最适合自己环境的方法,考虑一些原则以确保你的过程流畅高效是非常重要的(这也是企业 Linux 部署的关键要素)。我们将在这里介绍这些原则,以便在本书的后续部分深入探讨实际操作。让我们从构建简洁性的重要性开始。

保持构建简洁

让我们开始将之前讨论的 SOE(标准操作环境)对 Linux 构建过程的重要性应用到实际中。不论你选择哪条路径,环境如何,其中一个关键方面是确保你的构建标准尽可能简单明了。

每个企业环境都是独一无二的,因此每个企业的构建需求肯定也会不同。尽管如此,这里提供了一组常见的示例需求,以展示在构建过程中可能需要的内容:

  • 监控代理

  • 日志转发配置

  • 安全加固

  • 核心企业软件需求

  • 用于时间同步的 NTP 配置

这个列表只是一个开始,每个企业的需求都不同,但它能让你了解构建过程中可能涉及的内容。不过,接下来我们将开始探讨一些构建过程中的边缘案例。可以公平地说,每个 Linux 服务器的构建都有其特定的目的,因此将运行某种形式的应用栈。

同样,应用栈在不同企业之间肯定会有所不同,但可能常见的应用示例如下:

  • 一个 Web 服务器,如 Apache 或 nginx

  • 用于 Java 工作负载的 OpenJDK 环境

  • 一个 MariaDB 数据库服务器

  • 一个 PostgreSQL 数据库服务器

  • NFS 文件共享工具和内核扩展

现在,在你的标准化过程中,当你最初定义 SOE 时,你可能甚至已经指定了使用(仅作为例子)OpenJDK 8 和 MariaDB 10.1。这是否意味着你应该在你的构建过程中实际包含它们?

答案几乎总是。简单来说,添加这些应用程序会增加构建的复杂性以及后期配置和调试的复杂性。它还会降低安全性——稍后会详细解释这一点。

假设我们标准化使用 MariaDB 10.1,并将其包含在基础操作系统镜像中(因此每台部署的 Linux 机器都包含它),但我们知道,实际使用它的机器仅为一部分。

不将 MariaDB 包含在基础镜像中的原因有几个:

  • 安装仅包含 MariaDB 10.1 服务器组件的大小大约为 120 MB,具体取决于操作系统和打包方式——还会有依赖包,但我们先从这个数字开始。虽然现在存储便宜且充足,如果你在环境中部署了 100 台服务器(对于大多数企业来说实际上是很少的),那么大约有 11.7 GB 的空间将被专门用于一个你不需要的包。实际数字会更高,因为还需要安装依赖包等。

  • 这也可能对备份和所需的存储空间产生连锁反应,实际上,如果你在企业中使用虚拟机快照,也会有影响。

  • 如果有应用程序要求使用 MariaDB 10.3(或者实际上,业务决定将其标准更新为 10.3),那么需要升级镜像,或者可能在安装 10.3 之前卸载版本 10.1。这会增加不必要的复杂性,因为一个最小的 Linux 镜像本来只需要接收更新后的 MariaDB 工作负载。

  • 你需要确保当不需要时关闭 MariaDB 并对其进行防火墙隔离,以防止任何滥用——这是一个额外的审计和执行要求,而在许多不使用 MariaDB 的服务器上,这种要求是多余的。

还有其他的安全考虑,但这里的关键是,这样做会浪费资源和时间。当然,这不仅仅适用于 MariaDB 10.1——这只是一个例子,但它表明,一般来说,应用程序工作负载不应该包含在基础操作系统定义中。现在让我们更详细地看看构建的安全要求。

让你的构建更安全

我们已经提到过安全性以及不安装或运行不必要的软件包。任何正在运行的服务都为入侵者提供了潜在的攻击途径,尽管希望你永远不会在企业网络内遭遇此类入侵,但仍然最好以尽可能安全的方式构建环境。对于那些默认配置了密码的服务(有些情况下甚至没有配置密码——幸运的是,这种情况现在已经变得罕见),尤其如此。

这些原则同样适用于定义构建本身。例如,不要创建具有弱静态密码的构建。理想情况下,每个构建都应该配置为从外部来源获取初始凭证,尽管有许多方法可以实现这一点,但如果这是一个新概念,建议你查阅一下cloud-init。在某些情况下,尤其是在旧版环境中,可能需要一些初始凭证来允许访问新构建的服务器,但重用弱密码是危险的,这会让新建的服务器在配置之前被拦截,并可能被植入某种恶意软件。

简而言之,以下列表提供了一些确保安全构建的有力指导:

  • 不要安装不需要的应用程序或服务。

  • 确保所有构建中通用的服务(但需要部署后配置的服务)默认情况下是禁用的。

  • 如果可能的话,不要重用密码,即使是用于初始访问和配置。

  • 在过程尽早应用你的企业安全政策——如果可能的话,最好在镜像或服务器的构建过程中应用,但如果不行,也要在安装后尽早应用。

这些原则简单但至关重要,遵守它们非常重要。希望永远不会出现需要证明这些原则是否得到应用的情况,但如果发生了,这些原则可能会阻止或足够遏制对你基础设施的入侵或攻击。当然,这个话题值得单独写一本书,但希望这些提示以及第十三章《使用 CIS 基准》的示例,能为你指引正确的方向。现在,我们简要了解一下如何确保构建过程高效。

创建高效的流程

高效的流程主要依赖自动化,因为自动化可以确保最小化人为干预,并保证一致且可重复的最终结果。标准化也对此提供支持,因为它意味着大部分决策过程已经完成,因此所有参与的人都清楚自己在做什么以及该怎么做。

简而言之,遵循本书中概述的这些原则,你的构建流程自然会高效。即使是在选择唯一主机名(尽管这可以自动化)或用户请求一个 Linux 服务器时,某种程度的人工干预是不可避免的。然而,从此以后,你应该尽可能地进行自动化和标准化。我们将在本书中始终遵循这一理念。现在,我们先来看看构建过程中一致性的重要性。

确保 Linux 镜像的一致性

在第一章《在 Linux 上构建标准操作环境》中,我们讨论了 SOE 环境中共性的重要性。现在,我们实际上在查看构建过程本身,这一问题再次浮现,因为我们首次着眼于如何实际实现共性。假设 Ansible 是你首选的工具,考虑以下任务。我们正在为我们的镜像构建过程编写 playbook,并已决定我们的标准镜像需要与本地时间服务器同步时间。假设由于历史原因,我们选择的基础操作系统是 Ubuntu 16.04 LTS。

让我们创建一个简单的角色,以确保安装 NTP 并复制我们的公司标准 ntp.conf,其中包括我们内部时间服务器的地址。最后,我们需要重启 NTP 以应用这些更改。

本章中的示例纯粹是假设性的,目的是展示某个特定任务所需的 Ansible 代码。我们将在后续章节中详细扩展所执行的任务(如部署配置文件),并提供实际的示例供你尝试。

这个角色可能如下所示:

---
- name: Ensure ntpd and ntpdate is installed
  apt:
    name: "{{ item }}"
    update_cache: yes
  loop:
    - ntp
    - ntpdate
- name: Copy across enterprise ntpd configuration
  copy:
    src: files/ntp.conf
    dest: /etc/ntp.conf
    owner: root
    group: root
    mode: '0644'
- name: Restart the ntp service
  service:
    name: ntp
    state: restarted
    enabled: yes

这个角色简单、简洁且直截了当。它始终确保安装ntp包,并且确保我们复制相同版本的配置文件,确保每台服务器上的配置文件都是一致的。我们可以通过从版本控制系统中检出这个文件进一步改进,但这部分留给你作为练习。

立刻,你就能看到为这一个简单步骤编写 Ansible 角色的强大之处——通过在 playbook 中包含这个角色,可以实现很高的一致性,且如果你将这种方法推广到整个企业,那么所有配置的服务将会被一致地安装和配置。

然而,事情更好了。假设业务决定将标准操作系统基础重定为 Ubuntu 18.04 LTS,以利用更新的技术并延长环境的支持寿命。ntp包在 Ubuntu 18.04 中仍然可用,但默认情况下,chrony包已被安装。为了继续使用 NTP,该角色只需要做一些小的调整,确保首先删除(或如果你更喜欢,也可以禁用)chrony包——之后,它就完全相同了。例如,考虑以下角色代码,它确保了正确的包被删除或安装:

---
- name: Remove chrony
  apt:
    name: chrony
    state: absent
- name: Ensure ntpd and ntpdate is installed
  apt:
    name: "{{ item }}"
    update_cache: yes
  loop:
    - ntp
    - ntpdate

然后我们会继续添加两个任务,将配置复制过去并重新启动服务,确保它采纳新的配置:

- name: Copy across enterprise ntpd configuration
  copy:
    src: files/ntp.conf
    dest: /etc/ntp.conf
    owner: root
    group: root
    mode: '0644'
- name: Restart the ntp service
  service:
    name: ntp
    state: restarted
    enabled: yes

或者,我们也可以决定接受这个变化,并在新的基础镜像上使用chrony。因此,我们只需要创建一个新的chrony.conf,确保它与我们的企业 NTP 服务器通信,然后按照之前的步骤继续操作:

---
- name: Ensure chrony is installed
  apt:
    name: chrony
    update_cache: yes
- name: Copy across enterprise chrony configuration
  copy:
    src: files/chrony.conf
    dest: /etc/chrony.conf
    owner: root
    group: root
    mode: '0644'
- name: Restart the chrony service
  service:
    name: chrony
    state: restarted
    enabled: yes

注意这些角色有多么相似?即使支持底层操作系统或服务发生变化,也只需做出细微的调整。

尽管这三种角色在某些地方有所不同,但它们都执行相同的基本任务,具体如下:

  1. 确保已安装正确的 NTP 服务。

  2. 复制标准配置。

  3. 确保该服务在启动时启用并已启动。

因此,我们可以确信,采用这种方法,我们能够保持一致性。

即使完全更换平台,高级方法仍然可以适用。假设企业现在使用了仅支持 CentOS 7 的应用程序。这意味着我们的 SOE 发生了一个接受的偏差,但是,即便是新的 CentOS 7 构建,也需要确保时间正确。由于 NTP 是一个标准,它仍然会使用相同的时间服务器。因此,我们可以编写一个角色来支持 CentOS 7:

---
- name: Ensure chrony is installed
  yum:
    name: chrony
    state: latest
- name: Copy across enterprise chrony configuration
  copy:
    src: files/chrony.conf
    dest: /etc/chrony.conf
    owner: root
    group: root
    mode: '0644'
- name: Restart the chrony service
  service:
    name: chronyd
    state: restarted
    enabled: yes

再次强调,这些变化非常微妙。这也是我们选择 Ansible 作为企业自动化工具的一个重要原因——我们能够轻松地构建并遵循标准,而且当我们更改 Linux 版本甚至整个发行版时,操作系统的构建仍然保持一致。

总结

在这一阶段,我们已经定义了标准化的要求,确定了在实现自动化过程中使用的工具,并且现在对企业可能部署操作系统的基本环境类型进行了实际的探讨。这为我们的自动化之旅奠定了基础,并为本书的其余部分提供了背景——通过动手实践,了解在企业中构建和维护 Linux 环境的过程。

在本章中,我们了解了 Linux 可能部署的不同类型的环境以及每种环境的不同构建策略。然后,我们查看了一些实际的示例,确保我们的构建符合高标准,并且能够高效且可重复地完成。最后,我们开始探讨自动化的好处,以及它如何确保即使我们更换整个底层 Linux 发行版,构建的一致性也能得到保证。

在下一章,我们将开始动手实践企业级 Linux 自动化和部署,探讨如何使用 Ansible 来构建虚拟机模板,无论是从云环境镜像还是从头开始构建。

问题

  1. 构建 Docker 容器和 SOE 之间有什么相似之处?

  2. 如果 MariaDB 仅在少数服务器上需要,为什么不将其包含在基础构建中?

  3. 如何确保您的基础操作系统镜像尽可能小?

  4. 为什么在基础操作系统镜像中嵌入密码时需要小心?

  5. 如何确保所有 Linux 镜像将其日志发送到您的集中日志服务器?

  6. 在什么情况下您不会使用云提供商提供的基础镜像,而是选择构建自己的镜像?

  7. 如何使用 Ansible 安全配置您的 SSH 守护进程?

进一步阅读

第五章:使用 Ansible 构建用于部署的虚拟机模板

到目前为止,在本书中,我们已详细介绍了其余部分的基础工作——也就是说,我们已经为接下来的操作奠定了理论基础,并且提供了关于我们选择的自动化工具 Ansible 的速成课程。从前一章中我们知道,在企业规模的环境中,部署 Linux 有两种基本方法,选择哪种方法取决于你环境中使用的技术以及你的目标。

在本章中,我们将详细介绍如何构建适用于大多数虚拟化和云平台的虚拟机镜像。这两个平台之间的差异微妙但明显,我们将在本章结束时了解到这一点,并且你将学会轻松处理这两种环境。我们将从讨论初始构建需求开始,然后继续配置并准备镜像以便在你选择的环境中使用。

本章将涵盖以下主题:

  • 执行初始构建

  • 使用 Ansible 构建和标准化模板

  • 使用 Ansible 清理构建

技术要求

本章假设你可以访问运行 Ubuntu 18.04 LTS 的虚拟化环境。一些示例也会在 CentOS 7 上执行。在这两种情况下,示例可以在运行上述操作系统的物理机器(或笔记本电脑)上执行,只要该进程启用了虚拟化扩展,或者在启用了嵌套虚拟化的虚拟机上执行。

本章后续也会使用 Ansible 2.8,并假设你已经在所使用的 Linux 主机上安装了该版本。

本章中讨论的所有示例代码可以从 GitHub 获取:github.com/PacktPublishing/Hands-On-Enterprise-Automation-on-Linux/tree/master/chapter05

执行初始构建

正如在第四章《部署方法》中讨论的那样,部署方法,无论你使用传统的虚拟化平台(如 oVirt 或 VMware)还是基于云的平台(如 OpenStack 或亚马逊的 EC2),你在任何 Linux 部署(以及随后的自动化)中的起点都会是一个模板镜像。

就我们在第一章中定义的 SOE 而言,在 Linux 上构建标准操作环境,模板化镜像就是这一环境的实际初步表现。它通常是一个小型虚拟机镜像,安装了足够的软件并完成了配置,能够在企业的几乎所有部署场景中发挥作用。只要镜像能干净地启动,并且具有唯一的主机名、SSH 主机密钥等,那么它几乎可以立即通过进一步的自动化进行定制,正如我们将在本书的第七章中通过使用 Ansible 进行配置管理了解到的那样。让我们通过采用一个现成的模板镜像(由第三方提供)作为起点来深入了解构建过程。

使用现成的模板镜像

对于大多数平台,有大量现成的镜像可以下载,正如我们在上一章中讨论的那样。对于许多企业来说,这些镜像已经足够。但如果你绝对需要完全控制镜像定义呢?也许你正在采用一个新的标准(在写作时,Red Hat Enterprise Linux 8 刚刚发布,而 CentOS 8 也将在适当时候发布),并且你希望尽早实现它以获得经验并测试工作负载。如果你在一个安全的环境中工作(可能符合支付卡行业标准),并且你必须对镜像的构建过程 100%有信心,不能有任何被妥协的风险,又该如何处理?

当然,这并不是说任何公开可用的图像都有被妥协的风险,甚至不太可能发生这种情况,但历史上确实出现过少数的中间人供应链攻击,攻击者通过攻击常用的组件间接妥协了服务,而非直接攻击服务本身。

大多数公开可用的镜像都来自可信的来源,这些来源已经实施了各种检查和控制,以确保其完整性。只要你利用这些检查,并对下载的任何镜像进行尽职调查,大多数企业会发现几乎没有必要从零开始创建自己的镜像,因为像 Ansible 这样的自动化工具会处理所有部署后的配置。

让我们通过一个实际的例子来说明:假设在一组新的部署中,我们决定基于 Fedora 30 服务器镜像创建一个标准操作环境(SOE),并将在 OpenStack 基础设施上运行:

  1. 我们将从 Fedora 项目官方网站下载云镜像—具体详情可以在这里找到,注意版本号会随着 Fedora 新版本的发布而变化,网址为:alt.fedoraproject.org/cloud/

在为我们的环境确定了正确的 Fedora 云镜像之后,我们可以通过如下命令下载所需的镜像:

$ wget https://download.fedoraproject.org/pub/fedora/linux/releases/30/Cloud/x86_64/images/Fedora-Cloud-Base-30-1.2.x86_64.qcow2
  1. 很简单——现在,让我们进行验证。验证说明通常会随所有主要 Linux 发行版提供,无论是 ISOs 还是完整的镜像文件,我们的 Fedora 镜像下载的验证说明可以在 alt.fedoraproject.org/en/verify.html找到。

让我们逐步执行过程并验证我们的镜像。首先,我们将导入官方的 Fedora GPG 密钥来验证校验和文件,以确保它没有被篡改:

$ curl https://getfedora.org/static/fedora.gpg | gpg --import
  1. 现在我们将下载云基础镜像的校验和文件并进行验证:
$ wget https://alt.fedoraproject.org/en/static/checksums/Fedora-Cloud-30-1.2-x86_64-CHECKSUM
$ gpg --verify-files *-CHECKSUM
  1. 虽然你可能会收到关于密钥未通过可信签名认证的警告(这是 GPG 密钥信任建立方式的一个方面),但重要的是文件的签名已被验证为有效——请参阅下面的截图,了解输出的示例:

  1. 只要签名验证成功,最后一步就是使用以下命令将实际镜像与校验和进行验证:
$ sha256sum -c *-CHECKSUM

对于任何你没有下载的 *-CHECKSUM 文件中的文件,你会收到错误。但正如下面截图所示,我们下载的镜像与文件中的校验和匹配,因此可以继续使用它:

完成这些步骤后,我们可以继续在我们的 OpenStack 平台上使用下载的镜像。当然,部署后你可能想要定制这个镜像,后续我们会探讨如何进行定制。选择了一个现成的镜像并不意味着它必须保持原样。请注意,这些步骤在不同的 Linux 发行版中会有一些细微的差异,但整体流程应该是相同的。重要的是要验证所有下载的镜像。

使用公共操作系统镜像时,也有一个关于信任的问题。你怎么知道作者已经移除了所有冗余服务并正确地进行了 sysprep 操作?你怎么知道其中没有后门或其他漏洞?尽管有很多优秀的公共可用镜像,但你在下载任何镜像时应始终进行尽职调查,确保它们适合你的环境。

那么如果你必须生成自己的镜像呢?我们将在本章的下一部分进行探讨。

创建你自己的虚拟机镜像

前述过程适用于许多企业,但迟早会有需求,要求创建自己完全定制的虚拟机镜像。幸运的是,现代 Linux 发行版使得这一过程变得简单,你甚至不需要在与构建镜像相同的平台上操作。

让我们来看一下如何使用 Ubuntu 18.04 Server 主机构建 CentOS 7.6 虚拟机镜像:

  1. 在我们开始之前,第一步是确保构建主机能够运行虚拟机——这通常是一组包含在大多数现代 x86 系统中的 CPU 扩展。也可以通过嵌套虚拟化来构建虚拟机镜像,即在另一个虚拟机内创建虚拟机。然而,要做到这一点,你必须在构建虚拟机中启用虚拟化支持。这个过程因虚拟化平台不同而异,因此我们在这里不进行详细介绍。

如果你使用 VMware 虚拟化平台来执行嵌套虚拟化,你还需要启用 代码分析 支持以及启用 虚拟化应用程序——否则,这一过程中的某些步骤将无法成功。

  1. 一旦你的构建主机启动并运行,你需要安装 Linux 基于内核的虚拟机KVM)工具集——执行这些命令的方式会根据你的构建主机的 Linux 版本有所不同,但在我们的 Ubuntu 主机上,我们需要运行以下命令:
$ sudo apt-get install libvirt-bin libvirt-doc libvirt-clients virtinst libguestfs-tools libosinfo-bin
$ sudo gpasswd -a <your account> libvirt
$ sudo gpasswd -a <your account> kvm
$ logout

请注意,需要将你的用户帐户添加到两个与 KVM 相关的用户组——你还需要注销并重新登录,以使这些用户组变更生效。

  1. 完成此操作后,你还需要下载你选择的 Linux 镜像的本地 ISO 副本。我使用以下命令下载 ISO 镜像,这对于我要创建的 CentOS 7.6 SOE 镜像来说已足够:
$ wget http://vault.centos.org/7.6.1810/isos/x86_64/CentOS-7-x86_64-Minimal-1810.iso
  1. 将这些所有步骤准备好后,你现在将创建一个空的虚拟机磁盘镜像。最适合选择的格式是 快速写时复制QCOW2)格式,这与 OpenStack 和大多数公共云平台兼容。因此,我们将尽量使这个镜像尽可能通用,以便支持尽可能广泛的环境。

要在当前目录中创建一个空白的 20 GB QCOW2 镜像,我们可以运行以下命令:

$ qemu-img create -f qcow2 centos76-soe.qcow2 20G

请注意,其他镜像格式也是可用的。例如,如果你只为 VMware 构建镜像,那么使用 VMDK 格式会更合适:

$ qemu-img create -f vmdk centos76-soe.vmdk 20G

请注意,这两个命令创建的是稀疏镜像——也就是说,它们的大小仅与其包含的数据和元数据大小相等。如果你愿意,它们以后可以通过你选择的虚拟化平台转换为预分配的镜像:

创建好空的磁盘镜像后,是时候安装虚拟机镜像了:

  1. 我们将使用virt-install命令来实现这一点,该命令基本上会启动一个临时虚拟机来进行操作系统安装。不要担心像 CPU 和内存等参数——只要它们足够支持操作系统的安装,它们就没问题——这些不会影响已部署的虚拟机。

请注意--graphics vnc,listen=0.0.0.0选项中使用了 VNC——我们将使用它来远程控制虚拟机并完成安装。如果你喜欢,也可以选择其他图形选项,如 SPICE。

  1. 以下命令是如何使用virt-install从我们之前下载的 ISO 创建 CentOS 7 镜像的示例,使用我们之前创建的 20 GB QCOW2 磁盘镜像:
$ virt-install --virt-type kvm \
--name centos-76-soe \
--ram 1024 \
--cdrom=CentOS-7-x86_64-Minimal-1810.iso \
--disk path=/home/james/centos76-soe.qcow2,size=20,format=qcow2 \
--network network=default \
--graphics vnc,listen=0.0.0.0 \
--noautoconsole \
--os-type=linux \
--os-variant=centos7.0 \
--wait=-1

这些参数大多数都是自解释的,但请特别注意你的环境。例如,如果你编辑或删除了default网络,前面的命令将会失败。类似地,确保所有引用文件的路径是正确的。

要查看支持的--os-variant参数列表,请运行osinfo-query os命令。

自然地,你需要根据所安装的操作系统、磁盘镜像名称等来调整这些参数。

  1. 现在,我们运行这个命令——如果成功,它会提示你可以连接到虚拟机控制台以继续操作:

  1. 我们现在将使用virt-viewer工具从另一个终端连接到它:
$ virt-viewer centos-76-soe

从这里开始,你将以正常方式安装操作系统。正如我们在第四章《部署方法论》中讨论过的,尽量选择最小化安装。不要太担心主机名等问题,因为这些应该在后续的部署过程中设置;请指定以下内容:

  1. 选择与本地环境最相关的键盘和语言支持

  2. 选择适合你所在国家的日期与时间设置。

  3. 确保软件选择为“最小化安装”(这是默认选项)。

  4. 设置安装目标——使用前面的virt-install命令,只有一个虚拟硬盘附加到该虚拟机,因此只需选择它即可。

  5. 根据需要启用或禁用 KDUMP。

  6. 确保在网络与主机名称中启用网络。

结果应该是一个 CentOS 7 安装设置界面,类似于下面的截图:

允许安装按正常流程完成,然后登录到你刚创建的虚拟机。一旦登录到运行中的虚拟机,你应该进行所有希望出现在最终虚拟机模板中的定制设置。在本章的下一节中,我们将介绍如何使用 Ansible 配置已部署的虚拟机,并且使用 Ansible 构建模板没有什么不同——因此,为了避免与后续章节的内容重叠,我们这里不会详细讲解 Ansible 配置工作。

当你的虚拟机在初始安装后重新启动时,你可能会发现它关闭了。如果发生这种情况,你需要使用virsh工具将其取消定义,然后使用我们之前的virt-install命令的轻微变种重新运行它,并告诉virt-install这次从硬盘镜像启动,而不是从光盘启动:

$ virsh undefine centos-76-soe
$ virt-install --virt-type kvm \
--name centos-76-soe \
--ram 1024 \
--disk path=/home/james/centos76-soe.qcow2,size=20,format=qcow2 \
--network network=default \
--graphics vnc,listen=0.0.0.0 \
--noautoconsole \
--os-type=linux \
--os-variant=centos7.0 \
--boot=hd

在这个阶段需要注意的是,大多数云平台,无论是 OpenStack、Amazon Web ServicesAWS)还是其他平台,都使用 cloud-init 工具来执行虚拟机镜像部署并运行后的初始配置。因此,作为最低要求,我们将在关闭虚拟机之前将其安装到我们的虚拟机镜像中。以下是手动安装该工具所需的命令,接下来我们将把它转化为 Ansible 角色进行安装:

$ yum -y install epel-release
$ yum -y install cloud-init cloud-utils-growpart dracut-modules-growroot

当你成功执行这些命令后,你可能需要自定义 /etc/cloud/cloud.cfg 以配置 cloud-init,适配你将使用的环境,尽管默认配置已经适用于许多环境,并且可以作为良好的起点。

配置 cloud-init 作为练习留给你,因为云平台种类繁多。

最后,当你完成任何其他自定义配置后,你可以关闭虚拟机。确保干净地关闭虚拟机,而不是直接断电,因为这将成为一个模板,用于大规模部署。

一旦虚拟机关闭,接下来的步骤是对镜像执行 系统准备sysprep),然后压缩稀疏镜像文件,使其尽可能小,以便分发和存档。

Sysprep 过程是为大规模部署准备镜像。因此,所有唯一可识别的参数将被清除,以生成一个干净的镜像用于大规模部署,具体包括以下内容:

  • SSH 主机密钥

  • 历史文件

  • 本地会话配置

  • 日志文件

  • 网络配置中的 MAC 地址引用

前面的列表并不是详尽无遗的——实际上还有很多需要清理的项目,才能确保镜像真正干净并准备好部署,解释这些内容将需要一个完整的章节。幸运的是,我们可以使用 KVM 工具集中的两个命令来执行这些任务:

$ sudo virt-sysprep -a centos76-soe.qcow2
$ sudo virt-sparsify --compress centos76-soe.qcow2 centos76-soe-final.qcow2

尽管第一个命令的输出太长,无法放在一张截图中,但它展示了作为 sysprep 过程的一部分,所需执行的各种任务。如果你发现自己手动或使用 Ansible 执行该过程,virt-sysprep 工具将为你提供有关应执行任务的良好指导:

最后,我们再次将磁盘镜像稀疏化,实际上是压缩它,以便高效存储。请注意,如果在运行此工具时收到任何免费空间警告(默认情况下它需要在 /tmp 中大量空间——具体大小由虚拟磁盘镜像的大小决定),你通常不应忽略这些警告,因为工具可能会填满你的分区,从而导致构建主机无法正常工作:

本章这一部分执行的步骤应该适用于几乎任何 Linux 发行版,并且可以在几乎任何 Linux 主机上构建。像往常一样,参考你首选的发行版文档,获取有关软件包名称的指导。然而,通过遵循这个过程,你现在已经成功构建了一个完全定制的云镜像,应该能够将其上传到许多流行的云平台和虚拟化平台。

从这里开始,我们将更加详细地了解如何使用 Ansible 自定义模板,而不是像本节中那样手动输入命令。

使用 Ansible 构建和标准化模板

到现在为止,你应该已经有了一个用于部署到企业中的基础 Linux 镜像。如果你选择下载一个现成的模板(或者实际上,使用公共云提供商提供的模板),那么你的镜像将非常类似于一个空白模板,准备好进行定制。如果你选择自己构建,那么你可能已经选择执行了一些小的定制,如我们之前安装的cloud-init。然而,你会注意到,我们是手动进行的,这与我们在本书早期部分所推崇的可扩展、可重复和可审计的过程有些不一致。随着我们继续本章的这一部分,我们将了解如何使用 Ansible 定制基础模板,无论它的来源如何。

没有一种适用于所有人的 Linux 镜像,因此,本章并不是一个终极指南。然而,我们将查看一些与定制部署镜像相关的常见任务,如下所示:

  • 将文件传输到镜像中

  • 安装软件包

  • 编辑配置文件

  • 验证镜像

通过这些示例的结合,大多数读者应该能够轻松地根据自己的要求定制自己的镜像。让我们开始更深入地探索,看看如何使用 Ansible 将文件传输到我们之前创建的虚拟机镜像中。

将文件传输到镜像中

在作者的经验中,注入文件到操作系统镜像中以确保其符合给定的要求是非常常见的做法。这些文件可能是一个简单的文本文件,例如企业标准的每日信息、现有软件包的配置文件,或者甚至是一个在软件包中没有的二进制文件。Ansible 可以轻松处理所有这些问题,因此让我们来看看一些具体的例子。由于将 Ansible 代码写成角色以支持重用性和可读性通常是一个好习惯,我们将在这里为我们的示例定义一个角色。在这个示例中,我做出以下假设:

  • 我们已经按照本章前面的部分下载/构建了我们的 Linux 模板。

  • 我们正在虚拟机中运行这个裸模板。

  • 这个虚拟机的 IP 地址是192.168.81.141

  • 虚拟机已经配置了一个用户账户,凭证如下:

    • 用户名:imagebuild

    • 密码:password

    • 此账户启用了 sudo 权限。

自然地,我们不会分发一个包含启用了 sudo 权限且使用这种弱密码的账户的云镜像,因此我们假设在构建阶段只会使用此账户,之后将在清理阶段将其移除。Ansible 需要能够连接到远程主机以执行其任务,但它使用的账户可以是暂时性的,任务完成后可以删除。

  1. 根据我们的示例,我们将创建一个清单文件,其内容如下——你创建的文件可能会有所不同,如何根据你的镜像和环境定制它将留给你作为练习:
[imagesetup]
192.168.81.141

[imagesetup:vars]
ansible_user=imagebuild
ansible_password=password
ansible_sudo_pass=password

这是一个非常简单的示例;从某种意义上说,当我们没有配置 SSH 密钥认证时,这是执行此过程所需的最基本步骤。通常,SSH 密钥是处理 SSH 认证的最佳方式,因为它们有多种优点,最重要的是任务可以在没有密码提示的情况下运行。

尽管这个清单文件的本意是暂时性的,但使用ansible-vault来存储密码仍然是最佳实践,本文建议使用此方法。为了简化本章内容并减少所需的步骤,我们将密码保持未加密(明文)。

  1. 接下来,我们将为角色创建基本的目录结构:
$ mkdir -p roles/filecopyexample/tasks
$ mkdir -p roles/filecopyexample/files
  1. 现在,让我们创建一些示例文件以便复制。首先,在roles/filecopyexample/files/motd中创建一个定制的消息,附加到每日消息中:
------------------------
Enteprise Linux Template
Created with Ansible
------------------------
  1. 我们还将为chrony服务创建一个新的配置文件,以将时间同步到我们公司内部的时间服务器,文件路径为roles/filecopyexample/files/chrony.conf
pool ntp.example.com iburst maxsources 4

keyfile /etc/chrony/chrony.keys

driftfile /var/lib/chrony/chrony.drift

logdir /var/log/chrony

maxupdateskew 100.0

rtcsync

makestep 1 3

我们打算将这两个文件复制到远程服务器。然而,Ansible 不仅限于从 Ansible 主机复制文件——它还可以直接从远程服务器下载文件到目标主机:

  1. 假设你的构建需要docker-compose——我们可以从内部服务器下载它,或者如果你的镜像机器可以访问互联网,也可以直接从网上下载。假设我们想将docker-compose 1.18.0 安装到镜像中,我们可以指示 Ansible 直接从github.com/docker/compose/releases/download/1.18.0/docker-compose-Linux-x86_64下载它。

  2. 现在,让我们创建一个角色,将这两个文件复制到目标主机,并将docker-compose下载到镜像中——这必须写在roles/filecopyexample/tasks/main.yml中。角色的第一部分如下所示,它负责复制我们之前讨论的两个配置文件:

---
- name: Copy new MOTD file, and backup any existing file if it             exists
  copy:
    src: files/motd
    dest: /etc/motd
    owner: root
    group: root
    mode: '0644'
    backup: yes
- name: Copy across new chrony configuration, and backup any existing file if it exists
  copy:
    src: files/chrony.conf
    dest: /etc/chrony.conf
    owner: root
    group: root
    mode: '0644'
    backup: yes

接下来,角色将继续执行在虚拟机镜像上安装docker-compose的任务:

- name: Install docker-compose 1.18.0
  get_url:
    url: https://github.com/docker/compose/releases/download/1.18.0/docker-compose-Linux-x86_64
    dest: /usr/local/bin/docker-compose
    mode: 0755
    owner: root
    group: root

因此,我们的角色现在已经完成,但请确保根据你的环境正确自定义它。例如,可能会有更新版本的docker-compose,这意味着要更改前面get_url模块中的url参数。

chrony配置文件的路径可能会根据你的操作系统而有所不同——在运行前面的播放剧本之前,请检查此路径。示例中显示的路径适用于像我们之前构建的 CentOS 7 系统。

  1. 最后,我们将在顶层目录中创建一个名为site.yml的文件(该目录是之前创建roles/目录的地方),用来调用并运行这个角色。这个文件应包含以下内容:
---
- name: Run example roles
  hosts: all
  become: yes

  roles:
    - filecopyexample
  1. 最后,让我们使用ansible-playbook -i hosts site.yml命令运行我们的示例,看看会发生什么:

如我们所见,changed状态告诉我们,所有三个文件都已经成功传输或下载,作为示例,我们可以看到现在可以运行docker-compose,这个工具是在播放剧本执行期间安装的(虽然这需要 Docker 正确运行,而我们没有在这个示例中安装 Docker)。

很明显,这个例子做了一个基本假设——在我们示例镜像的构建阶段,已经安装了chrony包。尽管从我们之前讨论的原因来看,从一个最小化的操作系统镜像开始是有道理的,但几乎肯定会有需要在基础构建上安装一些额外软件包的要求,我们将在下一节探讨这一点。

安装软件包

我们在前一节中讨论了如何安装像docker-compose这样的独立二进制文件——但如果我们需要安装一些在基础镜像中没有安装的操作系统软件包怎么办?例如,cloud-init在大多数云环境中非常有用,但在我们之前进行的最小化 CentOS 7 安装中并没有包含。

在这里,Ansible 再次可以派上用场——这一次,我们将定义一个角色来安装所需的软件包。我们将重用上一节中的清单文件,并以我们之前的方式创建一个名为packageinstall的新角色:

  1. 现在,前面的复制文件示例将适用于所有 Linux 发行版——你需要注意的唯一事情是目标文件可能的位置。例如,我们的 CentOS 7 虚拟机镜像将把chrony配置文件安装在/etc/chrony.conf,而 Ubuntu 18.04 LTS 服务器则会把它安装在/etc/chrony/chrony.conf。除了copy模块中dest:参数的这个小改动,代码保持不变。

不幸的是,软件包安装会变得更加复杂。

  1. 假设我们想在 CentOS 7 示例镜像上安装cloud-initdocker——实现这一目标的角色可能如下所示:
---
- name: Install the epel-release package
  yum:
    name: epel-release
    state: present

- name: Install cloud-init and docker
  yum:
    name: "{{ item }}"
    state: present
  loop:
    - cloud-init
    - docker
  1. 我们必须首先安装 EPEL 仓库,然后才能安装所需的包。当我们运行时,输出应该类似于以下内容:

如果你使用的是其他 Linux 发行版,那么需要相应地更改包管理器。例如,在使用apt包管理器的 Debian 或 Ubuntu 等发行版中,相应的 Ansible 角色将类似于以下代码块:

---
- name: Install cloud-init and docker
  apt:
    name: "{{ item }}"
    state: present
  loop:
    - cloud-init
    - docker.io

请注意模块从yum变更为apt,以及用于 Docker 容器服务的不同包名。除此之外,剧本几乎是相同的。

我们可以进一步改进这一点——这会导致需要为两种不同的操作系统维护两个不同的角色——但如果我们能够智能地将它们合并为一个呢?幸运的是,Ansible 在首次运行时收集的信息可以用来识别操作系统,因此可以运行正确的代码。

我们将重用之前的示例代码,将这两项安装合并为一个 Ansible 角色:

  1. 代码的第一部分与前面的示例几乎相同,唯一的不同是我们现在指定了when子句,确保它仅在 Debian 或 Ubuntu 基础的 Linux 发行版上运行:
---
- name: Install cloud-init and docker
  apt:
    name: "{{ item }}"
    state: present
  loop:
    - cloud-init
    - docker.io
  when: ansible_distribution == 'Debian' or ansible_distribution == 'Ubuntu'
  1. 然后我们添加了两个额外的任务,执行在 CentOS 或 Red Hat Enterprise Linux 上安装 Docker 所需的步骤:
- name: Install the epel-release package
  yum:
    name: epel-release
    state: present
  when: ansible_distribution == 'CentOS' or ansible_distribution == 'Red Hat enterprise Linux'

- name: Install cloud-init and docker
  yum:
    name: "{{ item }}"
    state: present
  loop:
    - cloud-init
    - docker
  when: ansible_distribution == 'CentOS' or ansible_distribution == 'Red Hat enterprise Linux'

再次注意每个任务下的when子句——这些特定的示例用于根据 Ansible 在初次运行时收集的事实来判断任务是否应该执行。因此,如果我们现在在 Ubuntu 系统上运行此角色,我们会看到以下内容:

  1. 如你所见,首先与apt相关的任务已运行,但随后基于yum的两个任务被跳过了,因为when子句的条件没有满足。现在,如果我们将其应用于 CentOS 7 目标,则会看到如下内容:

现在情况正好相反:apt任务被跳过,但与yum相关的两个任务被执行。通过这种方式,即使在处理几个不同的基础操作系统时,也可以维护一个安装通用软件包需求的单一角色。将when子句与 Ansible 收集的事实结合起来,是确保单一代码库在多种系统上正确运行的强大方式,因此,如果你的 SOE 涵盖了 Debian 和基于 Red Hat 的系统,你仍然可以轻松简便地维护代码。

一旦补充包安装完成,通常需要进行配置才能使其发挥作用。在接下来的部分,我们将探讨在编辑配置文件时如何使用 Ansible。

编辑配置文件

到目前为止,我们所做的所有配置工作都是非常简单明了的——我们要么在安装某些东西(无论是文件还是软件包),要么同样轻松地删除它(关于这一点将在清理部分进一步讲解)。但是,如果需要更为微妙的操作呢?在本章之前的将文件传输到镜像中部分,我们替换了整个chrony.conf文件,使用了我们自己的版本。然而,这种做法可能有点过于暴力——例如,我们可能只需要更改文件中的一行,而为了修改一行就替换整个文件,这种方式有些过重,特别是考虑到配置文件可能会在未来的软件包版本中更新。

让我们来看看另一个常见的操作系统镜像配置需求:SSH 守护进程的安全性。默认情况下,如我们之前创建的 CentOS 7 安装,允许使用 root 账户进行远程登录。出于安全原因,这并不可取。那么问题是,我们如何在不替换整个文件的情况下更新 SSH 守护进程配置呢?幸运的是,Ansible 就有用于这种任务的模块。

要执行此任务,lineinfile模块将派上用场。考虑以下角色,我们称之为securesshd

---
- name: Disable root logins over SSH
  lineinfile:
    dest: /etc/ssh/sshd_config
    regexp: "^PermitRootLogin"
    line: "PermitRootLogin no"
    state: present

在这里,我们使用lineinfile模块处理/etc/ssh/sshd_config文件。我们指示它查找以PermitRootLogin开头的行(这可以防止我们不小心编辑被注释掉的行),然后将该行替换为PermitRootLogin no

让我们在一个 CentOS 7 测试系统上尝试一下:

这正如预期的那样工作。然而,正则表达式需要非常小心。例如,SSH 守护进程会处理包含行首空格的配置行。然而,我们在前面代码中的简单正则表达式没有考虑空格,因此可能会错过其他有效的 SSH 配置指令。制定能够考虑所有可能情况和文件排列的正则表达式本身就是一门艺术,因此在创建和使用时必须非常小心。

请注意,在实际系统中,你还需要重启 SSH 服务才能使更改生效;但是,由于这是一个我们将清理并关闭以进行未来部署的镜像,所以在这里不需要这样做。

上传整个文件和编辑现有文件之间的一个中间方法是使用模板。Ansible Jinja2 模板非常强大,对于需要根据某些变量参数来变化内容的文件非常有用。

再次考虑我们之前提到的chrony配置示例——在这里,我们传输了一个包含硬编码 NTP 服务器地址的静态文件。如果你的企业依赖于一个静态 NTP 服务器(或一组服务器),这是可以的,但有些企业则可能会根据镜像要部署的位置而依赖不同的 NTP 服务器。

让我们通过一个名为templatentp的新角色来演示这一点。为此,我们将在roles/templatentp/templates中定义一个模板目录,并将一个名为chrony.conf.j2的文件放入其中,内容如下:

pool {{ ntpserver }} iburst maxsources 4

keyfile /etc/chrony/chrony.keys

driftfile /var/lib/chrony/chrony.drift

logdir /var/log/chrony

maxupdateskew 100.0

rtcsync

makestep 1 3

请注意,这个文件与前面的例子几乎完全相同,唯一的不同是我们在文件的第一行用一个 Ansible 变量名替代了静态的主机名。

让我们为这个角色创建main.yml文件,如下所示:

---
- name: Deploy chrony configuration template
  template:
    src: templates/chrony.conf.j2
    dest: /etc/chrony.conf
    owner: root
    group: root
    mode: '0644'
    backup: yes

请注意它与copy例子有多么相似。我们的site.yml文件也只有一点点不同——在其中,我们将定义带有 NTP 服务器主机名的变量。Ansible 中有很多地方可以定义这个变量,用户需要自己决定最适合的定义位置:

---
- name: Run example roles
  hosts: all
  become: yes

  vars:
    ntpserver: time.example.com

  roles:
    - templatentp

最后,我们可以运行剧本并查看结果:

通过这种方式,Ansible 提供了强大的工具,不仅可以将整个配置复制或下载到目标环境中,还可以操作现有配置以适应你的环境。假设我们的镜像现在已经完成。我们可以凭信心认为它是好的,但良好的实践建议我们应始终测试任何构建过程的结果,尤其是自动化构建。幸运的是,Ansible 可以帮助我们根据要求验证我们创建的镜像,我们将在下一节中探讨这一点。

验证镜像构建

除了安装和配置你的镜像外,你可能还希望验证某些关键组件是否存在,并假设它们是存在的。特别是当你下载一个由别人创建的镜像时,这一点尤为重要。

在 Ansible 中执行此任务有多种方法——让我们以一个简单的例子为例。假设你有一个归档脚本,它使用了bzip2压缩工具来压缩文件。这个工具本身很小,但如果你依赖它完成某些任务,在没有它的情况下,脚本就会崩溃。它也是一个恰当的例子,因为我们之前执行的 CentOS 7 最小安装实际上并没有包含它!

Ansible 如何解决这个问题?我们可以采取两种方法。首先,根据我们之前对 Ansible 的了解,大多数模块是幂等的——也就是说,它们旨在使目标主机达到期望的状态,并且不会重复已执行的操作。

因此,我们可以非常容易地在我们的配置剧本中包含这样一个角色:

---
- name: Ensure bzip2 is installed
  yum:
    name: bzip2
    state: present

当运行这个角色时,如果bzip2没有安装,它将执行安装并返回结果changed。当它检测到bzip2已安装时,它将返回ok并且不执行任何进一步操作。但是,如果我们确实想检查某些东西,而不仅仅是执行某个动作,可能作为构建后的步骤呢?在本书后面,我们将探讨更详细的系统审计方法,但现在,我们先通过 Ansible 进一步了解这个例子。

如果你使用的是 shell 命令,你会通过两种方式之一来检查是否存在bzip2,即查询 RPM 数据库以查看bzip2包是否已安装,或者检查文件系统中是否存在/bin/bzip2

  1. 让我们来看一下 Ansible 中的后者示例。Ansible 的stat模块可以用来验证文件是否存在。请看以下代码,我们将在通常的方式下创建一个名为checkbzip2的角色:
---
- name: Check for the existence of bzip2
  stat:
    path: /bin/bzip2
  register: bzip2result
  failed_when: bzip2result.stat.exists == false

- name: Display a message if bzip2 exists
  debug:
    msg: bzip2 installed.

在这里,我们使用stat模块来检查/bin/bzip2文件是否存在。我们将模块执行的结果注册到名为bzip2result的变量中,然后我们为任务定义一个自定义的失败条件,若文件不存在,则导致任务失败(进而使整个 playbook 执行失败)。请注意,当遇到失败条件时,Ansible 会暂停整个 playbook 的执行,迫使你在继续之前解决问题。显然,这可能不是你希望的行为,但你可以轻松根据需要调整失败条件。

  1. 让我们看看实际操作中的效果:

如你所见,调试语句未被执行,因为遇到了错误。因此,我们可以完全确信,当运行此角色时,我们的镜像中将会安装bzip2——如果没有安装,playbook 将会失败。

  1. 一旦bzip2安装完成,运行结果会变得大不相同:

这种行为是非常明确的,正是我们所希望的。然而,Ansible 并不仅限于检查文件——我们还可以检查我们的sshd_config文件中是否有之前提到的PermitRootLogin no行:

  1. 我们可以通过角色来实现这一点,代码如下:
---
- name: Check root login setting in sshd_config
  command: grep -e "^PermitRootLogin no" /etc/ssh/sshd_config
  register: grepresult
  failed_when: grepresult.rc != 0

- name: Display a message if root login is disabled
  debug:
    msg: root login disabled for SSH
  1. 现在,当设置未生效时,再次运行会导致失败:

  1. 然而,如果我们设置了这个条件,我们会看到以下结果:

再次强调,它的行为非常明确。请注意前面输出中的changed状态——这是因为我们使用了command模块,它成功执行了command——因此,它总是返回changed。如果需要,我们可以通过在此任务中添加changed_when条件来改变这一行为。

通过这种方式,可以编写 Ansible playbook,不仅可以自定义构建过程,还可以验证最终结果。这在测试过程中尤其有用,尤其是在需要考虑安全性的场景中。

在完成本章之前,让我们在接下来的章节中看看,我们如何将迄今为止讨论的所有角色和代码片段组合起来,形成一个连贯的自动化解决方案。

将所有内容整合起来

在本章的这一部分中,你会注意到我们在所有示例中都使用了角色(roles)。自然地,当你最终构建你的镜像时,你不希望像我们在这里所做的那样单独运行多个 playbook。幸运的是,如果我们将所有内容合并起来,我们所需要做的就是将所有的角色放在roles/子目录中,然后在site.yml playbook 中引用它们。roles目录应该类似于以下内容:

~/hands-on-automation/chapter05/example09/roles> tree -d
.
├── checkbzip2
│   └── tasks
├── checksshdroot
│   └── tasks
├── filecopyexample
│   ├── files
│   └── tasks
├── installbzip2
│   └── tasks
├── packageinstall
│   └── tasks
├── securesshd
│   └── tasks
└── templatentp
    ├── tasks
    └── templates

然后,我们的site.yml文件应该是这样的:

---
- name: Run example roles
  hosts: all
  become: yes

  roles:
    - filecopyexample
    - packageinstall
    - templatentp
    - installbzip2
    - securesshd
    - checkbzip2
    - checksshdroot

运行这段代码的部分留给读者自行完成,因为我们已经在本章的早些部分运行了所有组件。然而,如果一切顺利,那么当所有角色执行完毕后,应该没有failed状态——只有changedok的混合状态。

如果你已经按照本章的详细说明完成了后期构建自定义,那么生成的镜像可能需要第二次清理。我们可以再次使用virt-sysprep命令,但 Ansible 也可以在这里帮助我们。在下一节中,我们将探讨如何使用 Ansible 来清理镜像以便进行大规模部署。

使用 Ansible 清理构建过程

到目前为止,你应该对如何构建或验证基础镜像,并使用 Ansible 进行自定义有了相当清晰的理解。在我们结束本章之前,值得回顾一下清理镜像以便部署的任务。无论你是从头构建镜像还是下载了现成的镜像,如果你已经启动并在其上运行了命令,无论是手动还是使用 Ansible,你可能会有很多你不希望在每次部署镜像时都出现的东西。例如,你真的希望每次部署的虚拟机上都有你执行的每个配置任务和初始启动的系统日志文件吗?如果你必须手动运行任何命令(即使是为了设置身份验证以允许 Ansible 运行),你是否希望这些命令出现在你运行它们的账户的.bash_history文件中,并在每次部署时都被记录?

这些问题的答案,当然是“否”。然后还有一些文件,如果被克隆可能会引起问题——例如,重复的 SSH 主机密钥或 MAC 地址特定的配置,如udev配置数据。在你认为镜像准备好分发之前,所有这些内容应该清理干净。

Ansible 也可以帮助完成此任务,尽管我们建议使用本章前面展示的virt-sysprep工具,因为它可以为您处理所有这些步骤。您可能有不想使用此工具的原因——例如,您的环境中没有访问权限,或者您的 Linux 发行版没有此工具的版本。在这种情况下,您可以使用 Ansible 执行最终的清理。Ansible 的优点是,您可以使用本章中展示的内置模块,但同样也可以使用原始的 shell 命令——这在需要跨文件系统执行通配符操作时尤其有用。

以下是一个角色的示例,该角色依赖原始的 shell 命令来清理镜像,为部署做准备。它没有virt-sysprep所执行的任务那么完整,但作为如何使用 Ansible 执行此操作的良好示例。请注意,此示例特定于 CentOS 7——如果使用不同的操作系统,则需要更改路径、软件包数据库清理命令等。因此,本剧本提供给读者的更多是一个实用示例,展示了如何在 Ansible 中执行清理操作,但读者可以根据自己的需求进一步完善。首先,我们清理软件包数据库,因为这些数据不需要在各个部署之间复制:

---
- name: Clean out yum cache
  shell: yum clean all

接着,我们继续清理日志——通过停止日志守护进程,强制日志轮换,然后递归删除包含日志的目录来实现:

- name: Stop syslog
  shell: service rsyslog stop

- name: Force log rotation
  shell: /sbin/logrotate -f /etc/logrotate.conf
  ignore_errors: yes

- name: Clean out logs
  shell: /bin/rm -f /var/log/*-???????? /var/log/*.gz /var/log/*.[0-9] /var/log/**/*.gz /var/log/**/*.[0-9]

- name: Truncate log files
  shell: truncate -s 0 /var/log/*.log

- name: Truncate more logs
  shell: truncate -s 0 /var/log/**/*.log

- name: Clear the audit log
  shell: /bin/cat /dev/null > /var/log/audit/audit.log

- name: Clear wtmp
  shell: /bin/cat /dev/null > /var/log/wtmp

然后,我们清理硬件和 MAC 地址特定的配置,这些配置在部署后的虚拟机镜像中无效:

- name: Remove the udev persistent device rules
  shell: /bin/rm -f /etc/udev/rules.d/70*

- name: Remove network related MAC addresses and UUID's
  shell: /bin/sed -i '/^\(HWADDR\|UUID\)=/d' /etc/sysconfig/network-scripts/ifcfg-*

接下来,我们清理/tmp目录,并移除用户主目录中的历史文件。以下示例不完整,但展示了一些相关的例子:

- name: Clear out /tmp
  shell: /bin/rm -rf /tmp/* /var/tmp/*

- name: Remove user history
  shell: /bin/rm -f ~root/.bash_history /home/**/.bash_history

- name: Remove any viminfo files
  shell: rm -f /root/.viminfo /home/**/.viminfo

- name: Remove .ssh directories
  shell: rm -rf ~root/.ssh m -rf /home/**/.ssh

最后,我们执行最终任务——在这种情况下,删除 SSH 主机密钥。请注意,在此之后,我们还关闭虚拟机——这是作为此命令的一部分执行的,以防止意外创建任何额外的历史或日志数据。还要注意ignore_errors子句,它可以防止在关闭虚拟机并断开 SSH 连接时剧本失败:

- name: Remove SSH keys and shut down the VM (this kills SSH connection)
  shell: /bin/rm -f /etc/ssh/*key* && shutdown -h now
  ignore_errors: yes

在 CentOS 7 虚拟机上运行此代码将生成一个相对干净的镜像,但这里没有涉及一些细节。例如,我们已经清理了所有 bash 历史记录,但如果使用了其他 shell,其数据将不会被清理。类似地,我们清理了 root 用户主目录中的 VIM 应用数据,但没有清理可能在镜像创建过程中使用过的其他应用数据。因此,您需要根据您的环境需求扩展此角色。

到这个阶段,你已经完整地了解了创建、定制和清理 Linux 操作系统的整个过程,用于我们的 SOE 提议。有效使用 Ansible 意味着整个过程可以自动化,因此使我们能够在企业中迈出自动化的坚实步伐。接下来,只需将我们创建的模板部署到你的环境中,从这里开始,你可以根据需要克隆并进一步构建它。

总结

我们已经看到几个实际示例,展示了如何获取或构建 Linux 虚拟机镜像,以便在各种场景和环境中使用。我们还看到 Ansible 如何帮助自动化这一过程,进而如何与镜像构建过程相结合,支持我们之前讨论过的企业自动化最佳实践,特别是 SOE(标准化操作环境)的创建与管理。

在本章中,你学习了如何构建用于模板的 Linux 镜像,并且了解了如何获取并验证现成的镜像。接着,你通过实际示例学习了如何使用 Ansible 自定义这些模板镜像,涉及的关键概念包括软件包安装和配置文件管理。最后,你学习了如何确保镜像构建干净整洁,不包含任何会浪费或有害的重复数据。

在本书的下一章,我们将探讨如何为裸金属服务器和某些传统虚拟化环境创建标准化镜像。

问题

  1. 系统准备(sysprep)的目的是什么?

  2. 你什么时候需要在角色中使用 Ansible facts?

  3. 如何通过 Ansible 将新配置文件部署到虚拟机镜像中?

  4. 使用哪个 Ansible 模块可以将文件直接从互联网下载到虚拟机镜像中?

  5. 如何编写一个 Ansible 角色,能够在 Ubuntu 和 CentOS 上安装软件包?

  6. 为什么你需要验证已下载的 ISO 镜像?

  7. 在这一阶段使用 Ansible 角色会如何有利于环境的部署?

深入阅读

第六章:使用 PXE 启动进行自定义构建

在使用物理硬件时,并不能保证你可以简单地将虚拟机模板克隆到硬盘上并期待它能正常工作。当然,使用正确的工具,这完全是可能的,但它很棘手,而且无法保证结果系统能正常运行。

例如,云就绪镜像只会安装常见虚拟化网络适配器的内核模块,因此,当安装在现代硬件上时,可能无法运行(或无法连接网络)。

尽管如此,在物理硬件上仍然完全可以执行自动化、标准化的构建,本章提供了一个完整的实践方法。结合前一章,到本章结束时,你将获得标准化所有平台镜像的自动化构建过程的实际经验,无论它们是虚拟的、基于云的还是物理的。

本章将涵盖以下主题:

  • PXE 启动基础

  • 执行无人值守构建

  • 向无人值守启动配置中添加自定义脚本

技术要求

在本章中,我们将探讨 PXE 启动的过程,适用于物理和虚拟服务器。你需要两台在同一网络上的服务器,建议该网络是隔离的,因为本章执行的一些步骤可能会对实际运行的网络产生干扰甚至破坏。

你需要一台预先安装了你选择的 Linux 发行版的服务器(或虚拟机)——在我们的示例中,我们将使用 Ubuntu Server 18.04 LTS。另一台服务器(或虚拟机)应该是空白的,并且适合重新安装。

本章讨论的所有示例代码都可以从 GitHub 获取,地址是:github.com/PacktPublishing/Hands-On-Enterprise-Automation-on-Linux/tree/master/chapter06

PXE 启动基础

在虚拟化和云平台广泛采用之前,需要在物理服务器上生成标准化的操作系统构建,而无需访问数据中心并插入某种形式的安装介质。PXE 启动应运而生,作为满足这一需求的常见解决方案之一,其名称来源于预执行环境(可以理解为一个小型的、最简操作系统),它会加载,以便进行操作系统安装。

高层次来看,当我们谈论给定服务器的 PXE 构建时,以下过程正在发生:

  1. 服务器必须配置为使用其一个(或所有)网络适配器进行网络启动。这通常是大多数新硬件的出厂默认设置。

  2. 启动时,服务器启动网络接口,并依次尝试与 DHCP 服务器联系。

  3. DHCP 服务器会返回 IP 地址配置参数,并提供进一步的信息,说明预执行环境应从哪里加载。

  4. 服务器然后检索预执行环境,通常使用 简单文件传输协议TFTP)。

  5. PXE 环境启动后,会在 TFTP 服务器的已知且定义明确的位置查找配置数据。

  6. 配置数据被加载,并指示 PXE 环境如何继续。通常,对于 Linux 来说,这涉及从 TFTP 服务器加载内核和初始 RAMDisk 镜像,其中包含足够的 Linux 系统来继续安装,并从另一个网络服务(通常是 HTTP)中提取更多的安装源。

尽管这一切听起来相当复杂,但实际上,当分解成一步一步的过程时,它相当简单。在本章中,我们将逐步完成构建一个 PXE 启动服务器的过程,该服务器能够执行 CentOS 7 或 Ubuntu 18.04 Server 的无人值守安装。这将作为一个很好的动手示例,并展示我们如何在物理硬件上编写构建过程脚本,因为在上一章中讨论的虚拟机模板过程在这里并不容易实现。

在 PXE 启动过程开始之前,我们必须先设置一些支持服务,提供所需的网络服务。在下一节中,我们将讨论如何设置和配置这些服务。

安装和配置 PXE 相关服务

与几乎所有的 Linux 设置一样,执行此操作的具体方法将取决于你进行安装的 Linux 发行版,以及你将使用的软件包。在这里,我们将使用 ISC DHCP 服务器、久负盛名的 TFTP 守护进程和 nginx。然而,你也可以同样使用 dnsmasq 和 Apache。

在许多企业中,这些决策可能已经做出——大多数企业已经有了某种形式的 DHCP 基础设施,许多使用 IP 电话系统的公司也会有 TFTP 服务器。因此,本章仅提供一个示例——实际的实施通常会遵循长期建立的公司标准。

没有安全机制来防止你在同一网络上运行两个 DHCP 服务器。DHCP 依赖于广播消息,因此,网络上的任何 DHCP 客户端都将接收到响应最快的服务器的回答。因此,通过设置第二个 DHCP 服务器,完全有可能使网络无法正常工作。如果你按照本章中概述的过程进行操作,请确保在一个隔离的网络中进行,以便进行测试。

对于此设置,我们假设我们有一个隔离的网络。我们的 PXE 服务器的 IP 地址为 192.168.201.1,子网掩码为 255.255.255.0。这些细节在设置 DHCP 服务器时非常重要。现在,让我们一步步完成服务器配置,以支持 PXE 启动:

  1. 我们需要安装以下所需的软件包:

    • DHCP 服务器

    • TFTP 服务器

    • Web 服务器

假设我们使用的是 Ubuntu 18.04 主机,如前所述,运行以下命令以安装本章所需的包:

$ apt-get install isc-dhcp-server tftpd-hpa nginx
  1. 安装这些后,下一步是配置我们的 DHCP 服务器,通过 /etc/dhcp/dhcpd.conf 文件配置前述的软件包。下面代码块中的配置文件是一个适合我们 PXE 启动网络的良好(虽然基础)示例,当然,你需要编辑子网定义以匹配你自己的测试网络。文件的第一部分包含一些重要的全局指令和网络的子网定义:
allow bootp;
# https://www.syslinux.org/wiki/index.php?title=PXELINUX#UEFI
# This one line must be outside any bracketed scope
option architecture-type code 93 = unsigned integer 16;

subnet 192.168.201.0 netmask 255.255.255.0 {
  range 192.168.201.51 192.168.201.99;
  option broadcast-address 192.168.201.255;
  option routers 192.168.201.1;
  option domain-name-servers 192.168.201.1;

文件的下一部分包含配置指令,确保我们根据使用的系统类型加载正确的预执行二进制文件。编写时,常见的是同时存在 BIOS 和 UEFI 基于的系统,因此以下配置非常重要:

  class "pxeclients" {
     match if substring (option vendor-class-identifier, 0, 9) = "PXEClient";

     if option architecture-type = 00:00 {
         filename "BIOS/pxelinux.0";
     } else if option architecture-type = 00:09 {
         filename "EFIx64/syslinux.efi";
     } else if option architecture-type = 00:07 {
         filename "EFIx64/syslinux.efi";
     } else if option architecture-type = 00:06 {
         filename "EFIia32/syslinux.efi";
     } else {
         filename "BIOS/pxelinux.0";
     }
  }
}

如果你之前使用过 DHCP 服务器,大部分内容是自解释的。然而,标题为 class "pxeclients" 的文本块值得特别提及。几年前,服务器硬件依赖于 BIOS 启动,因此 PXE 启动配置非常简单,因为只需要加载一个预启动环境。现在,大多数新的服务器硬件配置了可以在 Legacy BIOSUEFI 模式 中运行的固件,并且大多数默认启用 UEFI,除非另行配置。预执行二进制文件是不同的,具体取决于使用的固件类型,因此,这个块中的 if 语句利用了 DHCP option,当客户端发出 DHCP 请求时返回给服务器。

  1. 配置完成后,启用 DHCP 服务器,并重新启动它,步骤如下:
$ systemctl enable isc-dhcp-server.service
$ systemctl restart isc-dhcp-server.service
  1. 对于 TFTP 服务器,默认配置足以满足本示例的需求,因此,让我们启用此功能并确保它按如下方式运行:
$ systemctl enable tftpd-hpa.service
$ systemctl restart tftpd-hpa.service
  1. 最后,我们将使用 nginx 的默认配置,并从 /var/www/html 提供我们需要的所有文件——显然,在企业环境中,你可能希望做一些更高级的配置,但在下面的实际示例中,这样的配置已经足够:
$ systemctl enable nginx.service
$ systemctl restart nginx.service

这就是我们的服务器基础设施配置完成,但还有一个最后的任务。我们需要为 TFTP 服务器准备预执行环境二进制文件,以发送给客户端。

尽管这些工具对于大多数 Linux 发行版来说都很容易获得(Ubuntu 18.04 也不例外),但这些软件包通常都比较旧(PXELINUX 的最后一个稳定版本发布于 2014 年),并且我遇到过一些已知的 bug,特别是在处理 UEFI 硬件时。虽然您可以尝试较新的快照版本,但作者在使用版本标签为 6.04-pre2 的版本时取得了最好的成功,因此我们将解释如何构建此版本并将文件复制到我们 TFTP 服务器的正确位置,如下所示:

  1. 首先,下载并解压所需版本的 SYSLINUX(它包含 PXELINUX 代码),输入以下代码:
$ wget https://www.zytor.com/pub/syslinux/Testing/6.04/syslinux-6.04-pre2.tar.gz
$ tar -xzf syslinux-6.04-pre2.tar.gz
$ cd syslinux-6.04-pre2/
  1. 接下来,我们需要安装一些构建工具,以便成功编译代码,如下所示:
$ sudo apt-get install nasm uuid-dev g++-multilib
  1. 最后,我们将确保构建目录是干净的,然后构建代码,如下所示:
$ make spotless
$ make

当构建完成后,最后一步是将文件复制到正确的位置。回想我们之前的 DHCP 服务器配置,我们知道需要将与传统 BIOS 启动相关的文件与新发布的 UEFI 启动文件分开。在这里,我们将逐步介绍如何为 BIOS 和 UEFI 网络启动设置您的服务器:

  1. 在 Ubuntu 18.04 上,TFTP 服务器的默认根目录是 /var/lib/tftpboot。在此路径下,我们将创建 DHCP 服务器配置中提到的两个目录,如下所示:
$ mkdir -p /var/lib/tftpboot/{EFIx64,BIOS}
  1. 然后,我们将运行一组命令,将所有与 BIOS 相关的启动文件收集并复制到新创建的 BIOS 目录中:
$ cp bios/com32/libutil/libutil.c32 bios/com32/elflink/ldlinux/ldlinux.c32 bios/core/pxelinux.0 /var/lib/tftpboot/BIOS
$ mkdir /var/lib/tftpboot/BIOS/pxelinux.cfg
$ mkdir /var/lib/tftpboot/BIOS/isolinux
$ find bios -name *.c32 -exec cp {} /var/lib/tftpboot/BIOS/isolinux \;
  1. 然后,我们重复此步骤,不过这次我们指定与 UEFI 相关的启动文件,如下所示:
$ cp efi64/com32/elflink/ldlinux/ldlinux.e64 efi64/com32/lib/libcom32.c32 efi64/com32/libutil/libutil.c32 efi64/efi/syslinux.efi /var/lib/tftpboot/EFIx64 
$ mkdir /var/lib/tftpboot/EFIx64/pxelinux.cfg 
$ mkdir /var/lib/tftpboot/EFIx64/isolinux
$ find efi64/ -name *.c32 -exec cp {} /var/lib/tftpboot/EFIx64/isolinux \;

完成这些步骤后,我们现在已经有了一个完整的、功能正常的 PXE 服务器。我们还没有下载任何操作系统镜像,因此启动过程不会进行得很远,但如果此时进行测试,您的服务器固件应该报告它已从 DHCP 服务器获取了 IP 地址,并应显示一些与启动相关的消息。然而,在本书中我们将在进一步测试之前继续完善这个过程,在下一节中,我们将介绍如何为您选择的 Linux 发行版获取正确的网络安装镜像。

获取网络安装镜像

在我们的 PXE 启动设置过程中,下一步是构建所需的镜像。幸运的是,获取启动镜像非常简单——内核和软件包通常包含在您选择的 Linux 发行版的 DVD ISO 镜像中。显然,这可能因发行版而异,因此您需要检查这一点。在本章中,我们将展示 Ubuntu Server 和 CentOS 7 的示例——这些原则也适用于许多 Debian 派生版、Fedora 和 Red Hat 企业 Linux。

所需的网络启动安装镜像以及所需的安装包通常可以在完整的 DVD 镜像中找到——live 镜像通常不够,因为它们缺少足够完整的安装包,无法完成安装,或者缺少支持网络启动的内核。

让我们从 CentOS 7 镜像开始,如下所示:

  1. 首先,从最近的镜像站点下载最新的 DVD 镜像——例如,下面代码块中显示的镜像:
$ wget http://mirror.netweaver.uk/centos/7.6.1810/isos/x86_64/CentOS-7-x86_64-DVD-1810.iso
  1. 下载后,将 ISO 镜像挂载到合适的位置,以便可以从中复制文件,如下所示:
$ mount -o loop CentOS-7-x86_64-DVD-1810.iso /mnt
  1. 现在,支持网络启动的内核和初始 RAMDisk 镜像应该被复制到我们选择的位置,位于 TFTP 服务器根目录下。

请注意,在以下示例中,我们仅为 UEFI 启动进行此操作。要设置 传统 BIOS 启动,请完全按照相同的过程进行,但将所有由 TFTP 提供的文件放置在 /var/lib/tftpboot/BIOS 中。本章其余部分也适用这一点。

在我们的测试系统上实现这一点的命令如下:

$ mkdir /var/lib/tftpboot/EFIx64/centos7

$ cp /mnt/images/pxeboot/{initrd.img,vmlinuz} /var/lib/tftpboot/EFIx64/centos7/
  1. 最后,我们需要之前安装的 web 服务器来提供安装程序所需的文件——一旦内核和初始 RAMDisk 环境加载,剩余的环境将通过 HTTP 提供,因为 HTTP 更适合进行大数据传输。我们将再次为 CentOS 内容创建一个合适的子目录,如下所示:
$ mkdir /var/www/html/centos7/

$ cp -r /mnt/* /var/www/html/centos7/

$ umount /mnt

就这样!一旦这些步骤完成,我们将对 Ubuntu 18.04 Server 启动镜像重复此过程,如下所示:

$ wget http://cdimage.ubuntu.com/releases/18.04/release/ubuntu-18.04.2-server-amd64.iso

$ mount -o loop ubuntu-18.04.2-server-amd64.iso /mnt

$ mkdir /var/lib/tftpboot/EFIx64/ubuntu1804

$ cp /mnt/install/netboot/ubuntu-installer/amd64/{linux,initrd.gz} /var/lib/tftpboot/EFIx64/ubuntu1804/

$ mkdir /var/www/html/ubuntu1804

$ cp -r /mnt/* /var/www/html/ubuntu1804/

$ umount /mnt

完成这些步骤后,我们只剩下一个配置阶段,然后就可以进行我们选择的操作系统的网络启动了。

这个过程几乎是相同的——唯一的区别是,支持网络启动的内核和 RAMDisk 是从 ISO 镜像中的另一个目录中获取的。

在接下来的部分中,我们将配置到目前为止构建的 PXE 启动服务器,以便从这些安装镜像启动。

执行第一次网络启动

到目前为止,我们已经配置了服务器,在启动时为客户端分配 IP 地址,并且还构建了两个安装树,这样我们就可以安装 CentOS 7 或 Ubuntu 18.04 Server,而无需任何物理介质。然而,当目标机器通过网络启动时,它是如何知道该启动什么的呢?

解决这一问题的方式是 PXELINUX 配置。这与大多数 Linux 安装使用的 GRand Unified BootloaderGRUB)配置非常相似,用于定义从磁盘启动时的启动选项和参数。根据我们迄今为止构建的安装,这些配置文件应位于 /var/lib/tftpboot/EFIx64/pxelinux.cfg(或者对于传统 BIOS 机器,则位于 /var/lib/tftpboot/BIOS/pxelinux.cfg)。

现在,来谈谈文件命名。你可能希望所有通过网络接口启动的设备都执行网络启动。然而,想象一个服务器,它的本地磁盘上有一个有效的 Linux 安装,但由于某些错误(比如固件中的启动顺序配置错误,或者缺失的引导加载程序),它从网络接口启动,而不是从本地磁盘启动。如果你在 PXE 服务器上配置了一个完整的无人值守安装,这将会清除本地磁盘,可能导致灾难性的后果。

如果你希望所有服务器都执行网络启动,不管其他情况,你可以创建一个特殊的配置文件,名为default

然而,如果你想更加有针对性地配置,可以根据 MAC 地址创建一个配置文件。假设我们有一台 MAC 地址为DE:AD:BE:EF:01:23的服务器,且我们的 DHCP 服务器将为它分配 IP 地址192.168.10.101/24(这很可能是通过静态 DHCP 映射来确保该服务器始终获得这个 IP 地址)。当这台服务器使用 UEFI 网络启动时,它会首先查找/var/lib/tftpboot/EFIx64/pxelinux.cfg/01-de-ad-be-ef-01-23

如果这个文件不存在,它将查找一个以十六进制编码的 IP 地址命名的文件。如果该文件也不存在,它会依次去掉十六进制 IP 地址的每一位,直到找到一个匹配的文件。通过这种方式,我们的服务器会查找/var/lib/tftpboot/EFIx64/pxelinux.cfg/C0A80A65。如果没有找到,它会依次循环尝试更短的 IP 地址表示方式,直到没有更多选项。如果最终找不到合适命名的文件,它会回退到default文件,如果这个文件也不存在,客户端将报告启动失败。

因此,配置文件的完整搜索顺序如下:

  1. /var/lib/tftpboot/EFIx64/pxelinux.cfg/01-de-ad-be-ef-01-23

  2. /var/lib/tftpboot/EFIx64/pxelinux.cfg/C0A80A65

  3. /var/lib/tftpboot/EFIx64/pxelinux.cfg/C0A80A6

  4. /var/lib/tftpboot/EFIx64/pxelinux.cfg/C0A80A

  5. /var/lib/tftpboot/EFIx64/pxelinux.cfg/C0A80

  6. /var/lib/tftpboot/EFIx64/pxelinux.cfg/C0A8

  7. /var/lib/tftpboot/EFIx64/pxelinux.cfg/C0A

  8. /var/lib/tftpboot/EFIx64/pxelinux.cfg/C0

  9. /var/lib/tftpboot/EFIx64/pxelinux.cfg/C

  10. /var/lib/tftpboot/EFIx64/pxelinux.cfg/default

缩短 IP 地址文件名的目的是让你能够创建一个适用于整个子网的配置——例如,如果192.168.10.0/24子网中的所有机器都需要相同的启动配置,你可以创建一个名为/var/lib/tftpboot/EFIx64/pxelinux.cfg/C0A80A的文件。特别注意文件名中字母的大小写——基于 MAC 地址的文件名需要使用小写字母,而 IP 地址则需要使用大写字母。

这个配置文件的内容有许多配置组合,深入研究所有可能的配置将留给读者作为练习——PXELINUX 有充足的文档和示例可供参考。然而,考虑到我们特别要启动网络安装镜像,让我们来看以下文件。首先,我们定义菜单的头部,简单的标题和超时时间,如下所示:

default isolinux/menu.c32
prompt 0
timeout 120

menu title --------- Enterprise Automation Boot Menu ---------

接下来,我们定义了我们所构建的两个操作系统安装镜像的条目,如下所示:

label 1
menu label ¹\. Install CentOS 7.6 from local repo
kernel centos7/vmlinuz
append initrd=centos7/initrd.img method=http://192.168.201.1/centos7 devfs=nomount ip=dhcp inst.vnc inst.vncpassword=password

label 2
menu label ²\. Install Ubuntu Server 18.04 from local repo
kernel ubuntu1804/linux
append initrd=ubuntu1804/initrd.gz vga=normal locale=en_US.UTF-8 mirror/country=manual mirror/http/hostname=192.168.201.1 mirror/http/directory=/ubuntu1804 mirror/http/proxy="" live-installer/net-image=http://192.168.201.1/ubuntu1804/install/filesystem.squashfs 

与本书中的其他示例一样,这些是真实的、经过测试的示例,能够独立工作。然而,它们应该根据你的需求进行定制,并且你应该在生产环境中应用之前,阅读并理解这些代码。

在前面的示例中,192.168.201.1 是我在测试环境中 PXE 服务器的 IP 地址。确保将它替换为你 PXE 服务器的 IP 地址。

事实上,这是一个非常简单的示例——在这里,我们定义了一个简单的文本模式菜单,包含两个条目,每个条目对应一个操作系统。每个菜单条目都有一个 label,即菜单中显示的标题,然后是 kernelappend 行。kernel 行告诉客户端从哪里获取 TFTP 服务器上的内核,而 append 行用于指定 RAMDisk 映像的路径以及所有附加的启动参数。

如你所见,这些启动参数对于不同的 Linux 发行版差异很大,安装程序的功能也各不相同。例如,CentOS 7 安装程序是图形化的(虽然也有文本模式选项),并且支持 VNC 服务器,我们在第一个菜单项中配置了它,启用了使用 VNC 控制台的远程安装,使用了 inst.vncinst.vncpassword=password 参数。其他使用的参数如下:

  • method=http://192.168.201.1/centos7:设置我们 CentOS 7 仓库的地址

  • devfs=nomount:告诉内核不要挂载 devfs 文件系统

  • ip=dhcp:告诉预启动环境通过 DHCP 获取 IP 地址,以便能够访问 HTTP 服务器

相比之下,Ubuntu 安装程序通常以文本模式运行,因此不支持 VNC 服务器,因此需要不同的远程访问技术来执行交互式安装,例如 Serial-Over-LANSOL)。不过,这个菜单文件足够我们根据需要进行交互式安装,无论是选择哪个操作系统,并且作为模板提供给读者以供扩展和开发。使用的参数如下:

  • vga=normal:告诉安装程序使用标准 VGA 模式

  • locale=en_US.UTF-8:设置区域设置——根据你的环境调整此项

  • mirror/country=manual:告诉安装程序我们正在手动定义仓库镜像

  • mirror/http/hostname=192.168.201.1:设置我们之前创建的仓库镜像的主机名

  • mirror/http/directory=/ubuntu1804:设置仓库镜像主机上提供仓库内容的路径

  • mirror/http/proxy="":告诉安装程序我们没有使用代理

  • live-installer/net-image=http://192.168.201.1/ubuntu1804/install/filesystem.squashfs:安装程序磁盘映像可以下载的 URL

当然,在无人值守启动的情况下,您不会希望服务器提供操作系统选择——您只希望它启动您要安装的操作系统。在这种情况下,只需删除不需要的菜单项。

让我们看看实际操作。在成功的网络启动测试机器后,我们应该会看到之前定义的菜单:

  1. 如果我们选择 CentOS 镜像作为启动目标,您将看到内核和基础系统加载,然后最终会出现一个屏幕,提示您使用 VNC 客户端连接到安装程序,如下图所示:

  1. 按照说明连接到 VNC 查看器,显示出熟悉的交互式 CentOS 7 图形安装程序,如下图所示:

  1. 因此,完全远程安装是可能的,无需访问服务器位置,也无需连接键盘和鼠标!如果我们启动我们的 Ubuntu 服务器镜像,情况几乎也是一样的,只不过这次,控制台显示在主机屏幕上,而不是通过 VNC 提供,如下图所示:

这非常适合通过 SOL 实现或远程 KVM 选项来重定向控制台。这两者都不是特别方便,特别是本书的目标是自动化!

因此,在下一节中,我们将讨论执行自动化安装,使用无人值守构建的概念——也就是说,构建过程中不需要人工干预。

执行无人值守构建

该过程的最终目标是使服务器能够通过网络启动并完全配置,而无需人工干预。虽然这不是由 Ansible 控制的过程,但它仍然是我们标准操作环境SOE)架构中的一个重要组件,以确保构建的一致性,并且构建标准可以很好地文档化和版本控制。

幸运的是,CentOS(基于 Red Hat)和 Ubuntu(基于 Debian)的安装程序都提供了以编程方式完成无人值守安装的能力。遗憾的是,这个过程没有统一的标准,正如你在本节中看到的,两个我们讨论的 Linux 类型在这一过程中的语言完全不同。然而,通过覆盖这两种技术,我们提供了一个良好的基础,使你能够在各种 Linux 系统上执行远程、无人值守的安装。

请注意,本章中的示例是完整并且可用的,因此它们作为实际操作的示例提供——然而,它们实际上仅仅触及了这些无人值守安装技术的皮毛。如何扩展这些示例并根据自己的需求构建它们,将留给你作为练习。

让我们开始吧,下一节将介绍如何在基于 Red Hat 的平台(如 CentOS)上使用 kickstart 文件执行无人值守构建。

使用 kickstart 文件执行无人值守构建

Red Hat 安装程序 Anaconda 使用一种名为kickstart的脚本语言来定义无人值守构建。这有很多文档,并且在互联网上有许多示例可以供你参考——事实上,当你手动安装一个 Red Hat 衍生版本如 CentOS 7 时,你会在/root/anaconda-ks.cfg中找到一个 kickstart 文件,这个文件可以用来自动化未来的构建!接下来,我们将构建一个简单的 kickstart 文件,基本上是基于 CentOS 7 交互式安装程序的最小安装。

  1. 让我们开始构建本章中使用的示例 kickstart 文件。考虑以下这段代码:
auth --enableshadow --passalgo=sha512
url --url="http://192.168.201.1/centos7/"
graphical
firstboot --enable
ignoredisk --only-use=sda
keyboard --vckeymap=gb --xlayouts='gb'
lang en_GB.UTF-8 
reboot

大部分 kickstart 文件是非常易读的——在上面的代码块中,你可以看到以下内容:我们为密码哈希算法定义了sha512;我们的仓库服务器可通过http://192.168.201.1/centos7/访问;我们正在执行图形化安装,仅使用/dev/sda,并且设置了某些GB特定的地区设置。我们还告诉安装程序在安装成功完成后自动重启

  1. 然后我们在此基础上通过设置网络来继续(请注意,你必须在创建此文件之前知道网络设备名称,因此你可能会觉得启动到实时环境中先检查一下设备名称是有用的),通过运行以下代码:
network --bootproto=dhcp --device=ens33 --ipv6=auto --activate
network --hostname=ksautomation

这将把我们新建服务器的主机名设置为ksautomation,并启用名为ens33的网络设备上的 IPv6 和 IPv4 DHCP。

  1. 然后我们定义根账户密码,并且—可选地—通过运行以下代码定义我们希望作为构建的一部分添加的任何额外账户:
rootpw --iscrypted $6$cUkXdOxB$o8uxoU6arUj0g9SXqMGnigBYDH4rCkkQt9z/qYPm.lUYNwaZChCz2epQMUlbHUg8IVzN9lei9i/rschw1HydU.
user --groups=wheel --name=automation --password=$6$eCIJyrjn$Vu30KX//UntsM0h..MLT6ik.m1GL8ayILBFWjbDrKSXowli5/hycMaiFzGI926YXEMfXXjAuwOFLIdANZ09/g1 --iscrypted --gecos="Automation User"

请注意,这个文件中必须使用密码哈希值——有许多方法可以生成这些哈希值。我使用了以下 Python 代码片段来为password字符串生成唯一的哈希值(显然你应该选择更安全的密码!):

$ python -c "import random,string,crypt;
pwsalt = ''.join(random.sample(string.ascii_letters,8));
print crypt.crypt('password', '\$6\$%s\$' % pwsalt)"

在任何安装了 Python 的 Linux 服务器的 shell 中运行上述三行代码,将生成你需要的密码哈希值,可以将其复制并粘贴到你的安装中。

上述代码仅用于生成密码哈希值——不要将其包含在你的 kickstart 文件中!

  1. 最后,我们设置合适的时区,并启用 chrony 时间同步服务。我们初始化选定启动设备 sda 的磁盘标签,并使用 Anaconda 的自动分区(由 autopart 指令指定)来设置磁盘。

请注意,clearpart --none 实际上并不会清除分区表——如果你按照这里定义的 kickstart 文件运行该示例,安装只有在目标磁盘上有足够空间安装 CentOS 7 时才能完成。若要让 kickstart 文件清除目标磁盘并执行 CentOS 7 的全新安装(这可能有助于避免在重用旧机器前手动清除数据),请对 kickstart 文件进行以下修改:

  1. clearpart 语句上方插入 zerombr 指令,以确保清除启动扇区。

  2. clearpart 行修改为 clearpart --drives=sda --initlabel --all——请确保在 --drives= 参数中只指定你希望清除的磁盘!

以下代码片段未包含这些更改,因为它们是破坏性的——不过,你可以在测试环境中自由尝试这些更改:

services --enabled="chronyd"
timezone Europe/London --isUtc

bootloader --location=mbr --boot-drive=sda
autopart --type=lvm
clearpart --none --initlabel

接着,我们定义默认安装的包。这里,我们安装了 core 包组、minimal 系统包集以及 chrony 包。我们还为测试服务器禁用了 kdump,如以下代码块所示:

%packages
@^minimal
@core
chrony

%end

%addon com_redhat_kdump --disable --reserve-mb='auto'

%end

最后,我们可以进行额外的自定义,例如设置强密码策略——以下几行实际上是交互式安装程序的默认设置,应该根据你的需求进行自定义:

%anaconda
pwpolicy root --minlen=6 --minquality=1 --notstrict --nochanges --notempty
pwpolicy user --minlen=6 --minquality=1 --notstrict --nochanges --emptyok
pwpolicy luks --minlen=6 --minquality=1 --notstrict --nochanges --notempty
%end

当你完成了整个 kickstart 文件的编写后,是时候测试启动过程了。还记得我们在上一节中使用的 PXELINUX 启动配置吗?实际上,它几乎完全被重复使用,只是这次我们需要告诉它从哪里找到 kickstart 文件。我将刚才创建的文件存储在 /var/www/html/centos7-config/centos7unattended.cfg,这样它可以像安装程序的包一样从我们的 HTTP 服务器下载。在这种情况下,我们的 PXELINUX 配置看起来是这样的:

default isolinux/menu.c32
prompt 0
timeout 120

menu title --------- Enterprise Automation Boot Menu ---------

label 1
menu label ¹\. Install CentOS 7.6 from local repo
kernel centos7/vmlinuz
append initrd=centos7/initrd.img method=http://192.168.201.1/centos7 devfs=nomount ip=dhcp inst.vnc inst.vncpassword=password inst.ks=http://192.168.201.1/centos7-config/centos7unattended.cfg

让我们执行安装过程,看看会发生什么。最初,过程看起来与我们在本章早些时候执行的交互式安装相同。

上述 PXE 启动配置与之前相同,唯一不同的是末尾的 inst.ks 参数,它告诉 Anaconda 从哪里下载我们的 kickstart 文件。

实际上,当你连接到正在构建中的机器的 VNC 控制台时,最初的界面将看起来一样——CentOS 7 的图形化安装程序会加载,如下图所示:

到目前为止,一切看起来像是一个普通的交互式安装。然而,一旦安装程序完成了列出的各项任务(例如,保存存储配置...),你会注意到你看到的是一个看似完成的界面,除了“开始安装”按钮被禁用(如下图所示):

请注意这里的区别——安装源现在已经设置为我们为安装过程设置的 HTTP 服务器。所有通常需要手动完成的项目,例如磁盘选择,已经使用我们的 kickstart 脚本中的配置自动完成。事实上,如果我们再等一会儿,你会看到安装会自动开始,无需点击“开始安装”按钮,如下图所示:

安装现在正在进行,使用我们的 kickstart 文件中的参数。请注意,根密码和初始用户帐户的创建已经完成,使用的是 kickstart 脚本中的参数,因此这些按钮再次被禁用。简而言之,尽管安装过程看起来与普通的交互式安装非常相似,但用户无法以任何方式与安装过程进行交互。

用户将在以下两种情况下需要与 kickstart 安装交互:

  1. 配置不完整或不正确——在这种情况下,安装程序将暂停,等待用户干预,并(如果可能)修复问题。

  2. 如果在 kickstart 文件中没有指定reboot关键字。

在后一种情况下,安装将完成,但安装程序会等待点击“重启”按钮,如下图所示:

在 kickstart 安装结束时自动重启通常是可取的,因为这样可以省去连接控制台的步骤。然而,也有一些情况下这并不可取——或许你现在并不希望新建的服务器连接到网络,或者你正在为模板创建一个镜像,因此不希望第一次启动完成,因为那样会生成日志文件和其他需要清理的数据。

安装的确切路径由你决定——需要注意的是,你可以连接到 VNC 控制台,如前面的截图所示,准确查看安装过程。如果有任何错误或问题,你会收到警告。

测试一下,看看构建表现如何。如果遇到任何问题,安装程序会在物理服务器上启动多个控制台,这些控制台包含日志信息——你可以使用Alt + Tab,或Alt + F(其中 F 是功能键之一)在这些控制台之间切换——前六个控制台分别对应不同的控制台,包含有用的日志信息。你可以查询它们以调试可能出现的问题。实际上,说明会显示在文本模式控制台屏幕的底部——以下是一个示例截图:

在上面的截图中,我们可以看到我们处于控制台1,标题为main。控制台2用于调试,显示shell,控制台35显示与安装过程相关的log文件。

然而,如果一切顺利,你将看到安装程序在无需干预的情况下运行,接着,服务器将重启并呈现登录提示。从那里,你应该能够使用之前通过密码哈希定义的密码登录。

这就完成了通过网络使用 kickstart 文件构建 CentOS 7 服务器的过程。我们将在下一节中介绍如何使用预设文件进行 Ubuntu 及其他 Debian 派生系统的相同高层次操作。

使用预设文件进行无人值守构建

广义来说,Ubuntu Server 的构建(以及其他 Debian 派生操作系统的构建)完全相同。你指定一个脚本文件来告诉安装程序执行什么操作,代替人工选择选项。对于 Ubuntu Server,这被称为预设文件。让我们现在一起探讨并创建一个。

预设文件功能强大,且有很多文档可供参考——然而,它们有时看起来对肉眼来说更复杂。从以下几行代码开始,我们为服务器设置了适当的区域设置和键盘布局:

d-i debian-installer/locale string en_GB
d-i console-setup/ask_detect boolean false
d-i keyboard-configuration/xkb-keymap select gb

然后我们配置以下网络参数:

d-i netcfg/choose_interface select auto
d-i netcfg/get_hostname string unassigned-hostname
d-i netcfg/get_domain string unassigned-domain
d-i netcfg/hostname string automatedubuntu
d-i netcfg/wireless_wep string

在这里,你会注意到我们实际上不需要提前知道接口名称——我们可以让 Ubuntu 使用其自动检测算法来猜测它。我们将主机名设置为automatedubuntu;不过,请注意,其他参数用于防止安装程序提示用户输入主机名,这意味着安装并非完全无人值守。接下来,我们添加了一些有关安装程序从哪里下载软件包的细节,如下所示的代码块:

d-i mirror/country string manual
d-i mirror/http/hostname string 192.168.201.1
d-i mirror/http/directory string /ubuntu1804
d-i mirror/http/proxy string

这些应该根据你的网络、PXE 服务器上的 HTTP 服务器设置等自然调整。

其中许多也设置在内核参数中,正如我们之前在 PXELINUX 配置中看到的——我们只需要在这里确认其中一些。

然后我们设置根账户密码,以及任何其他用户账户,如下所示:

d-i passwd/root-password password password
d-i passwd/root-password-again password password
d-i passwd/user-fullname string Automation User
d-i passwd/username string automation
d-i passwd/user-password password insecure
d-i passwd/user-password-again password insecure
d-i user-setup/allow-password-weak boolean true
d-i user-setup/encrypt-home boolean false

在这里,请注意,我已将密码以明文形式指定,以突出显示在此处可以这样做的可能性—也可以指定其他参数来接受密码哈希,这在创建配置文件时更加安全。在这里,root 密码设置为password,并设置了一个名为automation的用户帐户,密码为insecure。像之前一样,我们的密码策略相当弱,可以在这里或稍后使用 Ansible 来增强。接着,我们根据需要设置时区,并开启 NTP 同步,如下所示:

d-i clock-setup/utc boolean true
d-i time/zone string Etc/UTC
d-i clock-setup/ntp boolean true

在我们这个简化的示例中,最复杂的一段代码是以下这段,用来分区和设置磁盘:

d-i partman-auto/disk string /dev/sda
d-i partman-auto/method string lvm
d-i partman-lvm/device_remove_lvm boolean true
d-i partman-md/device_remove_md boolean true
d-i partman-lvm/confirm boolean true
d-i partman-lvm/confirm_nooverwrite boolean true
d-i partman-auto-lvm/guided_size string max
d-i partman-auto/choose_recipe select atomic
d-i partman/default_filesystem string ext4
d-i partman-partitioning/confirm_write_new_label boolean true
d-i partman/choose_partition select finish
d-i partman/confirm boolean true
d-i partman/confirm_nooverwrite boolean true
d-i partman-md/confirm boolean true
d-i partman-partitioning/confirm_write_new_label boolean true
d-i partman/choose_partition select finish
d-i partman/confirm boolean true
d-i partman/confirm_nooverwrite boolean true

尽管内容较为冗长,但该文件的这一部分基本上是在说自动分区磁盘/dev/sda,设置 LVM,使用自动计算来确定文件系统布局,然后创建ext4文件系统。正如你所看到的,文件中有许多保护措施和确认提示,我们将其标记为true,否则安装程序会停止并等待用户输入以继续。如果发生这种情况,我们的安装过程将不会真正是无人值守的。接下来,我们指定要安装的软件包集,如下所示:

tasksel tasksel/first multiselect standard
d-i pkgsel/include string openssh-server build-essential
d-i pkgsel/update-policy select none

上述代码行基本上设置了一个包含openssh-server包和build-essential包的最小化服务器构建。自动更新策略配置为不自动更新。最后,为了完成文件的配置,我们告诉它安装启动加载程序的位置,并在成功完成后重启,如下所示:

d-i grub-installer/only_debian boolean true
d-i grub-installer/with_other_os boolean true
d-i finish-install/reboot_in_progress note

与我们的 CentOS 示例一样,我们将从我们的 Web 服务器提供这个文件,因此,PXELINUX 启动配置需要调整,以确保我们纳入这个文件—以下是一个适当的示例:

default isolinux/menu.c32
prompt 0
timeout 120

menu title --------- Enterprise Automation Boot Menu ---------

label 1
menu label ¹\. Install Ubuntu Server 18.04 from local repo
kernel ubuntu1804/linux
append initrd=ubuntu1804/initrd.gz url=http://192.168.201.1/ubuntu-config/ubuntu-unattended.txt vga=normal locale=en_US.UTF-8 console-setup/ask_detect=false console-setup/layoutcode=gb keyboard-configuration/layoutcode=gb mirror/country=manual mirror/http/hostname=192.168.201.1 mirror/http/directory=/ubuntu1804 mirror/http/proxy="" live-installer/net-image=http://192.168.201.1/ubuntu1804/install/filesystem.squashfs netcfg/get_hostname=unassigned-hostname

请注意,这次使用了以下新的选项:

  • url:告诉安装程序从哪里获取我们的预种子文件。

  • console-setup/layoutcodekeyboard-configuration/layoutcode:防止安装程序在第一次运行时询问键盘设置。

  • netcfg/get_hostname:尽管我们在预种子文件中已经设置了主机名,但我们仍需要在这里指定此参数,否则安装程序将停止,并提示用户输入主机名。

再次强调,如果你通过使用前述配置从网络启动服务器来进行测试,你应该会看到服务器构建完成。与 CentOS 7 的安装不同,你不会看到任何菜单选项—这些选项仅在你的预种子配置文件不正确或缺少某些重要细节时才会呈现给你。相反,你会看到一系列进度条快速闪过,表示安装的各个阶段正在完成。例如,以下截图显示了在分区和逻辑卷设置完成后,基础系统已安装到磁盘:

假设一切顺利,这个过程将继续,直到你看到一个最终的进度条,显示在服务器重启之前完成最后的整理工作。在下面的截图中,文件系统正在被卸载,为重启做准备:

当这个最终的进度条完成时,你的服务器将重启,并出现登录提示,你可以使用先前在 pre-seed 文件中指定的凭据登录,d-i passwd参数如前所示。请注意,如果你在构建过程中使用了不同的凭据,必须在此处使用这些凭据,而不是先前指定的凭据。

到此为止,你应该能够通过网络执行 CentOS 或 Ubuntu 服务器的无人值守构建,并进行基本更改,例如选择所需的软件包和设置凭据。在接下来的部分中,我们将探索超出原始操作系统的额外定制方法。

向无人值守启动配置中添加自定义脚本

正如你从本章中的示例所看到的,kickstart 和 pre-seed 文件在它们能做的事情上非常具有限制性。对于大多数目的,它们应该完全足够,允许你构建适合进一步使用 Ansible 进行自定义的机器。事实上,本书的其余部分主要讲解如何在按本章及前章中详细说明的方式构建的服务器群组上管理和自动化配置管理。

然而,如果你的企业有一个(或多个)任务必须在构建时执行——例如出于安全合规性的需要(我们将在第十三章中探讨,使用 CIS 基准)?幸运的是,我们在这里讨论的两种技术都提供了这样的选项。首先,让我们看看如何在 kickstart 无人值守安装中执行自定义命令。

使用 kickstart 进行自定义脚本编写

如前所述,建议大多数任务使用 Ansible 来执行构建后的配置。然而,让我们以一个简单的假设为例——假设出于安全原因,你需要在服务器构建后立即禁用 root SSH 登录,以满足安全合规性要求。kickstart 中没有可以执行此任务的指令,而在服务器等待 Ansible 运行期间仍然启用该功能,可能会被企业安全团队认为不可接受,因为潜在的攻击者可能会有机会利用这一点。幸运的是,在我们的 kickstart 文件的底部,我们可以放置一个%post块,来运行你放入其中的任何 Shell 代码。因此,我们可以在以下代码块中运行sed工具:

%post --log=/root/ks.log

/bin/sed -i 's/#PermitRootLogin yes/PermitRootLogin no/' /etc/ssh/sshd_config

%end

这段非常简单的代码块在安装过程完成后(但在重启之前)运行,并将其输出日志到/root/ks.log。您可以根据需要自定义此内容—然而,在此为了简单的示例,我们执行了对默认 SSH 守护进程配置的搜索和替换操作,以确保即使在第一次启动时,通过 SSH 的 root 登录也被禁用。

在下一节中,我们将看到如何在 Ubuntu 预种子文件中实现相同的操作。

使用预种子进行定制脚本

假设我们希望在 Ubuntu 中执行相同的自定义操作。Ubuntu 预先种子文件执行的是单行命令,而不是像 kickstart 中使用的代码块;因此,它们更适合简单任务,或者确实适合下载脚本以执行更复杂的操作。我们可以通过在文件末尾添加以下行,将sed命令嵌入到预种子文件中:

d-i preseed/late_command string in-target /bin/sed -i 's/#PermitRootLogin.*/PermitRootLogin no/' /etc/ssh/sshd_config

然而,假设我们有一个更复杂的脚本需要运行,并且将它写成一行会使得代码既难以阅读又难以管理——相反,我们可以修改前面的命令,从选定的位置下载脚本并运行它,如下所示:

d-i preseed/late_command string in-target wget -P /tmp/ http://192.168.201.1/ubuntu-config/run.sh; in-target chmod +x /tmp/run.sh; in-target sh -x /tmp/run.sh

请注意,我们在此使用wget(在构建过程中早些时候安装)从我们的 Web 服务器的/ubuntu-config/路径下载名为run.sh的文件。然后,我们将其设为可执行并运行。通过这种方式,构建过程结束时,在首次重启之前,可以运行更复杂的命令序列。

通过这种方式,可以远程安装非常复杂、定制化的操作系统构建,完全不需要人工干预。使用 kickstart 和预种子文件也意味着该过程是脚本化和可重复的,这是我们需要遵循的一个重要原则。

小结

即使在使用裸金属服务器(以及某些虚拟化平台)时,完全可以通过脚本化安装过程,确保所有构建的一致性,从而遵循我们在本书前面阐述的 SOE 原则。通过遵循本章中设定的过程,您将确保所有服务器都以一致的方式构建,无论它们运行在哪个平台上。

具体来说,您获得了使用 PXE 网络启动执行交互式 Linux 安装环境的经验。接着,您学习了如何使用 kickstart 和预种子脚本完全自动化构建过程,确保构建过程完全无人值守(因此,实现自动化)。最后,您学习了如何通过向构建定义中添加自定义脚本,进一步定制构建。

在下一章中,我们将继续探讨如何使用 Ansible 定制服务器,无论是在新建时,还是在后续的维护过程中。

问题

  1. PXE 代表什么?

  2. PXE 启动需要哪些基本服务?

  3. 您将从哪里获取网络启动的安装源?

  4. 什么是无人值守安装?

  5. kickstart 文件和 pre-seed 文件有什么区别?

  6. 为什么在 kickstart 文件中需要使用 %post 块?

  7. 在 TFTP 服务器根目录下,BIOSEFIx64 目录的作用是什么?

  8. 如何在 pre-seed 文件中为 /home 创建一个独立的分区?

进一步阅读

第七章:使用 Ansible 进行配置管理

到目前为止,在本书中,我们已经为我们的企业 Linux 基础设施建立了一个坚实的框架,这个框架非常适合企业中的大规模部署,并且可以在这种规模下使用 Ansible 进行自动化管理。在本章中,我们将深入探讨这一基础设施的自动化管理方面,从软件包的安装和配置开始。

几乎在每个企业中,几乎可以肯定在标准化 Linux 系统的生命周期内都会需要执行一项任务——服务的安装和配置。这可能仅仅涉及现有系统服务的配置,或者甚至可能是服务本身的安装,然后是后续的配置工作。

在本章中,我们将探讨以下主题,以深入了解 Ansible 配置管理:

  • 安装新软件

  • 使用 Ansible 进行配置更改

  • 在企业规模上管理配置

技术要求

本章包括基于以下技术的示例:

  • Ubuntu Server 18.04 LTS

  • CentOS 7.6

  • Ansible 2.8

为了运行这些示例,你需要访问两台服务器或虚拟机,每台运行一个操作系统,并且需要安装 Ansible。请注意,本章给出的示例可能具有破坏性(例如,它们会安装和卸载软件包并修改服务器配置),如果按原样运行,这些示例仅适用于在隔离的测试环境中执行。

一旦你确信你拥有一个安全的操作环境,我们就开始学习如何使用 Ansible 安装新的软件包。

本章讨论的所有示例代码可以从 GitHub 获取,网址为:github.com/PacktPublishing/Hands-On-Enterprise-Automation-on-Linux/tree/master/chapter07

安装新软件

根据你的需求,可能你的 SOE 操作系统构建已经安装了足够的软件,仅需要配置工作。然而,对于许多人来说,情况可能并非如此,因此我们将以软件安装部分开始本章内容。就像我们到目前为止做的所有工作一样,我们希望我们在这里做的任何事情都是可重复的,并且适合自动化,因此,即使需要新的软件,也希望我们不通过手动安装它。

让我们从最简单的情况开始——安装一个本地操作系统软件包。

从操作系统默认的仓库中安装软件包

假设你正在推出一个新服务,需要一个数据库服务器——例如 MariaDB。你不太可能在所有 SOE 镜像中都已安装并启用了 MariaDB,因此,在做任何其他事情之前,第一项任务将是安装该软件包。

本书中的两个示例操作系统(实际上,许多衍生版本也如此)都包含了 MariaDB 的本地包,因此我们可以轻松使用这些包。至于包安装,当然,需要了解我们的目标操作系统背后发生的事情。例如,在 Ubuntu 上,我们知道通常会通过 APT 包管理器来安装选定的软件。因此,如果我们想手动安装它,包括用于管理的匹配客户端,我们会发出以下命令:

# sudo apt install mariadb-server mariadb-client

当然,在 CentOS 上,情况完全不同——尽管 MariaDB 有可用的包,但安装它们的命令将是以下命令:

# sudo yum install mariadb mariadb-server

尽管 Ansible 可以自动化很多企业 Linux 的需求,但它不能抽象掉不同 Linux 操作系统之间的一些根本差异。然而,幸运的是,Ansible 使得其他一切都变得相当简单。考虑以下清单:

[servers]
ubuntu-testhost
centos-testhost

在本书中,我们一直倡导构建标准操作环境,因此这个清单在实际生活中不太可能出现——然而,它在这里作为一个很好的示例,因为我们可以展示如何在两个不同的平台上安装 MariaDB 服务器。像本书中的早期示例一样,我们将通过使用角色来完成这个任务。

基于我们在本书前面关于模板的工作,考虑以下角色:

---
- name: Install MariaDB Server on Ubuntu or Debian
  apt:
    name: "{{ item }}"
    state: present
  loop:
    - mariadb-server
    - mariadb-client
  when: ansible_distribution == 'Debian' or ansible_distribution == 'Ubuntu'

- name: Install MariaDB Server on CentOS or RHEL
  yum:
    name: "{{ item }}"
    state: present
  loop:
    - mariadb-server
    - mariadb
  when: ansible_distribution == 'CentOS' or ansible_distribution == 'Red Hat Enterprise Linux'

这个打包好的角色将在 Ubuntu 和 CentOS 上(如果需要,也可以在Red Hat Enterprise LinuxRHEL)和 Debian 上)正确运行,并考虑到了不同的包管理器和不同的包名称。当然,如果你幸运地拥有一个完全统一的环境(例如,只有基于 Ubuntu Server 的环境),那么代码可以进一步简化。

有一个名为package的 Ansible 模块,它会根据执行 Playbook 的操作系统尝试检测正确的包管理器。尽管这消除了之前我们使用的基于 yum 和 apt 的任务的需求,但你仍然需要考虑不同 Linux 操作系统之间包命名的差异,因此你可能仍然需要使用when条件。

我们将定义一个简单的 Playbook 来调用角色,如下所示:

---
- name: Install MariaDB
  hosts: all
  become: yes

  roles:
    - installmariadb

现在,我们可以运行 Playbook 并观察发生了什么,如下所示:

从前面的输出中,你可以看到与每个系统无关的任务被跳过,而我们想要的包成功安装后返回了changed状态。此外,注意到在我们 CentOS 测试系统上安装名为mariadb的 MariaDB 客户端包时,任务状态返回了ok。这是因为在我们的role中定义的loop会逐个遍历每个列出的包并安装它;在 CentOS 中,mariadb包是mariadb-server包的依赖,因此在执行该任务时,它也被安装了。

尽管手动指定这点看起来可能是多余的,但它对我们在角色中保留它没有坏处,因为它确保无论发生什么,客户端包都会存在。这也是一种自我文档化的方式——几年后,某人可能会回到这个 playbook 并理解无论如何,MariaDB 客户端和服务器包都是必需的,即使他们不知道 CentOS 7 操作系统的这一细节。

在继续构建这个示例之前,关于包移除的一点说明。正如我们之前讨论过的那样,Ansible 任务是幂等的。例如,如果我们第二次运行我们的 playbook,我们会看到返回的结果都是ok。在下面的例子中,Ansible 已检测到我们选择的包已经安装,并且不会尝试第二次安装:

然而,如果你需要整理一些东西呢?也许一个标准镜像中包含的包已经过时或者由于安全原因需要删除。在这种情况下,单纯删除 playbook 或角色并不足够。虽然我们示例中的角色确保了包的安装,但删除角色并不会撤销这个过程。简而言之,如果不再需要这些包,我们必须手动卸载或移除更改。撤销我们的安装将需要如下的角色:

---
- name: Uninstall MariaDB Server on Ubuntu or Debian
  apt:
    name: "{{ item }}"
    state: absent
  loop:
    - mariadb-server
    - mariadb-client
  when: ansible_distribution == 'Debian' or ansible_distribution == 'Ubuntu'

- name: Uninstall MariaDB Server on CentOS or RHEL
  yum:
    name: "{{ item }}"
    state: absent
  loop:
    - mariadb-server
    - mariadb
  when: ansible_distribution == 'CentOS' or ansible_distribution == 'Red Hat Enterprise Linux'

请注意,角色几乎完全相同,唯一的区别是我们现在使用的是state: absent而不是state: present。这在大多数你可能运行的 Ansible 任务中是常见的——如果你想定义一个过程来撤销或以其他方式恢复更改,你需要单独编写。现在,当我们通过适当的 playbook 调用上述角色时,我们可以看到包被干净地卸载,如下图所示:

当然,有时我们想要安装的包并不包含在默认的操作系统包仓库中。

在接下来的部分,我们将讨论如何根据我们目前设定的自动化原则来处理这个问题。

安装非原生包

幸运的是,使用 Ansible 安装非本地软件包并不比安装本地软件包更难。理想情况下,在企业环境中,所有所需的软件包都会通过内部仓库提供,实际上我们会在本书后面介绍这一点。在这个例子中,企业仓库将与 Ansible 角色一起使用,类似于前一节中提到的那些。

然而,偶尔这可能不可行或不希望如此。例如,考虑一个开发或测试系统,在这里正在评估一个新软件包——在这种情况下,你可能不希望将一个测试包上传到企业仓库服务器,特别是当尚不清楚是否会有持续使用该软件包的需求时。尽管如此,我们仍希望遵循自动化的原则,确保我们以可重复、自动文档化的方式进行测试。

假设你正在为企业评估 Duplicati 备份软件,并需要安装最新的测试版进行一些测试。显然,你可以从他们的发布页面手动下载这个版本,复制到目标服务器,然后手动安装。然而,这种方式效率低下,显然也不是可重复的过程。幸运的是,我们之前使用的 aptyum 模块支持从本地路径和远程 URL 安装软件包。

因此,要测试 Duplicati 测试版 2.0.4.23 的安装,你可以编写如下角色:

---
- name: Install Duplicati beta on Ubuntu
  apt:
    deb: https://github.com/duplicati/duplicati/releases/download/v2.0.4.23-2.0.4.23_beta_2019-07-14/duplicati_2.0.4.23-1_all.deb
  when: ansible_distribution == 'Debian' or ansible_distribution == 'Ubuntu'

- name: Install Duplicati beta on CentOS or RHEL
  yum:
    name: https://github.com/duplicati/duplicati/releases/download/v2.0.4.23-2.0.4.23_beta_2019-07-14/duplicati-2.0.4.23-2.0.4.23_beta_20190714.noarch.rpm
    state: present
  when: ansible_distribution == 'CentOS' or ansible_distribution == 'Red Hat Enterprise Linux'

从这个角色可以看到,安装过程不需要先单独下载软件包,正如以下截图所示:

因此,无论是用于测试还是生产目的,你都可以安装在所选操作系统的默认软件包仓库中不可用的软件包,并保持自动化的好处。在接下来的部分中,我们将探讨 Ansible 如何安装完全没有打包且需要手动安装的软件。

安装未打包的软件

当然,有些软件没有 neatly 打包,需要更手动的安装方法。例如,考虑一下托管控制面板软件 Virtualmin。在写作本文时,通常需要用户下载一个 shell 脚本并执行它来完成安装。

幸运的是,再一次,Ansible 可以提供帮助——考虑以下角色:

---
- name: download virtualmin install script
  get_url:
   url: http://software.virtualmin.com/gpl/scripts/install.sh
   dest: /root/install.sh
   mode: 0755

- name: virtualmin install (takes around 10 mins) you can see progress using: tail -f /root/virtualmin-install.log
  shell: /root/install.sh --force --hostname {{ inventory_hostname }} --minimal --yes
  args:
    chdir: /root

在这里,我们使用了 Ansible 的 get_url 模块来下载安装脚本,然后使用 shell 模块来执行它。还请注意,我们可以将有用的说明添加到任务名称中——虽然这不能替代好的文档,但它非常有帮助,因为它告诉任何运行脚本的人如何使用 tail 命令查看安装进度。

注意,shell 模块在使用时需要小心——因为它无法知道你给它的 shell 任务是否已经运行过,所以每次运行 playbook 时,它都会执行该命令。因此,如果你第二次运行之前的角色,它会尝试重新安装 Virtualmin。你应该在 shell 任务下使用 when 子句,以确保它只在特定条件下运行——比如在之前的示例中,当 /usr/sbin/virtualmin(由 install.sh 安装)不存在时。

这种方法几乎可以扩展到任何你能想象的软件——你甚至可以下载一个源代码 tar 包,提取它并使用一系列 shell 模块调用在 Ansible 中构建代码。当然,这种情况不太可能发生,但这里的重点是 Ansible 可以帮助你创建可重复的安装,即使你没有访问 RPM 或 DEB 格式的预打包软件。

通过这种方式,几乎可以安装任何软件——毕竟,软件安装过程就是下载文件(或归档文件)、将其放入正确的位置并进行配置。从本质上讲,yumapt 等包管理器在幕后做的正是这些工作,Ansible 也能像我们在这里演示的那样处理这类活动。在接下来的章节中,我们将探讨如何使用 Ansible 在已经构建和/或安装了软件的系统上进行配置更改。

使用 Ansible 进行配置更改

在配置新服务时,任务很少仅仅通过安装所需的软件就完成。几乎总是需要在安装后进行配置阶段。

让我们详细考虑一些可能需要的基础配置更改示例。

使用 Ansible 进行小规模配置更改

在进行配置更改时,lineinfile Ansible 模块通常是你的首选工具,能够处理许多可能需要的小规模更改。考虑一下我们在本章早些时候提到的部署 MariaDB 服务器的例子。尽管我们成功安装了软件包,但它们将会以默认配置安装,这不太适用于除最基本使用案例之外的所有情况。

例如,MariaDB 服务器的默认绑定地址是127.0.0.1,这意味着无法通过外部应用程序使用我们的 MariaDB 安装。我们已经明确了需要以可靠、可重复的方式进行更改,那么让我们来看看如何使用 Ansible 来更改这个配置。

为了更改这个配置,首先我们需要做的是确定默认配置的位置以及其内容。接下来,我们将定义一个 Ansible 任务来重写配置。

以我们的 Ubuntu 服务器为例,服务的 bind-address 配置在 /etc/mysql/mariadb.conf.d/50-server.cnf 文件中——默认指令如下所示:

bind-address       = 127.0.0.1

因此,为了更改这一点,我们可能会采用一个简单的角色,如下所示:

---
- name: Reconfigure MariaDB Server to listen for external connections
  lineinfile:
    path: /etc/mysql/mariadb.conf.d/50-server.cnf
    regexp: '^bind-address\s+='
    line: 'bind-address = 0.0.0.0'
    insertafter: '^\[mysqld\]'
    state: present

- name: Restart MariaDB to pick up configuration changes
  service:
    name: mariadb
    state: restarted

让我们详细分解 lineinfile 任务并逐步解析:

  • path:告诉模块要修改哪个配置文件。

  • regexp:用于定位现有的行进行修改,如果该行存在,以免我们最终会有两个冲突的 bind-address 指令。

  • line:要替换/插入到配置文件中的行。

  • insertafter:如果 regexp 未匹配(即,文件中没有该行),该指令确保 lineinfile 模块将在 [mysqld] 声明后插入新行,从而确保它位于文件的正确部分。

  • state:将其设置为 present 状态可确保该行出现在文件中,即使原始的 regexp 没有匹配——在这种情况下,会根据 line 的值将一行添加到文件中。

在进行此修改后,我们知道 MariaDB 服务器不会立即生效任何配置更改,除非我们重启它,因此在角色的最后,我们会进行重启。现在,如果我们运行此命令,可以看到它达到了预期效果,如下图所示:

对于像这样的简单配置调整,在少量系统上,这可以精确地实现我们所期望的结果。然而,这种方法也有一些需要解决的缺点,特别是在修改时机以及系统长期完整性方面。即使有最好的自动化策略,手动更改也可能会破坏一致性和标准化,而这些恰恰是良好自动化实践的核心。因此,必须确保未来的剧本运行仍然能够得到预期的最终结果。我们将在下一节中探讨这个问题。

保持配置的完整性

以这种方式进行更改的问题在于,它们不具有良好的可扩展性。为生产工作负载调优 MariaDB 服务器通常需要设置六个或更多的参数。因此,我们之前编写的简单角色可能会发展成一堆难以解读的正则表达式和指令,管理起来更是困难。

正则表达式本身并不是万无一失的,它们的效果取决于编写的质量。在我们之前的示例中,我们使用了以下这一行来查找 bind-address 指令,并计划修改它。正则表达式 ^bind-address\s+= 意味着查找文件中符合以下条件的行:

  • 在行首有 bind-address 字面字符串(由 ^ 表示)

  • bind-address 字面字符串后面留一个或多个空格

  • 在这些空格后面加上一个 = 符号

这个正则表达式的目的是确保我们忽略如下的注释:

#bind-address = 0.0.0.0

然而,MariaDB 对其配置文件中的空白符相当宽容,我们在这里定义的正则表达式将无法匹配以下这些有效的排列形式:

bind-address=127.0.0.1
 bind-address = 127.0.0.1

在这些情况下,由于regexp参数没有匹配,我们的角色将会向配置文件中添加一行,内容是bind-address = 0.0.0.0指令。由于 MariaDB 将前面的例子视为有效的配置,最终我们会在文件中得到两个配置指令,这可能会导致意外的结果。不同的软件包也会以不同的方式处理这些情况,进一步增加了混淆。还有其他复杂性需要考虑。许多 Linux 服务具有高度复杂的配置,通常将配置拆分到多个文件中以便于管理。我们在测试的 Ubuntu 系统上使用的原生 MariaDB 服务器包的文档中指出了以下内容:

# The MariaDB/MySQL tools read configuration files in the following order:
# 1\. "/etc/mysql/mariadb.cnf" (this file) to set global defaults,
# 2\. "/etc/mysql/conf.d/*.cnf" to set global options.
# 3\. "/etc/mysql/mariadb.conf.d/*.cnf" to set MariaDB-only options.
# 4\. "~/.my.cnf" to set user-specific options.

然而,这个配置顺序是由/etc/mysql/mariadb.cnf文件决定的,该文件的底部有指令包括前面代码块中的第 2 和第 3 行所列的文件。完全有可能有人(无论是好心还是其他)简单地覆盖/etc/mysql/mariadb.cnf文件,使用一个新版本,这个新版本删除了这些子目录的包含语句,而是包含了以下内容:

[mysqld]
bind-address = 127.0.0.1

由于我们的角色使用lineinfile完全不知道这个文件,它将忠实地在/etc/mysql/mariadb.conf.d/50-server.cnf中设置该参数,而不理解这个配置文件已经不再被引用,结果在服务器上的表现——最多——是不可预测的。

虽然企业自动化的目标是所有系统的变更都应该通过类似 Ansible 的工具集中管理,但现实情况是,你不能总是保证这一点。有时,事情会出错,急于修复问题的人可能会被迫绕过流程以节省时间。同样,不熟悉系统的新员工可能会像我们在这里建议的那样进行更改。

或者,例如,参考我们在第五章《使用 Ansible 构建虚拟机模板以便部署》中提出的 SSH 守护进程配置。这里,我们提出了一个简单的角色(再次在以下代码块中展示,供参考),它将禁用通过 SSH 的 root 登录,这是为 SSH 守护进程推荐的多个安全参数之一:

---
- name: Disable root logins over SSH
  lineinfile:
    dest: /etc/ssh/sshd_config
    regexp: "^PermitRootLogin"
    line: "PermitRootLogin no"
    state: present

请注意,我们的regexp在处理空白字符时与我们的其他角色存在相同的弱点。当sshd在其配置文件中有两个重复的参数时,它会将第一个值视为正确的值。因此,如果我知道上面代码块中的角色正在对一个系统运行,我所需要做的就是将这些行放在/etc/ssh/sshd_config最上面

# Override Ansible roles
  PermitRootLogin yes

因此,我们的 Ansible 角色将忠实地在这台服务器上运行,并报告成功管理了 SSH 守护进程的配置,而实际上,我们已经覆盖了该配置并启用了 root 登录。

这些示例向我们展示了两件事。首先,在处理正则表达式时要非常小心。你越是彻底,特别是在处理空白字符时,效果越好。显然,在理想的世界里,这些工作是不必要的,但像这样的意外变化已经导致许多系统崩溃。为了防止前面提到的 SSH 守护进程示例成为可能,我们可以尝试以下正则表达式:

^\s*PermitRootLogin\s+

这将考虑PermitRootLogin关键字前的零个或多个空格,然后考虑后面一个或多个空格,同时考虑到sshd中内建的空白容忍度。然而,正则表达式非常字面化,我们还没有考虑制表符的情况!

最终,这将引导我们到第二个因素——通过这些示例展示的因素——即为了在企业规模上保持配置和系统完整性,并确保对自动化以及其所生成的系统具有高度信心,可能需要另一种配置管理方法。这正是我们在下一节中要探讨的内容——在大型企业规模下可靠地管理配置的技术。

在企业规模上管理配置

显然,从这些示例来看,在企业规模上管理配置需要采用另一种方法。在一个控制良好的环境中,对于做少量更改的情况,我们之前讨论的lineinfile方法没有问题,但让我们考虑一种更稳健的配置管理方法,更适合大型组织。

我们将从考虑用于简单静态配置更改(即所有服务器都相同的更改)的可扩展方法开始,下一节将详细讨论。

进行可扩展的静态配置更改

配置更改至关重要,它们必须是版本控制的、可重复的和可靠的——因此,让我们考虑一种能够实现这一目标的方法。让我们通过重新审视我们的 SSH 守护进程配置来开始一个简单的示例。在大多数服务器上,这可能是静态的,因为诸如限制远程 root 登录和禁用基于密码的登录等要求可能适用于整个系统。同样,SSH 守护进程通常通过一个中央文件配置——/etc/ssh/sshd_config

在 Ubuntu 服务器上,默认配置非常简单,如果去除所有空白字符和注释,仅包含六行。我们来对这个文件做一些修改,禁止远程 root 登录,禁用 X11Forwarding,并只允许基于密钥的登录,如下所示:

ChallengeResponseAuthentication no
UsePAM yes
X11Forwarding no
PrintMotd no
AcceptEnv LANG LC_*
Subsystem sftp /usr/lib/openssh/sftp-server
PasswordAuthentication no
PermitRootLogin no

我们将把这个文件存储在 roles/ 目录结构中,并通过以下角色任务部署它:

---
- name: Copy SSHd configuration to target host
  copy:
    src: files/sshd_config
    dest: /etc/ssh/sshd_config
    owner: root
    group: root
    mode: 0644

- name: Restart SSH daemon
  service:
    name: ssh
    state: restarted

在这里,我们使用 Ansible copy 模块将我们在角色中创建并存储的 sshd_config 文件复制到目标主机,并确保它具有适用于 SSH 守护进程的所有权和模式。最后,我们重启 SSH 守护进程以使更改生效(请注意,这个服务名称在 Ubuntu 服务器上有效,其他 Linux 发行版可能会有所不同)。因此,我们完成的 roles 目录结构如下所示:

roles/
└── securesshd
    ├── files
    │   └── sshd_config
    └── tasks
        └── main.yml

现在,我们可以运行以下命令将配置部署到我们的测试主机:

现在,通过这种方式部署配置相较于我们之前探索的其他方法,具有一些优势,如下所示:

  • 该角色本身可以提交到版本控制系统中,从而隐式地将配置文件(位于角色的 files/ 目录中)纳入版本控制。

  • 我们的角色任务非常简单,别人很容易理解这段代码的功能,无需解读正则表达式。

  • 无论我们的目标机器配置发生什么变化,尤其是在空白字符或配置格式方面,都不重要。由于我们在部署时直接覆盖文件,因此避免了前一节中讨论的所有陷阱。

  • 所有机器的配置都是一致的,不仅在指令上,而且在顺序和格式上都保持一致,从而确保能够轻松审计企业中的配置。

因此,这个角色代表了企业级配置管理的一个重要进步。然而,让我们看看如果第二次对同一主机运行该角色时会发生什么。结果输出可以在以下截图中看到:

从前面的截图中,我们可以看到 Ansible 已确定 SSH 配置文件自上次运行以来没有被修改,因此返回了 ok 状态。然而,尽管如此,Restart SSH daemon 任务的 changed 状态表示 SSH 守护进程已经重启,尽管没有做配置更改。重启系统服务通常会带来中断,因此除非绝对必要,否则应避免重启。在这种情况下,我们希望只有在配置发生更改时才重启 SSH 守护进程。

处理这个问题的推荐方法是使用handlerhandler是 Ansible 中的一种构造,类似于任务,但只有在进行更改时才会被调用。此外,当对配置进行多次更改时,处理程序可能会被多次通知(每次适用的更改一次),但 Ansible 引擎会将所有的处理程序调用批量执行,并且只有在所有任务完成后才会运行处理程序。这确保了在像本例中重启服务时,服务只会重启一次,并且仅在进行更改时重启。让我们现在进行测试,如下所示:

  1. 首先,移除角色中的服务重启任务,并添加notify子句以通知处理程序(我们稍后将创建该处理程序)。最终的角色任务应如下所示:
---
- name: Copy SSHd configuration to target host
  copy:
    src: files/sshd_config
    dest: /etc/ssh/sshd_config
    owner: root
    group: root
    mode: 0644
  notify:
    - Restart SSH daemon
  1. 现在,我们需要在角色中创建一个handlers/目录,并将之前删除的处理程序代码添加到其中,使其看起来如下所示:
---
- name: Restart SSH daemon
  service:
    name: ssh
    state: restarted
  1. 结果,roles目录结构现在应如下所示:
roles/
└── securesshd
    ├── files
    │   └── sshd_config
    ├── handlers
    │   └── main.yml
    └── tasks
        └── main.yml
  1. 现在,当我们在同一台服务器上运行剧本两次(并且最初将 SSH 配置恢复到原始配置后),我们看到 SSH 守护进程只有在实际更改配置时才会重新启动,如下图所示:

为了进一步演示处理程序,在我们继续之前,让我们考虑对角色任务的这一改进:

---
- name: Copy SSHd configuration to target host
  copy:
    src: files/sshd_config
    dest: /etc/ssh/sshd_config
    owner: root
    group: root
    mode: 0644
  notify:
    - Restart SSH daemon

- name: Perform an additional modification
  lineinfile:
    path: /etc/ssh/sshd_config
    regexp: '^\# Configured by Ansible'
    line: '# Configured by Ansible on {{ inventory_hostname }}'
    insertbefore: BOF
    state: present
  notify:
    - Restart SSH daemon

在这里,我们部署配置文件并进行额外的修改。我们在文件的开头添加了一个注释,注释中包括一个 Ansible 变量,包含目标主机的主机名。

这将导致我们的目标主机上有两个更改的状态,但是,如果我们恢复到默认的 SSH 守护进程配置,然后运行新的剧本,我们会看到如下结果:

仔细查看前面的输出和任务执行的顺序。你会注意到处理程序并不是按顺序执行的,实际上它只会在剧本结束时执行一次。

尽管我们的任务发生了变化,因此会通知处理程序两次,但处理程序仅在剧本运行结束时执行,从而最小化了重启次数,正如所要求的那样。

通过这种方式,我们可以在大规模(跨越数百甚至数千台机器)上对静态配置文件进行更改。在接下来的部分,我们将以此为基础,展示如何管理需要动态数据的配置——例如,可能会因主机或组的不同而变化的配置参数。

进行可扩展的动态配置更改

尽管前面的例子解决了在企业中大规模进行自动化配置更改的许多挑战,但我们的最后一个例子还是显得有些低效。我们部署了一个静态、版本控制的配置文件,并再次使用lineinfile模块对其进行更改。

这使我们能够将 Ansible 变量插入到文件中,在许多情况下,这非常有用,尤其是在配置更复杂的服务时。然而,这种做法——充其量——是不优雅的,因为它将这个更改分拆到两个任务中。此外,重新使用lineinfile模块会再次让我们暴露于我们之前讨论的风险,并且意味着我们需要为每个要插入到配置中的变量创建一个lineinfile任务。

幸运的是,Ansible 正是为了解决这个问题而设计的。在这种情况下,Jinja2 模板化的概念为我们提供了帮助。

Jinja2 是一种为 Python 设计的模板语言,非常强大且易于使用。由于 Ansible 几乎完全用 Python 编写,因此它非常适合使用 Jinja2 模板。那么,什么是 Jinja2 模板呢?从最基本的层面来看,它是一个静态配置文件,比如我们之前为 SSH 守护进程部署的文件,但可以进行变量替换。当然,Jinja2 比这更强大——本质上,它是一个独立的语言,拥有常见的语言构造,如for循环和if...elif...else结构,就像你在其他编程语言中看到的那样。这使得它非常强大和灵活,整个配置文件中的某些部分(例如)可以根据if语句的求值情况被省略。

正如你可以想象的那样,Jinja2 完全值得写一本书来详细讲解这个语言——然而,在这里,我们将提供一个实用的 Jinja2 模板化入门,以便在企业的配置管理自动化中使用。

让我们回到之前的 SSH 守护进程示例,假设我们希望将目标主机名放入文件头部的注释中。虽然这是一个人为的例子,但将其从copy/lineinfile示例推进到单个template任务,将展示模板化带来的好处。从这里,我们可以进一步扩展到更全面的示例。首先,让我们为sshd_config文件定义我们的 Jinja2 模板,如下所示:

# Configured by Ansible {{ inventory_hostname }}
ChallengeResponseAuthentication no
UsePAM yes
X11Forwarding no
PrintMotd no
AcceptEnv LANG LC_*
Subsystem sftp /usr/lib/openssh/sftp-server
PasswordAuthentication no
PermitRootLogin no

请注意,该文件与我们之前使用复制模块部署的文件完全相同,只不过现在,我们在文件头部包含了注释,并使用了 Ansible 变量构造(用一对大括号表示)来插入inventory_hostname变量。

现在,为了保持理智,我们将这个文件命名为sshd_config.j2,以确保我们能区分模板文件和普通的配置文件。模板通常被放置在角色(role)中的templates/子目录下,因此像 playbook、角色及任何相关的静态配置文件一样,它们也受到版本控制的管理。

现在,我们不再是先复制静态文件然后通过一个或多个lineinfile任务进行替换,而是可以使用 Ansible 的template模块来部署这个模板并解析所有 Jinja2 构造。

因此,我们现在的任务看起来是这样的:

---
- name: Copy SSHd configuration to target host
  template:
    src: templates/sshd_config.j2
    dest: /etc/ssh/sshd_config
    owner: root
    group: root
    mode: 0644
  notify:
    - Restart SSH daemon

请注意,这个任务几乎与我们之前的copy任务相同,并且我们如之前一样调用了处理程序。

完成的模块目录结构现在如下所示:

roles
└── securesshd
    ├── handlers
    │   └── main.yml
    ├── tasks
    │   └── main.yml
    └── templates
        └── sshd_config.j2

让我们运行这个并评估结果,结果可以在以下截图中看到:

如图所示,模板已经复制到目标主机,头部注释中的变量已被处理并替换为适当的值。

当我们的配置变得越来越复杂时,这将变得非常强大,因为无论模板多么庞大复杂,角色仍然只需要一个template任务。回到我们的 MariaDB 服务器,假设我们想要在每个服务器上设置一些参数,以适应我们正在部署的不同工作负载。也许我们想设置以下参数:

  • 服务器绑定地址,由bind-address定义

  • 最大二进制日志大小,由max_binlog_size定义

  • MariaDB 监听的 TCP 端口,由port定义

所有这些参数都在/etc/mysql/mariadb.conf.d/50-server.cnf中定义。然而,正如之前讨论的那样,我们还需要确保/etc/mysql/mariadb.cnf的完整性,以确保它包括此文件(以及其他文件),以减少有人覆盖我们配置的可能性。让我们开始构建我们的模板——首先是一个简化版的50-server.cnf文件,带有一些变量替换。该文件的第一部分如以下代码所示——请注意portbind-address参数,现在使用 Ansible 变量定义,按惯例用成对的大括号表示:

[server]
[mysqld]
user = mysql
pid-file = /var/run/mysqld/mysqld.pid
socket = /var/run/mysqld/mysqld.sock
port = {{ mariadb_port }}
basedir = /usr
datadir = /var/lib/mysql
tmpdir = /tmp
lc-messages-dir = /usr/share/mysql
skip-external-locking
bind-address = {{ mariadb_bind_address }}

该文件的第二部分如下所示——在这里你将看到mariadb_max_binlog_size变量的存在,而所有其他参数保持静态:

key_buffer_size = 16M
max_allowed_packet = 16M
thread_stack = 192K
thread_cache_size = 8
myisam_recover_options = BACKUP
query_cache_limit = 1M
query_cache_size = 16M
log_error = /var/log/mysql/error.log
expire_logs_days = 10
max_binlog_size = {{ mariadb_max_binlog_size }}
character-set-server = utf8mb4
collation-server = utf8mb4_general_ci
[embedded]
[mariadb]
[mariadb-10.1]

现在,我们也来添加一个/etc/mysql/mariadb.cnf的模板版本,如下所示:

[client-server]
!includedir /etc/mysql/conf.d/
!includedir /etc/mysql/mariadb.conf.d/

这个文件可能很短,但它有一个非常重要的作用。它是 MariaDB 服务加载时读取的第一个文件,并引用其他要包含的文件或目录。如果我们没有通过 Ansible 来维护这个文件的控制,那么任何具有足够权限的人都可以登录并编辑文件,可能会包含完全不同的配置,完全绕过我们通过 Ansible 定义的配置。每当你通过 Ansible 部署配置时,考虑类似这样的因素非常重要,否则你的配置更改可能会被一个出于善意(或其他原因)的管理员绕过。

模板不一定要包含任何 Jinja2 构造——如果没有变量需要插入,如我们第二个示例中所示,文件将直接原样复制到目标机器。

显然,使用 copy 模块将这个静态配置文件发送到远程服务器会稍微更高效,但这需要两个任务,而我们可以通过一个循环来处理所有的模板,从而只使用一个任务。这样的示例展示在以下代码块中:

---
- name: Copy MariaDB configuration files to host
  template:
    src: {{ item.src }}
    dest: {{ item.dest }}
    owner: root
    group: root
    mode: 0644
  loop:
    - { src: 'templates/mariadb.cnf.j2', dest: '/etc/mysql/mariadb.cnf' }
    - { src: 'templates/50-server.cnf.j2', dest: '/etc/mysql/mariadb.conf.d/50-server.cnf' }
  notify:
    - Restart MariaDB Server

最后,我们定义一个处理程序来重新启动 MariaDB,如果配置发生了变化,如下所示:

---
- name: Restart MariaDB Server
  service:
    name: mariadb
    state: restarted

现在,在运行之前,先说一下变量的事。在 Ansible 中,变量可以在多个层次上定义。在这种情况下,由于我们正在为具有不同用途的不同主机应用不同的配置,因此在主机或主机组级别定义变量是有意义的。然而,如果有人忘记在清单中或其他适当的位置放置这些变量会怎样?幸运的是,我们可以利用 Ansible 的变量优先级顺序来让我们受益,并为我们的角色定义默认变量。这些变量的优先级是第二低的,因此几乎总是会被其他地方的设置覆盖,但它们提供了一张安全网,以防它们被意外遗漏。由于我们之前的模板是如此编写,如果变量没有在任何地方定义,配置文件将无效,MariaDB 服务器将拒绝启动——这是我们绝对希望避免的情况。

现在,让我们在我们的角色中定义这些变量的默认值,在defaults/main.yml中,如下所示:

---
mariadb_bind_address: "127.0.0.1"
mariadb_port: "3306"
mariadb_max_binlog_size: "100M"

完成这些后,我们的角色结构应如下所示:

roles/
└── configuremariadb
    ├── defaults
    │   └── main.yml
    ├── handlers
    │   └── main.yml
    ├── tasks
    │   └── main.yml
    └── templates
        ├── 50-server.conf.j2
        └── mariadb.cnf.j2

当然,我们希望覆盖默认值,因此我们将在我们的清单分组中定义这些变量——这是一个很好的使用清单组的案例。所有执行相同功能的 MariaDB 服务器都将放入一个清单组中,然后为它们分配一组共同的清单变量,从而使它们都接收相同的配置。然而,我们在角色中使用模板意味着我们可以在多种情况下重用这个角色,只需通过变量定义提供不同的配置。我们将为我们的测试主机创建一个看起来像这样的清单:

[dbservers]
ubuntu-testhost

[dbservers:vars]
mariadb_port=3307
mariadb_bind_address=0.0.0.0
mariadb_max_binlog_size=250M

完成这一切后,我们终于可以运行我们的剧本并观察发生了什么。结果显示在以下屏幕截图中:

在成功运行之后,我们展示了一个完整的端到端示例,演示了如何在企业规模上管理配置,同时避免常规表达式替代和多部分配置的陷阱。虽然这些示例很简单,但它们应作为任何精心设计的企业自动化策略的基础,在这种策略中需要进行配置。

总结

在企业级 Linux 环境中管理配置充满了陷阱,并且存在配置漂移的潜在风险。这可能是由于好心人造成的,即使是在修复场景中,迫切需要做出更改时也会发生。然而,这也可能是那些有恶意意图的人造成的,他们试图规避安全要求。良好的 Ansible 使用,尤其是模板化,能够构建易读、简洁的 playbook,确保配置管理是可靠、可重复、可审计和版本控制的——这也是我们在本书前面所提到的企业自动化实践的基本原则。

在本章中,你获得了通过为 Linux 机器扩展新软件包的实践经验。接着,你学习了如何对这些软件包应用简单的静态配置更改,以及相关的潜在陷阱。最后,你了解了使用 Ansible 管理企业级配置的最佳实践。在下一章中,我们将继续探讨 Pulp 的内部仓库管理。

问题

  1. 常用的 Ansible 模块有哪些,用于更改配置文件?

  2. Ansible 中的模板化是如何工作的?

  3. 在使用 Ansible 进行更改时,为什么必须考虑配置文件的结构?

  4. 在进行文件修改时,使用正则表达式存在哪些陷阱?

  5. 如果模板中没有变量,它会如何表现?

  6. 如何在将配置模板部署到磁盘之前,检查其有效性?

  7. 如何使用 Ansible 快速检查 100 台机器的配置,确保其符合已知模板?

进一步阅读

第三部分:日常管理

本节讲解了在企业中管理 Linux 服务器不仅仅依赖于良好的构建流程——持续有效的管理至关重要。在本节中,我们将探讨如何使用 Ansible 和其他工具实现这些目标。

本节包括以下章节:

  • 第八章,使用 Pulp 管理企业仓库

  • 第九章,使用 Katello 打补丁

  • 第十章,Linux 用户管理

  • 第十一章,数据库管理

  • 第十二章,使用 Ansible 执行常规维护

第八章:使用 Pulp 进行企业级仓库管理

到目前为止,本书已涵盖了与 Linux 服务器的构建和配置相关的多个任务,旨在用于企业环境中的部署。虽然我们完成的许多工作能够很好地扩展,适用于大多数场景,但必须指出的是,到目前为止,我们只从两种来源之一安装了软件包——要么是与我们使用的每个 Linux 发行版相对应的上游公共软件包仓库,要么是在我们的 PXE 启动章节中,来自我们下载的 ISO 镜像。

不用说,这带来了几个挑战,特别是在创建可重复、可管理的 Linux 构建时。我们将在名为用于补丁管理的 Pulp 安装的部分中更深入地探讨这些问题,但可以简单地说,使用公共可用的仓库意味着两次在不同工作日进行的构建可能是不同的!ISO 安装方法则呈现了另一端的情况,总是产生一致的构建,无论何时执行,但在这种情况下,无法接收安全(或其他)更新!需要的是这两者之间的折衷方案,幸运的是,Pulp 就是这种折衷方案。

本章将探讨 Pulp,具体内容包括:

  • 用于补丁管理的 Pulp 安装

  • 在 Pulp 中构建仓库

  • 使用 Pulp 的补丁管理过程

技术要求

本章包括基于以下技术的示例:

  • Ubuntu Server 18.04 LTS

  • CentOS 7.6

  • Ansible 2.8

要完成这些示例,你将需要访问两台服务器或虚拟机,分别运行前面列出的操作系统之一,并安装 Ansible。请注意,本章提供的示例可能具有破坏性,且如果按原样运行,仅建议在隔离的测试环境中执行。

本章讨论的所有示例代码都可以在 GitHub 上获取,网址为:github.com/PacktPublishing/Hands-On-Enterprise-Automation-on-Linux/tree/master/chapter08

用于补丁管理的 Pulp 安装

在我们深入探讨如何安装 Pulp 的实际操作之前,让我们更深入地了解一下为何要使用它。在整本书中,我们一直提倡构建一个标准化的 Linux 环境,并具备高重复性、可审计性和可预测性。这些不仅是自动化的基础,也是在企业中良好的实践。

假设你构建了一台服务器,并用 Ansible 部署了一个新的服务,正如我们在本书前面所描述的那样。到目前为止,一切顺利——Ansible playbook 提供了构建标准的文档,并确保构建在以后能够准确重复。然而,有一个问题。假设几个月后,你需要再创建一台服务器——也许是为了扩展应用程序或进行灾难恢复DR)场景。根据你获取软件包的来源,可能会发生以下两种情况:

  • 如果你从面向公共互联网的仓库中安装,那么这两个构建将包含在构建时日期安装的所有软件包的最新版本。这种差异可能非常显著,如果你已经投入时间在某个特定 Linux 构建版本上进行测试和验证,使用不同的软件包版本可能无法保证这一点。当然,一切都是最新的,你会得到所有最新的安全补丁和 bug 修复,但每次你在不同的日期执行这个构建时,可能会得到不同的软件包版本。这会导致可重复性问题,尤其是在确保在一个环境中测试过的代码能够在另一个环境中正常工作时。

  • 另一端是我们在第六章《使用 PXE 启动进行自定义构建》中使用的 ISO 构建仓库。这些仓库从不变化(除非有人下载了更新的 ISO 并将其覆盖到旧的 ISO 上),因此它虽然生成完全已知数量的构建(从而支持我们的可重复性目标),但它们永远不会收到任何安全更新。这本身可能是一个问题。

妥协的地方,当然是要在这两者之间找到一个中间地带。如果能够创建我们自己的软件包仓库,并且这个仓库是某一时刻公共仓库的快照会怎样?这样,当我们需要它们时,它们保持静态(从而确保构建的一致性),而如果出现重要的安全修复,仍然可以按需更新。Pulp 项目正是为我们解决了这个问题,能够做正是这些事情。它也是一些更复杂的基础设施管理解决方案中的一个组件,例如 Katello,正如我们在下一章中将看到的那样。

然而,对于不需要图形用户界面GUI)的安装,Pulp 完全满足我们的需求。让我们来看看如何安装它。

安装 Pulp

正如我们在第一章中讨论的在 Linux 上构建标准操作环境,在本书中,有时即使您可能已经围绕像 Ubuntu Server 这样的特定 Linux 发行版构建了标准化操作环境,您仍然需要创建一个例外情况。 Pulp 就是这样一个例子,尽管它可以管理.rpm.deb软件包(因此可以处理各种 Linux 发行版的存储库要求),但它只针对 CentOS、Fedora 和基于 RHEL 的操作系统打包(因此安装起来最简单)。您仍然可以使用 Pulp 管理您的 Ubuntu Server 环境,只需在 CentOS(或您喜欢的 Red Hat 变体)上安装它。

Pulp 安装有几个方面。例如,Pulp 依赖于 MongoDB 安装,如果需要的话可以是外部的。同样,它还依赖于消息总线,可以根据需要使用 RabbitMQ 或 Qpid。大多数组织将对这些事物有自己的标准,因此你可以根据你企业的需求来定义最适合的架构。在本章中,我们将在单个服务器上执行一个非常简单的 Pulp 安装,以演示涉及的步骤。

鉴于安装 Pulp 的相对复杂性,建议您为 Pulp 安装创建一个 Ansible Playbook。但是,在本章中,我们将完成手动安装,以演示所涉及的工作量——因为没有适合所有情况的 Pulp 安装:

  1. 在我们开始安装之前,我们必须构建一个虚拟(或物理)服务器来托管我们的 Pulp 存储库。在我们的示例中,我们将基于 CentOS 7.6 进行操作,这是撰写本文时 Pulp 支持的最新版本。另外,请注意以下文件系统要求:

    • /var/lib/mongodb:我们将在同一主机上使用 MongoDB 构建我们的示例 Pulp 服务器。MongoDB 数据库的大小可以增长到超过 10 GB,并建议将此路径挂载在专用的 LVM 支持的文件系统上,以便在需要时可以轻松扩展,并且如果它真的填满了,不会使系统的其余部分停止工作。

    • /var/lib/pulp:这个目录是 Pulp 存储库的所在地,同样应该在专用的 LVM 支持的文件系统上。其大小将由您希望创建的存储库决定——例如,如果您想要镜像一个 20 GB 的上游存储库,则/var/lib/pulp的最小大小应为 20 GB。此文件系统还必须基于 XFS——如果在ext4上创建,您可能会用尽索引节点。

  2. 满足了这些要求后,我们必须安装 EPEL 存储库,因为 Pulp 安装将从这里获取软件包:

$ sudo yum install epel-release
  1. 然后我们需要安装 Pulp 存储库文件:
$ sudo wget -O /etc/yum.repos.d/rhel-pulp.repo https://repos.fedorapeople.org/repos/pulp/pulp/rhel-pulp.repo
  1. 接下来,我们设置 MongoDB 服务器——这必须在继续进行 Pulp 安装之前完成。预计大多数企业会有一些关于数据库服务器的内部标准,他们会遵循这些标准——在这里,我们将使用带有 SSL 加密的默认安装:
$ sudo yum install mongodb-server
  1. 再次说,公平地讲,大多数企业会有自己的证书授权机构,无论是内部的还是其他的。对于我们的示例服务器,我们将使用以下命令生成一个简单的自签名证书:
$ sudo openssl req -x509 -nodes -newkey rsa:4096 -keyout /etc/ssl/mongodb-cert.key -out /etc/ssl/mongodb-cert.crt -days 3650 -subj "/C=GB/CN=pulp.example.com"
  1. 然后我们需要将私钥和证书合并成一个文件,供 MongoDB 使用:
$ sudo cat /etc/ssl/mongodb-cert.key /etc/ssl/mongodb-cert.crt | sudo tee /etc/ssl/mongodb.pem > /dev/null
  1. 完成此步骤后,我们必须重新配置 MongoDB 以使用新创建的证书文件并启用 SSL。编辑/etc/mongod.conf文件,并配置以下参数(文件中的其他参数可以保留默认值):
# Use ssl on configured ports
sslOnNormalPorts = true

# PEM file for ssl
sslPEMKeyFile = /etc/ssl/mongodb.pem
  1. 在此阶段,我们现在可以启用 MongoDB 服务,使其在启动时自动启动,并启动它:
$ sudo systemctl enable mongod.service
$ sudo systemctl restart mongod.service
  1. 在我们的 Mongo 数据库服务器运行后,我们现在需要安装消息总线。同样,大多数企业会有关于这方面的公司标准,建议遵循这些标准(如果已定义)。以下示例是功能性演示所需的最低步骤——它不应被视为完全安全,但为了测试和评估 Pulp,它是可用的。在这里,我们仅安装所需的包,然后启用并启动服务:
$ sudo yum install qpid-cpp-server qpid-cpp-server-linearstore
$ sudo systemctl enable qpidd.service
$ sudo systemctl start qpidd.service
  1. 完成底层基础设施后,我们现在可以安装 Pulp 本身。初步步骤是安装基础包:
$ sudo yum install pulp-server python-gofer-qpid python2-qpid qpid-tools

Pulp 采用基于插件的架构来托管它能够提供的各种仓库。写作时,Pulp 能够托管以下内容:

    • 基于 RPM 的仓库(例如,CentOS、RHEL 和 Fedora)

    • 基于 DEB 的仓库(例如,Debian 和 Ubuntu)

    • Python 模块(例如,用于镜像 PyPI 内容)

    • Puppet 清单

    • Docker 镜像

    • OSTree 内容

不幸的是,本章没有空间让我们详细讲解所有这些模块——不过可以说,在高层次上,Pulp 在这些不同技术中的运作方式是相同的。无论是处理 Python 模块、Docker 镜像,还是 RPM 包,你都可以创建一个稳定的中央仓库,并且可以进行版本控制,以确保能够维持一个最新的环境,同时又不会失去对该环境中内容的控制。

由于我们的用例是使用 Pulp 提供 Linux 包,我们将安装基于 RPM 和 DEB 的插件:

$ sudo yum install pulp-deb-plugins pulp-rpm-plugins
  1. 安装完 Pulp 后,我们必须配置核心服务。通过编辑/etc/pulp/server.conf来执行此操作——大多数默认设置对于我们这样的简单演示来说已经足够——然而,由于我们在 MongoDB 后端启用了 SSL 支持,我们必须告诉 Pulp 服务器我们已经这样做,并禁用 SSL 验证,因为我们使用的是自签名证书。上述文件的[database]部分应该如下所示:
[database]
ssl: true
verify_ssl: false

如果您查看此文件,您会看到可以进行大量配置,所有这些都有文档和注释。具体来说,您可以自定义以下部分:

    • [email]:默认情况下这是关闭的,但如果您希望 Pulp 服务器发送电子邮件报告,您可以在此进行配置。

    • [database]:在这一部分,我们仅启用了 SSL 支持,但如果数据库在外部服务器上,或者需要更多高级参数,这些都将在此指定。

    • [messaging]:用于不同 Pulp 组件之间的通信,默认的 Qpid 消息代理无需在此进一步配置,但如果您使用 RabbitMQ 和/或启用了身份验证/SSL 支持,则需要在此进行配置。

    • [tasks]:Pulp 可以为不同组件之间的通信和异步任务使用独立的消息代理,后者的代理可以在此配置。由于我们为这两个功能使用了相同的 Qpid 实例,因此此示例中无需进一步配置。

    • [server]:用于配置服务器的默认凭据、主机名等。

  1. 配置完 Pulp 服务器后,我们必须使用以下两条命令生成 Pulp 的 RSA 密钥对和 CA 证书:
$ sudo pulp-gen-key-pair
$ sudo pulp-gen-ca-certificate
  1. Pulp 使用 Apache 来提供其 HTTP(S) 内容,因此我们必须进行配置。首先,我们通过运行以下命令初始化后台数据库(注意它是以apache用户身份运行的):
$ sudo -u apache pulp-manage-db
  1. 如果您打算在 Apache 中使用 SSL 传输,请确保根据您的企业要求进行配置。CentOS 默认为 Apache SSL 安装了自签名证书,但您可能想用企业 CA 签发的证书替换它。此外,务必禁用不安全的 SSL 协议——至少建议将以下两个设置添加到/etc/httpd/conf.d/ssl.conf中:
SSLProtocol all -SSLv2 -SSLv3

SSLCipherSuite HIGH:3DES:!aNULL:!MD5:!SEED:!IDEA

当然,这只是一个指南,大多数企业会有自己的安全标准,必须遵守这些标准。

随着新漏洞的发现,这些要求可能会发生变化。上述配置在写作时被认为是最佳实践,但可能会随时发生变化,恕不另行通知。您有责任检查您环境中的所有安全相关设置。

  1. 配置完 Apache 后,设置它在启动时自动启动并启动它:
$ sudo systemctl enable httpd.service
$ sudo systemctl start httpd.service
  1. Pulp 还有其他一些后台服务,启动这些服务是其正常运行所必需的。每个服务都可以根据需要进行配置和调整,但再次说明,为了我们的示例服务器,依次启用并启动每个服务就足够了:
$ sudo systemctl enable pulp_workers.service
$ sudo systemctl start pulp_workers.service

$ sudo systemctl enable pulp_celerybeat.service
$ sudo systemctl start pulp_celerybeat.service

$ sudo systemctl enable pulp_resource_manager.service
$ sudo systemctl start pulp_resource_manager.service
  1. 我们的最终任务是安装 Pulp 的管理组件,以便我们能够管理我们的服务器:
$ sudo yum install pulp-admin-client pulp-rpm-admin-extensions pulp-deb-admin-extensions
  1. 对于我们的服务器,还有一个最后的任务需要完成。Pulp 设计为远程管理,因此它通过 SSL 进行通信,以确保所有交易的安全性。尽管我们创建了一个一体化主机,并且在本章中将从同一主机执行服务器管理,但我们需要告诉 Pulp 管理员客户端我们正在使用自签名证书——否则 SSL 验证将失败。为此,编辑/etc/pulp/admin/admin.conf,并在[server]部分定义以下参数:
verify_ssl: False
  1. 最后,我们可以通过登录到 Pulp 服务器来测试它是否正常运行。虽然 Pulp 支持多个用户账户,甚至与 LDAP 后端的集成,但像我们这样简单的安装只提供一个管理员账户,其中用户名和密码均为admin

如果一切顺利,你应该能看到类似于以下的输出,并能够查询服务器状态(请注意,输出已被截断以节省空间):

现在,我们已经有了一个完全可操作的 Pulp 服务器,我们将展示如何使用我们新建的 Pulp 系统创建用于管理稳定更新和系统构建的仓库。

在 Pulp 中构建仓库

虽然在本章中我们只会使用 Pulp 中可用功能的一个子集,但我们希望展示一个可行的工作流,展示为什么你可能会选择 Pulp 来管理企业仓库,而不是自己开发解决方案(例如,像我们在第六章中所做的那样,使用 PXE 启动进行自定义构建)。

处理基于 RPM 的软件包仓库和基于 DEB 的软件包仓库的过程大致相似。

让我们从探索如何创建和管理基于 RPM 的仓库开始。

在 Pulp 中构建基于 RPM 的仓库

尽管安装 Pulp 的过程相当复杂,但一旦安装完成,管理仓库的过程就非常简单。不过,这确实需要了解你所选择的 Linux 发行版的仓库结构。让我们继续使用本书中一直作为示例的 CentOS 7 版本。

核心的 CentOS 7 仓库分为两部分——首先是 OS 仓库;它包含 CentOS 7 最新点版本的所有文件——截至写作时,最新版本是 7.6。该版本在 2018 年 11 月更新过一次,并将在 CentOS 7.7 发布之前保持静态状态。此版本的更新将包含在一个单独的仓库中,因此,要在我们的 Pulp 服务器上构建一个完整的 CentOS 7 镜像,我们需要镜像这两个路径。

让我们从创建基础操作系统的镜像开始:

  1. 第一步是登录到pulp-admin客户端,正如我们在上一节的结尾所展示的。然后,在此基础上,我们运行以下命令来创建一个新的仓库:
$ pulp-admin rpm repo create --repo-id='centos76-os' --relative-url='centos76-os' --feed=http://mirror.centos.org/centos/7/os/x86_64/

让我们来拆解一下这个命令:

    • rpm repo create:这组关键字告诉 Pulp 服务器创建一个新的基于 RPM 的仓库定义。请注意,在此阶段并不会同步或发布任何内容——这只是为新仓库创建元数据。

    • --repo-id='centos76-os':这告诉 Pulp 我们的新仓库的 ID 是centos76-os——这类似于一个唯一的标识符,应当用来区分你的新仓库与其他仓库。

    • --relative-url='centos76-os':这指示 Pulp 将仓库发布到何处——基于 RPM 的仓库发布地址为http(s)://pulp-server-address/pulp/repos/<relative-url>

    • --feed=http://mirror.centos.org/centos/7/os/x86_64/:这是将同步 RPM 内容的上游位置。

  1. 在创建好我们的仓库定义后,下一步是从上游服务器同步软件包。只需运行以下命令即可:
$ pulp-admin rpm repo sync run --repo-id='centos76-os'
  1. 这将启动一个异步命令,在服务器后台运行——你可以随时使用以下命令检查状态:
$ pulp-admin rpm repo sync status --repo-id='centos76-os'
  1. 最后,一旦同步完成,必须发布该仓库——这实际上是通过 Pulp 安装时配置的 Apache Web 服务器,使同步的内容可以访问:
$ pulp-admin rpm repo publish run --repo-id='centos76-os'

现在,完成此步骤后,你将拥有由--feed参数定义的上游 CentOS 7.6 操作系统仓库的内部快照,即使在 CentOS 7.7 发布时,这个快照也会在我们的 Pulp 服务器上保持不变。

现在,当然,我们还需要更新,以确保获取最新的安全补丁、错误修复等。仓库的更新频率将取决于你的补丁周期、内部安全政策等。因此,我们将定义第二个仓库来存放更新包。

我们将发出一组与之前几乎相同的命令来创建更新仓库,只是这次有两个关键的区别:

  • 我们使用/updates/路径来获取源内容,而不是/os/

  • 我们在repo-idrelative-url中加入了日期戳——当然,你也可以采用自己的版本控制方案——然而,由于这个仓库将是 2019 年 8 月 7 日之前所有 CentOS 7 更新的快照,使用快照日期作为标识符是一个合理的做法:

$ pulp-admin rpm repo create --repo-id='centos7-07aug19' --relative-url='centos7-07aug19' --feed=http://mirror.centos.org/centos/7/updates/x86_64/
$ pulp-admin rpm repo sync run --repo-id='centos7-07aug19'
$ pulp-admin rpm repo publish run --repo-id centos7-07aug19

通过运行此命令,我们可以使用pulp-admin客户端来检查仓库并查看磁盘使用情况。目前,我们可以看到 Pulp 文件系统已经使用了 33GB 的空间,虽然并不是全部用于 CentOS,因为这个测试系统上还有其他仓库。稍后,这个使用情况将变得重要。

在企业环境中,一个好的做法是构建或更新一组测试用的 CentOS 7 系统到这次 8 月 7 日的快照,并在其上进行必要的测试,以确保对构建的信心。对于物理系统来说,这一点尤其重要,因为内核变更可能会引发问题。一旦建立了对这个构建的信心,它就成为所有 CentOS 7 系统的基准。对于企业场景来说,最棒的一点是,所有系统(只要它们使用 Pulp 仓库)将拥有所有软件包的相同版本。这与我们在本书中讨论的良好自动化实践结合,能为 Linux 环境带来几乎像 Docker 一样的稳定性和平台信心。

基于这个场景,假设过夜发布了一个关键的安全补丁,针对的是 CentOS 7。尽管及时应用这个补丁非常重要,但同样重要的是进行测试,确保它不会破坏任何现有服务。因此,我们不希望更新我们的 centos7-07aug19 仓库镜像,因为这是一个已知的稳定快照(换句话说,我们已经测试过它并且满意——它在我们的企业环境中是稳定的)。

如果我们只是使用上游的面向互联网的仓库,那么我们将无法控制这一点,我们的 CentOS 7 服务器会在下一次更新时盲目地获取这个补丁。同样,如果我们手动使用像 reposync 这样的工具来构建仓库镜像,我们将有两个选择。首先,我们可以更新现有的镜像,这样我们会节省一些磁盘空间,但这会带来与使用上游仓库相同的问题(即所有服务器在运行更新时都会立刻获取新的补丁)。另外,我们可以创建第二个快照进行测试。我估算,在 Pulp 服务器上镜像 CentOS 7 更新大约需要 16 GB 的磁盘空间,因此创建第二个快照需要大约 32 GB 的磁盘空间。随着时间的推移,更多的快照将需要越来越多的磁盘空间,这显然非常低效。

这正是 Pulp 真正出色的地方——它不仅能够高效地创建和管理基于 RPM 的仓库,而且还知道在同步操作中不下载已存在的包,并且在发布时不会重复包——因此,它在带宽和磁盘使用方面非常高效。基于此,我们可以执行以下命令集来创建一个新的 CentOS 7 更新快照,日期为 8 月 8 日:

$ pulp-admin rpm repo create --repo-id='centos7-08aug19' --relative-url='centos7-08aug19' --feed=http://mirror.centos.org/centos/7/updates/x86_64/
$ pulp-admin rpm repo sync run --repo-id='centos7-08aug19'
$ pulp-admin rpm repo publish run --repo-id centos7-08aug19

你会发现,这与我们在本节中早些时候运行的命令非常相似,用于创建 2019 年 8 月 7 日的快照——它们实际上是相同的,除了新的仓库 ID(--repo-id)和 URL(--relative-url),这些参数带有新的日期,用来与我们之前的快照区分开。这个过程将像以前一样运行,如下图所示——看起来所有包都被下载了,在此阶段,几乎没有任何线索说明后台发生了什么:

然而,现在让我们检查一下磁盘使用情况:

在这里,我们可以看到磁盘使用量已被精确到 34GB——如果我们使用更细粒度的测量,可能会发现使用量要少得多。通过这种方式,Pulp 使我们几乎可以根据需要创建快照,而不会消耗大量磁盘空间,同时保持旧的快照以保证稳定性,直到新的快照通过验证,此时可以删除多余的快照。

在这里值得一提的是,从 Pulp 中删除仓库不一定会释放磁盘空间。原因是后台的包去重操作必须小心,避免删除任何仍然需要的包。在我们的例子中,2019 年 8 月 7 日的快照中超过 99%的包也出现在 8 月 8 日的快照中,因此,如果我们删除其中之一,另一个必须保持完好。

在 Pulp 中,这个过程被称为孤立包恢复,它是寻找那些不再属于任何仓库(大概是因为仓库已被删除)并整理它们的过程。

完成我们当前的例子,假设我们测试了 2019 年 8 月 8 日的快照,并且其中更新的包在测试中导致了问题。由此,我们确定该快照不适合生产环境,因此我们将删除它,等待修复发布时创建一个新的快照:

  1. 首先,我们必须删除该仓库本身:
$ pulp-admin rpm repo delete --repo-id='centos7-08aug19'

这将移除仓库定义和 Apache 服务器上的发布 URL,使其不再可用。

  1. 要清理任何孤立的包,我们可以执行以下命令:
$ pulp-admin orphan remove --all

这个命令是一个通用的清理操作,会从整个 Pulp 服务器中移除所有孤立包,是一个很好的常规维护步骤。然而,该命令也可以接受更细粒度的控制,仅移除特定类型的孤立包(例如,你可以清除所有孤立的 RPM 包,但不清除 DEB 包):

  1. 完成此步骤后,我们将看到由新快照占用的额外磁盘空间已被回收:

在本节中,到目前为止,我们已经手动执行了所有 Pulp 命令和活动——这样做是为了让你充分了解设置 Pulp 和相应仓库所需的步骤。在常规服务中,最佳实践是通过 Ansible 来执行这些步骤,然而,目前并没有原生的 Ansible 模块来覆盖我们在本章中执行的所有任务。

例如,pulp_repo模块(在 Ansible 2.3 版本中引入)能够创建和删除仓库,就像我们在本章中使用pulp-admin rpm repo create所做的那样。然而,它不能执行孤儿清理,因此需要通过shellcommand Ansible 模块来发出此命令。完整的自动化操作将留给你作为练习。

一旦我们的仓库设置完成,最后一步就是在我们的企业 Linux 服务器上使用它们,接下来我们将在本章的下一部分中介绍这一内容。

不过,在此之前,我们将对比管理 Pulp 中的 DEB 包与 RPM 管理之间的一些细节。

在 Pulp 中构建基于 DEB 的仓库

虽然 Pulp 的 RPM 仓库插件和 DEB 仓库插件之间在命令行结构上存在一些细微差异,但整体流程是相同的。如同之前一样,创建有效镜像需要一些先前的仓库结构知识。本书中,我们以 Ubuntu Server 18.04 LTS 为例,配置的默认仓库集如下:

  • bionic:这是 Ubuntu Server 18.04(代号 Bionic Beaver)发布的基础仓库,和 CentOS 7 的操作系统仓库一样,发布操作系统后不会发生变化。

  • bionic-security:这些是针对 bionic 操作系统的安全更新,发布后提供。

  • bionic-updates:这些是针对 bionic 操作系统版本的非安全更新。

还有其他仓库,如backports,除了main组件(我们在这里关注的组件)外,restricteduniversemultiverse组件中也有大量的可用包。深入了解 Ubuntu 仓库结构超出了本书的范围,但可以肯定的是,有大量的文档可供参考。以下链接是了解你可能想要镜像的不同 Ubuntu 仓库的一个好起点:wiki.ubuntu.com/SecurityTeam/FAQ#Repositories_and_Updates

现在,假设我们正在更新一个最小化构建的 Ubuntu Server 18.04 LTS。在这种情况下,我们只关注main组件中的包,但我们确实需要在某个时刻的所有安全修复和更新的快照,就像我们在 CentOS 7 构建中所做的那样:

  1. 首先,在确保我们像之前一样登录到pulp-admin客户端后,我们将在 Pulp 中为main组件和操作系统发布包创建一个仓库:
$ pulp-admin deb repo create --repo-id='bionic-amd64-08aug19' --relative-url='bionic-amd64-08aug19' --feed='http://de.archive.ubuntu.com/ubuntu' --releases=bionic --components=main --architectures='amd64' --serve-http=true

如你所见,前面的命令与我们的 RPM 仓库创建命令非常相似。我们以与之前相同的方式指定repo-idrelative-url,并指定上游的feed URL。不过,这次我们在命令行选项中指定了 Ubuntu 的releasescomponentsarchitectures,而在我们的 CentOS 7 示例中,这些选项是在我们镜像的 URL 中隐含的。除了这些 DEB 特有的配置参数外,我们现在还指定了--serve-http选项。默认情况下,Pulp 仅通过 HTTPS 提供所有仓库内容。然而,由于 Pulp 在处理 DEB 包的签名时存在一些限制(将在本章稍后讨论),我们必须启用通过普通 HTTP 提供仓库内容。

请注意,正如--releases选项的复数命名所示,这里可以指定多个发布版本。尽管这在仓库创建时有效,但在写作时,同步过程存在问题,因此必须为我们希望镜像的每个 Ubuntu 发布版本创建一个单独的 Pulp 仓库。预计这个问题将在未来某个时候修复。

完成此操作后,我们将为securityupdates仓库创建另外两个仓库:

$ pulp-admin deb repo create --repo-id='bionic-security-amd64-08aug19' --relative-url='bionic-security-amd64-08aug19' --feed='http://de.archive.ubuntu.com/ubuntu' --releases=bionic-security --components=main --architectures='amd64' --serve-http=true

$ pulp-admin deb repo create --repo-id='bionic-updates-amd64-08aug19' --relative-url='bionic-updates-amd64-08aug19' --feed='http://de.archive.ubuntu.com/ubuntu' --releases=bionic-updates --components=main --architectures='amd64' --serve-http=true
  1. 完成了我们的仓库创建后,我们可以像之前一样运行同步过程:
$ pulp-admin deb repo sync run --repo-id='bionic-amd64-08aug19'

$ pulp-admin deb repo sync run --repo-id='bionic-security-amd64-08aug19'

$ pulp-admin deb repo sync run --repo-id='bionic-updates-amd64-08aug19'
  1. 最后,我们发布这些仓库:
$ pulp-admin deb repo publish run --repo-id='bionic-amd64-08aug19'

$ pulp-admin deb repo publish run --repo-id='bionic-security-amd64-08aug19'

$ pulp-admin deb repo publish run --repo-id='bionic-updates-amd64-08aug19'

值得注意的是,Ubuntu 仓库通常比 CentOS 仓库大,尤其是updatessecurity仓库。在同步过程中,软件包会暂时下载到/var/cache/pulp,然后再归档到/var/lib/pulp目录。如果/var/cache/pulp位于你的根文件系统上,存在根文件系统填满的重大风险,因此,最好为此创建一个新卷,并将其挂载到/var/cache/pulp,以防磁盘空间不足导致 Pulp 服务器停止运行。

Pulp 的 DEB 插件具有与其 RPM 对应插件相同的软件包去重功能,并且以相同的方式通过 HTTPS(并可选择 HTTP)发布软件包。通过对命令语法进行一些更改,我们可以有效地为大多数企业环境中常见的主要 Linux 发行版创建上游仓库的快照。

完成本节内容后,你已经学会了如何在 Pulp 中为基于 RPM 和 DEB 的内容创建自己的仓库镜像,这些镜像可以视为稳定且不变的,因此为企业中的补丁管理提供了一个优秀的基础。

在本章的下一节中,我们将学习如何将这些仓库部署到两种不同类型的 Linux 服务器上。

使用 Pulp 进行补丁处理

本节开始时值得提到的是,Pulp 支持两种主要的方法来分发从其中创建的仓库中的软件包。第一种是基于推送的分发方式,使用一种叫做 Pulp Consumer 的工具。

我们不会在本章中探讨这一点,原因如下:

  • Pulp Consumer 仅适用于基于 RPM 的仓库和发行版,目前没有适用于 Ubuntu 或 Debian 的等效客户端。这意味着我们的流程不能在整个企业中统一,在理想情况下,它们应该是统一的。

  • 使用 Pulp Consumer 意味着我们将有两种重叠的自动化方式。使用消费者将软件包分发到节点是一个可以通过 Ansible 完成的任务,如果我们使用 Ansible 来执行这个任务,那么我们就有了一种跨平台通用的方法。这支持我们在本书早些时候所建立的企业自动化原则,旨在降低进入门槛、简化使用等。

因此,我们将为管理仓库和更新构建单独的基于 Ansible 的示例,使用前一部分中创建的仓库,名为 在 Pulp 中构建仓库。这些可以与所有其他 Ansible playbook 一起管理,并可以通过像 AWX 这样的平台运行,以确保尽可能在所有任务中使用统一的管理界面。

让我们开始吧,看看如何通过结合使用 Ansible 和 Pulp 来修补基于 RPM 的系统。

基于 RPM 的 Pulp 补丁

在本章的前一部分,我们为 CentOS 7 构建创建了两个仓库——一个用于操作系统版本,另一个用于存放更新。

从这些仓库更新 CentOS 7 构建的过程,从高层次来看,按照以下步骤进行:

  1. /etc/yum.repos.d 中的任何现有仓库定义移开,以确保只加载来自 Pulp 服务器的仓库。

  2. 使用 Ansible 部署适当的配置。

  3. 使用 Ansible 从 Pulp 服务器拉取更新(或任何所需的包),使用新配置进行操作。

在继续创建适当的 playbook 之前,先看看如果我们手动创建仓库定义文件,它在 CentOS 7 机器上的样子。理想情况下,我们希望它看起来像这样:

[centos-os]
name=CentOS-os
baseurl=https://pulp.example.com/pulp/repos/centos76-os
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7
sslverify=0

[centos-updates]
name=CentOS-updates
baseurl=https://pulp.example.com/pulp/repos/centos7-07aug19
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7
sslverify=0

这个配置没有什么特别之处——我们使用的是之前通过 pulp-admin 创建的仓库的 relative-url。我们使用 GPG 校验包完整性,并使用 CentOS 7 的 RPM GPG 密钥,我们知道它已经安装在 CentOS 7 机器上。我们唯一需要调整的地方是关闭 SSL 验证,因为我们的示范 Pulp 服务器使用了自签名证书。当然,如果我们使用企业级证书颁发机构,并且每台机器上都安装了 CA 证书,那么这个问题就不复存在。

由于 Ansible 的强大功能,我们可以在做这件事时更加灵活。当我们知道在某个时刻会更新仓库时(至少baseurl可能会变化),就没有必要创建和部署静态配置文件了。

我们从创建一个名为pulpconfig的角色开始,部署正确的配置——tasks/main.yml应该如下所示:

---
- name: Create a directory to back up any existing REPO configuration
  file:
    path: /etc/yum.repos.d/originalconfig
    state: directory

- name: Move aside any existing REPO configuration
  shell: mv /etc/yum.repos.d/*.repo /etc/yum.repos.d/originalconfig

- name: Copy across and populate Pulp templated config
  template:
    src: templates/centos-pulp.repo.j2
    dest: /etc/yum.repos.d/centos-pulp.repo
    owner: root
    group: wheel

- name: Clean out yum database
  shell: "yum clean all"

随附的templates/centos-pulp.repo.j2模板应该如下所示:

[centos-os]
name=CentOS-os
baseurl=https://pulp.example.com/pulp/repos/{{ centos_os_relurl }}
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7
sslverify=0

[centos-updates]
name=CentOS-updates
baseurl=https://pulp.example.com/pulp/repos/{{ centos_updates_relurl }}
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7
sslverify=0

注意每个baseurl行末尾的变量替换——这些替换使我们能够保持相同的模板(对于大多数用途应该是通用的),但随着时间的推移更改仓库 URL 以适应更新。

接下来,我们将定义一个专门用于更新内核的第二个角色——对于我们的示例来说,这将非常简单,tasks/main.yml将包含以下内容:

---
- name: Update the kernel
  yum:
    name: kernel
    state: latest

最后,我们将在剧本结构的顶层定义site.yml,将所有这些整合在一起。正如我们之前讨论的那样,我们可以在许多地方定义相对 URL 的变量,但为了本示例的方便,我们将它们放在site.yml剧本中:

---
- name: Install Pulp repos and update kernel
  hosts: all
  become: yes
  vars:
    centos_os_relurl: "centos76-os"
    centos_updates_relurl: "centos7-07aug19"

  roles:
    - pulpconfig
    - updatekernel

现在,如果我们以通常的方式运行它,我们将看到类似以下的输出:

到目前为止,一切顺利——前面的剧本中的changed状态告诉我们新配置已成功应用。

眼尖的读者可能已经注意到清理 yum 数据库任务中的警告——Ansible 检测到使用了与模块功能重叠的原始 shell 命令,并建议为了重复性和幂等性,最好使用模块。正如我们之前讨论的那样,然而,由于我们希望确保彻底清除任何早期的yum数据库(因为它们可能会造成问题),所以我采用了暴力清理的方法来清理旧的数据库。

现在,正如你肯定已经发现的,这种方法的一个优点是,如果我们想测试在上一节中创建的08aug19仓库快照,我们只需要修改site.yml中的vars:块,使其看起来像这样:

  vars:
    centos_os_relurl: "centos76-os"
    centos_updates_relurl: "centos7-08aug19"

因此,我们可以通过仅仅改变一个或两个变量值,在各种场景中重用相同的剧本、角色和模板。在像 AWX 这样的环境中,这些变量甚至可以通过图形界面(GUI)被覆盖,从而使整个过程更加简单。

通过这种方式,将 Ansible 与 Pulp 结合起来,为管理和分发(甚至测试)更新提供了一个非常稳定的企业框架。然而,在我们讨论 Ubuntu 上的这个过程之前,先说几句关于回滚的内容。在上一节中,我们假设了一个例子,其中我们的08aug19快照在测试中失败,因此必须删除。就 CentOS 7 服务器而言,回滚并不像简单地安装早期的仓库定义并执行更新那样直接,因为更新将检测到已安装的更新软件包,并不会执行任何操作。

当然,Pulp 仓库提供了一个稳定的基础,可以回滚到之前的版本——然而,回滚通常是一个相当手动的过程,因为你必须在yum数据库中找到要回滚的事务 ID,验证将要执行的操作,然后回滚到该版本。当然,如果你有可靠的方法来获取事务 ID,这一过程是可以自动化的。

以下截图展示了一个简单的例子,如何识别我们刚刚自动化的内核更新的事务 ID,并建立执行的更改的详细信息:

然后,如果我们愿意,可以使用以下截图中的命令来回滚事务:

使用这个简单的过程,以及这里提供的 playbook 作为指南,应该能够为任何基于 RPM 的 Linux 发行版建立一个稳固、稳定、自动化的更新平台。

在下一节中,我们将探讨如何使用相同的方法来执行一系列任务,除了像 Ubuntu 这样的基于 DEB 的系统。

基于 DEB 的补丁管理与 Pulp

从高层次来看,从我们的 Pulp 服务器上管理 Ubuntu 的更新过程与管理 CentOS 的 RPM 更新过程完全相同(只是我们没有选择使用 Pulp Consumer 的选项,必须使用 Ansible 进行更新过程)。

然而,使用 Pulp 与 Ubuntu 的 APT 仓库系统时,有几个限制:

  • 在撰写本文时,存在一个问题,即 Pulp 同步过程无法从上游 Ubuntu 仓库镜像签名密钥。这意味着即使上游仓库中包含Release.gpg,它也不会在 Pulp 服务器上镜像。希望未来这个问题能够解决,但在本章中,我们将通过为软件包添加隐式信任来解决这个问题。

  • Ubuntu 上的 HTTPS 支持默认配置为不接受来自不可验证(即自签名)证书的更新。虽然我们可以像在 CentOS 上一样关闭 SSL 验证,但 Ubuntu 的 APT 包管理器随后会寻找一个InRelease文件(该文件应包含上述的 GPG 密钥)。正如我们在前面讨论的,Pulp DEB 插件不支持镜像仓库的签名,因此目前唯一的解决方法是使用未加密的 HTTP 流量。希望在未来的版本中,这两个问题能得到解决——然而,截至本文编写时,似乎没有公开的修复或解决方法。

了解了这两个限制后,我们可以为之前创建的仓库集定义我们的 APT 源文件。根据上一节的示例,我们的 /etc/apt/sources.list 文件可能如下所示:

deb [trusted=yes] http://pulp.example.com/pulp/deb/bionic-amd64-08aug19 bionic main
deb [trusted=yes] http://pulp.example.com/pulp/deb/bionic-security-amd64-08aug19 bionic-security main
deb [trusted=yes] http://pulp.example.com/pulp/deb/bionic-updates-amd64-08aug19 bionic-updates main

[trusted=yes] 字符串告诉 APT 包管理器忽略缺少包签名的情况。文件结构本身非常简单,因此与我们的 CentOS 示例一样,我们可以创建一个模板文件,以便使用变量填充相对 URL:

  1. 首先,我们将创建一个名为 pulpconfig 的角色,并创建以下 templates/sources.list.j2 模板:
deb [trusted=yes] http://pulp.example.com/pulp/deb/{{ ubuntu_os_relurl }} bionic main
deb [trusted=yes] http://pulp.example.com/pulp/deb/{{ ubuntu_security_relurl }} bionic-security main
deb [trusted=yes] http://pulp.example.com/pulp/deb/{{ ubuntu_updates_relurl }} bionic-updates main
  1. 然后,我们将创建一些任务,使用该角色来安装此模板,并移除任何旧的 APT 配置:
---
- name: Create a directory to back up any existing REPO configuration
  file:
    path: /etc/apt/originalconfig
    state: directory

- name: Move existing config into backup directory
  shell: mv /etc/apt/sources.list /etc/apt/originalconfig

- name: Copy across and populate Pulp templated config
  template:
    src: templates/sources.list.j2
    dest: /etc/apt/sources.list
    owner: root
    group: root

- name: Clean out dpkg database
  shell: "apt-get clean"
  1. 最后,我们将定义一个角色来更新内核,但这次使用 APT:
---
- name: Update the kernel
  apt:
    name: linux-generic
    state: latest
  1. 我们的 site.yml Ubuntu 系统的 playbook 现在如下所示——除了变量的不同,几乎与 CentOS 7 的版本完全相同,再次突显了使用 Ansible 作为自动化平台的价值:
---
- name: Install Pulp repos and update kernel
  hosts: all
  become: yes
  vars:
    ubuntu_os_relurl: "bionic-amd64-08aug19"
    ubuntu_security_relurl: "bionic-security-amd64-08aug19"
    ubuntu_updates_relurl: "bionic-updates-amd64-08aug19"

  roles:
    - pulpconfig
    - updatekernel
  1. 现在,整理好这些内容并运行后,我们应该看到类似以下屏幕截图的输出:

摒除当前 Pulp Debian 支持中的安全限制,这为在企业基础设施中以可重复和适合自动化的方式管理 Ubuntu 更新提供了一个简洁且节省空间的解决方案。与我们之前基于 CentOS 的示例一样,通过简单地更改传递给角色的变量定义,测试新快照中的包将变得非常容易。

与 CentOS 一样,如果新的一组软件包不适合用于生产环境,Ansible 使得恢复之前的仓库配置变得简单。然而,在 Ubuntu(以及其他基于 Debian 的发行版)上回滚软件包比我们在上一节中看到的过程要手动得多。幸运的是,/var/log/dpkg.log/var/log/apt/history.log* 中保存了大量关于软件包事务的历史记录,可以用来确定哪些软件包被安装和/或升级,以及何时进行的。然后,可以使用 apt-get 命令,通过 apt-get install <packagename>=<version> 语法安装特定版本的软件包。网络上有很多优雅的脚本化解决方案,因此这部分将留给你作为练习,自己决定哪种解决方案最适合你的需求和环境。

总结

在企业环境中管理软件包仓库可能会面临许多挑战,尤其是在高效存储、节省互联网带宽和确保构建一致性方面。幸运的是,Pulp 软件包为大多数常见的 Linux 发行版提供了一个优雅的解决方案,并且非常适合在企业环境中的有效管理。

在本章中,你学习了如何安装 Pulp 以开始修补企业 Linux 环境。接着,你通过实践示例学习了如何在 Pulp 中为基于 RPM 和 DEB 的 Linux 发行版构建仓库,并获得了使用 Ansible 部署适当的 Pulp 配置和更新软件包的实用知识。

在下一章中,我们将探索 Katello 软件工具如何在企业环境管理中与 Pulp 相辅相成。

问题

  1. 为什么要使用 Pulp 创建一个仓库,而不是仅仅创建一个可以手动下载的文件镜像?

  2. 在企业环境中构建和测试 Linux 补丁仓库时会遇到哪些问题?

  3. Pulp 需要哪些组件才能运行?

  4. 指定成功安装 Pulp 所需的文件系统要求。

  5. 如何从你之前创建的 Pulp 仓库修补一个基于 RPM 的系统?

  6. 为什么使用 Ansible 从 Pulp 仓库部署补丁,而不是使用 Pulp Consumer?

  7. 删除 Pulp 仓库是否释放磁盘空间?如果没有,如何执行此操作?

延伸阅读

  • 欲了解更多关于 Pulp 项目及如何使用该工具的详细信息,请参考官方文档(pulpproject.org/)。

第九章:使用 Katello 进行补丁管理

在第八章,使用 Pulp 进行企业级仓库管理中,我们探讨了 Pulp 软件包及其如何用于企业环境中的自动化、可重复、可控的补丁管理。本章将基于此,介绍一个名为 Katello 的产品,它是 Pulp 的补充,不仅适用于补丁管理,还能进行完整的基础设施管理。

Katello 是一个以 GUI 驱动的工具,为企业基础设施管理提供先进的解决方案,在许多方面可以看作是许多人熟悉的经典产品 Spacewalk 的继任者。我们将探讨为什么选择 Katello 来进行此项工作,然后通过动手示例展示如何构建 Katello 服务器并进行补丁管理。

本章将具体讨论以下主题:

  • Katello 介绍

  • 安装 Katello 服务器

  • 使用 Katello 进行补丁管理

技术要求

完成本章实践练习的最低要求是一台 CentOS 7 服务器,至少分配 80 GB 硬盘空间,2 个 CPU 核心(虚拟或物理),以及 8 GB 内存。虽然本章仅会查看 Katello 功能的子集,但需要注意,特别是 Foreman(在 Katello 下安装)能够充当 DHCP 服务器、DNS 服务器和 PXE 启动主机,因此如果配置不当,部署到生产网络上可能会造成问题。

因此,建议所有练习在适合测试的隔离网络中进行。给出的 Ansible 代码已经在 Ansible 2.8 中开发并测试过。要进行来自 Katello 的补丁测试,您需要一台 CentOS 7 虚拟机。

本书中讨论的所有示例代码都可以从 GitHub 获取:github.com/PacktPublishing/Hands-On-Enterprise-Automation-on-Linux

Katello 介绍

Katello 并非一个孤立的单一产品,而是多个开源基础设施管理产品的集合,形成一个统一的基础设施管理解决方案。Pulp 专注于有效、可控地存储软件包(以及其他重要的基础设施管理内容),而 Katello 则将以下功能整合在一起:

  • Foreman:这是一个开源产品,旨在处理物理和虚拟服务器的配置和配置管理。Foreman 包括一个丰富的基于 Web 的 GUI,一个 RESTful API,以及一个名为 Hammer 的 CLI 工具,提供了多种多样的管理方式。它还与多个自动化工具进行集成,最初是 Puppet,最近也支持 Ansible。

  • Katello:Katello 实际上是 Foreman 的一个插件,提供了额外的功能,如内容的丰富版本控制(比单独使用 Pulp 更强大)和订阅管理。

  • Candlepin:提供软件订阅管理,特别是与Red Hat 订阅管理RHSM)模型的集成。虽然在 Pulp 中镜像 Red Hat 的仓库是可行的,但这一过程繁琐,而且由于无法查看你所管理的系统数量或它们与 Red Hat 订阅的关系,存在违反许可条款的风险。

  • Pulp:这就是我们在上一章中探讨的 Pulp 软件,现在已整合为一个功能齐全的项目。

  • Capsule:一种代理服务,用于在地理位置分散的基础设施中分发内容并控制更新,同时保持单一管理控制台。

因此,使用 Katello 相较于仅使用 Pulp 有几个优势,即使你仅仅将其用于补丁管理(如我们将在本章中“使用 Katello 进行补丁管理”部分探讨的),其丰富的 Web GUI、CLI 和 API 使得它能够与企业系统集成。除此之外,Katello(更具体地说,支持它的 Foreman)还提供了许多其他好处,例如能够动态地通过 PXE 启动服务器,并控制容器和虚拟化系统,甚至可以作为你网络的 DNS 和 DHCP 服务器。实际上,可以说 Katello/Foreman 的组合被设计为坐落在你网络的核心,尽管它只会执行你要求的功能,因此已有 DNS 和 DHCP 基础设施的用户无需担心。

值得一提的是,Katello 还与 Puppet 自动化工具紧密集成。最初的项目由 Red Hat 赞助,并且在他们收购 Ansible 之前,Red Hat 与 Puppet 有战略联盟,这使得 Puppet 在 Katello 项目中占据了重要地位(该项目作为 Red Hat Satellite 6 商业化)。鉴于 Ansible 的收购,尽管 Puppet 集成仍然保留在 Katello 中,但通过 Ansible Tower/AWX 与 Ansible 的集成支持发展迅速,用户可以完全根据自己的需求选择使用的自动化工具。

在此阶段,值得一提的是久负盛名的Spacewalk软件工具。Spacewalk 是 Red Hat Satellite 5 的上游开源版本,仍在积极开发和维护中。在高级功能方面,这两个系统有很大的重叠;然而,Katello/Satellite 6 是对平台的从头重写,因此两者之间没有明确的升级路径。鉴于 Red Hat 在 Spacewalk 项目中的贡献可能会减少,尤其是在它们停用 Satellite 5 产品后,本书将重点关注 Katello。

事实上,可以公平地说,Katello 配得上一本自己的书,因为它的功能集非常丰富。我们在这一章的目标仅仅是提高对 Katello 平台的认识,并展示它如何在企业环境中进行补丁管理。许多额外的功能,如服务器的 PXE 启动,需要理解我们在本书中已经涵盖的概念,因此希望如果你决定将 Katello 或 Satellite 6 作为管理基础设施的平台,你能够在本书提供的基础上继续构建,并探索更多的资源,进一步深入。

让我们通过下一个部分实际看看如何安装一个简单的独立 Katello 服务器,以便我们能够更全面地探索这一点。

安装 Katello 服务器

这是一本实践书籍,因此不再多说,让我们开始动手设置自己的 Katello 服务器。除了之前讨论过的 Katello 优势外,还有一个优势是产品的打包。当我们设置 Pulp 服务器时,有很多独立的组件需要我们做出决策(例如,RabbitMQ 与 Qpid),然后还需要执行额外的设置(例如,为 MongoDB 配置 SSL 传输)。Katello 拥有比 Pulp 更多的活动组件(如果将 Pulp 视为 Katello 平台的一个组件的话),因此手动安装它将是一个庞大而复杂的任务。

幸运的是,Katello 提供了一个安装系统,只需几个命令就能让你开始运行,我们将在本章的下一部分进行探讨。

准备安装 Katello

Katello 与 Pulp 一样,目前仅支持安装在企业 Linux 7 版本上——因此在这里,我们将使用 CentOS 7 的最新稳定版本。随着产品的不断发展,Katello 的要求时有变化,因此在继续之前,自己查看安装文档总是值得的。撰写本文时,版本 3.12 是最新的稳定版本,安装文档可以在此找到:theforeman.org/plugins/katello/3.12/installation/index.html。现在,让我们按照这些步骤进行操作:

  1. 和以前一样,我们最大的关注点是确保我们分配了足够的磁盘空间,正如独立的 Pulp 安装一样,我们必须确保在/var/lib/pulp/var/lib/mongodb中为所有可能希望镜像的 Linux 发行版分配足够的磁盘空间。同样,与 Pulp 一样,它们应该与根卷分开,以确保如果一个卷填满,整个服务器不会崩溃。

  2. 文件系统设置好之后,我们的第一步是安装所需的仓库,以便下载所有安装所需的软件包——这需要设置几个外部仓库,这些仓库提供 CentOS 7 默认不包含的软件包。以下命令将为 Katello、Foreman、Puppet 6 和 EPEL 仓库设置仓库,随后才能安装 Foreman 版本包树:

$ yum -y localinstall https://fedorapeople.org/groups/katello/releases/yum/3.12/katello/el7/x86_64/katello-repos-latest.rpm
$ yum -y localinstall https://yum.theforeman.org/releases/1.22/el7/x86_64/foreman-release.rpm
$ yum -y localinstall https://yum.puppet.com/puppet6-release-el-7.noarch.rpm
$ yum -y localinstall https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm
$ yum -y install foreman-release-scl
  1. 从这里开始,建议将基础系统完全更新:
$ yum -y update
  1. 实际安装之前的最后一步是安装 Katello 包及其依赖项:
$ yum -y install katello
  1. 从这里起,所有安装任务都使用foreman-installer命令执行——可以指定大量选项,且对于大多数选项,如果你需要更改决定,可以重新运行安装程序并使用不同的标志,它会在不丢失数据的情况下执行更改。要查看所有可能的选项,运行以下命令:
$ foreman-installer --scenario katello --help
  1. 为了构建我们的演示服务器,默认设置大多数情况下已经足够——然而,如果你探索选项,你会发现许多选项在企业环境中需要特别指定。例如,可以在安装时指定 SSL 证书(而不是依赖于默认生成的自签名证书),可以设置底层传输的默认密钥等。强烈建议你在生产环境中安装时,查看前面命令的输出。现在,我们将发出以下安装命令以启动安装:
$ foreman-installer --scenario katello --foreman-initial-admin-password=password --foreman-initial-location='London' --foreman-initial-organization='HandsOn'

这可能是安装 Katello 服务器最简单的情况,并且完全适用于本书中的示例。然而,在生产环境中,我强烈建议你探索更高级的安装功能,以确保服务器能够满足你的需求,特别是在安全性和可用性方面。这部分留给你自己探索。

请注意,在此场景中,安装程序会检查几个前提条件,包括 Katello 服务器名称的正向和反向 DNS 查找是否能正确解析,以及机器是否有 8 GB 可用内存。如果未满足这些前提条件,安装程序将拒绝继续。

  1. 只要满足所有前提条件,Katello 安装应该可以顺利完成,完成后,你应该看到一个类似于以下截图的界面,详细列出了登录信息以及其他相关信息,比如如何为另一个网络设置代理服务器(如果需要的话):

  1. 安装程序唯一未完成的任务是设置 CentOS 7 机器上的本地防火墙。幸运的是,Katello 提供了一个包含防火墙服务定义的服务,该服务覆盖了所有可能需要的服务——它的名称来自商业版 Red Hat Satellite 6 产品,可以通过以 root 身份运行以下命令来启用:
$ firewall-cmd --permanent --zone=public --add-service=RH-Satellite-6
$ firewall-cmd --reload
  1. 完成这些步骤后,就可以加载 Katello 的 Web 界面并使用显示的详细信息登录:

从技术角度讲,Katello 是一个位于 Foreman 之上的模块,提供一些重要功能,我们将在本章稍后查看——例如,它为 Pulp 仓库管理系统提供了一个 Web 界面,该系统也在后台安装。因此,Foreman 的品牌标识非常突出,您会频繁看到该名称。一旦登录,您应该会看到默认的仪表板页面,我们可以开始配置一些用于补丁管理的仓库,这将在下一节中进行。

使用 Katello 进行补丁管理

由于 Katello 构建在我们已经探索过的技术之上,例如 Pulp,它带有与我们已知的 DEB 包相关的相同限制。例如,尽管可以在 Katello 中轻松构建 DEB 包的仓库,甚至可以导入适当的 GPG 公钥,但最终发布的仓库不包含 InReleaseRelease.gpg 文件,因此必须由所有使用这些仓库的主机隐式信任。类似地,尽管为基于 RPM 的主机提供了完整的订阅管理框架,包括 subscription-manager 工具和 Pulp Consumer 代理,但对于 DEB 主机而言,仍然没有类似的工具,因此这些主机必须手动配置。

虽然完全可以将基于 RPM 的主机配置为使用内置技术,但基于 DEB 的主机必须像使用 Pulp 一样通过 Ansible 配置,并且考虑到企业中各环境之间的通用性,建议以相同的方式配置所有服务器,而不是为两种不同的主机类型使用两种不同的解决方案。

Katello 相对于 Pulp 的优势之一,除了 Web 用户界面外,还在于生命周期环境的概念。这个功能承认大多数企业会为不同的用途设置独立的技术环境。例如,您的企业可能会有一个用于开发新软件和测试前沿包的 Development 环境,然后是一个用于测试发布的 Testing 环境,最后是一个包含最稳定构建并运行客户和客户服务的 Production 环境。

现在让我们通过一些实际的例子来探索如何在 Katello 中建立用于补丁管理的仓库。

使用 Katello 对基于 RPM 的系统进行补丁管理

让我们考虑使用 Katello 为我们的 CentOS 7 系统在多个生命周期环境中构建仓库。由于 Katello 支持基于密钥的 RPM 验证,我们的第一步是安装 RPM 的 GPG 公钥。该公钥可以从 CentOS 项目免费下载,并且可以在大多数 CentOS 7 系统中找到,路径为 /etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7

  1. 要将此公钥添加到 Katello,请从菜单栏导航到 Content | Content Credentials。然后,点击 Create Content Credential:

  1. 给密钥取个合适的名字,并选择上传密钥文件或将其内容复制粘贴到屏幕上的文本框中。完成后点击 Save:

  1. 接下来,我们将创建一个 product——在 Katello 中,product 是仓库的逻辑分组,这对于创建可管理的可扩展配置非常有用。对于我们的示例,我们只会镜像 CentOS 7 OS 仓库,但当你开始镜像更新和任何其他相关仓库时,将它们放在同一个产品下是有意义的。从菜单栏导航到 Content | Products,点击 Create Product 按钮:

  1. 现在,定义高级产品定义—对于一个简单的 CentOS 7 仓库镜像,我们只需创建 Name 和 Label 并关联之前上传的 GPG 密钥。各种 SSL 选项适用于具有双向 SSL 验证的上游仓库。还需注意,所有 products 都可以根据 Sync Plan (本质上是一个计划)进行同步—然而,在本示例中,我们将仅执行手动同步。完成后,屏幕应如下图所示:

  1. 完成高级 product 定义后,我们现在可以通过点击 New Repository 按钮在其下创建我们的 CentOS 7 仓库:

  1. 在提供的屏幕上填写仓库详情。将 Type 字段设置为 yum,并在相应字段中输入上游仓库的 URL(这与使用命令行中的 Pulp 时的 --feed 参数相同):

  1. 向下滚动同一屏幕,确保选中“通过 HTTP 发布”并关联之前上传的 GPG 密钥,如下图所示:

  1. 在我们的示例中,我们将立即通过在仓库表格中对其打勾并点击 Sync Now 按钮来启动此仓库的同步,如下图所示:

  1. 同步会立即在后台开始—你可以随时通过导航到 Content | Sync Status 页面来查看其进度(并启动进一步的手动同步):

  1. 在同步过程完成的同时,我们来创建一些生命周期环境。

请注意,尽管你可以在不同的产品中有独立的仓库,但生命周期环境是全局的,并适用于所有内容。在企业环境中,这很有意义,因为无论使用何种底层技术,你通常都会有开发测试生产环境。

从菜单栏中,导航至“内容 | 生命周期环境路径”,然后点击“创建环境路径”按钮:

  1. 按照屏幕上的指示创建一个名为开发的初始环境。你应该看到类似下图所示的界面:

  1. 现在,我们将添加测试生产环境,使得我们的示例企业能够在这三个环境之间有一个逻辑的流转。点击“添加新环境”按钮,然后依次添加每个环境,确保它们设置了正确的“前置环境”,以保持正确的顺序。下图展示了从测试环境创建生产环境的一个示例:

  1. 最终配置应该类似下图所示的示例:

一旦我们的同步过程完成并且环境创建成功,我们就可以进入 RPM 仓库设置的最后部分——内容视图。在 Katello 中,内容视图是用户定义的多种内容形式的组合,这些内容可以被摄取、版本控制并分发到指定环境中。通过一个实际的例子来解释最为清晰。

当我们单独使用 Pulp 时,我们创建了一个名为centos7-07aug19的仓库。当我们想测试一个稍后发布的更新时,我们创建了一个名为centos7-08aug19的第二个仓库。虽然这样做是可行的,并且我们演示了 Pulp 如何去重包并节省磁盘空间,同时整洁地发布看似独立的仓库,但很快你就会看到这种内容管理机制如何在企业规模下变得笨拙,尤其是当有多个环境和管理几个月(或几年)快照时。

这时,内容视图派上用场了。尽管我们已经在这里镜像了 CentOS 7 的操作系统仓库,假设我们镜像的是更新仓库。通过使用内容视图,我们不需要为测试更新而创建新的产品或仓库。相反,整体工作流程大致如下:

  1. 创建一个产品和相应的仓库并执行同步(例如,2019 年 8 月 7 日)。

  2. 创建一个包含前一步骤中创建的仓库的内容视图。

  3. 在 2019 年 8 月 7 日发布内容视图——这会在该日期为此仓库创建一个版本编号的快照(例如,版本1.0)。

  4. 将内容视图推广到Development环境。进行测试,验证后,将其推广到测试环境。重复此循环,直到达到Production环境。所有这些操作都可以异步进行,不影响后续步骤。

  5. 在 8 月 8 日,再次同步在第 1 步中创建的仓库(如果你通过Sync Plan进行自动同步,这将在 8 月 8 日早晨自动完成)。

  6. 在 2019 年 8 月 8 日发布内容视图,并进行同步。这将为该日期创建一个+1版本的仓库(例如,版本 2.0)。

  7. 到此为止,你已经拥有了 8 月 7 日和 8 月 8 日的 CentOS 7 通道的快照。但所有服务器仍然会接收来自 8 月 7 日通道的更新。

  8. Development环境提升至版本 2.0。此时,Development环境中的机器会接收(无需额外配置)8 月 8 日的仓库快照。

  9. TestingProduction环境没有被提升至此版本,因此仍然接收来自 8 月 7 日快照的包。

通过这种方式,Katello 使得在不同环境中管理多个版本(快照)的仓库变得容易,而且每台主机上的仓库配置始终保持不变,从而避免了像我们在 Pulp 中那样通过 Ansible 推送新的仓库信息的需求。

让我们通过一个示例来逐步了解前述过程,演示我们在 Katello 环境中的操作:

  1. 首先,为前述过程创建一个新的内容视图。

  2. 导航至 Content | Content Views 并点击 Create New View 按钮:

  1. 对于我们的目的,新的内容视图只需要一个名称和一个标签,如下图所示:

  1. 单击保存按钮后,导航到新内容视图中的 Yum Content 标签,并确保选择了 Add 子标签。勾选你想要添加到内容视图的仓库(在我们的简单演示中,只有一个 CentOS 7 仓库,所以选择它),然后点击 Add Repositories 按钮:

  1. 现在,返回到 Versions 标签页并点击 Publish New Version 按钮。这将创建我们之前讨论过的假设 8 月 7 日版本。请注意,PublishPromote操作会消耗大量磁盘 I/O,尤其是在慢速机械硬盘阵列上,速度会非常慢。虽然 Katello 和 Red Hat Satellite 6 对 I/O 性能没有发布要求,但它们在闪存存储上表现最佳,或者如果没有这种存储,最好使用快速的机械存储,并且该存储不与其他设备共享。下图展示了点击 Publish New Version 按钮的过程,针对 CentOS7-CV 内容视图:

  1. Publish操作是异步进行的,你可以在此屏幕上看到它完成,尽管如果你离开,它仍然会继续完成。你会看到它自动编号为Version 1.0——这个编号在写作时是自动生成的,你无法选择自己的版本编号。不过,你可以为每个已发布版本添加备注,这对跟踪每个版本的内容以及它们为何被创建非常有用。强烈推荐这样做。下图展示了我们在Version 1.0环境中的提升过程:

  1. 一旦Publish操作完成,Promote 按钮(如上图所示为灰显状态)将变为可用。你会注意到,这个版本会自动发布到Library环境——任何内容视图的最新版本始终会自动提升到这个环境。

  2. 为了模拟我们之前讨论过的 8 月 8 日快照,让我们对这个内容视图执行第二次发布操作。这将生成一个Version 2.0环境,然后可以通过点击 Promote 按钮并选择所需的环境,将其提升到Development环境。下图展示了我们两个版本的情况,其中Version 1.0仅对Production环境可用,而Version 2.0则对Development环境(以及内置的Library环境)可用。请注意,由于我们没有将Testing环境提升到任何版本,因此Testing环境中的机器没有任何包可用。你必须将其提升到所有需要包的环境——下图展示了我们发布的两个版本以及与之关联的环境:

  1. 在下图中,展示了提升过程供参考——这就是如何将Production环境提升到Version 2.0

这里唯一剩下的问题是配置客户端以从 Katello 服务器接收软件包。在这里,我们将执行一个简单的手动集成,因为这种方法适用于 DEB 和 RPM 基础的软件包,因此支持在整个企业中的通用方法。从 Katello 使用subscription-manager工具和 Katello 代理分发 RPM 包的过程已经有详细文档说明,留作练习。

Katello 官方文档关于激活密钥的部分是一个很好的起点:theforeman.org/plugins/katello/3.12/user_guide/activation_keys/index.html

为了利用我们在本示例中发布的内容,Development环境中的机器将有一个包含如下内容的仓库文件:

[centos-os]
name=CentOS-os
baseurl=http://katello.example.com/pulp/repos/HandsOn/Development/CentOS7-CV/custom/CentOS7/CentOS7-os/
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7

您的基础 URL 肯定会有所不同——至少您的 Katello 主机名会不同。在 Katello 中发布和推广的 RPM 基础仓库通常位于以下路径:

http://KATELLOHOSTNAME/pulp/repos/ORGNAME/LIFECYCLENAME/CONTENTVIEWNAME/custom/PRODUCT/REPO

这里,我们有以下内容:

  • KATELLOHOSTNAME:您的 Katello 服务器的主机名(如果使用了 Capsule/Proxy,则为它们的主机名)

  • ORGNAME:您的Content View所在的 Katello 组织名称——我们在安装过程中将其定义为HandsOn

  • LIFECYCLENAMELifecycle Environment的名称,例如Development

  • CONTENTVIEWNAME:您为您的Content View指定的名称

  • PRODUCT:您为您的Product指定的名称

  • REPO:您在Product内为仓库指定的名称

这使得 URLs 完全可预测,并且可以像我们在前一章中讨论 Pulp 时一样,使用 Ansible 轻松地部署到目标机器。请注意,通过 HTTPS 访问 Katello 的仓库需要安装 SSL 证书以进行信任验证,这超出了本章的范围——因此,我们将仅使用普通 HTTP。

由于生命周期环境名称始终保持不变,无论我们是同步、发布还是推广环境,前面所示的仓库 URL 始终保持不变,因此即使发布了新的包仓库快照,我们也无需执行客户端配置工作。这相较于 Pulp 有显著的优势,在 Pulp 中每次创建新版本时,我们都需要通过 Ansible 推送新的配置。

一旦如前所示构建了仓库配置,您就可以按照正常方式修补您的系统。可以按如下方式进行:

  • 手动管理,使用类似yum update的命令在每台机器上执行

  • 集中管理,使用 Ansible 剧本

  • 如果katello-agent包已安装在目标机器上,可以通过 Katello 用户界面进行操作。

鉴于可用工具的多样性,本章不会深入讨论,但将其作为练习留给你。经验表明,使用 Ansible 进行集中部署是最稳健的方法,但你可以自由尝试并找到最适合你的方法。

这就是我们简要介绍基于 RPM 的修补过程与 Katello 配合使用的结束,尽管希望它已经向你展示了足够的信息,让你初步了解它在企业中可能的价值。在下一部分中,我们将讨论如何使用 Katello 修补基于 DEB 的系统。

使用 Katello 修补基于 DEB 的系统

通过 Katello 修补基于 DEB 的系统(如 Ubuntu)与基于 RPM 的过程大致相似,只是 GUI 中有一些变化,以及本章前面讨论的关于包签名的限制,详见 使用 Katello 修补 部分。现在,让我们简要了解一下 Ubuntu Server 18.04 的示例:

  1. 首先,为我们的 Ubuntu 包仓库创建一个新的产品:

这里需要强调的是,导入 Ubuntu 签名公钥不会对已发布的仓库产生影响,因此可以根据需要指定或忽略。结果仓库将没有签名的 Release 文件,因此必须被视为隐式信任。

  1. 保存该产品后,在其中创建一个新的仓库来包含包——创建包镜像需要与使用 Pulp 命令行时相同的参数,如下图所示:

如前所述,同步新创建的仓库,并确保同步成功完成后,再继续进行内容视图的创建。

  1. 一旦完成,创建一个单独的内容视图用于我们的 Ubuntu 内容——以下截图展示了内容视图创建的过程:

  1. 这一次,导航到 Apt Repositories 选项卡,并选择适当的 Ubuntu 仓库——同样,在我们简单的示例中,我们只有一个仓库,以下截图显示了我们的唯一 Ubuntu 18.04 base 仓库被添加到 Ubuntu1804-CV 内容视图的过程:

  1. 从这里开始,我们的新内容视图将像基于 RPM 的视图一样被发布和推广。结果仓库再次可以通过可预测的 URL 访问,此时的 URL 格式如下:
http://KATELLOHOSTNAME/pulp/deb/ORGNAME/LIFECYCLENAME/CONTENTVIEWNAME/custom/PRODUCT/REPO

如图所示,这与基于 RPM 的示例几乎完全相同,唯一不同的是初始路径。为匹配我们在此示例中刚刚创建的内容视图,/etc/apt/sources.list 可能需要添加如下条目:

deb [trusted=yes] http://katello.example.com/pulp/deb/HandsOn/Development/Ubuntu1804-CV/custom/Ubuntu_18_04_Server/Ubuntu_18_04_base/ bionic main

与以往一样,无论何时同步、发布或推广此内容视图,此 URL 都保持不变,因此只需在目标系统上部署一次,以确保它们可以从 Katello 服务器接收软件包。同样,您可以通过在最终系统上手动执行 apt updateapt upgrade 命令或通过 Ansible 在中心化方式进行补丁操作。

请注意,在撰写时,Debian/Ubuntu 系统上没有 katello-agent 软件包。

在本章中,我们只是初步接触了 Katello 的功能,但仅凭这个例子就展示了它作为企业补丁管理工具的有效性。强烈建议您进一步探索,以确定它是否满足您更广泛的基础设施需求。

必须强调的是,在本章中,我们实际上只是初步介绍了 Katello 的功能——但希望到目前为止我们所做的工作足以让您对是否继续使用这个极其强大和多功能的平台作为您的 Linux 架构的一部分做出明智的决定。

摘要

实际上,Katello 是几个极其强大的开源基础设施管理工具的融合,包括我们已经探索过的 Pulp。在基础设施环境中进行补丁管理方面,它表现出色,提供了许多优势,远超单独安装 Pulp,并且可以从单一的界面处理大多数构建和维护任务——比我们现在所能涵盖的还要多!

在本章中,您了解到 Katello 项目的实际内容以及它所包含的组件。然后,您学习了如何为补丁目的执行独立安装的 Katello,并学习了如何构建适用于补丁的 RPM 和 DEB Linux 发行版的仓库,以及集成这两个操作系统与 Katello 内容视图的基础知识。

在下一章中,我们将探讨如何在企业中有效地使用 Ansible 进行用户管理。

问题

  1. 为什么您要选择 Katello 而不是像 Pulp 这样的产品?

  2. 在 Katello 术语中,产品是什么?

  3. Katello 中的内容视图是什么?

  4. Foreman(Katello 的基础)是否可以协助裸金属服务器进行 PXE 引导?

  5. 您如何在 Katello 中使用生命周期环境?

  6. 在内容视图上执行 PublishPromote 操作之间有什么区别?

  7. 什么时候您会想要在先前发布的内容视图上执行 Promote 操作?

进一步阅读

要更深入了解 Katello,请参阅官方的 Red Hat Satellite 6 文档,因为这是 Katello 的商业版本,所有文档通常都是为此平台编写的——但功能和菜单结构几乎完全相同(access.redhat.com/documentation/en-us/red_hat_satellite/)。

第十章:在 Linux 上管理用户

没有用户访问方法的 Linux 服务器是不完整的。无论是管理员还是终端用户,使用本地凭据还是集中式凭据,Linux 服务器都需要一种机制来让用户(甚至像 Ansible 这样的工具!)访问它们。

用户管理与所有良好的服务器配置和维护活动一样,是一项持续性的工作。为了确保系统的安全性和完整性,凭据需要定期更换。员工的进出意味着访问详情需要相应更新。实际上,在忙碌的组织中,访问管理可能本身就是一份全职工作!

在本章中,我们将通过实际示例探讨如何通过 Ansible 自动化用户和访问管理,并确保与我们的标准操作环境SOE)模型一致。

本章将涵盖以下主题:

  • 执行用户账户管理任务

  • 使用轻量级目录访问协议LDAP)集中管理用户账户

  • 强制执行和审计配置

技术要求

本章包含基于以下技术的示例:

  • Ubuntu Server 18.04 LTS

  • CentOS 7.6

  • Ansible 2.8

为了完成这些示例,你需要访问两台服务器或虚拟机,分别运行上述列出的操作系统之一,并且需要安装 Ansible。请注意,本章中的示例可能会具有破坏性(例如,它们会添加或删除用户账户,并更改服务器配置),如果按照原样运行,建议仅在隔离的测试环境中执行。

一旦你确认自己有一个安全的环境来进行操作,我们就开始使用 Ansible 安装新软件包吧。

本章讨论的所有示例代码都可以从 GitHub 上获取,网址为:github.com/PacktPublishing/Hands-On-Enterprise-Automation-on-Linux/tree/master/chapter10

执行用户账户管理任务

从最基本的层面来说,你环境中的每一台 Linux 服务器都需要为用户提供某种程度的访问权限。在一个可能有数百台甚至数千台服务器的企业环境中,像 LDAP 或 Active Directory 这样的集中式用户管理系统将是理想的解决方案。举个例子,当用户离职或更改密码时,他们只需在一个地方进行操作,系统就会自动应用到所有服务器上。我们将在下一节使用 LDAP 集中管理用户账户中探讨企业 Linux 管理和自动化的这一方面。

目前,让我们专注于本地账户管理——也就是说,在每台需要访问的 Linux 服务器上创建的账户。即使存在像 LDAP 这样的集中式解决方案,本地账户依然是必要的——至少它们作为紧急访问解决方案,在目录服务故障时仍然有其存在的价值。

请注意,正如本书中的所有 Ansible 示例一样,它们可以在 1 台、100 台甚至 1000 台服务器上同样运行。事实上,使用 Ansible 减少了对集中式用户管理系统的依赖,因为用户账户的更改可以轻松地推送到整个服务器集群。然而,也有充分的理由不完全依赖于此——例如,在 Ansible playbook 运行时,如果一台服务器因维护而停机,那么它将无法接收到正在进行的账户更改。在最坏的情况下,这台服务器重新投入使用时,可能会带来安全风险。

从下一节开始,我们将探讨 Ansible 如何帮助你进行本地账户管理。

使用 Ansible 添加和修改用户

无论你是在第一次配置新建的服务器,还是在新员工加入公司时进行更改,向服务器添加用户账户都是常见的任务。幸运的是,Ansible 有一个名为user的模块,专门用于执行用户账户管理任务,我们将继续使用这个模块。

在之前的示例中,我们非常小心地强调了 Ubuntu 和 CentOS 等平台之间的差异,用户账户管理在这里也需要一些考虑。

以以下 Shell 命令为例(我们稍后将在 Ansible 中进行自动化):

$ useradd -c "John Doe" -s /bin/bash johndoe 

该命令可以在 CentOS 7 或 Ubuntu Server 18.04 上运行,并且会产生相同的结果,即:

  • 用户账户johndoe将被分配下一个空闲的用户标识号UID)。

  • 账户注释将设置为John Doe

  • Shell 将被设置为/bin/bash

事实上,你几乎可以在任何 Linux 系统上运行这个命令,它都会正常工作。然而,当你考虑到组时,差异就开始显现了,尤其是内置组。例如,如果你希望该账户能够使用 sudo 进行 root 访问(即,johndoe是系统管理员),你会想要将该账户加入 CentOS 7 中的wheel组。然而,在 Ubuntu Server 中,并没有wheel组,尝试将用户加入这个组会导致错误。相反,在 Ubuntu 中,这个用户会加入sudo组。

正是这种细微的差别,可能会在自动化用户账户管理时,使你在不同的 Linux 发行版之间产生问题——然而,只要你注意到这些细节,就能轻松创建 Ansible playbook 或角色,轻松管理你的 Linux 用户。

让我们基于这个示例,改为在 Ansible 角色中创建johndoe用户,以便在所有 Linux 服务器上进行访问控制。以下是roles/addusers/tasks/main.yml中的代码,它实现了与前述命令外壳相同的功能:

---
- name: Add required users to Linux servers
  user:
    name: johndoe
    comment: John Doe
    shell: /bin/bash

如果我们以常规方式运行这个角色,我们可以看到用户账户在第一次运行时被创建,如果第二次运行 playbook 则不会采取任何行动。以下截图演示了前述角色运行两次的情况——changedok 状态分别表示添加用户账户时的操作,以及因为账户已存在而没有执行任何操作时的状态:

到目前为止,一切顺利——然而,这个示例有些简单——我们的用户没有设置密码、没有加入任何组,也没有授权的 SSH 密钥。我们之前演示过,可以多次运行包含用户模块的 Ansible 角色,并且只有在需要时才会进行更改,我们可以利用这一点来我们的优势。现在,让我们扩展我们的示例角色,加入这些内容。

在进入下一个示例之前,我们将演示如何使用 Ansible 生成一个密码哈希。在这里,我们将选择单词secure123。Ansible 的user模块能够设置和修改用户账户密码,但由于非常好的原因,它不允许您指定明文密码。相反,您必须创建一个密码哈希,并将其发送到被配置的机器中。在第六章《使用 PXE 引导的自定义构建》中,我们介绍了如何通过少量 Python 代码来完成此操作,您可以在这里重新使用该方法。但是,您也可以利用 Ansible 丰富的过滤器,直接从字符串生成密码哈希。请在 Shell 中运行以下命令:

$ ansible localhost -i localhost, -m debug -a "msg={{ 'secure123' | password_hash('sha512') }}"

运行此命令会生成一个密码哈希值,您可以将其复制并粘贴到您的角色中,如下图所示:

这本身非常有用——但是,让我们记住一件事:没有任何密码哈希是完全安全的。记住,曾经 MD5 哈希被认为是安全的,但现在不再安全。理想情况下,您不应将哈希值以明文形式存储,应该在每个系统上重新生成它,因为它包含一个唯一的盐值。幸运的是,我们可以直接在角色中使用password_hash过滤器来实现这一点。

在以下示例中,我们演示了如何将密码字符串存储在一个变量中,然后如何使用password_hash过滤器为远程系统生成哈希值。在实际使用中,您可以将明文变量文件替换为 Ansible vault 文件,这样在任何时候都不会存储未加密的原始密码或哈希值。

  1. 首先,让我们创建roles/addusers/vars/main.yml文件,并将 John Doe 的密码存储在一个变量中,如下所示:
---
johndoepw: secure123
  1. 接下来,我们在roles/addusers/files/目录下为此用户创建一对 SSH 密钥,方法是在该目录下运行以下命令:
$ ssh-keygen -b 2048 -t rsa -f ./johndoe_id_rsa -q -N ''

当然,在企业环境中,用户很可能会生成自己的密钥对,并将公钥提供给管理员以便分发到他们将使用的系统中——然而,在我们的例子中,使用新生成的密钥对更方便进行演示。

  1. 最后,假设johndoe将负责管理 Ubuntu 系统,因此,他应该属于sudo组。我们最终的角色现在应该是这样的:
---
- name: Add required users to Linux servers
  user:
    name: johndoe
    comment: John Doe
    shell: /bin/bash
    groups: sudo
    append: yes
    password: "{{ johndoepw | password_hash('sha512') }}"

- name: Add user's SSH public key
  authorized_key:
    user: johndoe
    state: present
    key: "{{ lookup('file', 'files/johndoe_id_rsa.pub') }}"
  1. 运行代码后,我们得到了changed的结果,正如我们预期的那样,以下截图显示了成功添加用户及其相应的 SSH 公钥:

请注意,我们已经成功修改了johndoe账户,因为我们之前在本节中创建了它——然而,我们也可以在账户创建之前运行最近的角色,最终结果将是一样的。这就是 Ansible 的魅力——你不需要为修改和添加编写不同的代码。user模块还有许多其他修改功能,它应该能够满足你大部分的需求。

简单回顾一下我们之前创建的vars/main.yml文件,为了简化本例,我们将其保留为明文文件。然而,我们可以非常轻松地加密现有文件,使用以下命令:

$ ansible-vault encrypt main.yml

以下截图展示了这个加密过程的实际操作:

数据现在已经加密存储!我们仍然可以在不解密的情况下运行剧本——只需在ansible-playbook命令中添加--ask-vault-pass参数,并在提示时输入你选择的 vault 密码。

在结束这一节之前,值得一提的是,我们还可以利用loops一次性创建多个账户。以下示例创建了两个新用户,它们具有不同的组成员身份,并且账户的用户名和备注不同。扩展此示例以处理初始密码和/或 SSH 密钥作为练习留给你,但你应该已经有足够的信息来扩展这个例子。代码如下所示:

---
- name: Add required users to Linux servers
  user:
    name: "{{ item.name }}"
    comment: "{{ item.comment }}"
    shell: /bin/bash
    groups: "{{ item.groups }}"
    append: yes
    state: present
  loop:
    - { name: 'johndoe', comment: 'John Doe', groups: 'sudo'}
    - { name: 'janedoe', comment: 'Jane Doe', groups: 'docker'}

注意到我们在本章早些时候创建了johndoe账户,我们可以看到,如果我们运行这个角色,只有janedoe账户会被创建,因为它之前并不存在——以下截图准确地展示了这一点。janedoe显示为changed状态,告知我们做出了更改——在这种情况下,账户被创建了。johndoe账户显示为ok状态,告诉我们没有执行任何操作,以下截图也展示了这一点:

通过这种方式,用户账户可以在大量 Linux 服务器上进行创建和管理。如前面的截图所示,Ansible 的工作方式是只对所需的更改进行修改,而不影响现有账户。虽然添加账户相对简单,但我们还必须考虑到员工会不时离开企业,因此在这种情况下,账户清理也是必需的。

在下一节中,我们将探讨 Ansible 如何帮助删除用户账户并清理相关内容。

使用 Ansible 删除用户

尽管我们已经展示了如何通过 Ansible 添加和修改用户账户,但我们必须将删除操作视为一个独立的案例。原因很简单——Ansible 假设,如果我们将 user 模块与 loop 一起使用,添加 johndoejanedoe,它会在账户不存在时添加它们;否则,它将修改现有账户。当然,如果它们与角色或 playbook 描述的状态相匹配,那么它将什么都不做。

然而,Ansible 在运行之前并不假设任何状态。因此,如果我们从之前描述的循环中删除 johndoe 并重新运行 playbook,那么该账户将不会被删除。由于这个原因,我们必须单独处理账户删除的操作。

以下代码将删除该用户账户:

---
- name: Add required users to Linux servers
  user:
    name: johndoe
    state: absent

现在,如果我们运行这个命令,输出应类似于以下屏幕截图:

运行此角色相当于在 shell 中使用 userdel 命令——用户账户将被删除,所有组成员关系也会被移除。然而,home 目录将被保留。这通常是最安全的做法,因为用户可能在 home 目录中存储了重要的代码或其他数据,通常情况下,在删除该目录之前,由某人检查确认目录是否安全是最好的做法。如果你确定要删除该目录(这是最佳实践,出于安全原因以及释放磁盘空间的考虑),那么可以将以下代码添加到我们刚刚创建的角色中:

- name: Clean up user home directory
  file:
    path: /home/johndoe
    state: absent

这将递归删除指定的 path,请小心使用!

通过这些实际示例和一些文档中的附加细节,你应该能够很好地利用 Ansible 自动化本地账户任务。在下一节中,我们将探讨如何使用 LDAP 来集中管理用户账户。

使用 LDAP 集中管理用户账户

尽管 Ansible 在管理整个服务器群体的用户账户方面表现良好,但企业中最好的做法是使用集中式目录系统。集中式目录可以执行一些 Ansible 无法做到的任务——例如,强制执行密码安全标准,如密码长度和字符类型、密码过期以及在尝试多次错误密码时锁定账户。因此,强烈建议在企业中使用这样的系统。

事实上,许多企业已经有了这样的系统,其中两种常见的系统是 FreeIPA 和 Microsoft Active Directory (AD) 。在接下来的章节中,我们将探讨这两个系统与 Linux 服务器的集成。

Microsoft AD

由于这是一本关于 Linux 自动化的书,关于 Microsoft AD 及其设置和配置的深入讨论超出了本书的范围。可以简要地说,在 Linux 环境下,AD 最适合用于集中式用户账户管理,尽管它的功能远不止如此。大多数需要 AD 服务器的组织已经设置了 AD,因此,我们的关注点不在于这一方面,而是如何使我们的 Linux 服务器与 AD 进行身份验证。

在大多数现代 Linux 发行版中,realmd 工具用于将目标 Linux 服务器加入到 AD 中。接下来,我们考虑一个将 CentOS 7 服务器加入 AD 的假设例子——然而,每个组织、他们的 AD 设置、组织单位等都会有所不同,因此,这里并没有一种通用的解决方案。

正如你现在无疑已经意识到的,在 Ubuntu 上执行此过程将非常相似,唯一不同的是你将使用 apt 模块来代替 yum,并且包的名称可能有所不同。一旦安装了 realmd 及其所需的包,整个过程就是相同的。

然而,希望下面提供的代码能为你提供一个良好的基础,帮助你开发自己的 Ansible 角色来加入 AD。

  1. 在开始加入目录的过程之前,确保 Linux 服务器使用的是包含适当 服务 (SRV) 记录的正确 DNS 服务器至关重要。这些 DNS 服务器通常是 AD 服务器本身,但这同样会因组织而异。

  2. 必须安装 realmd 工具以及若干支持包。让我们创建一个名为 realmd 的角色,使用我们熟悉的 roles 目录结构。roles/realmd/tasks/main.yml 应以以下代码开始,以安装所需的包:

---  
- name: Install realmd packages
  yum:
    name: "{{ item }}"
    state: present
  loop:
    - realmd
    - oddjob
    - oddjob-mkhomedir
    - sssd
    - samba-common
    - samba-common-tools
    - adcli
    - krb5-workstation
    - openldap-clients
    - policycoreutils-python

其中一些包提供了支持功能——例如,openldap-clients 不是直接必须的,但在调试目录问题时非常有用。

  1. 一旦安装了我们的前提软件包,接下来的任务是加入 Active Directory。在这里,我们假设roles/realmd/vars/main.yml文件中设置了realm_join_passwordrealm_join_userrealm_domain等变量。由于此文件可能包含具有足够权限的密码以加入 AD 域,建议使用ansible-vault加密此变量文件。运行以下代码:
- name: Join the domain
    shell: echo '{{ realm_join_password }}' | realm join --user={{ realm_join_user }} {{ realm_domain }}
    register: command_result
    ignore_errors: True
    notify:
      - Restart sssd

使用shell模块执行realm join时需要特别注意,因为运行此任务两次不会得到 Ansible 的正常清洁行为。实际上,当服务器已经是域成员时,再执行第二次realm join会导致错误。因此,我们设置ignore_errors: True,并register命令的结果,以便稍后评估它是否成功运行。我们还会通知一个稍后定义的处理程序,重新启动sssd服务。前述的vars文件应该类似如下:

---
realm_join_password: securepassword
realm_join_user: administrator@example.com
realm_domain: example.com

确保将变量值替换为适合你自己环境的值。

  1. 我们紧接着这项任务进行检查,看看realm join是否成功。如果成功,我们应该得到0的返回码,或者一个错误,告知我们服务器已经已加入此域。如果没有得到这些预期的结果,我们将失败整个剧本,以确保问题能够得到解决,具体如下:
- name: Fail the play when the realm join fails
    fail: 
      msg="Realm join failed with this error: {{ command_result.stderr }}"
    when: "'Already joined to this domain' not in command_result.stderr and command_result.rc != 0"
  1. 最后,我们在roles/realmd/handlers/main.yml中创建处理程序,重新启动sssd,如以下所示:
---
- name: Restart sssd
  service:
    name: sssd
    state: restarted
    enabled: yes

这些步骤足以完成将 Linux 服务器添加到 AD 域的基本操作。虽然示例以 CentOS 7 为例,但对于像 Ubuntu 这样的操作系统,过程应该大致相似,只要你考虑到不同的包管理器和包名即可。

当然,前述过程有大量可以改进的地方,大多数改进都可以通过realm命令来执行。遗憾的是,在写这篇文档时,Ansible 并没有realm模块,因此所有realm命令都必须通过shell模块执行——不过,这依然可以通过 Ansible 实现 Linux 服务器加入 AD 域的自动化部署。

你可以考虑对前述过程进行的可能改进(所有这些都可以通过扩展我们之前建议的示例剧本轻松自动化),具体如下:

  • 指定当加入完成后,Linux 服务器应该进入的组织单位OU)。如果未指定,它将进入默认的Computers OU。你可以通过在realm join命令中指定类似--computer-ou=OU=Linux,OU=Servers,OU=example,DC=example,DC=com的内容来更改此设置。确保 OU 已先行创建,并根据你的环境调整前面的参数。

  • 默认情况下,所有有效的域用户帐户将能够登录 Linux 服务器。这可能并不理想,如果不希望如此,您需要首先拒绝所有访问,使用命令realm deny --all。然后,如果您希望允许LinuxAdmins AD 组中的所有用户,则需发出以下命令:realm permit -g LinuxAdmins

  • 在您的 AD 中不太可能有名为wheelsudo的组,因此 AD 用户可能会发现自己无法执行特权命令。可以通过将适当的用户或组添加到/etc/sudoers或更好地说,添加到/etc/sudoers.d下 Ansible 可以管理的一个独立文件中来纠正这一问题。例如,创建以下内容的/etc/sudoers.d/LinuxAdmins文件将允许LinuxAdmins AD 组的所有成员在不重新输入密码的情况下执行 sudo 命令:

%LinuxAdmins ALL=(ALL) NOPASSWD: ALL

所有这些任务留给您自己去完成,尽管预期本章中提供的信息足以帮助您构建适合您的 AD 基础设施的 Playbook。

在下一节中,我们将介绍 FreeIPA 目录服务在 Linux 上的使用,以及如何通过 Ansible 将其集成到您的环境中。

FreeIPA

FreeIPA 是一个免费的开源目录服务,安装和管理都很简单。它运行在 Linux 上,主要在 CentOS 或Red Hat Enterprise LinuxRHEL)上运行,尽管客户端支持也 readily available on Ubuntu 和其他 Linux 平台。甚至可以与 Windows AD 集成,尽管这并非必须。

如果您正在构建纯粹的 Linux 环境,那么看看 FreeIPA 是有意义的,而不是采用专有解决方案如 Microsoft AD。

FreeIPA 和 Microsoft AD 绝不是市场上目录服务的唯一两个选项,现在还有许多基于云的替代方案,包括 JumpCloud、AWS Directory Service 等。始终根据自己的独立判断做出关于最佳选择的决定,特别是在涉及基于云的目录服务时,领域正在快速发展。

与前文关于 Microsoft AD 的部分类似,设计和部署 FreeIPA 基础设施超出了本书的范围。目录服务是网络核心服务——想象一下如果您只建立了一个目录服务器,然后不得不将其关闭进行维护。即使是简单的重新启动也会导致用户在服务停止期间无法登录到所有连接到该服务器的计算机上。因此,设计目录服务基础设施以考虑冗余和灾难恢复是非常重要的。此外,在您的目录基础设施出现故障的情况下,正如本章前面讨论的,在执行用户帐户管理任务一节中,拥有良好安全的本地帐户也非常重要。

一旦为您的 FreeIPA 安装设计了适当的冗余基础设施,FreeIPA 团队在 GitHub 上提供了一系列 playbooks 和角色,用于安装您的服务器和客户端,您可以在此进一步探索:github.com/freeipa/ansible-freeipa

本书将 FreeIPA 基础设施的安装任务留给您——然而,让我们来看一下如何使用自由提供的 FreeIPA 角色,在您的基础设施上安装客户端。毕竟,这是开源软件的一个关键优势——共享知识、信息和代码。

  1. 首先,我们将 ansible-freeipa 仓库克隆到本地机器,并进入该目录以便使用,如下所示:
$ cd ~
$ git clone https://github.com/freeipa/ansible-freeipa
$ cd ansible-freeipa
  1. 接下来,创建指向我们刚刚克隆到本地 Ansible 环境中的 rolesmodules 的符号链接,如下所示:
$ ln -s ~/ansible-freeipa/roles/ ~/.ansible/
$ mkdir ~/.ansible/plugins
$ ln -s ~/ansible-freeipa/plugins/modules ~/.ansible/plugins/
$ ln -s ~/ansible-freeipa/plugins/module_utils/ ~/.ansible/plugins/
  1. 完成上述操作后,我们必须创建一个简单的清单文件,包含适当的变量,定义 FreeIPA 域和域名,以及 admin 用户的密码(这是将新服务器加入 IPA 域所必需的)。以下示例已展示,但请确保根据您的需求进行自定义:
[ipaclients]
centos-testhost

[ipaclients:vars]
ipaadmin_password=password
ipaserver_domain=example.com
ipaserver_realm=EXAMPLE.COM
  1. 设置了适当的变量并编译了清单文件后,我们可以运行提供的 playbooks,并使用从 GitHub 下载的代码。以下展示了运行的 FreeIPA 客户端安装 playbook 示例:

上述输出是截断的,但展示了 FreeIPA 客户端安装的过程。与本书中的示例一贯简洁不同,这个过程也可以同样运行在 100 或甚至 1,000 台服务器上。

由于这些 playbooks 和角色是由官方 FreeIPA 项目提供的,它们是安装服务器和客户端的可靠来源,尽管强烈建议测试和审查任何下载的代码,但这些应该足以构建基于 FreeIPA 的基础设施。

在下一节中,我们将探讨 Ansible 如何帮助强制执行和审计用户账户和配置。

强制执行和审计配置

在用户账户管理方面,安全性至关重要。正如我们在《通过 LDAP 中央化用户账户管理》一节中讨论的那样,Ansible 并不是专门为执行或审计设计的——然而,它可以大大帮助我们。让我们考虑一些 Ansible 可以帮助缓解的用户管理相关的安全风险,从 sudoers 文件开始。

使用 Ansible 管理 sudoers

/etc/sudoers 文件是大多数 Linux 系统中最敏感的文件之一,因为它定义了哪些用户账户可以作为超级用户执行命令。无需多说,这个文件被篡改或未经授权的修改可能会对不仅仅是相关的 Linux 服务器,甚至整个网络带来巨大的安全风险。

幸运的是,Ansible 模板可以帮助我们有效地管理这个文件。像其他现代 Linux 配置一样,sudoers配置被分割成几个文件,以便更易于管理。这些文件通常如下所示:

  • /etc/sudoers:这是主文件,引用所有可能被考虑的其他文件。

  • /etc/sudoers.d/*:这些文件通常通过在/etc/sudoers文件中的引用被包含。

正如我们在标题为使用 Ansible 进行配置管理的章节中讨论的那样,某人可能会编辑/etc/sudoers并指示它包含一个完全不同的路径,除/etc/sudoers.d/*外,意味着我们必须通过模板部署这个文件。这可以确保我们控制哪些文件提供sudo配置。

我们不会重复关于模板及其在 Ansible 中的部署的讨论,因为在第七章中讨论的使用 Ansible 进行配置管理技术在这里同样适用。然而,我们会加一个重要的警告。如果你通过部署一个(例如)语法错误的文件来破坏sudo配置,你将有可能把所有用户从特权访问中锁定出去。这意味着解决问题的唯一方法是使用 root 账户登录到服务器,而如果此账户被禁用(像 Ubuntu 默认禁用一样,并且在许多环境中建议禁用),那么恢复路径将变得相当棘手。

就像许多情况一样,预防胜于治疗,而我们之前使用的template模块有一个窍门可以帮助我们解决这个问题。当你在 Linux 系统上使用visudo编辑sudoers文件时,所创建的文件在写入磁盘之前会自动进行检查。如果存在错误,你会收到警告,并有机会进行修复。Ansible 可以通过在template模块中添加validate参数来利用这个工具。因此,通过 Ansible 部署sudoers文件的新版本的一个非常简单的角色可能看起来像这样:

---
- name: Copy a new sudoers file on if visudo validation is passed
  template:
    src: templates/sudoers.j2
    dest: /etc/sudoers
    validate: /usr/sbin/visudo -cf %s

在前面的示例中,template模块通过validate参数中的命令传递dest指定的文件名——这就是%s的意义。如果验证通过,新的文件将被写入。如果验证失败,则新的文件不会被写入,旧的文件将保留。此外,当验证失败时,任务的状态为failed,因此该任务会结束并提醒用户修复问题。

这并不是validate参数可以完成的唯一任务——它可以用于检查任何模板操作的结果,只要你能定义一个能够对模板操作进行适当检查的 Shell 命令。这可能像使用grep检查文件中的一行,或者检查某个服务是否重新启动那样简单。

在下一节中,我们将看看 Ansible 如何帮助在大量服务器上强制执行和审计用户账户。

使用 Ansible 审计用户账户

假设你的企业有 1,000 台 Linux 服务器,所有的服务器都使用目录服务进行身份验证,正如我们之前讨论过的那样。现在,假设有一个不当用户,想要绕过这个权限管理,并在单个服务器上创建了一个名为john的本地账户。这种情况可能发生在临时授予权限用于变更请求,然后再撤销权限时——不道德的个人可以轻松创建自己的访问方式,从而绕过目录服务提供的安全性。

那么,如何发现这种情况呢?尽管 Ansible 本身并不是审计工具,但它有一个好处,就是可以在 1,000 台服务器上同时运行一个命令(或一组命令),并将结果返回给你进行处理。

因为所有的服务器构建都应该达到一定的标准(参见第一章,在 Linux 上构建标准操作环境),所以你应该知道每个 Linux 服务器上应该有哪些账户。可能会有一些差异——例如,如果你安装了 PostgreSQL 数据库服务器,这通常会创建一个名为postgres的本地用户账户。不过,这些情况是可以理解的,并且可以很快且轻松地过滤掉。

我们甚至不需要为 Ansible 写一个完整的剧本来帮助我们——一旦你有了包含 Linux 服务器的清单文件,你就可以运行所谓的临时命令。这只是一个单行命令,可以用来运行任何单个 Ansible 模块并带上一组参数——就像一个只有一个任务的剧本。

因此,要获取我所有服务器上的所有用户账户列表,我可以运行以下命令:

$ ansible -i hosts -m shell -a 'cat /etc/passwd' all

就是这么简单——Ansible 会忠实地连接到由-i参数指定的所有服务器清单,并将/etc/passwd文件的内容显示在屏幕上。你可以将输出通过管道保存到文件中,进行进一步处理和分析,而不必登录到每一台机器上。尽管 Ansible 本身并没有进行任何分析,但它为审计提供了一个非常强大且易于使用的数据收集工具,而且正如 Ansible 的魅力所在,远程机器上不需要安装任何代理。

以下截图展示了 Ansible 如何从我们的一个测试系统中获取本地用户账户,使用简单的grep命令过滤掉两个常见的账户。当然,你可以根据需要扩展这个示例,以改善数据处理,从而使任务变得更加轻松:

通过这种方式,你可以充分利用 Ansible 从大量系统中收集有用的信息,供进一步处理——由于结果直接返回到终端,便于将其管道传输到文件中,并用你喜欢的工具(例如 AWK)处理,检查是否有任何系统违反企业政策。虽然此示例是通过本地用户帐户列表执行的,但同样可以在远程系统的任何文本文件上有效执行。

正如你所看到的,这是一个非常简单的示例,但它是一个基础构件,你可以在此基础上构建其他剧本。以下是一些供你进一步探索的想法:

  • 修改我们之前运行的临时命令,将其作为剧本运行。

  • 在 AWX 中定期调度运行上述剧本。

  • 修改剧本以检查特定的关键用户帐户。

然而,你对用户的审计能力并不仅限于此——尽管集中式日志记录应该(并且可能会)是你基础设施的一部分,你也可以使用 Ansible 查询日志文件。通过之前展示的临时命令结构,你可以在一组 Ubuntu 服务器上运行以下命令:

$ ansible -i hosts -m shell -a 'grep "authentication failure | cat" /var/log/auth.log' all

在 CentOS 上,这些日志消息将出现在 /var/log/secure 中,因此你需要根据这些系统修改路径。

grep 命令在找不到你指定的字符串时返回代码 1,Ansible 进而将其解读为失败,并报告任务失败。因此,我们将 grep 的输出传递给 cat 命令,cat 始终返回零,因此任务不会失败,即使我们搜索的字符串没有找到。

正如你现在可能已经意识到的,这些命令作为剧本运行会更好,且可以根据操作系统进行一些检测,并使用适当的路径——然而,本节的目标不是给你提供一个详尽的解决方案集合,而是激发你根据这些示例自行编写代码,帮助你使用 Ansible 审计你的基础设施。

由于 Ansible 可以执行多种命令,并且在你的基础设施中具有无代理访问权限,这意味着它可以成为你工具箱中的有效解决方案,用于配置 Linux 服务器、维护配置的完整性,甚至对其进行审计。

总结

用户帐户和访问管理是任何企业 Linux 环境中不可或缺的一部分,Ansible 可以作为配置和推广这一管理方法的关键组件,覆盖大量服务器。事实上,针对 FreeIPA,已经有免费提供的 Ansible 角色和剧本,不仅可以设置 Linux 客户端,甚至可以配置你的服务器架构。因此,自动化管理 Linux 基础设施中的所有关键组件是完全可以实现的。

在本章中,你学习了如何有效地通过 Ansible 管理大量 Linux 服务器上的用户帐户。接着,你学习了如何通过 Ansible 将登录集成到常见的目录服务器中,如 FreeIPA 和 Microsoft AD,最后,你学习了如何使用 Ansible 强制实施配置并审计其状态。

在下一章中,我们将探讨 Ansible 在数据库管理中的应用。

问题

  1. 即使使用了目录服务,本地用户帐户的好处是什么?

  2. 在 Ansible 中,哪个模块用于创建和操作用户帐户?

  3. 如何仅使用 Ansible 生成加密的密码哈希?

  4. 哪个软件包用于将 Linux 服务器与 AD 集成?

  5. 如何使用 Ansible 对一组服务器的配置进行审计?

  6. 从模板部署 sudoers 文件时,验证该文件的目的是什么?

  7. 目录服务提供了哪些 Ansible 无法提供的额外好处,尽管 Ansible 可以在所有服务器上部署用户帐户?

  8. 你会如何在 FreeIPA 和 AD 之间做出选择?

进一步阅读

第十一章:数据库管理

没有数据的应用栈是不完整的,而数据通常存储在数据库中。对于 Linux 平台而言,数据库的选择众多,数据库管理和管理的整个主题通常需要专门的书籍来讨论——实际上,通常每种数据库技术都有一本书。尽管这个话题非常广泛,但了解一些 Ansible 知识对于数据库管理帮助巨大。

的确,无论是安装新的数据库服务器,还是对现有数据库服务器进行维护或管理,我们在第一章中讨论的原始原则,在 Linux 上构建标准操作环境,仍然适用。的确,你为什么要费力地去标准化你的 Linux 环境并确保所有变更都自动化处理,却还要坚持手动管理数据库层呢?这很可能导致缺乏标准化、审计能力,甚至追溯能力(例如,谁在什么时候做了哪些更改?)。Ansible 可以通过模块执行数据库操作和配置。它可能无法替代市场上更高级的数据库管理工具,但如果这些工具可以通过命令行操作,Ansible 可以代替你执行这些操作,并且可以自己处理许多任务。最终,你希望所有的更改都能被记录(或自我记录)并可审计,而 Ansible(结合 Ansible Tower 或 AWX)可以帮助你实现这一目标。本章将探讨一些帮助你实现这一目标的方法。

本章将涵盖以下主题:

  • 使用 Ansible 安装数据库

  • 导入和导出数据

  • 执行常规维护

技术要求

本章包括基于以下技术的示例:

  • Ubuntu Server 18.04 LTS

  • CentOS 7.6

  • Ansible 2.8

要运行这些示例,你需要访问两台服务器或虚拟机,每台运行上述提到的操作系统,并且需要安装 Ansible。请注意,本章中的示例可能具有破坏性(例如,它们会添加和删除数据库及表,并更改数据库配置),如果按原样运行,它们仅应在隔离的测试环境中运行。确保你有一个安全的环境来执行这些操作后,我们就可以开始讨论如何使用 Ansible 安装新的软件包了。本章讨论的所有示例代码可以从 GitHub 获取,网址如下:github.com/PacktPublishing/Hands-On-Enterprise-Automation-on-Linux/tree/master/chapter11

使用 Ansible 安装数据库

在第七章,使用 Ansible 进行配置管理,我们探讨了一些软件包安装的示例,并在其中使用了 MariaDB 服务器。当然,MariaDB 只是 Linux 上众多可用数据库中的一个,这里无法详细覆盖所有内容。尽管如此,Ansible 可以帮助您在 Linux 上安装几乎任何数据库服务器,在本章中,我们将通过一系列示例为您提供安装自己数据库服务器的工具和技术。

让我们在下一节开始,基于我们安装 MariaDB 的示例继续进行。

使用 Ansible 安装 MariaDB 服务器

虽然在本书的早些章节中,我们安装了随 CentOS 7 一起提供的本机mariadb-server软件包,但大多数需要 MariaDB 服务器的企业会选择直接从 MariaDB 标准化到特定版本。这通常比随特定 Linux 发布版本提供的版本更新,并因此提供了更新的功能和有时的性能改进。此外,标准化到直接从 MariaDB 获取的版本可以确保平台的一致性,这是我们在本书中始终坚持的原则。

让我们举一个简单的例子——假设您的基础架构正在运行Red Hat Enterprise LinuxRHEL)7。这个版本预装了 MariaDB 5.5.64。现在,假设您希望将基础架构标准化到新发布的 RHEL 8 上——如果您依赖 Red Hat 提供的软件包,那么这将立即升级您的 MariaDB 版本至 10.3.11,这意味着不仅仅是 Linux 基础架构的升级,还包括数据库的升级。

相反,最好是预先在 MariaDB 本身的发布上进行标准化。在撰写本文时,MariaDB 的最新稳定版本是 10.4——但假设您已经标准化到已知并在您的环境中成功测试过的 10.3 版本。

安装过程非常简单,并且在 MariaDB 网站上有很好的文档说明——请参阅mariadb.com/kb/en/library/yum/,获取针对 CentOS 和 Red Hat 的具体示例。然而,这些详细说明了手动安装过程,而我们希望使用 Ansible 进行自动化。现在让我们将其构建为一个真实且有效的 Ansible 示例。

在本示例中,我们将按照 MariaDB 的说明进行操作,包括从其存储库下载软件包。尽管出于简单起见,我们将继续按照这个示例进行操作,但您也可以将 MariaDB 软件包存储库镜像到 Pulp 或 Katello 中,详细内容请参见第八章,使用 Pulp 进行企业存储库管理 和 第九章,使用 Katello 进行补丁管理

  1. 首先,我们可以从安装文档中看到,我们需要创建一个 .repo 文件,以告诉 yum 从哪里下载软件包。我们可以使用模板来提供这个文件,以便 MariaDB 版本可以通过变量定义,从而在将来迁移到 10.4 版本(或其他任何未来版本)时能够进行更改。

因此,我们的模板文件,定义在 roles/installmariadb/templates/mariadb.repo.j2 中,将如下所示:

[mariadb]
name = MariaDB
baseurl = http://yum.mariadb.org/{{ mariadb_version }}/centos7-amd64
gpgkey=https://yum.mariadb.org/RPM-GPG-KEY-MariaDB
gpgcheck=1
  1. 一旦我们创建了这个文件,我们还应该为这个变量创建一个默认值,以防在执行角色时未指定该变量而导致任何问题或错误——这个默认值将在 roles/installmariadb/defaults/main.yml 中定义。通常情况下,这个变量会在给定服务器或服务器组的库存文件中提供,或者通过 Ansible 支持的其他方法提供,但 defaults 文件提供了一种兜底方式,以防它被忽略。运行以下代码:
---
mariadb_version: "10.3"
  1. 定义好之后,我们可以开始在 roles/installmariadb/tasks/main.yml 中构建任务,如下所示:
---
- name: Populate MariaDB yum template on target host
  template:
    src: templates/mariadb.repo.j2
    dest: /etc/yum.repos.d/mariadb.repo
    owner: root
    group: root
    mode: '0644'

这样可以确保正确的仓库文件被写入服务器,如果它被错误修改,也能恢复到原来的期望状态。

在 CentOS 或 RHEL 上,你还可以使用 yum_repository Ansible 模块来执行此任务——但是,这样做的缺点是无法修改现有的仓库定义,因此,如果将来我们需要更改仓库版本,最好使用模板。

  1. 接下来,我们应该清理 yum 缓存——这在升级 MariaDB 到新版本时尤其重要,因为软件包名称相同,缓存的信息可能会导致安装出现问题。目前,清理 yum 缓存是通过使用 shell 模块运行 yum clean all 命令来实现的。然而,由于这是一个 shell 命令,它会始终运行,这可能被认为是低效的——尤其是当这个命令运行时,任何未来的包操作都需要再次更新 yum 缓存,即使我们没有修改 MariaDB 仓库定义。因此,我们希望仅在 template 模块任务结果为 changed 状态时才运行它。

为此,我们必须先在 template 任务中添加这一行,以存储任务的结果:

  register: mariadbtemplate
  1. 现在,当我们定义我们的 shell 命令时,我们可以告诉 Ansible 仅在template任务结果为changed状态时才运行它,如下所示:
- name: Clean out yum cache only if template was changed
  shell: "yum clean all"
  when: mariadbtemplate.changed
  1. 清理完缓存后,我们可以安装所需的 MariaDB 软件包——以下代码块中使用的列表来自前面本节引用的 MariaDB 文档,但你应该根据自己的具体需求进行调整:
- name: Install MariaDB packages
  yum:
    name:
      - MariaDB-server
      - galera
      - MariaDB-client
      - MariaDB-shared
      - MariaDB-backup
      - MariaDB-common
    state: latest

使用 state: latest 确保我们始终从由 template 任务创建的仓库文件中安装最新的包。因此,这个角色可以用于初始安装和升级到最新版本。然而,如果你不希望这种行为,可以将这个语句改为 state: present——这仅仅确保列出的包已安装在我们的目标主机上。如果已经安装,它不会将它们更新到最新版本——它只是返回一个 ok 状态并继续执行下一个任务,即使有更新可用。

  1. 安装完包后,我们必须确保服务器服务在启动时自动启动。我们可能还想立即启动它,以便进行任何初步配置工作。因此,我们将在 installmariadb 角色的最后添加一个任务,任务内容如下所示:
- name: Ensure mariadb-server service starts on boot and is started now
  service:
    name: mariadb
    state: started
    enabled: yes
  1. 此外,我们知道 CentOS 7 默认启用了防火墙——因此,我们必须更改防火墙规则,以确保我们新安装的 MariaDB 服务器可以被访问。执行此任务的操作看起来如下所示:
- name: Open firewall port for MariaDB server
  firewalld:
    service: mysql
    permanent: yes
    state: enabled
    immediate: yes
  1. 现在让我们运行这个角色并看看它的实际效果——输出应该如下所示:

输出已被截断以节省空间,但清晰地显示了安装过程中的进展。请注意,该警告可以安全忽略——Ansible 引擎检测到了我们的 yum clean all 命令,并友好地建议我们使用 yum 模块——然而,在这个例子中,yum 模块并没有提供我们需要的功能,因此我们使用了 shell 模块。

安装并启动数据库后,我们接下来有以下三个高层任务要执行:

  • 更新 MariaDB 配置。

  • 确保 MariaDB 安装的安全性。

  • 向数据库加载初始数据(或模式)。

在这些任务中,我们详细探讨了如何有效使用 Ansible 的 template 模块来管理 MariaDB 配置,详细内容请参见第七章,《使用 Ansible 进行配置管理》(参见进行可扩展的动态配置更改一节)。因此,我们在这里不会再详细讲解——不过,请检查你所选择版本的 MariaDB 配置文件结构,因为它可能与上述章节中显示的有所不同。

如果你在像 CentOS 这样的平台上安装了 MariaDB RPM 包,你可以通过在 root shell 中运行命令 rpm -qc MariaDB-server 来查找配置文件的位置。

因此,假设你已经掌握了数据库服务器的安装和配置,让我们继续确保它的安全性。最低要求是更改 root 密码,尽管良好的实践建议你还应该移除远程 root 访问、test 数据库以及默认 MariaDB 安装时创建的匿名用户账户。

MariaDB 附带了一个名为 mysql_secure_installation 的命令行工具,专门执行这些任务——然而,它是一个交互式工具,不适合与 Ansible 一起自动化运行。幸运的是,Ansible 提供了与数据库交互的模块,可以帮助我们精确地执行这些任务。

为了将这些任务与安装过程分开,我们将创建一个名为 securemariadb 的新角色。在定义任务之前,我们必须定义一个变量来存储 MariaDB 安装的 root 密码。请注意,通常情况下,您会以更安全的方式提供此信息——也许通过 Ansible Vault 文件,或使用 AWX 或 Ansible Tower 中的一些高级功能。为了简化示例,我们将在角色中定义一个变量文件(在 roles/securemariadb/vars/main.yml 中),如下所示:

---
mariadb_root_password: "securepw"

现在,让我们为角色构建任务。Ansible 提供了一些原生模块,可用于数据库管理,我们可以在这里利用它们,对 MariaDB 数据库进行所需的更改。

然而,请注意,某些模块有特定的 Python 要求,在我们的示例系统——CentOS 7 上的 MariaDB——的情况下,我们必须安装 MySQL-python 包。

了解这一点后,构建我们角色的第一步是安装必备的 Python 包,如下所示:

---
- name: Install the MariaDB Python module required by Ansible
  yum:
    name: MySQL-python
    state: latest

我们的首要任务,一旦安装完成,就是设置本地 root 账户的密码,并防止任何人未经身份验证登录。请运行以下代码:

- name: Set the local root password
  mysql_user:
    user: root
    password: "{{ mariadb_root_password }}"
    host: "localhost"

到目前为止,这还是一个教科书般的 mysql_user 模块使用示例——然而,从这里开始,我们的使用方式有些不同。前面的示例利用了没有设置 root 密码的事实——它隐式地以 root 身份操作数据库,原因是我们将在 site.yml 文件中设置 become: yes,因此,playbook 会以 root 身份运行。在此任务运行时,root 用户没有密码,因此,上述任务将顺利运行。

解决方法是为所有未来任务的模块添加 login_userlogin_password 参数,以确保我们已经成功地通过身份验证与数据库进行交互,从而执行所需的任务。

这个角色只会在第一次运行时成功——在第二次运行时,root MariaDB 用户的密码将被设置,前面的任务将失败。然而,如果我们为上述任务指定了 login_password,且密码为空(如在初次运行时),任务也会失败。有多种方法可以解决这个问题,比如在另一个变量中设置旧密码,或者确实只运行一次此角色。您还可以在该任务下指定 ignore_errors: yes,这样,如果 root 密码已经设置,我们就可以继续执行后续任务,这些任务应该能够顺利运行。

在理解了这个条件之后,我们现在为该角色添加另一个任务,即移除远程根账户,如下所示:

- name: Delete root MariaDB user for remote logins
  mysql_user:
    user: root
    host: "{{ ansible_fqdn }}"
    state: absent
    login_user: root
    login_password: "{{ mariadb_root_password }}"

再次说明,这段代码自我解释性强——然而,也请注意,在第二次运行时会出现错误,因为第二次运行时,这些权限将不存在,因为我们在第一次运行时已将其删除。因此,这几乎肯定是一个只需运行一次的角色,或者在代码和错误处理逻辑中必须仔细考虑的地方。

现在我们添加一个任务来删除匿名用户账户,如下所示:

- name: Delete anonymous MariaDB user
  mysql_user:
    user: ""
    host: "{{ item }}"
    state: absent
    login_user: root
    login_password: "{{ mariadb_root_password }}"
  loop:
    - "{{ ansible_fqdn }}"
    - localhost

你会看到这里使用了loop——这是用来在一个任务中删除本地和远程的权限。最后,我们通过运行以下代码删除test数据库,这是大多数企业场景中多余的数据库:

- name: Delete the test database
  mysql_db:
    db: test
    state: absent
    login_user: root
    login_password: "{{ mariadb_root_password }}"

角色完全完成后,我们可以像往常一样运行它,并保护我们新安装的数据库。输出应该类似于这样:

通过这两个角色和来自第七章《使用 Ansible 进行配置管理》的部分输入,我们成功地在 CentOS 上安装、配置并加固了 MariaDB 数据库。显然,这是一个非常具体的示例——然而,如果你在 Ubuntu 上执行这个过程,方法也非常相似。不同之处在于:

  • 在所有任务中,将使用apt模块替代yum模块。

  • 在 Ubuntu 上,软件包名称需要更改。

  • 定义仓库源应在/etc/apt下进行,而不是在/etc/yum.repos.d,并相应调整文件格式。

  • Ubuntu 上 MariaDB 的配置路径可能会有所不同。

  • Ubuntu 通常使用ufw而不是firewalld——默认情况下,你可能会发现ufw被禁用,因此,这一步可以跳过。

考虑到这些变化,前面的过程可以非常快速地调整为适用于 Ubuntu(或者,实际上,任何其他平台,只要做出适当的更改)。一旦软件包安装和配置完成,由于诸如mysql_usermysql_db等模块是跨平台的,它们将在所有支持的平台上正常工作。

到目前为止,在本书中,我们主要关注 MariaDB——这并非因为对这个数据库有任何固有偏好,也不应被解读为任何推荐。它只是作为一个相关的示例,在整本书中不断展开。在我们继续学习如何将数据或模式加载到新安装的数据库中之前,我们将在下一节简要回顾如何将我们目前学到的过程应用于另一个流行的 Linux 数据库——PostgreSQL。

使用 Ansible 安装 PostgreSQL Server

在本节中,我们将演示如何将我们到目前为止在 CentOS 上为 MariaDB 所学习的原则和高层流程应用到另一个平台。总体来看,这些流程可以应用于几乎任何数据库和 Linux 平台,只要注意细节。这里,我们将在 Ubuntu Server 上安装 PostgreSQL Server,然后通过设置 root 密码来确保其安全——本质上,这与我们在前一节中所执行的过程类似。

让我们开始吧,首先创建一个名为 installpostgres 的角色。在这个角色中,我们将再次定义一个来自官方 PostgreSQL 源的软件包下载模板,这次——当然——将其调整为我们使用的是 Ubuntu Server,而不是 CentOS。以下代码展示了模板文件——请注意,这对于 Ubuntu Server 18.04 LTS(代号 bionic)是特定的:

deb http://apt.postgresql.org/pub/repos/apt/ bionic-pgdg main

如前所述,一旦我们定义了软件包源,就可以开始创建将安装数据库的任务。在 Ubuntu 系统中,我们必须手动将软件包签名密钥添加到 apt 密钥环中,除此之外,还需要将前面的模板复制到合适的位置。因此,我们在角色中的任务开始如下:

---
- name: Populate PostgreSQL apt template on target host
  template:
    src: templates/pgdg.list.j2
    dest: /etc/apt/sources.list.d/pgdg.list
    owner: root
    group: root
    mode: '0644'

我们也可以在这里使用 apt_repository,但是为了与之前的 MariaDB 示例保持一致,我们使用了模板。两者都能达到相同的最终结果。

template 软件包到位后,我们接着需要将软件包签名密钥添加到 apt 的密钥环中,如下所示:

- name: Add key for PostgreSQL packages
  apt_key:
    url: https://www.postgresql.org/media/keys/ACCC4CF8.asc
    state: present

然后安装 postgresql-11 及其他支持包(参考文档 www.postgresql.org/download/linux/ubuntu/),如下所示:

- name: Install PostgreSQL 11 packages
  apt:
    name:
      - postgresql-11
      - postgresql-client-11
    state: latest
    update_cache: yes

由于我们的默认 Ubuntu Server 安装没有运行防火墙,因此此剧本中的最后一项任务是启动服务,并确保它在启动时自动启动,如下所示:

- name: Ensure PostgreSQL service is installed and started at boot time
  service:
    name: postgresql
    state: started
    enabled: yes

运行此命令应该会产生类似以下的输出:

默认情况下,开箱即用的 PostgreSQL 安装比 MariaDB 更加安全。没有额外的配置时,远程登录完全不被允许,尽管超级用户账户没有设置密码,但它只能从本地机器上的 postgres 用户账户进行访问。同样,也没有测试数据库可供删除。

因此,尽管高层流程相同,但你必须注意所使用的数据库服务器和底层操作系统的细微差别。

作为示例并完成本节内容,我们创建一个名为 production 的数据库,并创建一个名为 produser 的关联用户,该用户将获得访问权限。虽然从技术上讲,这与下一节关于加载初始数据的内容有所重叠,但这里提供它是为了类比前一节关于 MariaDB 的内容,并展示如何使用 PostgreSQL 的本地 Ansible 模块。

  1. 我们来创建一个名为setuppostgres的角色,并首先定义一个任务来安装支持 Ansible PostgreSQL 模块所需的 Ubuntu 软件包,如下所示:
---
- name: Install PostgreSQL Ansible support packages
  apt:
    name: python-psycopg2
    state: latest
  1. 在此之后,我们添加一个任务来创建数据库(这是一个非常简单的示例—您需要根据您的具体需求进行调整),如下所示:
- name: Create production database
  postgresql_db:
    name: production
    state: present
  become_user: postgres
  1. 请注意,我们如何利用目标机器上的本地postgres账户,通过become_user语句实现数据库超级用户访问。接下来,我们将添加用户,并赋予他们该数据库的权限,如下所示:
- name: Add produser account to database
  postgresql_user:
    db: production
    name: produser
    password: securepw
    priv: ALL
    state: present
  become_user: postgres

像往常一样,您不会像这样直接以明文形式指定密码—这里是为了简化而这么做的。通常,您应该为变量替换适当的数据,并且如果这些变量是敏感的,可以使用 Ansible Vault 加密它们,或者在剧本运行时提示用户输入。

  1. 现在,为了让 PostgreSQL 监听来自该用户的远程连接,我们还需要执行两个动作。我们需要在pg_hba.conf中添加一行,告诉 PostgreSQL 允许我们刚创建的用户从适当的网络访问该数据库—以下示例已显示,但请确保根据您的网络和需求进行调整:
- name: Grant produser access to the production database over the local network
  postgresql_pg_hba:
    dest: /etc/postgresql/11/main/pg_hba.conf
    contype: host
    users: produser
    source: 192.168.81.0/24
    databases: production
    method: md5
  1. 我们还必须更改postgresql.conf文件中的listen_addresses参数,该参数默认仅支持本地连接。此文件的具体位置会根据您的操作系统和 PostgreSQL 版本而有所不同—以下示例适用于我们在 Ubuntu Server 18.04 上安装的 PostgreSQL 11:
- name: Ensure PostgreSQL is listening for remote connections
  lineinfile:
    dest: /etc/postgresql/11/main/postgresql.conf
    regexp: '^listen_addresses ='
    line: listen_addresses = '*'
  notify: Restart PostgreSQL
  1. 细心的读者会注意到这里也使用了处理程序—postgresql服务必须重启才能生效对该文件的任何更改。然而,这应仅在文件更改时执行,因此我们使用了处理程序。我们的handlers/main.yml文件将如下所示:
---
- name: Restart PostgreSQL
  service:
    name: postgresql
    state: restarted
  1. 在我们组装完剧本后,现在可以运行它,输出应该类似于以下截图:

尽管这个示例与上一节中mysql_secure_installation工具的复制并不完全相同,但它展示了如何使用原生的 Ansible 模块配置和保护 PostgreSQL 数据库,并且展示了 Ansible 如何强力协助您设置和保护新的数据库服务器。这些原则可以应用于几乎任何与 Linux 兼容的数据库服务器,尽管每个数据库可用的模块可能不同。完整的模块列表可以在这里找到:docs.ansible.com/ansible/latest/modules/list_of_database_modules.html

现在我们已经看过了安装数据库服务器的过程,在接下来的部分中,我们将基于安装工作加载初始数据和模式。

导入和导出数据

安装软件并配置数据库并不足以使其完整——通常,存在一个非常重要的中间步骤,涉及加载初始数据集。这可能是来自以前数据库的备份、用于测试的清理数据集,或者只是一个模式,用于加载应用程序数据。

尽管 Ansible 有一些模块可以实现有限的数据库功能,但在数据库管理方面,其功能并不像其他自动化任务那样完整。Ansible 对数据库的最全面支持是 PostgreSQL——对于其他数据库的支持较少。通过巧妙使用shell模块,您可以将命令行上执行的任何手动任务转换为 Ansible 任务。具体如何处理错误或特殊情况(例如数据库已经存在)则取决于您如何为任务应用逻辑,我们将在下一节中看到此类示例。

在下一节中,我们将介绍如何使用 Ansible 自动化将示例数据库加载到 MariaDB 数据库中的任务。

使用 Ansible 自动化 MariaDB 数据加载

MariaDB 是本章的一个好选择,因为它提供了一个中等程度的数据库管理视角,尤其是在使用 Ansible 时。Ansible 提供了一些原生模块支持,但对于您可能需要执行的所有任务,它并不完全支持。因此,我们将开发以下示例,使用shell Ansible 模块自动加载示例数据集。然后,我们将扩展这个示例,展示如何使用mysql_db模块来完成此任务,以便为您提供这两种自动化技术的直接对比。

请注意,以下示例使用shell模块执行的操作可以适用于几乎任何可以通过命令行管理的数据库,因此希望这些示例能为您自动化数据库管理任务提供有价值的参考。

在示例数据库方面,我们将使用公开可用的Employees示例数据库,因为这是所有阅读本书的人都可以访问的。您当然可以选择您自己的数据集来使用——然而,正如往常一样,希望以下实践示例能教会您使用 Ansible 将数据加载到新安装的数据库中的技能:

  1. 首先,让我们创建一个名为loadmariadb的角色。在roles目录结构中创建一个名为files/的目录,并克隆employees示例数据库。该数据库在 GitHub 上公开提供,在撰写本文时,可以通过以下命令克隆:
$ git clone https://github.com/datacharmer/test_db.git
  1. 从这里开始,我们在角色目录中创建一个tasks/目录,并编写我们角色任务的代码。首先,我们需要通过运行以下代码,将数据库文件复制到我们的数据库服务器:
---
- name: Copy sample database to server
  copy:
    src: "{{ item }}"
    dest: /tmp/
  loop:
    - files/test_db/employees.sql
    - files/test_db/load_departments.dump
    - files/test_db/load_employees.dump
    - files/test_db/load_dept_emp.dump
    - files/test_db/load_dept_manager.dump
    - files/test_db/load_titles.dump
    - files/test_db/load_salaries1.dump
    - files/test_db/load_salaries2.dump
    - files/test_db/load_salaries3.dump
    - files/test_db/show_elapsed.sql
  1. 一旦数据文件被复制到服务器,接下来只需要将其加载到数据库中。然而,由于没有用于此任务的模块,我们必须依赖 shell 命令来处理此操作,如下代码块所示:
- name: Load sample data into database
  shell: "mysql -u root --password={{ mariadb_root_password }} < /tmp/employees.sql"
  args:
    chdir: /tmp
  1. 角色任务本身非常简单——然而,在我们运行剧本之前,我们需要设置 mariadb_root_password 变量,最好将其放在一个 vault 中,但为了简便起见,在本书中我们会将其放在角色中的纯文本 vars 文件中。vars/main.yml 文件应如下所示:
---
mariadb_root_password: "securepw"

正如你可能已经发现的,这个剧本假设你已经在之前的角色中安装并配置了 MariaDB——前面代码块中使用的密码是我们在安装 MariaDB 并使用 Ansible 安全配置时设置的密码。

  1. 运行剧本后,应该得到如下结果:

在这里,我们不仅加载了一个示例架构,还将示例数据加载到了数据库中。在你的企业中,你可以根据需要选择单独执行这两个任务中的任何一个。

你可能已经注意到,这个剧本是极其危险的。正如我们之前讨论的,使用 Ansible 剧本中的 shell 模块的问题在于,任务的结果会有所不同,因为无论是否需要运行,shell 命令都会被执行。因此,如果你在一个已有名为 employees 的数据库的服务器上运行此剧本,它将覆盖数据库中的所有数据,替换为示例数据!与此相比,copy 模块仅在目标位置不存在文件时才会复制文件。

鉴于写作时缺乏原生的数据库模块,我们需要设计一种更智能的方式来运行此命令。在这里,我们可以利用 Ansible 内置的一些巧妙的错误处理机制。

shell 模块假设它正在运行的命令如果返回退出码为零,则表示命令成功运行。这导致任务返回我们在此剧本运行中看到的 changed 状态。然而,如果退出码不为零,shell 模块将返回 failed 状态。

我们可以利用这个知识,并结合一个有用的 MariaDB 命令,若查询的数据库存在,则该命令返回零退出码,若不存在则返回非零退出码。请参见下面的截图示例:

我们可以通过在加载数据的任务之前运行该命令来利用它。我们可以忽略该命令的任何错误,并将其注册到一个变量中。我们使用这个变量来有条件地运行数据加载,仅在出现错误时加载数据(即数据库不存在时,加载数据是安全的)。

copy 任务保持不变,但任务的尾部现在如下所示:

- name: Check to see if the database exists
  shell: "mysqlshow -u root --password={{ mariadb_root_password }} employees"
  ignore_errors: true
  register: dbexists

- name: Load sample data into database
  shell: "mysql -u root --password={{ mariadb_root_password }} < /tmp/employees.sql"
  args:
    chdir: /tmp
  when: dbexists.rc != 0

现在,我们只在数据库不存在的情况下加载数据。为了提供一个简单的示例,这段代码保持简洁,剩下的部分留给你来增强——例如,可以将文件名和数据库名放入变量中,这样角色在不同情况下就能重用(毕竟,编写可重用角色是编写角色的目标之一)。

如果我们现在运行这段代码,我们可以看到它按预期工作——第一次运行时,数据被加载,如下图所示:

然而,在第二次运行时,它没有执行——以下截图显示了第二次运行的 playbook,以及由于数据库已存在,数据加载任务被跳过的情况:

尽管这些示例特定于 MariaDB,但这里执行的高层次过程应该适用于几乎任何数据库。关键是使用shell模块来加载数据和/或架构,但以一种减少有效数据库被覆盖的方式,以防 playbook 被重复运行。你应该将这一逻辑扩展到你执行的任何其他任务——你的最终目标应该是,如果 playbook 被意外运行,那么不会对现有数据库造成损害。

完成这个示例后,值得注意的是,Ansible 确实提供了一个名为mysql_db的模块,可以原生处理诸如转储和导入数据库数据的任务。现在让我们开发一个使用原生mysql_db模块的示例:

  1. 如果我们开发一个角色,执行与之前相同的任务,但使用这个原生模块,首先要做的是像之前一样检查数据库是否存在,并将结果注册到一个变量中,如下所示:
---
- name: Check to see if the database exists
  shell: "mysqlshow -u root --password={{ mariadb_root_password }} employees"
  ignore_errors: true
  register: dbexists
  1. 然后我们在任务文件中创建一个block,因为如果数据库已经存在,那么在这一步之后运行任何任务都没有意义。block使用了我们之前使用的when条件语句,用于判断其中的任务是否需要执行,如下所示:
- name: Import new database only if it doesn't already exist
  block:

  when: dbexists.rc != 0
  1. block内部,我们像之前一样将所有 SQL 文件复制过来进行导入,如下所示:
  - name: Copy sample database to server
    copy:
      src: "{{ item }}"
      dest: /tmp/
    loop:
      - files/test_db/employees.sql
      - files/test_db/load_departments.dump
      - files/test_db/load_employees.dump
      - files/test_db/load_dept_emp.dump
      - files/test_db/load_dept_manager.dump
      - files/test_db/load_titles.dump
      - files/test_db/load_salaries1.dump
      - files/test_db/load_salaries2.dump
      - files/test_db/load_salaries3.dump
      - files/test_db/show_elapsed.sql
  1. 现在,使用shell模块和mysql_db模块之间有一个重要的区别。当使用shell模块时,我们使用了chdir参数将工作目录切换到/tmp,这是所有 SQL 文件被复制到的地方。而mysql_db模块没有chdir(或等效的)参数,因此在尝试加载通过employees.sql引入的*.dump文件时会失败。为了解决这个问题,我们使用 Ansible 的replace模块,将这些文件的完整路径添加到employees.sql中的相应行,如下所示:
  - name: Add full paths to employees.sql as mysql_db won't know where to load them from otherwise
    replace:
      path: /tmp/employees.sql
      regexp: '^source (.*)$'
      replace: 'source /tmp/\1'
  1. 最后,我们使用mysql_db模块加载数据(这类似于我们之前示例中执行的 shell 命令),如下所示:
  - name: Load sample data into database
    mysql_db:
      name: all
      state: import
      target: /tmp/employees.sql
      login_user: root
      login_password: "{{ mariadb_root_password }}"
  1. 当我们运行这段代码时,它达到了与我们之前使用shell模块的角色相同的最终结果,如下图所示:

这个过程同样适用于数据库的备份。如果你使用shell模块,可以使用mysqldump命令来备份数据库,然后将备份的数据复制到你的 Ansible 主机(或其他主机)进行归档。以下是一段简单的示例代码来实现这一点:

  1. 由于我们希望备份文件名是动态的,并包含有用的信息,例如当前日期和正在执行备份的主机名,我们使用set_fact模块,结合一些内部的 Ansible 变量,来定义备份数据的文件名,如下所示:
---
- name: Define a variable for the backup file name
  set_fact:
    db_filename: "/tmp/{{ inventory_hostname }}-backup-{{ ansible_date_time.date }}.sql"
  1. 然后我们使用shell模块运行mysqldump,并传入适当的参数来创建备份——深入讨论这些参数超出了本书的范围,但以下示例将创建一个服务器上所有数据库的备份,并且在备份过程中不会锁定表:
- name: Back up the database
  shell: "mysqldump -u root --password={{ mariadb_root_password }} --all-databases --single-transaction --lock-tables=false --quick > {{ db_filename }}"
  1. fetch模块用于检索数据以进行归档——fetch的工作方式与我们在本节前面使用的copy模块相同,只是它以相反的方向复制数据(即从库存主机到 Ansible 服务器)。运行以下代码:
- name: Copy the backed up data for archival
  fetch:
    src: "{{ db_filename }}"
    dest: "/backup"
  1. 按照常规方式运行会生成完整的数据库备份,备份文件将被复制到我们的 Ansible 服务器,以下截图展示了这一过程:

这个示例也可以使用mysql_db模块来实现,就像我们之前所做的那样——set_factfetch任务保持完全不变,而shell任务则替换为以下代码:

- name: Back up the database
  mysql_db:
    state: dump
    name: all
    target: "{{ db_filename }}"
    login_user: root
    login_password: "{{ mariadb_root_password }}"

因此,Ansible 不仅可以帮助你将数据加载到数据库中,还可以帮助你进行备份。正如我们之前讨论的那样,通常最好使用原生的 Ansible 模块(如mysql_db),但如果原生模块不存在或无法提供所需的功能,只要你应用正确的逻辑,shell模块也能为你提供帮助。

现在我们已经考虑了创建数据库并加载数据的过程,我们将在下一节展示如何在此基础上进行扩展,使用 Ansible 执行常规的数据库维护任务。

执行常规维护

加载架构和/或数据并不是你使用 Ansible 在数据库上执行的唯一任务。有时,数据库需要手动干预。例如,PostgreSQL 需要定期执行 VACUUM 操作,以释放数据库中未使用的空间。MariaDB 有一个名为mysqlcheck的维护工具,可以用来验证表的完整性并执行优化。每个平台都会有自己的特定工具来进行维护操作,你需要为你选择的平台建立最佳的数据库维护实践。此外,有时也需要对数据库进行一些简单的修改。例如,可能需要删除(或更新)表中的一行,以清除应用程序中出现的错误情况。

当然,所有这些操作都可以手动执行——然而,这样做(如同往常一样)会带来丢失操作记录的风险,包括谁执行了任务、如何执行的(例如,提供了哪些选项)。如果我们将这个例子放到 Ansible 和 AWX 的环境中,我们就会突然拥有完整的活动审计轨迹,且我们能准确知道执行了什么操作以及是如何执行的。此外,如果任务需要特定选项,这些选项将被存储在 playbook 中,因此 Ansible 提供的自我文档化功能在这里也能发挥作用。

由于到目前为止我们的例子非常以 MariaDB 为中心,让我们看看如何使用 Ansible 在 PostgreSQL 中对表执行完整的 vacuum 操作。

使用 Ansible 对 PostgreSQL 进行常规维护

在 Ansible 中,PostgreSQL 有些特殊,因为它拥有比大多数其他数据库更多的原生模块来支持数据库活动。让我们考虑一个示例:对公开的 AdventureWorks 样本数据库中的销售.creditcard表执行 vacuum 操作(可以在这里获取:github.com/lorint/AdventureWorks-for-Postgres)。

Vacuum 是 PostgreSQL 特有的维护过程,你可能希望定期运行它,特别是当你的表有大量删除或修改时。虽然对此的完整讨论超出了本书的范围,但需要考虑到,受这些活动影响的表可能会变得臃肿,查询可能会随着时间变慢,而 vacuum 操作是一种释放未使用空间并加速查询的方法。

现在,要手动执行 vacuum 操作,你需要使用适当的凭证登录psql客户端工具,然后运行以下命令连接到数据库并执行任务:

postgres=# \c AdventureWorks
AdventureWorks=# vacuum full sales.creditcard;

在实际企业中,这将是一个涉及更多表格,甚至数据库的任务,但在这里,我们将再次保持示例简单,以演示所涉及的原则。将其扩展为大规模应用任务就留给你自己去做吧。让我们首先使用 Ansible 中的shell模块来自动化这一过程。这是一个有用的示例,因为该技术适用于大多数主要数据库——你只需确定特定维护操作所需的命令,然后运行它。

执行此任务的简单角色看起来如下:

---
- name: Perform a VACUUM on the sales.credit_card table
  shell: psql -c "VACUUM FULL sales.creditcard" AdventureWorks
  become: yes
  become_user: postgres

注意——与之前一样——我们非常简单地使用了shell模块并带有适当的命令,唯一不同的是,这次我们使用了become_user参数切换到postgres用户账户,该账户对我们连接的主机上的数据库具有超级用户权限。让我们看看运行时会发生什么,具体如下:

自然地,这可以扩展到几乎任何其他数据库——例如,你可以在 MariaDB 数据库上使用mysql客户端工具,甚至运行之前讨论过的mysqlcheck工具。真正的限制在于你为shell模块编写的脚本,而因为 Ansible 通过 SSH 在数据库服务器上运行命令,你无需担心开放数据库的网络访问——它可以保持紧密的安全限制。

除了使用shell模块,Ansible 还为我们提供了直接从名为postgresql_query的模块运行查询的选项。虽然这种支持是独特的,但如果有人愿意编写并提交模块,也可以为任何其他数据库添加类似支持。

不幸的是,对于 2.9 之前的 Ansible 版本,我们无法将 VACUUM 示例扩展到这个模块,因为postgresql_query模块会在事务块内运行事务,而无法在事务块内运行 VACUUM。如果你正在运行 2.9 或更高版本,现在可以使用示例代码运行 VACUUM,如下所示:

---
- name: Perform a VACUUM on the sales.credit_card table
  postgresql_query:
    db: AdventureWorks
    query: VACUUM sales.creditcard
    autocommit: yes
  become_user: postgres
  become: yes

通过另一个简单的示例,我们还可以使用postgresql_query模块直接操作数据库。

假设使用此数据库的应用程序出现了一个错误,操作员必须手动将信用卡号码插入数据库。执行此操作的 SQL 代码可能如下所示:

INSERT INTO sales.creditcard ( creditcardid, cardtype, cardnumber, expmonth, expyear ) VALUES ( 0, 'Visa', '0000000000000000', '11', '2019' );

我们可以通过使用如下的角色,在 Ansible 中实现相同的最终结果:

---
- name: Manually insert data into the creditcard table
  postgresql_query:
    db: AdventureWorks
    query: INSERT INTO sales.creditcard ( creditcardid, cardtype, cardnumber, expmonth, expyear ) VALUES ( 0, 'Visa', '0000000000000000', '11', '2019' );
  become_user: postgres
  become: yes

自然地,你会为数据值使用变量,像这样的敏感数据应始终存储在保险库中(或者,也可以在角色运行时手动输入)。

AWX 有一个叫做 调查 的功能,它在执行 playbook 之前会向用户提出一系列预定义的问题。用户对这些问题的回答将存储在 Ansible 变量中——因此,像前面提到的角色可以被参数化,并且从 AWX 运行时,所有的值都可以通过调查输入,避免使用 vault 以及担心敏感客户数据存储在 Ansible 中的问题。

正如你在这里看到的,当我们运行这个角色时,实际上在 INSERT 操作成功时会显示更改状态——这对于监控此类任务并确保它们按预期执行非常有用。以下截图显示了此角色的运行情况以及 changed 状态,表示数据成功插入到 sales.creditcard 表中:

当涉及到使用 Ansible 进行数据库管理时,世界真的是你的舞台,无论需要什么任务,所有数据库任务都应以标准化、可重复和可审计的方式进行,就像您企业的其他 Linux 系统一样。希望本章能帮助您了解如何实现这一目标。

总结

数据库是大多数企业应用堆栈中的核心部分,Linux 平台上有多种数据库可供选择。尽管许多数据库有自己的管理工具,Ansible 非常适合协助执行各种数据库管理任务,从安装数据库服务、加载初始数据或架构(甚至从备份中恢复)到处理日常维护任务。结合 Ansible 的错误处理和安全自动化,几乎没有任何限制,您可以使用 Ansible 执行各种数据库管理任务。

本章中,您学习了如何使用 Ansible 一致且可重复地安装数据库服务器。然后,您学习了如何导入初始数据和架构,并扩展到自动化备份任务。最后,您获得了使用 Ansible 执行一些常规数据库维护任务的实践知识。

在下一章中,我们将探讨 Ansible 如何协助您执行 Linux 服务器的常规维护任务。

问题

  1. 为什么使用 Ansible 安装和管理数据库平台是明智的选择?

  2. 使用 Ansible 管理数据库配置文件的最佳实践是什么?

  3. Ansible 如何帮助您保持数据库在网络上的安全?

  4. 在什么情况下,您会使用 Ansible 的 shell 模块而不是本地数据库模块?

  5. 为什么要使用 Ansible 执行例行维护?

  6. 如何使用 Ansible 执行 PostgreSQL 数据库备份?

  7. 您会使用哪个模块来操作 MariaDB 数据库中的用户?

  8. 目前 Ansible 中对 PostgreSQL 的支持有何独特之处?

进一步阅读

第十二章:使用 Ansible 执行常规维护

在您完成本书的学习后,您将已经完成了多个步骤,为您的企业定义并构建了一个支持自动化的 Linux 环境。然而,Ansible 对您的环境的帮助并未就此结束。即便是一个已经构建并在使用中的环境,也需要定期进行维护和干预。从前,这些干预通常由系统管理员手动执行,使用 shell 命令或脚本。

正如我们在本书中多次提到的那样,手动执行的任务给企业带来了许多挑战——其中最重要的是,它们可能没有很好地文档化,因此新员工的学习曲线较陡。此外,我们的老朋友可审计性和可重复性也会出现——如果每个人都直接登录到 Linux 机器的 shell 并手动执行任务,您如何确保知道谁在什么时候做了什么?

在本章中,我们将探讨 Ansible 如何帮助企业管理 Linux 环境的日常工作,特别是在执行常规维护任务时。Ansible 功能强大,您进行常规维护的可能性不仅限于本章中的示例——这些示例旨在帮助您入门,并通过示范展示您可能能够自动化的任务种类。

本章将具体介绍以下主题:

  • 清理磁盘空间

  • 监控配置漂移

  • 使用 Ansible 管理进程

  • 使用 Ansible 执行滚动更新

技术要求

本章包括以下技术的示例:

  • Ubuntu Server 18.04 LTS

  • CentOS 7.6

  • Ansible 2.8

要运行这些示例,您需要访问两台服务器或虚拟机,每台运行一个上述操作系统,同时安装 Ansible。请注意,本章中给出的示例可能具有破坏性(例如,它们会删除文件并更改服务器配置),如果按原样运行,它们仅适用于在隔离的测试环境中运行。

一旦您确认已拥有一个安全的操作环境,就可以开始使用 Ansible 进行常规系统维护了。

本章讨论的所有示例代码都可以在 GitHub 上找到,网址为:github.com/PacktPublishing/Hands-On-Enterprise-Automation-on-Linux/tree/master/chapter12

清理磁盘空间

系统管理员需要定期完成的最常规、最琐碎(但却至关重要)任务之一就是清理磁盘空间。尽管理想情况下,系统应该是良好运作的——例如,日志文件应该进行轮换,临时文件应该被清理——但是在这个行业有经验的人都会知道,情况并不总是如此。本书的作者曾在一些环境中工作,在这些环境中,清理某个特定目录被视为常规任务——因此,它是自动化的最佳候选项。

当然,你不会随便从文件系统中删除文件。任何这样的任务都应该以精确的方式执行。让我们来看一个实际的例子——由于这是假设的,我们来创建一些测试文件。假设我们的虚构应用每天都会创建一个数据文件,并且从不清理其 data 目录。为了模拟这个情况,我们可能会创建一些数据文件,像这样:

$ sudo mkdir -p /var/lib/appdata
$ for i in $(seq 1 20); do DATE=$(date -d "-$i days" +%y%m%d%H%M); sudo touch -t $DATE /var/lib/appdata/$DATE; done

上面的命令创建了一个名为 /var/lib/appdata 的目录,并且为过去 20 天的每一天创建了一个(空的)文件。当然,我们也可以创建有数据的文件,但这对这个例子没有影响——我们并不想真正填满磁盘!

现在,假设我们的磁盘空间快满了,我们想清理这个目录,只保留最近 5 天的数据。如果我们手动执行此操作,可能会使用久经考验的 find 命令,列出符合条件的文件,并删除较旧的文件。这看起来可能是这样:

$ sudo find /var/lib/appdata -mtime +5 -exec rm -f '{}' \;

这是一个相当简单的命令,你可能会惊讶地发现,类似的命令在企业级的 Linux 服务器运行手册中非常常见。让我们用 Ansible 来改进这个过程。我们知道,如果我们在 Ansible 中实现它,以下情况将会发生:

  • Ansible 引擎将根据所采取的操作返回适当的状态——okchangedfailed。前面的代码块中展示的 find 命令无论是否删除任何文件,都会返回相同的输出和退出代码。

  • 我们编写的 Ansible 代码将具有自我文档化的特性——例如,它将以适当的 name 开始——可能是 Prune /var/lib/appdata

  • Ansible 代码可以从 AWX 或 Ansible Tower 运行,确保这个常规任务可以委派给适当的团队,使用内置的基于角色的访问控制。

  • 此外,任务可以在 AWX 中赋予一个用户友好的名称,这意味着操作员无需任何专业知识即可开始有效地协助管理 Linux 环境。

  • AWX 和 Ansible Tower 会忠实地记录任务执行的输出,确保将来能够审核这些清理任务。

当然,Ansible 的这些好处对我们来说并不新鲜——我们在整本书中经常提到它们。尽管如此,我还是希望强调企业中有效自动化的好处。让我们从定义一个角色开始,来执行这个具体的功能——使用 Ansible 修剪超过 5 天的文件目录:

  1. 我们首先利用 Ansible 的find模块,它使我们能够构建一个文件系统对象的列表(如文件或目录),就像find shell 命令一样。我们将find模块的输出register到一个 Ansible 变量中,以便稍后使用,具体如下:
- name: Find all files older than {{ max_age }} in {{ target_dir }}
  find:
    paths: "{{ target_dir }}"
    age: "{{ max_age }}"
    recurse: yes
  register: prune_list

这里展示的代码片段应该比较容易理解—然而请注意,我们已经为pathage参数使用了变量;这是有充分理由的。角色的核心在于代码的复用,如果我们通过变量定义这些参数,就可以复用这个角色来修剪其他目录(例如,针对不同的应用),而无需修改角色代码本身。你还会发现,我们可以在任务的name中使用这些变量—当我们将来回头审计 Ansible 运行时,这非常有用且强大。

  1. find模块将构建一个我们需要删除的文件列表——然而,考虑到我们审计的目标,可能对我们来说有用的是在 Ansible 输出中打印这些文件名,以确保我们稍后可以回来并准确了解删除了哪些文件。请注意,我们不仅可以打印路径—也许捕获文件大小和时间戳信息会更有用?所有这些信息都可以在我们之前捕获的prune_list变量中找到,你可以自行探索这些内容。(提示:将msg: "{{ item.path }}"替换为msg: "{{ item }}",查看find任务捕获的所有信息。)运行以下代码:
- name: Print file list for auditing purposes
  debug:
    msg: "{{ item.path }}"
  loop:
    "{{ prune_list.files }}"
  loop_control:
    label: "{{ item.path }}"

在这里,我们只是使用 Ansible 的循环,遍历find模块生成的数据——具体来说,从我们的变量中的files字典提取path字典项。loop_control选项防止 Ansible 在每个debug消息前打印整个字典结构,而是仅使用每个文件的path作为label

  1. 最后,我们使用file模块删除文件,依旧像之前那样遍历prune_list,具体如下:
- name: Prune {{ target_dir }}
  file:
    path: "{{ item.path }}"
    state: absent
  loop:
    "{{ prune_list.files }}"
  loop_control:
    label: "{{ item.path }}"
  1. 在角色完成后,我们必须为我们的 play 定义变量—在本例中,我在引用我们新角色的site.yml剧本中定义了它们,具体如下:
---
- name: Prune Directory
  hosts: all
  become: yes
  vars:
    max_age: "5d"
    target_dir: "/var/lib/appdata"

  roles:
    - pruneappdata

使用本节之前生成的测试文件运行此代码,将会得到类似于下面这样的输出:

为了确保截图能适应屏幕,前面的测试文件集已经进行了缩减—但是你可以清楚地看到输出,以及哪些文件被删除了。

虽然良好的维护是服务器管理的关键部分,但有时我们只希望在绝对必要时采取行动(例如修剪目录)。假如我们决定这个角色只有在包含/var/lib/appdata的文件系统剩余磁盘空间少于 10%时才会运行,会怎么样呢?

以下过程演示了如何使用 Ansible 执行条件性维护,只有当磁盘使用率超过 90%时才会运行:

  1. 我们首先修改现有的角色——首先,我们为角色添加了一个新任务,以获取target目录的磁盘使用率百分比,如下所示:
---
- name: Obtain free disk space for {{ target_dir }}
  shell: df -h "{{ target_dir }}" | tail -n 1 | awk {'print $5 '} | sed 's/%//g'
  register: dfresult
  changed_when: false

尽管 Ansible 有包含磁盘使用信息的事实数据,但我们在这里使用df命令,因为它可以直接查询我们的目录——如果我们想成功使用 Ansible 的事实数据,我们必须以某种方式追溯到它所在的挂载点。我们还使用changed_when: false,因为这个 shell 任务如果没有这个设置,会总是显示变化的结果,这可能会让输出变得令人困惑——这是一个只读查询,所以不应该有任何变化!

  1. 将这些数据收集并注册在dfresult变量中后,我们将现有代码包装在一个代码块中。Ansible 中的代码块只是将一组任务封装在一起的方式——因此,与其在我们之前示例中的三个任务上分别加上when条件,我们直接将条件放在代码块上。代码块的开始可能是这样的:
- name: Run file pruning only if disk usage is greater than 90 percent
  block:

  - name: Find all files older than {{ max_age }} in {{ target_dir }}
    find:

注意之前的任务集现在被缩进了两个空格。这确保 Ansible 理解它是该代码块的一部分。缩进所有现有任务,并用以下代码结束该代码块:

    loop_control:
      label: "{{ item.path }}"
  when: dfresult.stdout|int > 90

在这里,我们使用标准输出捕获的dfresult变量,将其转换为整数,然后检查它是否为 90%或更高。因此,只有当文件系统超过 90%满时,我们才会运行修剪任务。当然,这只是一个条件——你可以收集任何需要的数据,以在其他多种情况下运行你的任务。在我的测试服务器上运行这个新角色,服务器的磁盘利用率远低于 90%,可以看到修剪任务被完全跳过,如下截图所示:

通过这种方式,我们可以轻松地在大型企业环境中执行常规的磁盘维护任务,并且——就像 Ansible 的使用方式一样——你能做的事情没有上限。希望本节中的示例能给你一些关于如何入手的启发。在接下来的部分中,我们将探讨如何使用 Ansible 有效地监控 Linux 环境中的配置漂移。

监控配置漂移

在第七章中,使用 Ansible 进行配置管理,我们探讨了 Ansible 如何在企业规模上部署配置并强制执行它。现在,让我们在此基础上,增加一个新的内容——监控配置漂移。

正如我们在第一章中讨论的,在 Linux 上构建标准操作环境,手动更改是自动化的敌人。除此之外,它们也是一个安全风险。让我们通过一个具体的例子来演示。如本书前面所建议的,建议通过 Ansible 管理安全外壳(SSH)服务器配置。SSH 是管理 Linux 服务器的标准协议,不仅可以用于管理,还可以用于文件传输。简而言之,它是人们访问你的服务器的关键机制之一,因此确保其安全至关重要。

然而,也常见多个人拥有 Linux 服务器的 root 访问权限。无论是开发人员在部署代码,还是系统管理员在执行日常(或修复)工作,很多人拥有服务器的 root 访问权限是完全正常的。如果每个人都表现得很得体,并积极支持企业中的自动化原则,那是没问题的。但是,如果有人做出未经授权的更改怎么办?

通过 SSH 配置,可能会启用远程 root 登录。也可能在你禁用了密码认证并改用基于密钥的认证时,重新启用基于密码的认证。许多时候,这些更改是为了支持懒惰——例如,作为 root 用户复制文件更容易。

无论意图和根本原因如何,某人手动更改你之前部署的 Linux 服务器配置都是一个问题。那么,如何检测这些更改呢?当然,你没有时间登录到每台服务器手动检查文件。然而,Ansible 可以提供帮助。

在第七章中,使用 Ansible 进行配置管理,我们提出了一个简单的 Ansible 示例,该示例从模板中部署 SSH 服务器配置,并在配置发生更改时使用处理程序重启 SSH 服务。

我们实际上可以将这段代码重新用于我们的配置漂移检查。即使不做任何代码更改,我们也可以在检查模式下使用 Ansible 运行剧本。检查模式不会对正在操作的系统做出任何更改——相反,它会尽力预测可能发生的任何更改。这些预测的可靠性在很大程度上取决于角色中使用的模块。例如,template模块能够可靠地预测更改,因为它知道写入的文件是否与现有文件不同。相反,shell模块永远无法知道changeok结果之间的区别,因为它是一个通用模块(尽管它可以在合理的准确度范围内检测失败)。因此,我强烈建议在使用该模块时使用changed_when

让我们看看如果重新运行之前的securesshd角色,这次使用检查模式会发生什么。结果可以在以下截图中看到:

在这里,我们可以看到确实有人更改了 SSH 服务器配置——如果它与我们提供的模板匹配,输出将会像这样:

到目前为止,一切顺利——你可以将其运行在一百台,甚至一千台服务器上,你会知道任何changed结果都来自那些 SSH 服务器配置不再与模板匹配的服务器。你甚至可以再次运行剧本来纠正这种情况,只是这次不在检查模式下运行(也就是说,命令行上没有-C标志)。

在像 AWX 或 Ansible Tower 这样的环境中,作业(即运行剧本)被分类为两种不同的状态——成功和失败。成功是指任何运行到完成的剧本,只产生changedok结果。然而,失败则是由于剧本运行返回一个或多个failedunreachable状态。

因此,我们可以通过让剧本在配置文件与模板版本不同的情况下发出failed状态来增强它。角色的主体部分保持不变,但在我们的模板任务中,我们添加了以下子句:

  register: template_result
  failed_when: (template_result.changed and ansible_check_mode == True) or template_result.failed

这些对任务的操作产生了以下影响:

  • 任务的结果会被注册到template_result变量中。

  • 我们将这个任务的失败条件改为如下:

    • 模板任务的结果发生了变化,我们正在以检查模式运行它。

    • 或者,模板任务因其他原因失败——这是一个通用情况,确保我们仍然正确报告其他失败情况(例如,文件访问被拒绝)。

你将观察到failed_when子句中同时使用了逻辑andor运算符——这是扩展 Ansible 操作的一种强大方式。现在,当我们在检查模式下运行剧本并且文件发生变化时,我们会看到以下结果:

现在,我们可以非常清楚地看到我们的主机上存在问题,并且它将在 AWX 和 Ansible Tower 中报告为失败。

当然,这对于纯文本文件非常有效。那么二进制文件呢?Ansible 当然不能完全替代像高级入侵检测环境AIDE)或久负盛名的Tripwire这样的文件完整性监控工具——然而,它也可以帮助处理二进制文件。事实上,过程非常简单。假设你想确保/bin/bash的完整性——这是大多数系统默认使用的 Shell,因此该文件的完整性非常重要。如果你有空间在你的 Ansible 服务器上存储原始二进制文件的副本,那么你可以使用copy模块将其复制到目标主机。copy模块利用校验和来判断文件是否需要复制,因此,你可以确信,如果copy模块返回changed结果,那么目标文件与原始版本不同,完整性已被破坏。该角色的代码将与我们在此处的模板示例非常相似:

---
- name: Copy bash binary to target host
  copy:
    src: files/bash
    dest: /bin/bash
    owner: root
    group: root
    mode: 0755
  register: copy_result
  failed_when: (copy_result.changed and ansible_check_mode == True) or copy_result.failed

当然,将原始二进制文件存储在 Ansible 服务器上是低效的,而且还意味着你必须保持它们与服务器修补计划同步,这在需要检查大量文件时并不理想。幸运的是,Ansible 的stat模块可以生成校验和,并返回有关文件的其他有用数据,因此,我们可以非常容易地编写一个剧本,通过运行以下代码来检查我们的 Bash 二进制文件是否被篡改:

---
- name: Get sha256 sum of /bin/bash
  stat:
    path: /bin/bash
    checksum_algorithm: sha256
    get_checksum: yes
  register: binstat

- name: Verify checksum of /bin/bash
  fail:
    msg: "Integrity failure - /bin/bash may have been compromised!"
  when: binstat.stat.checksum != 'da85596376bf384c14525c50ca010e9ab96952cb811b4abe188c9ef1b75bff9a'

这是一个非常简单的示例,可以通过确保文件路径、名称和校验和是变量而不是静态值来显著增强。它还可以通过遍历一个包含文件及其相应校验和的字典来进行改进——这些任务留给你自己完成,这是完全可能的,使用我们在本书中介绍的技术。现在,如果我们运行这个剧本(无论是否在检查模式下),如果 Bash 的完整性没有得到保持,我们将看到失败的结果,否则将看到ok,如下所示:

校验和可以用来验证配置文件的完整性,因此,这个示例角色为你可能进行的任何文件完整性检查提供了良好的基础。

我们现在已经完成了对 Ansible 文件和完整性监控的探索,因此,也具备了检查配置漂移的能力。在本章的下一部分,我们将看看如何使用 Ansible 来管理企业 Linux 环境中的进程。

使用 Ansible 理解进程管理

迟早,你会需要在你的企业内部管理,甚至可能需要终止一个或多个 Linux 服务器上的进程。显然,这不是理想的场景,在日常操作中,大多数服务应该使用 Ansible 的service模块进行管理,在本书中我们已经看到过许多这样的示例。

但是,如果你需要真正终止一个挂起的服务呢?显然,系统管理员可以通过 SSH 进入故障服务器并发出如下命令:

$ ps -ef | grep <processname> | grep -v grep | awk '{print $2}'
$ kill <PID1> <PID2>

如果进程固执地拒绝终止,那么以下操作可能是必要的:

$ kill -9 <PID1> <PID2>

尽管这是一个相当标准的做法,大多数系统管理员都会熟悉(并且可能有自己喜欢的工具来处理,例如 pkill),但它与大多数服务器上的手动干预面临相同的问题——你如何追踪发生了什么,哪些进程受到了影响?如果使用了数字 进程 ID (PID),即使访问了命令历史记录,也仍然无法确定哪个进程曾经占用过那个数字 PID。

我们在这里提出的是一种非传统的 Ansible 用法——如果通过像 AWX 或 Ansible Tower 这样的工具运行,它将使我们能够追踪所有执行过的操作,以及谁执行了这些操作的详细信息,如果我们将进程名称作为参数传递,还可以追踪目标是什么。这个功能在将来可能会非常有用,尤其是当我们需要分析问题的历史时,这样就可以轻松检查哪些服务器被操作过,哪些进程被目标化,并且还能得到精确的时间戳。

让我们构建一个角色来执行这套任务。本章最初是针对 Ansible 2.8 编写的,当时没有用于进程管理的模块,因此,以下示例使用原生的 Shell 命令来处理这种情况:

  1. 我们首先运行我们之前在本节中提出的进程列表,但这次我们将 PID 列表注册到一个 Ansible 变量中,如下所示:
---
- name: Get PID's of running processes matching {{ procname }}
  shell: "ps -ef | grep -w {{ procname }} | grep -v grep | grep -v ansible | awk '{print $2\",\"$8}'"
  register: process_ids

熟悉 Shell 脚本的人应该能够理解这一行——我们在系统进程表中筛选出与 Ansible 变量 procname 完全匹配的项,并去除可能出现的任何多余进程名称(例如 grepansible),以免混淆输出。最后,我们使用 awk 将输出处理成一个逗号分隔的列表,其中第一列包含 PID,第二列则是进程名称。

  1. 现在,我们必须开始对这个输出进行操作。我们将对之前填充的 process_ids 变量进行循环,并对输出的第一列(即数字 PID)执行 kill 命令,如下所示:
- name: Attempt to kill processes nicely
  shell: "kill {{ item.split(',')[0] }}"
  loop:
    "{{ process_ids.stdout_lines }}"
  loop_control:
    label: "{{ item }}"

你将观察到这里使用了 Jinja2 过滤器——我们可以使用内置的split函数来拆分我们在前一个代码块中创建的数据,仅取输出的第一列(数字 PID)。不过,我们使用loop_control标签来设置任务标签,包含 PID 和进程名称,在审计或调试场景中可能非常有用。

  1. 任何有经验的系统管理员都知道,仅仅发出kill命令来终止进程是不够的——某些进程必须强制终止,因为它们可能挂起。并非所有进程都会立即退出,因此我们将使用 Ansible 的wait_for模块来检查/proc目录中的 PID——当它变为absent时,我们就知道该进程已退出。运行以下代码:
- name: Wait for processes to exit
  wait_for:
    path: "/proc/{{ item.split(',')[0] }}"
    timeout: 5
    state: absent
  loop:
    "{{ process_ids.stdout_lines }}"
  ignore_errors: yes
  register: exit_results

我们在这里将超时设置为 5 秒——然而,你应根据你的环境适当设置它。再次,我们将输出注册到一个变量中——我们需要知道哪些进程未能退出,因此尝试更强制地终止它们。注意,我们在这里设置了ignore_errors,因为wait_for模块如果未能在指定的timeout内使所需状态(即/proc/PID变为absent)发生时,会产生错误。对此我们不应视为错误,而应作为进一步处理的提示。

  1. 现在,我们遍历wait_for任务的结果——这次,我们使用 Jinja2 的selectattr函数,仅选择那些已经断言为failed的字典项;我们不想强制终止不存在的 PID。运行以下代码:
- name: Forcefully kill stuck processes
  shell: "kill -9 {{ item.item.split(',')[0] }}"
  loop:
    "{{ exit_results.results | selectattr('failed') | list }}"
  loop_control:
    label: "{{ item.item }}"

现在,我们尝试使用-9标志终止卡住的进程——通常足以杀死大多数挂起的进程。再一次,注意 Jinja2 过滤器的使用和循环的整洁标签,以确保我们可以将此角色的输出用于审计和调试。

  1. 现在,我们运行剧本,指定procname的值——没有默认的进程可供终止,我不建议为此变量设置默认值是安全的。因此,在下图中,我使用-e标志在调用ansible-playbook命令时设置它:

从前面的截图中,我们可以清楚地看到剧本终止了mysqld进程,且剧本的输出简洁明了,但包含了足够的调试信息,以备不时之需。

作为附加说明,如果你使用的是 Ansible 2.8 或更高版本,现在有一个原生的 Ansible 模块叫做pids,它将返回一个干净、整洁的 PID 列表,列出正在运行的指定进程名。如果我们要适应这个新功能,首先可以移除 shell 命令,并用pids模块替代,它更易于阅读,如下所示:

---
- name: Get PID's of running processes matching {{ procname }}
  pids:
    name: "{{ procname }}"
  register: process_ids

从这一点开始,角色几乎与之前相同,不同之处在于,我们不再使用从 shell 命令生成的逗号分隔列表,而是拥有一个简单的列表,仅包含与名称中的 procname 变量匹配的每个运行中进程的 PID。因此,在对变量执行命令时,我们不再需要使用 Jinja2 的 split 过滤器。运行以下代码:

- name: Attempt to kill processes nicely
  shell: "kill {{ item }}"
  loop:
    "{{ process_ids.pids }}"
  loop_control:
    label: "{{ item }}"

- name: Wait for processes to exit
  wait_for:
    path: "/proc/{{ item }}"
    timeout: 5
    state: absent
  loop:
    "{{ process_ids.pids }}"
  ignore_errors: yes
  register: exit_results

- name: Forcefully kill stuck processes
  shell: "kill -9 {{ item.item }}"
  loop:
    "{{ exit_results.results | selectattr('failed') | list }}"
  loop_control:
    label: "{{ item.item }}"

这段代码执行的功能与之前相同,只不过现在它更具可读性,因为我们减少了所需的 Jinja2 过滤器数量,并且我们去掉了一个 shell 命令,改用了 pids 模块。结合前面讨论的 service 模块,这些技术应能为你提供坚实的基础,满足你所有使用 Ansible 进行进程控制的需求。

在本章的下一部分也是最后一部分中,我们将看看如何在集群中有多个节点时使用 Ansible,并且你不希望一次性将它们全部停机。

使用 Ansible 进行滚动更新

关于常规维护的章节,如果没有涉及滚动更新,那么将是不完整的。到目前为止,在本书中,我们的示例保持简单,通常只有一个或两个主机,并假设所有示例都可以扩展以管理成百上千的服务器,使用相同的角色和 playbook。

总体而言,这种情况是成立的——然而,确实存在某些特殊情况,我们可能需要更深入地了解 Ansible 的操作。让我们构建一个假设的例子,其中有四个 Web 应用服务器位于负载均衡器后面。需要部署 Web 应用代码的新版本,且部署过程需要多个步骤(因此,需要多个 Ansible 任务)。在我们的简单示例中,部署过程将如下所示:

  1. 将 Web 应用代码部署到服务器。

  2. 重启 Web 服务器服务,以加载新代码。

在生产环境中,你几乎肯定会采取进一步的步骤来确保你的 Web 服务的完整性——例如,如果它位于负载均衡器后面,在代码部署期间,你需要将其下线,并确保在经过验证正常工作之前不会重新上线。预计并非所有读者都能接触到这样的环境,因此,示例保持简单,以确保每个人都能尝试。

我们可以轻松地编写一个简单的 Ansible 角色来执行这个任务——示例如下所示:

---
- name: Deploy new code
  template:
    src: templates/web.html.j2
    dest: /var/www/html/web.html

- name: Restart web server
  service:
    name: nginx
    state: restarted

这段代码按顺序执行我们的两个步骤,完全符合我们的需求。不过,让我们来看一下,当我们在 playbook 中运行这个角色时会发生什么。结果如下图所示:

注意 Ansible 如何执行这些任务。首先,新的代码在所有四台服务器上部署。然后,才重启它们。出于多种原因,这可能并不理想。例如,在第一个任务之后,服务器可能处于不一致的状态,而你可能不希望四台服务器同时处于不一致状态,因为任何使用 Web 应用程序的用户都会遇到错误。此外,如果 playbook 由于某种原因出错并进入失败状态,它将忠实地在所有四台服务器上失败,从而破坏整个 Web 应用程序并导致服务中断。

为了防止这种问题的发生,我们可以使用serial关键字,要求 Ansible 每次只对一定数量的服务器进行更新。例如,如果我们在调用此角色的site.yml playbook 中插入serial: 2这一行,行为将变得相当不同,正如下面的截图所示:

上面的输出为了节省空间被截断了,但清晰地显示了 playbook 现在每次只在两台服务器上运行—因此,在执行的初始阶段,只有cluster1cluster2是异常的,而cluster3cluster4保持一致且未受影响。只有在前两台服务器上的所有任务完成后,接下来的两台才会被处理。

故障处理同样重要,自动化的一个危险是,如果代码或 playbook 中存在问题,可能很容易破坏整个环境。例如,如果我们的部署新代码任务对所有服务器都失败了,那么每次只在两台服务器上运行 playbook 也无济于事。Ansible 仍会忠实地执行它被要求的操作——在这种情况下,就是将所有四台服务器都弄坏。

在这种情况下,将max_fail_percentage参数添加到 playbook 中是个好主意。例如,如果我们将其设置为50,那么当 50%的库存主机失败时,Ansible 将停止处理主机,正如下面的截图所示:

如我们所见,尽管我们的库存没有变化,Ansible 在处理完cluster1cluster2后停止了,因为它们失败了,因此没有对cluster3cluster4执行任何任务;因此,至少有两台主机保持正常工作,允许用户继续使用 Web 应用程序,尽管发生了故障。

在处理大型负载均衡环境时,使用这些 Ansible 功能非常重要,以确保故障不会传播到整个服务器群。以上就是我们对 Ansible 在日常服务器维护中使用的总结——如同往常一样,可能性是无穷无尽的,但希望本章能再次为你提供一些灵感和可供构建的示例。

总结

Ansible 是一个非常强大的工具,但不仅仅用于部署和配置管理。虽然这些是它的核心优势,但在日常管理任务中,它也能提供强大的帮助。正如以往一样,当它与 AWX 或 Ansible Tower 等企业管理工具结合使用时,它在管理你的 Linux 系统中变得非常重要,尤其是在审计和调试方面。

在这一章中,你学会了如何使用 Ansible 清理磁盘空间,并使其具备条件执行功能。你还学会了 Ansible 如何帮助监控配置漂移,甚至警报可能的二进制文件篡改。你了解了如何使用 Ansible 管理远程服务器上的进程,最后,你学会了如何在负载均衡的服务器池中,以优雅和受控的方式执行滚动更新。

在下一章中,我们将探讨如何通过 CIS 基准标准以标准化的方式保护你的 Linux 服务器。

问题

  1. 为什么在检查磁盘空间时,你可能会使用 df 命令的输出,而不是 Ansible 事实?

  2. 哪个 Ansible 模块用于根据给定的标准(例如,文件的年龄)定位文件?

  3. 为什么监控配置漂移很重要?

  4. 在 Ansible 中,有哪两种方式可以监控基于文本的配置文件的变化?

  5. 如何使用 Ansible 管理远程服务器上的 systemd 服务?

  6. Ansible 中的内置过滤功能是什么,它可以帮助处理字符串输出(例如,拆分逗号分隔的列表)?

  7. 如何在 Ansible 变量中拆分一个以逗号分隔的列表?

  8. 在负载均衡环境中,为什么不希望所有任务都在所有服务器上同时执行?

  9. 哪个 Ansible 功能可以防止你将失败的任务推送到所有服务器?

进一步阅读

第四部分:保护您的 Linux 服务器

在本节中,我们将动手操作安全基准,并介绍如何在企业中应用、执行和审计它们的实际例子。

本节包含以下章节:

  • 第十三章,使用 CIS 基准

  • 第十四章,使用 Ansible 进行 CIS 硬化

  • 第十五章,使用 OpenSCAP 审计安全策略

  • 第十六章,技巧与窍门

第十三章:使用 CIS 基准

在企业中实施 Linux 时,安全性至关重要。没有一步可以达到真正安全环境的极乐境界 —— 而是一系列不同步骤的融合,共同构建尽可能安全的环境。事实上,这种说法带出了另一个重要观点 —— 安全性是一个移动的目标。举个例子,SSLv2 曾被认为是安全的,并用于保护互联网上的网站多年。然后在 2016 年发生了 DROWN 攻击,使其不安全。因此,2015 年为互联网流量(也许是前端 Web 服务器)保护的服务器,在当时被认为是安全的。然而,在 2017 年,它被认为是极易受攻击的。

Linux 本身一直被认为是安全的操作系统,尽管其高水平和日益增长的采用率导致攻击不断增加。在本书中,我们在设计 Linux 系统时,高层次上提倡良好的安全实践,例如,在基础操作系统镜像上不安装不必要的服务。尽管如此,我们可以做更多工作,使我们的 Linux 环境更安全,在本章中,我们将探讨已开发的标准以确保 Linux 环境的安全性。具体来说,我们将考虑使用 CIS 基准,并举一些实际示例来应用它们。

具体来说,本章将涵盖以下主题:

  • 理解 CIS 基准

  • 明智地应用安全策略

  • 自动化部署服务器加固脚本

技术要求

本章包括基于以下技术的示例:

  • CentOS 7.6

  • Ansible 2.8

要运行这些示例,您需要访问两台运行前述操作系统的服务器或虚拟机,以及 Ansible。请注意,本章中给出的示例可能具有破坏性(例如,它们会删除文件并对服务器配置进行更改),如果按照示例运行,则仅应在隔离的测试环境中运行。

一旦您确信您有一个安全的操作环境可以操作,请开始使用 Ansible 进行日常系统维护。

本章中讨论的所有示例代码都可以从 GitHub 获取,网址如下:github.com/PacktPublishing/Hands-On-Enterprise-Automation-on-Linux/tree/master/chapter13

理解 CIS 基准

在深入了解 CIS 基准实际包含什么之前,让我们看看它们为何存在,以及它们在概念上是什么。

什么是 CIS 基准?

无论操作系统如何,保护服务器都是一项重大任务。它要求随时跟踪发现的新攻击向量和漏洞(请参阅本章介绍中关于 DROWN 攻击和 SSLv2 的提及)。有些事情是众所周知并被认为是正常的。例如,在 Linux 上,作为 root 登录通常是不被认可的——相反,几乎普遍认为每个用户应该有自己的用户账户,并且应该使用sudo命令执行所有需要提升权限的命令。因此,一些 Linux 发行版如 Ubuntu 默认情况下禁用了远程 root 访问。而其他一些,如 CentOS,则没有。即使在企业中广泛使用的这两个关键发行版之间,您也知道,对于其中一个,您需要积极关闭远程 root SSH 访问,而对于另一个,则只需检查是否已关闭。

当然,定义安全策略远不止于是否允许通过 SSH 获取 root 访问权限。多年来,个人积累了关于什么有效的知识,也许是通过艰难的方式,了解了什么是无效的。然而,您的环境安全性不应由系统管理员的经验决定。相反,应该制定一些定义良好的标准,以最佳方式保护服务器,以防止大多数常见攻击,并确保在需要审计以查找事件根本原因时记录适当级别的信息。

这就是 CIS 基准概念的所在。许多人熟悉基准的概念作为性能测试(即速度)。然而,您的服务器是否安全可以通过查找特定标准进行测试,因此 CIS 基准存在。直接引用来自互联网安全社区CIS简称)网站的内容:

"CIS 基准是通过一个由全球网络安全专业人士和主题专家组成的独特共识过程来开发的。"

因此,这些基准可以被视为行业专业人员最佳实践的综合。此外,它们定期更新,因此可以被工程师和管理员用来了解保护服务器的最佳实践。

当然,需要注意的是,存在比 CIS 基准更深入的其他安全标准,例如 FedRAMP 和 NSA 安全要求。在本书中,不可能详细讨论所有不同的配置文件,因此我们将重点放在 CIS 基准上,该基准是免费提供的(交换一些个人信息)并且也备受推崇。

本书专注于 CIS 基准规范,并不意味着您应该在服务器基础设施上实施这些规范以确保其安全。每位读者都有责任确保他们理解自己的安全需求,并相应地实施正确的安全措施。在本章中,我们通过 CIS 基准规范作为服务器硬化的工作示例来进行讨论。

还值得注意的是,CIS 基准规范是按技术分割的。例如,有适用于 Red Hat Enterprise Linux 7 和 Ubuntu Server 的 CIS 基准规范,您可以将其应用于您的企业 Linux 系统。然而,这些规范侧重于保护基本操作系统,如果在其上安装了应用层,则必须同样应用适当的安全策略。

CIS 基准规范涵盖了超过 140 种技术,包括常见的 Linux 服务,如 nginx、Apache 和 PostgreSQL。因此,如果您正在构建一个面向互联网的 Web 服务器,应用操作系统基准规范和所选 Web 服务器的适当规范是有意义的。

如果您有一个定制的应用层,或者只是在使用 CIS 网站未列出的技术,请不要绝望——请使用适当的基准规范保护底层操作系统,然后尽可能以最佳方式应用安全实践。互联网上通常会有很好的建议,但弄清楚这一点超出了本书的范围。

可在此处找到所有有关 CIS 基准规范的技术的完整列表:www.cisecurity.org/cis-benchmarks/

一旦您获得了所选操作系统的安全基准规范,就该考虑其应用了。然而,在这一步之前,在本章的下一节中,我们将更详细地探讨 Linux 操作系统的 CIS 基准规范的内容。

深入探讨 CIS 基准规范

让我们通过查看 RHEL 7 的 CIS 基准规范来深入探讨 CIS 基准规范的实际示例。在撰写本文时,此版本为 2.2.0 版本,共有 386 页!因此,我们立即可以看出,实施此基准规范不太可能是一个简单的活动。

当你浏览文档时,你会发现我们最感兴趣的部分——建议部分——被划分为几个子部分。每个部分都专注于操作系统中的特定安全领域。写作时,第一部分完全是关于操作系统的初始设置;在构建时可能会应用的参数和配置。第二部分则完全是关于保护可能默认安装在 RHEL 7 服务器上的常见服务。第三部分处理网络配置,而第四部分详细讲解了日志记录和审计日志设置,确保你在日常使用中捕捉到足够的数据。这是为了确保你可以审计服务器,并在不幸遭遇安全漏洞或故障时查明发生了什么。第五部分考虑了对服务器的访问和认证(这里你会找到提到 SSH 服务器安全的内容——实际上,你会看到禁用远程 root 登录的示例在文档版本 2.2.0 的基准 5.2.8 中)。最后,第六部分标题为系统维护,旨在定期运行,而不是仅运行一次,以确保系统的完整性。

当然,我们在本书中之前讨论过,任何拥有 root 权限的人都可以更改核心系统配置,因此建议定期运行(或至少检查)所有基准,以确保符合原始政策。

我们将在本书接下来的两章中探讨这个话题;然而,目前让我们先回到对 CIS 基准本身的进一步理解。当你查看每个建议时,你会注意到每个建议都有一个关联的等级,并且它们要么是已评分,要么是未评分(这一点会在每个基准的标题中说明)。

这些基准旨在作为合规检查的一部分,贡献于系统的最终报告或评分——而已评分的建议实际上会影响最终得分。因此,如果你的系统符合检查要求,最终得分会提高——然而,如果未符合要求,最终得分则会下降。那些标记为未评分的建议对最终得分没有任何影响。换句话说,未实施它们不会导致扣分。

当然,这并不意味着它们在考虑时不重要。举个例子,考虑一下版本 2.2.0 RHEL 7 基准的 3.7 条,它的标题是确保禁用无线接口。每个基准的背后都有一个理由,这个基准的理由如下:

“如果无线不打算使用,可以禁用无线设备,以减少潜在的攻击面。”

这是一个逻辑上的方法 — 我们知道如果您的设备有无线接口,则应禁用它,除非在使用中。此外,无线安全协议在历史上一直存在漏洞,就像 SSLv2 一样,因此从长远来看,无线网络通信可能不被认为是真正安全的。尽管如此,在运行 RHEL 7 的企业笔记本电脑上,您不能保证它将连接到有线网络连接。无线网络可能是唯一的选择,在这种情况下,您需要将其保持打开。

当然,CIS Benchmark 不能替你做出这个决定 — 只有你才知道你的系统是否需要启用其无线网络适配器(如果有的话),因此这是一个非计分项目是合理的。

相比之下,我们的老朋友 Benchmark 5.2.8(禁用远程 root SSH 访问)被评分,因为在企业环境中启用此项没有合理理由。因此,如果不能达到这个 Benchmark,我们期望我们的系统评分会降低。

每个 Benchmark 都详细说明了如何测试所述条件或配置的存在,以及如何应用所需的配置。

此外,您还会注意到每个 Benchmark 都与一个级别相关联,可以是 1 或 2。在每种情况下,对于 RHEL 7,您将看到这些级别适用于两种不同的情景 — RHEL 7 作为服务器和工作站的使用。再次深入探讨这些级别的含义时,这是有道理的。

第一级别旨在成为您应用于环境中的合理安全基线,以减少攻击面。它不打算对您的 Linux 环境的日常业务使用产生广泛影响,因此第一级别的 Benchmarks 是较不侵入性的实施方式。

相比之下,第二级别的 Benchmarks 提供了更严格的安全级别,极有可能对您环境的日常使用产生影响。

如果我们再次看 Benchmark 3.7,我们会看到它被分类为服务器的第一级别和工作站的第二级别。这是有道理的 — 服务器不太可能有无线网络适配器,即使有,也很少使用,因此禁用它对服务器的日常使用几乎没有影响。然而,如果在 RHEL 7 笔记本电脑上实施 Benchmark 3.7,那么它的移动性就会大大降低,因此第二级别的分类提醒了我们这一点。想象一下拥有一台笔记本电脑却无法在无线网络上使用 — 对许多人来说,这在今天的时代是不可行的概念!

Benchmark 5.2.8 被认为是服务器和工作站的第一级别,因为通常情况下不建议在日常操作中使用 root 账户 — 因此,在 SSH 上禁用对其的访问不应对日常运行产生任何影响。

在理想的情况下,你应该在应用基准之前阅读并理解所有基准,以免其影响你的操作方式 —— 例如,我仍然发现一些系统在脚本操作时使用 root 帐户进行 SSH 访问,虽然我的第一任务通常是纠正这一点,但如果我盲目地将 CIS 基准应用到这些系统上,我将破坏一个原本工作正常的设置。

然而,承认管理企业 Linux 环境的任何人都非常忙,你可能会原谅认为可以简单地将得分级别 1 的基准应用到你的系统上。确实,这会给你一个合理的安全基线,同时带来相对较低的风险 —— 但是彻底细致起见是无法替代的。在本章的下一节中,我们将更详细地探讨如何明智地选择基准,而不会在你的环境中引发问题!

明智地应用安全策略

正如我们在前一节中开始探讨的,每个 CIS 基准都有与之相关的级别和评分。级别对我们特别关注,因为虽然我们希望尽可能有效地保护我们的系统,但我们不希望破坏任何正在运行的系统。因此,强烈建议在隔离的测试环境中应用基准并测试你的应用程序,然后再将其部署到生产环境中。确实,如果应用某个基准导致某个系统出现故障,企业应执行以下流程来解决问题:

  1. 确定哪个基准引起了问题。

  2. 确定哪些内部系统受基准影响。

  3. 决定是否可以修改内部系统以与基准配合工作(例如,使用 SSH 的非特权帐户而不是 root)。

  4. 实施内部系统的更改,并普遍应用基准,或者(只有在有充分理由时)为该基准提出例外并记录下来。

CIS 基准甚至可能破坏你的 Ansible 自动化 —— 最简单的例子是,你正在使用 root 帐户执行你的自动化任务,并在 CIS 基准部署过程中禁用此帐户。在这种情况下,你会发现 Ansible 无法访问你的所有系统,甚至最糟糕的情况是,你将不得不手动修改每台服务器以恢复 Ansible 的访问。

虽然我们不能在本章逐个审查基准,但在以下小节中,我们将探讨一些相关示例以供注意。希望这能为你提供足够的信息,以审查适合你选择的 Linux 版本的基准,然后就你环境中安全策略的最佳利益做出知情决策。

我们将继续使用 RHEL 7 基准版本 2.2.0 的示例。然而,这里描述的大部分内容也适用于其他 Linux 平台。配置文件路径甚至日志文件路径可能会有所不同,但这些将在适合您操作系统的相关 CIS 基准中详细说明,请务必下载最适合您的基准。

现在我们已经考虑了安全策略应用的总体原则,我们将在下一节从 SELinux 策略的具体示例开始深入讨论。

应用 SELinux 安全策略

RHEL 7 基准的 1.6.1 节涉及 SELinux 的实施,并包括检查以确保 SELinux 处于强制模式,而不是在某个级别上被禁用。您将注意到,这些检查都是 2 级基准,这意味着它们可能会破坏现有系统。

在支持的操作系统上启用和应用 SELinux 是一个非常好的主意,但即使在撰写本文时,仍有许多 Linux 应用程序不支持它,并且其安装说明中指出必须禁用 SELinux 才能使应用程序正常工作。这显然并不理想,相反,您应该创建一个 SELinux 策略,允许您的应用堆栈在不需要禁用 SELinux 的情况下工作。

并非所有企业都有完成这项工作所需的技能和时间,因此对这一组基准需要仔细考虑 —— 简而言之,如果可能的话应用它,但可能需要做一些例外。

如果您使用的是 Ubuntu,则应该将相同的逻辑应用于默认情况下在 Ubuntu Server 上启用的 AppArmor。

在本章的下一节中,我们将看一下 CIS 基准如何影响 Linux 上文件系统的挂载方式。

文件系统的挂载

在 Linux 中,所有文件系统在使用之前必须被挂载 —— 这简单来说是将诸如磁盘分区之类的块设备映射到路径上。对于大多数用户来说,这是透明的,并且发生在启动时,但对于那些负责配置系统的人来说,这需要一些注意。例如,/tmp 文件系统通常对所有用户可写,因此不希望让人们在此目录中执行文件,因为他们可以将任意二进制文件放在那里,自己或他人都可以运行。因此,通常使用 noexec 标志挂载此文件系统来达到这个目的。

在已部署的机器上更改分区的挂载选项(甚至分区结构)可能会有问题。此外,许多云平台具有平坦的文件系统结构,因此 /tmp 的前述示例可能无法实现,因为它无法与 root 分区分开挂载。因此,建议您将此部分 CIS 基准纳入到您的服务器(或映像)构建过程中,并根据需要为公共云平台创建排除项。

CIS 基准测试中第 1.1 节(标题为文件系统配置)中的基准正是关注这些细节,且这些基准需要根据你的环境进行调整。例如,基准 1.1.1.8 建议禁用挂载 FAT 文件系统的能力,第 1.1.5 节则建议禁用在/tmp目录下执行二进制文件,如前所述。这些都是评分基准,目前,几乎不需要在/tmp目录下使用或挂载 FAT 卷或执行文件。然而,在某些遗留环境中,这仍然可能是必要的,因此在应用这些设置时需要小心。

同样,关于为重要路径(如/tmp/var)设置独立的文件系统,以及特别的挂载选项,也有很多建议。这些方法在很多情况下是有效的,但再次强调,直接声明这些方法对所有环境都有效是过于冒险的,尤其是在已有环境中,因此这些方法应该在了解环境要求的基础上应用。

在了解 CIS 基准对文件系统挂载的影响之后,我们将继续讨论使用文件校验和进行入侵检测的建议。

安装高级入侵检测环境(AIDE)

基准 1.3.1 涉及安装高级入侵检测环境AIDE)——这是一个现代化的替代工具,取代了久负盛名的Tripwire,可以扫描文件系统并校验所有文件,从而提供一种可靠的方法来检测文件系统的修改。

从表面上看,安装和使用 AIDE 是个非常好的主意——然而,如果你有一个包含 100 台机器的环境,并且对所有机器进行更新,你将收到 100 份报告,每份报告中都包含大量的文件变更细节。对此问题有其他解决方案,包括开源的 OSSEC 项目(www.ossec.net/),但这并不是 CIS 基准测试的一部分,因此需要你决定哪种解决方案最适合你的企业。

当然,这并不是说 AIDE 不应该使用——恰恰相反。更准确地说,是如果你选择使用 AIDE,确保你有相应的流程来处理和理解报告,并确保你能区分误报(例如,由于软件包更新而导致的二进制文件校验和变化)与真正恶意且意外的修改(例如,即使没有进行软件包更新,/bin/ls却发生了变化)。

在查看 AIDE 是否适合安装在你的 Linux 基础设施上之后,我们将继续讨论 CIS 基准对服务启动时默认配置的影响。

理解 CIS 服务基准

基准的第 2.2 节详细列出了围绕需禁用服务的若干评分为 1 级的基准。再次强调,背后的理由是攻击面应该最小化,因此,例如,除非服务器是作为网站服务器使用的,否则httpd不应运行。

尽管这一点本身合乎逻辑,但回顾这一部分时,会发现许多服务可能对你的环境至关重要,包括squidhttpdsnmpd。对于所有这些基准,只有在有意义的情况下才能应用。你不会在网站服务器上关闭 Apache,也不会在代理服务器上禁用squid

然而,关于这些基准,在应用时有很好的指导,特别是对于snmpd,如果你的环境依赖于此进行监控,甚至有关于如何保护该服务的指导。

X Windows

基准 2.2.2 进一步确保 X Windows 服务器从你的系统中实际卸载。大多数服务器都是无头的,因此可以做到这一点——但是,你不会对工作站或执行远程桌面功能的系统进行此操作。

一定要将这个基准应用到你的服务器上,但只有在你确认应用它是安全的情况下才进行。

按网络允许主机

基准 3.4.2 和 3.4.3 确保/etc/hosts.allow/etc/hosts.deny已正确配置——这意味着,对于所有处理这两个文件的服务,只有来自允许网络的连接会被处理。

这通常是个好主意——然而,许多组织拥有良好的防火墙,甚至有些组织的政策是不允许在服务器上使用本地防火墙,因为这会使调试过程更加复杂。如果连接被拒绝,防火墙越多,你就越需要检查,才能找出被拒绝的地方。

因此,建议你根据公司的安全政策来应用这两个基准。

本地防火墙

对于第 3.6 节中有关安装和配置 iptables 的基准也适用同样的原则。尽管这个本地防火墙增加了服务器的安全性,但它与许多公司安全政策相冲突,后者倾向于使用更少、更集中化的防火墙,而不是多个本地防火墙。请根据公司政策应用这些基准。

评分的总体指导

你会注意到,我建议你在应用时要小心的许多基准实际上都有评分。这引出了关于评分的更广泛的观点——应用 CIS 基准的目的不是为了获得 100%的分数。相反,它是为了获得最适合你环境的最高分数,从而使你的企业能够正常运行。

评分应当用来建立你自己的基准——一旦你按照本章讨论的方式完成了所有基准的工作,你将知道哪些适合你的企业,从而确定你的目标分数。

通过对基准反复应用的结果进行审计,可以进行多次评分练习,以跟踪环境的整体合规性和随时间推移的偏离情况。例如,如果反复审计显示分数逐渐下降,那么你就知道在合规性方面存在问题,必须找出根本原因——无论是用户对系统进行未经授权的更改,还是推出了未正确加固的新服务器。

无论哪种方式,你的 CIS 基准分数将成为监控你的 Linux 系统与安全策略合规性的重要工具。在本章的下一部分,我们将探讨如何使用脚本化的方法来应用和确保 CIS 基准的合规性。

服务器加固的脚本化部署

我们花了一些时间探索 CIS 基准及其预期的工作方式。现在,让我们将注意力转向更实际的问题——如何审计它们以及如何实施它们。在本书中,我们重点关注 Ansible 作为自动化这些任务的工具,事实上,Ansible 是一个非常适合这个目的的优秀解决方案。话虽如此,当然你已经注意到,CIS 基准文档中的示例通常是 shell 命令,或者在某些情况下,仅仅是关于应当存在(或不存在)于某个文件中的配置行的说明。

为了清晰地解释如何在 Linux 系统上审计和实施 CIS 基准,我将示例分为两部分。在本章的这一部分,我们将开发传统的 shell 脚本,用于检查是否符合 CIS 基准,并在需要时实施相关建议。这将与 CIS 基准文档本身非常相似,从而有助于我们理解如何实施它们。接下来,在下一章,我们将把这些基于 shell 脚本的示例发展成 Ansible 角色,以便我们可以使用自己喜欢的自动化工具来管理 CIS 基准的合规性。

让我们通过一些示例来演示如何开发这些脚本,从我们的 SSH 根登录示例开始。

确保禁用 SSH 根登录

RHEL 7 基准版本 2.2.0 中的 CIS 建议 5.2.8 指出,我们应当禁用远程 root 登录。我们在其他情境中已经讨论过这个例子,下面我们将特别查看 CIS 基准文档中的建议,以帮助我们理解应该如何实施这一建议。

文档中指出,为了审计这一要求(并因此对这一项进行评分),应观察以下测试结果:

# grep "^PermitRootLogin" /etc/ssh/sshd_config 
PermitRootLogin no

请注意,命令是供人类解释其输出的——该命令将返回该文件中的PermitRootLogin行,无论它是启用还是禁用。文本显示了所需的输出,但假设运行测试的人会读取输出并检查是否启用——这种方式在小规模上是可行的,但不适合自动化使用。建议的修复方法是编辑/etc/ssh/sshd_config,设置以下参数:

PermitRootLogin no

到目前为止,一切顺利——CIS 基准文档描述得相当清晰,甚至为我们的编码提供了一个良好的开端。然而,正如之前所述,这些代码片段实际上并没有帮助我们以自动化的方式检查或实施该推荐。

假设我们想使用 shell 脚本进行此条件的审核。在这种情况下,我们需要运行基准文档中提到的grep命令,但使用更精确的模式,以确保我们仅在PermitRootLogin行被设置为no时才匹配该行。然后我们会检查所需的输出,并根据检查结果通过echo输出适当的消息到控制台。这个脚本可能看起来是这样的(请注意,在 shell 脚本中有多种方式可以实现相同的最终结果!):

#!/bin/sh
#
# This file implements CIS Red Hat Enterprise Linux 7 Benchmark
# Recommendation 5.2.8 from version 2.2.0
echo -n "Ensure root logins are disabled on SSH... "
OUTPUT=$(grep "^PermitRootLogin no" /etc/ssh/sshd_config)
if [ "x$OUTPUT" == "x" ]; then
  echo FAILED!
else
  echo OK
fi

对于熟悉 shell 脚本的人来说,脚本相当简单,但简而言之,步骤如下:

  1. 我们在文件顶部加入了一些有用的文档注释,以便我们知道正在测试哪个推荐。请注意,推荐编号可能会在文档版本之间发生变化,因此记录这两个编号非常重要。

  2. 我们通过echo输出一行关于正在运行的测试的说明文本。

  3. 然后,运行 CIS 基准中建议的审核命令,不过这次我们要检查是否存在PermitRootLogin no行。输出将被捕获到OUTPUT变量中。

  4. 如果OUTPUT的内容为空,则说明我们检查的行在文件中不存在,测试被认为是失败的。我们可以放心地假设这一点,因为默认情况下 OpenSSH 服务器启用了 root 登录,因此如果配置文件中缺少此行,且假设我们的grep模式没有问题,那么 root 登录已启用。我们会将这个信息通过echo输出到终端,让用户知道需要采取行动。

  5. OUTPUT变量应该包含文本的唯一条件是grep命令找到了所需的模式。如果满足这一条件,那么我们会输出一条不同的消息,告知用户测试通过,并且不需要进一步的操作。

让我们看看这个脚本的实际操作,以及手动尝试修复问题:

在这里,我们可以看到一个典型的手动过程,许多系统管理员和工程师在管理其系统时都会遇到这个过程。我们运行了之前定义的检查脚本,结果返回了FAILED!。因此,我们的第一步是查看配置文件,看看为什么测试失败。造成这种结果的原因可能有两种——要么是包含PermitRootLogin的行根本不存在,要么是该行被注释掉了。在这种情况下,前者被证明是正确的。

如果该行已经存在,但被注释掉了,我们可以使用sed(或其他内联编辑工具)取消注释该行,并将参数设置为no。然而,由于该行并不存在,我们需要将该行添加到文件中,这在前一个截图中已经使用tee -a命令完成。请注意,这需要与sudo一起使用,因为只有root用户才能写入此文件。然后我们再次运行测试,测试通过了。当然,你会注意到,也完全可以直接用vim(或你喜欢的编辑器)打开这个文件,手动修复问题;然而,前面的示例可以为脚本化解决方案提供支持。

从前面的例子来看,这是一个极其缓慢且手动的过程。如果在单一服务器(例如模板镜像)上执行这个过程已经够糟糕了,想象一下要在整个 Linux 服务器系统中扩展这个过程,再加上所有 CIS 基准文档中的推荐项。这项任务将成为某个人的全职(且非常繁琐)的工作。

更好的做法是自动化这个过程,你会注意到,在 CIS 基准文档中,不仅有审计服务器上推荐设置的测试用例,还有推荐的修改内容。在大多数情况下,这只是说明在给定的配置文件中应该存在哪些行。 在这种情况下,我们要确保以下内容:

PermitRootLogin no

如果我们尝试通过进一步开发 Shell 脚本来解决这个问题,当测试结果为FAILED!时,我们需要执行以下步骤(对于OK结果,不需要进一步操作):

  1. 由于我们未能在文件中匹配到所需的模式,我们知道该行要么存在,但设置错误,要么根本不存在(可能是缺失或被注释掉)。我们可以忽略后两种可能性之间的区别,因为保留被注释掉的行并添加正确的行是不会造成任何问题的。因此,我们的第一项任务是测试PermitRootLogin行是否存在,无论它的设置是什么:
  OPTPRESENT=$(grep -e "^PermitRootLogin.*" /etc/ssh/sshd_config)
  if [ "x$OPTPRESENT" == "x" ]; then
  ...
  else
  ...
  fi
  1. 在前一个截图中,我们正在寻找配置文件中以PermitRootLogin开头的任何一行。如果没有返回任何内容(我们的正面测试案例),那么我们就知道必须通过在if语句下方直接添加以下内容来将该行添加到文件中:
    echo "Configuration not present - attempting to add"
    echo "PermitRootLogin no" | sudo tee -a /etc/ssh/sshd_config 1>/dev/null
  1. 到目前为止,一切顺利。然而,如果我们的grep命令确实返回了一些输出,我们就知道该行存在且值不正确,因此我们可以使用像sed这样的工具来就地修改该行:
    echo "Configuration present - attempting to modify"
    sudo sed -i 's/^PermitRootLogin.*/PermitRootLogin no/g' /etc/ssh/sshd_config
  1. 当我们修改了文件(无论采用哪种方式)后,我们知道必须重启sshd才能使更改生效。因此,在内层if结构的fi语句结束处,我们添加如下内容:
  sudo systemctl restart sshd
  1. 当我们在 SSH 配置中运行此命令且该设置不存在时,我们会看到以下行为——请注意,第二次运行脚本时,显示修改已成功:

  1. 类似地,如果我们运行它,且该行存在但不符合 CIS 基准,我们会看到以下情况:

这非常棒——我们刚刚使用了 shell 脚本来自动化 CIS 基准文档中的一项建议。然而,你会注意到我们开发的 shell 脚本包含了很多重复的部分,其他人要理解起来会有一定难度。

此外,这项建议是比较简单的——在这种情况下,只有一个文件中的一行需要修改。如果建议内容更为复杂呢?我们在下一节中看看这个问题。

确保禁用数据包重定向发送

版本 2.2.0 的 RHEL 基准中的推荐 3.1.2 稍微详细一些——这是一个评分为级别 1 的基准,确保你的服务器不会向其他主机发送路由信息。除非它们被配置为路由器,否则没有合理的理由这么做。

从文档本身可以看到,推荐的审计命令(及其结果)如下:

$ sysctl net.ipv4.conf.all.send_redirects 
net.ipv4.conf.all.send_redirects = 0
$ sysctl net.ipv4.conf.default.send_redirects 
net.ipv4.conf.default.send_redirects = 0
$ grep "net\.ipv4\.conf\.all\.send_redirects" /etc/sysctl.conf /etc/sysctl.d/*
net.ipv4.conf.all.send_redirects = 0
$ grep "net\.ipv4\.conf\.default\.send_redirects" /etc/sysctl.conf /etc/sysctl.d/*
net.ipv4.conf.default.send_redirects= 0

要运行的命令以$字符开始,而期望的结果显示在下一行。我们已经可以看到,将其开发成一个 shell 脚本需要一些工作——我们需要验证两个sysctl命令的输出,然后还需要检查配置文件,以确保这些参数在重启和内核参数重新加载时能保持不变。

我们可以通过一些 shell 代码轻松检查当前的内核参数设置,例如:

echo -n "Ensure net.ipv4.conf.all.send_redirects = 0... "
OUTPUT=$(sysctl net.ipv4.conf.all.send_redirects | grep "net.ipv4.conf.all.send_redirects = 0" 2> /dev/null)
if [ "x$OUTPUT" == "x" ]; then
    echo FAILED!
  else
    echo OK
fi

你会注意到,代码结构几乎与我们用于检查 SSH 中PermitRootLogin参数的代码相同——因此,尽管自动化审计过程的代码变得更容易,但它也变得高度重复且低效。类似的代码块将用于检查net.ipv4.conf.default.send_redirects参数的值。

我们还可以检查这些参数的持久性配置,同样通过将 CIS 基准文档中的审计命令构建成类似我们之前做的条件结构:

echo -n "Ensure net.ipv4.conf.all.send_redirects = 0 in persistent configuration..."
OUTPUT=$(grep -e "^net\.ipv4\.conf\.all\.send_redirects = 0" /etc/sysctl.conf /etc/sysctl.d/*)
if [ "x$OUTPUT" == "x" ]; then
    echo FAILED!
  else
    echo OK
fi

我们将再次复制这个代码块来处理net.ipv4.conf.default.send_redirects参数。再次,我们成功构建了一个脚本来审计这个基准——在我们的系统上运行它大致是这样的:

这是一段 35 行的 shell 脚本(虽然文件顶部有一些注释),其中许多部分是重复的,所有这些只是为了确认我们完全未能满足这个要求!再一次,如果我们要扩展这个示例以解决问题,我们需要扩展我们的脚本。

设置活动内核参数相对简单——我们只需要将一系列命令,例如以下命令,添加到第一个if语句的FAILED!分支中:

    echo "Attempting to modify active kernel parameters"
    sudo sysctl -w net.ipv4.conf.all.send_redirects=0
    sudo sysctl -w net.ipv4.route.flush=1

我们可以在适当的地方为net.ipv4.conf.default.send_redirects添加类似的内容。

然而,对于我们的持久化参数,事情变得有些复杂——我们需要处理两种可能的配置文件情景,类似于PermitRootLogin示例,但现在我们有了一个由多个文件组成的配置,我们必须选择哪个文件进行修改,如果该参数不存在的话。

因此,再一次,我们必须构建一块代码来处理这两种不同的情况:

    OPTPRESENT=$(grep -e "^net\.ipv4\.conf\.all\.send_redirects" /etc/sysctl.conf /etc/sysctl.d/*)
    if [ "x$OPTPRESENT" == "x" ] ; then
      echo "Line not present - attempting to append configuration"
      echo "net.ipv4.conf.all.send_redirects = 0" | sudo tee -a /etc/sysctl.conf 1>/dev/null
    else
      echo "Line present - attempting to modify"
      sudo sed -i -r 's/^net\.ipv4\.conf\.all\.send_redirects.*/net.ipv4.conf.all.send_redirects = 0/g' /etc/sysctl.conf /etc/sysctl.d/*
    fi

这是一段相当丑陋且难以阅读的代码。它的作用如下:

  1. 它会针对已知的配置文件运行第二次grep,查看该参数是否存在,无论它的值是什么。

  2. 如果参数未设置,那么我们选择将其附加到/etc/sysctl.conf文件中。

  3. 如果参数已设置,我们改用sed来修改该参数,强制其值为0

现在,当我们像之前一样运行这个脚本时,我们得到如下结果:

如我们所见,这个方法运行得很好;然而,我们现在已经有了 57 行 shell 代码,其中许多部分开始变得相当难以阅读。所有这些代码只是为了设置两个内核参数,尽管我们已经构建了一个相当稳固的代码库来执行 CIS 基准(以及它们推荐的审计和修复步骤),但它的可扩展性很差。

此外,在前面的示例中,这些脚本都是本地运行的——如果我们想从中央位置运行它们呢?在下一节中,我们将详细讨论这一点。

从远程位置运行 CIS 基准脚本

shell 脚本的挑战在于,虽然它在脚本所在的机器上运行很容易,但在远程机器上运行却稍显困难。

我们之前开发的脚本是设计为从非特权账户运行的——因此,我们在需要 root 权限执行的步骤上使用了sudo。当你设置了无密码 sudo 访问时,这没问题,但当使用sudo时需要密码来提升权限,这就进一步增加了远程运行脚本的难度。

当然,整个脚本也可以作为 root 用户运行,根据你的使用场景和安全需求,这可能是可取的,也可能不是。让我们看看如何在名为centos-testhost的远程系统上运行我们的重定向发送示例。为此,我们需要做以下几步:

  1. 通过 SSH 登录远程系统并进行身份验证——这可以通过密码或先前设置的 SSH 密钥来完成。

  2. 调用执行我们开发的脚本所需的 shell——在我们的示例中,这是/bin/bash

  3. 我们向bash命令添加了-s标志——这会导致 shell 从标准输入读取其命令(即,命令可以通过管道传输给它)。

  4. 最后,我们将脚本传输给bash

这种方法还有一个附加的警告——在我们的脚本中,我们大胆地假设我们依赖的命令(如sysctl)存在于 PATH 变量定义的某个目录中。有人可能会认为这种做法有缺陷——然而,这也可以使脚本开发更加便捷,特别是当编写的脚本可能在跨平台环境中使用时。

例如,尽管我们在本章中一直专注于 RHEL 7 CIS 基准,但可以合理推测,Ubuntu Server 也希望禁用 SSH 根登录,并且除非显式配置为路由器,否则不会发送数据包重定向信息。因此,我们可以合理地期望到目前为止开发的脚本能够在这两种系统上运行,并为我们节省一些开发工作。

然而,在 RHEL 7(和 CentOS 7)中,sysctl命令位于/usr/sbin/sysctl,而在 Ubuntu 中则位于/sbin/sysctl。这种差异本身可以通过在脚本顶部定义sysctl的路径变量来处理,然后通过此变量调用它——但是,即便如此,这也意味着需要修改许多与 CIS 强化相关的脚本,像这样:

# RHEL 7 systems
SYSCTL=/usr/sbin/sysctl
$SYSCTL -w net.ipv4.conf.all.send_redirects=0

# Ubuntu systems
SYSCTL=/sbin/sysctl
$SYSCTL -w net.ipv4.conf.all.send_redirects=0

简而言之,这比我们最初的方法更好,但仍然非常手动且杂乱。回到远程运行现有脚本的任务,结合我们所有的需求,我们可能会使用以下命令来运行它:

$ ssh centos-testhost 'PATH=$PATH:/usr/sbin /bin/bash -s' < cis_v2.2.0_recommendation_3.1.2.sh

上述命令假设我们以当前用户身份在本地系统上运行脚本——我们可以通过在主机名之前明确指定用户来实现:

$ ssh james@centos-testhost 'PATH=$PATH:/usr/sbin /bin/bash -s' < cis_v2.2.0_recommendation_3.1.2.sh

在我们的远程系统上运行此命令(包括第二次运行以确保修改生效)看起来可能像这样:

我们可以看到,这对我们的远程系统有效,并且无需对原始脚本进行修改。尽管这一方法非常有效,但与我们使用 Ansible 的经验相比,它显得有些低效和繁琐。事实上,可以公平地说,这些示例展示了 Ansible 在自动化基本系统管理任务中的价值。为了进一步发展这一点,在下一章中,我们将通过开发 Ansible playbook 来执行所需的任务,从而在 CIS 基准的基础上构建。

总结

在今天这个高度互联的世界中,系统安全至关重要,尽管 Linux 长期以来被认为是一个安全的操作系统,但仍有许多方法可以进一步增强其安全性。CIS 基准提供了这样一种标准化的方法,通过汇集技术行业中的安全最佳实践共识,来提升系统安全性。然而,CIS 基准非常庞大,如果手动应用,工程师在单一系统上实施时可能需要花费数小时。因此,自动化其部署变得至关重要。

在本章中,你已经学习了 CIS 基准,了解了它们的用途及带来的好处。接着,你学习了安全与应用支持之间的平衡,以及在应用服务器强化策略时如何做出明智决策。你还学习了如何利用 Shell 脚本在 Linux 服务器上应用一些示例安全策略。

在下一章,我们将进一步发展这一概念,通过展示使用 Ansible 自动化部署 CIS 基准推荐的方法。

问题

  1. 为什么 CIS 基准对于确保 Linux 服务器安全性至关重要?

  2. 如果你使用适当的基准来保护 Ubuntu Server,然后在该服务器上安装 nginx,是否还需要对 nginx 进行强化?

  3. Level 1 和 Level 2 基准之间有什么区别?

  4. 为什么有些基准有评分,而有些没有?

  5. 如何使用 Shell 脚本检查是否满足特定的审计要求?

  6. 列举三种可能与使用 Shell 脚本自动修改配置文件相关的问题。

  7. 为什么 Shell 脚本不适合用于 CIS 基准的自动化部署?

  8. 如何通过 SSH 在远程服务器上运行 CIS 基准 Shell 脚本?

  9. 为什么要使用变量来指定用于执行 CIS 推荐的二进制文件的路径?

  10. 为什么你会在脚本中对单个命令使用 sudo,而不是让整个脚本以 root 用户身份运行?

进一步阅读

第十四章:使用 Ansible 进行 CIS 加固

在第十三章中,使用 CIS 基准,我们详细探讨了 CIS 基准的概念,它们如何在企业中提升 Linux 安全性,以及如何应用它们。我们还详细审查了 CIS 加固基准的一个示例,即 Red Hat Enterprise Linux(和 CentOS)7 的基准。尽管我们得出结论,基准文档提供了大量关于验证检查的详细信息,甚至包括如何实施基准,但我们也看到整个过程极其手动。此外,对于单一操作系统基准,近 400 页的详细内容,我们认为工程师在仅为一台服务器实施该基准时,所需的工作量将是巨大的。

在本章中,我们将再次考虑 Ansible。我们已经确认 Ansible 非常适合在企业规模上实现自动化,实施 CIS 基准也不例外。在本章中,我们将学习如何用 Ansible 重写 CIS 基准,然后如何在企业规模上应用它们,甚至如何持续监督 Linux 服务器对这些基准的合规性。通过这样做,我们将开发一种高度可扩展、可重复的方法,在企业中以可管理、可重复、可靠且安全的方式实现安全基准——这正是企业自动化有效性的标志。

本章将涵盖以下主题:

  • 编写 Ansible 安全策略

  • 使用 Ansible 应用企业范围的策略

  • 使用 Ansible 测试安全策略

技术要求

本章包含基于以下技术的示例:

  • CentOS 7.6

  • Ansible 2.8

要运行这些示例,您需要访问一台运行前述操作系统和 Ansible 的服务器或虚拟机。请注意,本章中的示例可能具有破坏性(例如,它们会删除文件并修改服务器配置),如果按示例运行,只应在隔离的测试环境中执行。

一旦您确认您拥有一个安全的操作环境,就可以开始使用 Ansible 进行常规系统维护。

本章中讨论的所有示例代码都可以在 GitHub 上找到,网址为:github.com/PacktPublishing/Hands-On-Enterprise-Automation-on-Linux/tree/master/chapter14

编写 Ansible 安全策略

在第十三章《使用 CIS 基准》中,我们探索了 Red Hat Enterprise Linux 7(版本 2.2.0)的 CIS 基准,并详细查看了文档和实现技术。虽然在本书中我们重点关注了企业中常见的两种操作系统——Ubuntu Server LTS 和 RHEL/CentOS 7,但在上一章中,我们仅专注于 RHEL 7 的 CIS 基准。这是出于简化考虑,因为许多适用于 RHEL 7 的良好安全实践同样适用于 Ubuntu Server LTS。例如,两个系统都不应启用 root SSH 登录,且除非它们的角色核心需要,否则也不应启用数据包重定向发送。

在本章中,我们将继续开发基于 RHEL 7 的示例。请注意,本章中用于通过 Ansible 自动化实现此基准的大多数技术,同样适用于 Ubuntu Server LTS,因此希望您从本章中获得的知识能帮助您在实施安全基准时,特别是在 Ubuntu 或其他适用的 Linux 服务器上应用这些技术。

让我们直接进入一些实际操作的示例,开发 CIS 基准实现,这一次我们将使用 Ansible 而不是基于 CIS 基准文档中的示例代码的 shell 脚本。

让我们从考虑我们老朋友——远程 root 登录开始。

确保禁用远程 root 登录

在上一章中,我们编写了以下 shell 脚本,以测试 CIS 基准建议 5.2.8(RHEL 7,基准版本 2.2.0)中描述的条件,并在条件不满足时实现该条件。它在这里被包含进来,以便与我们即将创建的 Ansible 解决方案进行对比:

#!/bin/bash
#
# This file implements CIS Red Hat Enterprise Linux 7 Benchmark 
# Recommendation 5.2.8 from version 2.2.0
echo -n "Ensure root logins are disabled on SSH... "
OUTPUT=$(grep -e "^PermitRootLogin no" /etc/ssh/sshd_config)
if [ "x$OUTPUT" == "x" ]; then
  echo FAILED!
  OPTPRESENT=$(grep -e "^PermitRootLogin.*" /etc/ssh/sshd_config)
  if [ "x$OPTPRESENT" == "x" ]; then
    echo "Configuration not present - attempting to add"
    echo "PermitRootLogin no" | sudo tee -a /etc/ssh/sshd_config 1>/dev/null
  else
    echo "Configuration present - attempting to modify"
    sudo sed -i 's/^PermitRootLogin.*/PermitRootLogin no/g' /etc/ssh/sshd_config
  fi
  sudo systemctl restart sshd
else
  echo OK
fi

这个 shell 脚本仅适用于众多基准中的一个,虽然它确实能够工作,但它相当脆弱,且无法跨多个系统扩展。此外,该脚本完全不易于阅读,想象一下如果要实现所有 CIS 基准建议,所需要的脚本规模会有多大!

让我们考虑如何将此功能改写为 Ansible 角色。首先,我们知道我们正在测试单个文件中的特定配置行。如果该行不存在,我们就知道该配置(无论是隐式的还是其他)允许远程 root 登录。在这种情况下,我们执行两个操作:首先,我们修改配置文件,插入正确的行(如果该行已存在但配置值错误,则修改现有行)。然后,如果配置文件已更改,我们将重启 SSH 守护进程。

我们在使用 Ansible 的经验表明,lineinfile模块几乎可以处理与检查配置文件和修改配置文件(如果未正确配置必需的行)相关的所有工作。我们还学到了service模块可以轻松地重新启动 SSH 守护程序,并且这个模块将从一个handler而不是在主任务流中运行,以确保只有在实际修改配置时才重新启动守护程序。

因此,我们可以定义一个角色,其中包含一个单独的任务,如在名为rhel7cis_recommendation528的角色中所示:

---
- name: 5.2.8 Ensure SSH root login is disabled (Scored - L1S L1W)
  lineinfile:
    state: present
    dest: /etc/ssh/sshd_config
    regexp: '^PermitRootLogin'
    line: 'PermitRootLogin no'
  notify: Restart sshd

请注意,我们已经为任务赋予了一个有意义的名称– 实际上直接来自 CIS 基准文档本身。因此,我们完全知道这是哪个基准,它的用途以及是否计分。我们还在标题中插入了级别信息,因此这样可以避免在后面交叉参考原始的 CIS 基准文档。

除了我们的角色任务外,我们还希望创建一个处理程序,在修改配置文件后重新启动 SSH 守护程序(如果不这样做,它将无法获取更改)– 这个处理程序的示例代码如下:

---
- name: Restart sshd
  service:
    name: sshd
    state: restarted

我们已经可以看到,这个 playbook 比我们最初的 shell 脚本要容易阅读得多– 当我们在 shell 脚本中实现这个基准时,我们发现没有任何代码重复,并且lineinfile模块非常强大,可以将我们所有的各种检查都包装成一个单一的 Ansible 任务。

运行该角色应该会产生类似于在启用了远程 root 登录的系统上显示的以下屏幕截图的输出:

相比之下,如果建议已经实施,则输出将如以下屏幕截图所示:

正如您所见,如果条件得到满足,lineinfile模块将不会进行任何更改(导致在前面截图中看到的ok状态),而处理程序根本不会运行。

这本身就非常强大,并且在可管理性和编码工作方面明显优于我们的 shell 脚本。尽管如此,RHEL 7 CIS 基准包含了几乎 400 个建议,您不希望在 playbook 运行中创建和包含 400 个角色,因为这会削弱我们 Ansible 自动化的可管理性。

在本章的下一节中,我们将查看如何通过添加 CIS 基准第五部分中的另一个建议来扩展我们当前的 playbook,从而以可扩展和可管理的方式构建我们的 playbook 代码。

在 Ansible 中构建安全策略

如果我们按之前的方式进行,那么当涉及到 RHEL 7 CIS 基准版本 2.2.0 的第 5.2.9 节(确保禁用 SSH PermitEmptyPasswords)时,我们会创建一个新的角色,名为rhel7cis_recommendation529,并将相关的任务和处理程序放入其中。

我相信你会发现,这种方式并不具备良好的扩展性——创建一个新角色意味着我们需要在顶级剧本中指定它,可能会像下面这样:

---
- name: Test and implement CIS benchmark
  hosts: all
  become: yes

  roles:
    - rhel7cis_recommendation528
    - rhel7cis_recommendation529

如果每行一个角色,且几乎需要包含 400 个角色,这会很快变得繁琐,从而削弱我们 Ansible 代码的高可管理性。

你如何将 Ansible 任务划分为角色完全取决于你自己,你应该采用你认为最易于管理的方法。然而,作为建议,通过查看我们示例 CIS 基准的目录结构,我们可以看到建议被分为六个部分。第五部分特别涉及访问、认证授权,因此,我们完全可以将所有这些建议集中到一个角色中,可能称为rhel7cis_section5

在决定了剧本结构后,我们现在可以继续将建议 5.2.8 和 5.2.9 的检查构建到同一个角色中。它们也可以共享相同的处理程序,因为它们都与 SSH 守护进程配置相关。因此,我们新角色的任务可能如下所示:

---
- name: 5.2.8 Ensure SSH root login is disabled (Scored - L1S L1W)
  lineinfile:
    state: present
    dest: /etc/ssh/sshd_config
    regexp: '^PermitRootLogin'
    line: 'PermitRootLogin no'
  notify: Restart sshd

- name: 5.2.9 Ensure SSH PermitEmptyPasswords is disabled (Scored - L1S L1W)
  lineinfile:
    state: present
    dest: /etc/ssh/sshd_config
    regexp: '^PermitEmptyPasswords'
    line: 'PermitEmptyPasswords no'
  notify: Restart sshd

结果代码仍然具有很高的可读性,并被分解为可管理的块,但现在的粒度不再细致到难以维护顶级剧本。

我们的处理程序代码保持不变,现在当我们在一个不符合这些建议的系统上运行角色时,输出应该类似于下面的截图:

这非常简洁整洁,希望你能看到,如果你选择实施 CIS 基准中的近 400 个建议时,这种方法能够很好地扩展。然而,这也引出了一个重要的考虑:在理想的情况下,所有 CIS 建议都会应用于每一台机器,但在现实中,这并不总是可能的。在第十三章的明智地应用安全策略部分中,使用 CIS 基准,我们讨论了在实施时需要谨慎对待的各种建议。此外,尽管永远避免使用 root 账户通过 SSH 进行远程登录是理想的,但我遇到过一些系统,实际上需要这样做,以支持某种遗留系统,直到它能够更新。

简而言之,在政策执行的过程中,始终会有例外的需求。重要的是要以优雅的方式处理这一点。假设你有 100 台 Linux 机器需要应用我们新编写的小型安全策略,但其中有两台需要启用远程 root 登录。

在这个实例中,我们有两个选择:

  • 为需要例外的两台服务器维护一组独立的 playbook

  • 找到一种方法,在不修改角色的情况下选择性地运行任务

在这些选择中,第二种显然是更好的选择,因为它帮助我们维护一个单一的 playbook。但我们该如何实现呢?

Ansible 为我们提供了两种方法来处理这个问题。第一种是我们在本书中已经多次提到的when子句。到目前为止,我们只是查看了这个子句来编程地评估条件(例如,在磁盘的空闲空间低于某个值时执行磁盘清理)。在这个实例中,我们使用了一个更简单的实现——只需要评估布尔值是否为真。

假设我们在任务下方添加以下代码来实现推荐 5.2.8:

  when: 
    - recommendation_528|default(true)|bool

这两行代码评估一个名为recommendation_528的变量,并应用两个 Jinja2 过滤器,确保即使该变量未定义,也能正确处理:

  • default过滤器将变量默认设置为true,因为如果 Ansible 遇到任何未定义的变量,都会导致 play 失败并报错。这就避免了我们必须事先定义这些变量——我们的角色只是默认将它们设置为true,除非我们另行设置。

  • 第二个过滤器将其转换为bool类型,以确保可靠地评估条件。

请记住,true可以是字符串类型或布尔值,取决于你如何解释它。使用|bool过滤器确保 Ansible 将其在布尔上下文中进行评估。

类似地,对于第二个任务,我们将在notify子句下方立即添加以下内容:

  when: 
    - recommendation_529|default(true)|bool

现在,如果我们在没有做任何其他修改的情况下,针对一个不符合要求的系统运行 playbook,它将像之前一样运行,如下图所示:

当我们希望在系统上运行时,跳过这些推荐之一或两个时,魔法就发生了。假设我们的主机legacy-testhost是一个遗留系统,仍然要求启用远程 root 登录。为了在这个系统上使用这个角色,我们知道必须将recommendation_528设置为false。这可以在不同的级别进行,并且在清单中定义它可能是最合适的地方,因为它可以防止有人在未来意外运行 playbook 时没有定义这一点,从而破坏我们的遗留代码并拒绝远程 root 登录。我们可以为这个系统创建一个新的清单,可能看起来像这样:

[legacyservers]
legacy-testhost

[legacyservers:vars]
recommendation_528=false

将我们想要跳过的推荐变量设置为false后,我们可以使用这个新的清单运行角色,结果应该类似于以下截图所示:

这正是我们所期望的——推荐 5.2.8 在我们的旧系统中被跳过,我们所需要做的就是在清单中定义一个变量——来自所有其他服务器的角色代码被复用。

使用when条件和简单的布尔变量对于像这样的简单决策效果很好,但如果你有多个标准需要评估呢?尽管when条件可以评估逻辑上的andor构造,但随着复杂性增加,这可能变得有些难以管理。

Ansible 标签是我们在这里的第二个工具,它们是一个特殊功能,专门设计用于允许你只运行所需的部分角色或剧本,而不需要从头到尾运行整个内容。假设我们在实现推荐 5.2.8 的任务下方添加以下标签:

  tags:
    - notlegacy
    - allservers

在推荐 5.2.9 的任务下,我们可能会添加以下内容:

  tags:
    - allservers

这些标签的行为最好通过示例来解释,既然这是一本实践手册,我们将准确地做到这一点。首先需要注意的是,向剧本(或剧本中的角色)添加标签,除非你指定要运行或跳过哪些标签,否则完全没有任何作用。因此,如果我们以当前形式运行剧本,它的行为将和以往一样,尽管添加了标签,正如以下截图所示:

魔力就在于当我们指定要运行的标签时。让我们重复前面的命令,但这次添加--skip-tags=notlegacy。这个开关正如其名所示——所有带有notlegacy标签的任务都会被忽略。以下截图显示了运行该剧本后的输出:

在这里,我们看到与when条件的使用有明显不同——之前我们观察到推荐 5.2.8 的任务被评估后跳过,但它甚至没有出现在之前的剧本输出中——简而言之,整个任务被当做不存在。

如果我们使用--tags=allservers选项运行剧本,我们会看到两个任务都在运行,因为它们都带有这个标签。

这对于我们这里的示例非常有用,也适用于考虑更广泛的基准文档。例如,我们已经讨论过,所有的推荐都分为 1 级或 2 级。同样,我们知道有些是有评分的,而有些则没有。

知道级别 1 基准不太可能干扰 Linux 服务器的日常运行,我们可以在 playbook 中实施所有建议,并将级别作为每个建议的标签之一。然后,如果我们使用 --tag=level1 运行 playbook,那么只有级别 1 的建议将被实施。以此例子为基础,我们为建议 5.2.8 的任务的标签可能如下所示:

  tags:
    - notlegacy
    - allservers
    - level1
    - scored

在构建角色和 playbooks 来实施安全基准时,无论操作系统或安全标准如何,建议您充分利用 when 子句和标签。请记住,在企业规模自动化时,您最不希望管理许多碎片化的代码片段,它们都相似但功能稍有不同。您可以标准化的部分越多,您的企业管理就会越容易,合理使用这些功能将有助于确保您可以维护单一的 Ansible 代码库,并在运行时调整其操作以处理服务器群中的异常情况。

由于我们一直在考虑适合我们安全基准的适当 playbook 和 role 结构,因此我们在本节中故意保持了我们的例子简单。在下一节中,我们将重新审视我们在 第十三章 中突出显示的一些更复杂的例子,并演示 Ansible 如何使它们更容易编码和理解。

在 Ansible 中实施更复杂的安全基准

在 第十三章 中我们详细考虑的一个例子是 使用 CIS 基准,建议 3.1.2,这关注的是禁用数据包重定向发送。这在任何不应充当路由器的机器上都被认为是重要的(尽管在路由器上不应实施它,因为这会导致路由器功能异常)。

表面上看,这个建议看起来非常简单 – 我们只需设置这两个内核参数,如下所示:

net.ipv4.conf.all.send_redirects = 0
net.ipv4.conf.default.send_redirects = 0

尽管表面上看起来很简单,但我们最终开发了将近 60 行的 Shell 脚本来实施这个检查,因为我们需要检查当前活动的内核参数和持久配置文件的值,并在这些值未按预期设置时执行适当的更改。

在这里,再次,Ansible 来到我们的救援。Ansible 中的 sysctl 模块封装了我们在 Shell 脚本中构建的许多测试和配置工作。此外,我们可以使用循环使得相同的任务代码可以运行两次 – 一次针对上述每个内核参数。

在为此开发角色时,我们可以定义一个类似以下的单个任务:

---
- name: 3.1.2 Ensure packet redirect sending is disabled (Scored - L1S L1W)
  sysctl:
    name: "{{ item.paramname }}"
    value: "{{ item.paramvalue }}"
    reload: yes
    ignoreerrors: yes
    sysctl_set: yes
    state: present
  loop:
    - { paramname: net.ipv4.conf.all.send_redirects, paramvalue: 0 }
    - { paramname: net.ipv4.conf.default.send_redirects, paramvalue: 0 }
  notify:
    - Flush IPv4 routes

基准推荐还指出,如果我们实施这些更改,我们还应该刷新系统上的 IPv4 路由。这也是通过 sysctl 参数来实现的,因此我们只需再次使用 sysctl 模块,不过这次是在 handler 中使用:

- name: Flush IPv4 routes
  sysctl:
    name: net.ipv4.route.flush
    value: "1"
    sysctl_set: yes

在测试系统上运行此命令可能会产生类似于以下截图的输出:

从上面的截图可以看到,这段代码已经成功运行,并应用了基准推荐的设置,作为此更改的直接结果,处理程序已被触发并刷新了 IPv4 路由。最终结果是,原本需要 57 行难以阅读的 shell 脚本,现在可以通过 14 行更加可读的 YAML 完成。

到目前为止,我们已经清楚地了解了 Ansible 如何使 CIS 推荐的设计和实施变得简单,特别是与诸如 shell 脚本等替代方案相比。我们注意到,原生的 Ansible 模块,如 sysctllineinfile,可以优雅地封装许多原本由 shell 脚本执行的步骤。然而,作为剧本作者的你,有时必须为你的剧本做出一些重要决策,接下来的章节将更详细地探讨这一点。

在剧本设计中做出合适的决策

在你构建角色和剧本来实现安全基准时,你会发现有些实现是非常明确的(例如,你几乎肯定会知道是否希望允许 root 用户通过 SSH 登录),而对于其他方面则需要做出决策。时间同步就是一个例子,在本节中,我们将更详细地探讨这个问题,以展示在设计角色时可能需要做出的决策,以及如何以建设性的方式解决它们。

如果你查看 RHEL 7 CIS 基准(版本 2.2.0)中的 2.2.1 节,你会看到它完全关注时间同步。事实上,这是几乎所有企业 Linux 基础设施中的重要功能,服务器时钟之间的差异可能会导致如证书有效性和 Kerberos 票据等问题。

尽管几乎所有人都一致认为时间同步至关重要,但对于如何实现这一目标却意见不一。例如,大多数主流 Linux 发行版有两种主要的时间同步服务:

  • chrony

  • ntpd

尽管在 RHEL 7 上 chrony 现在是标准工具,这并不意味着老牌的 ntpd 服务将不再工作——实际上,一些企业仍然选择实施它,因为他们在这方面有着丰富的经验。

完全有可能让 Ansible 检测到给定 Linux 服务器正在使用这两项服务中的哪一项——从高层次来看,我们可以让 Ansible 做如下操作:

  1. 查询 RPM 软件包数据库,查看是否安装了 ntpdchrony 或两者。

  2. 如果安装了一个或两个服务,请检测哪一个是活跃的:

a. 如果两者都未活跃,则需要纠正,因为我们已经确认需要时间同步。

b. 如果两者都活跃,则服务将发生冲突,应禁用其中一个。

正如我相信您会看到的,前述过程中会有一个干预点 – 如果两个服务都未启动,我们需要选择一个来启动。如果两者都活跃,我们需要禁用一个。这是 Ansible 的帮助能力止步的地方 – 它无法为您的特定企业决定这两个完全有效的服务中哪一个最适合您的用例。

因此,重要的是提前做出关于使用哪种时间同步服务的决定。有了这个决定,playbook 可以专门编码以执行适当的检查,并按需要执行适当的补救步骤。此外,从我们在 第一章 中的讨论中知道,在 Linux 上构建标准操作环境,企业规模的自动化受到共同性和标准的支持 – 因此,我们知道根据这些原则,我们应选择一个标准的时间同步服务,并坚持使用,除非有很好的业务理由提出例外。

为了推进这个例子,让我们看看建议 2.2.1.1. 这项建议指出我们应确保正在使用时间同步服务 – 尽管它对于哪一个是不可知的。如果我们已经提前决定了哪个服务是相关的,我们的 playbook 开发就很容易。假设我们选择了 chrony(RHEL 7 的默认选择);我们为这个建议的角色可能如下所示:

---
- name: 2.2.1.1 Ensure time synchronization is in use (Not Scored - L1S L1W)
  yum:
    name: chrony
    state: present

- name: 2.2.1.1 Ensure time synchronization is in use (Not Scored - L1S L1W)
  service:
    name: chronyd
    state: started

这段简单的代码确保我们检查并满足建议 2.2.1.1,而无需检测正在使用哪个时间服务的任何逻辑。当然,我们可以选择更彻底地检查 ntpd 是否未启动,但这留给您作为一个练习。

当然,我们无法将大约 400 个建议中所需的所有 Ansible 代码都放入这本书中 – 这本身就值得一本书!此外,这个例子仅适用于一个基准 – 如果您的企业引入了新的操作系统,如 RHEL 8,您可以肯定会有一个单独的适用于该操作系统的 CIS 基准需要实施。但希望这些来自 RHEL 7 CIS 基准的示例足以让您设计和构建自己的策略。因此,在本章的下一部分,我们将探讨在企业规模下管理此任务的技术。

使用 Ansible 应用企业范围的策略

尽管我们已经看到 Ansible 在实施 CIS 基准方面带来的显著好处,但我相信现在你应该能够看出,开发和维护这些策略可能会变成一份全职工作,尤其是当需要在基础设施上运行它们,并管理每次运行的结果时。

幸运的是,开源开发精神为这一难题带来了一个解决方案。试想,如果有人已经花费大量时间和精力开发出了一套高质量的 Ansible 角色,用于实施 CIS 基准,而且这些角色以开源代码的形式提供,这样你就可以审计它,确保它适合你的环境,并在需要时轻松调整。此外,假设他们已经投入了大量时间和精力,为每个任务添加标签,并且加入了适当的变量结构,以便你能够轻松指定你的选择,比如你的企业使用的时间同步服务。

幸运的是,这项工作已经由 MindPoint Group 完成,他们的代码已经免费提供在 GitHub 上,网址是 github.com/MindPointGroup/RHEL7-CIS

在本文写作时,适用于 EL7 系统的最新 CIS 基准版本是 2.2.0,而前面提到的 playbook 是针对基准版本 2.1.1 编写的。你需要确保了解你正在实施的基准版本,并注意如果使用稍旧版本可能带来的安全隐患。

除此之外,正如 Ubuntu 的用户可以选择付费支持或使用免费的开源操作系统一样,EL7 用户也可以选择 Red Hat Enterprise Linux 7 或 CentOS 7,MindPoint Group 也提供了他们的 Ansible 加固代码的商业支持版本,网址为 www.lockdownenterprise.com/。因此,他们在整个领域提供了支持,尊重一些企业绝对需要企业支持合同,而另一些企业则更倾向于使用免费的开源软件。

让我们探索一下如何在我们的 CentOS 7 服务器上使用开源代码:

  1. 首先,我们需要克隆 GitHub 仓库:
$ cd roles
$ git clone https://github.com/MindPointGroup/RHEL7-CIS.git
$ cd ..
  1. 一旦完成这些工作,我们就可以像使用任何其他角色一样使用这段代码。在适当的地方,我们应该设置变量,这些变量可以在清单中或在主 playbook 中设置(稍后会详细说明)。

因此,一旦从 GitHub 克隆了角色,使用 MindPoint Group 的 CIS 基准在 Ansible 中的最纯粹、最简单的实现就是一个像下面这样的 playbook:

---
- name: Implement EL7 CIS benchmark
  hosts: all
  become: yes

  roles:
    - RHEL7-CIS
  1. 完成这些步骤后,你实际上可以在几分钟内开始在 Linux 服务器上实现 EL7 基准及其近 400 项推荐——playbook 按照正常方式运行,并且在执行所有检查和实现推荐时(如需要)会生成许多页面的输出。以下截图展示了 playbook 的运行情况和初始输出页面:

现在,关于变量的一点说明。正如我们在上一节中所提到的(编写 Ansible 安全策略),会有一些情况下你需要改变 playbook 的运行方式。变量和标签都在我们之前克隆的 GitHub 仓库中附带的 README.md 文件中有详细说明,为了说明问题,我们来看几个例子。

首先,假设我们只想实施第一级推荐(那些对日常操作风险较小的)。可以通过运行 playbook 并使用 level1 标签来实现:

$ ansible-playbook -i hosts site.yml --tags=level1

或者,你可能在对一组作为路由器的服务器运行加固 playbooks。在这种情况下,我们需要将rhel7cis_is_router变量设置为false,以确保不会设置那些禁用路由器功能的内核参数。

这可以通过如下命令在命令行上完成:

$ ansible-playbook -i hosts site.yml -e rhel7cis_is_router=true

然而,这非常手动化,如果有人不小心在没有设置此变量的情况下运行了 playbook,可能会导致路由器被突然禁用。

最好是在清单级别设置这个变量,从而确保每次运行 playbook 时它都能正确设置。因此,我们可以创建一个这样的清单:

[routers]
router-testhost

[routers:vars]
rhel7cis_is_router=true

在这个清单就位后,使用如下命令来执行 playbook 并对路由器进行操作:

$ ansible-playbook -i routers site.yml

只要使用这个清单文件,就不会有人忘记将rhel7cis_is_router变量设置为true

当然,这段讨论并不是说你必须下载并使用这些 playbooks——完全可以根据自己的需求开发和维护属于自己的 playbooks。实际上,在某些情况下,这种策略可能是更好的选择。

重要的是,你要选择最适合你企业的策略。在选择大规模实施安全策略时,你应考虑以下因素:

  • 无论你是否希望拥有自己的代码(以及由此带来的所有优缺点)

  • 无论你是否希望今后继续负责维护你的代码库

  • 你应该尽可能地标准化一个代码库,以确保代码结构保持可维护

  • 无论你是否需要第三方支持来实现这些基准,还是你自信自己拥有足够的技能和资源来内部完成

一旦完成评估,你将能够很好地定义前进的道路,通过创建 Ansible playbook 来实现你选择的安全标准。本章迄今为止提供的信息足以支持你选择任何路径。虽然本章重点讨论的是 EL7(Red Hat Enterprise Linux 7 和 CentOS 7),但我们讨论的内容同样适用于其他有安全基准的操作系统(例如,Ubuntu Server 18.04)。实际上,如果你使用 Ubuntu Server 18.04 的 CIS 基准,按照本章讨论的流程操作,你会发现能够实现很大的相似性。

到目前为止,我们几乎专注于实现 CIS 基准。然而,本章如果没有提供在不进行更改的情况下检查执行级别的方法,就不完整。毕竟,审计是大多数企业政策中重要的一部分,尤其是涉及安全时,而更改必须在授权的变更请求窗口内进行。

使用 Ansible 测试安全策略

如我们所讨论的那样,确保不仅能够高效且可重复地实施安全政策,而且还应该能够进行审计,这一点非常重要。为此任务提供了多种工具,包括闭源和开源工具。在考虑其他工具之前,值得先看看 Ansible 本身如何帮助完成这一任务。

让我们回到最初的一个例子,我们在其中实施了 CIS 基准 第五部分 的两个建议。

之前,我们使用以下命令运行了这个:

$ ansible-playbook -i hosts site.yml

这通过了两个检查,如果系统未符合安全建议,它会实施相应的更改。然而,Ansible 还有一个叫做检查模式的操作模式。在此模式下,Ansible 不会对远程系统进行任何更改——而是尝试预测可能对系统进行的所有更改。

不是所有模块都与检查模式兼容,因此在使用此模式时需要小心。例如,Ansible 无法预知运行特定 shell 命令时的输出,因为命令的组合可能有很多种。此外,运行 shell 命令可能具有破坏性或导致系统发生变化,因此,在检查运行期间,任何使用 shell 模块的任务都会被跳过。

然而,我们已经使用过的许多核心模块,如 yumlineinfilesysctl,都支持检查模式,因此可以在此模式下有效使用。

因此,如果我们再次运行我们的示例 playbook,只是这次在检查模式下运行,我们将看到类似于以下屏幕截图的输出:

你会注意到,这看起来与其他任何剧本的运行完全相同——实际上,唯一表明它正在运行检查模式的线索是命令行中调用此运行的 -C 标志。然而,如果你检查目标系统,你会发现没有进行任何更改。

上述输出对于审计过程非常有用——它向我们展示了目标系统不符合基准 第 5.2.8 节第 5.2.9 节 的建议——如果这些建议已满足,结果应该是 ok。同样,我们知道处理程序只在需要对远程系统进行更改时才会触发,再次表明该系统在某些方面不符合要求。

可以接受输出需要一定解释——然而,通过在编写角色时运用良好的设计实践(特别是在任务名称中加入基准章节号和标题),你可以非常迅速地开始解读输出,了解哪些系统不符合要求,进一步了解它们具体在哪些建议上没有达标。

此外,我们设立的变量结构用来确定哪些任务在何时运行,仍然适用于检查模式,因此,如果我们在需要启用远程 root 登录的旧主机上运行此剧本(但这次在检查模式下),我们可以看到该任务被跳过,确保我们在审计过程中不会得到假阳性。以下截图显示了此操作:

通过这种方式(结合良好的剧本设计),Ansible 代码不仅可以用于实现目的,还可以用于审计目的。

希望本章内容能为你提供足够的知识,使你在实施企业级 Linux 服务器安全加固时充满信心,甚至能够将其作为持续过程的一部分进行审计。

摘要

Ansible 是一个功能强大的工具,非常适合用于实施和审计安全基准,如 CIS 安全基准。我们通过实践示例展示了它如何将一个接近 60 行的 shell 脚本简化为不到 20 行,并且如何将相同的代码轻松地在多种场景中复用,甚至可以用于在整个企业范围内审计安全政策。

在本章中,你学习了如何编写 Ansible 剧本来应用服务器加固基准,如 CIS。然后你获得了在整个企业范围内使用 Ansible 应用服务器加固政策的实操知识,并了解了如何利用公开的开源角色来帮助你实现这一目标。最后,你了解了 Ansible 如何支持测试和审计成功的政策应用。

在下一章中,我们将介绍一个名为 OpenSCAP 的开源工具,它可以用于有效地审计整个企业范围内的安全政策。

问题

  1. lineinfile 这样的 Ansible 模块是如何让安全基准实现代码比 Shell 脚本更高效的?

  2. 如何为特定的服务器或服务器组设置条件性 Ansible 任务?

  3. 在编写 Ansible 任务以实现 CIS 基准时,命名任务时有哪些最佳实践?

  4. 如何修改 playbook,使得你能够轻松运行 CIS 1 级基准,而不评估任何 2 级基准?

  5. 运行 Ansible playbook 时,--tags--skip-tags 选项有什么区别?

  6. 为什么你会想利用公开的开源代码来实现 CIS 基准?

  7. 使用 ansible-playbook 命令时,-C 标志对 playbook 运行有什么影响?

  8. shell 模块是否支持检查模式(check mode)?

进一步阅读

第十五章:使用 OpenSCAP 审计安全策略

在前两章中,我们已经阐明了将安全策略(如 CIS 基准)应用于企业 Linux 基础设施的价值。我们讨论了多种方法来应用该策略,并确保其持续执行;尤其是在有大量人员拥有 Linux 服务器超级用户权限的基础设施中,确保策略持续执行显得尤为重要。虽然我们已经确定了 Shell 脚本和 Ansible 在审计基础设施是否符合所选安全策略方面的作用,但我们也指出,这两者都不太适合提供易读且具有可操作性的报告,尤其是当基础设施规模较大时。例如,基础设施安全团队可能需要一个易读的报告,展示基础设施与安全策略的符合情况,而 Shell 脚本和 Ansible 并不立即适合这个任务。

尽管市场上有多种基础设施扫描工具可供选择,但大多数是商业工具,本书的重点是那些任何企业都能使用的开源解决方案,无论其预算如何。因此,在本章中,我们将介绍免费的 OpenSCAP 工具。SCAP代表安全内容自动化协议,它是一个标准化的解决方案,用于检查 Linux 基础设施是否符合给定的安全策略(在我们的案例中是 CIS)。OpenSCAP 因此是 SCAP 的一个开源实现,已被包括 Red Hat 在内的多个企业 Linux 厂商广泛采用。因此,我们将探讨如何设置自己的 OpenSCAP 基础设施,以便进行合规扫描和报告。这将使所有关心基础设施安全的团队能够监督合规性水平。

本章将具体涵盖以下主题:

  • 安装你的 OpenSCAP 服务器

  • 评估和选择策略

  • 使用 OpenSCAP 扫描企业

  • 解释结果

技术要求

本章包括基于以下技术的示例:

  • Ubuntu Server 18.04 LTS

  • CentOS 7.6

  • Ansible 2.8

要运行这些示例,您需要访问两台服务器或虚拟机,每台运行前面列出的操作系统之一,以及 Ansible。

本书中讨论的所有示例代码均可从 GitHub 获取:github.com/PacktPublishing/Hands-On-Enterprise-Automation-on-Linux

安装你的 OpenSCAP 服务器

在扫描您的基础设施时,我们需要做出一些决策,因为 OpenSCAP 项目提供了一些功能重叠的工具。原因是它们面向不同的用户群体——有些工具纯粹是命令行驱动,因此非常适合定时、脚本化的任务,比如每月的合规性报告。到目前为止,共有五个 OpenSCAP 工具可用,我们将在接下来的章节中详细介绍每个工具,帮助您做出明智的决策,选择适合您企业的工具(或工具组合)。

在以下小节中,我们将从最基本的工具——OpenSCAP Base 开始。

运行 OpenSCAP Base

OpenSCAP Base 工具提供了扫描单台 Linux 机器并报告其是否符合给定政策所需的基本功能。它实际上由两个组件组成,因此是我们在接下来的子章节中将要介绍的其他工具的前提要求。

该工具的第一个组件是一个名为oscap的命令行工具。这个工具可以在本地机器上使用适当的安全政策和配置文件运行,以生成合规性报告。报告生成时采用 HTML 格式,因此尽管报告创建过程相当手动,但最终报告非常易于阅读,非常适合发送给安全或合规团队进行审计或评估。

OpenSCAP Base 的第二个组件包括一个库,它作为构建其他 OpenSCAP 服务的基础,比如 SCAP Workbench 和 OpenSCAP Daemon——我们将在本节稍后的部分详细介绍这些内容。

本书中,我们只会在使用其他 OpenSCAP 工具时利用这个库。我们将在本章名为使用 OpenSCAP 扫描企业的部分看到这些工具的实际应用。不过,目前我们先关注 OpenSCAP Base 的安装。

在单台机器上手动安装 OpenSCAP Base 非常简单——它已经为本书中讨论的两大主要 Linux 发行版预打包——Ubuntu Server 和 CentOS(因此,间接适用于 Red Hat Enterprise Linux)。要在 CentOS 7 或 RHEL 7 上安装它,您只需运行以下命令:

$ sudo yum -y install openscap-scanner

同样,在 Ubuntu Server 18.04 LTS 上,您可以运行以下命令:

$ sudo apt -y install libopenscap8

需要记住的是,这些包不仅包括oscap命令行工具,还包括本节前面提到的库。因此,即使您从不打算使用oscap命令行工具运行 OpenSCAP,这些包所包含的库仍可能是您某些用例所需要的(例如,使用 SCAP Workbench 进行远程扫描)。

因此,考虑使用 Ansible 部署这些包非常重要,甚至可能希望将它们包含在你的标准构建镜像中,这样你就能确保可以远程扫描任何给定的 Linux 服务器以检查合规性,而无需执行任何前提步骤。我们将在后续部分使用 OpenSCAP 扫描企业中介绍如何使用oscap工具运行扫描——不过现在,理解这个包是什么以及为什么可能需要它就足够了。

在下一节中,我们将介绍如何安装 OpenSCAP 守护进程,这是 OpenSCAP 工具集的另一个组成部分。

安装 OpenSCAP 守护进程

安全审计并非一次性的任务——在 Linux 环境中,拥有管理员级别(即 root)访问权限的人,随时可能故意或通过良好的意图改变,使得 Linux 服务器变得不合规。因此,安全扫描的结果实际上只能保证被扫描的服务器在扫描时是否合规(或不合规)。

因此,定期扫描环境至关重要。实现这一目标有许多方法,你甚至可以使用调度程序(如cron)运行oscap命令行工具,或者通过 AWX 或 Ansible Tower 中的计划 Ansible playbook 来实现。然而,OpenSCAP 守护进程是作为 OpenSCAP 工具套件的一部分提供的原生工具。它的作用是在后台运行并对指定的目标或目标集进行定期扫描。这些目标可能是运行守护进程的本地机器,或者是一组通过 SSH 访问的远程机器。

安装过程同样非常简单——如果你手动执行此操作,在 EL7 系统(例如 RHEL7 或 CentOS 7)上,你需要运行以下命令:

$ sudo yum -y install openscap-daemon

在 Ubuntu 系统上,包名相同,你可以运行以下命令进行安装:

$ sudo apt -y install openscap-daemon

尽管你可以在每台机器上设置这个守护进程,并为每台机器配置一个任务定期扫描自身,但这种方式容易被滥用,因为拥有 root 权限的人很容易禁用或篡改扫描。因此,我们建议你考虑设置一个集中式扫描架构,通过一台中央安全服务器在你的网络中执行远程扫描。

你需要在这样的服务器上安装 OpenSCAP 守护进程,安装完成后,可以使用oscapd-cli工具来配置定期扫描。我们将在本章稍后的使用 OpenSCAP 扫描企业部分中详细介绍这一过程。

尽管我们到目前为止考虑的两个工具都非常强大,可以满足你的所有审计需求,但它们完全基于命令行,因此可能不适合那些不习惯在 shell 环境中操作的用户,或者那些负责审计扫描结果但不一定运行它们的用户。这个需求可以通过 OpenSCAP 工具库中的另一个工具——SCAP Workbench来满足。我们将在下一节中讨论如何安装它。

运行 SCAP Workbench

SCAP Workbench 是 SCAP 工具集的图形用户界面,旨在为用户提供一种简单、直观的方式来执行常见的扫描任务。因此,它非常适合技术要求较低的用户或那些更习惯于图形环境的用户。

需要考虑的一点是,SCAP Workbench 是一个图形工具,在许多环境中,Linux 服务器都是无头运行的,并且没有安装图形 X 环境。因此,如果你在没有图形环境的普通 Linux 服务器上安装它,你将看到类似下面截图中显示的错误:

幸运的是,有几种方法可以运行 SCAP Workbench。首先,值得注意的是,它是一个真正的跨平台应用程序,提供适用于 Windows、macOS 和大多数常见 Linux 平台的下载,因此,对于大多数用户来说,最简单的路径是直接在他们的本地操作系统中运行它。

如果为了保持一致性,你希望在 Linux 上运行 SCAP Workbench,你需要设置一个远程 X11 会话,或者设置一个包含图形桌面环境的专用扫描主机。这里没有对错之分——真正的关键是你决定哪种路径最适合你的环境和工作模式。

如果你选择在 Linux 上运行,安装 SCAP Workbench 和我们考虑的其他 OpenSCAP 工具一样并不难:

  1. 要在 RHEL7/CentOS 7 上安装它,你需要运行以下命令:
$ sudo yum -y install scap-workbench

在 Ubuntu Server 上,你需要运行以下命令:

$ sudo apt -y install scap-workbench
  1. 一旦完成,你就可以使用适合你选择的操作系统的方法打开 SCAP Workbench。如果你是在使用远程 X 会话的 Linux 服务器上运行它,只需运行以下命令:
$ scap-workbench &

我们将在本章的《使用 OpenSCAP 扫描企业》一节中探讨如何在这个图形环境中设置并运行扫描。不过,在完成本章这一部分之前,我们将讨论 OpenSCAP 项目提供的另外两个工具——SCAPTimony 和 Anaconda Addon。

考虑其他 OpenSCAP 工具

到目前为止,我们已经考虑了各种用于扫描和审计基础设施的 OpenSCAP 工具。然而,还有两个工具我们尚未考虑,尽管它们并不像我们之前提到的那些工具那样是交互式工具,因此不在本书的范围内。尽管如此,它们值得一提,因为你可能会选择将它们集成到未来的环境中。

其中一个工具叫做SCAPTimony。它不同于 SCAP Workbench 或oscap这样的最终用户应用程序,而是一个中间件、基于 Ruby-on-Rails 的引擎,旨在供你集成到自己的 Rails 应用程序中。SCAPTimony 的优势在于,它提供了一个数据库和存储平台,用于存储你的 SCAP 扫描结果。因此,如果你决定编写自己的 Rails 应用程序来处理 OpenSCAP 扫描,它可以为你提供集中报告 OpenSCAP 扫描的功能。它还使你的 Rails 应用程序能够操作和汇总收集的数据,因此在管理扫描数据方面是一个非常强大的工具。

虽然开发一个 Rails 应用程序以使用 SCAPTimony 超出了本书的范围,但值得注意的是,Katello 项目(因此包括 Red Hat Satellite 6)已经在使用 SCAPTimony,因此你可以在不需要自己创建应用程序的情况下,利用这个工具。

在撰写本书时,最后一个可用的工具是 OSCAP Anaconda Addon。对于不熟悉的人来说,Anaconda 是 CentOS 和 Red Hat Enterprise Linux 等 Linux 发行版使用的安装环境。虽然这个插件无法帮助我们处理基于 Ubuntu 的服务器,但它提供了一种方法,可以在安装时构建符合要求的基于 Red Hat 的服务器。

正如我们已经讨论过如何使用 Ansible 应用安全策略(参见第十四章,使用 Ansible 进行 CIS 硬化),并且我们强烈推荐为你的 Linux 环境使用标准镜像,这些镜像我们在第五章,使用 Ansible 构建虚拟机模板进行部署,以及第六章,使用 PXE 启动进行自定义构建中创建过,因此我们不会探讨这个插件,因为它与我们已经提供的跨平台解决方案功能重复。

到目前为止,你应该已经对 OpenSCAP 工具有了较好的了解,并且知道哪些工具可能最适合你的环境。然而,在我们进行第一次扫描之前,我们需要一个 OpenSCAP 安全策略来使用。在下一节中,我们将讨论在哪里下载这些策略以及如何选择适合你环境的策略。

评估和选择策略

OpenSCAP 及其相关工具本身是引擎——没有安全策略作为扫描标准,它们无法实际帮助你审计环境。正如我们在第十三章中探讨的,使用 CIS 基准,Linux 有许多安全标准,在本书中,我们深入讨论了 CIS 基准。不幸的是,目前这个标准不能通过 OpenSCAP 进行审计,尽管许多其他安全策略可以用于保护你的基础设施。此外,由于 OpenSCAP 及其策略完全是开源的,完全没有障碍阻止你根据自己的需求创建自己的安全策略。

有许多安全标准可以供你自由下载,并用于审计你的基础设施,接下来的章节我们将讨论你最有可能考虑的主要标准——SCAP 安全指南。

安装 SCAP 安全指南

一些最全面、现成的安全策略可以在SCAP 安全指南SSG)项目中找到,你会经常看到ssg缩写出现在目录中,有时甚至出现在包名中。这些策略就像我们之前探讨的 CIS 基准一样,涵盖了 Linux 安全的许多方面,并提供了修复步骤。因此,OpenSCAP 不仅可以用于审计,还可以用于执行安全策略。然而,必须指出的是,鉴于其性质,我认为 Ansible 最适合执行此任务,值得注意的是,在 SCAP 安全指南的最近的上游版本中,Ansible 剧本与 XML 格式的 SCAP 策略一起提供。

OpenSCAP 策略与任何安全定义一样,随着新漏洞和攻击的发现会不断演变和变化。因此,在考虑使用哪个版本的 SSG 时,你需要考虑所使用版本的更新程度,以及它是否符合你的需求。虽然看似显而易见的说法是你应该始终使用最新版本,但正如我们稍后会看到的,还是有一些例外情况。

这个决定需要仔细考虑,正如最初看起来可能并不明显的那样,仅仅去下载最新的版本并不是最好的做法。尽管大多数主要 Linux 发行版中包含的版本通常落后于 SSG 项目 GitHub 页面上的版本(见github.com/ComplianceAsCode/content/releases),但在某些情况下(尤其是在 Red Hat Enterprise Linux 上),它们经过测试并已知在提供的 Linux 发行版中有效。

然而,在其他发行版上,结果可能有所不同。例如,在写作时,SSG 策略的最新公开版本是 0.1.47,而 Ubuntu Server 18.04.3 中包含的版本是 0.1.31。此版本的 SSG 甚至不支持 Ubuntu 18.04,如果您尝试使用 Ubuntu 16.04 策略对 Ubuntu Server 18.04 进行扫描,所有扫描结果将显示为notapplicable。所有扫描都会验证运行扫描的主机,并确保其与原定要扫描的主机匹配,因此,如果检测到不匹配,它们将报告notapplicable,而不是应用测试。

在 Ubuntu 18.04 的libopenscap8包中也存在一个错误,导致关于/usr/share/openscap/cpe/openscap-cpe-dict.xml文件缺失的错误。希望 Ubuntu 的 OpenSCAP 软件包能够在适当的时候更新并修复,以便能够可靠使用。

使用 Red Hat Enterprise Linux 的用户需要注意,Red Hat 仅支持使用 RHEL 附带的 SSG 策略进行 OpenSCAP 扫描的用户,因此在这种情况下,更加需要使用供应商提供的策略文件。

就像任何开源环境一样,关键在于选择权在于您——如果您希望评估可用的更新策略,您可以自由选择这样做,而对于 Ubuntu 18.04,您必须这样做,否则扫描将无法正常工作!不过,如果您希望利用商业支持的环境,那么 RHEL 就是一个不错的选择。

要在 CentOS 7 或 RHEL 7 上安装供应商提供的 SSG 软件包,您需要运行以下命令:

$ sudo yum -y install scap-security-guide

此软件包包含了所有由 Red Hat 直接支持的操作系统和应用程序的 SSG 策略(请注意,CentOS 是基于 RHEL 的)。因此,您在安装此软件包时,只会找到 RHEL 6 和 7、CentOS 6 和 7、Java 运行时环境JRE)以及 Firefox 的策略。写作时,此软件包安装了 SSG 版本 0.1.43。

在 Ubuntu Server 上,SSG 分布在多个软件包中,但提供跨平台支持。要在 Ubuntu Server 18.04 上安装完整的 SSG 软件包,您需要运行以下命令:

$ sudo apt -y install ssg-base ssg-debderived ssg-debian ssg-nondebian ssg-applications

这些软件包提供以下系统的策略:

ssg-base SSG 基础内容和文档文件
ssg-debderived 针对 Debian 衍生操作系统(如 Ubuntu Server)的 SSG 策略
ssg-debian 针对 Debian 操作系统的 SSG 策略
ssg-nondebian 针对其他 Linux 操作系统(如 RHEL 和 SuSE 企业版 Linux)的 SSG 策略
ssg-applications 用于保护应用程序的 SSG 策略,如Java 运行时环境JRE)、Firefox 和 Webmin

因此,可以公平地说,在写作时,尽管 Ubuntu Server 发布了一个较旧的包版本(0.1.13),但它支持更多的操作平台。

你选择安装哪种 SSG 是由你决定的,或者如果你敢于尝试,你甚至可以选择编写自己的 SSG!最重要的是,你要做出明智的选择,并在需要时获得操作系统供应商的支持。我们在继续探索你可能下载的其他策略之前,值得详细了解一下你在搜索和实现 OpenSCAP 审计架构时可能遇到的两种安全策略文件格式。我们将在下一节进行详细讲解。

理解 XCCDF 和 OVAL 策略的目的

当你下载策略时,你经常会看到开放漏洞评估语言OVAL)和可扩展配置检查表描述格式XCCDF)这两个术语。你遇到的一些安全策略只有在 OVAL 格式下才可用。因此,我们需要花点时间来考虑这些不同的文件类型。

首先,重要的是要声明它们不是可以互换的——相反,它们应该被视为具有层级性质。在层级结构的较低层次是 OVAL 文件,本质上描述了 OpenSCAP 扫描引擎应该执行的所有系统级检查。例如,这可能包括检查某个给定的包是否比某个版本更新,因为旧版本中可能存在已知漏洞。或者,它可能是一个检查,确保像/etc/passwd这样的关键系统文件的所有者是 root。

这些检查在审计系统是否符合你的安全策略时非常有价值,但它们可能对于经理或安全团队来说不太易读。他们可能更感兴趣的是高级别的安全策略,例如验证重要文件和目录的权限。实际上,这个检查几乎肯定会包括对/etc/passwd的所有权检查,以及一整套其他关键系统文件,例如/etc/group/etc/shadow

这就是 XCCDF 格式变得相关的地方——它可以被看作是层级结构中的下一级,因为它提供了一组可供人类阅读的安全策略(以及有价值的文档和参考资料),这对于经理或信息安全团队这样的观众来说会非常有用。这些策略描述了系统的状态,参考了 OVAL 定义执行的检查。XCCDF 文件不包含扫描引擎的任何检查定义(例如oscap),而是引用了在 OVAL 文件中编写的检查,因此可以被看作位于 OVAL 文件之上的一层。

因此,OVAL 文件可以单独用于审计目的,但除非相应的 OVAL 文件存在,否则无法使用 XCCDF 文件。

XCCDF 文件还包含一系列扫描配置文件,这些文件告诉扫描引擎你的策略是什么,因此它应该扫描哪些内容。这几乎肯定意味着只扫描 OVAL 文件中存在的检查项的子集。

可用的配置文件可以通过图形 SCAP Workbench 工具轻松列出,或者通过使用 oscap info 命令在命令行中列出。以下截图展示了运行该命令查询 CentOS 7 的 SSG 示例:

尽管为了节省空间输出被截断,但你可以清楚地看到可供 CentOS 7 使用的各种安全配置文件。在截图中,你会注意到(例如)对于运行图形用户界面的 CentOS 7 服务器和不运行图形界面的服务器,有不同的配置文件。这是因为图形系统需要额外的安全措施来确保 X Windows 子系统的安全性。还有适用于支付卡行业PCI)环境的配置文件,以及位于顶部的最基本配置文件,它应该是适用于几乎任何 CentOS 7 服务器的最小可行安全策略。

一旦你知道了从 XCCDF 策略文件中想要使用的配置文件,你可以在运行扫描时指定它,稍后的章节《使用 OpenSCAP 扫描企业》中将详细探讨这一点。

在我们结束本节内容之前,需要特别说明的是,OVAL 文件没有配置文件,如果你运行 OVAL 扫描,系统将自动运行 OVAL 文件中定义的所有测试,而不考虑其目的。这可能会导致问题,因为以 CentOS 7 SSG OVAL 文件为例,它包含了针对 X Windows 图形子系统安全性的测试。这些测试将在没有安装 GUI 的系统上失败,因此可能会在扫描结果中产生假阳性。

需要注意的是,SCAP Workbench 只支持使用 XCCDF 策略进行扫描,因此如果你使用的配置文件仅包含 OVAL 文件,则需要使用不同的扫描工具。

现在我们对可能下载的各种安全策略的文件格式有了更多了解,让我们来看一下你可能希望下载的一些其他安全配置文件。

安装其他 OpenSCAP 策略

SSG 安全策略很可能会成为你使用 OpenSCAP 进行审计框架的核心——然而,鉴于 OpenSCAP 的开源性质,任何人,包括你,都完全可以编写策略文件。

你最有可能希望将 SSG 策略补充的策略是那些可以检查服务器补丁级别的策略。考虑到 Linux 操作系统补丁发布的频繁性,将这些策略与 SSG 集成会给维护人员带来麻烦,因此通常将它们分开。

例如,在您的 CentOS 7 服务器上,您可以下载以下安全策略(请注意,它仅提供 OVAL 格式):

$ wget https://www.redhat.com/security/data/oval/com.redhat.rhsa-RHEL7.xml.bz2
$ bunzip2 com.redhat.rhsa-RHEL7.xml.bz2

这包含了迄今为止在 CentOS 7(以及 RHEL 7)上发现的所有软件包漏洞,并检查已安装的版本,以确保它们比已知漏洞存在的版本更为新颖。因此,这可以轻松生成报告,向您展示是否需要紧急修补您的 CentOS 7 或 RHEL 7 系统。

Canonical 也提供了一个类似的列表,适用于 Ubuntu Server 18.04,可以通过以下方式下载:

$ wget https://people.canonical.com/~ubuntu-security/oval/com.ubuntu.bionic.cve.oval.xml.bz2
$ bunzip2 com.ubuntu.bionic.cve.oval.xml.bz2

再次说明,这包含了在 Ubuntu Server 18.04 上发现的所有软件包漏洞,并再次检查以确保安装在系统上的软件包版本比有漏洞的版本更新。对于这两个安全策略,所有检查每次都会执行,因为它们是 OVAL 格式的—然而,测试只有在软件包安装且版本低于包含修复该漏洞版本的情况时才会报告失败。因此,您不应该收到任何由此扫描所产生的误报。

与 SSG 策略不同,这些策略会定期更新—在撰写本文时,我们使用前面的命令下载的 Ubuntu 包漏洞扫描配置文件仅有一个小时的历史!因此,您的审计过程的一部分必须涉及下载最新的包漏洞 OVAL 策略并对其进行扫描—这可能是 Ansible 的好工作(尽管这部分留给您自己练习)。

到现在为止,您应该已经对可以下载的策略类型、可能遇到的格式以及它们的预期用途有了充分的理解。因此,在下一节中,我们将展示如何使用它们来扫描您的 Linux 主机,并根据所选的安全策略审计合规性。

使用 OpenSCAP 扫描企业环境

到目前为止,我们已经介绍了 OpenSCAP 项目提供的各种工具以及您可能希望使用的安全策略,用于扫描您的企业 Linux 环境。现在,既然我们已经完成了这些基础工作,接下来就该看看如何实际利用这些工具扫描您的基础设施。正如我们之前讨论的,您可以使用三种关键工具来扫描您的基础设施。我们将从下一节开始,探索 oscap 命令行工具的使用。

使用 OSCAP 扫描 Linux 基础设施

正如本章前面讨论的,oscap 工具是一个命令行实用程序,旨在扫描其安装所在的本地机器。您希望审计主机的安全策略必须也位于该主机的文件系统中。如果您已经完成了评估和选择策略一节中的步骤,那么您应该已经具备了所需的一切。

话虽如此,如果使用 oscap 工具扫描你的基础设施将是你未来的方向,你可能会考虑使用 Ansible 来安装它并在扫描完成时收集结果。

在我们深入之前,先来看一下如何扫描单个主机:

  1. 假设我们正在处理 Ubuntu 18.04 服务器,并且已经将最新的上游 SSG 解压到当前工作目录中,以便我们拥有所需的 Ubuntu 18.04 支持,我们将使用 oscap info 命令查询 XCCDF 策略文件,以查看哪些策略可用:
$ oscap info scap-security-guide-0.1.47/ssg-ubuntu1804-ds.xml

info 命令的输出将类似于以下截图所示:

  1. 在此,我们将选择你希望进行审核的配置文件(或多个配置文件——毕竟,你始终可以运行多个扫描)。在我们的案例中,我们正在运行一台通用服务器,因此我们将选择 Id: xccdf_org.ssgproject.content_profile_standard 的配置文件。

  2. 要运行此扫描并将输出保存为人类可读的 HTML 报告,你需要运行如下命令:

$ sudo oscap xccdf eval --profile xccdf_org.ssgproject.content_profile_standard --report /var/www/html/report.html ./scap-security-guide-0.1.47/ssg-ubuntu1804-ds.xml

我们必须使用 sudo 运行此命令,因为它需要访问一些核心系统文件,否则无法访问。扫描运行并在屏幕上生成一个清晰的人类可读的输出,以下截图展示了其中的一个示例:

如你所见,XCCDF 策略生成了一个高度可读的输出,清晰地显示了每个测试的通过/失败结果。因此,即便在这些输出的前几行中,你也可以看到我们的测试系统在多个方面不符合要求。

此外,oscap 命令还生成了一个漂亮的 HTML 报告,我们已将其放入此服务器的 Web 根目录。当然,你不会在生产环境中这样做——你最不希望的事情就是公开你服务器的任何安全问题!然而,你可以将此报告发送给你的 IT 安全团队,如果你是使用 Ansible playbook 运行 OSCAP,Ansible 可以将报告从远程服务器复制到一个已知位置,便于收集报告。

下面的截图展示了这份 HTML 报告的一部分——你可以看到它的可读性如何。此外,即使是快速浏览,非技术人员也可以看到该系统未通过合规性测试,并且需要采取补救措施:

突然间,你会意识到这个工具是多么强大,以及为什么你希望使用它来扫描你的基础设施!除了这个报告,我们还可以使用我们在《安装其他 OpenSCAP 策略》一节中下载的com.ubuntu.bionic.cve.oval.xml策略来检查我们的测试系统的补丁状态。正如我们讨论的,OVAL 策略生成的报告不如 XCCDF 报告那样易于阅读,但它们仍然非常有价值。要扫描我们的 Ubuntu 系统,查看是否缺少任何关键的安全补丁,你可以运行以下命令:

$ sudo oscap oval eval --report /var/www/html/report-patching.html com.ubuntu.bionic.cve.oval.xml

如下图所示,输出不如 XCCDF 输出那样易于阅读,需要更多的解释。简而言之,false结果意味着被扫描的机器未通过合规性测试,因此推测所需的补丁已经应用,而true则表示系统缺少补丁:

然而,再一次,HTML 报告来拯救了我们—首先,它在顶部有一个总结部分,显示我们的系统总共有 432 个检测到的包漏洞,同时还有 8,468 次测试通过。因此,正如我们通过运行的策略文件理解的那样,我们迫切需要应用补丁来修复已知的安全漏洞:

当然,定期下载这个策略的更新版本是非常重要的,以确保它是最新的。如果你深入报告,你会看到每个检查项都有一个交叉引用的 CVE 漏洞报告,这样你就可以查找系统存在的漏洞:

通过这些简单的示例,我相信你可以看到这些报告是多么有价值,以及它们是如何被 IT 安全团队在没有任何特定 Linux 命令行知识的情况下轻松审查的:

在 CentOS 或 RHEL 上运行 OSCAP 扫描的过程大致相似:

  1. 假设你正在使用操作系统供应商提供并随操作系统一起打包的 SSG 策略,你需要查询 XCCDF 配置文件,以便了解应该运行哪一个:
$ oscap info /usr/share/xml/scap/ssg/content/ssg-centos7-xccdf.xml
  1. 然后,你可以像我们在 Ubuntu 上做的那样,运行基于 XCCDF 的扫描—在这里,我们选择标准配置文件来扫描我们的系统:
$ sudo oscap xccdf eval --fetch-remote-resources --report /var/www/html/report.html --profile standard /usr/share/xml/scap/ssg/content/ssg-centos7-xccdf.xml

你会看到这里也有--fetch-remote-resources标志—这是因为 CentOS 7 策略需要一些额外的内容,它会直接从 Red Hat 下载,以确保始终使用最新的版本。扫描过程与之前相似,生成相同的可读报告。你会看到,扫描运行时,许多测试结果返回notapplicable—不幸的是,CentOS 7 的安全策略仍在不断完善中,目前随 CentOS 7 附带的版本并没有完全支持该操作系统。这展示了 OpenSCAP 策略的严苛性——大多数 CentOS 7 的安全要求也适用于 RHEL 7,反之亦然,但这些策略被编码为非常特定地与某些操作系统配合使用。以下截图展示了正在进行的扫描以及前述的notapplicable测试结果:

尽管如此,审计仍然揭示了一些有价值的见解——例如,正如我们从以下 HTML 报告截图中看到的那样,我们不小心允许了密码为空的账户登录:

如果你正在使用 CentOS 7,你将不会从 Red Hat 获得供应商支持,因此尝试使用上游 SSG 策略是值得的,因为 CentOS 和 Ubuntu 等操作系统的支持在不断改善(正如我们在本节早些时候审计 Ubuntu Server 18.04 主机时看到的那样)。重新运行完全相同的扫描,但使用 SSG 0.1.47 后,我们的扫描结果看起来非常不同:

这突出了理解你所使用的策略的重要性,并确保你下载适合你情况的正确版本。如果你使用的是 RHEL 7,建议使用 Red Hat 提供的包,而对于 CentOS 7 和 Ubuntu Server 18.04,则最好尝试使用来自上游 GitHub 仓库的最新版本。实际上,以下截图显示了在我们的 CentOS 7 测试系统上,使用版本 0.1.47 SSG 进行的完全相同扫描的结果,我们可以看到这次我们总共运行了 958 个测试,并且对服务器的安全性有了更清晰的了解:

在 CentOS 7 上,你也可以像在 Ubuntu Server 上一样运行 OVAL 扫描来检查包漏洞,不过这次需要使用我们之前下载的com.redhat.rhsa-RHEL7.xml文件。就像我们在 Ubuntu Server 上做的那样,我们将使用以下命令运行此扫描:

$ sudo oscap oval eval --report /var/www/html/report-patching.html com.redhat.rhsa-RHEL7.xml

报告的解读与在 Ubuntu 上完全相同,如果我们直接参考 HTML 报告,可以看到此系统目前已经完全修补了已知的包漏洞:

这部分总结了我们对oscap命令行工具的探讨,但现在你应该已经拥有了所有必要的信息,能够定期运行自己的扫描。自动化这个过程作为练习留给你,但以下是我认为一个不错的 Ansible 解决方案的几点建议:

  • 在执行任何其他任务之前,使用yumapt模块安装所需的 OpenSCAP 软件包。

  • 使用get_url模块下载 SSG 文件和/或包漏洞 OVAL 定义文件,以确保你拥有最新的副本(在 RHEL 7 上除外,此时你应使用由 Red Hat 提供的版本)。使用unarchive模块解压下载的文件。

  • 使用shell模块运行 OSCAP 扫描。

  • 使用fetch模块获取 HTML 报告的副本,以便分发和分析。

在接下来的部分中,我们将介绍如何使用 OpenSCAP 守护进程运行定期调度的扫描。

使用 OpenSCAP 守护进程运行定期扫描

现在你已经了解了使用oscap命令行工具进行扫描的基础,设置定期扫描 OpenSCAP 守护进程将变得非常简单,因为所涉及的技术是相同的。假设你已经安装了守护进程,正如我们之前讨论的那样,创建自动化扫描相对容易,尽管在撰写本文时,OpenSCAP 守护进程并不支持在 Ubuntu Server 18.04 上运行。这是由于缺失的 CPE 文件,到目前为止尚未修复,虽然这并未影响我们使用oscap命令行工具(尽管细心的人可能会注意到扫描结束时与该文件相关的错误),但它确实阻止了 OpenSCAP 守护进程的启动。

因此,本节中的示例将仅基于 CentOS 7,然而,当 OpenSCAP 软件包修复后,在 Ubuntu Server 18.04 上的过程会大致相似。事实上,根据ComplianceAsCode GitHub 项目,2017 年 10 月首次报告的这个问题似乎已经存在较长时间,因此,这是一个很好的理由,可以结合使用 Ansible 和oscap工具进行扫描需求。

当与 Ubuntu 相关的问题修复后,你将能够从一个中央扫描主机调度 CentOS 和 Ubuntu 主机的扫描,方法与本章所述的相同。请注意,所有主机的 SSG 文件(无论是 CentOS、RHEL 还是 Ubuntu)必须与 OpenSCAP 守护进程存放在同一主机上——它们会在每次扫描时被复制到待扫描的每个主机,因此不需要在每台主机上都部署。

尽管如此,如果你想设置一个使用 OpenSCAP 守护进程的定期扫描,最简单的方法是通过在交互模式下使用oscapd-cli工具:

  1. 这是通过使用以下参数调用oscapd-cli来实现的:
$ sudo oscapd-cli task-create -i
  1. 这将启动一个基于文本的引导配置,您可以轻松完成。下面的截图展示了我如何设置守护程序,在我的 CentOS 7 测试系统上运行每日扫描的示例:

大多数交互式设置步骤应该是不言自明的——然而,您会注意到一个询问在线修复的步骤。OpenSCAP 配置文件包括自动纠正任何发现的合规问题的功能。您可以自行决定是否启用此功能,这取决于您是否愿意让自动化流程对系统进行更改,即使是出于安全目的。您可能希望将审计任务与策略执行任务分开,这种情况下,您将使用 Ansible 进行修复步骤。

如果您启用了修复功能,请务必首先在隔离环境中进行测试,以确保修复步骤不会破坏任何现有应用程序。这种测试不仅在应用程序代码更改时需要执行,还需要在下载新版本的 SSG 时进行,因为每个新版本可能包含新的修复步骤。这与我们在 第十三章 中探讨的指导原则相同,使用 CIS 基准,只是现在应用于 OpenSCAP SSG。

  1. 一旦您启用了扫描,您将发现在预定的时间,它会将扫描结果存储在 /var/lib/oscapd/results 下。在此目录下,您会找到一个以您创建任务时获得的任务 ID(在上述截图中为 1)命名的编号子目录,然后在另一个编号目录下,即扫描编号。因此,任务 ID 为 1 的第一次扫描的结果将在 /var/lib/oscapd/results/1/1 中找到。

  2. 当您检查此目录的内容时,您会注意到结果仅以 XML 文件存储,虽然适合进一步处理,但不太易读。幸运的是,我们之前看过的 oscap 工具可以轻松将扫描结果转换为人类可读的 HTML——对于这个结果,我们将运行以下命令:

$ sudo oscap xccdf generate report --output /var/www/html/report-oscapd.html /var/lib/oscapd/results/1/1/results.xml

一旦此命令运行完成,您可以像我们在本章早些时候所做的那样,在 Web 浏览器中查看 HTML 报告。当然,如果您在此机器上没有运行 Web 服务器,您可以将 HTML 报告简单地复制到具有 Web 服务器的主机上(甚至在您的计算机上本地打开)。

设置 OpenSCAP 守护程序的美妙之处在于,与 oscap 工具不同,它可以扫描远程主机以及本地主机。此扫描通过 SSH 执行,并且您必须确保已经从运行 OpenSCAP 守护程序的服务器设置了无密码 SSH 访问到远程主机。如果您使用非特权帐户登录,您还应确保该帐户具有 sudo 访问权限,同样不需要密码。对于任何有经验的系统管理员来说,这应该是相当容易设置的。

在 CentOS 7 上,默认的 SELinux 策略阻止了远程扫描在我的测试系统上运行。我不得不暂时禁用 SELinux,以便远程扫描能够运行。显然,这不是一个理想的解决方案——如果你遇到这个问题,最好构建一个允许远程扫描运行的 SELinux 策略。

一旦你设置了远程访问,按照交互式任务创建过程配置 OpenSCAP 守护进程与本地机器配置并无不同——这次唯一的区别是你需要按以下格式指定远程连接:

ssh+sudo://<username>@<hostname>

如果你直接以root身份登录(不推荐),你可以省略前面字符串中的+sudo部分。因此,为了从我的测试服务器设置另一个远程扫描,我执行了以下截图中显示的命令:

如你所见,这为此目的创建了任务编号2。这种设置的优点是,一旦你设置了 SSH 和 sudo 访问权限,你可以指定一个主机负责扫描你所有的 Linux 服务器。并且,正在扫描的主机只需要存在 OpenSCAP 库——它们不需要 OpenSCAP 守护进程或安全策略文件——这些都会在远程扫描过程中自动传输到主机。

定时扫描的结果仍然以 XML 格式存储在/var/lib/oscapd/results目录中,和之前一样,可以根据需要进行分析或转换为 HTML。

OpenSCAP 守护进程几乎肯定是你扫描基础设施的最快和最简便的途径,并且它将所有结果本地收集并存储,同时使用存储在自己文件系统上的安全策略,这意味着它相对抗篡改。对于基于 SCAP 的自动化、持续扫描环境,OpenSCAP 守护进程几乎肯定是你最好的选择,你还可以创建一个cron任务,自动将 XML 结果转换为 HTML 并将其放入你的 Web 服务器根目录,以便查看。

最后但同样重要的是,在下一节中,我们将介绍 SCAP Workbench 工具,看看它如何帮助你进行安全审计。

使用 SCAP Workbench 进行扫描

SCAP Workbench 工具是一个交互式的、基于图形界面的工具,用于运行 SCAP 扫描。它几乎具备与oscap命令行工具相同的功能,只是它可以通过 SSH 扫描远程主机(类似于 OpenSCAP 守护进程)。使用 SCAP Workbench 的高层次流程与使用oscap相同——你从下载的策略中选择你的策略文件,选择其中的配置文件,然后运行扫描。

然而,这次结果在图形界面中显示,并且不需要生成 HTML 报告并在浏览器中加载就能轻松解释。以下截图显示了在命令行上运行以下命令与oscap的等效操作:

$ sudo oscap xccdf eval --profile xccdf_org.ssgproject.content_profile_standard ./scap-security-guide-0.1.47/ssg-ubuntu1804-ds.xml

需要明确指出的是,扫描不会生成报告文件,但你可以通过点击屏幕底部的“保存结果”按钮,生成 HTML 或 XML 格式的报告:

如你所见,如果你需要对系统进行交互式和即时扫描,SCAP Workbench 是最简单的方法。唯一的限制是它只能处理 XCCDF 文件,因此用于检查你是否存在包漏洞的 OVAL 文件不能在此处使用。

在本节中,我们探讨了你可以使用各种 OpenSCAP 工具来扫描你的基础设施的方式。我们还展示了各种扫描,其输出通常非常易于理解。然而,在下一节中,我们将更深入地探讨这些内容,然后完成关于 OpenSCAP 的工作。

解释结果

到目前为止,我们已经看到 OpenSCAP 扫描,特别是基于 XCCDF 的扫描,生成了易于阅读的报告,你可以很轻松地采取行动。然而,如果报告对你来说不清晰,那么你就无法知道需要修复什么,以弥补不合规的地方。

幸运的是,我们之前用于检查漏洞包的 OVAL 策略和基于 XCCDF 的报告包含了足够的信息,可以让你做两件事。

让我们以之前使用 SSG 版本 0.1.47 扫描 CentOS 7 服务器的示例为例。在此过程中,我们未通过一个叫做Disable ntpdate Service (ntpdate)的检查,此外还有其他未通过的检查。假设这个结果对你来说并不明显,你不确定底层的问题是什么,也不明白为什么它会成为一个问题。幸运的是,在这个扫描生成的 HTML 报告中,你可以点击检查标题。屏幕上应该会弹出一个窗口,类似于下面的截图:

在这里,你可以看到所有你可能需要的详细信息——从扫描的细节到各种安全标准所引用的参考文献和标识符,这些标准提出了这个建议,甚至包括可以用来纠正问题的手动命令,以便系统在下一次扫描时符合要求。

更好的是,如果你向下滚动这个屏幕,你会发现许多最新版本的 SSG(包括版本 0.1.47)实际上包含了大量的 Ansible 代码,可以应用于解决这种情况,如下图所示:

因此,通过一些探索,你实际上可以利用这些扫描结果,不仅找出为什么你的基础设施不符合要求,还可以生成你需要的准确修复措施。

OpenSCAP 还可以修复(即解决)在扫描过程中发现的问题,帮助你进行审计并保持合规性。然而,我们在这里没有探索这个内容,因为在尝试自动修复之前,你必须了解扫描的内容以及它们会做什么。因此,这留给你作为练习——不过,你会看到在 OpenSCAP 守护进程和 SCAP 工作台中,都有一个简单的选项可以启用,它不仅会执行扫描,还会尝试修复。

尽管我们已经确认 XCCDF 配置文件是多么强大和用户友好,但我们也看到 OVAL 配置文件生成的报告可读性稍差。幸运的是,如果你查看下面的截图,你会注意到,识别出的漏洞的 CVE 编号实际上是超链接:

点击这些链接会将你带到操作系统供应商的官网,直接进入一个详细说明漏洞、受影响的包以及修复实施时间的页面。因此,你可以准确找出需要更新哪些包来解决该问题。

这就是我们审计 Linux 环境与 OpenSCAP 的全部内容——希望你觉得这有用,并且能够将其应用到你的环境中,以提升你的安全性和审计流程。

总结

关注 Linux 基础设施的安全合规性变得越来越重要,考虑到大量的安全建议,加上现代企业中可能存在的大量 Linux 服务器,显然需要一个能够进行合规性审计的工具。OpenSCAP 提供了正是这样一个框架,只要稍加注意和关心(并应用正确的安全配置文件),就能轻松审计整个 Linux 环境,并为你提供有价值、易于阅读和解读的合规性报告。

在本章中,你亲自体验了安装 OpenSCAP 工具进行服务器审计,了解了可用的政策以及如何在 OpenSCAP 中有效使用它们。接着,你学习了如何使用各种 OpenSCAP 工具审计 Linux 服务器,并最终探索了如何解读扫描报告,以便采取适当的措施。

在本书的下一章,也是最后一章中,我们将介绍一些技巧和窍门,帮助你简化自动化任务。

问题

  1. SCAP 代表什么?

  2. 为什么 SCAP 政策在审计你的 Linux 基础设施时是一个宝贵的工具?

  3. 你会使用哪个 OpenSCAP 工具来定期集中扫描多个 Linux 主机?

  4. XCCDF 文件和 OVAL 文件之间有什么区别?

  5. 在什么情况下,即使供应商提供的 SSG 政策比当前可用的政策要旧,你也会使用这些政策?

  6. 为什么使用 RHEL 7 政策文件扫描 CentOS 7 主机时,扫描结果会显示为 notapplicable

  7. 你能从 OpenSCAP 守护进程生成的 XML 结果中生成 HTML 报告吗?

  8. SCAP Workbench 或 OpenSCAP Daemon 执行远程 SSH 扫描需要满足什么要求?

进一步阅读

posted @ 2025-07-04 15:40  绝不原创的飞龙  阅读(24)  评论(0)    收藏  举报