DebOps-工程师的-Puppet8-指南-全-

DebOps 工程师的 Puppet8 指南(全)

原文:annas-archive.org/md5/74e7dee08e6c205ecc2a82f2d11edba8

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

随着 DevOps 和平台工程推动了对强大内部开发平台的需求,基础设施自动化解决方案的需求比以往任何时候都更加迫切。Puppet 是全球最大企业使用的最强大基础设施自动化解决方案之一,并且拥有强大的开源社区。本书全面介绍了 Puppet 语言和平台。从 Puppet 作为一种有状态语言的基本概念和工作方式入手,逐步讲解如何构建 Puppet 代码,使其具备可扩展性,并允许团队间的灵活性与协作。接下来,将探讨 Puppet 平台如何实现基础设施配置的管理和报告,展示如何将 Puppet 平台与 ServiceNow 和 Splunk 等其他工具集成。最后,本书还将讨论如何将 Puppet 实现应用于高度监管和审计的环境,以及现代混合云环境。

通过本书,你将全面了解 Puppet 语言和平台的功能,并能够构建和扩展 Puppet,以创建一个提供企业级基础设施自动化的平台。

本书适用对象

本书非常适合希望使用 Puppet 自动化基础设施配置的 DevOps 工程师。它专门聚焦于 Puppet 的配置管理能力,但也涉及到一般的其他基础设施管理实践。无论是初学者还是当前的 Puppet 用户,都能通过本书全面了解 Puppet 语言和平台的全部功能。

本书内容

第一章Puppet 概念与实践,重点介绍了为什么要开发 Puppet,它是如何随着时间变化的,以及 Puppet 的核心概念和实践。同时,还探讨了 Puppet 如何帮助实现 DevOps 转型以及我们对此的具体做法。

第二章重大变化、有用工具与参考资料,讨论了 Puppet 5 之后出现的重大变化,如有害术语、敏感值、延迟函数等高层次的内容,还会介绍一些已被 Puppet 抛弃的项目。此外,本章将介绍一些有助于开发的工具,如 VS Code 和Puppet 开发工具包PDK),并展示实验室和开发环境如何为本书的内容提供支持。还将展示各种 Puppet 和社区的参考资料,供进一步学习。

第三章Puppet 类、资源类型与提供者,介绍了 Puppet 的最基本构建模块以及如何使用它们,使你能够理解编写 Puppet 代码的初步阶段,展示了资源类型和提供者如何协同工作,创建与底层操作系统实现无关的有状态代码,以及类如何将这些资源进行分组。

第四章变量和数据类型,详细介绍了如何在 Puppet 中为变量分配数据类型,如何在数组和哈希中管理它们,如何使用敏感数据类型来保护变量,以及如何管理变量的作用域。然后,我们将提供一些关于如何在 Puppet 中有效使用这些变量和数据结构的最佳实践建议。

第五章事实与函数,探讨了 Puppet 提供的事实和因素,如何在 Puppet 代码中使用它们,以及如何自定义它们。它还将讨论函数:它们是什么,如何与 lambda 一起使用,以及如何使用相对较新的延迟函数。

第六章关系、排序与作用域,讲解了 Puppet 如何处理关系和排序,以及作用域和包含。这些问题结合在一起,帮助用户理解跨模块或跨类的资源和变量是如何交叉的。

第七章模板、迭代和条件语句,展示了如何使用模板、迭代、循环和各种条件语句,如if语句和选择器,来影响代码的流动和管理。

第八章开发和管理模块,讨论了模块的结构,使用 PDK 创建模块的方法,以及如何测试模块。还将讨论如何有效使用 Puppet Forge 来使用和分享代码,并了解共享模块的质量。

第九章使用 Puppet 处理数据,介绍了 Puppet 如何处理数据,讨论了什么是 Hiera,在哪些层级存储数据,以及在结构和方法上要避免的一些陷阱和错误。

第十章Puppet 平台的组成部分和功能,帮助你了解 Puppet 作为一个平台的构成,各个组件如何协同工作和通信,以及常见的架构方法以实现扩展性。

第十一章分类与发布管理,讨论了 Puppet 如何在环境中管理服务器和代码,如何对服务器进行分类,以及这种分类的 Puppet 运行实际是如何执行的。还将讨论部署代码到这些环境中的工具。

第十二章用于编排的 Bolt,讨论了如何使用 Bolt 作为程序任务的编排工具,展示了通过 Puppet 代理使用的各种传输选项——SSH、WinRM 和 PCP。你将看到任务和计划如何补充 Puppet 代码,以及如何通过 Bolt 本身编排和部署 Puppet 代码。

第十三章深入 Puppet 服务器,探讨了更高级的话题,确保你能够监控和扩展基础设施,处理常见问题,并整合外部数据源。

第十四章Puppet Enterprise 简介,突出了 Puppet Enterprise 与开源版本的差异,以及可用的集成和服务,以帮助扩展和调整基础设施。

第十五章采用方法,讨论了如何在实际的棕地环境中采用并使用 Puppet,强调了在该领域和各种采用过程中获得的经验教训,并着眼于正确界定使用案例以便定期交付。它将探讨 Puppet 如何在平台工程中工作,如何与传统遗产平台以及高度监管和变更管理的环境兼容。

如何最大化本书的价值

需要一些 Unix 和 Windows 系统的系统管理背景知识以及应用程序部署的基础知识。此外,还需要一些核心开发概念的知识,例如版本控制工具(Git)、虚拟化和测试工具,以及编码工具(如 vi 或 Visual Studio Code)。

本书中涉及的软件/硬件 操作系统要求
Puppet 7 或 8 Windows、macOS 或 Linux
Bolt Windows、macOS 或 Linux
Visual Studio Code Windows、macOS 或 Linux
Azure
Puppet 开发工具包 (PDK) Windows、macOS 或 Linux
PEADM 模块 Windows、macOS 或 Linux

实验环境所需软件的完整配置将在 第二章 中进行介绍。

如果你正在使用本书的数字版本,我们建议你自己输入代码,或者从本书的 GitHub 仓库访问代码(链接将在下一节提供)。这样做有助于避免与复制和粘贴代码相关的潜在错误。

下载示例代码文件

你可以从 GitHub 下载本书的示例代码文件,地址是 github.com/PacktPublishing/Puppet-8-for-DevOps-Engineers。如果代码有更新,它将会在 GitHub 仓库中同步更新。

我们还有来自我们丰富书籍和视频目录中的其他代码包,可以在 github.com/PacktPublishing/ 获取。快来看看吧!

下载彩色图片

我们还提供了一个 PDF 文件,其中包含本书中使用的截图和图表的彩色图片。你可以在这里下载:packt.link/vPsXh

使用的约定

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

Code in text:表示文本中的代码词汇、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 用户名。例如: “查找函数键,data_hash,接受yaml_datajson_datahocon_data作为值,但大多数 Puppet 实现仅使用 YAML 数据,因此本书默认使用yaml_data后端。”

一段代码块设置如下:

hierarchy:
- name: "YAML layers"
  paths:
    - "nodes/%{trusted.certname}.yaml"
    - "location/%{fact.data_center}.yaml"
    - "common.yaml"

当我们希望引起您注意代码块中的某个特定部分时,相关的行或项会加粗显示:

 type { 'title': 
   attribute1 => value1, 
   attribute2 => value2, 
 }

任何命令行输入或输出如下所示:

bolt --verbose plan run pecdm::provision @params.json

加粗:表示一个新术语、一个重要词汇或您在屏幕上看到的文字。例如,菜单或对话框中的文字显示为加粗。例如:“从管理面板中选择系统信息。”

提示或重要说明

显示如下。

联系我们

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

customercare@packtpub.com,并在邮件主题中注明书名。

勘误:尽管我们已尽力确保内容的准确性,但难免会有错误。如果您在本书中发现错误,我们将不胜感激,请访问www.packtpub.com/support/errata并填写表格。

copyright@packt.com,并附上相关材料的链接。

如果您有兴趣成为作者:如果您在某个话题上有专业知识,且有意写书或为书籍作贡献,请访问authors.packtpub.com

分享您的想法

阅读完Puppet 8 for DevOps Engineers后,我们很想听听您的想法!请点击这里直接访问此书的 Amazon 评论页面并分享您的反馈。

您的评论对我们和技术社区非常重要,能够帮助我们确保提供高质量的内容。

下载本书的免费 PDF 版本

感谢购买本书!

您喜欢随时随地阅读,但无法随身携带印刷版书籍吗?

电子书购买不兼容您选择的设备吗?

不用担心,现在每本 Packt 书籍,您都能免费获得该书的无 DRM PDF 版本。

随时随地、在任何设备上阅读。直接将您最喜欢的技术书籍中的代码搜索、复制并粘贴到您的应用程序中。

优惠不仅仅是这些,您还可以获得独家折扣、时事通讯,并每日收到丰富的免费内容到您的邮箱

按照以下简单步骤获取福利:

  1. 扫描二维码或访问以下链接

https://packt.link/free-ebook/9781803231709

  1. 提交您的购买凭证

  2. 就是这样!我们将直接把免费的 PDF 文件和其他福利发送到您的邮箱

第一部分 – Puppet 简介及 Puppet 语言基础

本部分将建立 Puppet 的核心概念,阐述 Puppet 的功能、如何与 DevOps 方法相结合,以及本书中我们将如何处理这些内容。接下来,我们将对 Puppet 的核心组件进行高层次的概览。本书中使用的开发实验环境将被回顾,并提供有用的参考资料和进一步学习资源。然后,我们将从语言的基础开始,介绍类、资源、变量和函数。

本部分包含以下章节:

  • 第一章Puppet 概念与实践

  • 第二章主要变化、实用工具与参考资料

  • 第三章Puppet 类、资源类型与提供者

  • 第四章变量与数据类型

  • 第五章事实与函数

第一章:Puppet 的概念和实践

本章将重点介绍 Puppet 的起源,为什么它被创建,以及它如何在 DevOps 工程中使用。它将探讨 Puppet 的配置管理方法,以及其声明式方法与更常规的过程性语言有何不同。Puppet 拥有许多在其他语言中常见的特性,如变量、条件语句和函数。但在本章中,我们将介绍语言的关键术语、结构和思想,以及它与客户需求和基础设施环境的关系。最后,鉴于关于 Puppet 有很多先入为主的看法,本章将结束时解决一些最常见的误解,包括它们的来源,并将其解开。

这将确保在我们在接下来的章节中深入了解语言之前,能够对 Puppet 及其方法有一个基本的理解。它还将确保本书不仅仅关于技术,而是关于如何通过 Puppet 提供的服务为客户创造真正的价值。

在本章中,我们将涵盖以下主要内容:

  • Puppet 的历史和与 DevOps 的关系

  • Puppet 作为声明式和幂等的语言

  • Puppet 语言中的关键术语

  • Puppet 作为一个平台

  • 常见误解

Puppet 的历史和与 DevOps 的关系

Puppet 由创始人卢克·凯恩斯(Luke Kaines)创建,他曾是系统管理员和顾问。由于找不到自己想要使用且客户可以依赖的工具,他于 2005 年创建了 Puppet,作为一个基于 Ruby 的开源配置管理语言。这个开源项目的成功促使在 2011 年 2 月推出了商业版本 Puppet Enterprise。但随着需求的增加,Puppet 在作为公司和开源项目的改革和扩展中,卢克选择了退出,表示将 Puppet 发展到企业级规模的挑战是远离我最喜欢做的事情,远离我的核心技能。我们需要扩展,并且需要 执行

随后接任的领导层采取了一个方向,使公司发展了其专业服务,并在扩展产品范围时,更多地关注开发者工具和教育,同时在开源社区和企业客户需求之间寻求艰难的平衡。Puppet 于 2022 年 5 月 17 日被 Perforce Software 收购,成为继 Chef(2020 年)和 Ansible(2015 年)收购之后,最后一家独立的配置管理初创公司。卢克总结了行业发生的变化:如今,DevOps 团队有所不同。公司正在寻找一个完整的解决方案,而不是想要集成单个 最优质的供应商。

这一历史见证了 Puppet 从一种让开发者决定如何使用它来解决问题的工具,发展到今天,成为一种具有模式和解决方案的工具,用户可以直接使用这些模式和解决方案来标准化他们的自动化和部署。这使得用户能够专注于他们的解决方案,而不是底层技术。

DevOps 本身在 IT 行业已成为一个令人沮丧的术语;正式来源给出的定义与公司实际使用它的方式有很大差异,且对其的引用往往被用作讽刺性的流行语或销售噱头。本书的重点是 DevOps 工程,尤其是在大公司中的应用,这些内容已经在如 Puppet 主办的DevOps 状况报告等研究中得到了深入的研究和讨论。DevOps 工程通常作为数字化转型、云优先迁移和其他各种现代化项目的一部分进行交付。在这些项目中,通常可以看到的目标是自动化自助部署、合规性并消除繁琐的操作。此方法遵循 DevOps 目标,即通过促进更好的沟通和建立共同目标来打破开发和运维团队之间的壁垒。值得注意的是,Luke 最初所工作的系统管理员角色,实际上已被 DevOps 工程师等新角色所取代。

Puppet 将作为 DevOps 工具链的一部分使用,图 1.1 展示了一组工具及其相对功能的示例。通常,Puppet 的作用开始于一个提供管道的末端,当基础设施在平台中搭建好并需要进行配置和执行时,Puppet 就会介入:

图 1.1 – DevOps 工具集

图 1.1 – DevOps 工具集

本书不仅专注于技术理解,还将着重于如何利用 Puppet 语言、工具和平台的成熟度以及带有明确观点的模式。这些方法是通过多年的客户合作以及 Puppet 和社区自身实现的经验发展而来,旨在帮助用户减少寻找合适方法的努力,专注于解决方案,并为客户带来即时的收益和回报。

Puppet 作为一种声明式和幂等的语言

理解的第一件重要事情是了解 Puppet 与普通的脚本或编程语言有何不同。Puppet 是声明式的,这意味着你描述的是你希望系统达到的状态。例如,你可以描述系统应该有一个名为username的用户,UID 为1234,配置文件不应该存在,内核设置应该是某个特定的值。与大多数语言不同,Puppet 的方法不要求描述如何达到这个状态,而是更接近客户请求服务的方式。客户并不关心过程如何,只关心最终结果能满足他们的需求。这些资源定义可以保存在你的版本控制系统中。通常,这种方法被描述为基础设施即代码的一部分。

Puppet 是幂等的,这意味着它只会进行必要的更改,以使系统达到声明的状态。而大多数过程性语言每次运行时都会执行步骤,并通常需要添加诸如if语句等各种检查,以避免重复。这一特性非常强大,因为所谓的强制执行可以通过 Puppet 语言来实现,确保你声明的状态已经达成,并能够检测是否是你更新了目标机器的状态导致了变化,或者变化是机器本身的变化,偏离了期望的状态。这在审计中非常有帮助,可以避免配置漂移,确保变更是经过管理且有意为之。

Puppet 是操作系统无关的;它关注的是系统状态,而不是特定操作系统如何安装软件包或添加用户的实现方式。这为我们提供了一种通用语言,不依赖于任何底层实现,减少了代码的重复,避免了使用case/if语句来检测差异的需求,并允许多种语言实现,比如 Windows 的 PowerShell 和基于 Unix 的系统的 Bash。此外,它还使得在应用代码失败后更容易恢复。如果在过程性语言中某个步骤失败,根据检查步骤的编写方式,可能无法安全地重新运行整个脚本。而 Puppet 代码则能够仅执行必要的步骤,以便恢复到正确的状态。

一个简单的 Puppet 代码示例如下,用于创建一个用户:

user { 'david'
  uid => '123'
}

相比之下,一个 shell 脚本可能包含如下部分:

if ! getent passwd david; then
  useradd -u 123 david
elif ! $(uid david) == 123; then
  usermod -u 123 david
fi

在上面的 shell 示例中,我们需要检查一个用户是否存在,如果不存在,就创建一个。如果它存在,那么它的 UID 是否正确?如果不正确,我们将进行更改。这个脚本仅覆盖能够使用 useraddusermod 的操作系统。为了实现跨多个操作系统的兼容性,我们需要检测操作系统类型并为每个操作系统或操作系统组及其所需的命令编写类似的代码段。通常,为了涵盖更广泛的操作系统版本,编写多种语言和脚本是更实用的做法,例如,如果我们想同时支持 Unix 和 Windows。

这与 Puppet 声明相对比,后者无需更改就可以在多个操作系统上工作,因为 Puppet 会检测所需的命令,并作为一部分执行所有必要的状态检查。

这个示例仅仅涉及一个具有单个属性的资源。你可以很快看到,随着检查项和选项的不断增加,shell 脚本示例将变得越来越复杂,并且难以扩展。

Puppet 语言中的关键术语

详细查看 Puppet 语言,Puppet 中最基本的元素是资源。每个资源描述系统的某个部分以及你希望它处于的理想状态。每个资源都有一个类型,它是 Puppet 语言中该资源如何配置的定义,包括哪些属性可以设置,以及可以使用哪些提供者。属性描述的是状态。因此,对于一个用户来说,属性可能是家目录;对于文件而言,属性可能是权限。提供者使得 Puppet 跨操作系统独立工作,因为它们执行底层命令,无论是创建用户还是安装软件包。

所以,让我们以一个公司为例,该公司通常会向环境团队提交构建请求表单,要求配置服务器:

表格 1.1 – 构建请求表单示例

表格 1.1 – 构建请求表单示例

表格 1.1中,请求表单里我们看到有用户、用户组和目录的分组,它们本质上都是类型。它们下面的每一项都是一个资源,而配置设置则是属性。

这个请求可以转化为如下内容:

user { 'exampleapp':
  uid => '1234'.
  gid => '123'
}
group { 'exampleapp':
  Gid => '123'
}
file { '/opt/exampleapp/':
  owner => 'exampleapp',
  group => 'exampleapp',
  mode  => 755
}
file { '/etc/exampleapp/':
  owner => 'exampleapp',
  group => 'exampleapp',
  mode  => 750
}

上面的示例展示了 Puppet 如何更加直接地转换为用户请求,并且即使不理解 Puppet 语言,也能保持可读性。

在这个示例中,未显示的是 usermod 提供者。如果我想使用 LDAP 命令创建用户,我将把我的 provider 属性设置为 LDAP。

下一个重要的注意事项是,由于 Puppet 是以有状态的方式编写的,我们并不是编写一个逐行执行的有序过程,而只是声明资源的状态,这些资源可以以任何顺序实现。因此,如果我们有任何依赖关系,就需要使用 relationship 参数;它描述了一个前后关系,正如字面意思,或者是一个订阅/刷新,例如,更新配置文件可能会导致服务重启。在之前的示例中,Puppet 会自动创建某些依赖关系,例如确保在用户之前创建组,因此我们不必添加 relationship 参数。通常,这些关系被视为适应 Puppet 时最难掌握的部分,因为许多程序员习惯于编写一个按顺序执行的过程,容易出错。这可能导致依赖关系的循环,其中一系列依赖关系循环往复,没有办法创建一个不依赖于其他资源的起始资源。

显然,我们声明的资源需要一个结构,第一步是将这些代码放入一个文件中。Puppet 将这些文件称为 .pp 文件。是 Puppet 代码块,它为我们提供了一种特定的方式来调用在主机上运行的代码段。通常,作为一种最佳实践,我们在一个 manifest 文件中只包含一个 。然后,Puppet 使用 模块 来将这些 manifest 进行分组。这个分组的原则是 模块 应该专注于做一件事并且做到极致,代表一个技术实现,例如,配置 IIS 应用程序的 模块 或者配置 postfix 作为邮件中继的 模块模块 只是一个目录结构,用来存储 manifest 和其他 Puppet 项目(我们将在 第八章 中详细讲解),它本身并不是语言中的关键字。因此,理想情况下,模块应该是可共享和可重用的,供不同的用户和组织使用,很多模块直接来自 Puppet Forge,即 Puppet 的模块目录,里面既有商业产品也有开源产品。

一个常见的模块风格和实践是,包含一个具有单一类的 manifest 文件,示例如下:

  • install.pp(与安装软件相关的资源分组)

  • config.pp(与配置软件相关的资源分组)

  • service.pp(与运行服务相关的资源分组)

  • init.pp(初始化模块并接受参数)

在更高层次上,我们有角色配置文件,它们用于创建您组织的结构。虽然模块应该是可共享和可重复的技术实现安装,例如 Oracle 或 IIS,角色配置文件仅在您的组织内有意义。角色配置文件,用于将模块和选定的参数组合成逻辑技术栈和客户解决方案。通常会创建一个角色模块和一个配置文件模块,同时保持使用的在一起。

到目前为止,可能会让人困惑的是,您可能会有一个 Oracle 角色、一个 Oracle 配置文件和一个 Oracle 模块。因此,虽然 Oracle 模块配置并安装 Oracle,并提供各种可用的参数以自定义安装,但 Oracle 配置文件是关于您的组织如何使用该模块,以及它可能会向该技术栈中添加其他模块。您可能会指定总是将 Oracle 与集群服务一起使用,因此您的 Oracle 配置文件包含 Oracle 模块和集群模块。或者,它可能会在配置文件中传递参数给 Oracle 模块,从而设置您组织配置的默认内核设置。

您可以将角色理解为客户在提交构建请求时实际需要的东西;他们需要特定类型的服务器,无论是 Oracle 服务器还是 IIS 服务器。他们不关心底层的实现——只关心它是否满足他们的需求。虽然 Oracle 角色肯定需要 Oracle 配置文件,但它期望满足操作系统安全标准,并且具备您组织定义的任何代理或其他支持工具。因此,对于许多组织来说,一个常见的配置文件是基本的操作系统安全标准,确保每台服务器都符合标准,这几乎是每个角色的一部分。

图 1.2 显示了刚刚描述的一个例子,即角色模块中的 Oracle 角色类,其中包括来自配置文件模块的 Oracle 配置文件类和操作系统安全配置文件类。然后,Oracle 配置文件包含一个 Oracle 模块,而os_security配置文件则包含 DNS 模块:

图 1.2 – 角色、配置文件和模块的结构

图 1.2 – 角色、配置文件和模块的结构

第八章中,我们将深入探讨更多技术细节,但从本概述中最重要的要点是理解模块提供了可共享和可重用的一次性技术安装。相比之下,角色和配置文件模式为你的组织提供了背景。角色是客户在订购服务器服务时使用的;他们不需要理解技术实现,只需要知道它符合他们的业务需求。你组织技术栈中的配置文件由技术设计师和架构师管理,他们根据组织的标准和配置来组合和指定模块。这些角色负责定义不同组件如何集成,以创建所需的技术栈。因此,虽然一个 Oracle 模块本身可以配置和安装 Oracle,但正是配置文件定义了应该传递给 Oracle 模块的具体配置,以及它可能依赖的其他模块,例如安装 NetBackup 客户端。

通过我们在模块、角色和配置文件中所讨论的内容,回到表 1.1,我们可以让客户提交构建请求表单,但不需要指定他们所需要的所有内容;他们可以简单地订购一个exampleapp角色服务器。

到目前为止,我们看到的内容适用于服务器满足所有规格且是标准的情况,但例外情况是常见的。Hiera是 Puppet 的数据信息系统,它可以用于将参数传递给角色和配置文件模型,以处理例外情况。Hiera 顾名思义是分层的。它定义了一个有序的数据源列表,以访问找到最相关的设置。这些数据源通常从所有节点的默认值到更具体的组,例如某个特定角色和单个节点的特定值。

例如,如果默认操作系统安全配置文件禁用了电子邮件服务器,但exampleapp需要它,我们可以使用以下 YAML 文件:

exampleapp.yaml

profile::os_security:email_enabled: true

类似地,如果server1需要一个不同的 UID,我们可以使用以下 YAML 文件:

server1.yaml

profile::exampleapp:uid: '1235'

创建这些模式的最重要的一个点是避免在模块中使用硬编码的值。通过使用 Hiera,你为自己提供了一种动态方式,可以在未来更改值,而无需修改代码。这可以演变为通过自助服务门户访问数据——从通过电子表格、电子邮件和讨论来订购构建的方式自动化出来,而这些构建必须由构建团队配置,而不是像 VMware vRealize Automation 或 ServiceNow 这样的门户:

图 1.3 – 示例门户

图 1.3 – 示例门户

图 1.3中,示例门户展示了如何向客户展示简化的产品。Puppet 语言的重点应当是为客户提供一致的产品,并让客户、架构师和技术人员专注于他们关心的内容,而无需自己深入技术要求或编码部分。

Puppet 作为平台

到目前为止,本章重点讨论了 Puppet 语言,但现在我们将探讨 Puppet 平台以及它如何将期望的状态应用于客户端服务器。Puppet 可以仅通过安装代理和所有文件本地运行,这在测试中很常见,但本概述将重点介绍客户端-服务器设置。在第 10、13 和 14 章中,我们将详细讨论弹性、可扩展性和更高级的运行选项。然而,目前我们将重点关注 Puppet 客户端如何与服务器通信,以请求并应用其期望的状态。

Puppet 控制下的每个客户端都会安装 Puppet 代理。图 1.4 显示了 Puppet 代理运行的步骤,本节将概述这些步骤:

图 1.4 – Puppet 代理运行生命周期

图 1.4 – Puppet 代理运行生命周期

第一步是代理通过 SSL 密钥向主服务器标识自己,或者为主服务器创建新的 SSL 密钥进行签名。这将确保服务器与客户端之间的通信安全。

下一步是客户端使用一个名为Facter的 Ruby 库。这是一个系统分析器,用于收集系统的事实。这些事实可以是操作系统版本或内存大小等内容。这些事实可以在代码中使用,或通过 Hiera 来决定主机应处于什么状态,例如 Windows Server 2022 可能需要特定的注册表设置。

然后,服务器识别应该应用于服务器的类。通常,这由所谓的端节点分类器ENC)脚本完成,该脚本基于事实和用户定义。通常,这会将一个角色类应用于服务器,正如我们在前一部分中讨论的那样,角色类会构建出配置文件和模块类的定义。

然后,主服务器会编译目录和要应用于节点的资源 YAML 文件(确保 CPU 密集型的工作发生在服务器上,而不是客户端)。

该目录随后被发送给客户端,客户端将该目录作为应有状态的蓝图,并进行任何必要的更改以在客户端上强制执行该状态。

最后,一份报告会被发送回主服务器,确认应用了哪些资源,以及这些资源是否因为 Puppet 代码的更改而需要进行调整,或者它们是否在 Puppet 控制之外被更改(可能是审计或安全漏洞)。

图 1.5中,我们看到一个 Puppet 报告的示例,展示了资源的名称、所做的更改类型以及所需更改的值。此外,报告还包括未更改资源的记录,突出显示了 Puppet 强制执行的部分:

图 1.5 – Puppet 控制台服务器报告

图 1.5 – Puppet 控制台服务器报告

默认情况下,这个周期每 30 分钟进行一次。在前面的部分中,重点讨论了语言如何自动化构建服务器。在这里,我们可以看到,通过该平台,我们可以确保所有部署的服务器都被强制执行我们设定的状态;无论是安全标准配置文件,还是我们决定更新某个实现中的设置,比如向 IIS 添加额外功能。这可以避免服务器漂移,即当服务器难以保持更新或容易受到手动错误更改或恶意违反标准的影响时。图 1.6显示了 Puppet Enterprise 的仪表板视图,清晰展示了一个服务器群体及其上次运行的状态。这突出显示了服务器是否符合我们的状态要求,或是否在上次运行时做出了更改:

图 1.6 – Puppet 控制台状态仪表板

图 1.6 – Puppet 控制台状态仪表板

到目前为止我们回顾的内容假设了一个共同的代码库,当任何代码更改发生时,所有客户端将在下一个 30 分钟内强制执行新的状态,因为代理会联系主服务器。这显然是个问题,因为漏洞将在短时间内影响所有服务器。这就是为什么 Puppet 使用git,其中版本可以声明为提交、标签或分支,我们可以在一个名为Puppetfile的文件中列出这些内容。

一个示例模块声明看起来像这样:

mod 'apache',
  :git => 'https://github.com/exampleorg/exampleapp'
  :tag => '1.2'

通过在所谓的控制仓库中维护这个git,可以通过拥有不同版本的 Puppet 文件的不同分支,来表示多个环境。

一种常见做法是根据您的组织如何分类服务器使用情况来匹配环境。通常,这意味着至少有一个开发环境和一个生产环境。因此,可以在开发服务器上测试更改,经过成功测试的更改可以部署到生产环境中。这个过程可以通过使用金丝雀环境(canary environments)来进一步测试服务器的小子集。这种方法可以根据不同组织的变化和风险设置进行定制。

我们提到的所有事实和报告,作为代理周期的一部分,都存储在PuppetDB中,这是一个基于 PostgreSQL 的前端数据库,专门用于管理 Puppet 数据,如报告和事实。它与CMDB风格的数据一起使用,可以检查某个角色的特定资源是否发生了变化,从而可能表明发生了变更违规。

因此,在这一部分中,我们已经看到 Puppet 平台提供了一种基于环境逐步部署新代码的方式。它存储有关客户端的事实以及每次运行生成的报告,提供了强大的 CMDB 视图,并在报告中提供了审核和合规性信息,我们可以确认服务器处于何种状态。这些信息都可以通过 PQL 进行搜索。这可以大大减少在审核和合规报告生成中的操作负担,并有助于避免随着标准和配置的变化而积累技术债务。

常见误解

难道 Puppet 已经过时了吗?

尖端技术的焦点已经转向无服务器和其他软件即服务SaaS)/容器化的解决方案,而在基础设施即服务IaaS)层面,Puppet 的发展已达到更高的成熟度。十年前,你可能会买这本书,认为无论是否打算使用 Puppet,它都是相关的。今天,你有一个 Puppet 解决方案需要实施或理解。

我需要了解 Ruby 才能 使用 Puppet

对于某些 Puppet 代码领域,具备 Ruby 的基础知识会有所帮助。本书将重点讲解如何良好地使用 Puppet 语言以快速获得回报,现实情况是,大多数 Puppet 专业人员并不会花太多时间在 Ruby 上进行自定义开发。即使是为 Puppet 公司工作的专家,也发现有时需要写自定义 Ruby 代码之前,可能需要等上一段时间。

Puppet 不能与我们的 变更管理 协作

一个重要的担忧是 Puppet 在治理和变更管理范围之外进行修改的想法。这通常反映了假设和与变更管理团队缺乏沟通的情况。Puppet 会强制执行你描述的状态;因此,只有在代码中描述的状态发生变化或在 Puppet 控制之外被修改时,才会发生变化。如前所述,只要达成一致,Puppet 就是定义特定资源的方式,任何对状态的更改都应该视为治理范围外的内容,因此应该恢复到原状态。后续章节将讨论如何发布代码和环境,确保 Puppet 保持适当的访问控制,从而确保其处于治理范围内。

我不能进行手动修改 或例外处理

如果用户试图绕过 Puppet,这种情况肯定会发生。为避免这种情况,明确 Puppet 的责任范围、其他工具或手动流程的责任范围以及如何在系统中请求和批准例外是非常重要的。正如第八章第九章中所讨论的,通过在模块和 Hiera 中使用参数来处理例外,可以采用一种受控的方法处理例外,并且能够在代码中保留记录。

我需要 Puppet Enterprise 才能使用附加组件 和集成

存在大量的混淆,尤其是行业分析师,他们对用户在使用 Puppet Enterprise 时所获得的内容以及开源可能带来的限制进行比较。本书将在第十四章中深入探讨这一点,但 Puppet Enterprise 的根本区别在于,你为支持、服务、预先制作的模块、基础设施和解决方案付费。如果你具备技能、开发人员和时间,所有这些功能都可以在开源中复现。最终,Enterprise 运行在开源组件上。

每个人都需要 学习 Puppet

本书的一个主要焦点是构建代码结构的重要性,以支持自服务流程。这可以避免用户在希望进行小的例外或集成时,必须像 Puppet 开发者一样学习所有内容,而只需理解你的提供内容。

它将与 其他系统 发生冲突

关键是要理解 Puppet 将负责什么,其他系统将负责什么,并清楚地记录下来。许多环境将运行多个配置管理、编排和软件管理工具。重要的是要利用它们的优势,并确保有明确的边界。

总结

本章介绍了 Puppet 是如何由 Luke Kaines 作为一种有状态语言创建的,旨在简化服务器配置管理的自动化。我们了解了使用这种有状态方法如何提供一种更自然的语言来描述用户在配置管理中的需求,并减少传统过程化方法中所涉及的复杂性。

我们概述了核心语言术语和组件,并了解它们如何通过角色、配置文件和模块来组织。这种结构提供了一种自然的方式来创建客户化的产品、技术栈和可重用的技术模块。

我们看了语言中描述的状态如何通过 Puppet 运行应用到主机上,并从这些运行中,检查了如何收集和存储有价值的审计和合规性信息到PuppetDB中。我们讨论了如何在环境中管理代码,以便在适合组织的风险承受能力和开发结构的服务器逻辑组中,以受控的方式逐步发布状态变更。

本章讨论了关于 Puppet 的一些误解,并涵盖了相关性、复杂性和灵活性等主要主题。Puppet 的成熟度和对 IaaS 的专注使其看起来不那么时髦,但通过使用 Puppet 和社区开发的模式和模块,你可以充分发挥 Puppet 的优势,为客户提供自动化、自服务配置和合规性。确保明确的边界和责任,确保 Puppet 能够与其他工具和团队集成并协同工作,避免冲突,并允许其他人与 Puppet 互动,获得其带来的好处。

在下一章中,我们将回顾自版本 5 以来,Puppet 发生的主要变化,以及最新版本 7 的变化。将提供推荐的工具,以帮助创建一个有效的开发环境,并将概述和演示实验室环境的创建。此外,还将列出额外的参考网站,供读者继续研究,并跟进 Puppet 的最新发展。这将确保在接下来的章节中,我们开始讲解技术细节时,你能够在自己的环境中进行测试和实验,并深入跟进你感兴趣的内容。

第二章:重大变化、有用工具与参考资料

本章将概述自 Puppet 5 以来到当前版本 Puppet 6.28 和 7.21 之间的主要变化。这被视为 Puppet 的现代时代,上一章回顾了 Puppet 历史中的焦点变化。本节中的变化总结还将涵盖一些早期版本 Puppet 中可能仍然可见的冗余模式和方法,因为它们在代码和各种来源中仍然可见。接着,本章将讨论工具链的建设,以创建一个高效的开发者环境,这个环境将贯穿整本书的实验室部分。目标是给出一种有见地的观点,如何开发 Puppet 代码和相应工具,以便辅助开发。用户可以在自己选择的环境中安装这些工具。实验环境将通过搭建一个简单的设置并登录来展示。最后,本章将展示如何利用现有资源保持 Puppet 更新,并深入研究感兴趣的其他主题。

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

  • 自 Puppet 5 以来的重大变化

  • Puppet 5 之前的遗留模式

  • 用于 Puppet 开发的 IDE 和工具

  • 如何部署你的 Puppet 实验室和开发工具

  • 参考资料与进一步研究

技术要求

开发环境需要具备访问互联网的操作系统,以下系统均可:

  • 使用 Homebrew 在 macOS 上安装软件

  • 使用 Chocolatey 在 Windows 10/11 或 Windows Server 上安装软件

  • 使用包管理工具的 Linux 环境,如 Ubuntu 的 apt 或基于 RHEL 的使用 Yum

开发环境所需的软件:

PECDM 模块 (github.com/puppetlabs/puppetlabs-pecdm) 将通过 bolt 命令创建指定的资源。应通过 Azure 成本分析工具仔细监控在 Azure 上运行实验室的费用,以避免意外账单。未使用的实验室应被销毁或至少释放,以减少费用。

所有这些组件都有你可能在自己组织中使用的等效项。然而,本开发和实验室设置的目的是尽可能简单和自动化的设置。随着书籍的进展,这可能是你想要做的一个练习,用来测试你自己的组件。PECDM 本身支持 AWS、Azure 和 GCP,并提供关于配置必要 CLI 的模块说明。

该部分的代码可以在 github.com/PacktPublishing/Puppet-8-for-DevOps-Engineers/tree/main/ch02 找到。

自 Puppet 5 以来的重大变化

Puppet 5 反映了 Puppet 作为一个组织方向的变化,这一点在前一章中有所强调。它的重点是基础设施的性能和扩展性以及语言的稳定性。本节将涵盖 Puppet 5 和 7 之间的变化;这些版本反映了你在工作中可能遇到的代码库和从 Puppet forge 获取的模块中使用的 Puppet 版本。它还将涵盖一些旧的模式和你在代码中可能遇到的问题,这些问题反映了 Puppet 在版本 5 之前的状态。

Puppet 5

Puppet 4 有大量被弃用的特性,这些特性几乎在 Puppet 5 中全部被移除。虽然不值得列出所有这些特性,但为了设定发布的背景,Puppet 5 更多的是通过引入新特性来完成 Puppet 4 中已开始的工作。它统一了包的编号,所有 Puppet 包的版本都从 5.0.0 开始,而不是之前不同版本包之间的不匹配,比如 Puppet 4 需要 Puppet Server 2.x 和 Puppet agent 1.x。

Puppet 5 作为服务器平台在性能上有了大幅提升:代理运行时间减少了 30%,CPU 利用率至少降低了 20%,Puppet Server 报告的目录编译时间减少了 7 到 10%,而且 Puppet 5 能够扩展至更多的代理,最多可扩展 40%。引入了 Puppet Server 指标,以便更好地观察 Puppet 平台。除了更高的性能和可扩展性之外,Puppet Enterprise 2017.4 及之后的版本还具备了灾难恢复能力以及软件包检查功能,无论 Puppet 是否管理这些软件,都会存储关于安装在整个环境中的软件的信息。Puppet Enterprise 功能的完整技术细节将在 第十四章 中讨论。

尽管Puppet 开发工具包PDK)与 Puppet 5 并无直接关联,但它在同一时间发布,自动化了许多工具的安装、测试、代码检查和模块目录的创建(这一部分将在第八章中详细讲解)。以前,这些工作需要手动完成或由单个开发者的自动化脚本来完成。此外,Hiera 5 与 EYAML(在第九章中介绍的加密数据机制)集成,这大大简化了数据的加密处理,并且仍然能够被使用。

Puppet 6

Puppet 6 带来了显著的变化,许多原本包含在 Puppet 核心安装中的类型被移除,并放入模块中,用户可以选择从 Puppet Forge 下载这些模块。这样做是为了缩小安装范围,因为核心类型的数量随着时间的推移不断增加,而让用户选择他们需要的类型更加高效。对于哪些功能被持续使用进行了评估,许多字符串和数学函数从stdlib模块移到了 Puppet 核心模块,以反映它们的核心用途。同时,引入了受信外部命令功能,这使得可以像查询事实一样查询外部数据源,从而可以调用并引入卫星服务器或数据库服务器的 API 供 Puppet 代码使用。这将在第十三章中详细讲解。此外,引入了延迟数据类型,使得变量能够在部署时本地执行延迟函数。这对于像秘密管理这样的用例尤为有用,例如一个金库,其中传统函数会从 Puppet 主服务器发起调用,并通过 Puppet 基础设施将秘密发送给代理。6.24 版本中引入了参数化exec,这使得在使用exec资源类型时,可以将命令与参数分开——这是一种强大的安全措施,防止命令被传递而不是参数。

在平台方面,Puppet 证书命令从puppet cert命令更改为puppet server ca命令,这些命令更加完整且功能更强大。此外,PuppetDB 被包含在 Puppet 编译服务器上,以更好地管理 PuppetDB 的请求负载。平台的详细内容将在第十章中详细讨论。

Puppet 7

Puppet 7 中的一个显著变化是删除了有害的术语,这是 2014 年开始进行审查和改进的结果。这一变化的焦点是“主从”和“黑名单/白名单”这样的短语。对于 Puppet 来说,这意味着主服务器变成了主控服务器,主服务变成了服务器服务,而在模块中,主分支变成了主分支。它还意味着“黑名单/白名单”术语被“允许列表/阻止列表”替代。

在 Puppet 6 更新中提到的参数化执行命令可在 7.9 版的 Puppet 语言中使用。Factor 被升级到版本 4,这是用 Ruby 重写的,提供了诸如基准测试、超时和用户缓存等功能,这些将在第五章中讨论。自 7.21 版起,include_legacy_facts选项被加入,用于排除旧版事实。

该平台升级到了 Postgres 11 和 Ruby 2.7,进一步提升了性能。

报告机制还可以通过exclude_unchanged_resources选项选择不将未更改的资源包含在报告中。

再次强调,虽然 PDK 2.0 并不直接与 Puppet 发布相关,但它是在 Puppet 7 发布时推出的,并且不再支持 Puppet 4。

旧版 Puppet 模式

本节将重点介绍一些旧的模式及其在旧版本 Puppet 中使用的原因。这将帮助你理解在较旧的、不再维护的模块中,或者是没有经过重构的代码中,常见的代码。Puppet 4 引入了数据类型,但在此之前,所有变量都是字符串,许多比较和其他函数的结果可能非常奇怪且不一致。要理解这一点的全面性,可以观看www.youtube.com/watch?v=aU7vjKYqMUo。因此,在历史代码中,你可能会看到对变量的奇怪处理和未定义变量的检查。最初,facter事实也只是称为顶级变量,这可能会与普通变量混淆,并且容易发生意外覆盖。后来改为事实哈希,我们将在第五章中详细介绍。

平台基础设施变得更加复杂,并且可以选择使用 Rack 或 WEBrick 配置。在非常早期的 Puppet 代码版本中,file_line功能尚未引入,且 Puppet 的stdlib模块也没有提供管理单行文件的功能。这导致了 Augeas(一个可以解析文件并允许操作的工具)和模板(允许通过条件逻辑和变量创建文件)被过度使用。Augeas 功能非常强大,但往往过于复杂且对性能产生负担,而模板的过度使用导致了整个文件被强制执行,而不仅仅是需要的单个行或设置。因此,在处理早期版本的 Puppet 代码时,值得回顾代码,确保你继承的代码真的需要控制整个文件,并且在现有更简单的解决方案下,避免过度使用 Augeas。params.pp模式在 Hiera 提供类参数覆盖功能之前,在模块中被广泛使用。直到 4.6 版本,才引入了敏感数据类型,这使得在代码中安全处理任何机密数据变得困难。最后,原始的 Puppet 版本没有提供循环的概念,直到 Puppet 4 引入了 lambda 函数。所以,你可能会在旧代码示例中发现一些晦涩的模式,用来实现类似的效果。

用于辅助 Puppet 开发的 IDE 和工具

早期 Puppet 开发中最大的一个问题是缺乏关于如何开发的共识,并且缺乏集成。如在第一章中讨论的那样,这一局面在 Puppet 5 发布时发生了巨大变化。本节突出了一些工具,作为基于 Puppet 使用和经验的意见性推荐,且它们大多数将在实验和演示中使用。当然,这并不是开发 Puppet 代码的唯一方法,你的组织可能会根据环境要求使用不同的工具。

pdk命令。此前,Puppet 开发人员需要收集工具,安装依赖项,然后运行pdk所包含的各种命令。

Visual Studio Code 已经成为一个非常强大且流行的源代码编辑器。它是免费的、跨平台的,并且拥有丰富的扩展库,包括 Puppet 扩展(marketplace.visualstudio.com/items?itemName=puppet.puppet-vscode)。它创建了强大的快捷方式,允许你在 IDE 中完成所有工作,整个过程中将在本书中进行演示。

我不会直接在实验中使用它,但因为许多人更喜欢命令行编辑器而不是 Visual Studio Code,所以值得注意的是,有一些 Vim 模块(github.com/rodjek/vim-puppet)可以在 VIM 中提供语法检查和 linting 功能。

一个特别有用的开发网页是validate.puppet.com/网站,可以快速粘贴 Puppet 代码进行验证和解析,并创建关系图。

更高级的工具是 Puppet 调试器(github.com/nwops/puppet-debugger),它允许运行 Puppet 代码并在代码中设置断点,从而查看变量的状态。随着更复杂代码的编写,这将变得非常有用。

如何部署你的 Puppet 实验室和开发工具

本节将演示如何安装和配置桌面环境,然后使用该环境在 Azure 中搭建 Puppet 基础设施,用控制库配置它,将一些模块部署到环境中,并测试登录网页控制台。这将确认实验室环境按预期工作,并且应该让你有信心根据需要启动和关闭实验室,避免在 Azure 上为不必要的虚拟机运行时间支付费用。

图 2.1中,展示了本练习的最终结果。你用作开发环境的设备将安装 Visual Studio Code,用于编辑从 GitHub 克隆的代码。根据操作系统的不同,PowerShell 或 Shell 会话将使用 Bolt 与 Terraform 在 Azure 上构建基础设施,并将配置应用于该基础设施,配置一个 Puppet Enterprise 服务器及附加到该服务器的实例。Puppet Enterprise 服务器的网页控制台将可以通过 HTTPS 在浏览器中访问:

图 2.1 – 实验室设置

图 2.1 – 实验室设置

Mac 桌面

Mac 安装将依赖 Homebrew 自动化安装过程,Puppet 为此创建了自己的仓库(github.com/puppetlabs/homebrew-puppet)。运行以下命令安装技术 要求部分中提到的桌面工具:

/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
brew update
brew install azure-cli
brew install --cask puppetlabs/puppet/puppet-agent
brew install --cask puppetlabs/puppet/pdk
brew install --cask puppetlabs/puppet/puppet-bolt
brew install --cask visual-studio-code
brew install gh
brew install shellcheck
brew install puppetlabs/puppet/pe-client-tools
brew install git

Windows 桌面

Windows 安装依赖 Chocolatey 进行安装。在 PowerShell 会话中运行以下代码;注意,只有第一个命令需要管理员权限:

Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))
choco install pdk -y
choco install puppet-agent -y
choco install vscode-puppet-y
choco install puppet-bolt -y
choco install vscode -y
choco install git -y
choco install pe-client-tools -y
choco install gh -y
choco install azure-cli -y
choco install shellcheck -y
Install-Module PuppetBolt
Add-WindowsCapability -Online -Name OpenSSH.Client~~~~0.0.1.0

Linux 桌面 – 基于 RPM

这个基于 RPM 的 Linux 桌面安装已在 Rocky Linux 8 上测试过。因此,根据你的操作系统版本和不同的发行版,可能需要进行一些本地化调整。然而,运行以下代码将从供应商那里添加必要的 Yum 仓库并安装相应的包:

release=$(rpm -E '%{?rhel}')
sudo rpm --import https://packages.microsoft.com/keys/microsoft.asc
sudo sh -c 'echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com/yumrepos/vscode\nenabled=1\ngpgcheck=1\ngpgkey=https://packages.microsoft.com/keys/microsoft.asc" > /etc/yum.repos.d/vscode.repo'
sudo rpm -Uvh https://yum.puppet.com/puppet7-release-el-${release}.noarch.rpm
sudo rpm --import https://packages.microsoft.com/keys/microsoft.asc
echo -e "[azure-cli]
name=Azure CLI
baseurl=https://packages.microsoft.com/yumrepos/azure-cli
enabled=1
gpgcheck=1
gpgkey=https://packages.microsoft.com/keys/microsoft.asc" | sudo tee /etc/yum.repos.d/azure-cli.repo
sudo dnf config-manager --add-repo https://cli.github.com/packages/rpm/gh-cli.repo
sudo rpm -Uvh https://yum.puppet.com/puppet-tools-release-el-8.noarch.rpm
sudo yum -y install epel-release
sudo yum check-update
sudo dnf install gh
sudo yum install code
sudo dnf install azure-cli
sudo yum install ShellCheck
sudo yum install puppet-bolt
sudo yum install https://pm.puppetlabs.com/pe-client-tools/2021.7.0/21.7.0/repos/el/8/PC1/x86_64/pe-client-tools-21.7.0-1.el8.x86_64.rpm

客户端工具有特定版本,应该根据你的安装版本进行调整。请访问puppet.com/try-puppet/puppet-enterprise-client-tools/查找curl命令。

Linux 桌面 – 基于 APT

基于 APT 的 Linux 桌面在 Ubuntu 20.04 上进行了测试,因此需要根据您的特定操作系统版本和不同的发行版本进行一些本地化调整。不过,运行以下代码应该可以添加所需的 APT 仓库并安装所需的桌面开发软件:

release=$(lsb_release -c | awk '{print $2}')
wget -qO- https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > packages.microsoft.gpg
sudo install -o root -g root -m 644 packages.microsoft.gpg /etc/apt/trusted.gpg.d/
sudo sh -c 'echo "deb [arch=amd64,arm64,armhf signed-by=/etc/apt/trusted.gpg.d/packages.microsoft.gpg] https://packages.microsoft.com/repos/code stable main" > /etc/apt/sources.list.d/vscode.list'
wget https://apt.puppet.com/puppet7-release-${release}.deb
wget https://apt.puppet.com/puppet-tools-release-${release}.deb
curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null
sudo apt-get update
sudo dpkg -i puppet7-release-${release}.deb
sudo dpkg -i puppet-tools-release-${release}.deb
rm packages.microsoft.gpg
rm puppet7-release-${release}.deb
rm puppet-tools-release-${release}.deb
sudo apt install apt-transport-https
sudo apt update
sudo apt install code
sudo apt –y install puppet-agent
sudo apt-get install git
sudo dpkg -i puppet-tools-release-${release}.deb
sudo apt-get install puppet-bolt
sudo apt install gh
curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash
sudo apt install shellcheck
curl -JLO ' https://pm.puppetlabs.com/pe-client-tools/2021.7.0/21.7.0/repos/deb/focal/PC1/pe-client-tools_21.7.0-1focal_amd64.deb'
sudo apt install ./pe-client-tools_21.7.0-1focal_amd64.deb

客户端工具是特定版本的,应根据您的安装进行调整。请访问puppet.com/try-puppet/puppet-enterprise-client-tools/ 查找curl命令。

配置工具

现在,您已经在所使用的桌面环境中安装了核心工具,运行和管理应用程序的核心步骤是相同的。

首先,我们需要在 GitHub 上注册账号(github.com/join)并在 Azure 上注册(azure.microsoft.com/en-gb/free/)。完成这些注册后,登录到两个 CLI 工具中。运行以下命令并登录将出现的网页:

gh auth login
az login

下一步是生成允许与 GitHub 通信的密钥。您可以通过运行以下命令来完成:

ssh-keygen -t rsa –b 4096 -P ''

然后,我们通过 GitHub CLI 上传我们创建的密钥。对于 Mac 或 Linux,请运行以下命令:

gh ssh-key add ~/.ssh/id_rsa.pub

对于 Windows 中 SSH 密钥的相应位置,请运行以下命令:

gh ssh-key add %USERPROFILE%\.ssh\id_rsa.pub

然后,您可以通过从 Packt 的 GitHub 仓库下载extensions.list文件,地址为github.com/PacktPublishing/Puppet-8-for-DevOps-Engineers/blob/main/ch02/extensions.list,并通过循环读取每一行来安装 Visual Studio Code 的扩展。

对于 Mac 或 Linux,您可以通过运行以下命令来实现:

cat extensions.list | xargs -L1 code --install-extension

对于 Windows,您可以运行以下命令:

foreach($line in get-content extensions.list) {code --install-extension $($line)}

下一步是为您的代码工作区创建一个区域,然后将pecdm模块下载到该区域。对于 Linux 和 Mac,我们将在主目录中创建一个工作区,并通过运行以下命令将pecdm克隆到该目录中:

mkdir ~workspace/pecdm
git clone git@github.com:puppetlabs/puppetlabs-pecdm.git ~workspace/pecdm
cd ~workspace/pecdm

对于 Windows,我们假设用户目录中有相应的文件夹,首先在其中创建一个workspace目录,然后通过运行以下命令进行克隆:

mkdir %USERPROFILE%\workspace
git clone git@github.com:puppetlabs/puppetlabs-pecdm.git %USERPROFILE%\workspace\pecdm
cd %USERPROFILE%\workspace\pecdm

现在,我们已经完成了安装并且拥有了带有克隆模块的工作区,我们可以配置该模块并运行以下 Bolt 计划来在 Azure 中创建 Puppet 基础设施。这将启动一个 Puppet 2021.7.0 主服务器,并注册一个单独的客户端。SSH 用户允许你使用之前创建的 SSH 密钥连接到主机。对于这个示例,params.json文件应该从github.com/PacktPublishing/Puppet-8-for-DevOps-Engineers/blob/main/ch02/params.json下载到 pecdm 目录。我使用的是英国南部区域,并允许开放任何连接的防火墙,但你应该选择离你最近的云区域,并设置一个仅允许你的桌面环境和 Azure 区域访问的防火墙规则。以下链接可以帮助你做出选择:

代码如下:

bolt module install --no-resolve
bolt --verbose plan run pecdm::provision @params.json

完成此操作大约需要 20 到 30 分钟。

然后,你可以运行以下 Azure CLI 命令来返回主机名和公共 IP 地址列表:

az network public-ip list -g packtlab --query "[].{Hostname:name,Public_IP:ipAddress}" --output tsv

输出将类似于以下内容:

pe-node-packtlab-0-cffe02      20.108.156.266
pe-server-packlab-0-cffe02    20.108.156.67

将列出的以pe-server开头的 IP 地址复制到网页浏览器中,以访问 Puppet Enterprise 控制台页面。然后,你可以使用用户名admin和密码puppetlabs进行登录。

为了销毁这些基础设施并确保不会产生不必要的费用,可以运行以下命令:

bolt plan run pecdm::destroy provider=azure

另外,如果实验需要长时间保持,可以通过停止并取消分配每个虚拟机来最小化费用,然后稍后使用以下命令重新启动它们:

az vm deallocate --resource-group packtlab --name <VM name>
az vm start --resource-group packtlab --name <VM name>

本节已经完整介绍了开发者桌面的创建,以及启动和销毁 Puppet 基础设施的过程。这确保你为后续章节中的实验做好准备。在本实验中,pecdmpeadm模块用于配置标准架构,这是 Puppet 支持的架构之一:puppet.com/docs/pe/latest/supported_architectures.html。在第十四章**中,我们将更详细地讨论不同的架构选项。但目前,理解标准架构作为提供单个 Puppet 服务器的基础层级非常重要。在这个场景下,pecdm使用 Terraform 配置必要的基础设施,而peadm则安装 Puppet Enterprise 组件。这两个模块将作为使用 Bolt 项目、任务和计划的示例,并将在第十二章**中进行回顾。

参考资料和进一步研究

本节将介绍可与本书一起使用的进一步资源和参考资料。这些内容深入探讨了 Puppet,并使您能够从 Puppet 和社区两个方面学习 Puppet。

一般页面 (puppet.com/docs/) 是核心文档页面,您可以在此找到 Puppet 的所有产品以及模式和策略等部分。在我们通过本书时,我们将重点介绍文档中的不同部分。

Puppet 通过各种媒体形式发布文章,涵盖了新产品发布、安全更新和实施指南等内容。它们的社交媒体账号如下所示:

Puppet 拥有自己的学习网站 (training.puppet.com/learn),该网站包括多个元素,如 Puppet 实践实验室,这些在线实验室可以完全通过 Web 浏览器运行,以及任务箱,它们是关于完成小型专注任务的指南。Puppet 的支持知识库于 2022 年 4 月公开,允许任何人无需登录即可搜索并查看故障排除指南、最佳实践和常见问题解答,网址为 support.puppet.com。旧版本 Puppet 的档案文章可以在 github.com/puppetlabs/docs-archive/tree/main/supportkb#readme 找到。

Puppet 之前提供了两门由讲师主导的培训课程,这些课程需要付费并持续 3 天(Puppet 入门Puppet 实践者)。在 2022 年,基础核心培训 模块取代了 Puppet 入门,而 高级核心培训 模块取代了 Puppet 实践者

关键的区别在于 基础核心培训 模块是免费的注册课程,且两个培训集都被拆分成三个模块集,每个模块持续一天。更多详细信息请访问 Puppet Compass 网站。

基础 核心培训

  • PE101: 部署与发现

  • PE201: 设计与管理

  • PE301: 开发与维护

高级 核心培训

  • PE401: 扩展能力

  • PE501: 持续交付

  • PE601: 规模化自动化

提供商业许可 Puppet 模块的企业模块(Enterprise Modules)在 Puppet forge 上有一个博客,讨论各种 Puppet 话题,网址是 www.enterprisemodules.com/blog/,同时也有一个 Twitter 账户 twitter.com/enterprisemodul

另两个著名的 Puppet 咨询和开发团队是在 Example42 GmbH 分拆后成立的,一个是 Example42,现在是 Lab42 的品牌,拥有一个博客 blog.example42.com/blog/ 和一个 Twitter 账户 twitter.com/example42;另一个是 Betabots,拥有一个博客 dev.to/betadots 和一个 Twitter 账户 twitter.com/betadots。这两个团队都提供了关于他们在 Puppet 开发工作和方法的见解。

要提问关于 Puppet 或与社区中的人交流,可以加入 slack.puppet.com/www.reddit.com/r/Puppet/ 来提问有关 Puppet 的问题以及与社区互动。

本节并不打算列出所有参考资料,而是提供一些更为知名和持久的信息源和社区,供读者参考和关注,以便更好地了解 Puppet。

小结

本章中,我们讨论了从 Puppet 5 到 7 的现代版本变化,并介绍了一些反模式,警惕可能仍然存在于遗留 Puppet 代码中的问题。如果你不熟悉 Puppet,可能在完成本书后回到这一部分,重新阅读这些变化会更实际。

我们讨论了在开发环境中使用的工具和 IDE,来自动化和加速 Puppet 开发环境,并已安装这些工具来介绍实验室内容。我们学会了如何在 Azure 上搭建读者的开发环境和 Puppet 基础设施。

在本章结束时,我们覆盖了可以用来进一步学习 Puppet 的各种资源和社区,帮助读者跟上最新的发展动态,并指引如何提问和与社区讨论 Puppet。

在下一章中,我们将开始学习 Puppet 语言,涵盖资源、类型和提供者的基本构建模块。我们将了解 Puppet 编程的基本语法和风格,以及如何使用各种引用和命令来简化代码生成和查找文档的过程。我们将首先学习 Puppet 中的核心类型,了解如何高效使用它们。接下来,我们将介绍如何使用定义类型来实现资源的可重复模式,如何使用类来包含和引用目录中的资源,最后,介绍如何使用更高级的功能——导出和收集资源,以便在多个客户端之间共享资源声明。

第三章:Puppet 类、资源类型和提供者

本章将讨论类和定义类型如何提供结构,并为资源分组提供一种方式,使代码具备模块化和可重用性。你将学习资源的组成部分;类型、提供者以及应用于它们的属性。你将看到如何使用 Puppet 命令了解系统的当前状态,并通过查看三种最常见的资源类型——包、文件和服务,了解如何查找资源可用的属性以及如何声明状态。

使用这三种资源类型,你将看到如何通过 Puppet 代码快速启动一个应用程序,如 Apache 或 Grafana,这包括简单的包安装、配置文件和服务。接下来将讨论其他核心资源类型,并强调最佳实践和方法。还会讨论一些元参数(可以应用于任何资源的属性),以及资源声明的一些高级模式。

你将遇到一些反模式,虽然这些仍然是已记录的 Puppet 语言特性,但不推荐使用。了解这些有助于你理解可能遇到的遗留代码,并考虑需要重构的代码部分。

在本章中,我们将讨论以下主要内容:

  • 类和定义类型

  • 资源、类型和提供者

  • 核心资源类型

  • 元参数和高级功能

  • 反模式

技术要求

通过下载 github.com/PacktPublishing/Puppet-8-for-DevOps-Engineers/tree/main/ch03 中的 params.json 文件,使用以下命令来配置一个标准大小的 Puppet 服务器,配有 Windows 客户端和 Linux 客户端:

bolt --verbose plan run pecdm::provision –params @params.json

类和定义类型

如在第一章中讨论的那样,Puppet 代码存储在以 .pp 结尾的清单文件中。可以将资源写入一个单独的清单文件,然后使用 apply 命令 puppet apply example.pp 在本地强制执行代码。也可以不使用清单文件,直接在命令行中使用 execute 标志执行 Puppet 代码,例如 puppet apply -e 'Package { '``vscode': }'

注意

puppet apply 也可以针对一个清单目录运行,它会按照顺序解析每个文件,并遍历目录结构。在第十一章中,节点定义将帮助我们利用这一点。

尽管这两种方法对于测试和学习很有用,但它们在缺乏任何结构方面有明显的局限性,这将导致必须运行许多大型静态命令或文件,并且无法传递数据。是命名的代码块,它们提供了这种结构,提供了一种将资源分组并分配数据的方式,我们可以将其应用到服务器上。类定义放入清单文件中,在类定义内部,我们放置我们的资源定义。语法如下:

  • class 关键字。

  • 类的名称。

  • ( ) 中的可选参数。

  • 带有 {} 的 Puppet 代码:

    class  example_class (
      String example_parameter
    )
    {
       <code block>
    }
    

class 参数允许为类提供外部数据。例如,一个类可能有一个安装包的资源,参数可以用来指定要安装的包的版本。

注意

可以向类中添加一个可选的 inherit 关键字,以允许类继承,通过这种方式,您可以创建一个通用的基类,然后在继承的类中扩展它。从 Puppet 6 开始,这种模式不再使用,并且在 Puppet 文档中也不再讨论,除了提到它作为一个关键字存在。通过数据,有更好的方法来实现这种行为,我们将在 第九章 中介绍。

早期对于类的常见困惑是,这个结构仅定义了一个类;它并没有声明该类会被包含在从 Puppet 代码编译的目录中。这与清单中的资源声明不同,后者通过编写并应用后,会被添加到目录中。

这意味着在包含类的清单上运行 puppet apply 什么也不会做。要将类添加到目录中,我们必须使用 include 函数声明该类,进行类资源声明,或者我们必须使用外部节点分类器ENC)。ENC 将在 第十一章 中介绍,但现在可以理解为 Puppet 服务器脚本,用于标识要包含在节点中的类。

包含一个类

include 函数是通过在清单文件的类代码块中声明 include class_name 来添加类的最简单方法。它可以在多个类中多次使用,并且只会产生一个条目。要直接通过 puppet apply 声明一个类,我们可以运行 puppet apply –e "include class_name",这将测试一个带有类的清单文件。遵循模块结构,这将应用来自 class_name/manifest/init.pp 路径的清单。

类资源声明

在下一节中,将更详细地介绍资源声明,但声明像资源一样的类使我们能够传递我们定义或查找的属性。它看起来是这样的,但只能在一个目录中使用一次:

Class {'class_name':
  paramter1 => 'value1'
}

定义类型

定义类型是一个 Puppet 代码块,与类不同,它可以在清单中多次声明,通过传递参数和唯一名称。像类一样,最佳实践是将其定义在单独的清单文件中。

语法如下:

  • define 关键字开头

  • 类型名称

  • 开括号((

  • 参数列表

  • 开括号({

  • 资源体

  • 闭括号(}

除了定义的参数列表外,$title$name 变量也可以在定义中使用。这确保了我们声明的资源是唯一的。一个非常简单的示例可能是通过名称和组来确保创建一个用户和一个组,并将文件放置在我们创建的用户和组拥有的user主目录中:

define exampledefine (
  String user = "${title}",
  String group
) {
user { ${user}: }
group { ${group}: }
file { '/export/home/${user}/.examplesetting':
  user => ${user},
  group => ${group},
  content => "User is ${user} and group is ${group}",
}
}

定义类型与类相同;应用清单文件不会产生任何效果。定义类型的资源声明必须在类中进行,然后可以包含在类中:

exampledefine {'user1':
  group => 'group1'
}
exampledefine {'user2':
  group => 'group2'
}

这个示例有一定的危险性,因为如果第二个 user2 的声明也使用了 group1 组,这将导致资源声明重复。

命名空间

命名空间是标识清单文件中类的目录和文件结构的片段。这些命名空间由两个冒号(::)分隔,例如,以下目录将转换如下:

文件 路径名称 命名空间
/manifests/base.pp base
/manifests/windows/grafana.pp windows::grafana
/manifests/linux/apache.pp linux::apache
/manifests/linux/ubuntu/landscape.pp linux::ubuntu::landscape

表 3.1 – 命名空间目录转换

如果我们只想应用 windows::grafana 类,我们可以在 manifest 目录中运行 puppet apply –e "include windows::grafana"

命名空间的深度没有限制,但最佳实践是保持在几级以内。

第八章中,我们将看到具有命名空间的模块,其中模块名称是所有类的根级别,只有一个类除外。

资源、类型和提供者

资源是 Puppet 语言的基本单位;我们希望描述的每个有状态项都是一个资源。资源在它们管理的内容上必须是唯一的,因为 Puppet 无法管理或优先处理资源之间的冲突。它只是会报告冲突存在,并且无法编译清单。

每个资源都会有一个类型,这是我们正在配置的描述,比如文件或注册表设置;参数,是包含我们可以自定义的设置的变量;以及提供者,是允许 Puppet 实现操作系统独立性的底层实现。这个提供者通常是基于操作系统的默认值,但如果需要,可以作为属性添加。因此,资源声明具有以下语法:

  • 以类型名称开头,例如 file,不带引号且小写

  • 大括号({

  • 资源标题应加引号

  • 冒号(:

  • 属性名称的列表以及该名称属性的值,二者之间用=>连接,最后以逗号(,)结尾

  • 一个闭合的大括号(}

注释

大括号之间的所有内容被称为资源体。在一个资源声明中可以有多个资源体,实际上是声明多个相同类型的资源,但为了清晰起见,我通常建议不要这样做。

作为伪代码,语法看起来如下所示:

type { 'title':
   attribute1  => value1,
   attribute2 => value2,
}

下面是一个实际示例,确保系统上的vscode包是最新版本:

package { 'vscode':
  ensure => 'latest',
}

语法列表中给出的资源和类声明/定义是最小要求,而代码示例则根据风格和最佳实践的考虑,进行了换行和空格的处理。虽然可以将声明和定义写成单行,但 Puppet 开发了一个风格指南——www.puppet.com/docs/puppet/8/style_guide.html,我们将在本书中遵循该指南,并结合其他一些具有明确意见的最佳实践,编写可读、可维护且简洁的代码。

以下是一些在代码示例中应用风格指南的例子:

  • 使用两个空格缩进

  • 不要有尾随空格

  • 属性名称应对齐

  • 属性=>符号应对齐

  • 属性值应对齐

  • 在所有属性后包含尾随逗号

虽然空白符号没有限制或语法意义,但 Puppet 语言风格指南的建议旨在使代码更具可读性和一致性。风格指南指出,所有属性应有尾随逗号;这可以确保添加新属性时只会在 Git diff 中显示一个更改,但你可能会发现某些代码遵循没有尾随逗号的模式,以便清楚地表示这是最后一个元素。这样做会通过 lint 检查,但如果希望代码获得 Puppet 模块使用批准,则可能会因不符合 Puppet 风格指南而遇到问题。

由于存在许多语法和风格规则,学习的最佳方式是使用风格指南 lint 检查,通过 Ruby gem puppet-lint 提供,语法验证通过 puppet parser validate 命令提供。Visual Studio Code 上的 Puppet 扩展集成了这些命令,因此在编辑时会突出显示语法和 lint 问题。在图 3.1的截图中,可以看到实验室的警告输出,其中包含一些风格和语法错误:

图 3.1 – Visual Studio Code 显示语法和 lint 问题

图 3.1 – Visual Studio Code 显示语法和 lint 问题

使用github.com/rodjek/vim-puppet可以在vim中实现类似效果。

重要说明

本书中将提供有关最佳实践和编码方法的建议,很多建议来自于 Puppet 风格指南等来源。一个组织在开发清晰一致的 Puppet 代码时可以做的最好的一件事,就是编写自己的最佳实践和风格指南,基于 Puppet 风格指南提供的基础,确保在代码审查时遵循该指南。这也可以与风格指南或本书中的某些观点不一致,只要这对你的组织和开发人员最有利并达成共识。

每种类型的资源必须唯一,ntp 资源不可重复命名为两个服务类型资源名为 ntp。在命名时,在字符或空格方面没有其他限制,但出于性能考虑,标题应该保持简短,并且永远不超过 140 个字符。这个 标题 是 Puppet 在生成目录时识别资源的依据。

namevar 属性(也被称为 namevar,默认情况下与标题相同,除非分配了其他属性)。在某些情况下,类型将使用多个属性来定义 namevar,例如一个包同时使用命令和名称。这在通过不同机制安装相同配置的多个副本时使用,比如安装与 Ruby gem 同名的包,以及作为 Red Hat 包管理器 (RPM) 安装的包。

安装 Apache 包可以演示 namevarapache_package 名称变量的区别,名称变量基于操作系统设置。对于 Fedora,包名将是 httpd,而对于其他所有操作系统,包名将是 apache2。这意味着我们这个包资源的标题是 apache,在 Puppet 代码中引用该资源时,我们可以始终将其称为 apache 资源包,而目标系统将通过适当的包名来引用它,确保它是一个唯一管理的安装:

$apache_package_name = $facts['os']['name']? {
  Fedora  => 'httpd',
  default => 'apache2',
}
package { 'apache':
  ensure => 'latest',
  name   => "$apach_package_name",
}

现在让我们继续一些实际的例子。

实验

为了实践目前所学的内容,请查看 github.com/PacktPublishing/Puppet-8-for-DevOps-Engineers/blob/main/ch03/lint_and_validate.pp 文件,并尝试在 VS Code 中修正高亮显示的错误。或者,使用 puppet-lint -f-f 会自动修复可能的问题)和 puppet parser validate 命令,这些命令可以在 VS Code 集成终端或单独的终端会话中执行。

validate.puppet.com/ 也可以用来进行在线验证检查。

检查当前系统状态

到目前为止,本章讨论了资源的结构和样式,以及在所有这些规则的影响下,开始编写自己的资源可能会让人感到有些不知所措。puppet resource 命令允许我们从当前机器的状态生成 Puppet 代码;该命令接受类型和 namevar 变量的参数。例如,查看已安装 Puppet 的 Windows 桌面上的目录会生成类似以下内容的输出:

C:\ProgramData\PuppetLabs>puppet resource file "c:\Program Files\Puppet Labs"
file { 'c:\Program Files\Puppet Labs':
  ensure   => 'directory',
  ctime    => '2022-01-31 22:01:02 +0000',
  group    => 'S-1-5-18',
  mode     => '2000770',
  mtime    => '2022-01-31 22:01:02 +0000',
  owner    => 'S-1-5-18',
  provider => 'windows',
  type     => 'directory',
}

从这个例子中可以注意到,某些属性仅在我们称之为属性的信息中返回,且不能由 Puppet 管理,如 mtimectime。其他属性,例如 provider,无需声明,因为在 Windows 机器上,windows 会被假定为提供者。除此之外,经过一些小的调整,这个输出可以直接放入 Puppet 清单并运行。(本章后续内容中,我们将展示如何查看类型属性。)

注意

Visual Studio Code 允许你通过命令面板运行 Puppet 命令(Ctrl + Shift + P,Mac 上为 Command + Shift + P)。输入 puppet resource,然后输入资源类型,最后可选地输入 var 名称。随后,它会将输出粘贴到你打开的文件中。

在之前的例子中,我们对单个 namevar 属性运行了 puppet resource。对于某些类型,你可以发现该类型在机器上每个资源的状态,比如运行 puppet resource package 查看软件包的状态。这显然无法用于文件类型,因为递归遍历主机上的每个文件会生成过多的信息,但你可以快速生成主机设置的信息。

在 VSCode 中,尝试打开一个新文件,使用 puppet resource 运行命令面板,输入 package。这将列出 Puppet 识别的所有包和可用的 Puppet 提供者。该输出的示例如可通过 github.com/PacktPublishing/Puppet-8-for-DevOps-Engineers/blob/main/ch03/puppet_resource_package.pp 查看。

引入使用包、文件和服务模式的类型

在讨论了声明资源的结构和样式之后,下一步是介绍 Puppet 可用的核心类型,以及如何发现类型的属性和功能。

核心类型的文档在线提供,地址为 www.puppet.com/docs/puppet/8/type.html#puppet-core-types,并可以通过 puppet describe Puppet 命令在命令行中查看。使用 puppet describe --list 会列出你环境中所有可用的类型;然后你可以通过传递类型名称来查看某一类型,例如 puppet describe package。当你将鼠标悬停在资源声明中的类型和属性名称上时,这些文档在 VS Code 中也可以看到。

从软件包、文件和服务类型的组合开始,你将能够安装、配置并启动应用程序。

软件包类型

运行puppet describe package或访问www.puppet.com/docs/puppet/8/types/package.html,我们可以查看该类型的描述及其属性列表和可用的提供者。

软件包在最简单的层面上可以仅作为一个具有标题的软件包资源声明:

package { 'vscode': }

这将多个属性设置为默认值,从而使用底层操作系统的默认提供者,例如 Red Hat 的yum,或 Windows 的 Windows 提供者,它处理.exe.msi文件。它还将安装最新的可用软件包版本,但在强制执行时,只会确保软件包已安装,而不会维持在最新版本。

这种版本控制行为由ensure参数控制,示例默认值为present,也可以声明为installedlatest值,顾名思义,确保软件包处于提供者可用的最新版本。对于更灵活的版本控制,可以将值设置为字符串版本,如1.2.3,并且根据提供者的支持,可以使用版本范围,如> 1.0.0 < 2.0.0。使用absent值是 Puppet 的重要部分,在这里,资源不仅确保服务器状态中存在的内容,还包括不应存在的内容。

与在ensure中使用absent值相关的是purged值,这是一个依赖于提供者的选项。如果设置为true,则在删除软件包时会移除配置文件。

providers属性通常保留默认设置,但如果需要通过其他软件包管理系统(如piprubygems)安装,可以将其值设置为适当的提供者名称。

要查看可用的提供者,可以在describe命令中使用-p标志:puppet describe package -p

以 Windows 为例,需要注意的是,它告诉我们 Windows 提供者是默认提供者,并列出了支持的特性,这些特性是与此提供者兼容的属性。这些属性的差异反映了该提供者使用的不同底层命令。

source属性是指向软件包文件的 URL;这允许通过远程调用 Web 源(如 JFrog Artifactory)或本地下载的文件,并且是某些提供者的必需参数,例如 Windows,它需要.bin.exe文件的位置。

command属性,自 Puppet 6 版本以来新增,允许你选择提供者应运行的命令。这在机器上有多个安装命令版本时是必要的。

name 属性,应该是软件包的名称,默认会将其设置为标题并与命令属性结合;自 Puppet 6 起,这就是使软件包拥有 namevar 属性的原因。在 Puppet 5 中,使用 provider 属性而不是命令属性。

注意

有时,由于依赖关系问题,可能需要在单个命令中运行多个软件包的安装命令,如 yum。在软件包类型下没有办法做到这一点;最佳做法是使用 exec 类型,我们将在本章稍后讨论。

因此,作为练习,编写以下内容的清单;为每个平台示例创建一个新文件 package_rhel8.pp,可以使用 vscode 或终端。

在 RHEL 8 上,执行以下操作:

  • 安装 rubygem activerecord,确保其版本大于 7

  • yum 安装最新的 cowsay

  • 确保 pinball 包在名为 no games 的资源中从系统中删除

查看建议的解决方案 github.com/PacktPublishing/Puppet-8-for-DevOps-Engineers/blob/main/ch03/package_rhel8_answer.pp

在 Windows Server 上,执行以下操作:

  • 从已下载的 .exe 文件 c:\tmp\rubyinstaller-devket-3.1.1-1-x64.exe 安装 rubydevkit,并使用 /VERYSILENT 安装选项

  • 安装 rubygem activerecord,确保其版本大于 7 但小于 9

  • 确保 pinball 包在名为 fun games 的资源中安装,并且版本为 2005-xp

查看建议的解决方案 github.com/PacktPublishing/Puppet-8-for-DevOps-Engineers/blob/main/ch03/package_windows_answer.pp

注意

对于更高级的 Windows 包管理,值得研究 Chocolatey,本章将介绍它,第八章forge.puppet.com/puppetlabs/chocolatey)。

文件类型

安装完软件包后,通常会添加应用程序配置文件和目录来容纳它们。文件类型非常适合创建文件并构建目录结构。它可以处理文件、链接和目录的内容、所有权和权限。

文件类型的最简单声明是将标题作为完全声明的路径:

file { '/var/tmp/testfile' : }

通过 puppet describe file 查看文件类型,在这种情况下,只有两个 providers——Windows 文件或 POSIX 文件,这将与您配置的操作系统族匹配。

对于 ensure 属性,有 presentabsent 选项。选择 present 将默认使用文件值,确保创建的资源是一个普通文件,但仅强制文件路径存在,无论它是符号链接、文件还是目录。

要创建和强制执行一个资源,我们必须选择一个文件的值,并使用 direct 来创建目录或目录嵌套,或使用 link 来创建符号链接。

路径是此类型的 namevar 属性,应为完全限定的路径,或者可以从标题中默认获取。

例如,一个名为Puppet directory的资源,它为位于C:\ProgramData\PuppetLabs的现有directory创建了ensure,如下所示:

file {'Puppet directory' :
  ensure => 'directory',
  path   => 'C:\ProgramData\PuppetLabs'
}

对于我们确保作为文件的资源,content属性为我们提供了多种将内容写入文件的方法。最简单的方式是直接将文本字符串放入文件中,但通过使用函数、文件和模板,我们可以复制存储在 Puppet 模块中的整个文件的内容,或使用模板文件,允许我们将值替换到预先解析的文件中。这些功能将在第七章第八章中详细介绍。

然后使用三个属性来管理所有权和权限:usergroupmode。对于 usergroup,这很简单,只需输入 UID 和 GID 或用户名和组名。如果未设置,这将默认为 Puppet 正在运行的用户和组。mode 使用 Unix 风格的 4 位权限模式来处理权限,但对于 Windows 系统,输入的模式会有一个非常粗略的转换,最好不要声明 mode,而是使用 ACL 模块来补充文件:forge.puppetlabs.com/puppetlabs/acl

举个例子,以下声明创建了一个名为config.test的文件,设置了ownergroup,并包含两行文本内容:

file {'Example config':
  ensure => 'file',
  path   => '/app/exampleapp/config.txt',
  owner => 'exampleapp',
  group => 'examplegroup',
  content => "verbose = true\nselinux=permissive"
}

recurse参数允许递归管理目录的内容。当确保目录并使用source时,如果设置为true,它将递归复制目录内容。需要注意的是,Puppet 不是文件同步工具,因此不要将过多的文件或过大的文件纳入 Puppet 管理。没有具体的文档限制,但常见的建议是递归文件资源中的文件数不超过 10 个,且大小不超过 25 MB。这是因为 Puppet 使用 md5 校验和来检查内容,而对大文件或大量文件执行此操作的开销较大。

信息

在文件数量和目录结构较大的情况下,可以使用模块归档 – forge.puppet.com/modules/puppet/archive – 来下载并解压到指定位置。或者,在审计和版本管理文件时,最好构建一个包并使用我们之前提到的包资源来进行管理。

使用recurse时,多个参数可以提供保护,包括max_files,当命令超出某个限制时,它可以发出警告或错误。recurselimit可以用于限制递归执行的层数。

只有两种情况建议使用此参数——当你有少量文件,并且文件内容应该被强制执行,或者在同时使用purge参数时,当其设置为true时,它将确保目录中没有 Puppet 控制之外的文件。

注意

我们将在下一章详细讨论数据类型和变量,但目前请注意,取值为truefalse的参数可以不带引号,这也是本书采用的风格。

purge参数只能与ensure设置为directoryrecursive设置为true时使用,它提供了一种强大的方法来确保目录中仅包含 Puppet 管理下的文件,删除它找到的其他文件。在以下示例中,我们演示了递归,确保/etc/httpd/conf目录中只包含 Puppet 控制下的文件:

file {'Remove apache config files outside of puppet control' :
  ensure  => 'directory',
  purge   => true,
  recurse => true,
  path    => '/etc/httpd/conf'
}

注意

有一个recursive_file_permissions模块(forge.puppet.com/modules/npwalker/recursive_file_permissions),它可以帮助高效地管理大量文件的递归权限。可以将其与我们之前提到的archive模块结合使用。

validate_cmd参数在配置文件中尤其有用,特别是当有已知方法检查我们放置的文件时。如果验证命令失败,旧文件将保留在原处,避免问题发生。

如果确保创建链接,则target参数是必需的。将其与path值结合使用时,我们可以得到一个符号链接,如下代码所示:

file {'Picking a python on Rhel 8' :
  ensure  => link,
  path    => /usr/bin/python3,
  target  => /usr/bin/python,
}

source参数可以有多种类型:URI、本地文件、NFS 共享,或者是 Web 或 Puppet 模块。也可以将其作为数组来提供多个选择,具体取决于主机名或操作系统,此时它会使用它能找到的第一个文件。在以下代码块中,我们展示了一个示例,其中host将替换为适用的主机名,operatingsystem替换为本地安装的操作系统:

file {'/etc/exampleapp.conf':
  source => [
    "nfsserver:///exampleapp/conf.${host}",
    "nfsserver:///exampleapp/conf.${operatingsystem}",
    'nfsserver:///exampleapp/conf'
  ]
}

在此示例中,在名为server1的 Windows 服务器上,应用此资源声明将会在nfsserverexampleapp共享下查找第一个匹配项,首先寻找conf.server1,如果找不到,再查找conf.windows,最后查找conf

不推荐使用backup参数,因为管理和扩展文件桶以存储这些备份是困难的,正如我们在第十一章中看到的,还有更好的方法可以考虑,例如在 Git 中管理我们的代码,以便应对回退场景。

replace 参数应谨慎使用,但如果设置为 true,则仅在文件不存在时强制执行内容。如果文件已存在,则状态已满足。这对于需要初始配置文件但随后会覆盖它的应用程序非常有用。

讨论了许多属性后,尝试通过编写清单文件来满足列出的要求,以实践构造示例:

  1. 在基于 Unix 的系统中,确保 /etc/sudoers.d 目录中仅包含 Puppet 控制的文件。

  2. 添加一个 /etc/sudoers.d/mongodb 文件,内容为 robin All=(ALL) NOPASSWD: su – mongo,并使用验证命令 visudo -c,文件归 root 用户所有,属于 root 组,权限设置为 0660

  3. 创建一个符号链接,从 /opt/mongodb/mongos/home/robin/mongos

  4. 查看建议的解决方案:github.com/PacktPublishing/Puppet-8-for-DevOps-Engineers/blob/main/ch03/file_unix_answer.pp

对于 Windows,请参阅以下内容:

  1. 在基于 Windows 的系统中,确保 c:\inetpub\wwwroot 目录中仅包含 Puppet 控制的文件,但子目录不受影响。

  2. 添加一个 c:\inetpub\wwwroot\page,内容为 nfsshare1:\\publish\page.html,并使用验证命令 c:\program files\httpvalidator\httpvlidate.exe 文件。

  3. 创建一个符号链接,从 c:\program files\httpvalidator\httpvlidate.exeC:\Users\david\Desktop,并使用 replace 选项在文件存在时替换它。

  4. 查看建议的解决方案:github.com/PacktPublishing/Puppet-8-for-DevOps-Engineers/blob/main/ch03/file_windows_answer.pp

服务类型

安装软件并创建配置文件后,下一步通常是启动具有服务类型的服务。由于系统服务在支持和提供内容上差异很大,我们必须小心提供所有必要的参数。一些服务缺乏正确的状态命令,但可以通过服务的参数提供。

运行并查看 puppet describe service -p 的输出,你将看到各种提供程序,尽管在大多数情况下,默认的服务提供程序是所需的。在某些情况下,如现代 Red Hat 系统上仅提供 init 脚本的旧版软件,我们可能需要选择不同的提供程序。

需要考虑的前两个参数是enableensureensure接受stoppedrunning的值,也可以分别表示为falsetrue。这是一个简单的二进制值,表示服务是否应该运行。enable定义了服务是否应在启动时自动启动,这仅由某些提供程序提供。可以设置为truefalse,表示启用或禁用,然后还有一些依赖于提供程序的选项;例如,在 Windows 上,false表示服务被禁用且无法启动,manual表示服务设置为手动启动类型,它不会随 Windows 一起启动,但允许手动启动该服务。true表示自动启动类型,delayed表示服务被设置为自动(延迟)启动类型,它会在 Windows 启动几分钟后启动服务。

需要强调的另一个 Windows 参数是logonaccount,它指定服务运行时使用的账户。

为了给出我们已覆盖的属性的示例,请查看以下 Windows 服务代码,wuauserv,这是一个具有延迟启动的正在运行的服务,并以localsystem用户身份运行。bam服务已停止并禁用:

service { 'wuauserv':
  ensure       => running,
  enable       => 'delayed',
  logonaccount => 'LocalSystem'
}
service { 'bam':
  ensure => stopped,
  enable => 'false'
}

systemd(RHEL 8 和其他 Linux 系统的默认提供程序)进行比较,我们可以在受支持功能的描述中看到,systemctl没有延迟登录或manual,但有mask,在系统术语中,意味着它禁用服务,以至于即使是依赖于它的服务也无法激活它。

注意

请注意,ensureenabled的默认值完全依赖于底层提供程序的实现。

在没有提供启动脚本的应用程序的情况下,结合startstop参数,您可以使用 Puppet 来弥补这一空白,在这些参数中定义启动和停止服务的命令。pattern参数默认会使用服务名称,并在进程表中查找该名称以确认运行状态,或者您可以提供正则表达式、字符串或任何允许的 Ruby 模式来搜索进程表。或者,可以使用status参数指向一个状态脚本,如果服务正在运行,该脚本应返回零退出代码。

以下示例展示了一个遗留服务,其中包含启动、停止和检查服务器状态的脚本,这些脚本被整合在这个服务资源中:

service {'legacy service':
  ensure       => running,
  enable       => true,
  start  => '/opt/legacyapp/startlegacy -e production'
  stop   => '/opt/legacyapp/stoplegacy -e production'
  status => '/opt/legacyapp/legacystatus -e production'
}

根据实现的性质可以看出,必须仔细选择参数,并且这会根据场景的不同而有所变化。本章稍后将展示如何在声明资源时使用星号(*)来覆盖这些差异的方法。

使用多个资源本地运行 Puppet

第八章第十章中,我们将讨论如何使用 Puppet 代理和分类来应用 Puppet 代码,但为了测试刚刚开发的代码,正如本章开始时所提到的,可以使用 puppet apply 在本地运行代码。在我们的实验室中,我们将使用 Bolt 自动将我们的清单文件复制到远程实验室,并运行 puppet apply

注意

应用资源的另一种方式是通过我们之前回顾过的 resource 命令。向命令添加参数和设置会使其应用于资源。可以使用 puppet resource service puppet ensure=running enable=true 命令强制 Puppet 服务启用并运行。你通常会在 Puppet 知识库文章中看到这个命令,用于修复 Puppet 服务,因为它可以方便地启动/重启服务,而无需考虑它运行在哪个操作系统上。

关系将在第六章中详细讨论,但为了支持彼此依赖的资源,正如包、文件和服务模式所要求的那样,需要了解 requirebeforesubscribenotify 这些元参数的基础知识。requirebefore 是镜像关系,它们在两个资源之间创建一个关系,使得当 Puppet 运行一个资源时,它会先运行另一个资源。定义关系的方向在语义上并不重要,尽管在多对一关系中,将依赖元参数应用于多个资源可能会更加合乎逻辑。

类似地,subscribenotify 元参数不仅允许一个资源具有依赖关系,还能在资源状态变化时向支持它们的类型发送刷新事件(可以通过 puppet describe 在类型文档中确认)。这在服务资源中特别有用,因为更新配置文件应导致服务重启。

这些元参数的语法是资源引用,它由一个首字母大写的资源类型和方括号中的资源名称组成。以下是使用 beforenotifyrequire 来使包、文件和服务模式的示例:

package { 'example app package':
  ensure => latest,
  name   => 'exampleapp'
  before => File['example app configuration']
}
file { 'example app configuration':
  content => 'attribute=value',
  notify    => Service['example app service']
}
Service {'example app service':
  name    => 'exampleapp',
  enable  => true,
  ensure  => running,
  require => Package['example app package']
}

在这个例子中,package 首先被安装,然后添加配置文件,最后服务应该启动。如果配置文件发生变化,这将导致服务重启。在接下来的章节中,我们将更详细地讨论资源引用。

可以使用简写方式在依赖关系中创建相同资源类型的数组:

file { ' C:\Program Files\Common Files\Example':
  require => Package['package1',package2],
}

运行 Puppet 会生成报告,描述资源如果不处于所需状态,如何被更改为正确状态,并且如果服务器处于正确状态,输出会非常少,仅仅是运行检查所花费的时间。代码也可以在 noop 模式下运行。

实验

所以,使用实验室环境将一些 Puppet 代码应用到我们的客户服务器上。

对于 CentOS,我们将安装 httpd 并提供一个显示 Hello World 的网页。创建一个 apache_linux.pp 文件;这将需要安装 httpd 包,并在 /var/www/html/index.html 创建一个文件,内容如下:

<html>
<head>
</head>
<body>
   <h1>Hello World<h1>
</body>
</html>

我们有一个 /etc/httpd/conf/httpd.conf 配置文件,内容来自 raw.githubusercontent.com/PacktPublishing/Puppet-8-for-DevOps-Engineers/main/ch03/httpd.conf,并通过运行 httpd -t -f 进行验证,以及一个在引导时启用并运行的 httpd 服务。

对于 Windows 系统,创建一个 grafana_windows.pp 文件;我们将从 dl.grafana.com/oss/release/grafana-8.4.3.windows-amd64.msi 安装 Grafana 服务器,并确保服务正在运行和启用,在 C:\Program Files\GrafanaLabs\grafana\conf\grafana.ini 配置文件中,确保内容包含以下内容:

[server]
Protocol = HTTP
Http_port = 8080

更新配置文件应重新启动服务。

您可以使用 Bolt 应用您编写的代码,将在 第十二章 中介绍。使用 bolt apply apache_linux.pp –server linuxclient.example.combolt apply grafana_windows.pp –server windowsclient.example.com 命令将清单复制到服务器,并在客户端运行 puppet apply。对于 Linux 和 Windows 的示例,通过访问 http://hostname:8080 并确认 Linux 的 Hello World 或 Windows 的 Grafana 登录页面可见来测试您的解决方案。

示例解决方案可在 github.com/PacktPublishing/Puppet-8-for-DevOps-Engineers/blob/main/ch03/apache_linux.ppgithub.com/PacktPublishing/Puppet-8-for-DevOps-Engineers/blob/main/ch03/grafana_windows.pp 找到。

要在 noop 模式下测试运行,可以将 _noop => true 选项应用于 Bolt 命令。

虽然详细讨论每个核心类型可能不切实际,但下一节将高层次地介绍其他核心类型,这些类型对于创建更高级的配置非常有用。

核心资源类型

在本节中,我们将讨论核心资源类型。

用户和组类型

用户类型和组类型是大多数配置的核心,允许将 ensure 属性设置为 presentabsent。使用 Unix 平台作为提供程序时,用户通常会有最小的 uidgid 属性,而组则最少有 gid 属性。用户还可以通过 password 属性进一步强制执行,确保为任何设置的密码定义限制,传递加密密码并强制执行主目录和 Shell 设置。对于 Windows Server,需要注意的是,只能管理本地用户和组,尽管一个组资源可以通过 members 参数管理将域账户添加到该组的成员中。Puppet 中的名称是区分大小写的,但在 Windows 中是不区分大小写的。名称应该匹配,以免丢失任何自动生成的需求。Windows 还使用多种类型的名称,因此可以是 <计算机名\<用户名>BUILTIN\<用户名> 或仅仅是 <用户名>

例如,'DESKTOP-1MT10AJ\david, 'BUILTIN\david'david 在 Puppet 中都会被视为相同。

以下代码展示了 Windows 和 Unix 中账户和组的示例:

user { 'david':
  ensure   => 'present',
  groups   => ['BUILTIN\Administrators', 'BUILTIN\Users']
}
group { 'Users':
  ensure   => 'present',
  members  => ['NT AUTHORITY\INTERACTIVE', 'NT AUTHORITY\Authenticated Users', 'DESKTOP-1MT10AJ\david'],
}
user { 'ubuntu':
  ensure             => 'present',
  comment            => 'Ubuntu',
  gid                => 1000,
  groups             => ['adm', 'dialout', 'cdrom', 'floppy', 'sudo', 'audio', 'dip', 'video', 'plugdev', 'lxd', 'netdev'],
  home               => '/home/ubuntu',
  password           => '!',
  password_max_age   => 99999,
  password_min_age   => 0,
  password_warn_days => 7,
  shell              => '/bin/bash',
  uid                => 1000
}
group { 'ubuntu':
  ensure   => 'present',
  gid      => 1000
}

我们在这里看到,Windows 用户 David 是管理员组和用户组的成员。我们看到用户组及其成员列表。接下来,我们可以看到 Unix 上 Ubuntu 用户的详细设置,包括密码设置、主目录和组设置。同样,某些用户和组可以作为资源声明被添加、确保不存在,并从系统中删除。

exec 类型

exec 类型与大多数 Puppet 类型有所不同,如果使用不当,可能会很危险。虽然大多数 Puppet 类型尝试描述服务器应该处于的状态,但 exec 提供了一种在服务器上运行脚本或命令的方式。这意味着声明一个 exec 类型需要特别注意,确保资源会是 apt-get update(Ubuntu 中用于更新包源的命令),如果我们使用 onlyif 属性、unlesscreates,或者 exec 具有仅刷新属性。

在第一种情况下,如果命令是 exec 报告运行。

使用 onlyif 属性,我们可以声明一个命令,如果它返回 true,则我们的 exec 将会执行。unlessonlyif 的反义词,使用一个命令,如果它返回 true,则我们的 exec 不会执行。最后,creates 会查找一个文件,以表明脚本已经运行。

第一个示例演示了如何禁用公共 Chocolatey 访问,除非命令在源中找到该访问已经被禁用:

exec { 'disable_public_chocolatey':
    command => "C:/ProgramData/chocolatey/choco.exe source disable -n=chocolatey",
    unless  => "\$sourceOutput = choco.exe source list; if (\$sourceOutput.Contains('chocolatey [Disabled]')) {exit 0} else {exit 1}",
    provider => powershell,
}

第二个示例展示了一个示例命令,除非该文件已经被创建,否则它会使用 cowsay 命令生成一个文件:

exec { 'Cowsay file':
   command => '/bin/cowsay Hello world > /etc/cowsaysays',
   creates => '/etc/cowsaysays'
}

注意

有一个可选的 PowerShell 提供程序,允许 exec 运行 PowerShell 脚本:forge.puppet.com/puppetlabs/powershell

第三种情况使用了 refreshonly 属性,因此通过使用 notifysubscribe 属性,我们可以设置 exec 仅在另一个资源被刷新时执行。以下的 exec 对于那些脚本无法被 Puppet 代码替代的情况非常有用:

exec  { 'refresh exampleapp configuration' :
  command   => [''/bin/exampleapp/rereadconfig']
  refreshonly => true
  subscribe    => File['config file']
}
file {'config file':
  path => '/etc/exampleapp/configfile',
  content => 'setting 1 = value'
}

如果脚本/命令是供应商提供的,或者只是一个已经有效的遗留脚本,并且将其重构为 Puppet 代码的工作量不值得,那么就可能出现这种情况。

在 Unix 平台上,Puppet 6.24+ 和 7.9+ 引入了一项名为参数化 exec 的新特性,允许你将 command 属性作为一个数组传递,数组的第一部分是命令,第二部分是参数。它采用了参数化系统调用的安全方法,确保代码无法被注入。在以下示例中,传统的 exec 只包含命令,它会执行由分号分隔的所有命令,在我们的简单示例中,会回显 real parameters 并运行 rm,而使用参数化 exec 的改进方法,它会将第二个参数作为要传递的字符串并回显,确保命令的原始目的并防止命令注入:

exec  { 'parametrized command'
  command => ['/bin/echo', 'real parameters; rm -rf /']
}

这个使用 echo 的示例显然被简化了,当我们查看 第八章第九章 时,这一点将变得更加清晰。到时我们将看到如何将用户数据传入 Puppet 代码中,并且我们必须进行防御性编码。

Augeas 类型

Augeas 是一个仅在 Linux 上可用的类型;它在 Puppet 的早期版本中使用得更多,当时用于操作文件的选项非常有限,但在更复杂的情况下,它仍然有其用处。它可能计算开销较大,因此你需要谨慎使用。Augeas 可以将文件从其本地格式解析成树状结构,然后你可以对其进行操作。它使用 lenses 来执行这些翻译。

举个例子,如果我们想操作 access.conf 文件,可以使用 augtool(Augeas 的 CLI 接口)查看文件并使用以下命令打印出来:

augtool print /files/etc/security/access.conf

假设我们的文件包含以下行:

+ : john : 2001:4ca0:0:101::/64
+ : root : 192.168.200.1 192.168.200.4 192.168.200.9
- : ALL : ALL

使用默认的 lens 时,结果将打印如下:

/files/etc/security/access.conf/access[1] = "+"
/files/etc/security/access.conf/access[1]/user = "john"
/files/etc/security/access.conf/access[1]/origin = "2001:4ca0:0:101::/64"
/files/etc/security/access.conf/#comment[83] = "All other users should be denied to get access from all sources."
/files/etc/security/access.conf/access[2] = "+"
/files/etc/security/access.conf/access[2]/user = "root"
/files/etc/security/access.conf/access[2]/origin[1] = "192.168.200.1"
/files/etc/security/access.conf/access[2]/origin[2] = "192.168.200.4"
/files/etc/security/access.conf/access[2]/origin[3] = "192.168.200.9"
/files/etc/security/access.conf/access[3] = "-"
/files/etc/security/access.conf/access[3]/user = "ALL"
/files/etc/security/access.conf/access[3]/origin = "ALL"

这允许你在语法中对单独的部分和数值进行编程引用,因此如果在客户端状态中,你想从所有条目中删除用户 john 的任何条目,augtool 可以执行以下操作:

augtool rm /files/etc/security/access.conf/*[user="john"]

要在 Puppet 中使用这个,Augeas 只有一个 changes,它是你希望运行的 Augeas 命令,lens 是你希望使用的非默认翻译,onlyif 可以检查树的内容,判断是否需要执行更改。将前面的示例创建为 Puppet 资源的方式如下:

Augeas { 'remove John from access.conf' :
  changes => 'rm /files/etc/security/access.conf/*[user="john"]'
}

注意

Augeas 是一个强大的工具,但应谨慎使用。有关语法的更多细节可以在augeas.net/docs/forge.puppet.com/modules/puppetlabs/augeas_core/reference中找到。

notify类型

notify类型用于向日志发送消息。它更可能用于调试目的,而非生产环境使用,因为它不是幂等的,并且每次运行时都会导致 Puppet 报告看到变化。使用message参数作为要打印的字符串时,会默认从标题中获取值。一个简单的示例如下:

notify { 'print a message to logs'}

注意

notice函数更适用于打印消息,因为这些消息不会出现在 Puppet 报告的变更日志中。请参见第五章

还有更多的核心类型,但本章演示的命令能够列出可用的类型、查看属性并提供文档,这些应该能帮助你理解如何进一步调查其他可能有用的类型,包括从puppet forge安装的类型,相关内容将在第八章中详细介绍。

信息

在本章中,我们已经强调了通过添加到目录下,Puppet 控制下的资源,无论它们是强制存在还是缺失。Puppet 没有回滚的概念,因此从 Puppet 控制中删除一个资源将仅仅使其变为未管理状态,保持在上一次 Puppet 运行时的状态。因此,在代码更改的回退过程中,应该始终考虑这一点。

元参数和高级资源

本节将首先介绍元参数,它们是适用于任何资源类型的属性。对于实验部分,我们涵盖了beforerequirednotifysubscribe,这些用于在资源之间创建依赖关系。接下来,还有几个其他有用的属性,具有不同的效果。要查看类型和提供者上元参数的完整文档,可以在describe命令中添加meta标志:puppet describe <file type> --meta

审计

audit元参数允许我们监视未管理的 Puppet 参数;这可以是一个属性的数组列表,也可以是监视所有未声明属性的all。在以下示例中,我们声明了这一点:

file {'/var/tmp/example'
  mode => 0770,
  audit  => [owner,group]
}

这会在 Puppet Enterprise 中创建一个/opt/puppetlabs/puppet/cache/state/state.yaml文件,或者在 Puppet 的开源版本中创建/var/lib/puppet/state/state.yaml文件,用于记录审计状态。应用前述资源将产生以下输出:

Notice: /Stage[main]/Main/File[/var/tmp/example]/owner: audit change: previously recorded value 'absent' has been changed to 'root'
Notice: /Stage[main]/Main/File[/var/tmp/example]/group: audit change: previously recorded value 'absent' has been changed to 'root'

当资源被创建时,它的状态将被记录为从absent(缺失)变化为present(存在),然后将报告在 Puppet 运行时是否发现先前记录的值已发生变化。state.yaml文件将更新为这个新值,因此如果需要,必须对这一变化采取相应的措施。

标签

tag参数允许我们为资源应用标签,这些标签可以是单个字符串,也可以是多个字符串组成的数组。默认情况下,多个标签会应用于资源:标题、资源类型和资源所在的类。标签在只希望运行清单的某些部分时特别有用,因为 Puppet 的本地运行和基于代理的运行都可以使用--tag标志,只运行具有特定标签的资源。

例如,来看一下名为example.pp的 Puppet 资源清单:

class example::access {
  group {'ubuntu':
    ensure   => 'present',
    gid      => 1000,
    tag      => ['pci','sox']
  }
  user {'ubuntu':
    ensure  => 'present',
    tag         => 'pci'
  }
}

该组将有groupubuntupcisox标签,而用户将有userubuntupci标签。此外,两者还将有一个类名标签,example::access。使用puppet apply --tags pci example.pp命令时,两个资源都会类似地应用;ubuntu标签会应用两个资源,而运行带有sox标签的命令则只会运行该组。

还有一些其他的元参数,如aliasloglevel,尽管它们没有太大使用频率且没有值得详细讨论的风险,但仍可以在www.puppet.com/docs/puppet/8/metaparameter.html查看,或者通过运行puppet describe <any type> -m来了解。

之前展示的资源声明遵循了相同的简单声明模式,但还有其他几种方法可以实现更多的灵活性和高级功能。

资源元类型

Puppet 有一个resources元类型,可以用于确保移除未管理的资源类型。如果把它当作<type> Puppet 资源的输出,可以找到任何没有匹配namevar属性的资源,并标记为absent。它使用四个属性;一个是purge属性,可以为truefalse,另两个属性在使用用户类型的资源时特别相关——unless_system_user,该属性接受truefalse或指定的最小 UID,并确保系统定义,或者你可以在minimum_uid参数中定义整数或整数数组,以保护这些 UID 免受清除。要生成数字列表,可以使用stdlib模块中的range()函数,这会使生成过程更加简便。我们将在第五章中讨论函数,以便清楚地了解函数的工作方式。与所有资源一样,元参数也可以使用,noop在这里是建议使用的,因为清除所有用户可能过于激进,因此首先查看哪些用户将被删除可能是最好的报告方法:

resources {'user':
  purge => true,
  noop => true
}

注意

ssh_authorized_key类型应通过purge_ssh_keys属性在用户类型上进行管理。

标题数组

当声明多个具有相同属性的资源时,可以将标题声明为资源数组,像多个资源声明一样工作。我们将在第四章中讨论数组,但目前请理解,标题数组可以使用开方括号和分隔逗号来声明,因此一个资源的标题将像以下示例:

file{ ['/opt/example1','/opt/example1/etc','/opt/example1/bin'] :
  owner => user,
  group => user,
  mode  => 0750
}

覆盖参数

这是资源引用的语法:

  • 类型首字母大写

  • 方括号中的标题

  • 开括号({

  • 要覆盖的属性

  • 闭括号(}

可以覆盖已声明资源的属性。在此示例中,我们将Audit设置为true,并将group设置为other_group,用于/opt/example/bin文件资源:

File['/opt/example/bin/'] {
  group  => other_group,
  Audit   => true
}

最好将其与标题数组结合使用,这样可以定义通用的默认值,然后为特定的命名资源设置特定的属性。在本书中,我们建议谨慎使用此功能,以避免在所有内容声明时引起混淆。

属性展开

属性展开符(*)是一种使用哈希值填充类型属性的机制;在需要覆盖不同提供者使用的属性差异时,这非常有用。在使用正常语法的资源中,你可以将属性集设置为*,然后创建一个包含你要使用的属性的哈希值。我们将在第四章第七章中讨论哈希、变量和条件语句,但在这个例子中,应该清楚我们正在设置软件包选项哈希,包含一个name属性,对于 Debian 设置为apache2,作为默认值则是httpd

case $facts['os']['name'] {
  /^(Debian|Ubuntu)$/: {
  $package_options = {
    "name"  => "apache2"
    }
  }
  default:  {
  $package_options = {
    "name"  => "httpd"
    }
  }
}
Package { 'http' :
  ensure => latest,
  *       =>  ${package_options}
}

这会导致http软件包资源在 Ubuntu 和 Debian 系统上使用http2作为名称,而其他系统则默认为httpd。此功能应谨慎使用,以免影响可读性。

实验

为了练习我们所讨论的内容,让我们回顾之前的例子,创建一个单一的清单文件all_grafana.pp,它可以在 Linux 和 Windows 上安装、配置并运行 Grafana。由于我们尚未讨论事实(facts),请理解,就像我们之前的例子一样,case语句可以使用$facts ['os']['family']来查找 Red Hat 或 Windows,从而区分我们的两个客户端。请注意,rpm install文件可以在dl.grafana.com/enterprise/release/grafana-enterprise-8.4.3-1.x86_64.rpm上获取,Linux 的配置文件位于/etc/grafana/grafana.ini

作为第二个练习,创建一个单独的清单来在 Linux 客户端上创建一些用户,linux_users.pp 创建 3 个用户 exampleappdevexampleapptestexampleappprod 和一个组 exampleapp,所有用户都使用此组作为其主要组。 exampleappprod 应该从 authorized 中清除 ssh 密钥。最后,它应检查客户端是否有其他非系统级别的用户(但不强制执行任何操作)。

根据上一个实验,您可以通过列出的命令来测试您的清单:bolt apply manifestname.pp –``server servername.example.com

您可以在 github.com/PacktPublishing/Puppet-8-for-DevOps-Engineers/blob/main/ch03/all_grafana.ppgithub.com/PacktPublishing/Puppet-8-for-DevOps-Engineers/blob/main/ch03/linux_users.pp 找到解决方案。

反模式

在本节中,我们将讨论一些 Puppet 文档中可找到和可用的资源功能,但本书强烈建议您不要使用,并将其作为最佳实践的一部分避免。我们在这里突出显示的资源功能强大,但会使资源声明变得更难阅读,并且需要更多的翻译和计算才能看到我们试图将服务器置于的状态。

抽象资源类型

当我们不希望预定义类型并且可能根据客户端决定要使用哪个资源时,可以使用抽象资源类型来声明资源。在这个简单的示例中,一个变量设置为类型,然后使用 Resource[<TYPE>] { <RESOURCE BODY>} 语法声明资源:

$selectedtype = exec
resource[$mytype] { "/bin/echo 'don't use this' > /tmp/badidea": creates => /tmp/badidea , }

对这种情况的简单翻译如下:

exec {"/bin/echo 'don't use this' > /tmp/badidea":
  creates  =>  /tmp/badidea
}

本书建议不使用抽象,因为它不常用且使代码变得不易读,尤其是对于经验较少的 Puppet 用户。最佳方法是使用case语句或if语句,我们将在第七章中详细讨论。如果代码分歧太大,最好将资源分开到不同的类中,而不要强制共享资源类型较少的平台。

默认

声明默认值有两种方法,但本书建议不使用任何一种。一个具有多个资源声明体的默认主体会破坏声明的单一用途的良好实践,而默认资源语句在理解其作用域时可能会很危险。

默认资源主体

在这里,资源可以有一个默认主体,遵循与普通资源声明相同的语法,但以 default: 开头其中一个主体;主体的顺序并不重要。

此示例展示了两组标题数组,取默认值并为第二组标题集更改默认模式:

file {
  default:
    ensure => directory,
    owner => 'exampleapp',
    group => 'exampleapp',
    mode => '0660'
  ;
  ['/opt/example','/opt/example/app','/etc/exampleapp']:
  ;
  ['/var/example','/var/example/app',]:
    mode => '0644'
  ;
}

如声明资源时所讨论的,本书强烈建议保持清晰的单一目的资源声明,因为将多个主体组合在一起会使代码更难阅读。对于类似的结果,推荐的方法是使用标题数组并在适当的地方覆盖参数。

资源默认语法

第二种方法是使用默认资源语句语法:

  • 类型以大写字母和 { 开始

  • 属性及默认值列表

  • } 结尾

如果类型有多个命名空间,例如 concat::fragment,则每个命名空间部分应大写。

在此示例中,我们使用一个文件来设置所有未声明属性所有者组或模式值的文件资源的默认值:

file {
  owner  => 'exampleapp',
  group  => 'exampleapp',
  mode   => 0660
}

在 Puppet 的早期版本中常用,现在被认为只适用于 site.pp 文件(我们将在第十一章中讨论的全局设置文件)。这是因为 Puppet 不再使用动态作用域进行变量查找,而默认资源仍然是动态作用域的,这可能导致作用域蔓延,并无意中影响到目录中的其他资源。(作用域将在第六章中详细讨论)。由于在 site.pp 中使用默认值会使它们变得不易察觉且不够透明,本书建议不要使用资源默认值。

schedule

schedule 是与 schedule 资源类型结合使用的元参数。它允许我们用资源类型描述特定的计划,定义何时可以运行某个特定资源,以便如果 Puppet 在这个时间外应用,它将忽略该资源,以及它在此期间可以运行多少次。schedule 资源类型使用各种属性来描述范围、重复或天数:一个简单的示例是覆盖从周五晚上到周六早上 6 点到 9 点的时间段:

schedule { 'Friday Night':
  day=> 'Friday',
  range  => '18 - 9',
}

然后可以将其应用于资源:

exec {'/bin/echo weekend start > /tmp/example'
  Schedule = > 'Friday Night'
}

本书反对这种使用方式。这可能看起来很诱人,特别是对于那些有严格变更时间窗口的高监管环境,但 Puppet 应该强制执行预期的状态,因此偏离该状态应该是一个问题。创建计划使得应用内容变得不明确,并让状态暴露在服务器只被部分 Puppet 强制执行的脆弱时期。

导出器和收集器

当 Puppet 尝试允许节点之间交换信息以实现相互依赖时,就会发生导出和收集 Puppet 资源。它允许在一个节点上声明并运行资源,然后其他节点也可以应用这些资源。这是通过将信息导出到PuppetDB数据库完成的,Puppet 运行时会在收集时查阅该数据库。这意味着它只能通过 Puppet 代理设置运行,而不能通过本地 Puppet 运行。

导出资源只需在正常的资源声明前加上@@。导出的资源在PuppetDB中必须是唯一的,因此通常会在声明中使用主机名事实(包含主机名的变量)。在这个示例中,一个主机条目被导出,并将被放入每个收集服务器的主机文件中:

@@host { "Oracle database host entry ${::hostname}" :
  name  => 'dbserver1',
  ip    => '192.168.0.6',
  tag   => 'oracle'
}

收集资源接着涉及声明一个收集器,它是以大写字母开头并带有飞船<<| |>>)声明的类型;在其中,可以声明标签来过滤集合。完成此示例后,此集合将确保所有标记为oracle的导出主机资源应用到服务器上:

Host <<| tag = oracle |>>

导出和收集有两个关键问题;第一个是代码的可读性变差,难以理解可能应用于节点的资源。第二个是它使得 Puppet 基础设施设置的可扩展性和高可用性考虑变得更加复杂。因此,根据最佳实践,本书建议避免使用任何导出器和收集器。

摘要

在本章中,你学习了如何声明资源以及可以执行的语法和样式检查,以便开发一致的代码。类被展示为一种将资源分组的方法,它允许我们调用类并将这些资源组应用于服务器。定义类型则被展示为一种创建可重复模式的 Puppet 代码的方法,这些模式可以根据参数有所变化。

我们展示了如何探索和使用类型与提供者,并了解了一些最常用的核心类型以及如何有效地使用它们。文件、包和服务类型被展示为安装、配置和启动应用程序的良好基础。我们看到,Puppet 资源如何相互关联以确保顺序,并如何将这些本地编写的资源应用到服务器上进行测试。

本章介绍了核心资源元参数,以帮助理解如何使用资源的各种功能——标签用于允许对资源进行过滤运行;审计用于监控发生在资源非托管属性上的变化,使用noop可以声明资源为不可执行但仍进行报告。

最后,介绍了各种反模式——默认资源,它们存在作用域问题;默认体,它们导致资源语句过载;调度,它使得理解 Puppet 运行变得复杂;以及导出和收集器,它们在可扩展性、可用性以及将数据从代码中抽象化方面存在问题。

在下一章中,我们将介绍变量和数据类型,这将使我们能够为变量分配值,并控制这些值是什么以及如何与它们进行交互。这将使我们减少重复,并使资源更易于更新和管理,同时提供了一种将数据传递给类的方式。

第四章:变量和数据类型

本章将介绍 Puppet 如何处理变量,特别是 Puppet 在如何使用和声明变量方面与大多数声明性语言的不同之处。我们将查看用于定义变量值所能包含的内容以及如何与之交互的核心数据类型。然后,我们将探讨数据类型和变量如何允许我们在第三章中讨论的类接收外部数据并处理默认值。

数组和哈希将会详细讨论,包括如何声明它们、访问值以及如何使用操作对它们进行操作。Sensitive 数据类型将会展示,你可以使用它来保护日志和报告中的值,同时明确该数据类型的限制以及它无法保护的内容。我们还将涵盖抽象数据类型,并展示如何允许更复杂和灵活的变量与值的定义。本章的最后,我们将讨论变量的作用域和命名空间如何与变量一起工作。我们还会讨论变量的作用域,以及如何访问不同作用域的变量,以及哪些作用域可以访问哪些级别的数据。

本章我们将讨论以下主要内容:

  • 变量

  • 数据类型

  • 数组和哈希

  • 抽象数据类型,包括 Sensitive

  • 作用域

技术要求

对于本章,你需要通过下载 github.com/PacktPublishing/Puppet-8-for-DevOps-Engineers/blob/main/ch04/params.json 文件,然后在 pecdm 目录中使用以下命令,来准备一个包含 Windows 客户端和 Linux 客户端的 Puppet 服务器标准架构:

bolt --verbose plan run pecdm::provision –params @params.json

本章的各个部分将给出使用 notify 函数的示例,该函数输出到代理命令行。这些示例可以通过将所有代码放入清单文件中——例如,example.pp——并运行 puppet apply example.pp 在本地开发环境中执行。

另外,任何必需的变量都可以使用 FACTER_variable_name 的环境变量格式进行设置,并运行 puppet apply –e '<example_code>'。要运行其中一个子字符串示例,你可以运行以下代码:

export FACTER_example_string='substring'
puppet apply -e 'notify{ "${example_string[3]}": }'

变量

本节将讲解如何在 Puppet 中使用变量,以及这与其他过程性语言的区别。理解 Puppet 变量的关键是,它们在给定作用域内的编译过程中仅被赋值一次。在传统的过程性语言中,通常会在代码中广泛使用变量,在代码运行时收集当前状态,使用变量来追踪并在代码的不同阶段更新它,以便做出处理和决策。以下是一个简单的 PowerShell 脚本示例,它多次运行命令,并将输出添加到一个单一变量中。它通过使用select-string在用户代码目录中的.sh.pp文件中查找包含?的文件来实现:

$Matches = Select-String -Path "$PSHOME\code\*.sh" -Pattern '\?
…
…
$Matches = $Matches + Select-String -Path "$PSHOME\code\*.pp" -Pattern '\?

由于所有的求值在目录开始时进行,并基于服务器发送过来的编译状态,Puppet 并不会执行这个状态检查。这样就提供了将服务器状态转化为所需状态所需的步骤。在 Puppet 中,我们为重复使用的内容(如文件路径)或条件逻辑(如ifcase)分配变量。在这些情况下,必须选择一个值并进行赋值,具体取决于初始状态。

注意

我们在这里稍微简化了过程,因为现在有延迟函数可以在复杂性处理后运行。然而,这仍然不允许我们重新赋值。我们将在第五章中更详细地讲解这一点。

Puppet 变量的声明格式是:美元符号($)后跟变量名,再加上等号(=)和要赋值的值。例如,一个名为example_variable,值为'this is a value'的变量应该像这样:

$example_variable = 'this is a value'

请注意,与资源不同,变量依赖于求值顺序,并且必须在代码中声明后才能调用。

注意

有几个被称为内置变量的变量,它们返回服务器信息。然而,由于这些更多涉及基础设施和环境,它们将在第十章第十一章中详细讲解,在相关部分讨论。

命名

变量名是区分大小写的,可以包含大写字母、小写字母、数字和下划线,但必须以下划线(_)或小写字母开头。例外的是正则表达式捕获变量,这些变量仅使用数字命名,例如$0$1等。我们将在第七章第十一章中讨论它们,并将它们作为条件语句和节点定义的一部分使用。

注意

变量名以下划线开头将限制其仅限于局部作用域使用,具体细节将在本章末尾讨论。

保留的变量名

有一些内置变量是不能在代码中使用的,具体如下:

  • $``facts

  • $``trusted

  • $``server_facts

这些都是从 Facter 生成的内置变量,不能被使用或重新赋值。我们将在 第五章中详细讨论这些变量,以及它们提供的值。

正如我们在 第三章中所讲,$title$name 变量是由类和定义的类型使用的,应该避免使用。

保留字的完整列表可以在 Puppet 文档中查看,链接为 www.puppet.com/docs/puppet/8/lang_reserved.html

插值

Puppet 变量在调用时可以被评估并解析为其赋值,前提是没有使用引号,或者作为变量与我们数据中的双引号部分混合时使用。lint 检查强制执行的样式指南将确保在赋值中不包含变量时使用单引号,而仅包含变量时不使用引号。值和变量的混合可以通过双引号来书写。样式指南中会指出何时使用这种混合写法。linter 会检查确保它使用了大括号 {}。这可以通过下面的示例看到,在使用双引号进行插值时,mixed_variable 被赋值给变量声明:

$database_id = $dbname
$base_directory = '/opt'
$database_directory = "${base_directory}/database/${database_id}"

正如我们在上一章中所描述的,notify 函数可以用来检查变量的值:

notify{'debug variable':
  message => "The database directory is ${database_directory}"
}

本节讨论了 Puppet 变量如何与其他编程语言的变量不同,特别是在有状态性方面,以及它如何声明、访问和命名变量。

数据类型

Puppet 中的每个值都有一个数据类型;例如,在上一节中,变量被赋值为 String 类型。数据类型,当作为大写的不带引号的字符串使用时,如 Integer,可以用来指定类、定义的类型或 Lambda 中应该包含的参数,从而实现数据验证:

class example (
  String example_string = 'hello world',
  Integer example_integer = 1
) {
}

数据类型还可以用来比较变量的值、条件检查值,并根据结果执行不同的操作。例如,为了确认一个变量是否包含整数,可以使用以下匹配表达式。在这里,我们确认 example_integer 变量包含一个整数:

$example_integer =~ Integer

条件语句和比较将在 第七章中详细介绍。

下一部分将介绍最常用的 Puppet 核心数据类型。不幸的是,Puppet 没有类似于 puppet describe 命令的数据类型,所以所有参考必须来自于网络和 GitHub 文档,具体请见www.puppet.com/docs/puppet/8/lang_data_type.html。如果您使用的是来自 Forge 模块提供的类型,这将在第八章中详细介绍,文档应该在模块的参考页面中。各种函数可与数据类型一起使用,但这里不会讨论此内容。我们将在第五章中详细讨论函数。

字符串

字符串是 Puppet 中最常用的数据类型,正如在第一章中讨论的那样,最早的 Puppet 中仅使用字符串类型。字符串是任何长度的非结构化文本,以 UTF-8 编码。有四种方式可以在 Puppet 中声明字符串:

  • 未加引号

  • 单引号

  • 双引号

  • Heredoc

未加引号的字符串

未加引号的字符串是以小写字母开头的单词,只包含字母、数字、连字符 (-) 和下划线 (_),且不能是保留字。保留字通常是诸如 class 之类的关键字或其他语言函数;完整列表可以在www.puppet.com/docs/puppet/8/lang_reserved.html#lang_reserved_words查看。

未加引号的字符串用于接受有限单词集的资源属性,例如 Puppet 服务资源类型,它的 ensure 属性只能接受 runningstopped

service { 'defragsvc':
  ensure => stopped
}

单引号字符串

单引号字符串可以包含多个单词,但如前所述,不能进行变量插值。然而,它们可以包含换行符和使用反斜杠 (\)、转义反斜杠或单引号 (') 的转义序列。这允许在字符串本身内使用单引号,并在字符串末尾使用反斜杠。此外,可以通过按下 Enter 键来实现换行符。

以下示例展示了 sed_command 变量,其中包含作为 sed 命令一部分所需的单引号,以在单引号字符串中进行转义,然后是 install_dir 变量,表示带有结尾反斜杠的 Windows 文件路径:

$sed_command = '/usr/bin/sed -i \'s/old/new/g\''
$intall_dir = 'c:\Program Files(x86)\exampleapp\\'

双引号

双引号字符串可以完全插入变量,并且支持更多可用的转义字符。除了单引号、反斜杠和转义字符,双引号还可以解释以下内容:

  • \``n: 换行符

  • \r: 回车

  • \``s: 空格

  • \``t: 制表符

  • \$: 美元符号,用于防止变量插值

  • \uXXXX: 其中 xxxx 是表示 Unicode 字符的四位十六进制数字

  • \u{X}: 其中 X 是两个到六位数字之间的十六进制数字,位于大括号 {}

  • \": 双引号

  • \r\n: Windows 换行符

和单引号一样,换行符也可以在文本中使用(即只需按下 Enter 键)。

注意

如果在双引号中使用反斜杠而没有进行转义,且后面没有有效的转义字符,它将继续处理并将其视为普通字符,但会在日志中显示以下信息:警告:未识别的转义序列。以下是一个示例:

警告:未识别的转义序列 '\T' 位于 C:/Users/david/code/test.pp:1:50

这通常影响使用双引号的 Windows 用户路径。

一个简单的双引号字符串示例,使用换行符和制表符(这些在 Makefile 内容的语法中非常重要),如下所示。这会创建一个字符串,然后可以在文件内容中使用:

$make_file_content = "hello:\n\techo \"hello world\""
file '/home/david/makefile' : {
  content => $make_file_content
}

Heredoc

Puppet 对 heredoc 的实现涉及使用标签来指示 heredoc 文件内容的开始和结束。起始标签通常由以下元素组成:

  • '@('

  • 一个字符串,称为结束文本,可以用双引号括起来以启用插值

  • 一个可选的转义开关(或多个开关),以斜杠(/)开头,用于在文本中启用转义开关

  • 一个可选的冒号(:),后跟语法名称检查

  • ')'

要在 Puppet 中使用 heredoc,内容应在起始标签后的行中输入,保持所需的精确格式。heredoc 的结束由结束标签表示,结束标签应包括以下元素:

  • 一个可选的竖线(|),表示应该从文本行中删除多少缩进

  • 一个可选的短横线(-),它会从 heredoc 中移除最后的换行符

  • 与起始标签中使用的结束文本标签相同

Puppet heredoc 中的结束文本是一个字符串,可以由大小写字母、数字和空格组成,但不能包含换行符、斜杠、冒号或圆括号。默认情况下,heredoc 的内容不会解析转义字符,因此如果需要转义开关,必须声明它们。以下转义开关可用,并且与双引号字符串的转义序列相同,但不需要双引号(因为它们在 heredoc 中没有特殊含义):

  • n: 换行符

  • r: 回车符

  • t: 制表符

  • s: 空格

  • $: 美元符号,用于防止在双引号文本中进行插值

  • u: Unicode 字符

  • L: 换行符或回车符

  • \:: 所有之前提到的转义序列均可用

当选择任何转义序列时,可以使用 \\ 来转义反斜杠。

默认情况下禁用变量插值,因此,如前所述,结束文本应在需要时用双引号括起来。

语法检查适用于各种内容,例如通过 pp 的 Puppet 清单或通过 ruby 的 Ruby 文件:

@(END:pp)
@(END:ruby)

语法检查仅在没有启用变量插值的情况下运行;如果输入了 Puppet 不支持的类型,它将被忽略。有关可用语法检查器的详细信息,可以在 Puppet 规范中找到,该规范还包含有关创建自定义语法检查器的详细内容。

Heredoc 声明可以放置在任何可以声明字符串的地方,因此,举个例子,exec 命令中的长命令可以如下声明:

exec { 'create databases':
  command => @("Database Commands"/L)
    sudo -u postgres psql \
    -c "CREATE DATABASE ${database1} ENCODING 'utf8' LC_COLLATE 'en_US.UTF-8' LC_CTYPE 'en_US.UTF-8'" \
    -c "CREATE DATABASE ${database2} ENCODING 'utf8' LC_COLLATE 'en_US.UTF-8' LC_CTYPE 'en_US.UTF-8'" \
    -c "CREATE DATABASE ${database3} ENCODING 'utf8' LC_COLLATE 'en_US.UTF-8' LC_CTYPE 'en_US.UTF-8'"
    |-"Database Commands"

本书建议谨慎使用 heredocs。在 exec 中执行长命令时,如前面示例所示,这种做法可能合适,但特别是对于文件内容,通常会使代码变得杂乱并且容易混淆,因此最好将其放在模板中,如第七章所讲,或作为模块中的文件,如第八章所述。关于如何最好地存储数据的问题将在第九章中讨论。

访问变量中的子字符串

在 Puppet 中调用字符串,最简单的方法是使用 $ 符号,后跟变量名。但是,如果变量名包含无效字符,如空格,Puppet 将认为变量名已结束。因此,为了确保在字符串中正确地插入变量,最好将变量名括在花括号 {} 中。

要访问字符串中的特定字符或子字符串,Puppet 允许你使用 [, ] 指定一个索引范围,这也支持负数索引,从字符串末尾倒数或改变返回字符的顺序。例如,下面的代码将一个名为 'example_string' 的变量设置为 '``substring' 字符串:

$example_string = 'substring'

可以使用各种组合;例如,可以通过从开头取索引(例如 3)来调用单个字符,从而返回 s(我们从 0 开始)。

要从字符串变量中提取单个字符,在 Puppet 中,你可以指定从 0 开始的字符索引。例如,要提取字符串变量的第三个字符,你可以使用索引 3(因为索引从 0 开始)。在 Puppet 中,这可以如下表达:

notify { "${example_string[3]}" :}

这将返回索引 3 处的字符,在本例中为 's'

负数索引可以从字符串末尾开始,使用 -6 返回相同的 s 字符:

notify { "${example_string[-6]}" :}

要在 Puppet 中提取字符串变量的特定部分,可以使用方括号表示子字符串的起始索引和结束位置。例如,如果你有一个名为 'example_string' 的字符串变量,其值为 'substring',并且你想提取一个从第三个字符开始并包含接下来的五个字符的子字符串,你可以在 Puppet 中使用以下语法:

notify { "${example_string[3,6]}" :}

这将返回从索引 3 开始(对应于 'substring' 中的字母 's')并包含接下来的五个字符,在本例中为 'string'

要提取从负索引位置开始的子字符串,可以为停止位置指定负值。例如,要提取从倒数第四个索引位置开始并包含接下来的三个字符,可以使用以下语法:

notify { "${example_string[-4,-1]}" :}

这将返回从倒数第四个字符开始的子字符串(它对应于'substring'中的字母't'),并包括接下来的三个字符,在这种情况下是'tri'

最后,要提取一个从负索引位置开始并包括一定数量正整数的子字符串,可以使用以下语法:

notify { "${example_string[-4,4]}" :}

这将返回从倒数第四个字符开始的子字符串(它对应于'substring'中的字母't'),并包括接下来的四个字符,在这种情况下是'ring'

这种子字符串操作在需要将包名、应用程序版本或其他一致的名称字符串拆分为不同变量时非常有用。作为一个更实际的例子,一个组织有主机名,它们以位置代码开始,并包含角色、环境和服务器 ID:

$hostname = flkoracprd00034
$location = $hostname[0,3]
$role =$hostname[3,3]
$environment = $hostname[6,3]
$id = $hostname[-5,5]

字符串数据类型参数

当将参数的类型设置为字符串时,使用大写关键字String,并可选地指定字符串的最小和最大长度:

String[<Minimum length>, <Maximum Length>] $variable_name

最小值默认为 0,最大值默认为无限。如果要隐式使用默认值,可以使用默认的未加引号字符串关键字。

让我们来看一个名为database的类,它接受一个四个字符的数据库 ID 字符串,一个长度在六到八个字符之间的用户名(如果未提供,默认值为dbuser),以及一个长度不限的描述:

class 'database': {
  String[4,4] database_id,
  String[6,8] username = 'dbuser' ,
  String description,
}:

数字

本节将介绍 Puppet 用于数字的两种类型:整数和浮动点数。我们还将看看可以对它们执行哪些算术操作,数字如何与字符串之间转换,以及这些类型的变化。

这两种类型的数字都没有使用引号进行声明。在这里,字母的大小写不重要。以下模式是可用的:

  • (假设为正数)

  • 数字字符(八进制数字以0开始)

  • (假设为正数)* 0x0X(大小写不重要)* 数字字符与大写或小写字母的混合* (假设为正数)* 数字字符(使用-1 到 1 之间的数字时需要0)* 小数点* 数字字符* 可选的eE,后面跟着数字(用于科学浮点数)

以下是一些简单且恰当命名的例子,涵盖了上述类型中的每一项:

$integer = 42
$negative_integer = -84
$float = 32.3333
$scientific float = 3e5
$octal = 0678
$hex = 0x

需要注意的是,八进制或十六进制数字不能表示为浮点数,如果尝试这样做,将会导致错误,因为它不是有效的八进制或十六进制数字。

算术运算符

我们无法对变量执行重新赋值操作,但可以基于已赋值变量之间的运算来赋予新变量。以下表达式可以用于变量之间:

  • +: 加法

  • -: 减法

  • /: 除法

  • *: 乘法

  • %: 取余,表示左边除以右边的余数

  • <<: 左移

  • >>: 右移

左移和右移运算较为陌生,需要进一步解释。左移是将第一个变量乘以 2 的第二个变量次幂。例如,5 << 3 等同于 5 * 23,结果为 40。

右移是将第一个变量除以第二个变量的 2 的幂。例如,32 >> 2 等同于 32 / 22,结果为 8。

注意

对于左移和右移,浮点数将向下舍入为整数。

此外,负号(-)可以用作前缀来取反变量,并且括号可以用来管理运算的优先级,在这里 括号、次序(指数/幂或根号)、除法、乘法、加法和减法 (BODMAS) 规则适用。移位运算在这个优先级中本质上被当作乘法和取余运算来处理。

整数与浮点数之间的任何运算都会得到浮点数,且对整数的运算会导致浮点数向下舍入为整数。

以下是使用这些运算符的一些示例:

$a = 5
$b = 3
$addition = $a + $b
$subtraction = $a - $b
$division = $a / $b
$multiplication = $a * $b
$modulo = $a % $b
$shift_left = $a << $b
$shift_right = $a >> $b
$negate = -$a

为了进一步展示括号如何强制执行 BODMAS 规则,以下示例将等于负 40:

$bodmas_example = ($a + $b) * -$a

字符串到数值的转换

如果字符串用于数值运算,它会自动转换,但在其他任何上下文中则不会发生此情况。要将字符串转换为数字,可以声明对象为整数、浮点数或数值(我们将在 抽象数据类型,包括敏感数据 部分介绍数值对象)。转换的示例是将字符串 1 转换为整数,字符串 1.1 转换为浮点数:

$string_integer='1'
$string_float='1.1'
$converted_integer=Integer($string_integer)
$converted_float=Float($string_float)

数值到字符串的转换

数值类型在与字符串插值时会自动转换为字符串;这种自动转换使用的是十进制表示法。String 对象声明也可以用于转换,示例如下:

$string_from_integer = String(342)

整数数据类型

当将参数类型设置为整数时,使用大写的 Integer 关键字,并可以指定整数的最小值和最大值:

Integer[<Minimum Value>, <Maximum Value>]

默认值在技术上是负无穷大和正无穷大,但由于 Puppet 使用 64 位带符号整数,这个范围大约是从 −9,223,372,036,854,775,808 到 9,223,372,036,854,775,807。

浮点数据类型

当将参数类型设置为浮点数时,使用大写的 Float 关键字,并可以指定整数的最小值和最大值:

Float[<Minimum Value>, <Maximum Value>]

默认值在技术上是正无穷大和负无穷大,但在实际操作中,这个范围是 Ruby 实现中双精度浮点数的 -1.7E+308 到 +1.7E+308。

例如,考虑以下代码块,它定义了Class application::filesystem,用于在已知的10010000的范围内为一个卷组分配百分比:

Class application::filesystem (
Float[0.1, 99.9] percentage_application,
Integer[100, 10000] volume_group_size
) {
}

undef

undef在 Ruby 中被视为等同于 nil,表示没有给变量赋值。默认情况下,strict_variables设置为false,这意味着未声明的变量默认值为undef。在第十章中,我们将看到可以在puppet.conf配置文件中设置此项。

作为一个简单的例子,以下代码会通知Printtest1尚未声明:

  notify {"Print $test1":}

undef数据类型的唯一值是未加引号的undef,并且它本身不用于参数数据类型。这是因为强制要求值的缺失没有意义。

在本章的抽象数据类型,包括敏感类型部分,我们将看到如何接受undef值作为参数的一部分,作为可行选项的选择。

当插入到字符串中时,undef会被转换为空字符串('')。

调用

第五章第八章中,我们将学习一些函数,如delete_undef_valuesfilter,它们可以用于修剪数组和哈希中的undef值。

布尔值

Puppet 中的布尔值代表truefalse,在第七章中,当我们查看if/case语句时,你会发现所有 Puppet 的比较都会返回布尔类型。布尔变量应该仅包含一个未加引号的truefalse值。因此,这使得数据类型非常简单,没有参数——只有大写的Boolean关键字。

举个例子,下面的代码是一个exampleapp类,它有一个管理用户的参数,默认设置为true,并且有一些硬编码的变量:

Class exampleapp (
  Boolean manage_users = true
) {
  $install_ssh = true
  $install_telnet = false
}

转换

在大多数情况下,除非显式指定了数据类型,否则会自动将其转换为布尔值。例如,在if语句中,变量可以像布尔值一样使用,只需写$variable_name。然而,自动转换可能会让人困惑,因为只有undef会被转换为false。这意味着'false'字符串、空字符串('')、整数0和浮点数0.0都会被转换为true

在使用布尔声明时,空字符串无法转换,undef也一样,而'false'字符串、整数0或浮点数0.0会转换为false

由于这可能会让人困惑,因此最好使用来自puppetlabs-stdlib模块的num2boolstr2bool函数,这将在第八章中讲解。

正则表达式

regexp类型不同于我们迄今为止看到的类型。它表示 Puppet 中的有效正则表达式,正则表达式由基于 Ruby 正则表达式实现的正斜杠之间的表达式表示:ruby-doc.org/core/Regexp.html

正则表达式的使用将在第七章中更详细地介绍,届时它将得到更实际的应用。不过,值得注意的是,本章稍后会介绍几个包含多个类型的抽象类型,包括regexp

实验室

在上一章中,创建了一个合并的all_grafana清单,并提供了一个解决方案,详见github.com/PacktPublishing/Puppet-8-for-DevOps-Engineers/blob/main/ch04/all_grafana.pp。调整此文件,使其包含在一个名为all_grafana的类中,并且不再使用 Facter,而是使用参数。

这些参数应包括以下内容:

要实现类的赋值,编写一个类声明,分配变量以确保该类包含在目录运行中。当你在清单上运行bolt时,它将确保你已经包含了变量。解决方案可参考github.com/PacktPublishing/Puppet-8-for-DevOps-Engineers/blob/main/ch04/all_grafana.pp

数组和哈希

本节将涵盖 Puppet 中的两种核心数据集合:数组和哈希。你将学习如何创建、访问以及执行操作,将值操作到一个新的变量中。

分配数组

Puppet 数组是通过用方括号括起来的以逗号分隔的值列表来创建的。最后一个元素后可以添加可选的逗号,但本书不推荐这样做,以保持样式一致。例如,名为example_array的数组,包含firstsecondthird字符串,可以如下声明:

$example_array = ['first','second','third']

数组可以包含任何数据类型,也可以包含不同类型的混合数据。Puppet 变量不能在单独的值上重新赋值,也不能通过添加或移除值等任何其他操作进行修改。以下代码演示了如何将mixed_example_array数组赋值为整数1、来自example_boolean变量的布尔值false,以及example字符串:

$example_boolean = false
$mixed_example_array = [ 1, $example_boolean , 'example']

数组也可以为空,方括号中没有任何内容,[]。它们不会被识别为 undef,而是一个空数组。通常不会直接声明一个空数组;这通常是由于插值变量和运算符导致数组变为空。

访问数组索引

要访问数组变量的元素,可以通过索引指定特定的元素,这将返回该元素。例如,要获取 example_array 中第二个索引位置的第二个字符串并将其赋值给变量,可以使用以下代码:

$example_array = ['first','second','third']
$second_index = $example_arrary [1]

以下代码展示了一个 notify 资源,它输出一个字符串,插入了从 example_array 中倒数第三个元素的负数:

notify{ "The first element is ${example_array[-1]}"

访问不存在的元素将返回 undef。你不能在方括号和变量名之间放置任何空格;否则,它会被解释为一个变量,方括号会被分开。

访问数组的子集

访问数组子集时,使用第二个数字表示停止点。这与处理子字符串的方式不同。对于数组,正数表示要返回的元素数量。例如,使用计数位置 1 将返回一个包含单一元素的数组。要从 example_array 中提取一个只包含 'second' 元素的子数组,你可以使用以下代码:

$sub_array =  example_array[1,1]

这将把 ['second'] 子数组赋值给 $``sub_array 变量。

选择超过数组长度的长度将仅返回可用的元素。

负长度将从数组的末尾开始倒数。重要的是,与访问子字符串不同,不能通过越过起始索引来反转顺序;这将仅返回一个空数组。在下面的示例中,negative_sub 数组将返回整个数组,因为它的起始位置是 0,结束位置是数组末尾的第一个元素。empty_sub_array 变量将被赋值为空数组,因为结束位置会在起始位置之前。second_element_array 变量将被赋值为一个包含第二个元素的数组:

$negative_sub_array = example_array[0, -1]
$empty_sub_array = example_array[1, -3]
$second_element_array = example_array[1, -2]

嵌套数组

可以通过在数组内插入数组值来声明嵌套数组,插入的次数根据需要决定。然后,可以通过使用多个方括号来访问所需的级别。例如,如果创建一个嵌套数组,第一项是一个字符串,第二项是一个包含三个字符串的数组,第三项是一个字符串,尝试将第一项作为嵌套元素访问时,将返回字符串 i,因为索引为 0 的元素返回的是字符串。然后,在 first 字符串上使用第二组方括号。

nest_second 变量返回 nest_second 字符串,因为它返回了索引为 1 的嵌套数组;然后,使用第二组方括号访问第二个元素:

$nested_array= ['first',['nest_first','nest_second','nest_third'],'third']
$sub_string = $nested_array[0][1]
$nest_second = $nested_array[1][2]

要在数组中插入嵌套变量,必须用大括号包围变量名和方括号。例如,以下 notify 资源将打印 nested_array 的第一个元素:

notify {"Print ${nested_array[1][0]}":}

可以在嵌套括号内使用子集方法,但这可能会产生令人困惑且难以跟踪的访问方式,因此本书中不推荐这种风格。

数组操作符

一旦数组被赋值,就不能再直接操作它,但操作符可以操作数组的内容,以便分配新的数组变量。以下是可用的操作符:

  • <<:追加

  • +:连接

  • -:删除

  • *:扩展(Splat)

追加(Append)

追加(Append)接受任何类型的值,并将其作为新元素添加到数组的末尾。这包括将一个数组作为嵌套数组添加。要合并两个数组,必须使用连接操作符(+)。为了演示这一点,我们来看一个包含整数 12 的数组,它追加 3 形成一个新数组。这将产生一个新数组 [1,2,'three'];将 [3,4] 数组追加到 example_array 会形成一个新的嵌套数组 [1,2,[3,4]]

$example_array=[1,2]
$new_array=$example_array << 'three'
$append_nest=$example_array << [3,4]

连接(Concatenate)

Concatenate(连接)接收一个数组,并本质上将其内容与另一个数组结合。如果第一个值不是数组,编译器会假定这是一个数值操作符。对于数字、字符串、布尔值和正则表达式,它的工作方式基本与追加相同,都会将值添加到数组的末尾。为了实现嵌套数组的条目,你必须提供一个嵌套数组。因此,举几个例子,combined_1 将变成一个数组 [1,2,1]combined_2 会被赋值为 [1,2,1,2],而 combined_3 会得到一个嵌套数组 [1,2,[1]]

$combined_1 = $example_array + 1
$combined_2 = $example_array + [1,2]
$combined_3 = $example_array + [[1,2]]

如果需要连接一个哈希,它会被转换成数组,除非它已经变成只有一个哈希元素的数组。例如,在下面的代码中,转换后的变量将被赋值为一个嵌套数组,元素为 testvalue,并赋值为 [1,2,[test,'value']],而 nested_hash 变量则会添加一个嵌套哈希,赋值为 [1,2,{test => '``value'}]

$converts = $example_array + {test => 'value'}
$nested_hash =$example_array + [{test => 'value'}]

删除(Remove)

删除操作会在从源数组中删除所有匹配元素后分配一个新数组。第一个变量必须是一个数组;否则,它将被视为数值操作符。对于第二个变量,如果是数字、字符串、布尔值或正则表达式,它会检查第一个数组中的每个元素,并在找到匹配时删除它。例如,从 another_example_array 中删除字符串 one 会匹配第一个元素和第三个元素并将其删除,但不会删除嵌套数组中的第一个元素,结果会将 ['two','three','four','three',['one','three','four']] 赋值给 remove_string 变量:

$another_example_array = ['one','two','one','three','four','three',['one','three','four']]
$remove_string = $another_example_array – 'one'

当第二个变量是数组时,它会遍历数组中的每个元素,像直接呈现一样删除它们,就像我们之前的例子。在这个例子中,它将按照之前的例子移除one,然后继续搜索匹配的threefour字符串,移除第四、第五和第六个元素,同时将['two',['one','three','four']]赋值给remove_array变量:

$another_example_array = ['one','two','one','three','four','three',['one','three','four']]
$remove_array = $another_example_array – ['one','three','four']

当嵌套数组作为第二个变量时,它会匹配与相同数组的所有元素并将其移除。所以,在这个例子中,remove_nested_array变量将被赋值为['one','two','one','three','four','three']

$remove_nested_array = $another_example_array – [['one','three','four']]

与连接操作一样,哈希必须放置在数组中;否则,它们将删除翻译后的嵌套数组中的任何匹配元素。

展开符

展开符与其他运算符不同,它们用于将数组作为函数调用的参数,提供以逗号分隔的列表。这在条件语句和选择器语句中都适用。使用数组展开符将在第五章第七章中详细讲解。

数组数据类型

当设置参数的数据类型为数组时,必须使用大写的Array关键字,并指定数组元素的数据类型、数组的最小大小和最大大小:

Array[<Data Type>, <Minimum Size>, <Maximum Size>]

数据类型的默认值是数据,这将在本章的抽象数据类型,包括敏感数据部分中讲解,但这意味着数字(包括整数和浮点数)、字符串、布尔值和正则表达式,以及这些类型的数组和哈希都适用。如果选择更具体的数据类型,例如String,则期望数组中的每个元素都包含一个字符串。在抽象数据类型,包括敏感数据部分中,还将讲解其他混合类型,这些类型提供了更多的灵活性。

最小大小为 0,最大大小为无限。

例如,database类可以接受一个db_uids变量,其中至少期望一个元素,但最多可以包含六个元素。user_names变量可以是一个空数组或最多五个元素,但大多数情况下只包含字符串。最后,extra_flags变量是一个具有默认值的数组,因此它可以是一个空数组,大小可无限,且内容与数据类型匹配:

class 'database': {
  Array[default,1,6] db_uids,
  Array[string,0,5] user_names,
  Array extra_flags,
}

赋值哈希

哈希作为以逗号分隔的键值对书写,键值对之间用=>分隔,列表被大括号{ }包围。最后一对键值后可以加上尾随逗号,但本书不推荐这种样式。例如,以下哈希对可以用来为make键赋值为skoda字符串,model键赋值为rapid字符串,year键赋值为2014整数:

$my_car = { make => 'skoda', model => 'rapid', year => 2014 }

为了格式化风格,通常每个键都会占用一行,以确保键的开始对齐,箭头也能对齐。本书推荐在编写数组时,使用一个新的空行来结束大括号,并将其与起始大括号对齐:

$my_car = { make  => 'skoda',
            model => 'rapid',
            year  => 2014
          }

哈希的键和值可以是任何类型,但键通常应该是字符串类型,其他类型很少有实际意义。就像数组一样,哈希在 Puppet 中是变量,且只能赋值一次,除非重新分配一个新的哈希,否则无法对其进行修改。

注意

Puppet 只能将字符串类型的哈希键序列化到目录中。因此,无法将非字符串键的哈希分配给资源属性或类参数。

访问哈希值

类似于数组,哈希值可以通过方括号和键值来访问。举个例子,下面的代码将打印出rapid值:

notify {"Print ${my_car[model]}":}

嵌套哈希

与数组一样,通过在哈希中声明一个哈希,可以创建一个嵌套哈希,这样就可以通过链式键来访问。以下示例展示了一个包含packagesservices键的变量包列表。packages键包含httpd键,其值为字符串latest,以及cowsay键,其值为浮动值4.0services键包含httpd键,其值为字符串running,以及nginx键,其值为字符串stopped

$package_list = { packages  => { httpd  => 'latest',
                          cowsay => 4.0
                        }
                  services => { httpd => 'running',
                                nginx => 'stopped'
                              }
                 }

为了打印出嵌套的两个httpd键,可以声明一个notify资源,如下所示:

notify {"Print ${package_list[packages][httpd]} ${package_list[services][httpd]}":}

哈希运算符

哈希有两个运算符——合并(+)和移除(-)。合并可以通过将键值对添加到现有的哈希中来分配一个新的哈希,而移除则通过从现有哈希中删除键值对来分配一个新的哈希。

合并

合并通过将一个哈希变量、一个+符号和一个具有偶数个值的哈希或数组来完成。请注意,合并时如果要添加的键已经存在,它将不会被重复添加。在下面的示例中,将一个包含database键和字符串oracleversion键和整数11的哈希与包含web_server键和字符串httpdversion键和12值的app_web哈希合并,最终会得到combined_app变量,包含database键值对和web_server键值对。然而,app_web中的version键将被忽略,因为app_db中已经存在该键:

$app_db    = { database => 'oracle', version = > 11}
$app_web = { web_server => 'httpd', version => 12 }
$combined_app = $app_db + $app_web

移除

删除操作符接受一个哈希变量,一个符号,以及一个哈希、一个键的数组或一个单一的字符串键。如果提供哈希,则哈希中的值不重要,因为删除操作只是删除匹配的键。在以下示例中,可以看到一个software_versions哈希,其中包含oracle键和整数11httpd键和值12,以及cowsay键和值9。当删除一个键以创建no_cowsay变量时,cowsay9的键值对被删除。当only_cowsay被赋值时,要删除的哈希中oraclehttpd的值不重要,它将简单地删除键值对。而对于only_oracle变量,删除一个数组将使删除操作符遍历每个匹配的键并删除匹配项:

$software_versions = { oracle => 11, httpd => 12, cowsay => 9}
$no_cowsay = $software_versions – cowsay
$only_cowsay = $software_versions – { oracle => 'anything' , httpd => 'anything' }
$only_oracle = $software_versions – [httpd,cowsay]

哈希数据类型

哈希数据类型接受可选的键类型和值类型;如果指定了键类型,则必须指定值类型。可以为键对的数量指定最小和最大大小:

Hash[<Key type>, <Value type>, <Minimum size>, <Maximum size>]

例如,以下类具有一个tunables参数,它必须包含一个包含110个键值对(字符串和整数)的哈希:

Class kernel_overrides (
  Hash[String,integer,1,10] tunables
)

混合哈希和数组

由于哈希键值或数组值可以是任何数据类型,因此可以进行嵌套操作。但应注意不要让结构过于复杂。

以下示例显示了包含nfs_share_servers哈希的server_cmdb哈希,其中proddev键包含字符串数组:

$server_cmdb = {
  'nfs_share_servers => {
     prod =>  ['prdnfs01','prdnfs02','prdnfs02']
     dev => [ 'devnfs01','devnfs02,'devnfs03']
  }
}

要访问第一个prod数组的第三个值prdnfs02,可以执行以下调用:

$server_cmdb[nfs_share_servers][prod][2]

实验

为了练习我们所讲解的内容,编写一个类,该类接受一个软件包数组,并使用哈希定义的标准参数来安装提供者和版本的包。记得根据之前的实验声明类中的变量。例如,你可以安装最新版本的 RubyGems,包括 webrick、puma 和 sinatra。建议的解决方案可以在github.com/PacktPublishing/Puppet-8-for-DevOps-Engineers/blob/main/ch04/packages_array_hash_paramters.pp中找到。

抽象数据类型,包括敏感数据

抽象数据类型为你提供了灵活性,可以混合核心数据类型进行参数强制和特定模式的实现,还能在参数检查方面提供一些更高级的功能。抽象数据类型有很多种,所以下面的部分将覆盖最常用的几种。其他类型可以在github.com/puppetlabs/puppet-specifications/blob/master/language/types_values_variables.mdwww.puppet.com/docs/puppet/8/lang_data_abstract.html#variant-data-type找到。

前缀

尽管这不是 Puppet 的术语,我们将回顾的类型将被描述为前缀,其中一个类型在另一个类型前加上前缀,且没有其他选项。

敏感

Sensitive 数据类型用于标记字符串为敏感值,这意味着该值将在代码和目录中以明文显示,但不会出现在任何 Puppet 报告或日志中。通过在参数和赋值前加上 Sensitive 关键字,这些字符串的内容会被标记为敏感。此操作影响字符串类型以及可以包含字符串或可以转换为字符串的资源。在以下示例中,我们展示了一个字符串、一个字符串数组和一个可以分配的数组。输出将打印 [value redacted],表示已标记为敏感的部分:

$secret_string = Sensitive('password')
notify {"Print ${secret_string}":}
$single_sensitive_array = [Sensitive('password'),'password']
notify {"Print ${single_sensitive_array}":}
$secret_array = Sensitive(['password','password'])
notify {"Print ${secret_array}":}

当值需要在代码中使用时,unwrap 函数允许我们查看敏感值。此示例展示了如何解开该值并使用 notify 资源打印:

notify {"Print ${secret_string.unwrap}":}

这仅仅是一个示例,且会违背隐藏日志和报告中值的目的;更可能的是,它会传递给另一个资源。像 password 这样的属性,其用户识别的敏感值不需要解包,但像 exec 这样的资源则不能插值,因此值必须解包。为了避免泄露数据,像 exec 这样的资源不能插值,你可以将其包装为 Sensitive,以确保在日志中不会暴露任何部分。以下示例展示了将敏感字符串传递给 user,并将敏感字符串作为密码传递给 curl 命令:

user { 'max'
  id => 7
  password => $secret_string
}
exec {'secure curl':
  command => Sensitive("C:\\Windows\\System32\\curl.exe -u david:${secret_string.unwrap} http://example.com")
}

如果在使用 debug 运行 Puppet 时仅执行解包操作,命令和密码将完全可见。

第七章 中,我们将讨论模板,包括如何使用敏感值。然而,从 Puppet 7.0 和 6.20 开始,你不再需要在模板中使用敏感值之前将其解包。

注意

完整的端到端数据保护将在 第九章 中讨论。

Enum 和更高级的模式数据类型模式将在下一节中介绍,这些模式与 Sensitive 不兼容,应避免使用。在此,你应仅使用基本类型,如 string

可选

Optional 数据类型允许 undef 作为数据类型的可接受输入,除此之外,还可以使用它所前缀的其他类型:

Optional <type> <variable name>

例如,要允许将 Integer 参数或 undef 分配给 oracle_uid 变量,只需在 Integer 类型前添加 Optional 关键字:

class oracle (
  Optional Integer orace_uid
)

notundef 类型有相反的效果,但用途更为有限且是例外情况。

模式

模式类型允许对属性应用类型组合,例如正则表达式或特定的字符串选择。

枚举

Enum 数据类型允许你列举字符串,使得多个选项可以在 class 参数中使用。以下代码声明了 Enum,后面跟着一个字符串数组,作为选项,最少包含一个或更多的字符串:

Enum[,*]

以下示例展示了如何在名为 regional 的类中使用这个,类的参数 uk_region 接受一个可用的英国区域:

class regional (
  Enum['Scotland,'England','Wales','Northern Ireland'] uk_region
)

变种

Variant 数据类型允许你将其他任何数据类型组合成一个数组。以下代码使用 Variant 关键字,并声明了参数允许的类型列表:

Variant[<type>,*<type>]

例如,下面的类接受 truefalse 的布尔值,或者 truefalse 字符串作为 create_user_home 变量的值。它还将接受一个字符串或一个字符串数组作为 user_names 变量的值:

class user_accounts(
  Variant[Boolean, Enum['true', 'false']] create_user_home
  Variant[String,Array[String]] user_names
)

模式

Pattern 数据类型类似于 Variant,但是它提供了一种方式来提供一组正则表达式,参数可以匹配其中任何一个。其语法如下:

Pattern[<regexcp>*<regexcp>]

在这里,我们使用 Pattern 关键字进行声明,后面跟着一个 regexp 类型的数组。例如,以下定义的类型 server_access,要求主机名是以 ediglaabe 开头的字符串:

Define server_access (
  Pattern[/^edi/,/^gla/,/^abe/] hostname
)

数组和哈希

在这一部分,我们将涵盖各种数组和哈希类型。

元组

在上一节中,我们讨论了数组类型可以声明一个类型来包含其所有内容。Tuple 允许在数组的特定索引位置使用任意数量的类型,并支持可选的最小值和最大值。最小值如果小于已分配的类型数量,则这些类型变为可选,而最大值则允许最后一个类型在最大值大于已声明类型数量时重复。最大值需要声明一个最小值:

Tuple[ <type>, *<type>,  <minimum size>, <maximum size>]

为了提供一个例子,假设有三个变量:user_declarationcalculationfile_downloaduser_declaration 变量需要一个字符串表示用户名,一个整数表示 UID,以及至少一个长度不超过八个字符的字符串,表示用户可以被分配到的组。calculation 变量需要一个整数,一个浮动数和一个整数。file_download 变量需要一个 URI 和一个字符串,另外,整数是可选的,并不是必须的:

class exampleapp (
Tuple [ string, integer, string, 3 , 10 ] user_declaration
Tuple [ integer, float, integer] calculation
Tuple [ uri, string , integer, 2] file_dowload
)

结构体

Struct 提供了一种类似于 Tuple 的类型来处理哈希。在 Hash 数据类型中,单个键类型和值类型被声明,而结构体允许按特定顺序声明字符串键,并且键的值可以选择性地为 optionalundef,同时允许声明值类型。与 Tuple 不同,没有最小或最大大小的限制:

Hash[<*optional *undef String name>, <Value type>, *(<*optional *undef String name >,<value type>)

为了说明可选键和值如何影响变量赋值,让我们考虑三个例子:config_fileapplication_binaryapplication_startupconfig_file 变量需要键值对,包括具有字符串值的 mode 键,其值可以是 filelink,以及具有字符串值的 path 键。application_binary 变量与 config_file 相似,但它允许可选的 owner 键,值为字符串。如果存在,owner 键必须有字符串值。application_startup 变量要求一个 owner 键,可以是未定义值或字符串。此外,每个键的值必须匹配预期的数据类型:

class skeleton (
Struct[{mode => Enum[file, link],
        path => String config_file
Struct[{mode            => Enum[file, link],
        path            => String,
        Optional[owner] => String}] application_binary
Struct[{mode            => Enum[file, link],
        path            => String,
        owner           => Optional[String]}] application_startup
)

父数据类型

以下数据类型允许将多种数据类型组合为单一参数。直接使用它们可以使代码更简洁、更清晰:

  • 任意任意类型匹配任何 Puppet 数据类型,当确切的数据类型未知或不重要时非常有用。

  • 集合集合类型匹配任何数组或哈希数据类型,当数组或哈希可以包含多种数据类型时非常有用。

  • 标量标量数据类型匹配字符串、布尔值、正则表达式和数字。当需要包含这些数据类型的单个值时非常有用。

  • 数据数据类型匹配标量、未定义值、包含匹配数据的数组,以及具有匹配标量的键和匹配数据的值的哈希。当需要复杂数据结构时非常有用。

  • 数字数字类型匹配浮动和整数数据类型,当需要数值时非常有用。

实验室

继续我们的 all_grafana 类的工作,创建一个 all_grafana_data_types 类,并将其添加,使其接受一个 file_options 参数。此参数必须有一个名称,但可以选择性地具有模式、用户和作为哈希的组。确保这些资源的每个数据类型都是受限制的。添加一个 Grafana 用户和一个传递给该用户的敏感参数密码。

要实现类赋值,请在赋值类之前编写类声明。当你在清单上运行 bolt 时,它将包含你的变量。解决方案可以在 github.com/PacktPublishing/Puppet-8-for-DevOps-Engineers/blob/main/ch04/all_grafana_data_types.pp 上找到。

范围

在 Puppet 中,作用域是具有对变量和资源默认设置有限访问权限的代码级别。作用域有三个级别:顶级作用域、节点作用域和本地作用域。顶级作用域变量反映的是全局声明的变量,最常见的是在 site.pp 清单文件中声明。节点作用域变量是在节点定义中分配的,通常也在 site.pp 中声明,或通过 Puppet 环境中的 site.pp 清单文件使其对所有节点全局可用。或者,可以在 site.pp 中的节点定义或 ENC 中声明变量,以便在节点级别为特定服务器或服务器组提供变量。site.pp 是 Puppet 中的一个特殊清单文件,包含 Puppet 环境的主要配置。资源默认值是资源的默认设置,可以在更具体的作用域中覆盖,例如节点作用域或本地作用域。site.pp、ENC 和节点定义的完整使用将在 第十章 中详细解释。

在访问变量时,默认情况下,服务器将首先访问最低级别的变量,并且本质上会覆盖更高级别中同名的变量。其他本地作用域可以通过使用命名空间进行访问,但不能赋值。

这是一个示例,展示这些概念如何在单个 Puppet 清单文件中一起工作。我们可以定义一个名为 'global' 的全局变量,其值为字符串 'world',并定义一个节点定义,该定义默认为所有节点分配一个名为 'node' 的变量,值为字符串 'mynode'。该节点定义包括两个类,'local''also_local'。在 'local' 类中,我们将一个名为 'global' 的变量赋值为字符串 'override',它具有本地作用域并覆盖全局值。我们将使用两个通知资源来演示变量作用域是如何工作的。第一个通知资源打印 'Print override',显示 'global' 本地变量已覆盖全局值。第二个通知资源使用 :: 语法引用全局变量,因此它打印 'Print world'。第三个通知资源打印 'Print node',因为没有具有该名称的本地变量。在 'also_local' 类中,我们定义了一个新变量 'another_global',其值为字符串 'another world'。该类中的第一个 notify 资源使用直接访问的变量打印 'Print another override'。第二个 notify 资源使用 :: 语法引用全局变量并打印 'Print another world',因为没有声明名为 'global' 的本地变量。notify 资源是 Puppet 的一种资源类型,简单地将消息记录到控制台或系统日志中,通常用于调试或信息目的。

$global = 'world'
node default {
  $node = 'mynode'
  include local
  include also_local
}
class local
{
$global = 'override'
  notify {"Print ${global}":}
  notify {"Print ${::global}":}
  notify {"Print ${node}":}
}
class also_local {
  notify {"Print another ${local::global}":}
  notify {"Print another ${global}":}
}

资源标题或对资源的引用不受作用域限制,因为它们必须在整个目录中唯一。如前面示例所示,在 also_local 类中使用的 notify 资源的标题被调整为包含 another。这有助于我们避免在变量插值时出现资源标题冲突。否则,localalso_local 类都会包含名为 Print overridePrint worldnotify 资源,且会因重复资源而无法编译。

如前所述,also_local 类可以从 local 类中调用 global 变量,但不能将其赋值给该本地作用域。

总结

在本章中,我们学习了 Puppet 变量与普通过程语言中的变量不同,因为它们只能被赋值一次。我们看到某些词是保留的,不能用作变量命名。我们还看到,Puppet 变量可以进行插值,这取决于字符串的放置方式和位置。

我们介绍了各种核心数据类型及其如何用于限制参数和赋值变量。我们还探讨了 undef 和布尔值,它们在转换值时需要小心管理,以获得预期的结果。

接下来,我们研究了数组和哈希以及如何为它们赋值。尽管它们不能被更改,但我们了解了运算符如何将它们转化为新的赋值。我们还讲解了数组和哈希如何嵌套以及它们如何混合为数组的哈希或哈希的数组。

接着,我们研究了抽象数据类型及其如何通过 Sensitive 类型更加灵活地限制参数,它为日志和报告提供了作用域保护。

之后,我们回顾了如何在不同的作用域中声明 Puppet 变量,以及如何在不同的作用域中共享/查看变量。

在下一章中,我们将介绍事实和函数。我们将查看系统配置工具 Facter,它收集的信息,以及如何定制以收集用户特定的系统配置数据。函数提供 Ruby 代码插件,允许在编译时运行代码,可以执行数据操作或影响目录的运行。我们将讨论内置函数以及来自标准 lib 模块的函数,这些函数可以用于将数据类型转化为我们在本章中讨论的变量。

第五章:Facts 和 Functions(函数)

本章将讲解 facts(事实)。我们将展示 Facter 工具如何收集它们以显示系统配置文件,如何与 Facter 交互,以及如何在 Puppet 代码中使用它们。我们还将介绍如何将自定义和外部的 facts 添加到提供的核心 facts 中,以便收集更多特定于用户的 facts。

接下来,我们将介绍函数。我们将解释函数的作用以及三种类型的函数——语句函数、前缀函数和链式函数。我们将考察一些核心函数,展示它们的功能。同时,也会展示来自 stdlib 模块的一些函数,并解释该模块的使用方法和方式。

延迟函数(Deferred functions)是在 Puppet 6 中引入的,本节将对此进行讲解。在这里,我们将向您展示延迟函数与普通函数的区别,如何使一个函数成为延迟函数,以及在使用延迟函数时应避免的陷阱。

简而言之,本章将涵盖以下主题:

  • Facts 和 Facter

  • 自定义 facts 和外部 facts

  • 函数

  • stdlib 模块函数

  • 延迟函数

技术要求

本章中,您需要通过下载 github.com/PacktPublishing/Puppet-8-for-DevOps-Engineers/blob/main/ch05/params.json 文件,并使用以下命令从 pecdm 目录中配置一个标准的 Puppet 服务器架构,其中包含一个 Windows 客户端和一个 Linux 客户端:

bolt --verbose plan run pecdm::provision –params @params.json

Facts 和 Facter

Facter 是 Puppet 的系统分析工具,是一组跨平台的 Ruby 库,用于收集关于客户端的信息(称为 facts)。这些工具提供了评估客户端配置文件所需的信息,并允许根据主机在 Puppet 代码中的先前状态做出配置决策。

Puppet 5 和 6 使用 Facter 3,而 Puppet 7 使用 Facter 4。Facter 4 中仅提供了一小部分功能,本文将重点介绍这些功能,并且少量事实(facts)有所变化,但大多数用户将不会发现差异。您可以通过运行 puppet facts diff 命令查看这些差异。在第八章中,我们将重点介绍如何通过模块测试确保代码在不同版本间的兼容性。

可以通过在命令行或 VSCode 终端中运行 facter -ppuppet facts 命令查看 Facter 的输出。运行这些命令而不添加任何额外选项时,将返回所有核心 facts。-p 标志确保收集 Puppet 特定的 facts。由于 Facter 和 Puppet 之间创建了循环依赖,之前计划废弃 -p 标志并用 puppet facts 命令代替,但随着 Facter 4 的发布,这一做法被放弃了。本书的示例将使用 facter 命令,这与文档和社区的实践一致。

注意

默认情况下,facter 命令以 Puppet 哈希格式输出,而 puppet facts 以 JSON 格式输出。这两个命令都接受选择适当格式的选项。

现在我们来看一些 Facter 输出的示例。最简单的事实类型是简单的键值对,例如 Kernel 事实,在本例中,它告诉我们内核是基于 Windows 的:

"Kernel": "windows"

还有一些称为结构化事实的哈希值,它们可以分解成嵌套级别。os 事实是常用的。以下是 Windows 10 笔记本电脑的示例,展示了可用的各种级别:

os => {
  architecture => "x64",
  family => "windows",
  hardware => "x86_64",
  name => "windows",
  release => {
    full => "10",
    major => "10"
  },
  windows => {
    display_version => "21H2",
    edition_id => "Core",
    installation_type => "Client",
    product_name => "Windows 10 Home",
    release_id => "21H2",
    system32 => "C:\WINDOWS\system32"
  }
}

核心事实的完整列表可以在 puppet.com/docs/puppet/latest/core_facts.html 中找到;建议在客户端系统上运行 facter -p 并查看输出。可以通过运行 facter 命令并指定事实名称来访问单个事实,例如运行 facter -p kernel 来返回 kernel 事实。要访问结构化事实中的特定嵌套级别值,使用点符号表示法,点号(.)分隔每个键级名称。因此,要访问 os 结构化事实中的 family 事实,可以运行 facter -p os.family 命令。

由于 Facter 经历了多个版本迭代,并且早期版本没有结构化事实,Facter 3 隐藏了多个遗留事实,如架构,它被放入 os 结构化事实中作为 os.structured--show-legacy 标志可以使这些事实在 Facter 输出中可见;它们在核心事实文档中有记录。

当 Puppet 运行时,无论是通过代理还是在命令行上运行 puppet apply,Facter 都会运行,使用遗留事实,并且输出将分配给全局变量。

然后,这些变量可以通过两种方式在 Puppet 清单中访问——要么直接通过事实的名称作为全局变量,要么通过 facts 数组。强烈建议仅通过 facts 数组访问事实,因为这样可以明确表示正在访问事实,而不是其他潜在的全局变量。

例如,在以下代码中,notify 资源将访问 kernelos family 变量,并打印包含主机 kernelos 系列的日志信息:

notify { "This clients kernel is ${facts[kernel]}": }
notify { "This client is a member of the os family ${facts[os][family]": }

请注意,并非所有事实都会出现在所有客户端上。事实通常会根据某些上下文进行过滤,例如正在运行的操作系统,或者是否使用了特定的底层硬件。

注意

正如你将在下一节中看到的那样,函数使用点号来表示链式函数,因此 facter 命令的点分隔访问语法不能用于直接调用 facts 变量。然而,可以使用 getvar 函数。

Facter 可以通过配置 facter.conf 文件,在每个主机上进行自定义和调整。默认情况下,此文件不会创建,应在 Nix 系统上的 /etc/puppetlabs/facter/facter.conf 和 Windows 上的 C:\ProgramData\PuppetLabs\facter\etc\facter.conf 创建。为了测试,可以使用 -c 标志运行 facter 命令,选择要运行的配置文件。

一个示例 facter.conf 组如下所示:

facts : {
    blocklist : [ "disks", "dmi.product.serial_number", "file system" ],
    ttls : [
        { "processor" : 30 days },
    ]
}
global : {
    external-dir     : [ "/home/david/external1", "/home/david/external2" ],
    custom-dir       : [ "/home/david/customtest" ],
    no-exernal-facts : false,
    no-custom-facts  : false,
    no-ruby          : false
}
cli : {
    debug     : false,
    trace     : true,
    verbose   : false,
    log-level : "warn"
}
fact-groups : {
 custom-exampleapp : ["exampleapp1", "exampleapp2"],
}

第一部分 facts 包括一个阻止列表,它允许我们列出将不会运行的事实和事实组。这在计算事实可能会非常耗费资源的情况下很有用。例如,在前面的示例中,我们阻止了 disksfile system 组,因为在一些传统的 UNIX 系统中,SAN 存储可能会配置有成千上万条路径。它还禁用了 dmi.product.serial_number,这可能被认为是某些安全信息,不应在 Puppet 中显示。要查看所有可阻止组的完整列表,可以运行 facter --list-block-groups 命令,它将列出组名以及其中包含的事实列表。例如,disks 组如下所示:

disks
- blockdevices
- disks

事实部分的另一部分是 ttls,它允许配置缓存。缓存的事实以 JSON 格式存储在 UNIX 系统上的 /opt/puppetlabs/facter/cache/cached_facts 和 Windows 上的 C:\ProgramData\PuppetLabs\facter\cache\cached_facts 中。在前面的示例中,processor 组将每 30 天刷新一次。要查看所有可缓存组的完整列表,可以运行 facter --list-cache-groups 命令,这将显示类似于块组的格式。

global 部分允许传递一个目录数组到 external-dir,以便定义 facter 应该在何处查找外部事实。类似地,可以传递一个目录数组到 custom-dir,定义 facter 应该在何处查找自定义事实。自定义和外部事实将在下一部分中讨论。

global 部分有三个布尔值:

  • no-external-facts:如果设置为 true,则禁用外部事实。

  • no-custom-facts:如果设置为 true,则禁用自定义事实。

  • no-ruby:防止通过 Ruby 加载 Facter。任何使用 Ruby 和自定义事实的事实如果设置为 true,则会被禁用。

所有这些设置更可能用于调试和开发目的。

cli 部分设置日志级别,值为(nonetracedebuginfowarnerrorfatal)的字符串,并有三个选项:verbosetracedebug。这三个选项的启用或禁用通过布尔值 truefalse 来设置。trace 选项将在自定义事实发生异常时显示回溯。这个选项不应与追踪日志级别混淆;这个选项的更合适名称可能是 stacktrace。verbose 选项启用 Facter 的详细信息输出,而 debug 选项启用 Facter 的调试级别输出。

fact-group 部分是 Facter 4 中 Puppet 新增的功能,允许你为缓存和阻止定义自定义组。可以指定核心事实和自定义事实,但不能指定外部事实。

注意

由于 facter.conf 文件使用 HOCON 格式,因此可以通过 Puppet Forge 的 HOCON 模块(forge.puppet.com/modules/puppetlabs/hocon)更轻松地管理它,在此过程中可以根据需要按单个节点或节点组进行分类。

Puppet 7 中的 Facter 4 重新引入了事实基准测试功能,这一功能之前在 Facter 2 中就已存在。要对某个特定的事实进行基准测试,可以运行 facter -t <fact name> 命令。例如,运行 facter -t os 将生成类似于以下的输出:

fact 'os.name', took: (0.000007) seconds
fact 'os.family', took: (0.000006) seconds
fact 'os.hardware', took: (0.000007) seconds

如果选择了结构化事实,它将对事实的每个部分进行计时,并在执行完后将其返回到 facter 调用的正常输出中。

在了解了核心事实是什么以及如何运行和配置 Facter 来测试和管理它们之后,下一步是通过自定义和外部事实添加个性化配置。

自定义事实和外部事实

在本节中,你将学习如何通过自定义事实将其添加到核心事实提供的事实中。这些事实是用 Ruby 编写的,类似于核心事实或外部事实,它们可以是硬编码的值,也可以是客户端本地可执行的脚本。虽然收集所有数据可能很有诱惑力,但应考虑到外部事实对 Puppet 基础架构的额外负担,特别是在有大量代理的情况下,并需要平衡数据需求与系统性能。

外部事实

外部事实是可执行文件,它们可以根据脚本中的逻辑设置事实,或者根据文件的结构化数据静态设置事实。

外部事实可以存储在以下目录中,适用于基于 Unix 的操作系统:

  • /``opt/puppetlabs/facter/facts.d/

  • /``etc/puppetlabs/facter/facts.d/

  • /``etc/facter/facts.d/

对于 Windows 系统,外部事实可以存储在 C:\ProgramData\PuppetLabs\facter\facts.d\ 中。

第八章 中,你将学习如何通过插件同步过程将外部事实从模块分发到客户端,在此过程中,模块中 facts.d 文件夹内的事实会被添加进来。

注意

Puppet 可以在基于 UNIX 的系统上作为非 root 用户运行,而外部事实可以存储在 ~/.facter/facts.d/ 中。然而,本书不会涉及作为非 root 用户运行的相关内容。

静态外部事实

静态外部事实必须采用 JSON、YAML 或 TXT 格式。例如,我们可以将 Application 事实设置为 exampleapp,将 Use 事实设置为 production,将 Owner 事实设置为 exampleorg。在 YAML 文件中,可以像这样创建:

---
Application :  exampleapp
Use : Production
Owner : exampleorg

在 JSON 文件中,可以像这样设置它们:

{ "Application": "exampleapp", "Use": "Production", "Owner": "exampleorg"}

在 TXT 文件中,同样的事实可以像这样设置:

Application=exampleapp
Use=Production
Owner=exampleorg

对于 Windows,这些文件中使用的行结束符必须是 LF(换行符,Unicode 字符 000A)或 CRLF(回车符和换行符,Unicode 字符 000D 和 000A),且文件的编码必须是 ANSI 或 UTF8 且不带 BOM。

到目前为止我们所看过的示例都被称为平面事实。然而,通过创建数组格式,可以返回结构化的事实。例如,在 YAML 中,我们可以通过添加数组和嵌套数组来允许两个所有者。在这个示例中,假设有多个应用程序,每个应用程序可以有联合所有权:

---
Application :
  Exampleapp
Use : production
Owner
- Exampleorg
- anotherorg
  Anotherapp
Use : Production
Owner : exampleorg

这将允许我们调用 facter application.exampleapp.owner 来检索所有者数组,或者调用 facter application.anotherapp 来接收使用者和所有者的键值对。

请注意,静态外部事实在输出中将始终返回字符串类型。

可执行外部事实

可执行外部事实在 Windows 和 UNIX 上有所不同,但它们都是可运行的脚本,输出键值对或数组以返回事实或结构化事实。

在 Windows 上,可以使用以下文件类型:

  • 二进制可执行文件(.com.exe 文件)

  • 批处理脚本(.bat.cmd 文件)

  • PowerShell 脚本(.ps1 文件)

在 UNIX 平台上,任何具有有效 shebang (#!) 声明的可执行文件都可以运行。如果缺少 shebang 声明,脚本执行将失败。

对于两个平台,这些脚本应该返回文本。文本将被读取为键值对,或作为 YAML 或 JSON,可以解析成结构化事实。

例如,一个返回 exampleapp 进程 PID 作为事实的 Unix bash 脚本,同时返回 exampleapp_cpu_useexample_memory_use 的事实,可能如下所示:

#!/bin/bash
echo "exampleapp_pid = ${pidof exampleapp}"
echo "exampleapp_cpu_use = ${ps -C exampleapp} %cpu"
echo "exampleapp_memory_use = ${ps -C exampleapp} %mem"

对于 Windows,一个 PowerShell 脚本返回相同的事实将如下所示:

Write-Output "exampleapp_pid=$((Get-Process explorer).id)"
Write-Output "exampleapp_cpu=$(Get-Process explorer).cpu)"
Write-Output "exampleapp_mem=$(Get-Process explorer).pm)"

注意

要查找外部事实的问题,可以运行 facter --debug。这将显示事实是否对 Facter 可见,以及是否有任何输出未被解析并被忽略。

自定义事实

自定义事实是可以用来设置事实并扩展核心 Facter 事实的 Ruby 代码段。使用自定义事实相较于外部事实的主要优势在于其内置的机制。在本节中,您将了解如何使用自定义事实访问其他事实的值,如何进行多个加权解析,以及如何使用 confine 确保只有特定的节点会尝试运行该事实。

使用自定义事实的主要缺点是它们需要用 Ruby 编写,而 Ruby 存在学习曲线。深入学习 Ruby 的细节超出了本书的范围,但本书将展示其基本结构以及一些在 Windows 和 UNIX 系统上运行良好的核心库,以便您能够为进一步研究打下基础。

与外部事实类似,自定义事实通常通过 Puppet 模块分发。然而,在进行本地测试时,有三种方法可以指示 Facter 查找我们存储本地事实的位置:

  • Ruby 库加载路径

  • 在 Facter 命令中使用 --custom-dir 选项(请注意,此选项可以多次标记)

  • 设置 FACTERLIB 环境变量

Ruby 库加载路径可以通过运行 ruby -e 'puts $LOAD_PATH' 来检查。记得确保所使用的 Ruby 二进制文件是 Puppet 提供的版本,在 Windows 上是 C:\Program Files\Puppet Labs\Puppet\puppet\bin\ruby.exe,在 UNIX 系统上是 /opt/puppetlabs/puppet/bin

自定义事实使用 Facter.add('<fact_name>') 声明自己,并使用 setcode 语句运行代码块来解析事实。这就是事实值确定的方式。作为一个简单的例子,可以通过将命令括起来使用反引号(`)直接运行 UNIX shell 或 Windows 终端命令:

Facter.add('exampleapp_version') do
  setcode do
    `exampleapp –version`
  end
end

由于只有一个命令,这也可以用单个 setcode 行来编写:

Facter.add('exampleapp_version') do
  setcode `exampleapp --version`
end

两者都会将 exampleapp_version 事实设置为 exampleapp --version 命令的输出结果。

如果你的事实更加复杂,需要运行多个命令或处理输出,可以通过 Ruby 类来运行命令。

在以下示例中,Facter::Core::Execution.execute Ruby 类将运行名为 exampleapp 的命令,并带有 version 标志,然后将命令的输出通过管道传递给 awk,以打印第二个返回值:

Facter::Core::Execution.execute('exampleapp –version' | awk '{print $2}' )

可以使用 powershell 命令执行 PowerShell 命令,示例如下:

Facter::Core::Execution.execute('powershell (Get-WindowsCapability -Online -Name "Microsoft.Windows.PowerShell.ISE~~~~0.0.1.0").state')

虽然出于熟悉感,可以将所有操作都当作终端命令来执行,但需要小心,并不是所有终端中可以使用的命令都能正常工作。例如,bash 风格的 if 语句无法使用,应当用 Ruby 代码来编写。

调用另一个事实的值到变量中可能会很有用。以下代码将 os arch 事实的值存入 arch 变量:

arch = Facter.value('os.arch')

限制自定义事实

自定义事实的主要优点之一是可以限制它们将运行的节点。可以通过 confine 语句实现,并选择事实和值来匹配运行的事实。confine 函数的语法如下所示:

confine <fact_name>: '<fact_value>'

confine 函数之后定义的事实,只有在条件满足时才会运行。例如,你可以将事实限制为仅在 Windows 内核节点上运行:

confine kernel: 'Windows'

也可以使用数组,匹配任何一个值都可以让事实运行。例如,我们可以检查内核是否来自 Linux 或 Solaris:

confine kernel: ['Linux', 'Solaris']

对于结构化事实,可以使用 Facter.value 方法来访问。例如,要测试 os.release.major 事实是否等于 10,可以使用以下代码,其中 => 被用来代替冒号(:)来匹配事实的值:

confine Facter.value(:os)['release']['major'] => '10'

除了事实之外,Ruby 命令和库命令也可以用来限制事实。例如,confine 可以与 Facter::Core::Execution.whereFacter::Core::Execution.which 一起使用,分别用于确认 Windows 或 Linux 的路径中是否存在某个命令。此外,Ruby 库如 File 也可以用来检查这一点。

例如,要限制一个事实,仅在 Windows 路径中找到 git 命令时运行,可以运行以下代码:

confine { Facter::Core::Execution.where('git') }

以下代码会限制事实仅在 /opt/app/exampleapp 存在作为文件或目录时运行:

confine { File.exist? '/opt/app/exampleapp' }

要编写一个可以涵盖多种实现并且具有粒度限制的单一事实,我们可以同时使用多个解析(Facter.add 语句)和多个限制块。以下示例展示了一个简单的示例,设置 whoami 的 Facter 值为 I am windows 10,如果内核事实是 Windows 并且 os.release.major10,或者设置为 I am Sparc 字符串,如果内核是 sparc

Facter.add('whoami') do
  setcode do
    confine kernel: 'Windows'
    confine Facter.value(:os)['release']['major'] => '10'
    'I am windows 10'
  end
end
Facter.add('whoami') do
  setcode do
    confine kernel: 'Sparc'
    'I am Sparc'
  end
end

另一种限制事实的方法是使用特性。特性是 Ruby 代码的一部分,添加到模块的 lib/puppet/feature 目录下。例如,exampleapp 模块可以包含一个 exampleapp.rb 特性,用来检查 exampleapp 是否安装在 Windows 或 Linux 上:

require 'puppet/util/feature'
Puppet.features.add(:example_app)
do
windows= `powershell '(Get-Command exampleapp).source'`.strip
linux = `sh -c 'command -v exampleapp`.strip
windows.empty? && linux.empty? ? false : true end

然后,自定义事实可以使用 confine 语句,这样只有在 exampleapp 命令可用的节点上才会运行该事实:

Facter.add('exampleapp) do
setcode do
confine { Puppet.features.example_app? }

这消除了需要创建额外事实并收集和处理不必要的信息(除了评估限制之外)的需求。

注意

setcodeconfine 块中执行所有逻辑代码非常重要;否则,在加载事实时,它会运行这些代码,而不是在查询事实进行解析时运行。这是因为事实加载的顺序是不可预测的,因此如果代码被事实要求但位于块外部,可能会导致顺序错误。

加权解析

写自定义事实的另一种方法是有多个解析,同时知道某些解析可能返回 null 值,但我们希望逐一尝试不同选项。审查解析时,Facter 会排除所有没有被限制的解析。然后,它会查看每个解析的权重。默认情况下,权重为 0,但可以通过 has_weight 函数进行设置。如果两个解析的权重相同,Facter 会使用代码中列出的第一个解析。

例如,要使用多个解析选项设置 exampleapp_version 事实,在第一个解析中,它将以 100 权重运行带有 version 标志的命令,然后尝试以 50 权重在配置文件中查找版本:

Facter.add('exampleapp_version') do
has_weight 100
setcode do
`exampleapp --version`
end
Facter.add('exampleapp_version') do
has_weight 50
setcode do
`grep version /etc/exampleapp/exampleapp.conf | awk '{print $2}'`
end

这允许命令失败,从而可以通过第二个来源进行备份。

注意

外部事实的权重为 1000。因此,为了防止外部事实覆盖自定义事实解析,可以将解析权重设置为高于 1000 的值。

异常处理块

默认情况下,如果任何解析失败并产生错误,Facter 将报告错误并无法返回任何值。使用rescue块可以在失败时返回默认值,并选择打印警告。这与加权解析配合使用,在加权解析中,通常会预期解析失败。

一个简单的rescue块,在运行exampleapp –version命令并记录失败后,返回nil,看起来像这样:

setcode do
`exampleapp --version`
rescue
  nil
  Facter.warn("exampleapp command failed")
end

使用Facter.warn可以确保当通过Facter命令使用时,这条消息会打印到 STDERR。当在 Puppet catalog 应用过程中使用时,它将确保该消息打印到 Puppet 的日志中。返回nil将确保其他解析可以在返回非 nil 值时被使用。

超时

作为 Puppet 7 中的 Facter 4 的一部分,现在可以为解析添加超时。可以通过在事实的名称后添加逗号,作为Facter.add解析语句的一部分,并使用{timeout: <秒数值>}语法来实现,其中秒数值可以是整数或浮动值。例如,要确保exampleapp_version事实的超时时间为 0.2 秒,代码可以像这样设置:

Facter.add('exampleapp_version', {timeout: 0.2}) do

尽管这是 Facter 4 和 Puppet 7 中的一个功能,但在 Facter 3 和 Puppet 5 及更高版本中,仍然可以通过直接在execute函数上设置options变量来对执行命令设置超时。例如,可以通过修改execute命令来将相同的 0.2 秒超时应用于exampleapp –version命令的执行,而不是整个解析:

Facter::Core::Execution.execute('exampleapp --version', options = {:timeout => 0.2})

聚合和结构化事实

聚合事实允许将事实的解析分成多个块。然后,这些块可以合并。合并数组或哈希会创建结构化的事实或执行其他功能,例如将事实的值相加。

聚合事实仍然有一个Facter.add声明,但在Facter.add内,它将类型变量设置为aggregate。然后,代替使用setcode部分,它使用chunk部分来进行解析。默认情况下,每个chunk都会被合并,除非声明了聚合块来执行其他功能。

例如,以下代码将创建一个名为exampleapp的结构化事实。它将包含exampleapp.versionexampleapp.fullpath,其中包含在块中运行的命令的输出:

Facter.add(:exampleapp, :type => :aggregate) do
  Chunk(:version) do
`exampleapp –version`
  end
  Chunk(:fullpath) do
`which exampleapp`
  end
end

要使用聚合块并将事实合并,可以使用以下代码,它创建了一个名为exampleapp_memory_usage的事实,该事实使用一个包含exampleapp使用的总内存的事实,并将其添加到exampleapp2使用的内存中,从而得出总内存使用情况:

Facter.add(: exampleapp_memory_usuage, :type => :aggregate) do
  chunk(:exampleapp1_usage) do
    Facter.value(:exampleapp1_usage)
  end
  chunk(:exampleapp2_usage) do
    Facter.value(:exampleapp2_usage))
  end
  aggregate do |chunks|
    total = 0
    chunks.each_count do |value|
      total += value
    end
    total
  end
end

Puppet 7 与 Facter 4 中提供了一种新的返回结构化事实的方法。这采用了事实名称中的点表示法,允许定义将不同层次的结构化事实进行赋值。例如,要设置带有嵌套层次的exampleapp事实,包括exampleapp.versionexampleapp.pid,可以使用以下代码:

Facter.add('exampleapp.version') do
setcode do
`exampleapp --version`
end
Facter.add('exampleapp.pid') do
setcode do
`pidof exampleapp`
end

这相比使用聚合有一个核心优势。与聚合不同,声明中某一部分的失败只会影响该声明,其他部分将继续赋值。

注意

本节试图为你提供足够的信息,以便你开始使用自定义事实。在 Puppet 的自定义事实和模块代码文档中,你会发现许多我们讨论过的功能的替代语法。由于它是 Ruby 代码,声明的方式有更多变化。本书选择了它认为最好的风格和实践,以保持简洁并避免列出过多选项。

一些可以帮助你进一步跟随示例的模块可以在 GitHub 上找到:

github.com/puppetlabs/puppetlabs-pe_status_check/blob/main/lib/facter/

github.com/puppetlabs/puppetlabs-stdlib/tree/main/lib/facter

github.com/puppetlabs/puppetlabs-lvm/tree/master/lib/facter

github.com/puppetlabs/puppetlabs-java/tree/main/lib/facter

实验

对于这个实验,我们将创建一个静态外部事实和一个自定义事实,并使用bolt upload进行分发,然后运行这些事实并在控制台上查看它们是否已变得可见。

对于静态外部事实,创建一个结构,将packtlab.use设置为lab,并将packlab.student设置为你的名字。

对于自定义事实,将创建一个tmp_count事实,它将计算 Linux 中/tmp目录和 Windows 中C:\Users\admin\AppData\Local\Temp目录中的文件数量。对于 Linux,第一个权重较高的解析应为'find /tmp -type f | wc -l',而第二个权重较低的解析应为ls /tmp | wc -l。对于 Windows,第一个权重较高的解析应为(ls $env:Temp | Measure-Object -line).Lines PowerShell 命令,权重较低的解析应为(Get-ChildItem $env:Temp | Measure-Object).Count

所有解析应在错误结果中返回undef,并且在 10 秒后超时。

请注意,在 Web 控制台查看客户端当前的事实可能很有用,这样你就能知道如何限制它们。

对于每个事实,使用以下bolt命令将其上传到正确的位置:

bolt file upload path_of_your_fact /path/to/destination --targets windows_server_fqdn linux_sever_fqdn
bolt task run facts --targets windows_server_fqdn linux_sever_fqdn

前往 Web 控制台并查看节点中的事实,以确认它们是否已出现在客户端。

你可以在 github.com/PacktPublishing/Puppet-8-for-DevOps-Engineers/blob/main/ch05/tmp_count.rbgithub.com/PacktPublishing/Puppet-8-for-DevOps-Engineers/blob/main/ch05/packlab.yaml 找到示例解决方案。

注意

在测试自定义或外部事实时,可以通过设置环境变量手动设置它们,在 UNIX 环境中使用 export FACTER_exampleapp ="test",或者在 Windows 环境中使用 env FACTER_exampleapp="test" —— 这样会强制设置 exampleapp 事实。此方法仅适用于自定义或外部事实,而不适用于核心事实。

函数

函数是可以在目录编译过程中运行的 Ruby 代码段,允许你修改目录或计算并返回值。Puppet 提供了许多内建函数,更多的函数可以通过 Puppet Forge 模块提供,如 forge.puppet.com/modules/puppetlabs/stdlib,或者通过自定义编写的函数添加到模块中。本书不会涵盖编写函数的内容,但可以在 puppet.com/docs/puppet/latest/writing_custom_functions.html 找到完整的指南。

本节将介绍三种不同类型的函数:语句函数、前缀函数和链式函数。我们将展示一组核心的 Puppet 函数,并按目的分组,展示最常用和最有用的函数。

注意

许多函数已从如 stdlib 模块等源移入核心 Puppet 函数。完整列表可在 puppet.com/docs/puppet/6/release_notes_puppet.html#release_notes_puppet_x-0-0 中查看。

语句函数

语句函数是 Puppet 语言提供的函数,仅用于它们的副作用,这些函数总是返回 undef 值。语句函数可以省略括号,不像我们将在本节中介绍的其他函数那样。你不能添加自定义的或由 Forge 提供的语句函数。

目录语句

目录语句会影响目录的内容,允许类被包含,依赖关系和包含关系影响目录的顺序,并且可以应用标签。以下是目录语句的示例语法:

Include <class name>
require <class name>
contain <class name>
tag <tag name> , *<tag name>

Includetag 的使用已在 第三章 中讨论,但我们没有详细探讨 tag 函数。tag 函数用于在类中标记该类,并将标签或标签列表应用于所有包含的对象。

第六章 中,我们将详细介绍 requirecontain 的完整使用。

日志语句

日志语句允许将字符串消息发送到 Puppet 服务器的日志输出。在 第十章 中,将全面回顾服务器和代理的日志记录,因为日志位置取决于配置以及使用的是 Puppet 企业版还是开源版。日志语句的语法就是 <logging level>()

可以应用以下日志级别:

  • debug

  • info

  • notice

  • warning

  • err

  • fail

要记录 'code unexpected' 的警告信息,Puppet 代码如下:

warning('code unexpected')

字符串消息可以包含变量,如果它们被双引号包围以进行插值。因此,为了在 pa-risc 架构系统上产生 'pa-risc is unsupported' 错误消息,可以在错误函数的字符串中使用 Facter os.arch 的事实:

error("${facts['os']['arch']} is unsupported")

这与本书之前使用的示例不同,特别是前一章示例中使用的 notify 资源。notify 资源会返回到客户端的日志,而日志级别函数则会记录到 Puppet 服务器上。由于 notify 是资源而不是函数,每次调用 notify 资源时,报告都会显示该资源发生了变化。

fail 与其他级别不同,因为将其作为函数调用时会终止编译,并且不会将目录发送到代理。

前缀和链式函数

Puppet 函数可以通过两种方式调用,对于许多函数,两种方式都可以适用。

前缀函数通过编写函数名称然后提供一个括号中的参数列表来调用:

function_name(argument, *argument)

链式函数是通过一个参数、一个句点 (.),然后是函数名称和括号,以及括号内的任何其他参数来创建的:

argument.function_name(argument, *argument)

一些内置函数的选择

核心 Puppet 中有许多可用的函数,本节将对不同函数进行分组,展示它们如何使用,或指明本书中的哪些地方会更详细地介绍它们。本章的目的是展示函数的多样性,而不是给出每个函数的完整语法。你可以在 puppet.com/docs/puppet/latest/function.html 查阅完整的函数列表。确保选择适合你正在使用的 Puppet 环境的文档版本。

比较和大小测量

以下函数允许你比较和测量变量的大小。它们提供了比数据类型直接操作更多的功能。

lengthsize 函数实际上是相同的,都可以作为前缀或链式函数应用于数组(元素数量)、哈希(键值对数量)、字符串(字符数)或二进制数据(字节数),以确认变量的相对大小/长度。例如,以下命令将返回 4 作为字符串 "four" 的长度,返回 5 作为数组的大小:

Stringwithfour = 'four'.length()
Array_of_five = Size([8,4,5,7,0])

match 用作字符串或字符串数组上的链式函数,结合正则表达式匹配模式。它返回一个数组,包含第一个匹配的字符串,后跟匹配的模式。如果没有匹配模式,则会返回一个示例,其中字符串必须以小写字母开头,长度为 68 的数字。变量匹配 a123456 并返回一个包含 [ 'a123456', 'a' , '123456' ] 的数组:

$matches = "a123456".match(/([a-z]{1})([1-9]{6,8})/)

如果我们在一个不匹配的字符串 1a23456 上尝试相同的正则表达式,则会返回 undef

$nomatch = "1a23456".match(/([a-z]{1})([1-9]{6,8})/)# $matches contains [abc123]

使用字符串数组('a123456''b1254678''1a23456')和相同的正则表达式,会导致 multi_match 变量包含一个数组的数组。如果对每个字符串单独使用 match,则输出将是:

$multi_match = ['a123456','b1254678','1a23456'].match(/([a-z]{1})([1-9]{6,8})/)

这意味着 multi_match 将包含 [['a123456','a','123456'],['b1254678','b','1254678'],undef]

maxmin 用作前缀函数。它们接受一个字符串或数字的数组,并返回每种情况中的最大值和最小值。在 Puppet 6.0 之前,关于如何转换和处理这些函数中使用的混合类型有一些指导意见。然而,由于它已经被弃用,现在强烈建议确保比较的类型一致。在以下示例中,highest number 变量将包含 88,而 lowest letter 变量将包含 'a'

$highest_number = max( [5,3,88,46] )
$lowest_letter = ['d','b','a'].min()

empty 用作前缀或链式函数,用来确认一个数组或哈希是否不包含元素,或者一个字符串或二进制是否为空。在以下示例中,empty_arrayempty 字符串将包含 true,而 non_empty_string 变量将包含 false

$empty_array = [].empty
$empty_string =empty('')
$nonempty_string='not_empty'.empty()

compare 用作前缀函数,用于比较两个值,并返回 -101,分别表示第一个值小于、等于或大于第二个值。两个值必须为相同类型,可以是数字、字符串、时间段、时间戳或语义版本。对于两个字符串,可以使用第三个参数(布尔值)来检查比较是否忽略大小写。

例如,numeric_compare 变量将包含 -1,而 string_compare 变量将包含 1,因为大写字母大于小写字母,A 会排在 b 前面。如果布尔值设置为 true,则返回 1

$numeric_compare = compare(5 , 6)
$string_compare = compare('A', 'b', false)

改变大小写

以下函数用于改变字符串或字符串的数组/哈希的大小写。对于整数,它们保持不变,并可能包含其他无法计算的数据类型错误。

capitalizecamelCasedowncaseupcase 都用作前缀或链式函数来改变字符串或可迭代对象(如数组)中字符串的大小写。downcaseupcase 也可以在数组上使用。它们都可以用于数字类型,但会返回未受影响的数字。

CamelCase 会去除应用时使用的所有下划线 (_)。camelCasecapitalize 对数组不是递归的,而 upcasedowncase 是递归的。

如果 downcaseupcase 在递归使用时更改了数组中的键,并且因此产生了重复项,它将覆盖该键,使用最后更新的键值对替代。为了举例说明,upper_case 变量在将整个字符串转换为大写后将包含一个名为 UPANDDOWN 的字符串,而 downcase 变量在将键都转换为小写并覆盖第一个时,将包含一个哈希值 {'lower' => 'case2'}

$upper_case = 'UpAnDdOwN'.upcase()

capitals 变量在将数组中的每个字符串首字母大写后,将包含一个名为 ['Up, Mix'] 的数组:

$capitals =capitalize(['down','miX'])

downcase 变量在将键值都转换为小写并覆盖第一个值后,将包含一个哈希值 {'lower' => 'case2'}

$downcase = {'lower' = > 'case', 'Lower => 'Case2}.downcase()

camel 变量在去除下划线并将大写方式设置为 camelCase 后,将包含 Word1Word2Word3

$camel = camelCase('word1_word2_word3')

如果你使用国际字符,你需要检查 Ruby 系统的区域设置是如何处理这些字符的,因为它用于处理大小写转换。

字符串操作

lstriprstripstrip 函数可以移除字符串中的空格。它们都是前缀或链式函数,用于从字符串中移除空格。lstrip 移除前导空格,rstrip 移除尾随空格,strip 同时移除前导和尾随空格(如空格、制表符、换行符和回车符),但不包括硬空格。它们可以在字符串或可迭代对象上使用,但不能递归使用。如果用于数字类型,它们将返回未经调整的数字类型,但在任何其他不支持的类型上会产生错误。

以下示例使用了所有三个函数,最终将使 left 变量包含 'first second'right 变量包含 'first second',并且 all 变量包含 'firstsecond'

$spaces = " first second "
$left = $spaces.lstrip()
$right = rstrip($spaces)
$all = $spaces.strip()

闭包

这些函数本身不是闭包,但在与闭包一起使用时最为有用,因为它们允许对数组或哈希变量进行迭代或转换,并传递给闭包,闭包是 Puppet 代码的一个部分。以下函数用于定义变量的行为:allanybreakeachfilterindexlestmapnextreturnreducereverse_eachstepthentree_eachuniquewith

这些函数的语法和行为将在第六章中详细讲解,但为了举个例子,这里我们使用了 each 函数和一个包含用户名键及其对应用户 ID 数字的哈希。each 函数可以将每对键值作为一个数组,并允许为用户资源创建已分配的 ID:

$usersids = {'admin' => 1, 'operator' => 2, 'viewer' => 3}
$userids.each |$users| {
  user { $users[0]:
    id  => $users[1]
  }
}

注意

许多函数可以使用 lambda 进行错误处理,这使得您可以循环处理错误部分、消息和问题代码,并允许采取更详细的消息或动作。这将在第六章中讨论。

模板

模板允许您通过简单的输入替换创建复杂的文本。在第六章中,我们将详细讨论模板,但templateepp函数允许通过file资源的content属性使用 ERB 和 EPP 格式的模板。使用 ERB 格式并通知content属性的示例可以在exampleapp模块中找到:

file { '/etc/exampleapp.conf':
  ensure  => file,
  content => template(exampleapp/exampleapp.conf.erb')
}

模块的结构以及如何存储模板文件将在第八章中介绍。

或者,为了使用包含模板格式的字符串并传递值,可以使用inline_templateepp_inline。例如,要使用 EPP 样式模板,假设$exampleapp_conf_template包含 EPP 模板格式的字符串,inline_epp将替换端口和调试变量值exampleapp_portexampleapp_debugging_enabled

file { '/etc/ntp.conf':
  ensure  => file,
  content => inline_epp($exampleapp_conf_template, {'port' => $exampleapp_port, 'debugging' => $exampleapp_debugging_enabled}),
}

哈希/数组

以下函数用于访问和操作哈希和数组数据,超出了第四章中讨论的常规操作符,或者用于将变量转换为哈希和数组。

dig函数用于通过提供各种键或索引在复杂数据结构中进行查找。当结构不明确时,它特别有用。例如,假设我们有一个名为exampleapp_proc的数据结构,并且我们想访问 ID 为124的进程状态。如果我们尝试使用哈希索引如exampleapp_proc['exampleapp_pids']['124']['state']来访问,但哈希中没有124这个键,我们将收到错误,目录运行将失败。然而,通过使用dig函数,通知将为未定义:

$exampleapp_proc = { exmpleapp_pids => { 123 => { state => running , user => root } }
notice exampleapp_proc.dig('exampleapp_pids','124','state')

getvar函数用于使用点符号返回结构化变量的部分。如果变量不存在,它将返回undef,而不是抛出错误,这与直接访问结构化变量不同。如果没有找到值,您还可以设置默认值;否则,它将返回undef

第一个命令使用getvar访问os.release.full事实,而第二个命令在未找到结构化事实时将返回'not_found'

getvar('facts.os.release.full')
getvar('facts.os.release.full','not_found')

join函数用于将一个数组转换为使用指定分隔符的元素字符串。例如,如果你有一个数据中心位置数组dc_locations = ['london', 'falkirk', 'portland', 'belfast'],你可以使用join函数打印一个以冒号分隔的字符串,表示这些位置;例如,notice(join(${dc_locations}, ":"))。这将在通知中生成字符串"london:falkirk:portland:belfast"

dc_locations = [ 'london','falkirk','portland','belfast']
notice ( join(${dc_locations}, ":")

然而,如果你对包含嵌套数组的数组使用join,它将展平数组,但不会影响哈希或哈希中的数组。例如,join([{London => ['bromley', 'brentford']}, 'Berlin', 'Falkirk', 'Grangemouth'], '@@')将打印[ { London => [ 'bromley', 'brentford' ] }@@Berlin@@Falkirk@@Grangemouth ],因为数组的第一个元素是哈希,它不会被展平,尽管它包含了哈希:

dr_locations = [ { London = > [ 'bromley','brentford']},Berlin,['Falkirk','Grangemouth']]
notice ( join(${dr_locations}, "@@")

keysvalues函数接受一个哈希并返回哈希中键的数组,可以作为前缀或链式函数运行。例如,要打印offices变量的键列表,前两个notice函数将打印数组['Germany','Holland'],而接下来的两个将打印数组['Berlin',Amsterdam']

$offices = {'Germany' => 'Berlin', 'Holland' => 'Amsterdam'}
notice(keys(${offices})
notice($offices.keys())
notice(values(${offices})
notice($offices.values())

这些键或值将与它们在哈希中声明时的顺序相同。如果哈希为空,则返回一个空数组。

split函数接受一个字符串,并使用一个模式来表示字段分隔符,可以将字符串拆分为一个数组元素。这个模式可以是字符串、正则表达式或正则表达式。以下示例展示了如何使用不同的模式方法进行拆分,并选择不同的分隔符或多个分隔符:

$exmple_split = north@south.east@west
$split_on_at = split($example_split, /@/)
$split_on_fullstop = split($example_split, '[.]'
$split_on_both = split($example_split, Regexp['[.@]')

split_on_at变量将包含数组['north','south.east','west']split_on_fullstop将包含数组['north@south ','east@west'],而split_on_both将包含数组['north','south','east','west']

sort函数接受一个数组,并按数字顺序或字典顺序对数组进行排序。无法混合这些排序方式,也无法同时进行数字和字典值的排序,否则会导致错误且没有转换。字符比较基于系统语言环境,并且区分大小写,除非使用compare和匿名函数。

在最简单的形式下,sort将按升序对数字和字符串进行排序——例如,我们可以拿一个无序的数字数组和一个无序的字符串数组,并使用sort作为前缀或链式函数。在这个示例中,代码将得到按升序排列的数字[0,1,2,3,4,5,7,8,9]和按升序排列的字符串['a','b','c','d']

$unordered_numbers = [7,9,8,0,2,4,3,1,5]
$unordered_strings = ['d','c','b','a']
$ordered_numbers = $unordered_numbers.sort()
$ordered_strings = sort($unordered_strings)

为了明确指定顺序,你可以使用compare函数对变量进行排序,强调它们应该是升序还是降序。在以下示例中,整数将在升序变量中按[1950,1980,1984,1985]排序,而在降序变量中按[1985,1984,1980,1950]排序:

$ascending =(sort([1984,1950,1985,1980]) |$a,$b| { compare($a, $b) })
$descending = (sort([1984,1950,1985,1980]) |$a,$b| { compare($b, $a) })

正如我们在讨论 比较和大小 部分的 compare 时学到的,布尔值可以用于 compare,以确定是否按大写字母排序。

注意

可以使用 比较和大小 部分中的其他函数,如 maxmin,来替代使用 compare 函数。

数据处理

针对 Hiera 和加密的 EYAML,提供了几个与数据相关的函数。它们将在第九章中详细讨论,但作为参考,它们是 eyaml_look_up_keylookupyaml_data。函数文档指出,几个 hiera_<type> 函数已被废弃,取而代之的是 lookup 函数。

unwrap 函数已在第三章中讨论过,该函数用于在必要时使敏感数据类型在 Puppet 代码中可见/可访问。

stdlib 模块函数

模块将在第八章中详细讨论,但 stdlib 模块(forge.puppet.com/modules/puppetlabs/stdlib)被广泛使用,值得突出一些该模块提供的函数,因为几乎每个 Puppet 安装都会使其可用。

需要注意的是,stdlib 中的函数允许一些高级行为,这些行为并不总是 Puppet 代码的最佳实践方式,例如能够将 YAML 文件的内容读取为字符串,并使用 ensure_package 函数,后者用于允许对包资源进行多次声明。在复杂的情况下或代码在多个团队的政治环境中进行管理时,它们可以提供有用的变通方法。

注意

许多函数已被文件类型转换所替代,该功能自 Puppet 5 版本开始提供,此外还有其他新特性,但这些函数为了兼容性目的被保留。

数组和字符串

以下函数通过合并、操作和以多种方式生成新数组来与字符串和数组进行交互。

intersection 函数是一个链式函数,当提供两个数组时,会生成一个包含两个数组中都存在的值的单一数组。例如,以下代码将 ['both'] 数组放入 chained_array 变量中:

$chained_array = intersection(['first','both']['second','both])

union 函数是一个链式函数,当提供两个数组时,会生成一个包含唯一值的单一数组。在以下示例中,union_array 变量将包含 ['first','second'] 数组:

$union_array = union(['first','both'],['second','both']

range 函数是一个链式函数,提供起始、结束或步长区间(如果没有提供,默认步长为 1)。起始和结束可以是字符串或数字,而可选的步长应该是整数。

例如,onetoten 变量将包含一个数组 [1,2,3,4,5,6,7,8,9,10]etog 变量将包含 ['E','F','G'],而 good_trek 变量将包含 ['StarTrek2','startrek4','startrek6','starttrek8']

$onetoten = range(1,10)
$etog = range('E','G')
$good_trek = ('StarTrek2', 'StarTrek8', 2)

start_withend_with函数是链式函数,允许你检查一个字符串是否以提供的字符串或字符串列表开始或结束,尝试匹配列表中的任意字符串。它将根据匹配情况返回truefalse。在以下示例中,truestart将包含true,因为server匹配了server1234的开头,falseend将包含false,因为wales并没有以land结尾,而trueoptions将包含true,因为aws104aws开头,并且可能匹配以gcpaz开头的其他字符串:

$truestart = 'server1234'.startswith('server')
$falseend = 'wales'.endswith('land')
$trueoptions = 'aws104'.startswith['gcp','az','aws']

文件信息

basenamedirnameextname函数可以作为单独的函数使用,也可以链式使用,从文件路径中提取文件名、目录或扩展名。以下是一个示例:

$full_path = 'C:\Users\david\fact.ps1'
$file_name = basename(${full_path})
$dir_name = dirname(${full_path})
$ext =  ${full_path}.extname

请注意,extname仅适用于格式为filename.extension的文件名。如果字符串不包含点(.),或者点出现在字符串的开头或结尾,它将仅返回空字符串。

实验

在涵盖了各种功能之后,让我们练习使用其中的一些。我们创建一个名为example_functions的类,它接受一个作为字符串的用户前缀和若干个作为整数的用户数量。此类应接受两个参数:一个用户前缀字符串和若干个用户整数。确保前缀是小写的。应从0开始创建一个用户名数组,直到指定的用户数量。然后将该数组传递给用户资源来创建用户。

使用user字符串和数字5来定义你的类。

代码还应记录一条警告消息,其中包含os.windows.product_name事实的内容,或者如果你不使用 Windows 机器,则为linux

最后,代码应采用fact路径,并确保每个目录都经过审计。提示:你可能需要将此路径拆分为一个数组,并将其传递给文件资源。windowslinux使用不同的路径分隔符——即:。以下的if语句应该能帮助你:

if $facts['os.family'] == 'windows' {
}else{
}

你应使用bolt使stdlib在我们的客户端上可用:

bolt command run "puppet module install stdlib" -t windowsclient linuxclient

然后,通过以下命令使用bolt应用puppet类:

bolt puppet apply example_functions.pp -t windowsclient linuxclient

你可以在github.com/PacktPublishing/Puppet-8-for-DevOps-Engineers/blob/main/ch05/example_functions.pp找到一些示例解决方案。

延迟函数

Deferred函数(也称为代理端函数)是应用了Deferred类型的函数。这会导致该函数在应用目录时在客户端本地运行,而不是在 Puppet 服务器编译时运行。一个延迟函数的目录包含要在客户端运行的内容,而不是函数的输出。Deferred类型是在 Puppet 6.0 中引入的,并且在所有后续版本中可用。

当编译服务器无法访问函数中所需的源时,通常会使用此方法——例如,当从 HashiCorp Vault 服务器检索秘密时,安全设置只允许客户端访问秘密。

应用Deferred的语法如下:

Deferred( name of function, [arguments])

以下是从vault检索秘密的示例。这可以在exampleapp的用户资源中使用,从 Vault 路径exampleapp/password设置密码:

user { 'exampleapp':
password => Deferred('vault_lookup::lookup', ["exampleapp/password"])
}

该函数来自vault_lookup模块(forge.puppet.com/modules/puppet/vault_lookup),并需要根据模块中的说明以及 Hashicorp 的指南:www.hashicorp.com/resources/agent-side-lookups-with-hashicorp-vault-puppet-6)设置底层的 Vault 客户端。

理解使用带有Deferred的函数之间的差异很重要。你不能使用Deferred函数将变量传递给字符串。这样会导致目录创建该对象的字符串化版本。在以下示例中,涉及从vault查找名为exampleapp/message的键值,第一个notify将返回一个字符串,其中包含目录中函数名称的字符串化翻译,而第二个notify将返回vault lookup本身的值:

$deferred =>  Deferred('vault_lookup::lookup', ["exampleapp/message"])
notify {'this will return the object name':
  message => "Secret message is ${deferred"
}
notify {'this will return the message':
message => $deferred
}

这反映了在编译时计算字符串值的目录编译过程。这种不匹配可能会出现在其他地方,例如模板中,但可以通过确保任何延迟的值仅在单独使用或在其他延迟函数中使用来克服。在第七章中,你将学习如何使用延迟模板。

函数只能在使用核心数据类型时延迟,因为客户端在运行时通过插件同步仅提供核心数据类型。在第十章中,你将学习插件同步如何与客户端协作。

同时需要注意,是否返回敏感值以及如何失败,取决于函数本身的实现。对于vault_lookup函数,无法优雅地失败;它将返回一个错误,导致目录运行出错。

注意

从 Puppet 7.17.0 开始,延迟函数现在可以按需调用,而不是预处理。使用此方法,目录可以为延迟函数提供输入。如果延迟函数失败,则只有受影响的资源会失败,而所有其他资源仍然会被应用。要启用此行为,设置Puppet[:preprocess_deferred] = false或使用--no-preprocess_deferred

所有这些行为适用于本地的puppet apply run,因为puppet apply run将生成目录并在本地应用。

摘要

在本章中,你学习了 Facter 工具如何通过其事实提供系统配置文件信息,以及如何使用外部事实和自定义事实来扩展这些信息。我们提醒你,收集事实会产生基础设施成本,且应平衡其适用的规模。我们指出,外部事实可以是操作系统允许的简单静态数据的平面文件或可执行脚本。尽管自定义事实是用 Ruby 编写的,但它们相比外部事实具有若干优势。能够将自定义事实仅限于在某些系统上运行,可以让你选择不同的分辨率,并根据权重选择应被选中的分辨率,以及在 Puppet 7 中的分辨率级别或 Puppet 6 及以下版本中的执行级别设置超时。

接下来,我们回顾了函数,并强调了函数在操控目录或返回计算值方面可以完成的广泛任务。在这里,我们讨论了目录语句,它们用于在目录中包含类,以及日志记录语句,它们用于设置日志信息。我们还突出了另外两种类型的函数,即前缀函数和链式函数,并展示了它们的语法。然后,我们展示了一个核心函数的选择,并介绍了暴露可用函数的各种类别。

然后,我们讨论了stdlib模块中的一小部分函数,重点介绍了可以提供的内容。请注意,一些stdlib函数已经被弃用,仅用于向后兼容或处理极限情况,这并不是最佳实践。

最后,我们讨论了延迟函数,这些函数允许在客户端应用目录时运行。我们强调了这对某些只对客户端可用的服务(例如对安全服务进行 API 调用)或可能不希望在与其他服务共享的 Puppet 基础设施上运行的服务的优势。

在下一章中,你将学习资源和类之间的关系和依赖关系如何工作。我们将查看作用域和包含性如何影响资源、变量和类,以及如何构建代码和必要的依赖关系,而不陷入常见的陷阱和依赖地狱。

第二部分——在 Puppet 语言中结构化、排序和管理数据

本部分将介绍更高级的 Puppet 语言特性。我们将展示如何使用迭代和条件来管理代码中的依赖关系和流程。接着,我们将看到如何使用最佳实践将 Puppet 结构化为模块,采用角色和配置模式。Puppet Forge 将展示为一个有用的预构建模块源,我们将查看如何理解和审查这些模块的源代码和内容。接着,我们将查看如何使用 Hiera 管理 Puppet 中的数据,并了解何时使用独立的数据源和变量的最佳实践。

本部分包括以下章节:

  • 第六章关系、排序与作用域

  • 第七章模板化、迭代**和条件语句

  • 第八章开发与管理模块

  • 第九章使用 Puppet 处理数据

第六章:关系、排序和范围

在本章中,我们将讨论 Puppet 中的关系、顺序和范围。这些话题常被认为很复杂,因为 Puppet 的处理方式与传统编程语言大不相同。不过,我们将向你展示如何有效地管理这些方面,避免不必要的复杂性。

我们将首先讨论 Puppet 对关系和排序的处理方式。默认情况下,Puppet 将资源视为独立的,可以按任意顺序在目录中应用。然而,当排序是必需时,我们将向你展示如何使用 beforeafternotifysubscribe 等元参数来强制执行排序并在资源之间创建关系。

接下来,我们将介绍封装的概念。我们将解释,包含类并不包含在其调用类内,因此,类之间建立的关系/依赖关系不会自动与这些类中的资源建立关系和依赖关系。为了解决这个问题,我们将介绍 contain 函数,它允许你将资源包含在类内并创建这些关系。

最后,我们将讨论作用域,以及变量和资源默认值如何根据它们在代码中的位置及其相对作用域具有不同的可见性。然后,我们将提供最佳实践和常见陷阱,以确保你采取最简单的路径并避免不必要的复杂性。

总的来说,本章将帮助你掌握在 Puppet 中有效管理关系、排序和范围的知识与技能。

在本章中,我们将介绍以下主要内容:

  • 关系和排序

  • 封装

  • 范围

  • 最佳实践与常见陷阱

技术要求

本章中的所有示例和实验可以在你自己的本地开发环境中运行。

关系和排序

默认情况下,Puppet 将所有资源视为相互独立的,这意味着它们可以按任意顺序应用。这与传统的声明式代码不同,传统代码按行执行并按照书写顺序执行。Puppet 方法的主要优势之一是,如果代码的某个部分失败,Puppet 会继续应用所有其他资源。这消除了停止或需要大规模故障处理来继续执行代码的需求。因此,即使某些资源失败,Puppet 也能将客户端服务器尽可能接近所需状态。

很明显,一些资源之间会相互依赖,例如一个配置文件只能在安装完某个包后才能存在。Puppet 提供了元参数来创建这些依赖关系:

  • before: 该资源应在指定的资源之前应用。

  • require: 该资源应在指定的资源之后应用。

  • notify: 该资源应在指定的资源之前应用。如果该资源发生变化,指定的资源将被刷新。

  • subscribe:资源应在指定的资源之后应用。如果指定的资源发生变化,资源将刷新。

beforerequire元参数可以用来强制执行依赖关系。然而,重要的是要注意,依赖关系只需在一个方向上应用。因此,不需要在依赖关系的两侧都使用beforerequire

例如,要表示在管理文件之前应安装httpd包,可以使用beforerequire,如下面所示:

package { 'httpd':
  ensure => latest,
  before => File['/etc/httpd.conf'],
}
file { '/etc/httpd.conf':
  ensure => file,
  require => Package['httpd'],
}

依赖关系图,也称为puppet命令中的--graph选项,用于生成一个 dot 文件,该文件可以用来在适当的程序中创建图形。

图 6.1中,文件资源上的require已经被移除,从而为示例代码生成了一个 DAG:

图 6.1 – 资源依赖的 DAG

图 6.1 – 资源依赖的 DAG

如果同时存在beforerequire元参数,DAG 中将会看到一个额外的箭头,但它不会影响编译或应用资源。值得注意的是,示例代码中的起始和结束类Main反映了代码不包含在类中,而是处于全局范围内。这将在作用域部分进一步讨论。

在 DAG 中,通常不期望出现循环,因此依赖关系的流动应该只向下进行。如果添加了第三个资源(例如服务),并且该资源应在/etc/httpd.conf文件之后强制执行httpd包,那么 DAG 应该是这样的:

service { 'httpd':
  ensure  => running,
  before  => Package['httpd'],
  require => File['/etc/httpd.conf'],
}

这将导致依赖循环,如图 6.2所示。编译时,代码将产生错误,因为无法确定资源应用的顺序。

图 6.2 – 显示依赖循环的 DAG

图 6.2 – 显示依赖循环的 DAG

也可以使用数组表示多个依赖关系,数组可以包含相同类型或不同类型的名称。例如,如果一个包被exampleapp的两个文件和两个服务所需要,可以这样表示:

package { 'exampleapp':
  ensure => latest,
  before => [File['/opt/exampleapp.content','/var/exampleapp.variables],Service['exampleapp','exampleapp2']]
}

有时候,将所有资源依赖集中在一边比分别在每个资源上处理要更简单。

如在第三章中提到的,某些 Puppet 类型具有自动创建依赖关系的规则,这些规则可以在 Puppet 类型的文档中找到,位于Autorequires部分,可以在线查看或使用 Puppet 的describe命令。例如,用户类型会自动要求 Puppet 控制下的任何组,作为用户资源的主组或副组。

除了排序概念,Puppet 还有refresh属性,因此如果一个资源依赖于另一个资源,它将刷新自身。这在配置文件更新并且服务需要重启以重新读取配置文件的情况下非常有用。

notifysubscribe元参数创建与beforerequire相同的依赖关系,但将refresh属性添加到依赖资源。对于内置的 Puppet 类型,service execpackage可以被刷新。如果使用了notifysubscribe元参数与无法刷新资源类型,它只会强制执行依赖关系,并在刷新事件时不执行任何操作。

注解

notify元参数不应与notify资源类型混淆,后者用于向代理日志发送消息。

例如,一个service资源可以使用file资源的subscribenotify,使得该服务依赖于文件的创建。如果文件被更新,它也会接收到一个刷新事件,并重启服务,前提是提供者具有此能力。如以下代码所示,我们展示了依赖关系的双方,尽管只应提供一个关系属性:

service { 'httpd':
  ensure => running,
  subscribe => File['/etc/httpd.conf'],
}
file { '/etc/httpd.conf':
  ensure => file,
  notify => Service['httpd'],
}

在 DAG 图中,这与使用beforerequire是相同的,并且可以使用相同的资源引用或资源引用数组。

每种类型的刷新事件的默认行为和参数如表 6.1所示。这里,我们看到默认情况下,服务将使用提供者的restart变量(如果提供)。否则,hasrestart可以定义一个init脚本,或restart可以定义一个自定义的重启脚本。如果没有提供init脚本,将在进程树中搜索服务名称,但强烈建议提供明确的服务管理脚本。

对于包类型,默认行为是忽略restart事件,但可以将参数设置为在refresh事件后重新安装包。

Exec将在刷新时重新运行其命令,但可以更改为运行不同的refresh命令或仅在refresh事件后运行。

类型 默认行为 参数
Service 如果提供者有重启功能,则重启服务;否则,停止并重新启动 hasrestartrestart
Package 忽略刷新事件 reinstall_on_refresh
Exec 重新运行命令 refreshrefresh only

表 6.1 - Puppet 本地类型刷新选项

元参数依赖可能会产生三种类型的错误。第一种是缺少依赖,即在编译后的目录中找不到资源。这通常应该检查是否存在拼写错误或逻辑错误,意味着资源没有被包含。第二种错误是依赖失败,即资源存在问题,导致无法应用它的任何依赖。此时需要排查该资源并重新运行 Puppet,这样所有依赖的资源就能被应用。第三种错误是依赖循环,我们在 图 6.2 中讨论过并展示过,通过生成有向无环图(DAG)可以帮助识别循环的位置并修复依赖逻辑。

尽管我们之前说过资源除了依赖关系外没有顺序,但这并不完全准确,因为 Puppet 是按照所谓的 清单顺序 运行的。因此,单个清单文件将按其编写的顺序应用,除非依赖关系发生变化。尽管这允许你不使用依赖关系,但其主要目的是防止随机编译导致代码在不同的服务器上表现不同,这可能会发生在随机读取时。

注意

Puppet 在早期版本中经历了一个关于排序的奇怪哲学/纯粹性争论。开发人员习惯性地认为排序应像其他语言一样,按行逐行处理,因此 Puppet 最初选择了随机排序。这种做法混乱不堪,导致在实验室中运行的代码可能可以工作,但在生产环境中按不同的顺序运行并导致失败。

依赖元参数的一种变体是链接箭头,其中 beforerequire 通过 -><- 表示,而 notifysubscribe 则通过 ~><~ 表示。它们通常用来表示类之间的关系,比如表示模块模式,详情见 第八章。例如,如果我们希望 install 类在 config 类之前应用,并且在 config 类更新时应用并刷新 service 类,可以表示为:

include examplemodule::install, examplemodule::config, examplemodule::service
Class['examplemodule::install']
-> Class['examplemodule::config']
~> Class['examplemodule::service']

正如在 第三章 中讨论的那样,include 函数是必要的,以确保类被添加到目录中。

为了风格上的一致性,建议仅使用右箭头,以确保阅读时的一致性。虽然依赖参数可以在类和资源声明中使用,并且可以在其他资源类型中链接箭头,但不推荐这样做,以保持阅读的清晰性。

在简单的情况下,可以通过类内部使用所需的函数来创建对其他类的依赖。然而,没有类似 refreshbefore 的功能,因此为了风格和一致性,通常使用排序箭头会更方便。一个简单的例子,使用 require 函数表示 install 类应该在 config 类之前应用,示例如下:

class examplemodule::config {
  require examplemodule::install
}

我们刚刚讨论的类依赖方法并不像看起来那么简单,因为 Puppet 类实际上并不包含其他类。一个类默认会包含其他类,因此依赖关系不会覆盖它们。接下来我们将探讨这个包含问题的含义以及如何处理它。

包含

Puppet 中的包含意味着包含的类不会像类中的资源那样被包含;因此,当设置对一个类的依赖关系,并通过 include 函数或 class 资源来包含另一个类时,依赖关系只会覆盖资源。例如,假设我们创建了一个要求 class1class2 之前应用,并且 class2 包含一个 package 资源和一个对 class3include 调用,如以下代码所示:

include examplemodule::class1, examplemodule::class2
Class['examplemodule::class1'] -> Class['examplemodule::class2']
class examplemodule::class2 {
  include examplemodule::class3
  package{'PDS':}
}

因此,虽然可能有一种假设认为这将确保 class1class3 之前,但是 class1 确实在 class2 之前,这并不会发生,正如在 图 6.3 的 DAG 图中所看到的那样。

图 6.3 – DAG 显示缺乏包含

图 6.3 – DAG 显示缺乏包含

回想一下在 第三章 中介绍的 include 函数,这种包含并非自动发生,因为我们可能希望将该类包含在不同的地方,以应对不同的情况,并且它在目录中只出现一次,没有依赖或包含问题。

要包含一个类,可以使用 contain 函数。将 include 行改为 contain examplemodule::class3,这将使 DAG 图包含 examplemodule::class3,正如我们在 图 6.4 中所看到的那样。

图 6.4 – DAG 显示使用 contain 函数

图 6.4 – DAG 显示使用 contain 函数

如果 class 资源与 contain 语句一起使用,它必须在 class 资源之后按清单顺序出现。如果没有这样做,class 资源将把 contain 语句解释为尝试声明一个重复的资源,从而导致错误。例如,如果使用以下代码,属性将被成功传递:

class {'examplemodule::class3':
  attribute1 => 'value1''
}
contain examplemodule::class3

对于这个包含问题,直接的问题可能是为什么不使用 contain 来处理所有的内容呢?这归结于它可能产生的不必要且令人困惑的依赖关系。如果我们将原始示例更新为使用 contain 替代 class,并且我们有另一个类 anothermodule:class,它要求 examplemodule:class3 出现在目录中,那么我们可以添加如下代码:

class anothermodule::class {
  contain examplemodule::class3
  package{'PTOP':}
}

然后,DAG 会像 图 6.5 所示。可以立刻看出,我们仅通过少数几个类就创建了不必要的依赖关系。

图 6.5 – DAG 显示由于过度使用 contain 而导致的循环

图 6.5 – DAG 显示由于过度使用 contain 而导致的循环

更糟的是,很容易创建一个循环依赖。例如,如果 security::default 类被包含在所有应用程序类中,application2 类通过 require 函数引用 application1 类,就可能会创建一个循环依赖,代码如下所示:

class application1 {
  contain security::default
}
class application2 {
  contain security::default
  require application1
}

这将生成如 图 6.6 所示的 DAG。如果仅使用 include,就能避免应用程序类与 security::default 之间的不必要关系:

图 6.6 – DAG 显示了过度使用 contain 导致循环依赖的情况

图 6.6 – DAG 显示了过度使用 contain 导致循环依赖的情况

最佳实践与陷阱 部分,我们将进一步讨论如何通过一致的模式避免担心包含问题。

在 Puppet 3.4 版本引入 contain 函数之前,有另一种方法,您可能会在遗留代码中看到:使用 anchor 资源。这可以通过 stdlib 模块提供的特定锚点资源或类中的其他资源对来完成。为了确保当前类包含 examplemodule::class3,使用 anchor 资源的代码如下所示:

anchor {['start', 'stop']: }
include examplemodule::class3
Anchor['start'] -> Class[' examplemodule::class3'] -> Anchor['stop']

或者,如果这两个软件包资源,pdkcowsay,在此类中,它们可以被借用来创建关系并包含该类:

Package['pds'] -> Class[' examplemodule::class3'] -> Package['cowsay']

这种模式的问题是,它会用额外的锚点资源或不必要的关系使 DAG 变得杂乱,可能会引起混淆。因此,如果发现正在使用锚点,建议您通过使用 contain 关键字来现代化您的方法。

讨论了依赖关系和资源及类的包含后,我们将看到变量和资源默认值在 Puppet 语言中的作用域。

作用域

在 Puppet 中,作用域反映了代码中可以直接访问变量的地方,而无需使用命名空间,并且可以影响资源默认值的地方。

作用域有三个级别:

  • 顶层作用域:类、类型或节点定义之外的任何代码。顶层作用域中的任何变量或资源声明将在任何地方都能读取或使用。

  • 节点作用域:在节点定义中定义的任何代码。节点作用域中的任何变量和资源默认值将对与该节点定义匹配的节点在节点和局部作用域级别可见。

  • 局部作用域:在类、定义类型或 Lambda 中定义的任何代码。因此,在该特定资源内定义的任何变量和资源默认值将仅在该资源内可见。

外部节点分类器ENCs)和节点定义将在第十一章中讨论。我们在本节中需要理解的是,ENC 是一个可执行脚本,它返回变量和类,这些变量和类将应用于主机。此脚本可以通过执行各种操作(例如,执行数据库查找或使用 AWS Lambda)注入自定义逻辑和数据。它还可以用于访问第三方源,如配置管理数据库CMDBs)。返回的变量位于顶级作用域,而类位于节点作用域级别。这使得提供的变量可以在任何地方可见,但只有声明了访问节点作用域变量的类才能访问这些变量。相比之下,节点定义是应用于匹配节点的代码段。

类具有所谓的命名作用域,其中类的名称用于命名空间。例如,在exampleclass中创建的名为test的变量可以通过exampleclass::test从任何地方访问。在全局作用域中创建的变量(如site.pp)可以通过调用::variablename从空命名空间访问。然而,通常不推荐以这种方式访问数据。在第九章中,我们将展示如何集中管理数据。

在 lambda 和已定义类型中的节点作用域定义和本地作用域定义是匿名的,只能通过其可见的名称直接访问。从当前作用域声明一个变量(例如,类覆盖全局变量)也可以覆盖更高作用域的变量。

为了展示这一点,请考虑以下代码:

$top='toptest'
$test='testing123'
notify "Top = ${top} node = ${node} local = ${local} test = ${testing}"
notify "Access directly ${example::local}"
node default {
  include example
  $test='hello world'
  $node='nodetest'
  notify "Top = ${top} node = ${node} local = ${local} test = ${testing}"
  notify "Access directly ${example::local}"
}
class example {
  test='an example'
  $local ='localtest'
  notify "Top = ${top} node = ${node} local = ${local} test = ${testing}"
}

第一个notify无法找到本地或节点变量,因为它位于全局作用域,而testing将被设置为testing123。第二个notify将直接访问本地命名空间example,并打印localtest。第三个notify将无法访问本地变量,并打印hello world。第四个notify将再次通过命名空间访问本地作用域。最后一个notify将能够访问所有变量,并将local设置为localtest。此示例展示了变量在作用域之间的流动。

资源标题和资源引用不受作用域的影响,可以在任何作用域中声明。例如,资源可以声明对目录中任何资源的依赖关系。然而,依赖于访问外部变量的做法并不推荐。

最佳实践与陷阱

在早期版本的 Puppet 中,作用域、依赖关系和包含性是一些最具挑战性的问题,这导致了对新开发者的重大困扰。一项主要的解决方案是广泛采用角色和配置文件方法,这在第八章中将详细介绍。Hiera 数据将在第九章中进行详细讲解。

角色和配置文件方法涉及将执行独立功能的单用途组件模块进行分组。例如,一个组件模块可以安装和配置 Oracle。模块结构将包含一组具有特定目的的清单,如安装软件包或管理服务。这简化了模块的组织,并允许更容易地对类进行排序。例如,install类可以在service类之前应用。

组件模块应该相互独立运行,并且模块之间没有直接的依赖关系。配置文件层将模块组合在一起以创建技术栈,并可以在必要时对模块进行排序。角色则抽象出另一层,利用这些技术栈创建业务解决方案,并可以对配置文件进行排序。在这种结构中,任何全局或节点数据应该来自 Hiera,而不是在节点或全局作用域中设置,从而减少代码复杂性。对于开发者来说,避免在代码中设置全局变量可能会感觉不直观,但推荐遵循这一做法。

如在第三章中提到的,建议避免使用资源收集器/导出的资源。然而,值得注意的是,它们可以作为链式箭头的一部分使用。使用它们可能具有风险,因为这可能导致不可预测的结果,并且可能产生难以在运行时映射的大量依赖循环。依赖关系应始终根据需要创建,不应依赖清单的顺序来实现这一点。省略这些依赖关系可能会显著降低代码的可维护性,并在未来重构时带来复杂性。

使用链式箭头表示类依赖关系,并仅在必要时包含它们,如在角色和配置文件方法中所示。避免在全局范围内强制资源默认值,例如在site.pp或节点定义中,因为这种方法会使代码变得不可预测,特别是在与多个应用团队协作时,他们可能不了解自己代码中的这些默认值。总之,避免尝试过于复杂或从其他语言中借鉴的方式,应该遵循既定的角色/配置文件和 Hiera 模式。仔细审查角色/配置文件和 Hiera 模式,并重构任何不符合这些指导原则的代码。

实验室 – 关系、顺序和范围概述

在本实验中,我们将提供一些代码进行回顾和运行,以确保理解所讨论的概念。所有代码都可以在 github.com/PacktPublishing/Puppet-8-for-DevOps-Engineers/tree/main/ch06 中找到。

github.com/PacktPublishing/Puppet-8-for-DevOps-Engineers/blob/main/ch06/lab6_1.pp 中的代码目前没有依赖关系。为了满足以下要求,需要相应调整代码:

  • install 类及其所有资源应在 configservice 之前运行

  • config 类及其所有资源应在 service 之前运行

  • 如果 config 类中的任何资源被更新,则应刷新 service 类及其所有资源

  • httpd 包应在 exampleapp 包之前安装

  • exampleuser 用户应在 examplegroup 组之后创建

  • 应在创建 exampleuser 用户和 examplegroup 组之后创建 /etc/exampleapp/ 目录

  • 应在创建 exampleuser 用户、examplegroup 组和 /etc/exampleapp 目录之后创建 /etc/exampleapp/exampleapp.conf 文件

  • httpd 服务应在 exampleapp 服务之前启动,并且如果 httpd 服务重启,则应刷新 exampleapp 服务

建议使用 validate.puppet.com/ 检查你的 Puppet 代码,因为你不应仅仅依赖于清单顺序。此外,重要的是要记住某些资源具有自动依赖行为。一个示例可以在 github.com/PacktPublishing/Puppet-8-for-DevOps-Engineers/blob/main/ch06/lab6_2.pp 中找到。检查代码并查看 notify 函数将打印什么。

概要

在这一章中,我们讨论了资源默认按任何顺序应用的假设,以及如何使用元参数如beforerequirenotifysubscribe来定义所需的顺序。我们了解到,DAG(有向无环图)可以用来可视化资源之间的依赖关系,并且应该避免依赖关系的循环,以确保目录能够成功应用。我们还讨论了某些资源如何自动应用依赖关系,例如用户需要其主组。我们解释了notifysubscribe元参数,并特别强调它们在资源如execpackageservice中的refresh用法。这允许这些资源在必要时被重启、重新安装或重新运行,例如当配置文件发生变化时。此外,我们还承认,尽管资源应被假定没有特定顺序,但实际上它们会按清单中书写的顺序被应用,以确保在不同环境中的一致性。我们还讨论了三种可能发生的错误:循环依赖、缺失的依赖关系和依赖资源失败。

随后,我们讨论了作为元参数变体的链式箭头,允许它们在类之间使用。我们强调只有右向箭头应当被使用,以遵守样式指南。虽然元参数可以在类上使用,而链式箭头可以在资源上使用以保持一致性和样式,但我们建议避免这种做法。相反,我们展示了require函数如何在一个依赖于另一个类的类中使用,以处理相对简单的类依赖关系。

然后我们讨论了封装问题,这个问题出现在将类包含在其他类中时,并未创建资源依赖关系。通过使用contain函数代替类中的include函数,达到了使该类包含其他类的资源并创建依赖关系的效果。我们讨论了这可能引发包含所有类的诱惑,但我们演示了这会创建不必要的或循环的依赖关系。我们展示了旧的锚点模式,因为遗留代码仍然可能包含这种模式。我们强调anchor函数不再是推荐的做法,且在发现时应当更新为使用contain函数。

作用域影响着变量和资源的默认值,其中全局作用域指的是类、类型或节点定义之外设置的任何内容。节点作用域是指节点定义中的任何内容,而局部作用域是指类、类型或 lambda 中的任何内容。

最后,作为最佳实践,建议遵循角色和配置文件方法,以确保依赖关系和顺序的一致性。还建议使用 Hiera 代替复杂的变量使用,并避免在全局范围内设置资源默认值,例如 site.pp 或节点定义。重要的是,绝不要依赖清单的顺序,应该使用显式依赖关系来确保一致性。

下一章将探讨 Puppet 中的模板、迭代和条件语句。它将展示如何通过利用变量、条件和文本处理函数,Puppet 能够生成文件内容。此外,还将解释如何通过迭代函数和 Lambda 代码块,Puppet 能够循环处理并操作数据集合。最后,本章将介绍如何在 Puppet 中使用条件语句,根据条件逻辑创建不同的配置。

第七章:模板化、迭代和条件语句

本章将介绍 Puppet 语言中的高级结构,包括允许在模板文件中插入变量的模板。Puppet 中有两种格式可用:嵌入式 RubyERB)模板,它基于原生 Ruby 模板化,和嵌入式 PuppetEPP)模板,它是基于现代 Puppet 语言的模板。本章将讨论这两种格式,重点介绍它们之间的区别,以及使用 EPP 而非 ERB 的核心优势。

此外,本章还将深入探讨 Puppet 中的迭代和循环,展示如何使用 Puppet 中的迭代函数和代码块(lambdas),而不是其他语言中常见的loop关键词。最后,本章将讨论 Puppet 中可用的不同类型的条件语句,包括ifcaseunless语句,这些语句是任何编程语言中常见的,以及 Puppet 特有的选择器,它允许根据事实或变量选择键或变量上的值。本章还将详细探讨在条件语句中使用正则表达式的情况。

本章将涵盖以下主要内容:

  • Puppet 中的模板格式 – EPP 和 ERB

  • 迭代和循环

  • 条件语句

技术要求

本节中的所有代码都可以在本地开发服务器上进行测试。

Puppet 中的模板格式 – EPP 和 ERB

Puppet 中的模板化允许通过替换变量并使用条件逻辑来定制内容,从而生成标准格式的内容。Puppet 支持两种模板格式:ERB,它是一个原生 Ruby 模板格式(github.com/ruby/erb),并且在所有版本的 Puppet 中都可用;EPP 模板,它基于 Puppet 语言,在 Puppet 4 中引入,并且在启用未来解析器的 Puppet 3 版本中也可以使用。

模板提供的灵活性超过了字符串,但比使用file_lineaugeasconcat等资源控制单个或一组设置的灵活性要低。因此,在决定使用模板还是资源时,需要在复杂性之间找到平衡。

对于相对较短的heredoc文件或简单字符串,使用带变量插值的模板可能已足够。然而,对于更复杂的文件,尤其是多个模块可能管理不同设置或接受手动编辑的文件,使用资源来处理每个设置或部分会更加简单且易于管理。

在旧代码中,可能会发现模板使用过度,这反映了在 Puppet 早期版本中缺乏资源类型(如 file_line)。因此,审查尝试实现的目标状态非常重要,并确保通过使用模板控制所有内容设置时,整个文件不会被不必要地强制执行,避免其中包含已变得冗余的设置,因为与配置文件相关的底层应用程序已经更新并更改了其配置设置。

虽然新代码中没有必要使用 ERB,但许多 forge 模块和遗留代码库可能包含 ERB,因此本节将涵盖这两种格式,以帮助理解。在展示两种格式的语法后,将讨论使用 EPP 的优点以及将 ERB 转换为 EPP 的理由。

模板可以通过使用模板文件中的内容或通过字符串(称为内联模板)生成。对于模板文件,ERB 使用 template 函数,EPP 使用 epp 函数。对于内联模板,EPP 使用 inline_epp 函数,ERB 使用 inline_template 函数。

EPP 模板

EPP 模板文件是一个文本文件,包含文本和 Puppet 语言表达式的混合,表达式被标签包围。这些标签指示 Puppet 表达式应如何评估,并可以修改模板中的文本,从而基于 Puppet 语言特性(如变量插值、逻辑语句和函数)创建文件。

表 7.1 显示了可以使用的标签类型:

标签名称 起始标签(****带修剪) 结束标签(****带修剪) 目的
参数 <% &#124; (<%- &#124;) &#124;%> (&#124; - %>) 声明模板接受的参数
非打印表达式 <% (<%-) %> (<%-) 评估 Puppet 代码但不打印
表达式打印 <%= %> (-%>) 评估代码并打印其值
注释 <%# (<%#-) %> (-%>) 允许仅为模板文件本身添加注释行

表 7.1 – EPP 模板标签

当模板被评估时,它在遇到起始标签时会切换到 Puppet 模式,遇到结束标签时返回到文本模式。在文本模式下,它将文本输出为内容,当找到标签时,起始标签和结束标签之间的 Puppet 代码会被评估,具体取决于起始标签的类型。

表 7.1所示,一些标签可以通过使用短横线(-)来修剪适当的空格和换行。

参数标签是可选的,如果使用,必须是模板文件中的第一个内容,注释标签除外,且注释标签必须使用闭合连字符。其行为类似于 Puppet 类的参数声明方式,如在 第八章 中所示。参数遵循与类相同的模式,因此它们可以选择性地在开头包含类型。接着必须有一个美元符号($),后跟变量名,后可选择一个等号(=)和默认值,最后必须以逗号结束。

例如,若要使一个选项参数包含一个默认值为空字符串的字符串,一个 application_mode 参数(可以包含 full、partial 或 none 字符串,并且默认值为 node),以及一个 cluster_enabled 参数(布尔值),以下代码将开始我们的模板:

<%- |
String $options = '',
Enum[full,partial,none] $application_mode = 'none',
Boolean $cluster_enabled,
|-%>

当参数传递给 EPP 模板时,它们变为局部作用域,并且可以直接按名称调用,但来自调用类的变量必须使用完整的命名空间名称;这类似于定义类型。任何没有默认值的参数,例如前述示例中的 cluster_enabled,都是必填的,必须传入。

注意

推荐始终在参数前使用连字符,以避免模板开头的任何意外空格。

如果没有使用参数,可以直接使用类作用域访问类变量,例如 $example_module::example_param

参数使得模板在多个不同位置使用时更加灵活,确保数据更为明确且与需求紧密绑定,并且一目了然地展示了消耗的是什么数据。当需要使用大量变量时,使用变量可能比使用参数更合适,因为参数无法扩展。当未在参数列表中定义的参数被传递时,将导致语法错误,尽管如果不使用参数,可以向模板传递任何参数。本节后面将展示如何在引用 epp_template 函数时传递哈希值。

Puppet 模板中的 comment 标签允许在模板文件内添加注释。这些注释在模板被评估并生成内容时不会出现在输出中。以下是 Puppet 模板中注释的示例:

<%#- An example comment. -%>

注意

<%#- 连字符修剪功能自 Puppet 6.0.0 版本起提供。在此之前,修剪行为是默认假定的。

打印表达式标签将 Puppet 表达式的返回值输出到结果中。这可以是一个变量或事实,函数的输出,或者运算符的输出。最终的输出是一个字符串,并会在必要时自动转换。在最简单的情况下,这可以用来打印事实作为值。例如,以下行将读取 application = exampleapp,如果 application_name 事实包含 exampleapp 值:

application = <%= $facts[application_name] -%>

本示例也是变量在这个上下文中首次展示,但它们的访问方式与常规 Puppet 代码中的变量相同。

非打印标签包含迭代和条件逻辑。这与其他标签不同,因为标签的效果可以跨越多行,直到另一个标签关闭迭代器或条件逻辑。例如,if 语句(将在条件语句部分中讨论)以花括号 { 开始,以花括号 } 结束。在下面的示例中,我们可以确保如果应用程序名称从 getvar 函数返回 undef,则不会输出 application =,就像我们之前的例子中所做的那样。相反,如果变量未定义,则会忽略该行:

<% if getvar(facts.application_name) { -%>
 application = <%= $facts[application_name] -%>
<% } -%>

可以使用多级非打印标签来创建适当的嵌套 ifcase 语句。

有一些语法错误需要小心处理。如果非打印表达式标签包含注释,则基本上会注释到行尾,并且需要在下一行上关闭标签,如本例所示:

<%-# I don't finish commenting here  -%> but on the next line
-%>

这种错误显然可能会发生在误键入的 comment 标签中,因此必须小心,并且任何标签,而不仅仅是注释关闭标签,都会被忽略直到新行。

要在模板输出中包含文字 <%%> 字符而不让它们作为 EPP 标签进行评估,您可以使用额外的 % 字符来进行转义。例如,要将 <% Puppet expression example %> 输出为文本,您应该写成 <%% Puppet expression example %%%>。请注意,转义仅适用于遇到的第一个 <%%>,因此如果您需要在一行中仅转义其中一个,您可以使用一次转义,然后正常使用另一个符号。

可以使用 puppet epp validate <template_name.epp> 命令验证 EPP 模板,并且在第八章中,将看到 Puppet 开发工具包 (PDK) 将在其验证过程中运行此命令。

要测试模板的渲染,可以使用 render 命令,并根据需要使用值哈希:puppet epp render <template_name.epp> --values '{key1 => value1, key2 => `value2}'。

注意

EPP 模板的完整规范可以在线查看 github.com/puppetlabs/puppet-specifications/blob/master/language/templates.md

在审查 EPP 模板文件的语法之后,让我们看看如何在 Puppet 资源中使用 epp 函数。可以通过将其传递给 content 属性,将 epp 函数用于诸如 file 等资源。此外,可以提供键值哈希以指定参数,正如在关于 parameter 标签的前一节中讨论的那样:

file { '/etc/exampleapp.conf':
  ensure => file,
  content => epp('exampleapp/exampleapp.conf.epp', {'version' => '1', 'clustered' => false}),
}

注意

如果你希望在开发环境中尝试前面的示例,请在系统上创建一个合适的模板文件,并将exampleapp模块名更改为包含模板的绝对路径,例如/var/tmpC:\Users\David Sandilands

epp中使用的命名空间假设它要么形成一个模块路径<modulename/templatename.epp>,该路径转换为modulepath/modulename/templates/templatename.epp,要么是磁盘上的绝对路径。在第八章中,将详细介绍模块的结构。

内联模板类似于常规模板,但它们不使用单独的模板文件,而是需要传递一个字符串或变量给它们。它们通常用于解决方法或在使用 heredoc 比使用模板文件更简单的情况下。

一个解决方法的例子是在使用第五章中讨论的 Vault 模块时,通过延迟函数来检索密钥。Vault 模块返回一个键值对,但我们可能只想访问密码的值。由于该值是延迟的,因此不能像字符串一样操作。使用inline_epp函数,如以下示例所示,可以在代理运行时解包字符串并将其应用到文件中:

$vault_keypair = { 'password' => Deferred('vault_lookup::lookup', ["secret/examleapp", 'https://vault:8200']), }
file { '/etc/exampleapp_secret.conf':
  ensure => file,
  content => Deferred('inline_epp', ['PASSWORD=<%= $password.unwrap %>', $vault_keypair]),
}

在介绍了用于管理遗留代码的 EPP 模板后,我们将回顾 ERB 的不同之处。

ERB 模板

ERB 模板与 EPP 模板类似,但有一些值得注意的区别。ERB 模板是包含文本和 Ruby 语言表达式的混合文本文件,表达式被标签包围。ERB 使用与 EPP 相同的标签,但它没有参数标签,并且无法传递参数。

在 ERB 中,模板具有局部作用域和父作用域,后者是评估模板的类或定义的类型。当前作用域中的变量可以使用@符号访问,这是 Ruby 通常访问变量的方式。要访问作用域外的变量,可以使用scope对象或较旧的scope.lookup函数,后者是在引入哈希格式之前在 Puppet 中使用的。

为了给出一些简单的 Ruby 示例,可以使用if语句检查exampleapp_extras变量是否不包含NONE,并在模板中输出extras <exampleapp_version>字符串。还可以使用unless语句检查exampleapp_key变量是否为nil,如果它有定义值,则输出key <exampleapp_nill>字符串:

<% if @exampleapp_extras!= "NONE" %>extras<%= @exampleapp_version%><% end %>
<% unless @exampleapp_key.nil? -%>
key <%= @exampleapp_%>
<% end -%>

Ruby 中的迭代类似,each函数也可用。以下示例显示了通过迭代将一个变量中的设置数组逐个输出到模板内容中:

<% @array_of_settings.each do |setting| -%>
<%= val %>
<% end -%>

Puppet 变量的数据将从 Puppet 类型转换为等效的 Ruby 类型(更多信息请参见官方 Puppet 文档:www.puppet.com/docs/puppet/latest/lang_template_erb.html#erb_variables-puppet-data-types-ruby)。然而,如何将这些数据转换为 Ruby 类型已经超出了本书的讨论范围。

还可以使用 <%scope.function_name(<函数名称>, <参数数组>)%> 语法在 ERB 模板中调用 Puppet 函数。

例如,要对 example_variable 变量使用 downcase 函数并将结果输出到模板,可以使用以下代码:

<%= scope.call_function('downcase', [@example_variable]) %>

验证 ERB 模板的语法可以通过运行 erb 命令来完成:erb -P -x -T '-' example.erb | ruby -c。与 EPP 一样,PDK 在运行验证时会检查两种模板。遗憾的是,无法呈现 ERB 模板。

在文件中使用 ERB 模板文件的内容看起来与 EPP 非常相似,但如前所述,它没有参数,并且使用 template() 函数。转换 EPP 示例的方式如下:

file { '/etc/exampleapp.conf':
  ensure => file,
  content => template('exampleapp/exampleapp.conf.erb'),
}

可以传递并评估多个模板文件,这些文件将会合并在一起。例如,更新如下内容将合并两个模板:

content => [ template('exampleapp/exampleapp.conf.erb'), template('exampleapp/exampleapp2.conf.erb')]

内联 ERB 仅使用与内联 EPP 相同系统中的inline_template函数,并且过去经常用来通过 Ruby 代码提供一种解决方案,弥补早期版本的 Puppet 缺乏迭代/循环功能,并执行数据转换。

现在既然已经讨论了 ERB,是时候突出 EPP 相较于 ERB 更受青睐的原因以及考虑转换遗留代码的理由了。

EPP 与 ERB 比较

在审查了两种语法模板后,很明显 EPP 相较于 ERB 具有多个优势。首先,EPP 的性能明显优于 ERB。每次评估模板时,ERB 会为所有事实和顶层变量创建一个作用域对象,而 EPP 仅使用与模板相关的事实和变量。在拥有大量事实的环境中,这可能会对性能产生显著影响。

此外,EPP 提供了更高的安全性,因为模板可以提供一个有限的数据范围供使用,并在使用前验证所有数据是否存在。而 ERB 则没有内建的验证,且不存在的变量会被简单地忽略。例如,如果类中的某个变量在模板使用前未被评估,ERB 将无法捕捉到这一点。

EPP 也被认为更易于使用,因为它采用 Puppet DSL 风格,并且不需要任何 Ruby 知识。这使得编码更加容易,特别是能够使用puppet epp rendervalidate命令。此外,EPP 正在积极开发中,最近的功能(例如,6.20 及以后版本中能够自动解开敏感变量的模板)仅在 EPP 中可用。

迭代和循环

Puppet 的迭代和循环方法受其变量不可变性的影响,这意味着一旦设置,变量就不能更改。这使得许多正常使用loopdo关键字来转换数据的方法变得不可能。在语言的早期版本中,通过传递数组给定义类型来解决这个问题,如第三章所述,或者使用带有 Ruby 代码的内联 ERB 模板来操作数组和哈希。

然而,使用定义类型方法的问题在于,执行工作的代码被抽象化了,不可见。而且,每次需要不同类型的迭代时,都需要其自定义的定义类型,这使得代码膨胀。因此,回顾旧代码并重构这些模式为接下来将讨论的方法非常重要。

在现代 Puppet 中,采取的方法是使用迭代函数将数据从数组和哈希传递给 lambda。lambda 是一个没有名字的函数,因此除非通过某个函数调用,否则无法在其他地方调用。lambda 可以附加到任何函数调用中,包括自定义函数。表 7.2提供了涉及迭代和 lambda 的完整函数列表。虽然一些函数可能不被认为是迭代器,但它们有类似的行为。还应注意,其他函数可以与这些示例组合/链接,例如unique

函数名称 目的 返回类型 参数
all 遍历所有元素,直到 lambda 返回false或完成并返回true truefalse 1 或 2
any 遍历所有元素,直到 lambda 返回true或完成并返回false truefalse 1 或 2
break 用于 lambda 内部,停止迭代。
each, reverse_each, tree_each 依次传递哈希或数组的每个元素供 lambda 处理(逆序或递归变体)。 1 或 2
filter 遍历所有元素并与 lambda 代码匹配,返回匹配的元素数组。 数组 1 或 2
index 遍历所有元素,并在 lambda 代码中首次匹配时,返回匹配元素的索引。 整数 1 或 2
lest 该函数接受一个参数;如果该值未定义,它将运行一个 lambda 并返回结果。如果参数未定义,它将返回该参数。 任意有效类型 0
map 遍历所有元素并对该元素应用 lambda 代码。返回应用 lambda 后的元素数组。 数组 1 或 2
next 在 lambda 中使用,用于改变迭代中下一个元素的值。 n/a n/a
reduce 遍历所有元素并应用 lambda 代码,将结果传递给每次迭代。 数组 2
return 用于使 lambda 返回(不能在顶层作用域中使用)。 n/a n/a
slice 按切片大小遍历元素,例如每次迭代三个元素。 数组 1 或切片大小
step 链接到另一个可迭代函数,传递一个从起始元素到结束元素按步长递增的元素序列。 可迭代 n/a
then 接受一个参数,如果该参数不是 undefined,则调用一个 lambda 并传递该参数。否则,返回 undefined 任何有效类型 1
with 接受一个参数,并无条件将其传递给 lambda 并使用该参数运行。返回 lambda 的结果。 任何有效类型 1

表 7.2 – 迭代和 lambda 函数

在 Puppet 中使用 lambda 的迭代函数的基本语法结构如下:

<function acting on data> | <parameter(s)> | { lambda of Puppet code }

例如,考虑使用 each 函数对一个数组进行迭代,使用一个参数(可选类型),并打印输出:

['first', 'second', 'third'].each | String $x | { notice $x }

这将导致 notice 函数为数组中的每个字符串打印输出,类似于大多数语言中的 for 循环和 print/echo 命令。

each 函数也可以使用两个参数,第一个参数为索引,第二个参数为该索引对应的内容。以下代码将在第二次迭代时打印 index 2 contains second

['first', 'second', 'third'].each | $index $value | { notice "index $index contains $value" }

注意

要在开发者桌面上测试这些示例,只需运行 puppet apply -e '<``example code>'

为了明确,当在哈希上使用 each 函数并且仅使用一个参数时,每个键值对将作为一个数组传递给 lambda。例如,运行代码 [{ key1 => 'val1', key2 => 'val2' }].each | $key_pair | { notice $key_pair } 将输出两个数组,每个键值对对应一个数组:['key1', 'val1']['``key2', 'val2']

如果 lambda 使用两个参数,第一个参数代表键,第二个参数代表值。例如,运行代码 [{ key1 => 'val1', key2 => 'val2' }].each | $key, $value | { notice "$key contains $value" } 将输出两个字符串,每个键值对对应一个字符串:key1 contains val1key2 contains val2

还值得注意的是,其他数据类型(例如字符串)可以自动转换为数组,其中字符串中的每个字符都被视为一个元素。此外,还可以使用 Integer 类型声明数字范围;例如,运行代码 Integer[100, 150].each | Integer $number | { notice $number } 将输出从 100150 的所有整数。

最后,迭代可以嵌套;例如,为了处理具有数组值的哈希,可以在 lambda 内使用迭代函数。运行以下代码将输出 key1 键值对数组中的每个值——'value1''value2'

[{ key1 => ['value1', 'value2'], key2 => 'val2' }].each | $key, $value_array | {
  $value_array.each | $value | {
    notice $value
  }
}

总体而言,本节概述了 Puppet 中最常用的函数,但若想了解更深入的描述,用户可以参考官方文档:www.puppet.com/docs/puppet/latest/function.html

迭代循环

到目前为止审查的主要函数是 each,还有几个函数执行元素的循环或操作循环。reverse_each 函数简单地按名称所示取元素的反向顺序。tree_each 允许返回嵌套数组/哈希中的值,并根据提供的标志采用不同的行为。它相对复杂且较为冷门。slice 函数允许我们在每次迭代中获取特定数量的元素。例如,以下代码会每次传递三个数字的数组给 lambda:

Integer[100, 151].slice(3) | Array $numbers | { notice $numbers }

在最后一次迭代时,它将提供剩余的元素;在此示例中,即一个仅包含 [151] 的数组。也可以使用多个参数,但参数的数量必须与 slice 的大小相同:

Integer[100, 151].slice(3) | Integer $first, Integer $second, Integer $third  | { notice $numbers }

step 函数允许我们选择要传递的可迭代元素。在这个代码示例中,它将从第一个元素开始,然后是第四个、第七个,以此类推:

Integer[100, 150].step(3) | Integer $numbers | { notice $numbers }

这在链式调用到另一个可迭代函数时非常有用。下一个函数类型是用于匹配模式。这是一种不同风格的迭代函数,在这种方式下,迭代函数定义了 lambda 如何返回,而不是仅将元素传递给 lambda 执行某些操作。例如,all 寻找所有元素是否都满足 lambda 中的检查条件以返回 true。如果任何一个 lambda 返回 false,该函数将返回 false。例如,以下代码将输出 true,因为所有元素都大于 99

Integer[100, 151].all | Integer $number | { $number > 99 }.notice()

any 函数与 all 相反,如果没有匹配项,则返回 false,如果在任何迭代中 lambda 返回 true,则返回 trueindex 函数类似于 any,但它不是返回 truefalse,而是返回匹配元素的索引号,若没有匹配项则返回 undef。例如,以下代码会输出 20,因为 number 会匹配到第 20 个元素:

Integer[100, 151].index | Integer $number | { $number == 120 }.notice()

所有这些函数都可以在数组或哈希上使用两个参数,如 each 函数的示例所示。

数据转换

数据转换是迭代器用于遍历元素并在返回之前进行调整的另一种方式。这也是迭代器与 lambda 模式开发的主要原因之一,因为 Puppet 无法重新赋值变量。例如,map函数会遍历每个元素,并应用一个 lambda,其结果会存储在一个数组中。例如,下面的代码会将每个元素除以1024,并返回一个数组[2, 3, 1]

[2048, 3096, 1024].map | $size | {  $size / 1024 } .notice()

filter函数在每次迭代中处理每个元素,并应用 lambda 中的代码。如果 lambda 返回true,则该元素将被添加到数组中并返回。否则,如果返回false,则会继续到下一次迭代。例如,下面的 filter 会遍历每个数组,检查其大小是否大于0,结果会输出[[1, 2, 3], ['a', 'b', 'c']],并移除第二个元素中的空数组:

[[1,2,3], [], [a,b,c]].filter | $array | { $array.size > 0   } .notice()

filtermap都可以处理一个或两个参数,如在数组和哈希上的each函数所示。

reduce函数允许在 lambda 中进行累积操作。它与其他函数不同,需要两个参数:第一个参数在迭代过程中保持其值,而第二个参数是元素。此外,可以通过传递一个值给reduce函数来选择第一个参数的初始值。在这个例子中,total参数的初始值为1,并且在每一轮中将元素加到它的总和上,最终返回并打印15

[2, 4, 8].reduce(1) | $total, $number | {  $total + $number } .notice()

在 lambda 中,也可以改变迭代的流程。next函数可以改变下一个元素是什么:如果没有传递任何值给next函数,则为undef,否则为提供给next函数的值。break函数会在代码的这一点停止迭代器,并返回到迭代函数,从而有效地结束迭代器的执行。相比之下,return函数会从迭代函数中返回,因此不会继续执行,并返回到包含类、函数或定义的类型。

为了演示这一流程的变化,第一个使用map的例子会遍历一系列数字,运行next函数,当元素等于101时,会将下一个元素102替换为1984,然后当元素大于104时会运行break函数。因此,最终会打印的输出将是一个数组[100, 1984, 102]

Integer[100, 151].map | Integer $number | { if $number == 101 { next(1984) } if $number > 103 { break() } $number }.notice()

为了突出用return函数替换break函数后的不同行为,下面的例子将不会打印任何内容:

class example {
  Integer[100, 151].map | Integer $number | { if $number == 101 { next(1984) } if $number > 103 { return() } $number }.notice()
} 

这就是在这个例子中我们将函数放入类中的原因,因为return只能在类、函数或定义类型中调用,不能在顶层作用域调用。

嵌套数据

最后一类函数在处理嵌套数据或处理未定义值时非常有用。then 会在 lambda 后链接,如果它从该 lambda 输出 undef,则返回 undef;否则,它会将该值传递给另一个 lambda。所以,以下示例将使用 dig 函数来尝试访问数组中第二个哈希表中不存在的 c 元素,由于 then 将接收到 undef,它因此会返回 undef

$example = {first => { second => [{a => 10, b => 20}, {d => 30, e => 40}]}}
$example.dig(first, second, 1, c ).then |$x| { $x / 10 }.notice()

为了澄清前面的语句,如果将 dig(first, second, 1, d) 改为 dig(first, second, 2, d),它将把 30 传递给 lambda,该 lambda 将除以 10 并打印出 3

lestthen 的反义词,并返回其定义的值;否则,它会将 undef 传递给 lambda,这样就可以采取诸如设置默认值等操作。这在与 then 一起使用时非常有用。以先前的示例为例,添加 lest 将允许在值为 0 时返回 undef

$example.dig(first, second, 1, c ).then |$x| { $x / 10 }.lest() || { 0 } .notice()

with 函数有些特殊,因为它用于通过 lambda 传递值,如果我们的 lambda 能处理 undef 或已定义的值。

因此,经过对各种函数的审查,以及数据转换和数据探索的可能性,值得再次强调,与过去使用定义类型的方式不同,当我们需要创建多个资源时,应该使用另一种方法。所以,例如,要为请求的多个实例创建目录,可以使用以下代码:

Integer[1,$instance_number].each |Integer $id | {
  file {"/opt/app/exampleapp/instance${id}":
    ensure => directory,
  }
}

在讲解了 Puppet 如何执行循环和迭代后,接下来将回顾条件语句。

条件语句

Puppet 具有你在任何语言中都能预期的条件语句,ifunlesscase 允许根据事实或来自外部源的数据等不同因素使代码行为有所不同。此外,Puppet 还使用选择器,这类似于 case 语句,但返回一个值,而不是在结果上执行代码。

ifunless 语句

if 语句遵循特定的语法,其中包括 if 关键字,后跟条件,一个左花括号({),当条件为 true 时要执行的 Puppet 代码,最后是一个右花括号(})。

以下示例是对 example_bool 中布尔值的简单检查,如果它包含 true,则打印一条通知:

if $example_bool {
  notice 'It was true'
}

这可以通过在if语句的闭括号(})后可选地添加 else 关键字,并使用左花括号({)结合 Puppet 代码来执行,当条件为 false 时。然后再用右花括号(})结束。如果要在 example_boolfalse 时也打印,则代码更新如下:

if $example_bool {
  notice 'It was true'
}else{
  notice 'It was false'
}

类似地,要一起执行多个if检查,可以在if语句的闭括号(})之后使用elsif关键字,允许再次使用相同的if语法。可以根据需要进行嵌套和重复。举个例子,在布尔值检查后,使用elsif,我们可以添加第二个检查,查看值变量是否大于2,如果是,则打印一条通知,并使用else语句打印两个条件都是false的情况:

if $example_bool {
  notice 'It was true'
}elsif $value > 2{
  notice 'It was false and value is greater than 2'
}else {
  notice 'It was false and the value was 2 or less'
}

unless语句只是if语句的反义语句。它允许你避免对条件进行取反,并且它也可以与if语句结合使用。然而,它没有与elsif相对应的部分,如果使用将导致编译失败。以之前的if示例为例,可以改用unless语句来检查example_bool是否为false,如果是,则打印一条通知:

unless $example_bool {
  notice 'It was false'
}else{
  notice 'It was true'
}

条件中使用的任何非布尔值将根据数据类型规则转换为布尔值,如在第四章中所述。

Puppet 风格指南建议,在ifunless关键字之后的代码行应该缩进两个空格,并与示例中显示的对齐。

case语句

case语句通过匹配控制表达式输出的值来工作。通常这是事实或变量的内容,但它也可以是一个表达式或函数。该格式以case关键字开始,后跟一个控制表达式,解析为一个值,并用大括号括起来。每一行以匹配的 case 或逗号分隔的 case 列表开始,后跟冒号,然后是要应用于匹配 case 的 Puppet 代码,代码用大括号括起来。case语句最后以大括号结束。

例如,要测试hardwareisa事实的值并根据使用的处理器架构类型包含一个配置文件,可以使用以下代码。它包括sparcpowerpc值的 Unix 配置文件,i686i386值的 Linux 32 位配置文件,任何以64结尾的值的 64 位配置文件,以及任何无法匹配到某个情况的值的默认配置文件:

case $facts[' hardwareisa'] {
  'sparc', 'powerpc': { include profile::unix}
  'i686', 'i386': { include profile::linux::32bit}
  /(*64)/: { include profile::linux::64bit}
  default: { include profile::default }
}

Puppet 风格指南建议始终使用default case,它可以是一个失败的情况,甚至只是一个空的大括号。

选择器

选择器类似于case语句,但它们不会应用 Puppet 代码,而是返回一个值。选择器可以在期望值的任何地方使用,如变量赋值、资源属性和函数参数。Puppet 风格指南建议仅在变量赋值中使用选择器以提高可读性,但在遗留代码中,它也可以在资源属性中看到,因为它曾是一个流行的模式。

选择器的语法是一个控制表达式,解析为一个值,一个问号(?)和一个左花括号({)。然后它有多个匹配案例,从一个单独的案例或default关键字开始,后跟一个哈希火箭符号(=>),返回的值,最后是一个闭合的逗号。选择器以一个右花括号(})闭合。

以下示例展示了如何根据os.family事实的输出分配apache_package_name变量,使用httpd作为 Red Hat 的名称,apache2用于 Debian 或 Ubuntu,apache-httpd用于 Windows,如果没有匹配,则默认为httpd。然后,包资源可以使用这个名称来安装相关包:

$apache_package_name = $facts['os']['family'] ? {
  'RedHat' => 'httpd',
  /(Debian|Ubuntu)/ => ' apache2 ',
  'Windows' => 'apache-httpd',
  default => 'httpd',
}
package { $apache_package_name }

case语句一样,Puppet 风格指南建议选择器始终使用default案例,这可以是一个失败,甚至只是一个空的花括号。

捕获变量

如果使用的case语句是正则表达式,则所称的捕获变量将在相关代码中作为数字变量(如$1$2)可用,整个匹配项则可通过$0访问。

要修改Case 语句部分中的示例,如果匹配的是amd64,这将包括profile::linux::amd64

/(*64)/: { include profile::linux::${1} }

在回顾了模板、条件语句、迭代和循环的各个方面后,我们将通过一个实验来总结并结合这些概念。

实验 – 创建并测试包含循环和条件的模板

在这个实验中,我们将把你在本章中看到的所有内容结合起来,测试和验证一些示例模板,并创建一个包含逻辑和迭代的模板:

  1. github.com/PacktPublishing/Puppet-8-for-DevOps-Engineers/tree/main/ch07/templates_to_check下载模板文件,并验证和解析它们以确保模板生成以下内容(<>显示需要插值的部分):

    • “此模板在具有<#你机器的 cpu 数量>个 cpu 的机器上运行”

    • “自定义事实包.lab 已被<设置为事实的内容或字符串‘未设置’>”

提示

你会想使用getvar函数来测试事实,并通过传入哈希来测试它,在解析时设置它以进行测试。

  • “系统运行时间是<仅显示天、小时和分钟,如果它们非零>”

  • “此机器是<不是/是>虚拟的<并且运行在<$virtual>上”

  1. 创建一个模板,打印以下内容

“这是一个机器,运行版本 “以下目录在路径中<列出每个路径>”

提示

使用 split 函数 (www.puppet.com/docs/puppet/latest/function.html#split) 将路径事实字符串分割成可以迭代的数组,并记住,Windows 路径是通过 ; 分隔,而基于 Unix/Linux 的路径则通过 : 分隔。参见 github.com/PacktPublishing/Puppet-8-for-DevOps-Engineers/blob/main/ch07/2_sample.epp 中的答案。

总结

本章审视了在 Puppet 中使用模板。展示了 Puppet 的两种模板类型——EPP 和 ERB——它们以类似的方式工作,通过将纯文本和代码周围的标签混合,允许 Puppet/Ruby 应用逻辑和变量,并在评估时创建更复杂的内容。文中警告,在使用模板代替诸如 file_line 等函数或单独控制资源之前,应仔细考虑复杂性的层级。此外,由于缺乏用于单行或文件设置控制的函数,模板已被过度使用,遗留代码应仔细审查,以确保模板是正确的复杂性级别。

显示了 EPP 是推荐的模板生成方式,因为它是用 Puppet 语言编写的,且对 Puppet 开发者来说更容易学习。它也更安全,因为可以通过参数限制其作用域,并且由于它仅为所需的变量和事实创建作用域,因此性能也更好;而 ERB 每次使用模板时,都会为所有事实生成一个作用域。此外,还提到所有 Puppet 的未来开发工作都将集中在 EPP 上,例如 EPP 文件中自动解包敏感变量的功能,且渲染模板的能力仅在 EPP 中可用。

epptemplate 函数显示可以通过文件引用和评估模板,其中多个文件可以组合在一起。同时也展示了通过 inline-templateinline-epp 函数可以实现内联模板,这样文本可以直接传递给函数,而不需要存储在文件中。

然后展示了迭代和循环,强调了 Puppet 之前由于 Puppet 变量的不可变性,使得更传统的loop关键字变得不切实际。相反,Puppet 被展示为使用对数组和哈希的迭代函数,将值作为参数传递给 lambda 函数。这些没有命名的 lambda 函数只能通过其他函数调用,允许创建完全局部于 lambda 函数的作用域,因此允许变量名被重用。迭代函数选择如何将值传递给 lambda,例如每次传递一个值或键值对,或者使用Reduce,它允许在每个 lambda 函数中传递一个参数,连同每个值和键值对一起传递,并且可以用于执行累积转换。

然后讨论了 Puppet 的条件逻辑,显示其与大多数其他语言相似。if 检查会将检查评估为布尔值语句/比较,如果为true,则使用 Puppet 代码进行操作。else 关键字允许在布尔值为false时执行某个操作,elsif 关键字允许将多个检查链式连接在一起。unless 被显示为 if 的反义操作,当检查为负时进行操作,并允许 else 在检查为 true 时执行,尽管它没有 elsif 的等价项。

然后讨论了 case 关键字。我们展示了它是通过获取值并将其与某个匹配项进行匹配,基于匹配结果运行 Puppet 代码,或者如果没有找到匹配值,则执行默认操作。selector 关键字的行为类似于 case 语句,但它用于分配一个值,而不是运行 Puppet 代码。需要强调的是,尽管过去在资源中使用选择器是一个常见模式,但这已不再被认为是最佳实践。最后,捕获变量作为可用于条件语句的变量被展示,它们通过正则表达式显示匹配结果。

现在已经回顾了 Puppet 核心语言,是时候学习如何构建我们迄今为止使用的 manifest 文件和类了。下一章将展示模块如何提供必要的结构来容纳我们所研究的 manifest、类以及其他配置和实现文件。还将探讨角色和配置文件模式,它提供了一种额外的抽象,用于表示客户的技术堆栈和业务需求。此外,将演示 Puppet Forge 作为一个模块来源,可以通过它减少开发需求,并与 Puppet 社区协作以增强现有代码。

第八章:模块的开发与管理

在审视了 Puppet 语言的许多方面后,我们可以清楚地看到,仅使用清单文件和类是不够的,随着代码库的增长,无法提供所需的结构,特别是在需要支持多种服务器和客户需求时。在本章中,我们将回顾在大规模创建 Puppet 代码时所需的组件。我们将讨论 Puppet 模块,它们允许我们将代码和数据捆绑在一起,专注于单一的技术实现,从而使得与其他实现共享和结合变得更加容易。接着,我们将探索 角色与配置文件方法,展示如何通过配置文件将模块组合在一起,创建技术栈和角色,然后通过组合配置文件来满足业务需求。之后,我们将介绍 Puppet 开发工具包PDK),展示如何通过它自动化创建和管理模块的过程。我们将展示 PDK 模板化的目录和文件,重点介绍其内置的验证、代码检查和单元编译检查。接下来,我们将介绍 Rspec,一种扩展方法,用于提供更为彻底的单元测试,以及用于服务器测试的 ServerSpec,并进行概述。然后,我们将讨论 Puppet Forge 目录,它作为 Puppet 官方、供应商和社区成员开发模块的来源。我们将展示如何过滤模块的各个方面,以了解它们的支持情况、与操作系统和 Puppet 版本的兼容性,以及评分/扫描评分,从而帮助你选择最适合组织需求的模块。

本章我们将讨论以下主要内容:

  • 什么是模块?它包含哪些内容?

  • 角色与配置文件方法

  • PDK 及如何编写和测试模块

  • 使用 RSpec 与 PDK 进行测试

  • 理解 Puppet Forge

技术要求

本章不需要部署任何基础设施。所有操作都可以从开发者的桌面进行。

什么是模块?它包含哪些内容?

模块为我们提供了一种将代码和数据进行分组的方式,使得共享和重用特定技术实现的代码变得更加容易。几乎所有的 Puppet 代码都将存储在各种类型的模块中。你应该明确模块的范围,以创建具有单一明确职责的专注模块。如果你正在部署 LAMP 或 WAMP 堆栈,你不会创建一个包含所有组件的单一模块;而是会将其拆分成多个独立的模块,包括操作系统设置、MySQL 和 Apache。这种方式可以更好地重用代码,并减少单个模块的复杂性。

模块是一个目录,其命名规则类似于类,因此必须以小写字母开头,并且只能包含小写字母、数字和下划线。与类不同,模块不能嵌套,也不使用 :: 符号。保留字和类名不应作为模块名使用。

模块具有一种目录结构,使 Puppet 能够知道各种类型的代码和数据将存储在何处,并按请求自动加载。如第六章所述,该模块将创建一个作用域命名空间和文件服务命名空间。核心代码和数据存储在以下目录中:

  • data:包含模块化数据,用于参数默认值,相关内容将在第九章中讲解。

  • examples:包含如何声明模块类和定义类型的示例。

  • files:包含可以由 Puppet 放置的静态文件内容。

  • manifests:包含模块的所有清单以及提供结构的目录。

  • template:包含将由 Puppet 代码使用的 EPP 和 ERB 模板文件。

  • tasks:包含程序化工作的任务。将在第十二章中讲解

模块还有一种叫做插件的功能,可以将各种自定义的 Puppet 组件分发到 Puppet 服务器或代理端,具体取决于相关情况。以下是一些插件示例:

  • lib/facter:由Ruby编写的自定义事实,在代理端使用

  • lib/puppet/functions:由 Ruby 编写的自定义函数,供服务器使用

  • lib/puppet/type:在服务器和代理端都可以使用的自定义资源类型

  • lib/puppet/provider:由 Puppet 编写的自定义资源提供者,服务器和代理端都可以使用

  • lib/augeaus/lenses:在代理端使用的自定义 Augeas 镜头

  • facts.d:在代理端使用的外部事实或静态脚本

  • functions:由 Puppet 编写的自定义函数,供服务器使用

需要注意的是,某些插件类型,例如资源类型,并没有在环境中完全隔离。环境将在第十一章中详细讨论,我们将重点讨论分类和发布管理,但现在需要注意的是,环境允许通过节点使用隔离的代码库,从而使用不同版本的代码。这是由于 Ruby 加载第一个资源类型并将其设置为全局,忽略任何发现的重复项。因此,如果使用包含无法隔离插件的模块,请务必查阅 Puppet 文档:puppet.com/docs/puppet/latest/environment_isolation.html#environment_isolation。如果需要,您可以配置环境隔离,Puppet Enterprise 默认提供环境隔离。

模块可以以不同的方式使用。尽管本章的大部分内容将集中在使用 Puppet 代码进行配置的模块上,但模块也可以用于分发一个或多个项目。例如,Puppet Forge 中的 PowerShell 模块(forge.puppet.com/modules/puppetlabs/powershell)仅用于通过提供者插件目录分发新的执行提供者。

关注manifests目录时,清单文件将与其包含的类名相同。一个主要的例外是主清单,它命名为init.pp,但其类名为模块的类名。这个主清单通常用作模块的入口点。如第六章中讨论的那样,为模块创建了一个模块命名空间,允许我们通过运行include <module name>来在代码中包含该模块。

类应该是自包含的并且简洁,专注于一个方面。识别类是否过大的一个常见建议是,当它太大,以至于无法在单个编辑器屏幕中查看时,就是过大了。考虑到这一点,开始使用模块时最常见的模式之一是使用主清单init.pp作为入口点,接受参数以供整个模块使用。然后,它调用其他类并设置它们的顺序。例如,使用install类来安装资源(如包),使用config类来添加任何配置文件或用户,以及使用service类来管理服务。以下代码展示了这一模式的主清单示例:

class exampleapp (
  Boolean $package_managed = true,
  Integer $package_version = 3,
  Boolean $user_managed = true,
  Integer $user_id= 10,
  Boolean $service_enable = true,
  Integer $jmx_heap_size = 1024,
  Integer $thread_number = 10,
)
{
  contain exampleapp::install
  contain exampleapp::config
  contain exampleapp::service
  Class['exampleapp::install']
  -> Class['exampleapp::config']
  ~> Class['exampleapp::service']
}

考虑到可用的参数,如模块的公共 API,可以使模块具有灵活性;此外,命名这些参数时应该保持一致性。在这里,我们使用一种方法,根据参数的作用来命名参数。因此,对于exampleapp,可以看到包和用户都采用布尔值来声明模块是否将其作为资源进行管理。布尔值用于service_enable来决定服务是否在启动时启用,而user_idpackage_version则使用整数。接着,使用两个额外的整数来配置应用程序,设置 Java 内存大小和线程数。这些参数可以通过模块命名空间访问,并通过在modulename::variablename处执行数据查找。此方法被称为自动参数查找,我们将在回顾 Puppet 如何处理数据时,在第九章中详细讲解。

注意

模块的参数及其他方面可以通过代码头部的注释进行文档化,然后使用 Puppet Strings gem 生成不同格式的文档。详细信息可以在 Puppet Strings 风格指南和以下网页中找到:puppet.com/docs/puppet/latest/puppet_strings_style.html

我们采用在模块内对类进行包含和排序的方法。这确保了在请求时,先安装软件包,添加配置,再管理或刷新服务,因为每个阶段都依赖于下一个阶段。contain 关键字不应被视为替代 include 的默认关键字;它应仅在组件模块风格下使用,并且类只会在主类中使用。在 角色和配置方法 部分,你会看到类似的包含和排序在某些情况下是不合适的。

从中可以看出,这些子类是如何使用来自主清单的参数的。例如,install 清单对 package_managed 变量使用 if 语句;如果它为 True,则安装由 package_version 变量设置的 exampleapp 包版本:

class exampleapp::install {
if $exampleapp::package_managed {
  package { 'exampleapp':
    ensure => $exampleapp::package_version
  }
 }
}

对于 config 清单,我们可以看到如何通过使用模块命名空间将 jmx_heap_sizethread_number 变量替换到模板中,并通过该命名空间访问存储在 exampleapp 模块中的模板:

class exampleapp::config {
  file { '/etc/exampleapp/app.conf':
    ensure  => file,
    content => epp('exampleapp/app.conf.epp', {' jmx_heap_size ' => $exampleapp::jmx_heap_size , ' thread_number' => $exampleapp::thread_number }),
  }
}

service 类的风格与 install 类非常相似。它使用 if 语句;如果条件为 True,则添加 exampleapp 服务,并根据 service_enable 变量设置 enable 参数:

class exampleapp::service
  if $exampleapp::service_managed {
    service{'exampleapp': 
    ensure => 'running', 
    enable => $exampleapp::service_enable ,
    }
  } 
}

注意

一个常见的模块模式是使用 params.pp 文件来管理默认的模块数据。现在,Hiera 5 能比清单文件以更结构化的方式管理模块级数据,具体内容将在 第九章 中展示。params.pp 文件仍然常见于代码中,特别是在数据结构简单且没有太大意义改变为 Hiera 的情况下。

examples 目录中可能包含一个名为 init.pp 的文件,指定如何调用类:

class { 'exampleapp':
  package_managed => true,
  package_version => 3,
  user_managed => true,
  user_id => 10,
  service_enable => true,
  jmx_heap_size => 1024,
  thread_number => 10,
}

examples 目录中,文件的命名并不重要;可以有多个不同的示例,展示不同设置下常见模块属性的选择。例如,一个模块可能展示如何使用最小默认值包含它,也可能展示特定架构所需的属性,例如在集群设置中进行部署。

随着配置用例变得更加复杂,模块结构的另一种常见方法可以在 Puppet Forge 目录中的 Apache 模块中看到:forge.puppet.com/modules/puppetlabs/apache。与基于简单 packageconfig 类的资源分组不同,apache 模块将应用程序的不同组件进行了拆分。在此示例中,Apache 的主清单执行了 Apache 的默认安装,具有默认虚拟主机和文档根目录,并启动了 Apache 服务。可以通过使用相关模块参数来配置此操作。在这里,Apache 服务由单独的类管理,但通常在 packageconfig 类中管理的资源是在 Apache 主类中管理的。还有各种实现类,例如 vhostsmodmpm,用于不同的 apache 配置项。这使得主类具有执行基本安装和配置 apache 服务器的明确目的,以便实现类可以专注于特定的定制。例如,vhosts 类是定义类型,可以为 apache 服务器所需的每个虚拟主机定义。

这些示例提供了一个结构,您可以根据需要为您的模块进行调整。但是,需要记住的关键教训是,模块应专注于单一任务,仅管理其资源(无跨模块依赖),并且应该是细粒度且可移植的。

在本节中,我们查看了 Puppet 模块的目录和文件结构,以及创建模块的两种常见模式。这些模块本身配置了专注于个别技术实现。在下一节中,我们将看到如何使用模块的结构来组合模块以生成技术堆栈,并通过组合技术堆栈和配置,满足客户对服务器的业务需求。

实验室 - 审查 Apache 模块

在本书中打印 apache 模块代码的所有关键部分是不切实际的。但是,查看 forge.puppet.com/modules/puppetlabs/apache 上的代码并阅读 examples 目录,以了解如何将主类 apache 与模块内的各种类结合起来配置不同组件,将帮助您理解如何结构化此模块模式。

角色和配置文件方法

在上一节中,我们讨论的模块是所谓的组件模块,因为它们覆盖单一实现。这些模块主要引起那些直接参与技术实现的用户的兴趣,例如 Unix 或 Windows 管理员,在他们看来,理解哪些特定资源已被应用是配置中最重要的方面。但不同的用户并不关心节点如何配置;他们关心的是它的作用。例如,一个应用程序专家关心的是,Tomcat 和 MySQL 已为他们的应用程序安装,而不关心如何配置。项目经理关心的是他们获得了一台满足业务需求的服务器,而不关心使用了哪些技术栈。项目经理也可能认为每个实现都是独特的,但通常会有很多相似之处,例如在多个应用程序中使用 Apache 或 Java 的不同技术栈,并根据位置或环境的不同而有所不同的设置。

如果没有为这些逻辑层次提供某种结构,将这些模块应用于节点将需要大量的重复和复杂的if语句。

一种名为角色和配置文件的模式采用模块化结构来实现这一点。角色和配置文件不是关键字,而只是模块和类中使用的模式。一个简单的方法是有一个名为role的模块和一个名为profile的模块,模块中的每个类表示一个角色或配置文件。

这些角色代表了客户(如项目经理)所需的业务需求,而配置文件则反映了应用程序的技术栈。

在将角色和配置文件方法应用于现有应用程序的配置时,重要的是从角色开始到模块结束,避免自然地首先寻求技术解决方案而忽视业务逻辑。这个过程涉及将事物拆解成组件,并且要具体思考它是什么,而不是它看起来像什么。识别角色的一个技巧是使用主机名,主机名通常包含关于位置、环境使用和应用程序的信息。例如,主机名可能看起来像fk1ora005prd,其中fk1是数据中心的位置,005是一个编号,prd是生产环境,而ora则是 Oracle 应用程序,它与角色的名称相匹配。因此,角色应该是业务名称,例如buildserverproxyserverecomwebserver,而配置文件应该是技术栈的名称,例如 Apache、Jenkins 或 nginx。

这种命名并不总是完美的,有时其中一些术语可能只是项目经理用来订购 Oracle 服务器的术语。他们可能没有意识到 Oracle 角色背后的配置文件,这些配置文件包括一个 Oracle 配置文件和其他相关的配置文件。

在这种情况下,role 类应简单地调用所需的配置文件,不带变量,如下所示:

class role::exampleapp {
  include profile::core
  include profile::java
  include profile::apache
}

相反,profile 类应包含参数,以定制模块和类声明,进而添加所需的模块:

class profile::java(
  Pattern[/present|installed|latest|^[.+_0-9a-zA-Z:~-]+$/] $java_version
  String $java_distribution
) {
class { 'java':
  version      => $java_version,
  distribution =>  $java_distribution,
  }
}

第九章 中,涉及到 Puppet 和数据,你将看到 Hiera 如何对覆盖配置文件默认值的数据建模。每个服务器应该只拥有一个角色;如果需要两个角色,那么它本身就是一个新角色,但一个角色会有多个配置文件。图 8.1 展示了使用角色、配置文件和模块的简单示例,以及这些类如何相互包含。在这个设置中,正如我们稍后在 第十一章 中看到的那样,对主机进行分类就像确保正确的角色被分配到节点一样简单:

图 8.1 – 角色、配置文件和模块的示例

图 8.1 – 角色、配置文件和模块的示例

上图中显示的框架完全是关于抽象的,因此我们将业务逻辑、实现和资源管理解耦,并减少节点级别的复杂性。

这种模式不是强制要求,而是提供了一些如何构建代码的建议,以避免重复并提供一种模型。在这种情况下,可以考虑几种适应性调整。

使用复杂度升级允许我们在最初代码较少时不创建过多结构。如果配置文件中只有少量资源,那么保持这种方式并在变得复杂时扩展到模块可能更容易。

根据你所在组织的变更管理和交付要求,可能有多个配置文件或角色模块,以便实现更细粒度的控制和访问——例如,teama_profilesteamb_profiles

正如在 第三章 中讨论的那样,一般不建议在 Puppet 代码中使用继承,但通过在目录中分组清单(例如,在 profile 中创建 exampleapp 目录,并创建 client.ppserver.pp 来表示服务器和客户端版本(分别为 profile::exampleapp::serverprofile::exampleapp::client))来扩展 profile 模块的命名空间可能是值得的。这也可以针对特定的操作系统进行。在考虑这种方法之前,请注意,这种结构是一个边缘情况,使用继承时风险较大。

如果发现配置文件在变化的技术栈中过于死板,或者采用遗留服务器意味着必须丢弃配置文件或角色的某些部分,那么使用参数使配置文件更具动态性,可以通过配置文件类的参数(无论是通过 Hiera 还是默认值)来定义类。

作为一个简单的示例,以下代码使用 include_classesexampleapp 模块中列出的默认值:

class profile::exampleapp(
  Array[String] $include_classes = ['exampleapp'],
) {
  include $include_classes
}

这样我们就可以覆盖 Hiera 或配置文件中的 include_classes 数组。通过只允许来自某个特定模块的类,我们可以使包含更加严格:

class profile::exampleapp(
  Array[String] $include_classes = ['server'],
 ) {
$modules = $include_classes.map | String $module | {
  "exampleteam_exampleapp::${profile}"
}
include $modules
}

为了为参数增加更多结构,并在审批和代码审核过程中使其更加清晰,可以进一步细分类参数。在这里,我们可以添加默认、必需、附加和剔除数组,从而提供完全的灵活性:

class profile::exampleapp(
  Array[String] $include_default          = ['my_default'],
  Array[String] $include_mandatory        = ['my_base_profile'],
  Array[String] $include_additional       = ['my_test_default_profile'],
  Array[String] $include_removal          = ['my_default'],
){
$profiles = $include_default + $include_mandatory + $include_additional + $include_removal
include $profiles
}

通过限制多个命名空间,并为每个命名空间创建类数组的列表,可以进一步混合这一模式。这将取决于采用什么方法,能够为组织提供足够的灵活性,同时明确代码会影响什么内容以及谁应进行审核。

通过这种方法,也许可以通过参数定义一个 noop 标志,并在资源上执行 noop 操作。你也可以通过 forge.puppet.com/modules/trlinkin/noop 中的 noop 函数来实现,让模块在被接受之前处于 noop 模式。

这些模式的调整更加复杂,需要读取 Hiera 数据以理解角色和配置文件所代表的含义,但最终的决定将取决于贵组织,选择哪种方法最适合。虽然通过使用严格的角色和配置文件来减少变化是理想的,但如果没有适当的管理方法,这可能会导致采用上的阻力或遗留问题。

在审视了角色和配置文件模式所能创建的模块结构及其内容后,我们可以看到,这需要大量的内容来手动管理,通过创建文件并管理各种测试工具。下一部分将讨论如何通过使用 PDK 来自动化模块创建和测试的生命周期。

使用 PDK 编写和测试模块

PDK 的引入旨在减少一致性地创建模块目录和文件的工作量,同时将一些常用的测试和验证工具组合在一起。我们将回顾 PDK 版本 2.7.1,这是写作时可用的最新版本。PDK 安装了自己的 Ruby gems 和环境,以提供以下工具:

Ruby Gem 名称 Ruby Gem 功能 项目页面
metadata-json-lint 验证语法并根据样式指南检查 metadata.json github.com/voxpupuli/metadata-json-lint
pdk 生成模块及模块内容,并使用自动化测试命令 github.com/puppetlabs/pdk
puppet-lint 根据 Puppet 语言样式指南检查 Puppet 清单代码 github.com/puppetlabs/puppet-lint
puppet-syntax 检查 Puppet 清单、模板和 Hiera YAML 的语法是否正确 github.com/voxpupuli/puppet-syntax
puppetlabs_spec_helper 提供必要的工具,以便在不同版本的 Puppet 中进行测试 github.com/puppetlabs/puppetlabs_spec_helper
rspec-puppet 编译 Puppet 代码并使用 Puppet 特定实现的 Ruby RSpec 测试预期行为 github.com/puppetlabs/rspec-puppet
Rspec-puppet-facts 提供一种方法,通过 facterdb 的输出为支持的操作系统提供事实数据 github.com/voxpupuli/rspec-puppet-facts
facterdb 提供不同操作系统和不同 Facter 版本的事实输出示例 github.com/voxpupuli/facterdb

表 8.1 – PDK gem 列表

关于 PDK 的常见误解是,它在打包和安装这些工具。实际上,它正在为每个创建的模块运行 bundle install。之后,PDK 缓存被保存,看起来像是 PDK 正在打包这些工具。

使用 表 8.1 中讨论的 gem,PDK 可以生成以下内容:

  • 包含完整模块骨架、元数据和 README 模板的模块

  • 类、定义类型、任务、自定义事实、函数和 Ruby 提供者

  • 类和定义类型的单元测试模板

PDK 会执行 lint 检查样式和最佳实践,并对以下内容进行语法验证:

  • metadata.json 文件;有关详细信息,请参见 表 8.2

  • 针对特定 Puppet 版本的 Puppet 清单文件(.pp

  • 针对特定 Puppet 版本的 Ruby 文件(.rb

  • EPP 和 ERB 模板文件

  • Puppetfileenvironment.conf,它提供了环境的模块列表及其环境设置,具体内容将在 第十一章 中讨论

  • YAML 文件

PDK 在模块和类上运行 RSpec 单元测试。有关详细信息,请参阅 使用 PDK 的 RSpec 测试 部分。

PDK 具有构建和发布命令,可以生成 .tar 文件,用于上传到 Puppet Forge 和 Puppet 调试控制台。

要创建一个模块,执行 pdk new module 命令(可选地在末尾加上模块名称)。回答关于模块名称的问题(如果没有提供模块名称,则指定你的 Puppet Forge 用户名,若有的话),模块的作者,代码应遵循的许可协议,以及支持的操作系统。此过程可以在以下截图中看到:

图 8.2 – pdk 新模块问题

图 8.2 – pdk 新模块问题

对用户详细信息和许可协议提供的答案将作为默认值,在以后运行时提供,并可以通过运行 pdk get config 并检查 user.module_defaults 设置来查看。

注意

在引入 PDK 之前使用的 puppet module generate 命令,在 Puppet 5 中已弃用,并在 Puppet 6 中被移除。

一旦输入并确认了答案,将创建一个包含模块名称的目录。该目录将包含先前在 模块是什么及其内容 部分中讨论的以下内容目录:

  • data

  • examples

  • files

  • Manifests

  • spec

  • tasks

  • templates

使用默认的内建模板后,它将创建以下额外的配置文件和目录:

文件/目录名称 文件/目录用途
appveyor.yml Appveyor CI 集成配置文件
CHANGELOG.md 可维护的变更日志
.``devcontainer 配置容器以测试此模块的方式
.``fixtures.yml 测试模块依赖配置
Gemfile Ruby gem 依赖
Gemfile.lock Ruby gem 依赖
.``gitattributes 将属性和行为与文件类型关联
.``gitignore Git 应忽略的文件
.``gitlab-ci.yml 用于 GitLab CI 的示例配置
metadata.json
  • 创建时填写的元数据,包括问题回答

|

.``pdkignore
  • 用于构建 Puppet Forge 包时忽略的文件

|

.``puppet-lint.rc puppet-lint gem 的配置
Rakefile Ruby 任务配置
README.md 模块的 README 页模板
.``rspec rspec 单元测试的配置默认值
.``rubocop.yml Ruby 风格检查设置
/``spec 包含 rspec 单元测试文件的目录
/``spec/default_facts.yaml 所有测试都可用的默认事实
/``spec/spec_helper.rb rspec 的入口脚本,设置各种配置
.``sync.yml 自定义正在使用的 PDK 模板的文件
.``vscode VSCode 配置,例如推荐的扩展
.``yardopts Puppet Strings 配置文件

表 8.2 – PDK 默认模板文件和目录

对于已有的模块,也可以运行 pdk convert 将模块适配到模板中。它会在应用更改之前确认将要做出的修改。

PDK 内容的大小随着时间的推移而增长,默认模板可能包含许多未使用的文件。可以通过从 github.com/puppetlabs/pdk-templates 进行 fork 并按照 README 文件的指导调整模板,来创建自定义模板。然后,可以通过在新模块或转换命令中使用 --template-url 来使用该模板。此外,.sync.yml 文件可以设置为删除、不受管理或更改设置。以下的 .sync.yml 文件示例将设置 .gitlab-ci.yml 文件,使其不包含在模块中。它将确保 .vscode 目录不受 PDK 模板管理,从而避免将来的更新。它还将禁用旧版事实(事实的全局变量,详细介绍见 第五章):

common
  disable_legacy_facts: true
.gitlab-ci.yml
  delete: true
.vscode
  Unmanaged: true

可以调整的完整设置已在 PDK 模板的 README 文件中进行了文档化:github.com/puppetlabs/pdk-templates/blob/main/README.md

注意

如果需要在多个现有模块中进行配置更改,可以使用 modulesync 模块来管理此操作。它可以通过以下网页获得:github.com/voxpupuli/modulesync

现在我们已经详细描述了 PDK 模块及其工具的内容,接下来我们将描述开发模块的工作流程,如 图 8.3 所示。

¶¶¶

图 8.3 – PDK 工作流程

如前所述,通过运行 pdk new 创建新模块或在未受控模块上运行 pdk convert,可以建立一个初始的 PDK 内容模块及其设置。使用 pdk update,可以更新模块的配置,因为我们可以更改其设置、提供新的模板或更改 PDK 版本。

下一步是添加任何需要的新内容文件。这可能包括 classdefined_type、fact、函数提供者、任务或传输,可以通过使用 pdk new 命令和相关内容来完成。这将使用所选内容的模板和一个 rspec 测试文件创建一个文件。对于 PDK 无法提供的内容,例如外部 facts 或计划,必须手动创建文件和测试。

一旦文件和内容测试就位,应该添加你的代码。可以通过定期运行pdk validate命令来验证和测试代码,这个命令会检查代码的代码风格和语法解析。此命令也可以与-a标志一起使用,该标志会尝试自动修正任何错误。对于代码风格错误,可以通过使用内联注释或通过在代码区域周围加注释lint:ignore:<rule name>来忽略文件某部分的特定检查。以下示例展示了如何设置某一行忽略 140 字符的代码风格规则。在这种情况下,该段代码会忽略双引号检查,双引号只应在同时使用字符串和变量进行变量赋值时使用:

$long_variable_text = "Pretend this is more than 140 characters" # lint:ignore:140chars
  # lint:ignore:double_quoted_strings
  $variable1 = "don't do this"
  $variable2 = "this is just a simple example"
  # lint:endignore

如果必须在所有代码中忽略该检查,可以通过添加类似--no-selector_inside_resource-check的标志更新.puppet-lint.rc文件,以确保puppet-lint不执行检查,确保选择器代码位于资源内部。可以在github.com/puppetlabs/puppet-lint/tree/gh-pages/checks查看puppet-lint检查的完整列表。请注意,尽可能避免禁用检查,因为这可能会影响模块评分或影响模块是否能在 Puppet Forge 获得认证。这会使你的代码远离推荐的 Puppet 实践。

注意

puppet-lint.com/checks/ 不是 Puppet 公司所有,并且已经过时。Puppet 将该模块分支到github.com/puppetlabs/puppet-lint,因此建议使用puppetlabs.github.io/puppet-lint

一旦pdk validate成功运行,可以执行pdk test unit命令来进行单元测试。模板提供的检查是基础性的,旨在检查代码是否能正常工作;对于 Puppet 代码,它确保代码能够编译。一个强大的功能是,通过使用--puppet-version标志或--pe-version,检查可以针对特定的 Puppet 或 Puppet Enterprise 版本进行运行——例如,pdk test unit –pe-version=2019.11——这样可以在升级之前进行代码测试。在下一部分,你将学习如何进一步扩展rspec检查。

Puppet Forge 将在本章最后详细讨论。本书不涉及发布到 Puppet Forge 的内容,但如果要将代码发布到 Puppet Forge 供使用,可以运行pdk build命令来创建.tar文件进行上传,或者运行pdk release命令来自动化上传模块到 Puppet Forge 的过程。

保持 metadata.json 文件的最新状态非常重要,因为它将限制根据 Puppet 支持的版本进行的测试,并且是文档的重要部分。可以在 docs.puppet.com/puppet/latest/modules_metadata.html 查看其格式及选项。

若要查看所有可用的选项,您可以在 puppet.com/docs/pdk/2.x/pdk_reference.html 中查阅完整的 PDK 命令参考。

在回顾了如何使用 PDK 创建和管理模块,以及其验证和测试功能后,我们来学习如何使用 RSpec 进行完整的单元测试。

使用 PDK 进行 RSpec 测试

为了在单元测试层面进一步进行初步验证和编译测试,RSpec 可用于测试模块的行为和逻辑,而 ServerSpec 可用于进行系统集成测试。

RSpec 是一个用于测试 Ruby 代码的 Ruby 框架,rspec-puppet 测试是 RSpec 的一个实现,专门用于测试 Puppet 模块。

注意

需要注意的是,当前项目代码可以在 github.com/puppetlabs/rspec-puppet 中找到,它是从 github.com/rodjek/rspec-puppet 派生的,核心指南和文档可以在 rspec-puppet.com/ 找到。

当用户开始使用 RSpec 时,有些人可能会觉得它只是用不同的语言模仿 Puppet 代码。RSpect 会运行 Puppet 代码中的不同逻辑和行为,确保在各种环境和情况下会产生正确的目录和输出。这可以防止在重构或升级到新版本的 Puppet 时出现回归。如果 RSpec 代码只是简单地模仿清单中的代码,那么测试场景就没有得到正确的审查。

这种单元测试风格的优点在于,它允许你在不启动任何特定基础设施或进行任何更改的情况下测试代码。

RSpec 测试包含在 spec 目录下的 Ruby 文件中的模块里,目录中包含了针对不同类型代码的测试,例如 spec/classes 目录中的类,和 spec/defines 目录中的定义类型。

我们将忽略其他可能的测试目录(如 typestype_aliasfunctions 测试目录),因为创建它们超出了本书的范围。然而,这里讨论的大部分内容可以应用于这些类型。

RSpec 配置包含在 PDK 中,文件会通过 pdk new 命令自动创建。但是,当转换模块或使用 PDK 时,可以通过向 convert 命令添加 --add-tests 标志 (pdk convert --add-tests),以及使用 pdk new test --unit <name> 命令分别进行添加。

在我们查看 PDK 默认为定义类型和类提供的内容之前,我们必须在exampleapp模块上运行pdk new class exampleapppdk new define example_define命令,以创建主清单和一个名为example_define的定义类型。这样会生成一个名为spec/classes/exampleapp.rb的文件,内容如下:

require 'spec_helper'
describe 'exampleapp' do
  on_supported_os.each do |os, os_facts|
    context "on #{os}" do
      let(:facts) { os_facts }
      it { is_expected.to compile }
    end
  end
end

此外,spec/defined/example_define.rb可以如下创建:

require 'spec_helper'
describe 'exampleapp::example_define' do
  let(:title) { 'namevar' }
  let(:params) do
    {}
  end
  on_supported_os.each do |os, os_facts|
    context "on #{os}" do
      let(:facts) { os_facts }
      it { is_expected.to compile }
    end
  end
end

分解这个过程,第一步是require spec_helper,它会加载spec/spec_helper.rb文件。由于spec目录会自动加载到路径中,因此只需要声明标题;这将配置 RSpec,稍后将在本节中详细讨论。接下来的部分是describe,它是 RSpec 中的一个关键字,用于描述一组测试。对于exampleappexample_define测试,描述的是类和定义类的名称,因为每个测试组只有一个基本的测试组。

注意

如果你以前使用过puppet-rspec,你可能在describe语句中设置了额外的类型定义,比如describe 'exampleapp', :type => :class do。由于文件夹本身充当了类型的自动标识符,这一步是多余的。

定义类型始终需要一个标题和任何参数。使用let关键字时,设置了标题以及参数,在这种情况下,参数为空。

然后,exampleappexample_define类使用on_supported_os函数进行循环,该函数由rspec-puppet-facts宝石提供,输入来自metadata.json文件,该文件包含有关支持的操作系统的详细信息,并生成一个存储在os_facts变量中的事实数组。接着,这些事实被传递到另一个let中,将这些事实赋值给os_facts数组的内容。

it关键字是一个 RSpec 术语,可以是单行的,也可以包含在doend块内。它是一个测试用例,包含一个叫做is_expected.to的期望语句,这是对某个条件的验证步骤。这个条件通过匹配器来表达。在这种情况下,它将编译类和定义类型的 Puppet 代码,并确认会成功生成目录。

注意

我们推荐使用www.betterspecs.org/上的样式指南,它适用于通用的 Ruby RSpec 风格。我们将在本章中引用其中的建议。

简要查看了默认的编译测试后,让我们看看每个组件以及如何进一步扩展它们。

describecontext关键字

对于许多曾尝试使用 RSpec 的 Puppet 开发者来说,一个大的困惑是理解何时使用describe关键字,何时使用context。它们看起来是可以互换的,这有充分的理由。context关键字是describe的别名,所以它们是可以互换的,你的使用仅影响代码的可读性。

Betterspecs建议使用describe来描述正在测试的方法。在 Puppet RSpec 中,这也是我们在使用 PDK 进行 RSpec 测试部分看到describe与类名exampleapp以及其定义类exampleapp::example_define的原因。

推荐将context写成何时不与等情境方式,这样可以明确说明正在测试的场景。

本书的风格推荐是写一个单独的describe来匹配 Puppet 类型,比如class,然后使用context来匹配要测试的场景。

describecontext的块允许描述正在测试的情况,并设置事实、变量和参数。由于它们可以嵌套,因此可以实现继承,这将构建更详细的场景,或尝试不同的逻辑路径,但需要注意不要使这些案例过于难以阅读。

目标应该是测试所有情况。因此,应该计划测试有效、边缘和无效情况,允许正面和负面情况都能被测试。作为一个简单的示例,在没有任何代码测试或参数设置的情况下,以下exampleapp类的代码将根据install版本是否为中间值、低边缘版本或无效版本来查看每个受支持操作系统的上下文:

describe 'exampleapp' do
  on_supported_os.each do |os, _os_facts|
    context "on #{os}" do
      context 'When install_version is 6' do
        it { is_expected.to compile }
      end
      context 'When install_version is 1' do
        it { is_expected.to compile.and_raise_error('unsupported version') }
      end
      context 'When install_version is invalid string' do
        it { is_expected.to compile.and_raise_error('Invalid version string') }
      end
    end
  end
end

现在我们已经有了测试场景的基本结构,下一步是使用匹配器来测试根据context在目录中生成的内容。

示例、期望和匹配器

示例it语句可以是单行的,如在使用 PDK 进行 RSpec 测试部分所示,或者当所使用的匹配器太长,无法放在一行时,可以分成多行。使用doend,相同的编译示例可以表示如下:

it do
is_expected.to compile
end

在一般的 Ruby RSpec 实现中,期望有更广泛的选择,但在puppet-rspec中,我们的期望将仅限于使用is_expected关键字。不过,可以通过使用not_to来否定它——例如,It { is_expected.not_to }

匹配器提供了多种测试,用于测试不同的资源类型。匹配器的语法为contain_<resource_type>('<title>').<options>

对于编译匹配器,我们可以通过添加with_all_deps选项来更明确地进行编译测试——例如,it { is_expected.to compile.with_all_deps }。这将测试目录中所有关系是否包含资源。或者,我们可以通过使用and_raise_error('error_message')选项来查找编译错误,这将包含我们期望抛出的消息作为字符串——例如,it { is_expected.to compile.raise_error('lets cause failure' }

主要的匹配集合是基于资源类型,使用contain_<resource_type>('<resource_title>')模式——例如,

it { is_expected.to contain_class('exampleclass::install') }it { is_expected.to contain_service('httpd') }

Rspec-puppet不会进行类名解析或查找,因此匹配器只接受没有前导冒号的限定类。因此,installexampleclass中找不到,但exampleclass::install可以找到。如果资源类型包含::符号,则需要将其转换为__符号,这样它将变成contain_exampleapp__exampletype

资源匹配器可以通过使用withonly_withwithout方法进一步扩展。这使我们能够检查资源的参数;with确保目录中的资源具有指定的参数,only_with确保仅设置提供的参数而没有其他参数,without接受一个参数数组并确保这些参数没有被设置。在使用这些方法时,使用it do...end格式更具可读性,以下是一个示例:

it do
  is_expected.to contain_package('httpd').with(
  'ensure' => 'latest',
  'provider' => 'solaris',
  )
end

通过遵循withonly_with的方法语法,这可以简化为仅一个参数:

<with_method>_<parameter name>
it {is_expected.to contain_server('exampleserver').only_with_enable(true)  }

对于without,该方法接受一个不应设置在资源上的参数数组:

it {is_expected.to contain_user('exampleuser'). .without(['managehome', 'home']) }

这些方法可以通过链式调用的方式组合在一起,无论是作为相同的方法,还是作为混合方法:

it {is_expected.to contain_server('exampleserver').with_enable(true).without_ensure  }

另一种资源匹配器是使用count,它允许使用have_<resource_type>_count语法。例如,要验证资源的总数是否为5,类的总数是否为4,可以运行以下代码:

it { is_expected.to have_resource_count(5) }
it { is_expected.to have_class_count(4) }

在回顾了如何设置示例后,显然对于describecontext关键字,必须设置参数和前置条件才能形成测试场景。例如,如果上下文是安装版本为 1,则需要将安装版本参数设置为 1。

参数和前置条件

在定义类型的默认示例中,我们解释了如何使用let关键字来指定定义类型的测试实例的标题和空参数。然而,这些也可以用于其他类型,如带参数的类。

要填充参数,可以提供一个由=>符号分隔的键值对数组,在字符串中未定义的值声明为:undef,在编译测试时会转换为undef。例如,要将param1设置为yup字符串,并将param2设置为undef,可以使用以下let

let(:params) { {'param1' => 'yup', 'param2' => :undef } }

除了参数外,还可以设置前置条件。因此,如果正在测试的清单依赖于目录中存在另一个类或变量,则可以将其添加,以便在测试类之前进行评估。例如,在模块模式中,我们展示了config类需要在install类之后但在service类之前在目录中进行评估。这可以通过以下代码完成:

let(:pre_condition) { 'include exampleapp::install' }
let(:post_condition){ 'include exampleapp::service' }

如果有多个条件,也可以使用字符串数组。如果测试针对特定的节点或环境,可以按如下方式设置:

let(:node) { puppet.packtpub.com' }
let(:environment) { 'production' }

节点应该是完全合格的域名FQDN)。

关系

资源之间的关系可以通过that_requiresthat_comes_beforethat_notifiesthat_subscribes_to方法进行测试。无论 Puppet 代码使用require,还是 RSpec 使用that_comes_before,或者 Puppet 代码使用方向箭头,只要这些变体在逻辑上是等价的,都不重要,因为测试是在目录上进行的。

这些方法被链式调用到示例中与需求一起,但是在 Puppet 清单中声明关系与在rspec测试中声明关系之间存在一些区别:名称不应加引号,不能在单一类型下有多个资源名称,如果引用了类,则不应使用前导::标记它为顶级作用域。举个简单的例子,一个名为exampleconfig的文件要求exampleapp包,可以通过以下方式进行检查:

it { is_expected.to contain_file('exampleconfig').that_requires('Package[exampleapp]') }

要检查exampleapp包是否在exampleapp::serviceexampleapp::config类之前,可以传递一个数组。然而,请注意,它们不能在一个类下:

it { is_expected.to contain_package('exampleapp').that_comes_before('Class[exampleapp::service]','Class[exampleapp::config]') }

使用it do...end的带参数资源示例如下,其中通知了两个文件:

it do
  is_expected.to contain_service('anotherapp').with(
    'ensure' => 'running',
    'enable' => 'true',
  ).that_notifies('File[config_a]', 'File[config_b]')
end

如果测试的是类似定义类的内容,并且它的定义中包含requirebefore,则可以在参数中设置此关系。然而,必须使用ref辅助函数来命名它所依赖的资源,使用ref('<type>','<title>')语法。对于一个需要exampleapp包的定义类型,以下代码会通过参数添加该关系:

let(:params) { 'require' => ref('Package', 'exampleapp') }

来自 Hiera 和事实的数据

来自 Hiera 和事实的数据对我们代码中的逻辑有很大的影响,因此必须能够提供并自定义,以覆盖不同的测试场景。如在使用 PDK 进行 RSpec 测试章节中的默认示例所示,rspec-puppet-facts gem 会检查metadata.json文件以查找支持的操作系统列表。然而,metadata.json并没有提供架构的方式,默认情况下,rspec-puppet-facts会根据操作系统选择一个默认架构,例如 Solaris 的 i86PC 或 Fedora 的 x86_64。如果您希望能够检查其他架构,可以通过逗号分隔的数组传递硬件模型。这样将与以下代码结合使用:

  additional_archs = {
    :hardwaremodels => ['i386'],
  }
on_supported_os(additional_archs).each do |os, os_facts|

如果只需要测试一个子集,例如专为某个操作系统制作的类,则可以通过operatingsystemoperatingsystemreleases参数传递相关的详细信息;这将覆盖metadata.json

    ubuntu = {
      supported_os: [
        {
          'operatingsystem'        => 'Ubuntu',
          'operatingsystemrelease' => ['18.04', '16.04'],
        },
      ],
    }
on_supported_os(ubuntu).each do |os, os_facts|

使用on_supported_os方法时,这只能在所有选择上进行设置。如果没有找到任何内容,例如 Windows 11 上的 i386,它将默默地失败。查看facterdb模块github.com/voxpupuli/facterdb以查看可用的内容。

使用on_supported_os并不是强制性的,但如果没有它,默认情况下将不会有任何事实。当你需要测试facterdb中不存在的数据时,可以通过let(:facts)声明事实以及你想要的值。例如,如果你要测试一个理论上的 RedHat 10 事实集,你可以使用以下代码:

Context "when OS is redhat-10-x86_64" do
    let(:facts) do
      {
        :osfamily                  => 'RedHat',
        :operatingsystem           => 'RedHat',
        :operatingsystemmajrelease => '10',
        …
      }
    end

同样,如果要在嵌套的context中向os_facts变量添加额外的事实,可以使用merge方法与super方法:

     let(:facts) do
        super().merge({
          :student => 'david',
        })
      end

注意

对于结构化事实,这些合并可能变得更加复杂。Voxpupli 在github.com/voxpupuli/voxpupuli-test中提供了一个override_facts助手,可以帮助解决这个问题。

要添加可以被 PDK 用于验证和测试代码的事实,请添加一个spec/default_module_facts.yml文件。这个文件将包含类似下面的 YAML 内容:

---,
choco_install_path: C:\ProgramData\chocolatey
chocolateyversion: 0.9.9

default_facts.yml文件不应被编辑,因为它由 PDK 管理,并提供 PDK 运行所需的最小事实。

通过.sync.yaml添加默认事实是可能的,方法是添加标准代码块或default_facts.yml,但与default_module_facts.yml相比,这种方式不必要地复杂。

在 spec 中通过let(:facts)提供的任何事实将会覆盖默认事实。

除了这些事实外,还有三个额外的变量来自分类和外部数据源:节点参数,这是从分类中分配给节点的全局变量;可信事实,这是从 Puppet 客户端证书中分配的变量;以及可信外部事实,这是由脚本从外部数据源获取的变量。这些的完整实现将在第十一章第十四章中详细描述。

可以通过在 spec 文件中使用let语句或在spec_helper中设置为默认值来添加所有三种类型的变量。

从 Puppet 4.3 版本开始,可信事实将包含基于节点名称(通过:node设置)填充的可信事实键(certnamedomainhostname)。然而,可信外部事实和节点参数将为空。

可信事实使用trusted_facts,可信外部数据使用trusted_external_data,节点参数使用node_params。例如,要声明可信事实和可信外部数据,可以使用以下let语句:

let(:trusted_facts) { {'pp_role' => 'puppet/server', 'pp_cluster' =>
'A'} }
let(:trusted_external_data) do,
{ 
  pds: {
     puppet_classes: some_class,
     example: hiera_data,
   },
}
end

要设置默认值,.sync.yaml 可以通过传递一个数组给 spec_overrides 来添加额外的行;然而,添加一个包含必要行的 spec_helper_local.rb 文件会比遵循 YAML 语法更为简便。在 Rspec.config 块中,需要按照 c.<fact_type> = {<fact/parameters_keys>} 格式,并使用带有 default_ 前缀的 fact/parameter 名称。因此,要将节点参数指定为默认值,可以按如下方式更新 spec_helper_local.rb

RSpec.configure do |c|
  c.default_node_params = {
    'owner'  => 'oracle',
    'site'   => 'Falkirk1',
    'state' => 'live',
  }
end

同样,可信的外部数据也可以像这样设置:

Rspec.configure do |c|
  c.default_trusted_external_data = {
    pds: {
      puppet_classes: some_class,
      example: hiera_data,
    },
  }
end

Hiera 会在 第九章 中详细介绍,但现在知道 Hiera 提供一个 hiera.yaml 文件来帮助你学习如何查找数据和配置文件就足够了。我们在 spec/fixtures/hiera/hiera.yaml 中创建了一个 hiera.yaml 定义,通常会在 spec/fixtures/hieradata 中定义一个 datadir

Hiera 的配置可以通过两种方式进行设置,具体文档可以参考 github.com/puppetlabs/rspec-puppet。第一种方式是使用 let 并设置必要的变量,如下所示:

let(:hiera_config) { 'spec/fixtures/hiera/hiera.yaml' }
hiera = Hiera.new(:config => 'spec/fixtures/hiera/hiera.yaml')

查找操作可以按如下方式进行:

 primary_dns = hiera.lookup('primary_dns', nil, nil)
  let(:params) { 'primary_dns' => primary_dns}

或者,可以将以下内容添加到 spec_helper_local.rb 中。这里,参数的自动查找将会发生:

RSpec.configure do |c|
  c.hiera_config = 'spec/fixtures/hiera/hiera.yaml'
end

在了解如何为单独的模块创建测试后,你会很快发现,模块内使用了各种资源,例如函数,而这些资源依赖于其他模块的内容。在接下来的章节中,你将学习如何使用 fixtures 使这些内容可用于测试。

使用 fixtures 管理依赖关系

puppetlabs_spec_helper 可以将依赖的模块放在 spec/fixtures/modules 目录下,供运行 RSpec 测试单元时使用。.fixtures.yml 文件可以指定 GitHub 仓库源的 repositories: 和 Puppet Forge 模块的 forge_modules:

主要的参数是 repo,它可以是 Git 仓库链接或 Puppet Forge 模块名,ref 表示 Git 提交 ID,或 Forge 模块版本号,branch 是 Git 分支名。refbranch 参数可以一起使用来修改分支。

所以,包含两个 Git 仓库和两个 Forge 模块的 .fixtures.yml 示例文件如下:

fixtures:
  forge_modules:
    peadm: "puppetlabs/peadm"
    stdlib:
      repo: "puppetlabs/stdlib"
      ref: "2.6.0"
  repository:
     pecdm:  "git://github.com/puppetlabs/pecdm"
   Puppet-data-service:
      repo: "git://github.com/puppetlabs/puppetlabs-puppet_data_service"
      Ref:  "feature_branch_1"

如果除了 repo 之外没有其他参数,则可以简化为一行,如此所示。如果 fixtures 文件发生了变化,可以使用 --clean-fixtures 标志和 pdk test unit 命令来确保所有内容被删除。

更多的标志和选项可以与 fixtures 一起使用,具体文档请参考 github.com/puppetlabs/puppetlabs_spec_helper#fixtures-examples

覆盖率报告

可以通过将以下代码添加到 spec_helper_local.rb 来生成覆盖率报告:

RSpec.configure do |c|
  c.after(:suite) do
    RSpec::Puppet::Coverage.report!
  end
end

该工具检查 Puppet 资源是否被覆盖,并生成覆盖的资源百分比和未覆盖资源的列表。被检查的资源必须位于正在测试的模块内,并且不能包含任何由 fixtures 引入的依赖。资源覆盖的百分比也可以通过在括号中添加通过率来设定为通过或失败的标准。例如,通过将代码行更新为 RSpec::Puppet::Coverage.report! (100),可以确保每个资源(100%)都被覆盖。这有时可以作为推动 RSpec 使用和覆盖的动力,且只有在出现特定问题或例外时,资源覆盖百分比才会减少。

进一步的 RSpec 研究和工具

本节旨在为你提供足够的信息,以便你能够使用事实数据和依赖关系构建有意义的 rspec-puppet 测试。同时,请注意,可以使用普通的 Ruby 代码,如 caseif 语句和变量,并且在 spec_helper_local 中还有许多更多高级配置选项,相关文档请见:rspec-puppet.com/documentation/configuration/

本书不建议使用 Augeas,但实际上可以在 RSpec 中测试 Augeas。详细信息请参阅:github.com/domcleal/rspec-puppet-augeas

尽管这超出了本书的范围,但在使用自定义函数和类型时,必须执行存根和模拟,这可以通过 rspec-mocks 完成,相关文档可见:github.com/puppetlabs/puppetlabs_spec_helper#mock_with

使用 PDK 进行 RSpec 测试部分的开头提到,对于大型清单,必须为资源编写所有 RSpec 代码可能会很痛苦。然而,有几个工具可以为你完成这项工作。这些工具包括 github.com/logicminds/puppet-retrospecgithub.com/enterprisemodules/puppet-catalog_rspecgithub.com/alexharv074/create_specs.git;这些工具都可以从代码或清单生成 RSpec。

几乎所有任务也可以通过使用 rspec-puppet-yaml gem 在 YAML 中完成,相关文档可见:rubydoc.info/gems/rspec-puppet-yaml。然而,我们强烈不推荐这样做。

对于进一步的 RSpec 研究,查看核心 RSpec 文档可能会很有帮助,网址是:rspec.info/documentation/

Serverspec

Serverspec 是一个 RSpec 实现,用于在配置管理部署完成后进行服务器级别的测试。它是一个独立于 Puppet 的工具,且不与 PDK 集成;通常,它会被添加到流水线工具中运行,并需要你从服务器远程连接到测试目标。许多我们在 RSpec 中看到的相同原理和理念仍然适用。相关的文档和教程可以在 serverspec.org/ 找到。

在本章了解了如何创建和测试模块后,我们现在可以看看如何使用 Puppet Forge 获取预编写的模块。

理解 Puppet Forge

Puppet Forge 提供了一个丰富的资源库,包括 Puppet、Puppet 社区和第三方供应商提供的模块,旨在减少你组织必须编写和维护的代码量。它还允许你为项目做出贡献或发布模块,进而让他人也能为你的项目做贡献。

理解 Puppet Forge 中不同类型的作者、认可和质量评分非常重要,这有助于你了解是谁在开发模块、可以期待什么样的模块,以及如何在 7000 多个模块中做出选择。

任何人都可以注册并发布模块。然而,Puppet 公司本身是通过 puppetlabs 用户名发布的,而 puppet 用户名则是由 Vox Pupuli 社区发布的。这种混淆源于 Puppet 最初被称为 Puppet Labs。尽管如此,这并不影响 Vox Pupuli 社区在 Puppet 的高标准开发以及与 Puppet 的紧密合作,两个组织在相互贡献。有关 Vox Pupuli 社区的详细信息可以访问 voxpupuli.org/,包括如何贡献和参与其中。

还有一些其他重要的咨询贡献者,比如 example42enterprisemodulescamptocampbetadots,他们贡献了模块并提供服务。还有一些供应商组织,如 foremandatadogSIMPcyberarkElastic,它们提供与自己产品相关的模块。最后,像 sazghoneycut 这样的个人贡献者也贡献了多个高质量的模块。Puppet 有一个 Champions 计划,专门突出介绍 Puppet 知名贡献者,这有助于了解模块作者的可靠性:puppet-champions.github.io/profiles.html

注意

将模块发布到 Puppet Forge 的过程超出了本书的范围,但可以通过访问 puppet.com/docs/puppet/latest/modules_publishing.html 进行查看,并配合 pdk buildpdk release 命令使用,如在 使用 PDK 编写和测试模块 部分中所讨论。

在了解如何在查看如图 8.4所示的屏幕时筛选使用模块时,我们有多种选择,该屏幕允许我们搜索 Puppet Forge 中所有可用的模块:

图 8.4 – Puppet Forge 搜索屏幕

图 8.4 – Puppet Forge 搜索屏幕

最直接的有用过滤器是metadata.json文件,用于操作系统和 Puppet 版本兼容性。发布日期、最新版本和下载次数是衡量模块是否常用以及是否保持更新的关键指标。

Puppet 实施了一种认可方案,由内容与工具团队CAT)管理,分为三种类型:合作伙伴、批准和支持。

批准的模块通过了forge.puppet.com/about/approved/criteria中记录的具体标准,这些标准确保模块符合可用性和质量标准。这可以帮助你在选择可靠模块时,或让你的团队以标准为目标,并通过github.com/puppetlabs/puppet-approved-modules提交模块。

支持的模块遵循与批准模块相同的标准,但由 Puppet 或 Puppet 批准的第三方供应商完全支持,允许 Puppet Enterprise 客户在遇到问题时提交支持案例。请注意,只有模块的最新版本才会得到支持,且 Puppet Enterprise 操作系统版本在生命周期结束后的支持窗口有限。详细信息可以在forge.puppet.com/about/supported查看。

第三种合作伙伴类型是由非 Puppet 提供支持和测试。但为了使此支持有效,可能需要单独的合作伙伴许可方案。

除了这种认可方法,每个 Puppet 模块都会有一个评分。自从评分机制最后一次更新以来,详细信息尚未完全公开,评分的细分也不可见,但模块质量评分基于代码风格检查、兼容性测试和元数据验证。这个评分可以帮助你了解模块在运行anubis-docker评估时是否符合 Puppet 代码标准。github.com/puppetlabs/anubis-docker

恶意软件扫描于 2021 年引入,使用 VirtusTotal。puppetlabs用户模块上可以看到模块的通过或失败情况,但这将在稍后的日期扩展到已批准、合作伙伴以及所有未来模块版本。

随着新实现的推出,或者因为用例不再有效而不再得到支持,模块可能会被弃用。这些模块默认会被隐藏,但可以通过选择显示****已弃用选项使其可见。

最近发布的 Puppet Comply 产品新增了高级模块,但它们目前仅适用于cem_windowscem_linux模块,这些模块只有在购买 Puppet Comply 后才能使用。

由于历史发展重点在 Linux 平台,Puppet 在 Windows 平台上的支持曾一度被忽视。Puppet Forge 提供了一个集合页面(forge.puppet.com/collections/windows),该页面展示了为 Windows 设计的模块,例如 Chocolatey 包提供程序:forge.puppet.com/modules/puppetlabs/chocolatey。另一个重要的发展是自动生成 PowerShell xInternetExplorerHomePage,用于设置 Internet Explorer 的首页,以及如xActiveDirectory等模块,用于部署和配置 Active Directory。xInternetExplorerHomePage非常简单,只有一个名为dsc_xinternetexplorerhomepage的资源类型,可以用来设置默认首页,如下所示:

dsc_xinternetexplorerhomepage { 'set home page':
  dsc_startpage => 'https://www.packtpub.com'
}

xActiveDirectory具有多种资源类型,用于配置和部署 Active Directory 的不同方面。

由于这是完全自动化的转换,而且 Puppet 并不拥有 DSC 代码,因此存在一定限制。这使得测试受到限制,并且依赖于 DSC 代码所有者提供的代码和文档质量。你还可能会发现一些模块在 PowerShell Gallery 中已被弃用,因此值得检查。此外,由于minitar中的一个 bug,只有 Puppet Enterprise 代码管理器才能正确地从 Puppet Forge 直接解压这些模块。对于开源用户,请参阅模块文档说明,了解如何从 Puppet Forge 的 Web 链接下载模块并手动解压归档文件,确保模块已安装并且 DSC 代码完全解压。

还有一些进一步的博客和工具需要关注,虽然它们超出了本书的范围,但值得调查以获取更多信息。为了跟上 Puppet Forge 和 Puppet 管理模块的最新动态,CAT 团队运行了一个博客:puppetlabs.github.io/content-and-tooling-team/。Puppet Forge 还提供了一个 API,地址为forgeapi.puppet.com/,可以执行更多的编程查询,而 Ben Ford 开发的denmark模块提供了额外的扫描和检查,帮助审查模块:github.com/binford2k/denmark

实验—创建并测试一个模块

在这个实验中,你将运用你所学到的关于模块结构、PDK 和测试的知识,创建并测试一个 Grafana 模块。然后,利用你对 Puppet Forge 的了解,你将探索 Forge 网站并选择模块:

查看建议答案,链接地址为 github.com/PacktPublishing/Puppet-8-for-DevOps-Engineers/blob/main/ch08/module_choice.txt

总结

在本章中,您学习了模块如何帮助您将代码和数据进行分组,从而更容易共享和重用代码。我们讨论了模块应该专注于清晰的单一用途责任。我们检查了模块的目录结构,并重点介绍了存储特定 Puppet 代码和数据的位置。展示了一个好的入门清单结构,重点介绍了用作入口点的主清单(init.pp),并通过参数像公共 API 一样使模块灵活,能够包含其他所需的类。我们还看到了 install.ppconfig.ppservice.pp 类,分别聚焦于安装、配置和服务。在应用程序变得比这更复杂的情况下,我们讨论了模块如何使用类和目录来处理不同的组件。

接下来,我们了解了 PDK 如何作为一种自动化模块创建的工具,并将常用的工具集成在一起,帮助我们管理和测试 Puppet 模块。我们创建了一个 Ruby 环境,并在模块目录中安装了社区最常用的开发工具和配置文件。我们审查了生成模块的默认模板,以及如何通过在 sync.yaml 上进行分支来定制它。

接着,我们了解了在使用各种 PDK 命令创建或转换模块时,开发生命周期的过程,以及添加不同的 Puppet 类型,如类或定义类型,这些都用于创建单元测试。我们还查看了 pdk validate 命令,它用于执行代码检查和语法验证,并通过 -a 标志在可能的情况下进行自动修正。模板创建了基本的 RSpec 测试,用于检查目录的编译。PDK 的 buildrelease 命令也被提及,作为将 PDK 打包到 Puppet Forge 中或将其作为一个命令打包并上传的方法——release

接下来,你学习了如何使用 describecontext 扩展 RSpec,以构建测试用例和期望,并使用匹配器来定义单个测试。你了解到,可以通过 let 语句设置前置条件,从而在测试中创建类的依赖关系。你还学习了如何通过链式调用定义关系。你看到,let 语句可以用于在数据中定义事实、节点数据、受信任的事实和受信任的外部事实,并且通过使用 default_module_facts.yamlspec_helper_local 文件,可以为模块设置默认值。随后,我们讨论了 Hiera,详细介绍了如何在 spec 或通过 spec_helper 设置配置文件,以及如何执行查找操作。对于外部依赖,展示了 fixtures.yml 文件,能够从 Puppet Forge 或本地仓库引入模块依赖,以支持目录编译。然后,将覆盖率报告添加到本地 spec helper,使单元测试能够显示哪些资源未被测试覆盖,并在测试中显示通过百分比。接着,我们了解了一些进一步的 RSpec 工具和资源,这些工具允许你生成 RSpec 代码,并进行一些超出本书范围的检查。然后,重点介绍了 ServerSpec,它是一个基于 RSpec 的服务器级测试框架。它独立于 Puppet,超出了本书的范围,但值得投资,理想情况下应该加入到流水线中。

在向你展示如何开发和构建模块之后,你学习了如何从 Puppet Forge 获取模块,了解了 Puppet 提供的不同类型的模块支持和背书,如何对模块进行评分和扫描,以及如何了解贡献者及其在 Puppet 社区中的地位。提到了 Windows 模块集合,以及 PowerShell DSC 集合,它为 PowerShell Gallery 中的模块提供了自动封装,使内容可以在 Puppet 代码中下载和使用。还提到 CAT 团队作为 Puppet Forge 的维护者,通过博客发布更新来支持内容。接着,介绍了丹麦模块,作为一种额外的模块评分方式。

在下一章中,你将学习 Puppet 如何处理数据,并了解 Hiera,探讨它如何将数据分层到不同的作用域。我们将讨论何时最好使用 Puppet 代码、变量和 Hiera 来存储数据,以及如何构建和将数据传递给模块参数。我们还将涵盖如何正确存储数据的安全性,无论是在静止状态还是传输过程中,以及使用 Puppet 数据时的一些常见问题和如何应对它们。

第九章:使用 Puppet 处理数据

本章将重点介绍如何使用 Puppet 处理数据。我们将讨论 Hiera,Puppet 的键值数据查找工具,以及它如何确保 Puppet 的可重用代码在不增加过多逻辑和变量的情况下更加可配置。将回顾 Hiera 的基本结构,展示它如何以层级方式存储数据,提供基于规则的键查找,且无需繁琐操作,以及如何使用不同的后端查找数据中的键以返回值,这些后端实现可能是 YAML 数据文件或应用程序的 API 调用。将讨论自动参数查找的使用,展示它如何让参数化配置文件自动接收数据,以及如何在 Puppet 代码中直接使用查找功能来调用数据。我们将简要讨论 Hiera 3 和 Hiera 5 在传统 Puppet 中的变化。接下来,将详细回顾三个 Hiera 层级(全局层、环境层和模块层),讨论在这些不同层级中如何管理层级和数据。将展示查找合并和优先级行为的选项,突出如何通过第一次匹配或合并不同的值来查找数据。然后,我们将根据使用案例和最佳实践讨论数据应该在何时何地使用,以及代码应该直接保存在控制仓库中还是保存在单独的 Hiera 数据仓库中。接着,我们将讨论数据的安全性,展示如何通过不同的方法在存储、传输和在 Puppet 代码中使用时保持数据安全,重点介绍使用 Sensitive 类型、node_encrypt 模块以及通过 eyaml 加密文件的效果和局限性。最后,将回顾一些常见问题及故障排除方法/工具,展示如何最佳地使用 lookup 命令调试和解释值,以及为什么我们永远不应该在层级中使用全局变量,如何避免使用默认值,使用 Hiera 进行分类的风险,以及如何通过 Hiera 数据管理器 (HDM) 工具使数据更易于访问。

本章将涵盖以下主要主题:

  • 什么是 Hiera?

  • Hiera 层级

  • 决定何时使用静态代码或动态数据

  • 保持数据安全

  • 陷阱、难点和问题

技术要求

github.com/puppetlabs/control-repo 克隆控制仓库到你的 GitHub 账户,并将其命名为 controlrepo-chapter9,然后在生产分支中更新以下内容:

)

)

)

+   `hiera.yaml` 与 [`github.com/PacktPublishing/Puppet-8-for-DevOps-Engineers/blob/main/ch09/hiera.yaml`](https://github.com/PacktPublishing/Puppet-8-for-DevOps-Engineers/blob/main/ch09/hiera.yaml)

通过从github.com/PacktPublishing/Puppet-8-for-DevOps-Engineers/blob/main/ch09/params.json 下载 params.json 文件,并更新控制仓库位置和控制仓库中的 SSH 密钥,来构建一个包含两个 Linux 客户端和两个 Windows 客户端的标准集群。然后,在 pecdm 目录下运行以下命令:

bolt --verbose plan run pecdm::provision –params @params.json

首先,让我们了解一下 Hiera 是什么以及它为何被使用。

什么是 Hiera?

到目前为止,我们讨论了如何使用 Puppet 创建有状态且可重用的代码,以及如何通过使用角色和配置文件方法使得参数可用,从而使模块可配置。我们还展示了如何在代码中使用这些参数,但为了创建一个可扩展、可读且特定于站点的数据源,Puppet 使用了一个名为 Hiera 的工具。如果不在 Puppet 代码中使用 Hiera 数据,将需要无休止的逻辑和变量来表示节点例外、位置差异、操作系统版本差异、组织差异以及许多其他情况所需的数据变化。

Hiera 是一个数据查找工具,可以在 JSON、HOCON、YAML 和 EYAML 文件中查找值,支持内置后端,或使用自定义后端调用外部数据源,例如网站或数据库。它以键值对的形式存储数据,可以通过代码中的函数调用显式查找,也可以通过自动参数查找自动查找,后者通过将类中的参数名称与 Hiera 数据值进行匹配来实现。正如这个名字所暗示的那样,Hiera 专注于使用层级来查找数据,查找过程遵循一个常见的默认值,数据源的层级越具体,匹配的节点数据越精确。这些层级在 hiera.yaml 文件中配置;该 YAML 文件按优先级列出各个层级。此 hiera.yaml 文件设置了要使用的 Hiera 版本,这是必需的,虽然 5 是唯一的活跃版本。

使用内置后端

对于层级映射中的内置后端,将会有一个层级列表,每个层级将包含以下内容:

  • name – 描述层级的可读标签

  • datadir – 相对于 hiera.yaml 的基础路径,所有数据都存储在此路径下

  • data_hash – 要使用的 Hiera 后端/文件类型

  • pathpathsglobglobsmapped_paths——文件路径或相对于datadir的数据路径。

还可以使用这些键创建默认映射,这样就不需要在每个层次结构中重复值。

data_hash查找函数键接受yaml_datajson_datahocon_data作为值,但大多数 Puppet 实现仅使用 YAML 数据,因此本书将默认使用yaml_data后端。

文件路径允许层次结构级别使用与节点相关的代码中插值的变量,声明数据文件的特定位置,例如与%{<variable_name}相关的全局变量,并通过点(.)访问facts数组来调用事实。因此,%{facts.application_owner}将访问application_owner事实。进一步的点可以用来访问结构化事实,例如%{facts.os.family}可以访问os事实中的family值。类似地,受信任的事实可以从trusted数组中访问,例如%{trusted.certname},并且可以使用%{trusted.external.pds.data}访问受信任的外部事实。

因此,可以在hiera.yaml文件中使用以下代码创建一个简单的层次结构:

---
version: 5
defaults:
  datadir: data
  data_hash: yaml_data
hierarchy:
  - name: "Node data"
    path: "nodes/%{trusted.certname}.yaml"
  - name: "Location"
    path: "location/%{fact.data_center}.yaml"
  - name: "Common data"
    path: "common.yaml"

这个层次结构意味着,具有certname可信事实为examplehostdata_center事实为enterprisedc1的主机,首先会在data/nodes/examplehost.yaml中查找,然后在data/location/enterprisedc1.yaml中查找,最后在/data/common.yaml公共文件中查找。

也可以将多个变量插值组合在路径中,例如更新位置层以根据另一个事实进行区分——例如,假设存在brand事实,并且组织中的不同品牌将对数据中心有所不同,那么路径可以写成path: "location/%{facts.brand}-%{fact.data_center}.yaml"

所以,如果examplehostbrand事实设置为retail,它将会在data/location/retail-enterprisedc1.yaml中查找。

在这些查找中,如果在当前层次找不到匹配的文件,它将返回空值并进入下一层。改用paths路径文件变量可以简化这一过程。由于层次结构之间唯一的实际差异是路径,因此可以通过一个单一的层次结构声明和带有paths数组的路径来简化。例如,前一个示例中的层次结构可以简化为一个层次结构,使用paths

 hierarchy:
  - name: "YAML layers"
    paths:
    - "nodes/%{trusted.certname}.yaml"
    - "location/%{fact.data_center}.yaml"
    - "common.yaml"

如果需要为不同的后端添加额外的 Hiera 层次结构,那么必须理解,任何层次结构都会按照顺序检查所有路径,然后才会进入下一个层次,这可能会防止简化并保持正确的层次顺序。

在本节中,我们将讨论 glob,因为它们在代码库中可能会出现,但它们不应被使用,因为它们会使数据结构比任何环境实际需要的更复杂。

文件路径可以使用globglobs来传递 Ruby 风格的Dir.glob方法。此方法的完整文档可以在www.puppet.com/docs/puppet/latest/hiera_config_yaml_5.html#specifying_file_paths查看。这允许使用以下功能:

  • 星号(*)作为通配符

  • 两个星号(**)用于递归匹配目录

  • 问号(?)用于匹配任意一个字符

  • 用逗号分隔的列表({this,that,or,not})用于与列表中的任何选项进行字面匹配

  • 方括号内的字符集([xyz])用于匹配给定集合中的任意一个字符

  • 反斜杠(\)用于转义特殊字符

例如,取facts.os.windows事实,然后从display_id(在 Windows 2019 的后续版本中引入)或release_id(在 Windows 2016 中引入并在 Windows 2019 中弃用)中进行匹配。这个组合允许为一个反复变化的来源创建一个一致的 Hiera 层,并且需要组合事实来查找不同的版本:

- name: "Windows Release"
  glob: "windows_release/{%{facts.windows.display_id},%{facts.windows.release_id}}"

要创建一个包含主接口或网络域的网络信息层,可以创建以下代码,它将搜索网络文件夹中的任何目录结构以进行匹配:

- name: "Domain or Network"
    glob: "network/**/{%{facts.networking.domain},%{facts.networking.interfaces.ethernet.bindings.0.network}}.yaml"

如果找到多个匹配项,文件将按字母数字顺序进行搜索。此外,多个字符串可以使用globs:进行搜索,并以类似路径的方式传递字符串数组。

最终的文件路径选项是mapped_paths。此选项通过提供一个包含字符串集合的变量、一个变量名(该变量映射字符串集合中的每个元素)和一个模板来工作。例如,如果一个名为$oracle_sids的事实包含['ora1','ora2','ora3']的数组,则以下层次结构将在/oracle_dbs/ora1.yaml/oracle_dbs/ora2.yaml/oracle_dbs/ora3.yaml文件中执行查找:

- name: "Oracle sids"
    mapped_paths: [oracle_sid, sid, "oracle_dbs/%{sid}.yaml"]

虽然我们已经花了一些时间来讲解通配符(globs),但需要重申的是,这应该仅用于理解代码中预先存在的复杂数据结构,并帮助你进行重构和简化。这不应在新的 代码库 中使用。

在详细讨论了层次结构之后,现在是时候转向使用的数据以及如何调用该层次结构的查找了。正如在使用内置后端部分中提到的,YAML 是最常用的内置数据类型,并将在所有示例中使用,但差异仅体现在语言的表示方式,而非实际使用的结构。

在 YAML 数据文件中,我们创建键值对和带有值列表的键。键可以是单一值,但更常见的是使用格式<module_name>::<paramater_name>来构造,其中module_name可以包含多个段,反映模块内的某个类命名空间。

举个例子,对于exampleapp配置文件模块,一个数据文件可能包含将enable_service参数设置为true的设置,它可能包含[opt1,opt2,opt3]的选项数组,对于user的参数,它可能包含一个每个用户设置的哈希,用于创建exampleuseranotheruser。这将如下所示:

---
profile::exampleapp::enable_service: true
profile::exampleapp::options:
  - opt1
  - opt2
  - opt3
profile::exampleapp::users:
  exampleuser:
    uid: 101
    home: /app/exampleapp
    gig: 102
  anotheruser:
    uid: 201

访问数据

下一个要点是如何在 Puppet 代码中访问这个层级和数据,正如本章开头所提到的,Puppet 有两种方式在代码中查找数据:通过自动类参数查找或通过 Puppet 查找函数。推荐的模型是通过自动参数查找将几乎所有需要的数据传递给配置文件类,使用角色和配置文件模型(在第八章中讨论)和 Forge。

自动类参数查找通过获取任何已被包含/声明为资源的类的参数来工作,首先检查参数是否已通过声明设置,如果没有,则对每个<module_name>::<parameter_name>形式的参数执行 Hiera 查找。需要注意的是,这本身不是 Hiera 中的命名空间键;它只是一个字符串名称,值不能插入到数据结构中。在使用配置文件并具有已设置的配置文件模块和 Oracle 配置文件的情况下,这可能看起来像是profile::oracle::version。为了设置此数据,我们可能会在data/nodes/server1.example.com.yaml文件中为server1.example.com节点设置特定的版本,如以下行所示,将profile::oracle的版本参数设置为 Oracle 21c:

profile::oracle::version: 21c

如果此查找失败,它将查看类清单中是否为参数设置了任何默认值,然后将其赋值为undef

默认情况下,通过 Hiera 找到的数据将以字符串或字符串数组的形式返回;稍后我们将展示如何将其转换。

注意

自动类参数查找不适用于定义的资源类型,仅适用于类。为了模仿这一功能,您可以在代码中使用显式的lookup()调用。

Puppet 代码中的另一个机制是lookup函数。它更直接,可以在 Puppet 代码中使用;它通过一个键调用,这个键可以是多个段,每个段由两个冒号(::)分隔,或者它可以是简单的全局值。这里使用冒号只是约定,并不深入到数据结构中。为了查找相同的 Oracle 参数,以下示例将其赋值给oracle_version变量:

$``oracle_version=lookup(profile::oracle::version)

如果数据是一个数组,可以使用点表示法访问特定的键:

$``exampleuser_id=lookup(profile::exampleapp::users.exampleuser.id)

如果查找返回的值为空,可以通过函数中的参数或选项哈希提供默认值(完整选项可以在文档中查看:www.puppet.com/docs/puppet/latest/hiera_automatic.html#puppet_lookup-arguments),并提供一个返回的值——例如,如果上一个示例的查找未返回任何值,可以使用以下内容来返回 no id found 字符串:

exampleuser_id=lookup(profile::exampleapp::users.exampleuser.id, {default_value => 'no id found'})

这将在 Pitfalls, gotchas, and issues 部分中详细讨论,但提供默认值被认为是一个不好的实践,因为它隐藏了失败,并可能让人误以为值已经找到且一切正常运行。还可以注意到第二个和第三个参数被标记为 undef;这些是数据类型和合并策略,将在接下来的部分讨论。

注意

lookup 函数替代了 Hiera 3 中的传统 hiera_<data_type>hiera 函数。由于这些函数已被弃用,不应使用它们,因为它们可能产生不一致的结果。

到目前为止讨论的内容是最简单的情况,我们只期望查找一个值并找到第一个匹配项。这是 Hiera 的默认行为,允许你根据不同的具体程度重写值。不过,有时候,你可能希望返回所有层级中所有值的某种组合。可以在数据文件中设置查找选项,来描述应该如何进行这种操作。

lookup_options 保留键允许为查找操作设置不同的合并行为,查找操作可以是针对特定键或遵循以下格式的正则表达式:

lookup_options:
  <key name or regular expression>:
  merge: <MERGE OPTION>

最常见的方法是将这种行为放在common.yaml文件中,但如果例如,节点重写或某些优先级重写可能更重要,那么将其放到层次结构的不同层级中也是有意义的。

可以在数据文件中设置四种合并行为:

  • first – 返回层次结构顺序中的第一个匹配项

  • unique – 返回层次结构中所有匹配的唯一值的数组

  • hash – 返回一个哈希值,浅度合并哈希键,使用最高层次的键匹配

  • deep – 返回一个哈希值,深度合并哈希键,使用最高层次的键匹配

Hiera 的默认行为 first 会按照层次结构顺序查找第一个匹配的值。假设没有为该键声明其他的 lookup_option 值,那么就不需要隐式声明它。但是,如果例如,common.yaml 被设置为 unique,而对于我们的节点异常,我们只希望设置在 profile:oracle::limits 中声明的值,我们可以在节点的 YAML 数据文件中设置以下内容:

lookup_options:
  profile::oracle::limits:
    merge: first

unique关键字将查找所有匹配的键,并返回合并后的扁平化数组。因此,例如,如果我们想在一个配置文件中安装所有请求的 Oracle 版本,我们可以设置如下:

lookup_options:
  profile::oracle::versions
    merge: unique

如果在节点级别找到了11值,在组织级别找到了12,并且在公共层级找到了11,13,那么返回的值将是一个数组[11,12,13]

hash关键字将通过合并哈希的顶层键来合并所有匹配级别的哈希。这本质上执行的是浅层哈希合并,意味着顶层键会被合并,但合并不会递归地下降并合并嵌套在其下的数据结构。这将保持键的书写顺序,从最低优先级的数据源中匹配,但会从最高优先级的源中获取值。可以将其理解为在从最高到最低级别的过程中,将键逐步添加到哈希中。它会覆盖并附加值,但不会递归地合并键中的值。例如,假设在profile::oracle::limits上执行查找,在最低级别,common.yaml存在并包含以下内容:

lookup_options:
  profile::oracle::limits
    merge: hash
profile::oracle::limits:
  '*/nofile':
    soft: 2048
    hard: 8192
  'oracle/nofile':
    soft: 65536
    hard: 65536
  'oracle/nproc':
    soft: 2048
    hard: 16384
  'oracle/stack':
    soft: 10240
    hard: 32768

然后假设/node/examplenode.server.com.yaml由于以下hiera.yaml部分而具有更高优先级:

hierarchy:
  - name: "Per-node data"
    path: "nodes/%{trusted.certname}.yaml"
  - name: "Common data"
    path: "common.yaml"

/node/examplenode.server.com.yaml包含以下内容:

profile::oracle::limits:
  'oracle/nproc':
    soft: 4096
    hard: 16384
  'oracle/memlock':
    soft:  3145728
    hard:  4194304
  'oracle/stack':
    hard: 65536

profile::oracle::limits的哈希查找将返回以下内容:

profile::oracle::limits:
  '*/nofile':
    soft: 2048
    hard: 8192
  'oracle/nofile':
    soft: 65536
    hard: 65536
  'oracle/nproc':
    soft: 4096
    hard: 16384
  'oracle/stack':
    hard: 65536
  'oracle/memlock':
    soft:  3145728
    hard:  4194304

请注意,在这种情况下,profile::oracle::limits.oracle/stack键是从最高优先级获取的,因此只看到了硬值,没有执行递归合并。使用带点(.)的简化语法可以访问哈希或数组中的元素,在数组的情况下,会使用索引号。

deep合并结合了任意数量的哈希或数组,并且能够递归地合并哈希或数组中的值。这意味着hash值与另一个deep合并一起合并,且数组不会被扁平化,可以包含嵌套的数组。如果之前的查找选项被配置为deep_merge,则该查找将返回oracle/stack键的硬性和软性限制。

注意

在哈希中合并超过三个嵌套层级会对 Hiera 的性能产生严重影响,因此应避免这种做法。

还有一些选项可以分配以影响数组的合并。例如,sort_merged_arrays将导致合并后的数组按键排序,而不是默认行为,即数组按从最低优先级到最高优先级的顺序排序,merge_hash_arrays则表示如果设置为true,数组中的哈希将进行深度合并。另一个选项允许deep合并具有knockout_prefix键,其中包含一个值的键,通常以双破折号(--)表示,作为值前缀使用,将导致移除而不是添加该值。

例如,如果在第八章中给出的灵活类模型得到了实现,使用 deep 合并和 knockout 前缀将允许在每个层级添加或移除类:

lookup_options:
 profile::base::extra_classes:
   merge:
     strategy: deep
     knockout_prefix: --
     sort_merged_arrays: true

一些示例数据可能是 node/example.server.com.yaml,其中,层级的最高级别 node 包含以下代码:

profile::base::extra_classes:
  - pci::dss
  - email

相比之下,datacenter/europe.dc.1.yaml 这个较低层级包含了以下内容:

profile::base::extra_classes:
  - email
  - gdpr

这将导致 profile::base::extra_classes 查找包含 gdprpci::dss,按此顺序排列,但不包含 email

到目前为止,示例使用了在 common.yaml 中设置 lookup_options 的最常见位置。但 lookup_options 执行哈希合并,这将获取每个键找到的最高顺序。所以举个例子,假设 /data/common.yaml,这个最低层级,包含以下代码:

lookup_options:
  profile::base::extra_classes:
    merge:
      strategy: deep
      knockout_prefix: --
      sort_merged_arrays: true

/data/example.server.com.yaml,在更高的层级,包含了以下内容:

lookup_options:
  profile::base::extra_classes:
    merge: first:

然后,在 /data/example.server.com.yaml 中匹配 profile::base::extra_classes 键的查找将使用第一个匹配查找,而不是 deep 合并。

另一种查找选项是使用正则表达式和 convert_to 选项,将值转换为其他类型,而非字符串。一个特别有用的例子是,当使用我们希望保持敏感的值时,我们可以简单地在层级的公共级别添加一个正则表达式字符串,这将匹配所有以 profile 开头,且最终键名以 password 结尾的键,并确保该参数被转换为 Sensitive

---
lookup_options:
  '^profile::.+::\w+_password$':

数据安全保持部分中,将会有更多关于保护数据的讨论。

虽然基本上可以在 lookup 函数中覆盖数据文件中设置的查找设置,但我们强烈建议避免这样做,因为数据中可能会说明一种情况,而 lookup 函数却表现不同。这可能导致数据的变化对 lookup 函数产生意外的后果。如果确实需要,语法可以在文档中找到:www.puppet.com/docs/puppet/8/hiera_automatic.html#puppet_lookup

插值也可以通过变量和函数在 Hiera 数据中使用。虽然这可以避免数据的重复,但它也可能使数据变得比我们希望的更加复杂,因此一般建议避免这样做。

与使用事实的层级相同,trustedserver_facts 可以提供一致的变量,且这些变量以相同的方式进行插值,因此,一个简单的例子是设置一个使用主机名的 config 文件,如下所示:

tivoli_config_file: '/opt/app/tivoli/client/%{trusted.hostname}.conf'

Hiera 提供了有限数量的特殊插值函数。它们不同于 Puppet 函数。以下函数可用于插入 Hiera 数据:

  • lookup(或 hiera

  • alias

  • literal

  • scope

使用与变量相同的格式,可以声明一个函数,如 ${<function>(<arguments>)}

lookup 函数允许从数据中查找 Hiera 值。这可以有效地防止数据重复输入并减少维护工作,因为如果数据发生更改,只需在一个地方进行更改。例如,类似于仓库服务器这样的内容可能根据客户端的位置有所不同,或者可能反复使用以提供包的完整位置。以下示例展示了如何使用查找功能来提供两个二进制文件的完整路径,从而减少重复:

profile::base::artifactoryserver: artifactory.example.com
profile::exampleapp1::binary:  %{lookup (profile::base::artifactoryserver)}/exampleapp1.rpm
profile::anotherapp::binary:  %{lookup (profile::base::artifactoryserver)}/anotherapp.rpm

这也会使维护变得更加简单;如果 artifactory 服务器发生更改,只需更新一行即可。

alias 函数允许在 Hiera 数据中返回数据结构,因为 lookup 仅返回字符串。因此,如果 base 配置文件有一个 extensions 参数,它接受一个字符串数组并且我们希望将相同的扩展名列表传递给另一个配置文件 exampleapp,则可以像这样编写:

profile::base::extensions:
  -  'option1'
  -  'option2'
  -  'option3'
profile::exampleapp::extensions: "%{alias(profile::base::extensions)}"

literal 函数允许转义百分号符号(%),以避免它被解释为变量或函数进行插值。为了做到这一点,我们可以使用 %{literal('%')} 函数,其中 % 符号需要被使用。这在某些场景下非常有用,比如 Apache 配置文件或 Windows 环境变量;例如,如果我们想在 profile::nuget:: 中使用 %PACKAGEHOME%/External 字符串,则可以使用以下代码:

profile::nuget::
: %{literal('%')}{PACKAGEHOME} %{literal('%')}

scope 函数可能仅在遗留代码中使用。它实际上只是进行变量插值,只有在 Puppet 变量动态作用域时才有用。在本节中的 Tivoli 示例将写成 tivoli_config_file: '/opt/app/tivoli/client/%{scope(facts.hostname)}.conf'

使用自定义后端

除了到目前为止描述的内置后端外,还可以编写自定义后端或从 Forge 下载并配置到 Hiera 中。编写自定义后端超出了本书的范围,但 Puppet 的文档涵盖了如何编写它们,详见 www.puppet.com/docs/puppet/8/hiera_custom_backends.html#custom_backends_overview

自定义后端使用三种数据类型之一,根据数据访问的性能需求选择。

data_hash 后端类型,如同内置后端所示,用于读取成本较低的数据源,如磁盘上的文件。该配置文件用于数据小、静态、可以一次性读取且大部分数据都被使用的场景。它返回键值对的哈希值。

lookup_key 类型用于读取成本较高的数据源,例如安全的 HTTP API 连接。此配置用于数据量大,且仅部分数据使用,并且在编译过程中可能发生变化的情况。它返回一个键值对。最常用的自定义后端是 hiera-eyaml,用于加密 Hiera,这将在 保持数据 安全 部分详细讲解。

data_dig 后端类型用于访问集合中任意元素的数据源,例如数据库。与 lookup_key 的配置相似,但它访问元素的子键来返回一个键值对,该函数将深入到一个点分隔的键。

另一个需要提及的数据类型是 hiera3_backend,它仅在从旧版 Puppet 配置中迁移时相关;本书不会覆盖此配置,但详细信息可以在 Puppet 文档中找到,网址为 www.puppet.com/docs/puppet/8/hiera_config_yaml_5.html。Puppet 文档提供了如何从 Hiera 3 后端迁移的指导,如果你在遗留代码中遇到它们,可以访问 www.puppet.com/docs/puppet/8/hiera_migrate.html

注意

从用户的角度来看,Hiera 版本 5 是 Hiera 3 的演进,Hiera 4 是实验性版本,但 Hiera 5 在 Puppet 本身中得到了完全实现,而 Hiera 3 则是独立的实现。Puppet 7 及以下版本依赖于 Ruby gem 来支持 Hiera 版本 3,支持任何扩展了 Hiera:Backend 的遗留 Hiera 3 后端。这个依赖在 Puppet 8 中被移除。

这些数据类型可以与文件路径结合使用,正如之前与内置后端讨论的那样,另外还可以使用 uriuris 路径来指向如 Web 来源等 URI。

options 参数允许传入一个哈希,包含自定义后端所需的任何内容,例如凭证或密钥信息,具体内容将依赖于实现。

大多数模块会在其 README 文件中解释如何使用 options 参数。例如,forge.puppet.com/modules/petems/hiera_vault/ 是 HashiCorp Vault 的 Hiera 后端;根据他们的示例,以下代码展示了一个假设的示例,其中密钥都以 secret_ 开头,来自 vault.example.com 服务器,并为两个团队(digitaltrade)设置了挂载点,这些团队使用节点名称、位置和 common 作为他们的密钥层次结构:

hierarchy:
  - name: "Vault secrets"
    lookup_key: hiera_vault
    options:
      confine_to_keys:
        - "^secret_.*"
      ssl_verify: false
      address: https://vault.example.com:8200
      token: notreallyatoken>
      default_field: value
      mounts:
        digital:
          - %{::trusted.certname}
          - %{::trusted.extensions.pp_region}
          - common
        trade:
          - %{::trusted.certname}
          - %{::trusted.extensions.pp_region}
          - common

另一个示例是 forge.puppet.com/modules/tragiccode/azure_key_vault/,它允许访问 Azure 中的秘密。如果我们创建一个基于服务器部门分配的查找,查找以 secret 开头的密钥,结果将如下所示:

- name: 'Department Azure secrets'
    lookup_key: azure_key_vault::lookup
    options:
      vault_name: "%{trusted.extensions.pp_department}"
      vault_api_version: '2023-02-04'
      metadata_api_version: '2023-02-11'
      key_replacement_token: '-'
      confine_to_keys:
        - '^secret_.*'

第十三章中,将讨论Puppet 数据服务PDS),以及一系列用于扩展 Puppet 数据访问的后端。

现在我们已经了解了 Hiera 的工作原理,让我们来看看它如何在 Puppet 的不同层次中工作。

Hiera 层次

Hiera 仅在单一层级的上下文中进行过讨论,但实际上有三个层次的层级,每个层级都有其自身的层次配置。当 Puppet 在执行时进行查找时,它将遍历这些层级,检查每个层次中的层级结构。

全局层是第一层,并默认配置在$confdir/hiera.yaml中,通常路径为/etc/puppetlabs/puppet/hiera.yaml。Hiera 3 仅在此层工作,它的存在更多是为了兼容性考虑。Puppet 的文档建议,它的唯一目的应该是为了 Hiera 3 兼容性,并作为全局覆盖,但我们建议你完全不要使用它,因为它存在于代码部署和控制流程之外,这些将在第十一章中详细讲解。这将使文件的控制局限于 Puppet 服务器,只有在你希望绕过代码部署过程时,才可能需要这种情况。

环境层是下一个也是主要的数据层,它通常配置在每个环境中,路径通常为/etc/puppetlabs/code/production/hiera.yaml。环境和控制库将在第十一章中详细讨论,但为了理解这里的背景,环境是为特定组的 Puppet 节点设置的、具有固定版本的 Puppet 模块和清单,而控制库是用于管理环境的模块结构,包含一个名为 Puppetfile 的文件,详细说明了模块的源、应部署的版本以及部署位置。

需要决定hiera.yaml文件和数据是与控制库一起包含,还是将其与包含 Hiera 数据的模块分开。这是通过控制库将模块部署到环境中的数据目录,并确保 Hiera 在其hiera.yaml文件中使用该数据路径来配置的。当一组数据的控制需要由特定团队或组进行管理,而将其包含在控制库中可能会导致过多的访问/可见性时,这种分离是有意义的。例如,如果我们的hiera.yaml文件配置为使用数据作为源路径,我们可以通过在 Puppetfile 中添加条目,将模块中的 Hiera 数据添加到该路径中:

mod 'exampleorg_hieradata',
  :git    => 'https://<your_git_server>/exampleorg/hieradata.git',
  :install_path => 'data'

最后一层是模块层,这一层通过每个模块内的 hiera.yaml 文件进行配置,通常模块内还会有一个数据文件夹。因此,当在服务器的环境中部署时,hiera.yaml 文件可能位于类似 /etc/puppetlabs/code/environments/production/modules/example_module/hiera.yaml 的位置。模块层的最佳用途是为模块中所有类的参数设置默认值,同时要小心保持它们与模块的关注点相关,而不是外部的组织数据,这些数据更适合放在环境层中。可以在 puppetlabs/ntp 模块中看到设置默认值的示例,访问地址为 github.com/puppetlabs/puppetlabs-ntp,该模块根据操作系统版本设置默认值。hiera.yaml 文件还可以配置为支持对特定操作系统版本的逐渐细化,从默认值和一般的操作系统系列(如 Windows)到特定的完整操作系统版本,如 AlmaLinux-8.5,如以下代码所示:

hierarchy:
  - name: 'Full Version' 
    path: '%{facts.os.name}-%{facts.os.release.full}.yaml' 
  - name: 'Major Version' 
    path: '%{facts.os.name}-%{facts.os.release.major}.yaml' 
  - name: 'Distribution Name' 
    path: '%{facts.os.name}.yaml' 
  - name: 'Operating System Family' 
    path: '%{facts.os.family}-family.yaml' 
  - name: 'common' 
    path: 'common.yaml

注意

模块层通常被视为 params.pp 类的替代方法,后者曾是模块模式的一部分,包含默认值和 Hiera 查找调用。在现代 Hiera 层和自动参数查找机制出现之前,params.pp 类曾被广泛使用。

你只能在模块的命名空间中绑定数据键,因此在 exampleapp 模块中,只能设置 exampleapp::key 的值,不能设置像 key1 这样的全局键或其他模块如 anotherapp::key。这可能会导致另一种模式选项,特别适用于内部编写的模块,其中利用这个限制可以让应用团队完全控制模块的环境数据,而不影响其他模块。这对于由特定团队拥有的配置文件模块尤其重要,该团队希望管理期望。

default_hierarchy 有时被称为第四层,仅在模块层可用;它本质上是在模块层次结构中声明一个 default_hierarchy 键。与这一层的主要区别在于,只有当其他三层中没有匹配时,才会调用这一层,因此没有合并行为:

default_hierarchy:
  - name: 'defaults'
    path: 'defaults.yaml'
    data_hash: yaml_data

注意

default_hierarchy 产生的行为与 params.pp 方法相同,因为在三个 Hiera 层中只要有任何匹配项,它都会忽略并且不会合并任何匹配的值。

在回顾了这些层次之后,接下来会提出一个问题:应该如何构建这些层次结构。层次结构可以迅速变得复杂,但我们应当记住,基本的方法是应从节点的最具体数据开始,到通用数据为止。它们应尽可能简短,因为数据文件更容易处理,创建的层次结构越多,对 Puppet 基础设施性能的影响越大。过多的后端(特别是定制的后端)会带来复杂性和外部依赖,可能会破坏 Puppet 的编译。使用角色和配置文件方法应当减少在 Hiera 中管理的数据量,如果内建事实不够用,可以创建自定义事实,并且可以在路径中一起使用多个事实。

全局层适合仅基于节点名称和所有节点共享的数据来构建,因为它仅在 Puppet 代码环境控制之外进行覆盖时使用。

对于环境层,常见的节点数据结构如下所示:

  • 节点的名称

  • 节点所有者

  • 节点的目的

  • 节点的位置

  • 所有节点共享的数据

这可能导致一个简单的层次结构,如下所示:

- name: "Node data"
  path: "node/%{trusted.certname}.yaml"
- name: "Org data"
  path: "node/%{facts.org}.yaml"
- name: "Application-Tier"
  path: "app_tier/%{facts.app_tier}.yaml"
- name: "Datacenter"
  path: "datacenter/%{facts.datacenter}.yaml"
- name: "Common data"
  path: "common.yaml"

如前所述,模块层则成为以操作系统版本和平台等事实为基础的默认值的集中地。

注意

不要在任何层次结构中直接使用 environment 事实。应当使用环境层来处理基于环境的数据。

实验 – 向模块添加数据

在这个实验中,从 第八章 下载并更新 Grafana 模块,将默认值存储在 Hiera 数据中,而不是在参数中。

为此,假设 common.yaml 文件将包含 init.pp 中的所有当前默认值。

对于 Red Hat,我们将有如下内容:download_source = 'dl.grafana.com/enterprise/release/grafana-enterprise-8.4.3-1.x86_64.rpm'package_provider ='yum'

对于 Windows,我们将有如下内容:

download_source = 'https://dl.grafana.com/enterprise/release/grafana-enterprise-9.4.1.windows-amd64.msi'
package_provider = 'windows'

你可以参考github.com/PacktPublishing/Puppet-8-for-DevOps-Engineers/tree/main/ch08/grafana

一个示例答案可以参考 github.com/PacktPublishing/Puppet-8-for-DevOps-Engineers/tree/main/ch09/grafana

)

在后续的保持数据安全部分,将展示如何正确地保护密码,而不仅仅是将其作为明文保存在 YAML 文件中。

在这一部分,我们已经看到了如何使用 Hiera 的三层结构,以及如何在这些层中构建层次结构。接下来,我们将探讨何时应当在 Hiera 中使用数据,何时应直接在代码中使用数据。

决定何时使用静态代码或动态数据

在浏览了所有管理数据结构的可能性并查看了本书中介绍的代码示例后,可能会提出一个问题:何时编写代码,何时使用数据。图 9.1 展示了一个决策树:

图 9.1 – 数据或代码决策树

图 9.1 – 数据或代码决策树

第一个关键点是,如果数据在节点之间没有变化并且仅使用一次,最简单的方法是将数据硬编码在 Puppet 代码中——例如,直接将文件资源属性中的文件所有者设置为exampleuser

如果某个值被多次使用,那么显然将该值分配给一个变量并在需要的地方使用这个变量是有价值的。如果值需要更改,这简化了维护,但这也意味着在阅读代码时需要跟踪变量。

另一方面,如果在不同节点之间存在变化,并且需要在某些条件下覆盖该值,首先应该考虑逻辑的复杂性。如果只是一个简单的检查,那么将其抽象到 Hiera 中的收益并不大;将值抽象到 Hiera 中的问题在于它们在查看代码时不再明显,需要进行翻译和思考。因此,如果可以使用简单的条件逻辑,通常更好的做法是将值保留在代码中。

一旦逻辑变得更加复杂,并且可能会根据条件的组合而变化,我们可以使用 Hiera 数据和自动参数查找,或者如果有必要的话,使用lookup函数。

在整个过程中,最好使用当时可用的最简单方法,并随着代码的变化和增长逐步增加复杂性。为了将来创建复杂的数据结构和进行抽象仅仅会增加复杂性,需要更多的工作而无法带来实际的好处。

保持数据安全

管理数据的一个关键要素是确保机密数据的安全,使用 Puppet 时,必须将数据存储、传输到客户端并在 Puppet 代码中设置状态,这可能会带来挑战。在本节中,我们将讨论保护数据的可用方法、数据可以在哪些层级上进行保护,以及在每个层级使用的方法的局限性。

最常见的第一步是保护存储中的数据。这可以通过使用hiera-eyaml来实现,hiera-eyaml是一个可用的自定义 Hiera 后端,地址为github.com/voxpupuli/hiera-eyaml。该模块创建pkcs7密钥,然后用于加密和解密数据。在按照模块中的指示创建并分发密钥后,可以创建一个层次结构,例如以下示例:

hierarchy:
  - name: "Hiera data in yaml and eyaml files committed to the control-repo"
    lookup_key: eyaml_lookup_key
    options:
      pkcs7_private_key: /etc/puppetlabs/puppet/eyaml/private_key.pkcs7.pem
      pkcs7_public_key:  /etc/puppetlabs/puppet/eyaml/public_key.pkcs7.pem
    paths:
      - "nodes/%{trusted.certname}.yaml"
      - "location/%{facts.whereami}/%{facts.group}.yaml"
      - "groups/%{facts.group}.yaml"
      - "secrets/nodes/%{trusted.certname}.eyaml"
      - "os/%{facts.os.family}.yaml"
      - "common.yaml"

可以简化层级结构,注意 eyaml 后端也可以读取 YAML 文件,且没有理由将 yamleyaml 文件分离到不同的层级中,只要它们的路径和选项相同,如前面的示例所示。

hiera-eyaml 对于简单的加密和涉及有限用户加密秘密的情况是可行的,但对于更大的设置,使用 gpg 密钥与 github.com/voxpupuli/hiera-eyaml-gpg 比在多个团队之间共享签名密钥更为实用。配置和密钥管理完成后,这只需通过使用 gpg_gnugpghome 选项而不是 pkcs7 密钥选项来变化,例如如下所示:

    options:
      gpg_gnupghome: /opt/puppetlabs/server/data/puppetserver/.gnupg

这些加密数据文件方法的替代方案是,如果存在合适的安全密钥存储,例如 HashiCorp Vault,或云原生密钥存储,如 Azure Key Vault,那么使用能够访问这些服务的后端将确保数据安全存储。

无论选择哪个后端,这仅能确保数据在存储中是安全的。正如在访问数据部分所讨论的那样,默认情况下,当通过 Puppet 代码访问时,Hiera 将返回一个字符串。在 Puppet 5.5 及以上版本中,可以使用lookup_options将参数类型转换为Sensitive,并应谨慎确保所有安全参数都通过通配符或显式命名来覆盖。

必须小心使用 Sensitive 数据类型;容易错误地将其保密,使得无法在需要的位置使用它,或在使用 unwrap 函数时不小心暴露它。

当使用 filecontent 时,例如,以下尝试将 secret_value 放入 /etc/secure 文件中的做法会暴露在文件差异中,正如在 第三章 中讨论的那样,这是在报告日志中记录文件变化的比较时:

file {'/etc/secure':
  ensure => present,
  content => ${secret_value},
}

可以通过将file_diff参数设置为false或设置服务器不使用文件差异来防止此问题。

类似地,对于模板,也需要小心。如果使用 Puppet 6.2 或更高版本,模板将直接与 Sensitive 值一起工作,你可以在模板中直接使用 Sensitive 值:

file {'/tmp/test1':
  ensure => present,
  content => (epp('example.epp', { 'password' => $secure_password })),
}

对于低于 Puppet 6.2 的版本,你需要在模板中解包变量,然后将内容标记为 Sensitive,如以下示例所示:

content => Sensitive(epp('example.epp', { 'password' => unwrap($secure_password)})),

正确使用Sensitive选项可以避免将数据记录到日志中,但不幸的是,它不会阻止数据出现在目录文件本身中,如果你正在使用 PuppetDB,目录也会在那里存储。在这种情况下,使用forge.puppet.com/modules/binford2k/node_encrypt中提供的node_encrypt模块,可以使用客户端的密钥加密目录中的任何机密数据,并通过使用Deferred函数在应用目录时解密这些数据。这可以将机密数据从目录和应用目录后生成的报告中排除。

假设在基础设施上已经按照配置node_encrypt的说明进行设置,那么在之前代码中为content参数赋值的行可以更新为调用node_encrypt::secret函数,如下所示:

content => (epp('example.epp', { 'password' => $secure_password })). node_encrypt::secret,

注意

当前版本的node_encrypt依赖于 Puppet 6 中引入的Deferred函数,因此在旧版本上工作时需要使用版本 0.4.1,并且应使用node_encrypt::file类型,而不是file类型来加密文件资源。

本节展示了如何确保数据在存储、传输到目录和报告处理中的安全性,以及可能遇到的一些问题。在下一节中,我们将讨论在 Hiera 中处理数据时的常见问题。

实验 - 使用 eyaml 存储秘密数据

在本实验中,使用了puppet-hiera_eyaml模块来配置默认的pkcs密钥,设置了一个全局的 Hiera 配置,以查看节点名称、操作系统和通用值。在site.pp中,执行了一个 Hiera 查找,用来查找secret::examplefiles的值,该值作为内容创建了/var/tmp/secret_example文件并将其放置在 Puppet 主服务器上。查找的默认值未设置。在本实验中,你将加密一个秘密并将其添加到操作系统级别,使得文件的内容发生变化。

SSH 到主服务器并提升为 root 用户:

ssh centos@<primary_host>
sudo su -

/etc/puppetlabs/puppet目录中运行eyaml encrypt –p命令,并在提示符下输入你选择的秘密数据:

cd /etc/puppetlabs/puppet
eyaml encrypt -p

将以ENC[开头的字符串后面的输出复制,并粘贴到/etc/puppetlabs/puppet/data/os/RedHat.eyaml的 data 部分,使其包含如下内容:

---
secret::example: ENC[PKCS7,<long string of chars>]

运行puppet agent –t,观察/var/tmp/secret_example的内容变化为你设置的内容。

这是一个非常简单的示例,需要注意的是,正如Hiera 层部分所强调的,你更可能使用环境层次结构并保持数据安全,正如保持数据安全部分所示,通过在查找的options参数中使用 Sensitive 选项来实现。此外,eyaml使用的公钥可以复制到桌面上以加密秘密数据,前提是这对你所在组织的安全政策足够安全。

现在我们已经全面回顾了 Hiera 配置,接下来我们将展示如何理解查找和数据的问题。

陷阱、注意事项和问题

在处理包含多个层级和层次的大数据集时,可能会变得很难理解为什么某些答案被生成或错误是如何插入的。本节将专注于理解和调试数据查找的方式,以及可以使数据更清晰的工具。

Hiera 的问题通常可以归纳为几个类别:语法、格式、后端通信和性能问题、层级顺序错误等。

puppet lookup命令是测试 Hiera 数据的最佳方式,实际上就像在 Puppet 代码中使用的lookup函数一样。在主服务器上使用此命令时,其基本语法是puppet lookup <key> --node <node_name> --environment <environment_name>

此命令将返回值(如果找到),否则返回空。了解此命令的各种标志的效果非常重要,这些标志可以返回更详细的信息。一个常见的错误是同时使用--debug--explain标志;它们不应该一起使用,因为前者侧重于高水平的日志记录,帮助你理解为什么会生成如语法、格式或后端之类的错误,而后者侧重于展示如何得到一个值,Hiera 查找了哪里,以及它找到了什么。

例如,motd::contentexplain查找可能如下所示:

puppet lookup --explain motd::content --node node-name --environment production
 Searching for "lookup_options"
  Global Data Provider (hiera configuration version 5)
    Using configuration "/etc/puppetlabs/puppet/hiera.yaml"
    Hierarchy entry "Classifier Configuration Data"
      No such key: "lookup_options"
  Environment Data Provider (hiera configuration version 5)
    Using configuration "/etc/puppetlabs/code/environments/production/hiera.yaml"
    Merge strategy hash
      Hierarchy entry "Yaml backend"
        Merge strategy hash
          Path "/etc/puppetlabs/code/environments/production/data/nodes/pe-server-0-540983.05eqwrwxv1ourfszstaygpgbth.zx.internal.cloudapp.net.yaml"
            Original path: "nodes/%{trusted.certname}.yaml"
            Path not found
          Path "/etc/puppetlabs/code/environments/production/data/common.yaml"
            Original path: "common.yaml"
           Found key: "motd::content" value: "test"

从调试输出中,我们可以看到更多关于 Facter 和其他系统操作的信息,如下所示的命令和示例输出:

puppet lookup motd::content –-node node-name –-environment production -–debug
Debug: Facter: Managed to read hostname: pe-server-0-d6a9f5 and domain: vhcpsckl41fedgadugqovud0sa.cwx.internal.cloudapp.net
Debug: Facter: Loading external facts
Debug: Facter: fact "domain" has resolved to: vhcpsckl41fedgadugqovud0sa.cwx.internal.cloudapp.net
Debug: Lookup of 'motd::content'
  Searching for "lookup_options"
    Global Data Provider (hiera configuration version 5)
      Using configuration "/etc/puppetlabs/puppet/hiera.yaml"
      Hierarchy entry "Example yaml"
        Merge strategy hash
          Path "/etc/puppetlabs/puppet/data/nodes/pe-server-0-d6a9f5.vhcpsckl41fedgadugqovud0sa.cwx.internal.cloudapp.net.eyaml"

如果没有提供节点,查找操作将默认假设查找的是你运行命令的服务器,并且环境将默认设置为production

在语法和格式问题方面,最常见的错误之一是 YAML 文件的开头---格式错误。这可能会以几种方式发生:

  • 行的开头不小心添加了空格或发生了 Unicode 字符转换,导致它变成。在这种情况下,debug中的错误将如下所示:

错误:无法运行:(<unknown>):在第 2 行 第 8 列的上下文中不允许映射值

  • 如果在破折号中间插入了空格,例如-- -,那么在debug中将看到如下错误:

错误:无法运行:(<unknown>):在解析块集合时没有找到预期的--指示符,在第 1 行 第 1 列

另一个常见的语法错误是使用键值对时,冒号符号(:)和值之间没有空格;因此key: valuekey : value是有效的,而key:value不是,它在调试时会像下面这样报错:

错误:无法运行:(<unknown>):在第 3 行 第 10 列的上下文中不允许映射值

如果使用制表符而不是空格进行缩进,那么在调试时可能会导致类似以下的错误:

Error: Could not run: (<unknown>): found character that cannot start any token while scanning for the next token at line 4 column 1

在格式化时,使用单引号包围包含变量的数据会导致返回变量名的字面字符串,而不是变量插值。

文件权限也可能是一个问题,因此,确保以相同用户身份运行查找命令是值得的,因为 Puppet 通常在pe-puppetpuppet用户下运行。

使用--debug,可以帮助查看是否是自定义后端出现问题、错误或性能下降。通常,我们建议检查像 PDS 和外部数据提供者这样的模式。

请注意,这不会调试实际数据,只会调试hiera.yaml文件,数据文件如果不是有效的 YAML 格式会被忽略,可以通过--explain查看。

在层次结构问题方面,--explain标志会非常有用,因为它会逐步解释所使用的配置文件、找到的层次结构、合并策略和详细检查的路径,从而清楚地展示它如何逐步遍历层次结构,以及为什么它可能没有按预期工作。

根据在层次结构中使用的变量,可能需要使用--compile标志,因为默认情况下,在使用 Puppet lookup时,它不会执行目录编译,因此只有$facts$trusted$server_facts变量可用。我们强烈建议避免使用清单中的任意值,因为这些值可能会极大地增加查找的复杂度并产生不可预测的结果。

从中可以看出,你总是应该使用Facter数组,以避免模块变量和顶层Facter变量冲突的风险。

一些其他选项可以用来测试更改配置后会发生什么,比如使用--merge标志更改合并策略,或者通过--facts提供更新的事实数据等。

查找命令选项的完整命令参考可以在www.puppet.com/docs/puppet/latest/man/lookup.html查看。

如果更新全局 Hiera 文件,请小心重启Puppet 服务器服务以确保重新读取该文件。

在前面访问数据部分中提到过,我们不建议在lookup函数中使用默认值。模块或配置文件中的数据默认值应该是有意义的。因此,提供默认的配置文件位置对一个模块来说是有意义的,尤其是如果你期望大多数用户只会使用它。但如果仅仅为了避免查找失败而添加默认值,那可能是一个严重的错误,这会掩盖 Hiera 数据或代码中的问题,这些问题不会被注意到,因为代码会用默认值成功应用。需要避免的关键问题是,传递一个默认值,然后需要在 Puppet 代码中使用大量逻辑来处理该值。

在 Hiera 中进行分类是可能的,因为一些用户选择查找 Hiera 数据并将类包含在site.pp文件中。像github.com/ripienaar/puppet-classifier这样的模块专注于这种方法。需要考虑代码结构的平衡,正如我们灵活的角色和配置文件方法中所展示的那样。通过将过多数据放入 Hiera,它可能会使代码不再直观,因为数据在代码中不直接可见。因此,最好考虑提高复杂度是否值得。

Hiera 的一个问题是其结构,这使得不太参与的用户无法访问。为了让 Hiera 数据更可见,Betadots Hiera Data Managerforge.puppet.com/modules/betadots/hdm)是一个很好的选择,因为它允许图形化搜索、更新和删除 Hiera 数据。然而,在生产环境中,这应仅限于查看数据。

图 9.2 – Hiera 数据管理器示例查找

图 9.2 – Hiera 数据管理器示例查找

另一个让 Hiera 数据更易于自助服务的选项是 PDS,详细内容将在第十三章中讨论。

实验室 – 排查 Hiera 问题

在生产环境中排查 Hiera 数据问题:

  1. 通过 SSH 连接到主服务器,提升为 root 用户,并部署lab_error环境:

    ssh centos@<primary_host>
    sudo su -
    puppet code deploy environment lab_error --wait
    
  2. lab_error环境中的主服务器上,以debug标志执行对profile::error::example键的查找,并解决发现的错误,纠正control仓库中的问题,并运行前一步的code deploy命令:

    puppet lookup profile::error::example --debug --environment lab_error
    
  3. 解决controlrepo-chapter9/datahiera.yaml中的数据错误。

  4. 运行相同的命令并使用explain,以了解它如何到达当前解决方案,并找出为什么它没有基于os.family事实找到值:

    puppet lookup profile::error::example --debug --environment lab_error
    
  5. 更新control仓库分支中的 Hiera 数据,lab_error,并重新部署,使得查找现在能找到主节点的os.family事实值:

puppet code deploy environment lab_error --wait

puppet lookup profile::error::example --debug --****environment lab_error

查看github.com/PacktPublishing/Puppet-8-for-DevOps-Engineers/tree/main/ch09/data_solutions中的评论解决方案。

作为在技术要求部分创建实验室环境的一部分,已通过puppet-HDM模块安装了 HDM。尝试使用 HDM 按照以下步骤查看数据:

  1. http://<public IP of puppetserver>:3000处打开一个网页浏览器。

  2. 完成注册详细信息以创建管理员用户(详细信息不重要)。

  3. 点击non-admin user(详细信息同样不重要)。

  4. 点击右上角的管理员用户名,注销,然后以您创建的非管理员用户身份重新登录。

  5. 依次选择 environment productionlab_error

  6. 探索在每个环境中 HDM 可见的 Hiera 键值。

总结

在这一章中,我们探讨了 Puppet 如何通过 Hiera 工具处理数据,从而减少在代码中表示节点、数据中心、组织、操作系统及其他配置差异所需的复杂性。Hiera 被证明是一个基于数据层级结构的工具,它允许我们根据事实访问不同的文件。它具有内置的后端,可以将数据存储在 YAML、JSON、HOCON 和 EYAML 文件中。展示了数据结构,我们研究了如何将值放入数据文件以及如何执行查找;这里还考察了合并类型,以及如何在数组中使用如 knockout 前缀等特殊设置。

接着,我们展示了一些可以使用的自定义后端,这些后端具有不同配置文件上的数据类型;通常,这些是特定的集成工具,如 Vault 或来自 Forge 的 EYAML,或者是公司内部开发的集成工具,用于访问数据。

接下来,我们讲解了 Hiera 如何在三个层级上工作——全局、环境和模块——展示了全局层在现代 Puppet 设置中的作用较小,但可以作为覆盖系统使用,环境作为数据的主要来源,模块则允许为模块设置默认值。然后讨论了一些常见的层级结构设计方法,包括一种通过节点名称、节点所有者、节点目的、节点位置和所有节点共同的数据来逐步构建的方式。

关于如何决定是在代码中使用数据还是在 Hiera 中使用数据的回顾显示,这取决于数据需要的灵活性,而这从硬编码在 Puppet 代码中的静态数据到需要精确描述完整层级结构的更复杂灵活的数据都有不同的需求。建议不要提前构建,而是根据需要重构,以避免使数据变得比实际需求更复杂。

接着,我们讨论了如何确保数据在存储和传输中的安全性,以及在 Puppet catalogs、报告和 PuppetDB 中使用时的安全性。我们展示了如何使用 eyaml 通过更灵活的 PGP 方法加密值来确保存储中的数据安全,这允许多个密钥和团队的使用。然后,展示了 Sensitive 值,以确保值不会暴露在日志或代码中。这不会防止在 catalogs 和报告中暴露值,node_encrypt 模块被展示用于在配置时加密资源和值,并通过 Deferred 函数应用。

然后回顾了调试和故障排除的方法,重点介绍了--explain--debug之间的区别。前者可以帮助理解如何审查层级结构,后者则返回如语法错误和后端失败等错误。建议小心使用 Hiera 作为分类器,因为这会将分类信息从代码中抽象出来,但也强调在后续章节中 PDS 确实采用了这种方法。

在下一章,在详细回顾了 Puppet 语言之后,焦点将转向 Puppet 基础设施。我们将审视构成 Puppet 平台的开源组件,它们如何通过 API 向系统提供服务,以及它们如何进行通信和日志记录。将详细探讨 Puppet 代理的完整生命周期,包括代理注册过程及其与平台的通信。PuppetDB 和 PostgreSQL 将被用来存储诸如事实、报告和清单等数据,并允许通过Puppet 查询语言PQL)进行发现和审查。然后,我们将讨论编译服务器作为 Puppet 水平扩展的方法。

第三部分 – Puppet 平台和 Bolt 编排

在这一部分,你将了解 Puppet 是如何作为一个平台构建的,各个组件如何协作和通信,以及用于实现规模的常见架构方法。接下来我们将展示可以用来分类哪些代码应用到服务器上的各种方法,以及如何对代码进行版本控制和部署到基础设施。Bolt 将作为 Puppet 执行过程脚本和代码的方式被介绍,它可以是传统脚本,也可以是基于 Puppet 语言的计划。然后我们将回顾如何通过各种工具和第三方产品来监控、调整和集成 Puppet 基础设施。

这一部分包含以下章节:

  • 第十章Puppet 平台的组成部分和功能

  • 第十一章分类和发布管理

  • 第十二章用于编排的 Bolt

  • 第十三章进一步使用 Puppet 服务器

第十章:Puppet 平台部分和功能

到目前为止,我们讨论了 Puppet 作为一种语言,但在本章及后续章节中,我们将开始关注 Puppet 作为一个平台,以及平台的基础设施和组件。

图 10.1 中,展示了本章将讨论的 Puppet Server 和 Puppet 客户端服务的完整架构。这些服务专注于如何在服务器上强制执行 Puppet 代码:

图 10.1 – Puppet 服务器和客户端组件

图 10.1 – Puppet 服务器和客户端组件

我们将首先强调,在本书中不会详细介绍安装方法。对于开源 Puppet 和 Puppet Enterprise,有几个开源项目可以作为自动化基础;在本书中,我们使用了 peadpecd 模块作为最自动化的 Puppet 编辑器PE)安装机制。随着各组件的讨论,我们还将提到 Puppet 包的版本如何不同,并查看一些相关的安装版本、关键用户、目录、配置文件和已安装的服务。

首先,我们将检查 Puppet Server 提供的核心服务。这些服务包括接收客户端请求的清单编译、处理它们的当前状态,并根据 Puppet 代码确定如何配置它们。证书授权中心CA)允许代理安全地注册并与 Puppet 服务器通信。它还包括一些相关的 API 服务,以便访问、请求和控制这些服务。

在了解了服务器的功能后,我们将展示 Puppet agent 如何与服务器通信,请求由 CA 签署密钥,清单编译的通信过程,以及 agent 如何处理并存储返回的清单。

接下来我们将查看 PuppetDB 如何用于存储事实、清单和事件,以及如何通过 Puppet 查询语言PQL)和 API 访问这些信息。我们还将研究 PuppetDB 和 PostgreSQL 之间的关系,作为前端应用程序与后端数据库架构的连接,并讨论 Puppet 服务如何直接将其他数据存储在 PostgreSQL 中。

接着将展示如何使用编译服务器水平扩展,以编译数十万台服务器的清单。

在这些主题中,我们将突出 PE 和开源 Puppet 配置之间的细微差异。

本章不涉及与 PE 相关的编排器特性、PE 控制台或支持的架构(这些可以使服务拆分到更具可扩展性的基础设施中);这些将在 第十四章 中讨论。

在本章中,我们将覆盖以下主要内容:

  • Puppet 平台安装和版本控制

  • Puppet 服务器

  • Puppet agent 到 server 的生命周期

  • PuppetDB 和 PostgreSQL

  • 使用编译器进行扩展

注意

作为努力从其产品中移除有害术语的一部分,Puppet 放弃了使用master servercompile master的术语,现在使用primary servercompile server。由于这些名称已深植人心,某些地方的类或配置设置仍然会提到master

技术要求

github.com/puppetlabs/control-repo克隆控制库到你的 GitHub 账户,创建一个名为controlrepo-chapter10的库。

通过下载github.com/PacktPublishing/Puppet-8-for-DevOps-Engineers/blob/main/ch10/params.json中的params.json文件,并用你的控制库位置和控制库的 SSH 密钥更新它,来构建一个包含三个编译器和三个客户端的大型集群。然后,从你的pecdm目录运行以下命令:

bolt --verbose plan run pecdm::provision –-params @params.json

Puppet 平台的安装与版本管理

本书选择不深入探讨 Puppet 的安装方法;对于开源版本的安装说明,几乎没有需要补充的内容,详细内容请见puppet.com/docs/puppet/latest/server/install_from_packages.html,任何进一步的自动化选择都将高度依赖于你组织的使用场景,以及你希望集成的工具和产品集。

对于开源 Puppet,有多个项目可以自动化 Puppet 的部署、配置和集成,比如 example42 的psick(github.com/example42/psick)或 Foreman 项目(github.com/theforeman/foreman-installer),后者有一个专门用于安装 Puppet Server 的模块(forge.puppet.com/modules/theforeman/puppet),即使不使用 Foreman,也可以用来安装 Puppet。类似 PE 设置提供的仪表板也可以在诸如 Puppetboard(forge.puppet.com/modules/puppet/puppetboard)或 Puppet Summary(github.com/skx/puppet-summary)等项目中找到。

对于 PE,尽管可以在puppet.com/docs/pe/2021.7/installing_pe.html找到手动安装说明,但自动化的选择是显而易见的,即使用 Puppet 支持的peadm模块;在第十二章中,我们将回顾如何在实验中使用该模块,并将pecdm作为 Bolt 项目使用。

安装的包中需要注意的关键点是,Puppet 仓库提供了不同版本的 Ruby、OpenSSL、Hiera 和 Facter,以供不同版本的 Puppet 使用,且像 puppetserver 这样的包可能与正在安装的 Puppet 版本不匹配——例如,Puppet 7.17 会安装 Puppet Server 版本 7.8;这些关联版本可以在发布说明中找到。对于 PE,你可以在文档中查看所有底层开源包版本,网址为 puppet.com/docs/pe/2021.7/component_versions_in_recent_pe_releases.html#component_versions_in_recent_pe_releases

Puppet Server

在 Puppet 的历史版本中,基于 Ruby 的解决方案如 WEBrick 或 Passenger 被用来运行 Puppet 服务,但在所有现代版本的 Puppet 中,为了提高扩展性和性能,Puppet Server 作为一个 Clojure 和 Ruby 应用程序运行在 Java 虚拟机 (JVM) 上。Puppet Server 具有多个相关的服务,这些服务共享状态并在它们之间路由请求。这些服务运行在单一的 JVM 进程中,使用 Trapperkeeper 服务框架,Trapperkeeper 是一个用于托管长时间运行应用程序的 Clojure 框架。

Puppet Server 通过 open source Puppet 中的 puppetserver 包和 PE 中的 pe-puppetserver 包进行安装。这样会创建一个同名的系统服务,并生成配置文件,默认情况下,这些文件会放置在 /etc/puppetlabs/puppetserver/conf.d 目录下,采用 人类优化配置对象表示法 (HOCON) 格式。

注意

Puppet 的 hocon 模块是自动化管理 HOCON 文件的最佳方式 (forge.puppet.com/modules/puppetlabs/hocon)。

接下来,我们将查看构成 Puppet Server 的服务。

内嵌 Web 服务器

Puppet 在 JVM 中包含一个基于 Jetty 的 Web 服务器,用于设置挂载点和通信,以便在组件之间进行 Web 请求并访问 API。

webserver.conf 文件设置了 Web 服务器的主要配置,如 web-routes.conf 文件的位置,后者通过挂载处理程序来设置 Web API 访问的挂载点,如以下示例文件所示:

# Configure the mount points for the web apps.
web-router-service: {
    # These two should not be modified because the Puppet 4 agent expects them to
    # be mounted at these specific paths.
    "puppetlabs.services.ca.certificate-authority-service/certificate-authority-service": "/puppet-ca"
    "puppetlabs.services.master.master-service/master-service": "/puppet"
    # This controls the mount point for the Puppet administration API.
    "puppetlabs.services.puppet-admin.puppet-admin-service/puppet-admin-service": "/puppet-admin-api"
}

在此文件中列出了客户端与服务器之间通信所需的核心挂载点:

puppet-ca 挂载点供客户端与 CA 服务进行通信,并检查或发出 证书签名请求 (CSR)。

  • master-service 提供一个挂载点,供客户端通过 JRuby 解释器编译的目录请求。

  • 默认情况下,webserver.conf 中设置的请求日志记录配置位于 /etc/puppetlabs/puppetserver/request-logging.xml,它决定了 HTTP 访问请求的记录方式。默认情况下,消息将被记录到 /var/log/puppetlabs/puppetserver/puppetserver-access.log

本节内容应帮助你了解嵌入式 Web 服务如何在 JVM 中设置 Web 服务器,及其为不同组件的 Puppet Server 请求提供必要的挂载点,并记录这些请求。接下来,我们将查看通过挂载点提供的两个核心 API,分别是通过 /puppet/puppet_ca 访问的 Puppet API,以及通过 /puppet_admin_api 访问的 Admin API。

Puppet API 服务

Puppet API 服务由嵌入式 Web 服务器创建的两个端点组成——/puppet 用于配置相关服务,/puppet-ca 用于 CA。

两者都通过如 /v3 这样的字符串进行版本控制,授权通过 auth.conf 文件控制,该文件是 HOCON 格式的文件。除非需要更高级的访问权限来集成服务,否则你不太可能需要编辑该文件,但为了展示示例内容,以下代码允许 Puppet 节点从 API 请求自己的目录:

        {
            # Allow nodes to retrieve their own catalog
            match-request: {
                path: "^/puppet/v3/catalog/([^/]+)$"
                type: regex
                method: [get, post]
            }
            allow: "$1"
            sort-order: 500
            name: "puppetlabs v3 catalog from agents"
        },

注意

有关自定义授权的更详细说明,请参见 github.com/puppetlabs/trapperkeeper-authorization/blob/main/doc/authorization-config.md

所有 Puppet 5 到 8 的现代版本中的 Puppet 代理使用 /puppet/v3 端点服务来管理客户端。v3 API 具有两种类型的端点——间接指令环境端点。

间接指令的格式为 /puppet/v3/<indirection>/<key>?environment=<environment>

在这里,间接值是请求的间接指令,键是与调用间接指令相关的键,环境是该请求应该使用的环境。例如,若要请求编译目录,客户端将构建以下内容:

/puppet/v3/catalog/pe.example.com?environment=production

服务器下/puppet/v3/路径下存在以下间接指令:

  • 事实facts 端点允许为指定的节点名称设置事实

  • 目录:返回指定节点的目录

  • 节点:返回节点信息,例如分类

  • 文件桶 文件:管理文件桶的内容

  • 文件内容:返回文件内容,例如模块中的文件

  • 文件元数据:返回文件的元数据,例如模块中文件的权限

  • 报告:允许存储节点的 Puppet 报告

服务器下/puppet/v3/路径下存在以下间接指令:

  • 环境类:返回请求环境中可以解析的所有类

  • 环境模块:返回环境中所有模块的信息,例如它们的名称和版本

  • 静态文件内容:返回特定版本的文件资源在某个环境中的文件内容

未作为间接指令的独立环境端点允许简单调用 /puppet/v3/environments,该调用返回服务器已知的所有环境。在下一章中,我们将更详细地讨论环境。

工具和服务也可以访问这些相同的端点来检查数据,并且存在一个v4 API,具有一个目录端点,可以更广泛地使用 PuppetDB 来操作事实和目录。它被如 octocatalog-diffgithub.com/github/octocatalog-diff)等工具使用,这些工具可以生成、比较和操作目录。

/puppet-ca 端点采用类似的格式,使用 v1 和指令,如下所示:

  • 证书:返回指定名称的证书

  • 证书清理:吊销并删除证书

  • 证书状态:请求证书或 CSR 的状态

  • 证书吊销列表:请求 证书吊销列表 (CRL) 文件

例如,要请求server.example.com的证书,可以访问以下端点:/puppet-ca/v1/certificate/server.example.com

这些操作将在本章的 CA 部分进行更详细的讨论。

在本节中,我们没有详细讨论每个端点及其 API 调用,但在本章后面,我们将查看客户端与服务器的生命周期,跟踪调用日志,并强调它们的用途,以展示 Puppet 如何使用这些 API。端点的完整详细信息可以在puppet.com/docs/puppet/latest/http_api/http_report.html查看。

Admin API

Admin API 只有两个端点在 /puppet_admin/v1/,如下所示:

  • 环境缓存:用于清除环境数据的缓存

  • JRuby 池:用于清除 JRuby 池或获取正在运行的 JRuby 实例的 Ruby 线程转储

这两个端点用于更深入的开发工作,因此超出了本书的范围,但有助于完整地展示 Puppet 服务器组件。可以在puppet.com/docs/puppet/latest/server/admin-api/v1/jruby-pool.htmlpuppet.com/docs/puppet/latest/server/admin-api/v1/environment-cache.html查看这些端点的详细信息。

CA

默认情况下,Puppet 使用其内置的 CA 和 公钥基础设施 (PKI) 来保护所有 SSL 通信。

有两个命令用于与 Puppet CA 设置进行交互——puppetserver ca 用于服务器端操作,如签署或吊销证书,puppet ssl 用于代理端任务,如请求和下载证书。这些命令通过 CLI 调用 puppet-ca 端点。

注意

尽管引入了 puppet-ca 端点,之前 ruby ca 实现的五个命令在 Puppet 6 之前仍然可用:puppet certificatepuppet certpuppet certificate_requestpuppet capuppet certificate_revocation_list。这些命令已被 puppetserver capuppet ssl 命令取代。即使你使用的是 Puppet 5,强烈建议不要使用这些 Ruby 命令,因为同时使用 API 和 Ruby 实现可能会破坏 CA。

尽管在介绍中讨论的安装自动化应该涵盖初始设置,但通过运行 puppetserver ca setup 检查 CA 设置是否已执行也是值得的。在 puppetserver/pe-puppetserver 服务启动之前,它将创建一个单独的根 CA 和一个中间签名 CA。如果在此步骤之前启动了 puppetserver/pe-puppetserver 服务,它将创建一个单一的根 CA 和签名 CA,这是 Puppet 以前的操作方式。除非有特定需求使用单一证书,否则应避免此情况。从 PE 2019.x 和 Puppet 6.x 开始,这些证书的有效期为 15 年;之前为 5 年,而且需要理解的是,升级 Puppet 版本并不会延长 CA 的有效期。

注意

通过 ca_extend 模块可以扩展过期的 CA(forge.puppet.com/modules/puppetlabs/ca_extend)。

在此步骤中创建的密钥和证书将保存在一个名为 /etc/puppetlabs/puppetserver/ca 的目录中(适用于 Puppet 7 及以上版本),或者保存在 /etc/puppetlabs/puppet/ca 目录中(适用于 Puppet 6 及以下版本)。为了避免混淆,新的目录位置下会有一个指向 /etc/puppetlabs/puppet/ca 的路径。该目录将包含以下内容:

  • ca_crl.pem:CRL 文件

  • ca_crt.pem:CA 签名的证书公钥

  • ca_key.pem:CA 私钥

  • ca_pub.pem:CA 公钥

  • inventory.txt:CA 签名的证书列表,包括其序列号和到期日期

  • requests:未签名的 CSR 文件

  • root_key.pem:如果使用单独的根 CA 和中间 CA,这是用于签署 CA 证书的根密钥

  • serial:此文件包含证书新序列号的递增计数器

  • signed:此文件夹包含所有已签名的 CSR 文件

除了这些文件外,还可以维护基础设施 CRL,默认情况下,开源 Puppet 不使用该 CRL,但 PE 使用该 CRL。为了保持较小的 CRL,infra_inventory.txt 文件用于管理 Puppet 基础设施服务器;当被吊销时,这些系统会被添加到infra_crl.pem中。通过在puppet.conf文件中将infra certificate-authority.enable-infra-crl设置为true,可以启用此功能。我们将在本章后续部分详细讨论puppet.conf文件。此方法意味着 Puppet 客户端只需要接收较小的基础设施 CRL,这对于有大量服务器更替的环境非常重要。将维护以下文件:

  • Infra_inventory.txt:CA 为基础设施服务器签名的证书列表

  • Infra_serials:此文件包含基础设施服务器新序列号的递增计数器

  • Infra_crl.pem:基础设施服务器的 CRL

如果您的组织需要使用外部 CA,可以使用组织自己的根 CA,并通过puppetserver ca import命令导入它(完整过程请参考puppet.com/docs/puppet/latest/server/intermediate_ca.html),让 Puppet 充当中间 CA。或者,可以通过部署一个单独的外部生成的根 CA 和签名 CA 来禁用 CA 服务,详细说明请参见puppet.com/docs/puppet/latest/config_ssl_external_ca.html。本书不推荐使用此方法,因为它需要自动化证书分发,而 Puppet 服务不再执行此操作。

当代理向 CA 发出请求时,CSR 会被发送,默认情况下,签名策略需要等待手动签名,CSR 存储在requests文件夹中。待签名的请求可以通过运行puppetserver ca list进行查看,然后通过运行puppetserver ca sign --certname < certname to sign >进行签名。所有已签名的证书可以通过运行puppetserver ca list --all来查看。

如果您使用 PE,可以在 PE Web 控制台上执行和查看证书签名,如图 10.2所示:

图 10.2 – PE 控制台证书签名

图 10.2 – PE 控制台证书签名

可以使用puppetserver ca revoke --certname < certname to revoke >命令吊销证书,并且可以运行puppetserver ca clean --certname < revoked certname >来清理并从 CA 中删除被吊销的证书。

在使用手动自动签名的工作流中,像 VMware 的vRealize OrchestratorVRO)这样的工具通常会在部署和退役服务器时调用 CA API。

为了自动化此过程,可以通过三种方式配置自动签名。将 autosign = true 添加到 puppet.confmaster 部分时,该更改会导致 CA 签署任何请求,但绝不应在生产环境中使用。

第二种方法是在 /etc/puppetlabs/puppet/autosign.conf 创建一个 autosign.conf 文件。在此文件中,可以包含服务器名称或域名通配符,每一行代表一个可以自动签名的节点名称或域名。例如,假设文件内容如下:

server1.puppet.com
*.example.com

这意味着 server1.puppet.comexample.com 域中的任何服务器都会被自动签名。

第三种方法是将 autosign 值设置为 puppet.conf 文件中的一个脚本。该脚本可以是任何语言编写的,并且将接收证书名称作为第一个参数,然后将 CSR 内容作为标准输入。脚本应以零返回码结束以进行签名,或者以非零返回码结束以不进行签名。这导致了一个常见的方法,即在 CSR 中包含一个用于检查的秘密,或者在公共云中使用标签。讨论编写这些脚本超出了本书的范围,尽管 Puppet 只提供了如何构建这些脚本的说明,地址为 puppet.com/docs/puppet/latest/ssl_autosign.html#ssl_policy_based_autosigning,而亚马逊在 aws.amazon.com/blogs/mt/aws-opsworks-puppet-enterprise-and-an-alternate-implementation-for-policy-based-auto-signing/ 提供了一个很好的示例。

本节已阐述了如何配置 CA 并将其作为 Puppet 服务器运行。本章稍后将回顾代理的完整生命周期,展示客户端如何创建 CSR 并使用 CA 完成 Puppet Server 提供的服务,并查看 JRuby 解释器。

JRuby 解释器

JRuby 是 Ruby 的 Java 实现,允许在 JVM 上使用 Ruby;这比传统的 Ruby 部署(如 Ruby on Rails)具有更好的可扩展性,因为大多数 Ruby 解释器不支持线程安全,且使用锁来一次运行一个线程。Puppet Server 拥有一个 JRuby 解释器/实例池,这些实例可以执行各种应用程序工作,如编译目录和处理报告。池中的解释器数量反映了可以同时运行的 Ruby 应用程序操作的数量,可以通过 puppetserver.conf 文件中的 max-active-instances 参数配置,或通过控制台中的 Hiera 在 PE 中配置,或通过 puppet_enterprise::master::puppetserver::jruby_max_active_instances 在代码中配置。我们将在 第十三章 中更详细地讨论这一点,届时我们将讨论用于审查和设置此大小的度量标准和工具。

在讨论完 Puppet Server 的组件后,我们将查看诸如用户、日志记录和文件系统等配置,以了解这些服务可以如何定制以及它们的要求。

Puppet Server 的配置和日志

我们在讨论每个组件时简要提到了某些配置文件和可用设置,但我们将在此总结。对于大多数配置文件,通常不需要进行自定义,大多数默认设置就能满足您的要求。

对于 PE,pe-puppetserver Puppet Server 服务将在 pe-puppet 账户下运行,而在开源 Puppet 上,puppetserver 服务将在 puppet 账户下运行。在这两个账户中,它们将设置 nologin shell,以便用户仅提供一个账户来运行服务并拥有服务相关的文件。

以下配置文件和应用目录将被创建并使用:

  • /etc/puppetlabs/puppetserver/bootstrap.cfg:此文件包含 Trapperkeeper 应启动的服务列表;这些是由嵌入式 Web 服务器挂载的处理程序。

  • /etc/puppetlabs/puppetserver/request-logging.xml:定义 HTTP 访问请求如何被记录的文件。

  • /etc/puppetlabs/puppetserver/conf.d:此目录包含以下主要的 HOCON 格式配置文件:

    • global.conf:此文件为 Puppet 设置全局配置,默认仅包含日志配置文件的位置。

    • webserver.conf:此文件配置嵌入式 Web 服务器的细节,如端口和日志记录。

    • web-routes.conf:此文件为 Puppet 的 Web 服务设置挂载点。

    • puppetserver.conf:此文件设置核心 Puppet Server 应用程序的配置,例如正在运行的 jruby 实例数量。

    • auth.conf:此文件设置由 web-routes.conf 挂载的端点的访问权限。

    • ca.conf:此文件配置 CA 的设置。

    • products.conf:一个可选文件,可以设置产品设置,如分析数据和更新检查。

  • /etc/puppetlabs/puppetserver/ssl/ca:与 Puppet CA 相关的证书和密钥(在 Puppet 6 及以下版本中为 /etc/puppetlabs/puppet/ssl/ca)。

  • /opt/puppetlabs/puppet/lib/ruby/vendor_gems:Puppet Server 将与 CA 操作相关的 Ruby gems 放置在此目录中。

  • /opt/puppetlabs/server:此目录包含用于运行 Puppet Server 的 JRuby-gems 和二进制文件。

  • /var/run/puppetlabs/puppetserver/puppetserver.pid:此文件包含正在运行的 Puppet 进程的 PID。

  • /etc/puppetlabs/puppet.conf:此文件包含主机上 Puppet 客户端和 Puppet Server 的配置。可以通过运行 puppet config print 查看这些设置。

文件中的绝大多数设置将使用默认值,除非需要外部集成,如外部根 CA 等,这些设置只作为参考来帮助理解 Puppet 的配置。有关设置的完整参考和选项,可以在puppet.com/docs/puppet/latest/server/configuration.html查看/etc/puppetlab/puppetserver基础设置。

注意

如果您选择了引言中提到的某个开源 Puppet 自动化工具/模块,它可能在安装时允许设置配置值。

PE 用户应注意,由于配置的自动化程度较高,许多设置(例如puppetserver.conf中的设置)是通过 Hiera 配置的,应遵循puppet.com/docs/pe/2021.7/config_puppetserver.html中的文档进行配置。

调整这些设置的配置将在第十三章中详细讨论。

/etc/puppetlabs/puppet.conf的完整设置选项可以在www.puppet.com/docs/puppet/latest/config_file_main.html查看;该文件本身提供了配置 Puppet 服务器、Puppet 代理,以及puppet apply运行方式的各个部分。各部分包括main(提供默认值)、agent(为 Puppet 客户端提供设置)、user(提供使用 Puppet apply时的设置),以及master/server(用于将设置应用于 Puppet 服务器)。

自 Puppet 6 版本以来,已可以使用server部分代替master部分,但许多自动化工具尚未跟进这一变化,由于它们不是可互换的术语,且可能会引起混淆,因此请小心,仅使用与您的实现相关的术语。

Puppet 首先应用来自master/serverapplyagent部分的设置,然后回退到main部分,如果找不到设置,则会使用默认值。

让我们看一下在peadm构建的 Puppet 实验室服务器上某个文件的示例内容:

[master]
node_terminus = classifier
storeconfigs = true
storeconfigs_backend = puppetdb
reports = puppetdb
certname = pe-server-davidsand-0-cffe02.tq2kpafq5bsehkpub4ur5a35ya.xx.internal.cloudapp.net

方括号表示一个部分的名称,后面跟着一组键值对。这里的设置展示了我们 Puppet 服务器的证书名称(certname),还表明它通过reports设置将报告发送到 PuppetDB,设置为storeconfigs=true时,它会存储目录、节点和事实信息,这些信息将存储在 PuppetDB 中,并且storeconfigs_backend设置为 PuppetDB。最后,node_terminus设置为classifier,这反映了主服务器应如何分类客户端。这个内容将在下一章中详细讨论。

查看和操作设置(包括puppet.conf中未设置的默认值)最好的方法是使用puppet config命令,它可以显示所有设置。通过运行puppet config print all known,设置将被打印出来,或者可以通过详细说明部分和要打印的值来打印单个设置,命令为puppet config print --section master certnamepuppet config命令还可以使用setdelete选项添加或删除值,并选择一个部分键和值来执行操作。例如,以下命令将从master部分删除storeconfigs并将证书名称更改为newname.example.com

puppet config delete --section master storeconfigs
puppet config add --section master certname newname.example.com

这些命令将在文件中没有相应部分时自动添加该部分,但 Puppet 服务需要重启以使任何更改生效。

在下一部分我们将通过更多示例操作puppet.conf文件,查看代理生命周期,但puppet.conf文件的完整选项和语法可以查看puppet.com/docs/puppet/latest/config_file_main.html

默认情况下,Puppet 服务器会将日志保存在/var/log/puppetlabs/puppetserver下的以下文件中:

  • Puppetserver.log:这是记录主要服务器活动(如编译错误和警告)日志的地方。

  • Puppetserver-access.log:这是记录对 HTTP 端点的请求的地方。

  • Puppetserver_gc.log:这是收集垃圾回收日志的地方。

现在我们已经全面回顾了 Puppet 服务器组件,接下来我们将查看 Puppet 代理的配置和生命周期,了解这些服务如何被客户端使用,以及如何监控和查看一个周期的日志。

Puppet 代理到服务器的生命周期。

本节将讨论 Puppet 代理如何向我们运行的 Puppet 服务器组件发出请求,以及它在请求配置以强制执行时如何确保其通信安全。需要注意的是,Puppet 服务器本身也包含 Puppet 代理。

Puppet 代理的安装详细信息请参见puppet.com/docs/puppet/latest/install_agents.html#install_agents(开源)和puppet.com/docs/pe/2021.7/installing_agents.html#installing_agents(PE)。将此安装与服务器部署工作流集成,并确保将必要的配置放置在/etc/puppetlab/puppet.conf中,对于自动化至关重要。

注意

puppet_conf模块提供了管理 Puppet 配置文件的任务(forge.puppet.com/modules/puppetlabs/puppet_conf)。

大多数设置将取决于你的环境配置,但对于大多数环境,默认设置将会被采用,关键设置是确保在agent部分的服务器设置已正确配置,以便代理知道应联系哪个 Puppet 服务器——开源 Puppet 或 PE-Puppet。然后可以在 root 用户下启动 PE 服务。默认情况下,这将每 30 分钟联系一次 Puppet 服务器,或者可以通过运行puppet agent -t命令手动触发。

图 10.3 显示了该 Puppet 证书过程的工作流,客户端确保其拥有签名的 SSL 证书以确保与 Puppet 服务器的安全通信:

图 10.3 – Puppet 客户端证书工作流

图 10.3 – Puppet 客户端证书工作流

第一步是验证证书。在ssl目录/etc/puppetlabs/puppet/ssl中,以下文件将已经存在或在此过程中创建:

  • private_keys/<certificate_name>.pem:用于创建 CSR 的私钥

  • certs/<certificate_name>.pem:返回的为该客户端签署的证书

  • certs/ca.pem:从 Puppet 服务器发送的 CA 证书副本

  • crl.pem:来自 Puppet 服务器的 CRL

  • certificate_requests/<certificate_name>.pem:将发送到 Puppet 服务器的 CSR,在收到签署的证书后将被删除

除了此目录,还可以创建一个/etc/puppetlabs/puppet/csr_attributes.yaml文件,并在其中包含将被包含在 CSR 中的受信事实。这将导致在 Puppet 服务器签署 CSR 时,受信事实被包含在客户端的证书中。

使用受信事实可以确保硬性分类信息不会被更改,例如将生产服务器重新分类为开发环境,或更改角色,因为这两者都可能导致安全性降低。组织 IDOID)号码转换为名称,可以在puppet.com/docs/puppet/latest/ssl_attributes_extensions.html查看。此文件必须在创建 CSR 之前存在;否则,唯一的方法是重新开始来更改 CSR 或证书。

图 10.3所示,如果私钥不存在,客户端会在检查本地的ca.pemCRL.pem副本之前,先生成一个新的密钥,并向服务器发起请求,若这两者中的任何一个不存在,则进行下载。接下来,客户端会检查是否存在已签名的证书,如果没有,则向客户端请求该证书。如果存在已签名的客户端证书,客户端可以继续请求节点数据;否则,客户端会创建一个 CSR 文件并将其发送到主服务器。如果在puppet.conf中启用了waitcert设置,客户端将等待 CSR 由服务器签名,并每 2 分钟检查一次主服务器的状态。在未来的运行中,客户端会向服务器提供其已签名的证书,以证明其身份。

在确保了安全通信之后,第一步是从服务器到客户端执行插件同步,确保所有的事实、功能、资源类型、资源提供者和 Augeas 镜头都通过file_metadata端点下载到客户端。

一旦完成此步骤,客户端运行facter,将输出发送到 Puppet 客户端,并通过\catalog端点向 Puppet 服务器请求目录。该目录的副本将存储在客户端的cache目录中(通过在puppet.conf中配置vardir参数)。默认情况下,路径为%PROGRAMDATA%\PuppetLabs\puppet\cache\client_data\catalog\<certname>.json。(PROGRAMDATA通常是C:\Program Data\,在 Linux 和 Unix 系统上为/opt/puppetlabs/puppet/cache/client_data/catalog/<certname>.json。)客户端接收此目录并执行步骤,强制执行 Puppet 代码中描述的状态,或者如果客户端设置为以无操作模式运行,则模拟该目录。客户端生成报告,并默认通过report端点将其发送回 Puppet 服务器。此操作可以配置为将报告发送到其他报告处理器,例如 Splunk,这将在第十三章中讨论。

除了client_data文件夹中的目录外,还会在cache目录中生成其他几个用于调查的有用文件:

  • lib:这是由插件同步从主服务器同步的各种插件的缓存。

  • facter:此文件将包含自定义事实。

  • facts.d:在这里,外部事实由插件同步从主服务器缓存。

  • reports:包含最后生成的报告文件。

  • state:此目录包含与先前 Puppet 运行状态相关的文件和目录:

    • classes.txt:列出上次应用的目录中包含的类。

    • graphs:如果在 Puppet 运行期间使用了graph选项,生成的资源和依赖关系的.dot 图形文件将保存在这里。

    • last_run_report.yaml:这是一个完整的报告,列出所有资源以及它们在目录强制执行期间如何被检查或更改。

    • resources.txt:上次应用的目录中包含的资源列表

    • state.yaml:所有资源的列表及其上次检查或同步的时间,用于诸如audit等功能

一些目录和文件已被忽略,因为它们要么是为了遗留目的,要么是为了本书不推荐的做法,例如filebucket。完整列表请参见puppet.com/docs/puppet/latest/dirs_vardir.html

注意

如果客户端与 Puppet 基础设施失去连接,缓存的目录将用于确保继续执行其最后已知的状态。

代理到服务器的最后一步是将事件报告发送到 Puppet Server。这些报告将反映目录中每个资源的事件。这些事件可能具有以下状态之一:

  • 失败 – 这是一个应用目录时出错的事件,或是如依赖关系问题或该特定资源的问题

  • 修正 – 资源在上次运行时是正确的,但必须修正

  • 有意的 – 资源必须创建或修正,但在上次运行时状态不正确

  • 未更改 – 资源处于正确状态,无需更改

默认情况下,未更改的事件不会在 Puppet 8 中报告。此更改是为了减少存储报告所需的存储空间。可以通过在每个代理的puppet.conf文件中设置exclude_unchanged_resources=false来更改此设置。

报告事件还将反映客户端代理运行的模式,或资源是否设置为与客户端的应用方式不同。尽管相同的事件状态仍然适用,但每个事件将报告该事件是在执行模式下发生,还是在无操作模式下发生。如在第三章中所讨论的,无操作模式意味着资源只是有效地测试,看资源是否需要更改以符合声明的状态。在第十五章中,我们将讨论如何在遗留环境中使用此方法,查看配置漂移的大小,并选择逐步的方法,以避免在生产系统中造成问题。

关于访问这些报告,我们将在第十三章中看到如何使用报告处理器将其发送到第三方工具,并在第十四章中看到 Puppet Enterprise 如何作为其图形控制台的一部分提供事件查看器界面。

实验室 – 监控证书签名日志

为了更好地理解这个过程,我们将描述如何通过删除节点的证书并重新注册来监控 Puppet 运行的过程。在注册过程中,我们将监控日志以查看此过程中的 API 请求,并记录过程步骤。以下是步骤:

  1. 打开 SSH 终端会话到 Linux 客户端,并为主 Puppet 服务器打开两个独立的 SSH 终端会话。

  2. 在 Linux 客户端上,运行以下命令:

    puppet ssl clean
    
  3. 在其中一个服务器会话中,运行puppetserver ca clean --certname <实例名称>(请注意,这应该是证书名称,可以通过在节点上运行puppet config print certname来检查)。

  4. 在 Linux 客户端上,使用以下命令将ssl目录移动到备份位置:

    mv /etc/puppetlabs/puppet/ssl /etc/puppetlabs/puppet/ssl.old
    
  5. 在其中一个 Puppet 服务器会话中,运行tail -f /var/log/puppetlabs/puppetserver/puppetserver-access.log,而在另一个会话中,运行tail -f /var/log/puppetlabs/puppetserver/puppetserver.log

  6. 在节点上运行puppet agent -t并查看 Puppet 服务器会话中的调用。

  7. 在 Web 控制台中,使用客户端上的puppet agent –t。注意服务器中的access.logpuppetserver.log文件中的新调用,并了解这些如何与本节中讨论的步骤相关。

  8. 查看为客户端接收到的目录,并检查缓存中的其他文件。

提示

使用像jq这样的工具可以使查看 JSON 更加轻松 (stedolan.github.io/jq/download/)。

要查看此实验的日志输出示例,请参见以下文件:

github.com/PacktPublishing/Puppet-8-for-DevOps-Engineers/blob/main/ch10/puppet_access_log_extract 显示了带有注释的访问日志,解释了输出内容

github.com/PacktPublishing/Puppet-8-for-DevOps-Engineers/blob/main/ch10/puppet_server_log_extract 显示了 Puppet 服务器日志,带有注释,解释了输出内容

github.com/PacktPublishing/Puppet-8-for-DevOps-Engineers/blob/main/ch10/puppet_client_terminal.txt 显示了客户端终端和输入的命令

github.com/PacktPublishing/Puppet-8-for-DevOps-Engineers/blob/main/ch10/puppet_server_terminal.txt 显示了服务器终端和输入的命令

PuppetDB 和 PostgreSQL

PuppetDB 允许收集 Puppet 数据和一些高级功能,如导出的资源。在开源 Puppet 中,它是完全可选的,而 PE 默认安装 PuppetDB。以下是 PuppetDB 保存的内容:

  • 来自节点的最后事实

  • 为每个节点编译的最后一个目录

  • 每个节点的事件报告默认为 14 天

  • 导出的资源

PuppetDB 是一个运行在 JVM 上的 Clojure 前端应用程序,使用 PostgreSQL 作为后端数据库。这种常见架构是后端数据库只提供表格,而前端数据库包含应用程序对象,相较于单一数据库,这具有一些关键优势。它简化了 PuppetDB 的更新过程,因为实际数据可以保留在后端表中,并且它还允许良好的可扩展性——正如我们将在本章的最后一节中看到的,通过编译器扩展——PuppetDB 可以通过在多个编译服务器上运行 PuppetDB 进行水平扩展,从而减轻主服务器 PuppetDB 服务的负载。

有关 PuppetDB 安装和配置的信息,请参阅forge.puppet.com/modules/puppetlabs/puppetdb。PuppetDB 可能会包含在您选择的任何自动化工具中,并且是 PE 的一部分。

PostgreSQL 为 PE 创建一个pe-postgres用户,或为开源 Puppet 创建一个postgres用户,该用户用于运行 PostgreSQL 数据库。此用户将使用nologinshell 并拥有运行 Postgres 所需的相关文件。PostgreSQL 使用以下目录:

  • /opt/puppetlabs/server/apps/postgresql/{version}:用于安装数据库应用程序

  • /opt/puppetlabs/server/data/postgresql/{version}:用于存储数据库的数据文件

  • /var/log/puppetlabs/postgresql/{version}:用于存储数据库的日志

PuppetDB 为 PE 创建一个pe-puppetdb用户,或为开源 Puppet 创建一个puppetdb用户,该用户用于在nologinshell 下运行 PuppetDB 数据库,并拥有运行 PuppetDB 所需的相关文件。由于 PuppetDB 是一个运行在 JVM 上的 Clojure 应用程序;它在结构上与 Puppet Web 服务器非常相似,具有挂载在/pdb端点的处理程序,并且通过auth.conf文件定义谁可以访问此端点。PuppetDB 使用以下目录,并突出显示了一些关键文件:

  • /etc/puppetlabs/puppetdb:此目录包含 PuppetDB 的配置文件,包括以下内容:

    • bootstrap.confbootstrap.conf文件列出了应在 Trapperkeeper 框架中启动的服务
  • /etc/puppetlabs/puppetdb/conf.d:此目录包含ini格式的配置文件:

    • auth.conf:配置谁可以访问已公开的端点

    • routing.ini:配置哪些处理程序应在端点处公开

  • /opt/puppetlabs/server/apps/puppetdb:此目录包含 PuppetDB 的应用程序二进制文件

  • /opt/puppetlabs/server/data/puppetdb:此目录包含 PuppetDB 的数据

本书的范围不包括深入探讨PuppetDB的配置,但你可以参考puppet.com/docs/puppetdb/latest/configure.html获取更多信息。然而,在第十三章中,我们将更深入地探讨如何监控、审查和优化 PuppetDB 和 PostgreSQL 性能,以及如何使用像forge.puppet.com/modules/puppetlabs/pe_databases这样的模块来帮助维护。

目前,我们将回顾如何通过 PQL 和 HTTP 调用访问数据,使用/pdb端点,或者通过puppet query命令行调用端点。

注意

抽象语法树AST)查询语言也可以作为查询格式使用。然而,随着 PQL 的使用,它现在几乎没有用处,但可以通过www.puppet.com/docs/puppetdb/8/api/query/v4/ast.html查看。

PuppetDB 被结构化为多个实体,以便访问不同类型的数据。以下是每个实体的列表以及它所包含的端点简要描述:

  • aggregate_event_countsevent_counts实体的汇总计数

  • catalogs:每个节点存储的目录

  • edges:边是目录中关系信息,如包含依赖

  • environments:PuppetDB 已知的环境

  • event_counts:报告中关于各个资源的事件计数

  • events:事件反映了报告中执行的资源操作

  • facts:每个节点返回的事实

  • fact_contents:此实体结构化为更方便地访问事实内容

  • fact_names:所有已知的事实名称

  • fact_paths:类似于fact_names实体,但为结构化事实提供了进一步的粒度

  • nodes:节点信息

  • producers:生成器是编译目录并发送报告的服务器

  • reports:报告包含应用目录的结果

  • resources:目录中的资源信息

要开始查看 PQL 查询,最简单的方式是返回实体中的所有数据。这可以通过简单列出实体名称和空的大括号来完成。例如,要返回所有节点数据,可以使用nodes {};要在大括号中查找具有特定参数的节点,使用属性名称及其应等于(=)、包含(~)、小于(<)或大于(>)的值。例如,要返回最后报告状态未改变的节点,查询为nodes { latest_report_status = "``unchanged"}

我们不会列出这些查询的输出,因为它们可能非常冗长,但你将在本节末尝试在实验中制作一些示例。

这些属性语句可以通过 ! 进一步否定,使用 and/or 链接,并用括号 () 括起来以包含不同的语句。例如,要进行更复杂的查询,查找某个文件是否以错误的权限声明,我们可以运行此 PQL 查询:

resources { (type = "File" and title = "/etc/motd") and ! ( parameters.mode = "0644" and parameters.owner ="root") }

在命令行中,这也可以通过 puppet query resource {'latest_report_status = "``unchanged"}' 来运行。

PuppetDB 查询还可以在 Puppet 代码中使用 PuppetDB 函数。以下是一个示例:

$changed_nodes = puppetdb_query(node[certname]{ resource {'latest_report_status = "unchanged"}}) .map |$value| { $value["certname"] }
notify {"Nodes changed":
    message => "The following nodes changed on their last run ${join($changed_nodes, ', ')}",
}

在所有这些示例中,假设证书已经设置,以便通过 Puppet 基础设施或运行查询的客户端进行安全的 SSL 通信。如果使用默认位置,puppet query 命令会自动拾取证书,但也可以像这样进行设置:

puppet query '<PQL query>' \
  --urls https://puppetdb.example.com:8081 \
  --cacert /etc/puppetlabs/puppet/ssl/certs/ca.pem \
  --cert /etc/puppetlabs/puppet/ssl/certs/<certname_of_local_host>..pem \
  --key /etc/puppetlabs/puppet/ssl/private_keys/<certname_of_local_host>..pem

Web 点也可以通过 curl 或等效的命令访问,如下所示:

curl -X GET <fqdn_of_puppetDB_host>https://<fqdn_of_puppetDB_host>:8081/pdb/query/v4\
  --tlsv1 \
  --cacert /etc/puppetlabs/puppet/ssl/certs/ca.pem \
  --cert /etc/puppetlabs/puppet/ssl/certs/<certname_of_local_host>.pem \
  --key /etc/puppetlabs/puppet/ssl/private_keys/<cert_name_of_local_host.pem \
  --data-urlencode 'query=<PQL query>'

为了允许从桌面或其他节点直接进行查询,可以使用 Puppet 客户端工具。关于在 Open Source Puppet 上安装的设置说明详见puppet.com/docs/puppetdb/latest/pdb_client_tools.html,而 Puppet Enterprise 的安装说明可参考www.puppet.com/docs/pe/2021.7/installing_pe_client_tools.html

另外,可以通过按照puppet.com/docs/puppetdb/latest/configure.html#jetty-http-settings中的说明,禁用 SSL 身份验证,以允许未经身份验证的查询。本书强烈建议不要这样做,因为这会使网络上的任何人都能访问数据。

在本节中,我们展示了一些可以与 PQL 一起使用的实体和查询。虽然逐一列举这些实体的所有可能选项以及 PQL 可用的选项范围是不现实的,但完整的详细信息可以在文档中查看:puppet.com/docs/puppetdb/latest/api/query/v4/entities.html。此外,更多的 PQL 查询示例可以在文档中查看:puppet.com/docs/puppetdb/latest/api/query/examples-pql.html,而 Vox Pupuli 社区正在其网页上建立有用的示例,网址为voxpupuli.org/docs/pql_queries/

实验 – 查询 PuppetDB

SSH 连接到主服务器并查询 PuppetDB 获取以下信息:

  • 列出所有编译器服务器的内存大小(提示:编译器服务器都有一个受信任的事实,Facter 也有内存事实)。

  • 列出在 Puppet 服务器上强制执行的所有服务。

  • 列出每个服务器最新报告的开始和结束时间。

示例答案可以在github.com/PacktPublishing/Puppet-8-for-DevOps-Engineers/blob/main/ch10/PQL_samples_answers.txt找到。

注意

在大规模生产系统中使用这些查询时要小心;某些端点(如报告)可能包含大量数据,查询可能会给系统带来很大压力和负载。

使用编译器进行扩展

  1. 到目前为止,Puppet 平台组件的回顾假设所有组件都位于单一的主服务器上。然而,随着托管节点数量的增加,单一服务器处理这些节点变得不切实际。根据 Puppet 的文档,默认设置下,主服务器最多可以管理 2,500 个客户端。为了处理日益增长的节点数量,Puppet 采用了水平扩展,使用了 Puppet 编译服务器。在图 10.4中,显示了一部分主服务被移到编译服务器上。这些服务器可以在客户端的配置文件中配置为轮询选择,或放置在负载均衡器后面。这使得多个节点可以协同工作来编译目录,同时仍然允许某些服务在主服务器上运行。根据 Puppet 的文档,在默认的编译器设置下,每个编译器最多可以服务 3,000 个客户端:

图 10.4 – Puppet 编译器服务

图 10.4 – Puppet 编译器服务

编译服务器托管着主服务器上存在的一部分服务,例如 Puppet Server 和 PuppetDB。这使得能够远程完成和同步目录编译请求,从而增加了编译目录所需的 JRuby 实例数量。

  1. 将客户端请求指向编译服务器的最常用方法是利用硬件或基于云的负载均衡器。由于有多种负载均衡器可供选择,Puppet 并未提供明确的配置指导。然而,它建议使用/status/v1/simple端点来检查编译服务器的健康状况。如果负载均衡器不支持 HTTP 健康检查,可以检查主机是否在端口8140上侦听 TCP 连接,这可以提供有限的检查。

  2. 还有一些替代负载均衡器的方法,例如使用 DNS SRV 记录,详细信息可以参考puppet.com/docs/puppet/latest/server/scaling_puppet_server.html#using-dns-srv-records,或者使用具有轮询设置的 DNS 条目,详细信息可以参考puppet.com/docs/puppet/latest/server/scaling_puppet_server.html#using-round-robin-dns,但由于这些方法较少使用,本书不会详细讲解。

注意

puppet.conf文件中,可以将多个服务器列表添加到客户端服务器值中,以便联系,但该列表仅在发生故障时有效,并不会尝试平衡连接。

  1. 对于编译服务器,CA(证书颁发机构)仍然保留在单一的 Puppet 主服务器上,并在客户端发送其 CSR 或证书进行检查时进行回溯。

  2. 正如本章开头所述,我们将避免详细讨论安装过程,因为这对 Puppet 自带的说明并没有太大帮助,相关说明可以在puppet.com/docs/puppet/latest/server/scaling_puppet_server.html(开源版本)和puppet.com/docs/pe/2021.7/installing_compilers.html(PE 版本)中找到。然而,必须注意,如果在 TCP 代理模式或 DNS 轮询方法中使用负载均衡器,则编译服务器可能需要在其puppet.conf文件中添加dns_alt_names。这是为了启用所有可能在负载均衡器请求中使用的服务器名称。

  3. 即使启用了负载均衡器,也可以通过运行puppet agent -t server=<server to send request>直接定向到编译服务器。

  4. 第十三章中,我们将提供有关如何监控和管理服务器设置以实现可扩展性的更详细信息,而在第十四章中,我们将讨论 Puppet 的参考架构以实现可扩展性。然而,重要的是要注意,如果编译服务器距离主服务器过远,可能会出现延迟问题。因此,建议根据最佳实践将它们保持在同一区域内(云计算术语中)。

实验 – 查看编译器和负载均衡器配置

部署的实验环境由三个编译服务器组成。你可以查看它们正在编译的报告以及 pecdm 如何配置负载均衡器,具体如下:

  1. 登录到网页控制台并查看 Puppet 实例服务器的报告运行。在报告的Metrics部分,查找Report submitted by部分,并注意这可能在不同报告中有所不同。如果报告数量较少,请进入Jobs部分,并多次运行 Puppet,以生成更多报告。

  2. 查看 PECDM 如何在 Terraform 模块中创建 Azure 负载均衡器,网址为github.com/puppetlabs/terraform-azure-pe_arch/blob/main/modules/loadbalancer/main.tf

总结

在本章中,我们了解了 Puppet 服务器提供的服务以及嵌入式 Web 服务器如何将处理程序附加到挂载点,这些挂载点可以通过 HTTP 请求访问。

展示了/puppet端点为配置请求提供服务,演示了 indirectors 或环境如何请求特定的组件,例如从服务器请求目录。/puppet-ca端点同样通过 indirectors 允许向 CA 发出请求。接着展示了/puppet-admin-api端点,允许清除环境缓存和 JRuby 实例,这是更高级的管理操作。

接着展示了 Puppet 如何创建一个包含根 CA 和中间 CA 的 CA 服务器来签名证书,或者可以在传统模式下运行,使用单一合并的 CA。然后讨论了使用外部提供的证书的选项。展示了证书请求签名的过程,使用puppetserver certificate命令管理证书和请求,使用puppet ssl命令管理代理证书管理。接着展示了如何通过自动签名来自动化这个过程,可以根据命名规则或通过运行脚本来自动签署所有请求。

讨论了 JRuby 解释器,展示了 JRuby 是 Ruby 在 Java 上的实现,能够以可扩展和并发的方式运行 Puppet 的 Ruby 组件,比如编译 Puppet 代码。

展示了用户、服务、配置文件和日志的概述,检查了puppet.conf的服务器端配置,并展示了如何配置和查看文件中的设置,以及如何使用puppet config命令查看默认值。

在回顾了 Puppet Server 的组件之后,接下来查看了 Puppet 客户端生命周期,了解了代理如何向 CA 发出 CSR 请求,并发送事实信息和目录请求。查看了日志,展示了请求的位置以及如何通过请求进行追踪。展示了如何通过puppet.conf配置客户端,以及如何向 CSR 添加额外的信息。

接着探讨了 PuppetDB 和 PostgreSQL,作为前端/后端数据库架构,能够存储通过应用 Puppet 目录生成的报告,以及来自节点的最新事实和事件。我们回顾了文件目录和日志位置,接着查看了如何使用 PQL 在 API、命令行和 Puppet 代码中查询 PuppetDB。

接着展示了编译器如何允许 Puppet Server 水平扩展,支持将 Puppet Server 和 PuppetDB 服务部署到多个服务器上,并为客户端进行负载均衡。

在下一章中,我们将展示 Puppet 如何对请求目录编译的客户端进行分类,以便它知道应用哪个版本的代码以及哪些类。我们将展示如何通过环境使多个版本的代码共存于主服务器,并展示如何使用控制库来管理应该包含的模块和版本。

第十一章:分类与发布管理

本章的重点将是 Puppet 如何部署代码,并将这些代码分类到服务器上。首先将讨论环境,展示如何创建具有特定模块版本的服务器隔离组。我们将讨论如何提供静态和临时环境。我们将展示现代 Puppet 如何使用基于目录的环境,将环境代码放在特定位置,如 site.pp 主清单文件或一组清单文件,并通过这些节点定义中的 Hiera 查找,或者通过主服务器运行的 外部节点分类器 (ENC) 脚本来实现。还将讨论 Puppet Enterprise 中 分类服务 的实现,展示如何在这些解决方案的基础上构建,使用其自身的 ENC 脚本,并增加了在 Web 控制台中使用节点组的额外功能。

将详细查看 Puppet 代理的运行,展示其中的步骤,以及在编译目录时,数据是如何加载、缓存和刷新。

接下来,将展示如何使用控制库结构和 Puppetfiles 管理模块,以便使用 r10kg10k 将代码部署到环境中,并讨论根据本地基础设施的配置使用不同方法来同步代码。然后将讨论特定于 PE 的实现 r10k

在审视了分类与发布管理的技术结构后,将重点放在使用这些技术与受监管流程和多个团队合作时面临的挑战与局限性。

在本章中,我们将涵盖以下主要内容:

  • Puppet 环境

  • 理解节点分类

  • Puppet 运行

  • 管理和部署 Puppet 代码

  • 实验—分类与部署代码

技术要求

github.com/puppetlabs/control-repo 克隆控制库到你的 controlrepo-chapter11 GitHub 账户,并更新此库中的以下文件:

Puppet 环境

Puppet 环境是一种定义用于服务器组的特定版本模块、清单和数据的方法。不幸的是,环境是一个在组织中用于其他目的的通用技术术语,很容易造成混淆。最好的建议是在讨论 Puppet 之外的内容时,始终使用Puppet 代码环境,以防止 Puppet 环境与其他任何东西直接关联。

现代 Puppet 环境是基于目录的动态环境,这意味着 Puppet 服务器——或者在puppet apply的情况下,客户端——将查找分配的环境是否存在于一个目录中。多个变量设置了相关目录的位置,包括environments目录本身,我们强烈建议将所有这些设置保持为默认值,以避免混淆和问题。接下来我们将了解环境中的代码目录和路径的层级。

环境目录和路径

第一层是由puppet.conf中的codedir变量设置的代码和数据目录,默认值为 Unix 上的/etc/puppetlabs/code,Windows 上的%PROGRAMDATA%\PuppetLabs\code(通常为C:\ProgramData\PuppetLabs\code)。Puppet Server 不使用puppet.conf中的codedir设置,而是使用puppetserver.conf中的jruby-puppet.master-code-dir,因此如果更改了这两个设置,都需要进行配置。

注意

Puppet 3.3之前,环境是通过puppet.conf文件声明的,每个环境都必须在一个包含modulepathmanifests变量的节中声明。今天的 Puppet 仍然可以技术性地实现这一点,如果没有设置codedir,但没有理由采用这种方式。

代码和数据目录包含两个目录。首先,有一个模块目录,用于提供在puppet.conf中默认的basemodulepath变量中包含的全局用户模块。默认情况下,basemodulepath变量在 Unix 上包含$codedir/modules:/opt/puppetlabs/puppet/modules,在 Windows 上包含$codedir\modules。Unix 上的额外目录由 PE Server 安装使用,用于放置用于配置 PE 的模块。这些模块以pe为前缀,以避免与环境中已经使用的任何模块混淆。

第二个目录是环境目录;根据puppet.confenvironmentpath的默认设置,它是$codedir/environments,并且是查看环境的地方。

注意

codedir目录用于包含全局 Hiera 数据和配置,并且默认使用hiera_config设置。如果找到$codedir/hiera.yaml文件,它将覆盖默认的$confdir/hiera.yaml文件,这个文件现在是标准的,正如在第九章中讨论的那样。

environments目录中,要创建的每个环境都会有一个包含小写字母、数字和下划线的名称的目录。每个环境目录可以包含以下内容:

  • $modulepath指定的目录中的 Puppet 模块

  • 目录中hiera.yaml文件中配置的 Hiera 数据

  • $manifest指定的目录中清单或一组清单中的分类数据

  • 目录中的environment.conf文件中的环境配置数据

在回顾了环境的目录和路径之后,我们将更详细地查看环境配置文件。

环境配置文件

可以在environment目录中的environment.conf文件中设置环境配置数据;该文件具有类似于puppet.conf的 INI 格式,但没有节(sections)。

默认情况下,如果modulepath环境变量没有在environment.conf中设置,它将被设置为$environmentpath/$environment/modules:$basemodulepath

因此,在基于 Unix 的系统中,默认情况下将是以下内容:

/etc/puppetlabs/code/environments/$environment/modules: /opt/puppetlabs/puppet/modules

在 Windows 系统中,它将是这样的:

C:/ProgramData/PuppetLabs/code/environments/production/modules;C:/ProgramData/PuppetLabs/code/modules

请记得使用分号(;)分隔 Windows 系统中的目录列表,使用冒号(:)分隔 Unix 系统中的目录列表。

管理和部署 Puppet 代码部分,我们将讨论如何将模块部署到该目录中,并如何列出modulepath中每个目录的内容。

注意

永远不要将modulepath变量设置为从另一个环境目录读取。在Puppet 运行部分,我们将讨论环境数据被缓存和刷新时可能带来的不一致效果。

manifest变量可以是单个清单文件,也可以是包含多个清单的目录,这些清单将按字母顺序读取。如果路径以正斜杠(/)或句点(.)结尾,Puppet 会将此变量视为包含目录,并能识别它是一个目录。如果environment.conf中没有设置,默认值将是$environmentpath/$environment/manifests目录,对于基于 Unix 的系统,这个路径为/etc/puppetlabs/code/environments/$environment/manifests,对于基于 Windows 的系统,这个路径为C:/ProgramData/PuppetLabs/code/environments/$environment/manifests。目录环境将永远不会使用puppet.conf中的全局manifest设置。在下一节中,我们将更详细地讨论如何使用节点定义和 Hiera 查找来对这些清单进行服务器分类。

environment_timeout变量表示 Puppet Server 将缓存特定环境的时间,并覆盖设置的值。Puppet 建议不要在environment.conf中设置此项,只使用puppet.conf中的全局版本,并且只使用0unlimited。缓存的作用将在本章的Puppet 运行部分进一步讨论。

config_version变量可以设置一个脚本,在目录编译后运行,并将输出作为日志的一部分返回。如果默认没有设置,脚本将返回目录编译时的时间,格式为 Unix 纪元(自 1970 年 1 月 1 日午夜 UTC/GMT 以来经过的秒数)。对于默认的纪元脚本,输出将如下所示:

Info: Applying configuration version '1663239677'

管理和部署 Puppet 代码部分中将展示一个更有用的示例,当使用基于 Git 的部署解决方案时。

注意

environment.confconfig_version脚本可以使用basemodulepathenvironmentcodedir全局变量。

现在我们已经审查了环境配置,了解如何验证配置以及部署的环境类型是非常有用的。

环境验证和部署

可以使用puppet config print命令检查在puppet.confenvironment.conf中讨论的设置,通过部署--environment标志查看特定环境,使用--section查看puppet.conf中的特定部分。例如,要检查puppet.conf中的codedir变量和生产环境中的modulepath变量,可以运行以下命令:

puppet config print codedir
puppet config print --environment production modulepath

默认情况下,Puppet Server 会创建一个生产环境,但运行apply的 Puppet 客户端不会。对于这两种情况,生产环境是 Puppet 默认运行的环境。在本章的下一部分,我们将展示服务器如何被分类到其他环境中。

有三种环境策略:永久性、临时性和组织隔离。永久性环境通常是长期存在的,环境命名通常与服务器的用途相匹配,例如服务器是产品服务器还是开发服务器。临时性环境是在进行变更测试后推广之前可以使用的环境,而组织隔离环境则反映了分割的基础设施,其中不同团队(如 Windows 和 Linux 团队)拥有不同的服务器并且有不同的环境。这些策略可以根据需要结合使用,以满足组织的需求。

既然我们已经了解了 Puppet 代码环境,接下来我们将学习如何根据环境中的使用情况以及该环境中的模块集合来分类客户端。

理解节点分类

节点的分类涉及确定一个节点应该使用哪个环境,应该应用哪些类,以及应该应用哪些参数。理想的情况是为一个主机应用单一的角色类,但业务逻辑可能更复杂。这适用于 Puppet Server 上的代理运行和puppet apply运行。

在定义了什么是节点分类之后,我们将看看可以用于分类的方法,首先介绍节点定义作为最简单的方法。

节点定义

节点分类的最基本方法是使用puppet.conf,其中节点名称与puppet.conf中的certname设置相同,默认为节点的完全限定域名 (FQDN)。

节点定义的语法在这里设置:

  • node关键字

  • 一个作为字符串的节点名称,default

  • 以下 Puppet 代码项的混合,位于花括号({})内:

    • 类别声明

    • 变量

    • 资源声明

    • 收集器

    • 条件语句

    • 链接关系

    • 函数

建议将节点定义控制在最低限度,并仅使用类声明和变量。如果任何清单包含节点定义,则节点定义必须匹配所有节点,否则与不匹配的节点的编译将失败。通常通过确保存在默认定义,即使默认定义不包含任何代码,也能确保安全。

一个节点将只匹配一个节点定义,并按以下优先级进行排序:

  • 完全匹配的名称

  • 正则表达式匹配(多个正则表达式匹配是不可预测的,只有一个会被使用)

  • default(如果节点未能匹配任何其他定义,节点将匹配此关键字)

注意

default之前的优先级步骤会查找主机名的部分匹配,如果puppet.conf主服务器中的strict_hostname_checking设置为false。为了避免这种不安全的匹配,Puppet 5.5.19+ 和 6.13.0+ 默认设置为true,在 Puppet 7 及之后的版本中,已删除该选项。

例如,以下代码将把server1.exampleapp.com分类到role::oracle类别,将server2.exampleapp.comserver3.exampleapp.com分类到role::apache类别。其他以exampleapp.com结尾的服务器将根据操作系统系列分类为role::example_common_windowsrole::example_common_linux,例如server5.exampleapp.com,其他节点将被分类为role::common,例如server1.anotherapp.com

node /.exampleapp.com$ {
  if $facts['os']['family'] {
    include role::example_common_windows
  else
    include role::example_common_linux
  }
}
node 'server1.exampleapp.com' {
  include role::oracle
}
node 'server2.exampleapp.com','server3.exampleapp.com' {
  include role::apache
}
node default {
  include role::common
}

默认情况下,manifest目录中会有一个site.pp文件,以保持简单,但该目录中的多个清单可以包含节点定义,这些定义可以根据组织、用例或所有权来组织文件。显然,拥有大量节点定义并不适用;保持节点定义简洁的推荐方法是使用一个默认定义,该定义查看节点的证书以具有一个pp_role扩展名,包含角色名称,如此代码示例所示:

node default {
  $role = getvar('trusted.extensions.pp_role')
  if ($role == undef) {
    fail("${trusted['certname']} does not have a pp_role trusted fact")
  }
  elsif (!defined($role)) {
    fail("${role} is not a valid role class")
  }
  else {
    include($role)
  }
}

使用getvar函数来避免没有证书的主机出现问题,并使用defined函数确认声明的角色在环境中可见,它将包括证书中声明的角色。

任何在节点定义外应用的代码将适用于所有节点,但像这样设置不受控的全局默认值并不是推荐的做法。在之前的代码块中,使用了角色类,但也可以为例外包含任何类。

本地的 puppet apply 调用将不会查找 puppet.conf 中的 manifest 变量设置,而是会根据命令行传递的内容进行操作,可以通过 –e 标志或传递特定的清单文件来实现。

在查看了基于代码的节点分类方法后,我们现在将查看如何使用 Hiera 数据来对节点进行分类。

使用 Hiera 进行节点分类

可以使用 Hiera 数组和 lookup 函数在默认节点定义中采取更具数据驱动的方法。虽然 lookup 函数可以在节点定义外使用,但我们建议避免这样做,以确保如果为节点特别添加了其他节点定义,它只会应用该节点定义,而不是更难预测的混合结果。

第一步是,如我们在 第九章 中看到的,确保每个环境中有适当的 Hiera 层次结构,假设在 hiera.yaml 环境中有一个简单的层次结构,包括节点、操作系统和默认设置,如此处所示:

datadir: data 
data_hash: yaml_data 
  - name: "Node data" 
    path: "nodes/%{trusted.certname}.yaml"
  - name: "OS defaults" 
    path: "os/%{facts.os.family}.yaml" 
  - name: "Common data" 
    path: "common.yaml

然后,我们可以在 default 节点定义中添加查找:

node default {
lookup( {
  'name'          => 'classes',
  'value_type'    => Array,
  'default_value' => [],
  'merge'         => {
    'strategy' => 'unique',
  },
} ).each | $classification | {
  include $classification
}

虽然将变量命名为 class 看起来更合适,但由于 class 是一个保留字,因此无法这样做。

环境级别的 Hiera 数据可以添加到 common.yaml 文件中,以确保默认情况下服务器获得 core 角色:

---
classes:
  - role::core

然后,在数据文件中创建一个 os/RedHat.yaml 文件,包含以下代码:

---
classes:
  - role::core::redhat

这将确保所有来自红帽家族的服务器,如 CentOS,将被分配到 role::core::redhat 类。要将特定角色分配给服务器,我们创建一个 node/exampleapp.example.com.yaml 文件,包含以下代码:

---
classes:
  - role::docker

这将把 role::docker 类分配给 exampleapp.example.com 节点。

为了允许例外和更复杂的组合设置,可以使用哈希而不是数组,将 site.pp 中的查找策略从唯一查找改为深度合并策略,并将数据从数组改为哈希:

node default {
lookup( {
  'name'          => 'classes',
  'value_type'    => Hash,
  'default_value' => []
  'merge' =>
    'strategy' =>  'deep',
}).each | $classification | {
  include $classification
}

在这种情况下,我们可以使用仅在 Hiera 中可见的键,接管角色构建并直接使用配置文件,设置一个 common.yaml 文件以确保默认分类获得核心配置和安全配置文件:

---
classes:
base profile: profile::core
security profile: profile::security

然后,对于特定的服务器 exampleapp.example.com,可以在 node/exampleapp.example.com.yaml 中设置 security_profile 变量:

---
classes:
security_profile: profile::security::legacy

这将覆盖安全配置文件键,并导致 exampleapp.example.com 被分类为 profile::security::legacyprofile::core

可以构建更复杂的基于 Hiera 的键查找来基于 Facter 值查找,但由于这在本书中不推荐使用,因此已展示足够的细节来理解 Hiera 的使用方法。值得一提的是,example42 的 psick 模块 forge.puppet.com/modules/example42/psick 使用了 Hiera 方法,并且可以用于在 Linux 环境中以预设和分阶段的方式包含模块。只需包含 psick 类并简单地通过哈希设置 Hiera 键,就足以对主机进行分类:

psick::firstrun::linux_classes
psick::pre::linux_classes
psick::base::linux_classes
psick::profiles::linux_classes

在详细审查了分类的代码和数据方法后,我们将介绍使用 ENC 脚本的更高级方法。

ENC 脚本

ENC 是一个脚本,Puppet Server 或 puppet apply 调用可以运行该脚本。该脚本的要求是接收客户端的 certname 参数,并返回一个非零的返回码(表示未知节点)或包含类、参数和环境的 YAML 输出,用于目录编译。在这个 ENC 内部,可以访问各种外部数据源引用,例如 PuppetDB 或你组织的内部数据源。重要的是 ENC 使用的编程语言,而不是它用什么语言编写。

一个示例输出如下所示:

---
classes:
  role::core::windows
  sqlserver_instance:
    features:
      - SQL
    source: E:/
    sql_sysadmin_accounts:
      - myuser
parameters:
  dns_servers:
    - 2001:4860:4860::8888
     - 2001:4860:4860::8844
  mail_server: mail.example.com
  vault_enabled: true
environment: uat

在这个示例中,可以看到服务器将应用 role::core::windows 类,并且会使用 sqlserver_instance 类及其相关参数,这些参数将作为目录中的全局变量,并且环境为用户验收 测试UAT)。

通常最好通过 Hiera 数据传递类参数,但这只是为了演示在 ENC 输出中可以实现的内容。

要配置 ENC 脚本的使用,必须在 puppet.conf 中设置两个变量:首先是 node_terminus,默认值为 plain,只使用清单来定义分类。将 node_terminus 设置为 exec 后,第二个变量 external_nodes 将被检查,这应该设置为脚本的位置。例如,Foreman 项目使用一个在其配置模块中定义的 ENC,如下所示:

node_terminus = exec
external_nodes = /etc/puppetlabs/puppet/node.rb

脚本的内容可以在这里查看:github.com/theforeman/puppet-puppetserver_foreman/blob/master/files/enc.rb

用于放置此脚本的配置模块可以在 forge.puppet.com/modules/theforeman/puppetserver_foreman 找到。

开发 ENC 脚本超出了本书的范围,建议避免通过这种方式访问外部数据,因为访问可能会很昂贵。

我们已经介绍了 ENC 脚本的工作原理,但 PE 使用自己类型的 ENC 脚本,并具有额外的功能。

PE 分类器

PE 提供了自己的 ENC 分类器,访问分类服务 API,这是一个 Clojure 应用程序,并将节点组信息存储在 PostgreSQL 分类数据库中。

通过在 puppet.conf 中设置 node_terminus = classifier 来进行配置,安装程序已设置该项,并且不应更改,因为更改后将不受支持。

注意

node_terminus 在 PE 中曾在 PE 4 及之前版本中被称为 console

节点组有两种类型:环境组和分类组。环境组用于将环境分配给节点,而分类节点用于分配类并添加参数和变量。可以在 PE Web 控制台的 节点 部分查看和配置节点组。

所有节点组都可以包含规则,这些规则可以基于事实或通过直接命名要包含在节点组中的服务器来定义。它们可以包含任何带有任何定义的类参数的类,这些参数会被分类到这些匹配的节点中,这些参数被称为配置数据,像 Hiera 数据一样充当覆盖项,并优先于 Hiera,以及作为全局变量为组声明的变量。

注意

较旧版本的 PE 默认不启用配置数据,必须在 /etc/puppetlabs/puppet/hiera.yaml 中添加一个部分:

hierarchy: - name: "分类器配置数据" data_hash: classifier_data

默认情况下,如图 11.1所示,PE 将拥有一个 所有节点 节点组,作为所有配置的父节点组,并在其下分为 所有环境,一个作为所有声明的环境组的父组的环境组,以及 PE 基础架构,一个用于配置 PE 架构的分类组:

图 11.1 – PE 默认节点组

图 11.1 – PE 默认节点组

环境组在图 11.1中被标记为 trusted.extensions.pp_environment 事实,在规则中将生产或开发环境匹配到同名的组,并确保分配相应的环境。如果没有设置 trusted.extensions.pp_environmentpp_enviroment 受信事实将防止服务器被移动到另一个环境,而无需重新生成服务器证书,这将需要访问客户端和主服务器。命令为 puppet agent –``t --environment=myfeaturebranch

环境的开发和部署方法将在管理和部署 Puppet 代码部分进一步讨论,但可能需要在生产和开发之间增加更多环境层级,在这种情况下,推荐的做法是在所有环境下创建一个该环境名称的节点组,并创建一个规则,匹配 trusted.extensions.pp_environment 和您设置的环境名称。

环境组应保持简单,因此避免分配任何类参数或变量。

当分类组嵌套时,它们会继承父组的定义。在创建组结构时,从一般的配置层开始,然后将其细化到更具体的分类组是有意义的。例如,可以看到puppet_master_host被设置,这适用于所有 Puppet 基础设施主机,然后设置特定的服务和功能,如编译器或 PuppetDB,这将仅在部分节点上配置。

这可能会引起混淆,因为这种继承也适用于规则,因此,如果父规则已经设置了限制节点的规则,子节点组的规则将与父节点组的规则结合使用。这同样适用于节点的固定;您不能忽略规则并将任何可见于主服务器的服务器固定住。还需要注意的是,如果子节点组没有规则,它将不会应用分类,即使是从父组继承的分类。

关于分类节点组中环境变量的目的,可能会引起进一步的混淆;这并不是定义分配的类将从哪里运行,而是告诉节点组在哪个环境中查找可用的类名。如果节点组在开发和生产节点之间共享,并且新的类最初在开发环境中引入,然后再推广到生产环境,那么这可能会导致问题,因此通常情况下,应用节点组使用最低级别的环境来全面查看类名是最有意义的。

为了简化操作,建议使用直接的分类角色,这些角色作为所有节点的子角色,并仅通过将trusted.extensions.pp_role匹配到特定的类角色名称来设置规则,然后将该角色类分配给分类角色组。

为了自动化节点组的创建,可以使用node_manager模块(forge.puppet.com/modules/WhatsARanjit/node_manager)通过 Puppet 代码管理它们,这也是peadm模块本身配置 Puppet 节点组信息的方式。例如,peadm确保带有puppet/puppetdb-database可信扩展的节点被分配到PE 数据库节点组,代码如下:

node_group { 'PE Database':,
  rule => ['or',
    ['and', ['=', ['trusted', 'extensions', peadm::oid('peadm_role')], 'puppet/puppetdb-database']],
    ['=', 'name', $primary_host],
  ]
}

注意

node manager模块有一个purge_behavior设置,如果将资源的该设置为none,则确保仅应用您希望对节点组进行的特定更改。默认情况下,这个设置为all,会移除您未声明的任何设置。

另外,可以使用 API 执行节点组数据的备份和恢复,使用/classifier-api/v1/groups保存到文件,并使用/classifier-api/v1/import-hierarchy恢复。Peadm使用这些 API 实现备份和恢复分类任务:github.com/puppetlabs/puppetlabs-peadm/tree/main/tasks

注意

从 PE 版本 2019.2 开始,提供了一个$pe_node_groups顶级作用域变量,返回所有节点组。

使用Puppet 数据服务PDS)通过外部数据添加类的进一步方法将在第十三章中展示。但在回顾了各种分类方法后,我们将讨论最佳实践方法来对节点进行分类。

推荐方法

可以使用 ENCS 和节点定义方法的混合方式,因为它会合并信息,但这可能会使理解分类发生的位置变得更加困难。如果可能的话,最佳做法是选择一种方法,或者至少明确每种机制的目的,例如基于证书匹配角色的节点定义和匹配节点异常的 Hiera。

假设分类尚未由您的组织选择,或者在您的配置模型中是特定的,比如使用 Foreman 或psick,我们建议使用基于开源 Puppet 证书中pp_role扩展的默认节点定义的简单模式:使用与节点组角色匹配的pp_role扩展和与 PE 使用的环境匹配的pp_environment。这是 Puppet 支持所期望的,也是构建模型,但它限制了在 Hiera 数据设置中使用任何变量或配置数据。

节点定义使用 Hiera 对节点进行分类部分讨论了其他机制,因为在许多组织中,分类已经存在,并且不容易更改,因此必须理解它。如果必须生成复杂的分类,重要的是要知道这是否意味着数据没有放在正确的位置,或者——更糟糕的是——Puppet 没有得到有效使用,生产了过多的服务器变种。当我们维持严格的标准并尽量减少例外时,服务器可以轻松处置并重建,从而减少运营复杂性和支持团队的认知负担。

现在您已经理解了服务器如何被分类到环境和类中,我们将展示在 Puppet 运行期间如何加载和缓存不同的数据。

Puppet 运行

本节将详细介绍 Puppet 运行和分类的步骤。对于 Puppet 运行的情况,puppet apply命令应被视为 Puppet 服务器和客户端在同一节点上的等价物。

当客户端发出目录请求时,四项内容会被发送到服务器:

  • 节点名称

  • 节点的证书(未发送apply

  • Facts

  • 请求的环境

节点名称是 certname,并与请求的环境一起嵌入到 API 请求中——例如,/puppet/v3/catalog/exampleserver.example.com?environment=uat

证书可以包含扩展,这些扩展将被转化为可信的事实。

服务器接收到代理数据后,向配置的节点终端请求节点对象。在 plain 的情况下,这将是空白的;对于 execclassifier,将返回包含类、参数和环境的 YAML 输出。

默认情况下,puppet.confstrict-environment-mode 设置为 false,并且返回的环境将覆盖代理请求;如果设置为 true,则目录编译将失败。如果代理在 Puppet 执行过程中指定了环境,agent_specified_environment 事实将会出现。

变量将根据事实设置,既作为顶级作用域变量,也作为 $facts 哈希中的变量,将证书中的扩展作为 $trusted 哈希中的可信事实,以及从节点终端返回的参数作为顶级作用域变量。

主清单将被评估,首先查看它是否由环境配置定义,如果未设置,则由客户端的 puppet.conf 文件定义。如果存在任何节点定义,Puppet 将尝试匹配 certname,如果匹配失败,则编译将失败。

任何在节点定义之外的资源都会被评估并添加到目录中以及任何类中。如 节点定义 部分所述,不建议在节点定义之外声明任何内容。匹配的节点定义将评估代码,覆盖节点定义中声明的任何顶级作用域变量,将资源添加到目录,并加载并声明节点定义中的类。

Puppet 然后将加载包含在主清单中声明的类,使用为该环境配置的 modulepath 变量。每当加载一个类时,代码会被评估,资源会被添加到目录中,任何在其中声明的类也会被加载并评估。

Puppet 然后加载并评估从节点对象返回的类。

在了解了 Puppet 如何分类节点以及代理如何处理这些分类方法后,现在是时候查看如何管理和部署环境到主服务器,以便将正确版本的代码提供给节点。

管理和部署 Puppet 代码

默认情况下,只需创建文件夹并将模块内容放置到适当位置,再结合 puppet module install 命令从 Forge API 自动拉取,就足以使模块在环境中可见,并允许它们被打包到包管理中以创建版本。但我们并不推荐这种方法,因为它将模块和环境的部署集中化,很可能使得单个团队成为 gatekeeper。我们将看到控制库提供了更灵活的控制。

最常见的方法是使用一个名为控制仓库的 Git 仓库。Puppet 提供了这个仓库的模板,地址为 github.com/puppetlabs/control-repo

注意

Puppet Forge 作者 example42 提供了其自己的模板化控制仓库,用于与其集成和预设计的实现方法:github.com/example42/psick

Puppet 的控制仓库模板包含了本章第一部分中讨论的许多目录和文件,以及 Hiera 数据和一些特定于模块部署的附加文件。图 11.2 显示了 Puppet 控制仓库的内容:

图 11.2 – Puppet 控制仓库模板的文件结构

图 11.2 – Puppet 控制仓库模板的文件结构

在本章的第一部分,Puppet 环境,我们讨论了许多文件和目录,其中包括 environment.conf、配置版本脚本以及用于分类的 manifests 目录。还可以看到 hiera.yaml 中的 Hiera 配置以及数据目录,显示了一个简单的初始节点的两层结构,用于匹配特定节点名称和公共数据,作为不匹配节点的默认设置。site-modules 目录旨在展示临时计划和任务如何作为该控制仓库的一部分部署,并可能为角色和配置文件提供存放位置。scripts 目录也值得查看,以了解在 github.com/puppetlabs/control-repo/blob/production/scripts/config_version.sh 中的配置版本脚本如何将有关环境的 Git 修订控制信息添加到执行中。我们尚未审查的部分是 Puppetfile 文件。

Puppetfile 文件是基于 Ruby 的 moduledir 作为变量,或某个模块的 installpath 参数。我们不推荐这样做,因为这可能会让不熟悉你环境的用户感到困惑,并且如果设置在环境目录之外,可能会影响缓存,导致环境不一致。本节稍后会讨论这一点。

Puppetfile 模块声明的最简单形式包含以下内容:

  • mod 关键字

  • 单引号中的名称

  • 可选地跟一个逗号,再加上版本号或 : latest 关键字

例如,以下代码块假设 Puppet Forge 作为源,并在模块不存在时安装 dsc-octopusdsc 的最新版本,但不会导致模块被更新:

mod 'dsc-octopusdsc'
mod 'puppetlabs-chocolatey', '6.2.0'
mod 'puppetlabs-stdlib' , :latest

这段代码将安装 puppetlabs-chocolatey 到固定版本 6.2.0,并将安装 puppetlabs-stdlib 并保持更新到最新版本。需要注意的是,这不会导致 Puppet Forge 依赖项被安装——这些必须在 Puppetfile 中手动管理。在 Puppet Forge 查看模块文档时,你会看到如何将模块添加到 Puppetfile 的示例代码。

要访问其他 Git 仓库中的模块,应提供 git 选项和仓库的 HTTP 地址。然后,可以将其与以下选项之一配对,以克隆 Git 仓库的特定版本:

  • ref,指向标签、提交或分支的引用

  • tag,使用特定标签

  • commit,具有特定的提交引用

  • branch,具有分支名称或 :control_branch 关键字(它将自动查找控制仓库的分支名称)

  • default_branch,如果所有前述选项失败时使用的分支

以下代码演示了如何混合和匹配前述列表中的 git 选项:

mod 'exampleorg-examplemodule1',
  :git => 'https://internalgitservice.com/exampleorg/examplemodule1',
  :tag =>  'v.0.1'
mod 'exampleorg-examplemodule2',
  :git => 'https://internalgitservice.com/exampleorg/examplemodule2',
  :commit => '68a140bd096a55019b3d5c8c347436b318779161'
mod 'anotherorg-anothermodule',
  :git => 'https://internalgitservice.com/anotherorg/anothermodule',
  :branch => :control_branch,
  :default_branch => 'main'

这段代码块从同一个 Git 组织中获取 examplemodule1tag 版本 v.0.1examplemodule2commit 版本 68a140bd096a55019b3d5c8c347436b318779161。对于 anothermodule,如果存在与我们要部署的环境同名的分支,它将使用该分支;否则,它将克隆 main 分支。

在访问 Puppet Forge API 受限的隔离网络环境中,或在受到监管的环境中,若要求公司存储所有代码的副本以便审计,可能需要从 Forge 下载代码副本,并从公司自己的 Git 系统使用。在这种情况下,强烈建议你按照模块页面上的项目 URL,执行 Git 克隆 Puppet Forge 模块的源代码,然后将远程目录切换到你自己 Git 仓库的副本。这样可以确保提交历史得以保留,并且你可以定期克隆代码并将新提交添加到本地仓库。

无论 Forge 模块是如何下载的,如果它们不是直接从 Forge 最新版本下载的,那么频繁检查版本并将其作为定期测试和更新的流程非常重要。这可以确保你获取到最新的功能和修复,并避免执行大版本升级,因为大版本升级更难测试。关注 Content and Tooling (CAT) 团队的博客 puppetlabs.github.io/content-and-tooling-team/blog/ 可以帮助你跟踪模块发布。

注意

JFrog Artifactory 用户可以使用 Puppet Forge 插件在内部同步和托管模块,具体操作请参考 www.jfrog.com/confluence/display/JFROG/Puppet+Repositories

使用这种结构来管理多个环境时,只需在 Git 仓库中创建分支,每个分支代表一个环境,且每个环境可以有其独立的内容进行部署。

管理部署的标准系统是r10k,它还为 PE 提供了更多的集成功能。

r10k的安装说明简单明了,并且可以直接从forge.puppet.com/modules/puppet/r10k的仓库中获取。配置 PE 中 Code Manager 的说明可以通过节点组或通过 Hiera 提供,详细信息请参见puppet.com/docs/pe/2021.7/code_mgr_config.html

在这两种情况下,作为这些说明的一部分,将生成一个 SSH 密钥,用于r10k与任何你已声明的 Git 仓库之间的通信。

Puppet 开源的另一个替代选项是使用g10k ([forge.puppet.com/modules/landcareresearch/g10k](https://forge.puppet.com/modules/landcareresearch/g10k)),它是r10k`在Go语言中的重写,并且在性能上有显著的提升。

注意

你仍然可以在 PE 中直接使用r10k,但这是 Puppet 不提供支持的做法。

对于开源 Puppet,在配置并部署r10k之后,可以运行sudo -H -u puppet r10k deploy production命令来部署特定的分支,或者省略环境名称以部署所有可用的环境。还可以使用 Sinatra 服务器配置 Webhook,详细信息请参见r10k的说明,forge.puppet.com/modules/puppet/r10k/readme#webhook-support

对于 PE,Puppet Code Manager 是一个使用在 PE puppet code deploy命令中生成的令牌的/code-manager API。例如,下面的代码将为当前登录用户生成一个令牌,该令牌在接下来的 2 小时内有效,然后在生产环境中进行部署:

puppet-access login --lifetime 2h
puppet code deploy production --wait

在这两种版本中,要查看已部署的模块,可以使用puppet module --list,该命令还会显示任何依赖问题。

注意

Puppet Code Manager 在底层使用r10k。为了获得更详细的调试信息,可以运行以下命令,该命令用于在生产环境中进行部署:

runuser -u pe-puppet -- /opt/puppetlabs/puppet/bin/r10k -c /opt/puppetlabs/server/data/code-manager/r10k.yaml deploy environment production --puppetfile --``verbose debug2

对于这些部署,理解可能发生的缓存非常重要。所有 Puppet 代码在加载环境时都会被读取和解析——hiera.yaml文件也是如此——直到环境缓存过期或 JRuby 实例被刷新,才会重新读取。environment.conf文件默认将此设置为unlimited。虽然 Puppet 模板和 Hiera 数据在每次函数调用时都会从磁盘重新读取,但它们不会被缓存。这意味着,如果对r10k之外的 Hiera 数据或 Puppet 模板进行任何本地编辑,它们将被视为有效。也意味着如果环境具有查看其他环境的模块路径,部署只会看到 Hiera 和模板的更新。因此,强烈建议避免使用这种方法。

使用编译器同步代码时,开源 Puppet 根据你的环境提供了不同的部署方式:在每个编译器节点上安装并运行r10k、从主服务器到编译器执行rsync操作,或者使用从主服务器到所有编译器的只读网络文件共享NFS)。这一选择完全取决于你的组织在网络配置和安全标准方面的最佳方案。

在 PE 中,代码管理器使用文件同步客户端和服务器进行特定实现,如图 11.3所示:

图 11.3 – Puppet 代码管理器架构

图 11.3 – Puppet 代码管理器架构

代码部署请求将通过命令行或工具以带有 RBAC 令牌的请求形式传入。这将把代码拉取到主服务器的提交暂存目录。所有基础设施节点的文件同步客户端都有一个轮询监视器,能够看到部署并提醒文件同步过程。根据是否启用了无锁代码部署(此功能在 PE 2021.2 中引入),文件同步过程会做出两种响应中的一种。如果相关服务器未启用无锁代码部署,则需要保留所有 JRuby 实例,以防止任何目录运行使用不一致的环境。记住在Puppet 运行部分中不同环境数据如何被缓存,一旦保留,文件将同步到环境目录,并释放 JRuby 实例。这意味着代码部署可能会对性能产生影响。

如果启用了无锁代码部署,则会使用符号链接或 symlink 来管理环境目录,这意味着文件同步会同步到一个以版本提交命名的文件夹,并且在同步完成后,会将环境的符号链接重定向到这个新文件夹。这需要更多的磁盘空间,因为多个环境将同时部署,但它确保目录能继续运行,因为它们会使用符号链接开始时的目录。要启用无锁代码部署,请按照puppet.com/docs/pe/2021.7/lockless-code-deploys.html上的说明进行操作。

现在我们了解了 Puppet 如何将代码部署到环境中,我们将看一下可用于管理模块代码在这些环境中推广的工作流。

创建工作流

创建工作流来部署代码有两种常见的方法。第一种方法是将控制仓库作为版本的中央守门人。这意味着在 Puppetfile 中的每个模块声明都有特定版本,并且通常会使用如tagcommitbranch等特定引用更新最低级别的环境。这些更改会在功能分支中进行测试,然后通过将更改从一个分支合并到另一个分支,运行服务器上的代码,并确认预期结果,从而推动这些更改通过环境。例如,这样的过程可能包括以下步骤:

  • 创建控制仓库的功能分支并将module1标签版本从 1.1 更新为 1.2

  • 将功能分支与开发分支合并并部署开发环境

  • 将开发分支与 UAT 分支合并并部署 UAT 环境

  • 将 UAT 分支与生产分支合并并部署生产环境

这不是一种自然的 Git 流,并且不使用主分支。它非常专注于部署,要求更多地管理环境。这种方法对于多个团队尤其困难,因为它要求像 Puppet 平台团队这样的守门人来管理对 Puppetfile 控制仓库的更改,并管理何时进行代码部署的时间表。

如果采用这种方法,建议使用多个控制仓库并使用前缀配置设置——这对于希望使用不同模块集的团队(如 Windows 和 Linux)或希望在控制仓库周围进行隔离和保护,并且希望拥有代码和服务器的独立所有权但又想共享基础设施的团队非常有用。

第二种方法是将控制仓库中的所有模块都设置为使用control_branch分支,默认分支为main。维护 Puppetfile 时,只需要添加和移除模块。版本管理将由模块本身负责,代码更改会从临时功能分支推送到主分支,然后再合并到每个静态环境分支。以下是一个示例:

  • module1和控制仓库上创建一个功能分支,并测试代码更改

  • module1的功能分支与main分支合并

  • 将模块分支的更改从main合并到开发环境,然后进行部署和测试

  • 将模块分支的更改从开发环境合并到 UAT,然后进行部署和测试

  • 将模块分支的更改从 UAT 合并到生产环境,然后进行部署和测试

强烈建议将管道工具作为拉取请求PR)和部署过程的一部分。PE 的持续交付CD4PE)(在第十四章中讨论)配有预构建的检查,帮助简化这一过程,但也存在各种工具,如 Jenkins 或 GitHub,可以确保在完成 PR 之前执行我们在第八章中讨论的 pre-commit 钩子检查和测试。

注意

一些现成的优秀 pre-commit 钩子的来源可以在以下网址找到:pre-commit.com/hooks.htmlgithub.com/pre-commit/pre-commit-hooks,以及github.com/mattiasgeniar/puppet-pre-commit-hook

实验 – 分类和部署代码

在本实验中,完成以下任务:

总结

本章中,我们讨论了如何使用 Puppet 环境来管理模块的特定版本、分类和要应用于 Puppet 客户端组的数据。回顾了用于配置这些内容的目录结构和变量。

对将服务器分类到不同环境、分配类和参数的选项进行了审查,查看了清单文件中的节点定义,使用 Hiera 在节点定义中创建更复杂的基于数据的计算,然后是 ENC 脚本,这些脚本可以访问如 PuppetDB 等源并返回类、环境和参数的 YAML 输出,以便进行分类。随后,展示了 PE 如何在 ENC 方法的基础上进行扩展,并使用自己的 ENC 脚本与节点组结合使用,存储如何将服务器分类到环境并分配类的数据。

强调了可以将各种方法结合使用,但推荐的做法是保持简洁;对于开源 Puppet,只需使用默认节点定义来查找pp_role受信事实进行分类,并将环境设置放入puppet.conf,而对于 PE,建议使用节点组与pp_rolepp_environment受信事实的一一匹配。

随后展示了 Puppet 目录请求如何将数据发送到 Puppet 服务器,以及如何使用分类文件和脚本来生成目录,重点介绍了如何缓存不同类型的 Puppet 资源。

随后展示了如何部署环境,使用基于 Git 的 Puppet 控制库来包含环境的文件和目录,每个 Git 分支代表一个特定的环境。Puppetfile 被展示为列出应部署到环境中的模块,并指定模块的版本和位置。

接着讨论了如何通过r10k及其在r10k基础上的 PE 代码管理器实现来部署代码到服务器。对于使用编译器的服务器,我们回顾了各种方法来保持所有基础设施上的代码部署,这将取决于本地基础设施和标准。对于 PE,展示了代码管理器包含文件同步功能,以保持代码的同步。

随后介绍了工作流方法,展示了使用带有 Puppetfile 的控制库,设置版本并在推送模块版本更改时更新最低级别环境(如开发环境)的传统方法。第二种推荐的方法显示,控制库将依赖于模块本身,控制库会查找按环境命名的分支,允许团队独立工作和部署。无论哪种系统,重点都是使用合适的流水线工具并配合 Webhooks 来自动化部署。

本章重点介绍了用于有状态配置管理的 Puppet 基础设施和语言,下一章将介绍 Bolt 和 Orchestrator,展示如何使用 Bolt 作为独立工具或通过 PE 基础设施中的 PE Orchestrator 来执行过程任务。

第十二章:用于编排的 Bolt

在本章中,我们将介绍Bolt和 Puppet Enterprise 的orchestrator。我们将展示 Bolt 是 Puppet 用于临时编排的工具,能够处理不适合 Puppet 基于状态强制执行模型的工作。我们将讨论如何配置它以连接具有不同传输机制和凭证的客户端,并执行简单命令和上传文件。此外,我们将展示如何通过 Bolt 运行tasks,这些任务可以是多种语言的单一操作脚本,而plans则允许通过逻辑和变量在 Puppet 或 YAML 语言中编写任务组合。我们还将探讨项目目录结构,允许存储和共享 Bolt 内容。这将与如何使用Puppet Enterprise Cloud Deployment ModulePECDMBolt 项目作为示例,将计划和任务存储在 Puppet 模块中进行比较。接着,我们将展示如何通过插件扩展 Bolt,从其他来源动态加载信息。我们还将展示如何将 Bolt 与 Puppet 直接结合使用,应用清单块,连接到 PuppetDB,并使用 Hiera。

在本章中,我们将介绍以下主要内容:

  • 探索和配置 Bolt

  • 理解项目的结构

  • 任务与计划介绍

  • 插件

技术要求

将控制仓库controlrepo-chapter12github.com/puppetlabs/control-repo克隆到您的 GitHub 账户,并用github.com/PacktPublishing/Puppet-8-for-DevOps-Engineers/blob/main/ch12/Puppetfile的内容更新 Puppetfile。

通过从github.com/PacktPublishing/Puppet-8-for-DevOps-Engineers/blob/main/ch12/params.json下载params.json文件,并更新其中的控制仓库位置和控制仓库的 SSH 密钥,构建一个标准集群,包含两个 Unix 客户端和两个 Windows 客户端。然后,在pecdm目录中运行以下命令:

bolt --verbose plan run pecdm::provision --params @params.json

探索和配置 Bolt

到目前为止,本书主要集中在 Puppet 作为基于状态和幂等的配置管理工具的优势。但也有一些场景,其中这种方法并不适用,例如作为故障排除一部分的服务重启,或者使用供应商提供的安装脚本进行应用部署排序。许多任务属于更广泛自动化努力的一部分,属于临时和一次性的任务;因此,Puppet 推出了 Bolt,作为一个无代理的编排工具。自 2017 年发布以来,Bolt 已经进入 3.x 版本,并且经历了快速的发展。到了 2022 年,Bolt 趋于稳定,发布和功能更新大大减少,但我们强烈建议您尽可能保持 Bolt 的最新版本,以避免任何混淆。

在审阅了 Bolt 作为临时任务运行器的通用目的之后,第一步是理解 Bolt 如何通过传输和目标连接到客户端。

通过传输和目标连接到客户端

Bolt 是一个完全开放源项目,位于github.com/puppetlabs/bolt,使用bolt编写。它通过提供的各种传输连接到设备,这是一种机制/协议,允许它在不需要代理的情况下建立到多个平台(如虚拟机、网络设备或容器)的连接。可用的传输如下。

系统传输:

  • 本地,顾名思义,仅在本地机器上运行命令。

  • net-ssh Ruby 库或native ssh,如果选择使用的话。通常用于 Linux 和 Unix 机器。

  • Windows 远程管理WinRM),用于连接基于 Microsoft Windows 的机器。

远程,用于 API 或基于 Web 的设备,例如网络设备如交换机。

Puppet Enterprise 传输:

  • Puppet 通信协议PCP),与 Puppet Enterprise 编排服务一起使用,在第十四章中讨论。

容器传输:

  • Docker,由 Docker Inc 开发的应用容器技术。

  • Pod 管理器Podman),由 Red Hat 开发的应用容器引擎。

  • Linux 容器超级监视程序LXD),是一个使用Linux 容器LXC)的系统容器引擎,由linuxcontainers.org开发并由 Canonical 赞助。

注意

除非在传输设置中将native-ssh设置为true,否则 Bolt 无法使用 SSH 连接到 Windows 目标,具体参见puppet.com/docs/bolt/latest/bolt_known_issues.html#unable-to-authenticate-with-ed25519-keys-over-ssh-transport-on-windows

默认情况下,Bolt 将使用本地 SSH 配置,在其最简单的级别上可以直接在被称为目标的设备上运行命令。一个简单的例子命令如下:

bolt command run 'uname' --targets examplehost.example.com

这里,命令在单引号内,提供的目标是可解析的主机名或 IP 地址。Bolt 还具有PowerShell 命令,为 PowerShell 用户提供更集成的体验,具有更灵活的命令链接和使用结构化数据作为参数的能力。与之前相同的命令作为 PowerShell 命令看起来如下:

Invoke-BoltCommand -Command 'uname' -Targets examplehost.example.com

这会使用默认的 SSH 传输设置,使用当前用户和任何已保存的凭证。如果想在命令行中做出选择,可以在传输名称前加上传输选择 <transport_name>://,多个目标使用逗号(,)分隔,并设置其他选项来配置传输。例如,如果 WinRM 没有配置 SSL 连接,则需要设置用户名、密码和 no-ssl 选项。以下是一个示例命令:

bolt command run 'systeminfo' --targets winrm:// host1.example.com,winrm://host2.example.com --user windows --password Pupp3tL@b5P0rtl@nd! --no-ssl

此命令将使用 winrm 连接在 host1.example.comhost2.example.com 目标上运行 systeminfo,并使用 windowsPupp3tL@b5P0rtl@nd! 凭证,同时不进行 SSL 检查。Bolt 默认并行运行请求,最多同时运行 50 个请求。可以通过 --concurrent 参数调整并发数。

可以在文档中查看每种传输方式的完整选项列表:puppet.com/docs/bolt/latest/bolt_transports_reference.html

注意

Bolt 1.3.6 弃用了 nodes 标志,改为使用 targets,并在 Bolt 2.0.0 中移除了该标志。

使用 Bolt 运行临时命令

本节将展示如何使用 Bolt 运行临时命令,包括 Windows PowerShell 和 Linux Shell 命令示例。下表展示了这些命令在不同实现中的对比:

图 12.1 – PowerShell 和 Linux Bolt 命令

图 12.1 – PowerShell 和 Linux Bolt 命令

要运行带引号的命令,请使用双引号或反斜杠(\)进行转义。例如,我们可以在 /etc/locale 中搜索 lang,命令为 grep -I 'lang'。为了做到这一点,可以在 PowerShell 中运行以下命令:

Invoke-BoltCommand -Command "grep -i 'lang' /etc/locale" -Targets ssh://examplehost.example.com –User centos -PasswordPrompt -RunAs root

在这个例子中,password-prompt 选项将在命令行上安全地提示输入密码,而不是直接将其输入到执行的命令中。

要运行文件中列出的多个命令,我们不是建议运行脚本,而是逐步执行一组命令;对于文件中的多个目标,可以使用 @ 符号和引号包围的文件名('')。例如,要从名为 commandlist 的文件中运行一组命令,并在 targetfile 中列出的目标上执行,可以运行以下命令:

bolt command run '@commandlist' --targets '@targetfile'

对于基于 Unix 的系统,要从 stdin 读取输入以供目标或命令使用,可以用减号(-)替代目标或命令字符串。因此,若想使用相同的 targetfile 并将 cat 命令的输出传递给 bolt 命令,可以运行以下命令:

cat targetfile | bolt command run '@commandlist' --targets -

要在 hosts1.example.comhost2.example.com 目标上运行 unamedate 命令,可以使用以下命令:

echo -e "uname \\ndate" | bolt command run - --targets host1.example.com, host2.example.com

注意

同时使用文件和 stdin 列出命令将导致与目标建立单一连接,执行所有命令。

要运行文件中的脚本,可以使用bolt script run命令或Invoke-BoltScript -ScriptPowerShell cmdlet,并在命令末尾传递任何参数。例如,在 Unix 主机上,可以使用以下命令在application_clients文件中的目标上运行带有10.6 no-gui参数的install.sh脚本:

bolt script run ./scripts/install.sh --targets @application_clients 10.6 no-gui

可以使用arguments标志来明确每个传递值的参数名称。任何带空格的参数可以用引号('')括起来。例如,在 Windows 系统上运行带有-Channel LTS参数的dotnet-install.ps1脚本,命令如下:

Invoke-BoltScript -Script dotnet-install.ps1 -Targets @targetsfile '-Channel LTS'

在 Unix 中,任何脚本都可以通过在文件顶部包含一个 shebang(#!)行来指定解释器,从而在目标上执行。对于 Windows 目标,.ps1.rb.pp文件默认启用,但可以在配置文件中启用其他扩展,这将在下一部分讨论。脚本可以从modulepath中找到,形式为<modulename>/scripts/install.sh,也可以是bolt文件夹根目录的相对路径,或者是绝对路径。

在 Unix 系统中,Puppet 清单文件和 Puppet 代码的部分可以通过以下方式应用到一组目标:

bolt apply manifests/exampleapp.pp --targets @targetsfile

在 PowerShell 中,可以使用以下命令实现:

Invoke-BoltApply -Manifest manifests/exampleapp.pp -Targets @targetsfile

要应用 Puppet 代码,以下命令会确保 Unix 系统上的/etc/exampleapp目录存在:

bolt apply --execute "file { '/etc/exampleapp: ensure => present }" --targets servers

对于 PowerShell cmdlets,使用的命令如下:

Invoke-BoltApply -Execute "file { '/etc/exampleapp': ensure => present }" -Targets servers

这种格式应该与puppet applypuppet apply -e '<code>'相似。类似地,对于通过 Bolt 应用的代码,我们必须确保代码被声明为包含在目录中,而不仅仅是定义了。当一个类或类型被定义时,它可以在目录中使用,但不会被添加到目录中。在前面的示例中,如果exampleapp.pp包含一个带有资源的类定义,那么会出现警告:Manifest only contains definitions and will result in no changes on the targets。类本身需要被包含才能将其添加到目录中,并通过 Bolt 应用。

还有一些命令可以将文件从本地机器上传到目标,或从目标下载到本地机器。以下命令展示了在 Unix 和 Windows 版本中的一些简单示例。第一个列出的文件是源文件,第二个是目标文件,无论是上传还是下载:

bolt file upload /rpms/cowsay.rpm /tmp/ --targets @targets
Send-BoltFile -Source /installer/installer.exe -Destination /users/exampleuser/installer.exe -Targets @targets
bolt file download /etc/exampleapp//logfile.log /var/tmp/logfile.log --targets @targets
Receive-BoltFile -Source /ProgramData/exampleapp/logfile.log\puppet.log -Destination /user/exampleuser/puppet.log -Targets @targets

现在,让我们来看一下输出。

输出和调试

到目前为止,重点一直是如何运行命令,而不是输出。默认情况下,Bolt 会将这些命令记录到从中运行 Bolt 命令的目录中的bolt-debug.log文件中,并显示在控制台上。共有六个日志级别:

  • trace:最详细的日志级别,显示 Bolt 的内部工作过程。

  • debug:关于特定目标步骤的信息。

  • info:这是高级日志,显示 Bolt 中发生的步骤。

  • warn:关于弃用功能和其他有害场景的警告。这是默认的控制台级别。

  • error:在执行 Bolt 命令时遇到的错误消息。

  • fatal:来自 Puppet 代码的错误消息,这些代码与 Bolt 一起使用。

可以使用 --log-level 标志选择特定的日志级别,并使用 format 标志选择输出格式,支持 humanjsonrainbow。在三台主机上运行 uname 的 Bolt 命令输出,JSON 格式如下所示:

{ "items": [
{"target":"host1.example.com","action":"command","object":"uname","status":"success","value":{"stdout":"Linux\n","stderr":"","merged_output":"Linux\n","exit_code":0}}
,
{"target":"host1.example.com","action":"command","object":"uname","status":"success","value":{"stdout":"Linux\n","stderr":"","merged_output":"Linux\n","exit_code":0}}
,
{"target":"host1.example.com","action":"command","object":"uname","status":"success","value":{"stdout":"Linux\n","stderr":"","merged_output":"Linux\n","exit_code":0}}
],
"target_count": 3, "elapsed_time": 2 }

相比之下,以人类可读格式显示时,内容将如下所示:

Started on host1.example.com...
Started on host2.example.com...
Started on host3.example.com...
Finished on host1.example.com:
  Linux
Finished on host2.example.com:
  Linux
Finished on host3.example.com:
  Linux
Successful on 3 targets: host1.example.com, host2.example.com, host3.example.com
Ran on 3 targets in 2.89 sec

rainbow 输出与人类可读格式类似,但正如其名字所示,它使得每一行都呈现为多种颜色。

作为输出的一部分,将生成一个 .rerun.json 文件。此文件列出了在运行过程中处理的目标,指示哪些目标失败,哪些目标成功。对于下一个 Bolt 命令,我们可以使用 --rerun 标志,值可以为 successfailureall。此命令将读取 .rerun.json 中的相关目标部分,并使用上一次运行的目标。例如,以下命令可能是在 install 任务失败并选择对所有失败进行清理任务时运行:

Invoke-BoltTask -Name install_failure_cleanup -Targets @targets.file -Rerun failure

命令有更多选项;完整的命令参考文档可以通过以下链接查看:puppet.com/docs/bolt/latest/bolt_command_reference.html 针对 Unix 系统的命令和 puppet.com/docs/bolt/latest/bolt_cmdlet_reference.html 针对 PowerShell 系统的命令。

注意

Bolt 具有内置的 CLI 指南,可以通过在 Unix 或 PowerShell 命令行中运行 bolt guide 来访问。

到目前为止,我们使用 Bolt 讨论的内容在非常小的规模下有用,但显然也适用于大规模的服务器和更复杂的配置。因此,接下来要讨论的是项目结构和配置文件。

理解项目结构

在其中存在一个 bolt-project.yaml 文件,该文件包含一个 name 键。要创建该文件,请在希望添加 Bolt 项目文件的目录中,分别针对 Unix 系统运行 bolt project init 或 PowerShell 中运行 New-BoltProject。此命令将使用目录名作为项目名称,但你可以通过运行带有名称的命令来覆盖这一点,分别为 Unix 系统的 bolt project init customname 或 PowerShell 中的 New-BoltProject -Name customname 命令。

项目名称必须以小写字母开头,并且只能使用小写字母、数字和下划线。这是因为 Bolt 项目类似于模块,并会加载到模块路径中。需要注意的是,如果 Bolt 项目与模块具有相同名称,则 Bolt 项目中的模块将被覆盖在模块路径中。

在该目录中,init 命令将创建 bolt-project.yamlinventory.yaml.git-ignore 文件。

现在,让我们看看如何配置一个 Bolt 项目。

配置项目

bolt-project.yaml 包含用于覆盖默认 Bolt 行为的设置,许多内容在前一节中已经讨论过。可以在此处设置用于 Bolt 命令的设置,以及项目配置,例如配置文件和数据的路径。通常情况下,这些设置的默认值不需要更改,核心设置将包括 modules 属性,该属性定义了在 Bolt 项目中管理的模块,以及 planspoliciestasks 属性,这些属性通过提供可见的列表来限制每个项目项的可见性,用户可以看到该列表。一个包含一些模块,并选择要公开可见的计划、策略和任务的 bolt-project.yaml 文件示例如下:

name: packtproject
modules:
- name: puppetlabs-stdlib
- name: puppetlabs-peadm
  version_requirement: 3.9.0
- name: puppetlabs/bolt_shim
- git: https://github.com/binford2k/binford2k-rockstar
  ref: 0.1.0
plans:
- packproject
- peadm::provision
policies:
- packproject::lab
tasks:
- bolt_shim::command

设置的完整列表可以在 puppet.com/docs/bolt/latest/bolt_project_reference.html 中找到。

module 属性有多种更新方式。当从 Forge 添加项目时,可以通过 bolt module add Unix 命令或 Add-BoltModule PowerShell cmdlet 更新。例如,在 Unix 系统中,bolt module add puppetlabs/apt 将更新 bolt-project.yaml 中的 modules 参数,包含 - name:puppetlabs-apt

然后,可以使用 bolt module install Unix 命令或 Install-BoltModule PowerShell cmdlet,这将自动完成几项操作:

  • 查找所有 Forge 模块的依赖项

  • 查找兼容版本

  • 更新 Puppetfile

  • 将模块安装到 Bolt 项目中

模块还可以在项目创建时通过以下 Unix 系统命令添加:

bolt project init example_project --modules puppetlabs-apache,puppetlabs-mysql

在 PowerShell 中,可以使用以下命令来完成此操作:

New-BoltProject -Name example_project -Modules puppetlabs-apache,puppetlabs-mysql

如果需要将模块固定在特定版本或添加 Git 模块,则需要手动将这些模块添加到 Bolt 项目文件中,并使用以下 Bolt 模块安装命令运行 Force 标志:在 Windows 上使用 Install-BoltModule -Force 或在 Unix 系统上使用 bolt module install --force

这些模块允许我们在计划中使用 Puppet 代码,并从模块中引入计划和任务,详细内容将在 介绍任务和 计划 部分中展示。

配置传输

inventory.yaml 文件包含有关目标的配置信息,创建目标组并提供有关 Bolt 如何与它们连接的详细信息。清单包含一个顶层,其中包括作为所有目标默认设置的设置,允许基于共同设置(如所有 Windows 节点使用某些 WinRM 设置)对目标进行分组的组对象,以及目标对象(即单独的设置)。对于每个设置,都可以使用一些通用字段:

  • 别名:用来代替统一资源标识符URI)的别名,它可以更简短且更易于人类阅读

  • 配置:目标的传输配置选项的映射

  • 事实:目标(们)的事实映射

  • 特性:要启用的特性数组(特性将在本章后续部分讨论)

  • 名称:与组一起使用,以提供易于阅读的名称

  • 插件钩子:插件配置的映射(插件将在本章的 插件 部分讨论)

  • URI:目标的 URI

  • 变量:变量的映射

一个示例清单文件可能如下所示:

config:
  transport: ssh
  ssh:
    host-key-check: false
    run-as: root
    native-ssh: true
    ssh-command: 'ssh'
groups:
  - name: agents
 groups:
  - name: linux_agents
    targets:
      - 20.117.165.119
  -name: windows_agents
    targets:
     - 20.117.165.218
     config:
      winrm:
        user: windowsuser
        password: Pupp3tL@b5P0rtl@nd!
        ssl: false
targets:
  - name: primary:
  - 20.117.166.6

这将提供 SSH 传输的默认设置。需要注意的是,在此示例中,展示了如何在任何清单中创建组内的组,以便简化管理和组的设置。在此案例中,我们有一个名为 agents 的组,其中包含 linux_agents 组和 windows_agents 组。windows_agents 组包含 WinRM 传输配置。这使得我们可以对所有代理运行 Bolt,但为每个代理设置不同的传输方式。然后,在这些组外有一个名为 Primary 的单一目标。

完整的 inventory.yaml 配置文档可以在 puppet.com/docs/bolt/latest/bolt_inventory_reference.html 上找到,而传输配置文档可以在 puppet.com/docs/bolt/latest/bolt_transports_reference.html 上查看。

要返回 inventory.yaml 文件的内容,可以使用 bolt inventory show Unix 命令或 Get-BoltInventory PowerShell cmdlet。可以使用 targets 标志查看特定目标。

如前一节所述,对于 Windows 脚本,可以使用清单文件允许附加扩展,因此在 config 部分,可以添加以下内容以允许运行 .py.pl 脚本:

config:
  winrm:
    extensions:
      - .py
      - .pl

在回顾如何在 Bolt 中配置项目级别的设置之后,现在需要了解如何在 Bolt 中设置系统级别的设置,以及如何将以前的遗留版本的 Bolt 项目配置为不同的方式。

系统级别和遗留版本

除了项目设置外,系统级别的设置可以在基于 Unix 的系统中的/etc/puppetlabs/bolt/bolt-defaults.yaml文件以及 Windows 系统中的%PROGRAMDATA%\PuppetLabs\bolt\etc\bolt-defaults.yaml文件中进行设置。用户级别的设置可以在用户主目录下的.puppetlabs/etc/bolt/bolt-defaults.yaml文件中进行设置。

Bolt 会根据以下优先级顺序选择使用哪个项目:

  1. BOLT_PROJECT环境变量中设置的项目位置

  2. 在设置了项目位置的 Bolt 命令中的project标志(--project /tmp/myproject

  3. 通过从当前目录向上遍历,直到找到bolt-project.yamlboltdir目录

  4. 用户主目录下的.puppetlabs/bolt/文件夹

注意

在 Unix 环境中,Bolt 不会加载一个世界可写的 Bolt 项目目录。

如果你希望将 Bolt 嵌入到一个应用项目中,但基本的 Bolt 项目文件会使应用变得杂乱,你可以通过在应用目录中创建一个boltdir目录来嵌入一个 Bolt 项目。即使是这样,Bolt 仍然可以从父目录运行,因为它会识别boltdir作为包含项目的目录。

如果你之前使用过 2.36 版本之前的旧版 Bolt,你会注意到项目曾经只创建一个bolt.yaml文件,而不是bolt-project.yamlinventory.yaml。对 v1 bolt.yaml 项目的支持在 Bolt 的 v3.0.0 版本中被移除。此外,随着 v2.42 版本中手动编辑 Puppetfile 的弃用和 v3.0.0 版本中手动编辑的移除,Bolt 管理的模块也发生了变化。这也改变了模块路径,从包含site-modulessite模块变更为现代版本的modules.modules。之前,托管的模块存在于modules中,非托管的模块则位于sitesite-modules中。现在已经更改为托管的模块位于.modules中,非托管的模块位于modules中。为了将旧版的 Bolt 项目迁移到新版本,可以运行bolt project migrate Unix 命令或Update-BoltProject PowerShell 命令。像所有自动化转换一样,请确保在迁移之前备份好配置并进行版本控制。有关迁移过程中的更改详细信息,请参阅puppet.com/docs/bolt/latest/projects.html#migrate-a-bolt-project

在回顾了为 Bolt 配置和目标传输创建的结构之后,现在是时候通过任务和计划来查看更结构化的运行 Bolt 方式了。

引入任务和计划

任务计划更像是脚本,允许用户管理参数、逻辑和动作之间的流程。与普通的 Puppet 代码不同,计划和任务按顺序运行脚本,即使是那些编译目录清单的 Puppet 计划也是如此。

创建任务

任务是单一操作的脚本,可以使用任何在目标机器上运行的语言。与我们之前使用 Bolt 执行的普通脚本相比,任务的主要区别如下:

  • 任务与 JSON 文件配对,以提供元数据,例如参数,使它们更易于共享和重用

  • 任务可以处理结构化/类型化的输入和输出

  • 任务可以处理多种实现,使其跨平台

它们可以存储在 Bolt 项目的任务目录中,或者在 Puppet 模块的任务目录中。任务实现应在名称中包含其扩展名。名称可以包含数字、下划线和大小写字母

在调用这些任务时,会创建一个命名空间,由包含任务的 Bolt 项目或模块的名称以及任务名称组成,除非任务被命名为 init,在这种情况下它只会通过 Bolt 项目或模块的名称来引用。

例如,安装代理的任务是 peadm::agent_install

注意

.json.md 扩展名是保留的,不能用于任务。

对于 Unix Shell 系统,脚本部分必须在文件顶部包含一个 shebang (#!) 行,指定解释器。

任务实现的一个例子是当使用 PEADM 模块配置实验室时,在 Unix 系统下的 agent_install.sh 任务中使用以下代码:

#!/bin/bash,
set -e
if [ -x "/opt/puppetlabs/bin/puppet" ]; then
echo "ERROR: Puppet agent is already installed. Re-install, re-configuration, or upgrade not supported. Please uninstall the agent before running this task."
exit 1
fi
flags=$(echo $PT_install_flags | sed -e 's/^\["*//' -e 's/"*\]$//' -e 's/", *"/ /g')
curl -k "https://${PT_server}:8140/packages/current/install.bash" | bash -s -- $flags

参数是基于以 $PT_ 开头的变量传递的。

使用 PowerShell,它具有内置的参数处理器,可以通过在名为 agent_install.ps1 的任务中使用 param 函数来完成,而无需使用 $PT_

param(
  $install_flags
  $server
)
if (Test-Path "C:\Program Files\Puppet Labs\Puppet\puppet\bin\puppet"){
Write-Host "ERROR: Puppet agent is already installed. Re-install, re-configuration, or upgrade not supported. Please uninstall the agent before running this task."
Exit 1
}
$flags=$install_flags -replace '^\["*','' -replace 's/"*\]$','' -replace '/", *"',' '
[Net.ServicePointManager]::ServerCertificateValidationCallback = {$true}; $webClient = New-Object System.Net.WebClient; $webClient.DownloadFile("https://${server}:8140/packages/current/install.ps1", 'install.ps1'); .\install.ps1 $flags

为了使这些文件对 Bolt 命令可见并允许调用者传递参数,会写一个与任务同名的 JSON 文件。对于 agent_install 示例,它看起来是这样的:

{
  "description": "Install the Puppet agent from a master",
  "parameters": {
    "server": {
      "type": "String",
      "description": "The resolvable name of the Puppet server to install from"
    },
    "install_flags": {
      "type": "Array[String]",
      "description": "Positional arguments to pass to the shell installer",
      "default": []
    }
  },
  "implementations": [
    {"name": "agent_install.sh", "requirements": ["shell"]},
    {"name": "agent_install.ps1", "requirements": ["powershell"]}
  ]
}

元数据提供了任务的描述,列出任务时会显示。此外,元数据包括参数列表,参数名称必须以小写字母开头,并且仅包含小写字母、下划线和数字。还可以指定参数类型,该类型可以匹配任何可以在 JSON 格式中表示的 Puppet 类型,以及参数的默认值。

确保类型是枚举类型或更具体的类型,例如在指定大小范围内的整数,可以使任务更加安全,限制输入,从而减少攻击面。此外,在任务中,您应确保正在处理的实现的参数得到正确分隔,并且不允许调用字符串。具体示例可以参考 puppet.com/docs/bolt/latest/writing_tasks.html#secure-coding-practices-for-tasks

implementations 参数允许我们定义在什么环境中使用哪些脚本。在此情况下,确保 .sh 实现运行在 Unix shell 上,而 .ps1 实现运行在 PowerShell 上。

有了这个文件,bolt task show Unix 命令或Get-BoltTask PowerShell cmdlet 将显示模块路径中所有可用的模块,并且可以通过bolt task show <taskname>Get-BoltTask –Name <taskname>查看特定任务。

private参数设置为true可以防止任务出现在任务列表中,这对于隐藏正在开发中的任务非常有用,尽管如我们在配置项目部分所示,这也可以在 Bolt 项目级别实现。

通过将参数值设置为true,可以将参数标记为sensitive,并且在代码中将变量设置为sensitive可以确保它们在日志和输出中被屏蔽。

元数据中的supports_noop参数允许用户向任务传递noop参数,这将使得_noop参数的值为truefalse。然后,你可以在任务代码中使用此参数来逻辑检查是否应进行更改或仅进行测试。

如果将remote参数设置为true,则任务只能在远程传输上运行,以防止任务在不兼容的传输上运行。

对于一个选项较多或返回信息较多的任务,使用结构化输入输出可能会比仅使用简单参数更好。

默认情况下,Bolt 将任务参数作为单个 JSON 对象传递给STDIN,并将环境变量一并传递。然后,Ruby 脚本可以使用以下代码行读取这些参数:params = JSON.parse(STDIN.read)

对于复杂的输出,应该确保任务在任务中打印单个 JSON 对象到stdout。这在你希望在另一个任务中使用结果时非常有用。例如,在 Python 中,以下代码片段将把两个值集的 JSON 转储到 stdout,使用json.dump将结果字符串转换为 JSON 并传递给 Python 用于打印到 stdout 的sys.stdout方法:

result = { "example1": "value1 , "example2": "value2" }
json.dump(result, sys.stdout)

要从任务中返回错误消息,可以返回一个Error对象。在结构化输出中,预期会有_error键,并且msg键作为 UI 中的人类可读消息,kind作为脚本处理的字符串,details包含有关任务失败的结构化数据,例如退出码尾部。以下是一个例子:

{ "_error": { "msg": "Task exit code 1", "kind": "puppetlabs.tasks/task-error", "details": { "exitcode": 1 } } }

如果没有_error键,Bolt 将生成一个通用错误。

注意

在一个模块中,可以运行pdk new task <taskname>来生成<taskname>.json文件和<taskname>.sh文件,文件会保存在任务文件夹中。

要运行这些任务,可以使用bolt task run Unix 命令或Invoke-BoltTask PowerShell cmdlet,并通过传递参数作为命令行参数或使用@符号来传递一个 JSON 字符串或带有.json扩展名的文件。例如,第一个任务会在 agents 组中的目标上安装 Puppet 代理,并设置 server 和install_flags参数:

bolt task run peadm::install_agent --targets agents server=primary.example.com install_flags= ["--puppet-service-ensure","stopped","agent:certname=node.example.com"]

第二个任务将运行package任务,并通过params标志传入 JSON 字符串,以检查apache2软件包的状态:

Invoke-BoltTask -Name package -Targets @targetservers -Params '{action="status";name="apache2"}'

在学习了如何创建和运行任务后,现在是时候回顾一下计划,计划允许在管理任务时应用更强的结构、逻辑和流程控制,并且能够使用 Puppet 代码。

创建 Puppet 计划

计划是用 Puppet 代码或 YAML 编写的,它允许将多个任务和命令结合起来,并在它们之间应用逻辑和数据流控制。

Puppet 计划是以清单形式编写的,格式类似于 Puppet 类。它以plan关键字开始,接着是计划的名称、括号()内的属性,以及大括号{}中的代码。例如,位于计划目录中的一个示例项目的计划如下所示:

plan exampleproject::exampleplan(
  TargetSpec $nodes,
  Enum ['true', 'false'] $manage_user,
) {
  <code>
}

计划的命名方式与任务类似,第一部分是模块或项目的名称,第二部分及其后续部分以小写字母、数字和下划线命名。

它们不能使用保留字,也不能与 Puppet 数据类型相同。

init.pp类与任务和模块不同。它会跳过任务直接命名的需求。然而,它只能在基础层级使用,而不能在任何子目录中使用。

要创建一个新计划,可以使用以下命令,分别适用于 Unix 系统和 PowerShell:

bolt plan new <PLAN NAME> --pp
New-BoltPlan -Name <PLAN NAME> -Pp

在回顾了如何创建计划后,我们将看到计划如何通过TargetSpec类型接收目标和传输信息。

构建目标

除了普通的属性数据类型外,计划还使用TargetSpec类型,这使得可以使用与在通过传输和目标连接客户端部分中用于 Bolt 命令目标相同的字符串,例如ssh://examplehost.comTarget类型的数组,以及递归的TargetSpec类型数组。

Target类型表示一个目标及其特定连接方式,以便它们可以被添加到清单文件中。

在计划中,可以使用get_targets函数从TargetSpec中返回目标。以下是一个简单的使用示例:

plan restart_apache_servers(
TargetSpec $apache_servers,
){
 get_targets($apache_servers).each |Target $apache_server | {
 run_task('apache', $target_node, 'action' => 'reload')
 }
}

该计划接受一个TargetSpec对象apache_servers,该对象被传递给get_targets函数。然后,Apache 的reload任务在每个目标服务器上运行,action参数设置为reload

目标对象还可以在计划清单中构建并通过以set_add_开头的函数进行更改,这些函数用于配置文件中各个部分的操作,如set_configset_varadd_factsadd_to_group函数。例如,可以像这样组装一个新目标:

$example_server = Target.new('name'; => 'exampleserver')
$example_server.set_config('transport', 'ssh')
$example_server.set_config(['ssh', 'password', 's3cur3!')
$example_server.add_facts({'application' => 'example'})

可以访问目标的部分内容,例如$example_server.config['ssh'],但是这些目标只会在计划运行时存在于内存中。

现在我们已经理解了如何使用计划连接到客户端,我们将展示如何在计划的 Puppet 代码块中使用函数,利用 Bolt 和 Puppet 核心语言的特性。

使用计划函数

正如构造目标部分所示,使用run_task,Bolt 计划函数可以在 Puppet 代码块中本身使用,其中许多与直接在 Bolt 中运行的命令类型相同,例如run_commandrun_scriptrun_task。完整的命令列表可以在puppet.com/docs/bolt/latest/plan_functions.html中找到。

也可以使用run_plan函数在一个计划中运行另一个计划。这对于确保没有计划变得过大,并且可以更容易地重复使用它们是非常有用的。在 PEADM 模块中可以观察到的一种模式是使用subplan文件夹存放我们只期望在计划中使用的计划,从而减少目录的大小和复杂性。

需要注意的是,大多数 Puppet 语言特性,如函数、sensitive 类型和 lambdas,可以在此代码中使用,但其他特性,如延迟函数,不能使用,因为目录并未发送到节点以供应用。这些差异已在puppet.com/docs/bolt/latest/writing_plans.html#puppet-and-ruby-functions-in-plans中详细记录。

例如,在 PEADM 中,以下run_command函数停止所有存储在$all_targets变量中的目标上的 Puppet,然后在covert_target变量中的目标上运行modify_certificate计划,传入一个主要的add参数和要添加的扩展:

run_command('systemctl stop puppet', $all_targets)
run_plan('peadm::modify_certificate', $convert_targets,
  primary_host => $primary_target,
  add_extensions => {
    'pp_auth_role' => 'pe_compiler',
  },
)

Puppet 代码也可以通过apply函数应用,类似于运行puppet apply命令。例如,PEADM 使用以下代码创建节点组:

apply($primary_target) {
class { 'peadm::setup::node_manager_yaml':
  primary_host => $primary_target.peadm::certname(),
}

这应用了node_manager_yaml类,传入了primary_host参数。需要注意的是,如果在应用 Puppet 代码之前需要 Puppet 库,可以使用apply_prep函数,确保在使用apply函数之前这些库已经可用。

日志记录和结果

要在计划中添加日志记录,使用out::messageout::verbose函数,在每次运行时记录消息,只有当 Bolt 以verbose模式运行时才会输出详细消息。以下是一个示例:

out::message('Error')
out::verbose("Heres the error: $detailed_output")

Error将在每次 Bolt 运行时打印,但只有在使用–verbose标志时,第二条消息才会显示。

每个函数返回一个ResultSet类型的对象,每个目标包含自己的Result对象类型,除了apply函数,其ResultSet包含ApplyResult对象。计划返回一个PlanResult类型的输出,可以包含所有这些数据类型以及几乎所有 Puppet 数据类型。

这些对象可以分配给变量,然后使用函数来公开数据。所有这些对象类型中常用的两个函数是ok,它返回一个简单的布尔值来确认是否有任何错误;和value函数返回运行的输出。

更多类型特定函数可以在puppet.com/docs/bolt/latest/bolt_types_reference.html文档中查看。

要从计划返回输出,应使用返回函数与任何适当的数据类型;这可以是来自任务的直接输出或仅仅是一个字符串。如果没有使用返回函数,则输出将为undef。例如,以下代码将运行任务error_check_task,仅当成功时才会从任务output_task返回ResultSet类型的输出;否则,将返回字符串OH NO

plan return_result( $targets )
$did_this_work = run_task('error_check_task', $targets)
If $did_this_work.ok {
out::message('It worked')
return run_task('output_task', $targets)
}else{
Return "OH NO"
}

现在,让我们看看如何处理错误。

处理错误

要执行简单检查并因此失败计划,可以使用fail_plan函数。例如,以下代码将检查$targets变量是否仅包含单个目标:

unless get_targets($targets).size == 1 {
    fail_plan('This plan only accepts one target.')
  }

如果 Bolt 函数失败并且未将_catch_errors设置为true,则计划将失败。如果使用了_catch_errors,则可以允许计划继续执行并处理错误:

$install_agent_results = run_task('agent_install', $agents , '_catch_errors' => true)
$ install_agent_results.each |$agent_result| {
$target = $agent_result.target.name
if $result.ok
 { notice("${target} installed correctly ${result.value}")
} else {
 notice("${target} failed install with error: ${result.error.message}")
 }
}

或者,可以使用catch_result函数来捕获特定类型的错误,如下所示:

$install_agent_results = catch_error(agent_install/connection_error) || { run_task('agent_install', $agents , '_catch_errors' => true)
}

通过了解计划中的日志记录和错误处理,我们现在可以看看如何在计划中使用外部数据。由于 Bolt 使用 Puppet 作为库,因此可以使用 Hiera 访问外部数据。正如在第九章中所述,这可以确保我们将代码和数据分离,就像我们在 Puppet 代码中所做的那样。

管理数据源

可以使用内置的 facts 计划从主机收集事实,或者使用puppetdb_facts从 PuppetDB 收集,假设已在 Bolt 配置中设置了 PuppetDB。使用任何计划都会导致目标自动查询 PuppetDB 并更新其内存中的库存。以下示例将在targets上运行facts,并且将os.name事实等于Windows的目标分配给windows_targets变量:

run_plan('facts', 'targets' => $targets)
$windows_targets = get_targets($targets).filter |$target| { $target.facts['os']['name'] == 'Windows' }

PuppetDB 还可以使用puppetdb_query函数执行通用查询。要返回 PuppetDB 中列出的windows主机的所有certnames事实值,请使用以下代码:

$windows_targets = get_targets (puppetdb_query('inventory[certname] { facts.os.name = "windows" }'))

可以通过使用模块或 Bolt 项目级别的 Hiera,并具有适当的hiera.yaml,来在计划中使用 Hiera。然后可以在apply函数内或直接在计划中使用lookup函数。如果在apply函数内使用lookup,并假设已运行apply_prep函数,我们可以收集所有事实,并且 Hiera 将按预期工作。在计划中使用时,需要注意的重要区别是:Bolt 不像正常的 Puppet 代码(通过类)那样具有自动参数查找功能,并且 Bolt 层次结构不能使用顶层变量或事实。在 Bolt 中使用 Hiera 时,它使用两个层次的层级,项目和模块层级,其中项目层级具有更高的优先级。

Bolt 层次结构的一个示例是一个包含带有节点数据的层级的hiera.yaml项目,以及没有节点数据的plan_hierarchy键:

Hierarchy: -
- name: "Nodes" path: "targets/%{trusted.certname}.yaml"
- name: "Org" path: "%{org}.yaml"
plan_hierarchy:
- name: "Org" path: "%{org}.yaml"

计划中的lookup函数可以执行以下操作,并通过application变量能够在计划层次结构的组织级别查找dns_server_name变量:

plan exampleproject::exampleplan(
TargetSpec $nodes,
String $application
){
$dns_server_name = lookup('dns_server_name)
}

在接下来的部分,我们将探讨如何使用注释来记录元数据。

记录计划元数据

与任务不同,由于计划没有metadata.json文件,因此需要通过注释来记录,以便在运行puppet plans show <plan name>时提供描述。第一行注释将作为描述,或者可以使用@summary标签。使用@param <param name>的注释表示它是参数的描述,使用@api private将计划标记为私有。使用所有这些字段的示例如下:

# @summary This plan is just for example
# @api private
# @param example_servers The targets to run this plan on
# @param manage_user Whether the user account should be managed
plan exampleproject::exampleplan(
TargetSpec $example_servers,
Enum ['true', 'false'] $manage_user
){

数据类型的详细信息可以通过bolt plan show命令自动获取。

注意

将计划和任务添加到控制仓库可能是有用的,但需要注意的是,在使用 PDK 验证时,PDK 无法验证计划,并且只会忽略默认的最低级计划目录中的计划。如果你的结构将计划放在较低级别,你需要运行pdk来忽略这些低级目录中的计划,例如pdk set config project.validate.ignore subdir1/subdir2/plan

计划测试

测试 Puppet 计划超出了本书的范围。这是因为计划测试目前尚未完全实现,并且相比于我们在第八章中看到的常规 RSpec 测试,计划测试更具挑战性。某些功能尚未实现,例如模拟上传文件或自定义函数,这使得与模块测试相比,进行有意义且完整的测试变得困难。目前可以使用的测试功能可参见puppet.com/docs/bolt/latest/testing_plans.html

介绍 YAML 计划

由于 YAML 计划的使用频率远低于 Puppet 计划,这里将简要概述 YAML 计划。它们的命名方式与基于 Puppet 的计划类似,但以.yaml扩展名(而非.yml)结尾。然而,没有创建它们的命令。YAML 计划包含以下内容:

  • 描述:将在show命令中显示的内容

  • 参数:可以传递给计划的参数哈希

  • 私有:一个布尔值,表示计划是否对show命令可见

  • 返回值:计划返回的数组、布尔值、哈希值、数字或字符串

  • 步骤:将要运行的步骤数组

步骤本质上表示将在该步骤中执行的操作和步骤所需的变量。Bolt 中可用的选项与 Puppet 计划中的操作类似,例如命令、任务、脚本、文件下载和文件上传。与 Puppet 计划一样,YAML 计划可以通过计划步骤调用其他计划。

以下示例任务计划使用来自 Forge 的 Docker puppetlabs模块forge.puppet.com/modules/puppetlabs/docker来创建并加入一个额外的管理节点到 Docker swarm,展示了一些这些功能的使用:

description: configure docker swarm
paramters:
  firstnode
    type: TargetSpec
  Othernodes
    Type: Targetspec
- name: init
    task: docker::swarm_init
    targets: $firstnode
  - name: token
    task: docker::swarm_token
    targets: $firstnode
  - name:facts
    Fact:
    targets: $firstnode
  - name: managersjoin
    task: docker::join_swarm
    targets: $othernodes
    parameters:
      token: $token.map |$token_result| { $token_result['stdout'] }
       manager_ip: $facts.map |$facts_result| { $facts_result['stdout']['networking']['interfaces']['ip'] }
return $managersjoin.map | $managersjoin_result| {$managersjoin_result['stdout']}

该任务使用TargetSpec类型的firstnodeothernodes变量将服务器提供给目标。它使用swarm_init任务在第一个节点上初始化,并在此节点上运行swarm_token任务。接下来,Fact任务将在firstnode上运行,最后一步,join_swarm任务将在othernodes上运行。可以看到,调用具有前一步名称的变量可以让我们访问该步骤创建的输出。因此,我们可以获取令牌步骤的输出,并映射返回的 taskspec 类型,将stdout作为令牌使用。对于manager_ip参数,我们执行类似的操作,但这次,由于stdout中有更多内容,我们必须找到希望传递的networking.interface.ip地址事实。计划接着设置返回键,使用join步骤的stdout输出来确认计划的结果。

还可以使用eval步骤来计算值,并且可以使用 Puppet 和 Bolt 函数。messageverbose步骤用于输出,就像在 Puppet 计划中一样,而字符串插值遵循正常的 Puppet 原则,单引号('')没有插值,仅打印文本,双引号("")进行插值,同时使用管道符(|)和换行符可以将一块 Puppet 代码的表达式显示到下一行。

为了展示其中的一些内容,以下计划将安装一组字符串作为包:

parameters:
  packages:
    type: Array[String]
  servers:
    type: Targetspec
Steps:
  -name: unique_packages
  eval: $packages.unique
  -name: numer_of_packages
  eval: $unique_packages.size
  - verbose: 'Installing ${number_of_packages} packages'
  - name: install
    task: example::install_packages
    parameters:
      packages:  $unique_packages
      Targets: $servers
Return: $install.map | $install_result| {$install_result['stdout']}

我们可以看到,unique_packages eval 步骤使用 unique 函数来查找数组中的唯一值,而 numer_of_packages eval 步骤使用 size 函数,其结果传递给 verbose 输出并插入到一个字符串中,显示包的数量。example::install_packages 任务在 unique_packages 评估步骤的输出之后运行,然后将其输出用于返回值。

这只是使用 YAML 计划的总结。每个步骤的完整选项和更多内容可以在文档中找到:puppet.com/docs/bolt/latest/writing_yaml_plans.html

在接下来的部分,我们将查看一些常用的 Bolt 插件示例。

插件

插件允许 Bolt 在执行期间动态加载数据。插件本质上只是包含任务的模块,bolt_plugin.json 文件标识了哪些任务是插件,以及它们是何种类型的插件。有些插件内建于 Bolt 中,而其他插件可以被添加以扩展功能。

Bolt 插件有三种类型:

  • 引用:用于从外部源获取数据,例如将信息加载到库存文件中

  • 秘密:用于创建密钥来加密文本和解密密文

  • 在目标上调用 apply_prep 函数

我们将在以下小节中详细查看这些内容。

引用插件

inventory.yamlbolt-project.yaml 文件使用 _plugin 键,其值为插件名称,并随后列出与插件相关的参数。例如,要使用 puppetdb 插件并查询和选择 PuppetDB 中所有窗口节点,我们可以在 inventory.yaml 中添加以下组:

groups:
  - name: windows
    targets:
      - _plugin: puppetdb
        query: 'inventory[certname] { facts.kernel = "Windows" }'

这是假设 PuppetDB 连接配置详情已设置在其中一个配置文件中。

注意

配置了 PuppetDB 插件后,可以使用类似以下的单次查询来查询 PuppetDB:bolt task run 'inventory[certname] { facts.kernel = "``Windows" }'

另一种引用插件的方法是使用密码,其中 prompt 插件会提示用户从命令行输入密码。例如,以下内容将确保在对 target1.example.com 进行操作时,Bolt 会使用 winrm 连接,用户为 bill,密码则通过提示 Enter your password 来输入:

targets:
  - target1.example.com
  config:
  winrm:
    user: bill
    password:
      _plugin: prompt
      message: Enter your password

插件还可以通过 resolve_references 函数在计划中使用。以下示例展示了 pecdm 模块通过 resolve_references 函数使用插件的一个小节:

$inventory = ['server', 'psql', 'compiler', 'node', 'windows_node' ].reduce({}) |Hash $memo, String $i| {,
$memo + { $i => resolve_references( {
'_plugin' => 'terraform',
'dir' => $tf_dir,

在前面的代码块中,它本质上是通过每个组名进行迭代,并构建一个从 tf_dir 变量设置的 terraform 目录中读取的目标条目的数组。要查看进一步的示例,请查看你的实验室设置中的 inventory.yaml 文件内容,该文件使用了 terraform 插件。

秘密插件

pckcs7是 Bolt 的默认且唯一的密钥插件。要创建加密密钥,请运行bolt secret createkeys -–force Unix 命令或New-BoltSecretKey -Force PowerShell cmdlet。这将在项目的keys文件夹中创建密钥。可以通过bolt secret encrypt 'N33dt0kn0wba515!' --plugin pckcs7 Unix 命令或Protect-BoltSecret -Text 'N33dt0kn0wba515! ' -Plugin pckcs7 PowerShell cmdlet 生成密文。

此命令生成的密文可以在如inventory.yaml这样的地方使用,举个例子,使用pkcs7引用插件:

targets:
  - uri: target1.example.com
    config:
      ssh:
        password:
          _plugin: pkcs7
          encrypted_value: |
            ENC[PKCS7,MIIBiQYJK]

请注意,之前的加密字符串已经缩短,且其默认密钥大小为2048。可以通过在bolt-project.yaml文件中配置插件或默认配置和用户配置来更改这一点。

Puppet 库

apply_prep函数在计划中被调用。插件运行的每个目标必须能够使用插件所用的脚本语言。目前,只有puppet-agent作为 Puppet 库插件存在,并且默认配置为可用。但任何未来的库或自定义编写的库,都将以类似本例的方式添加到 Bolt、用户或默认配置中:

plugin-hooks:
  puppet_library:
    plugin: task
    task: package
    parameters:
      name: puppet-agent
      action: install

支持的内置插件完整列表可以在puppet.com/docs/bolt/latest/supported_plugins.html查看。编写插件不在本书的范围内,但puppet.com/docs/bolt/latest/writing_plugins.html中的文档提供了进一步的建议。

在详细讲解了 Bolt 后,我们将在以下实验中实践创建和使用 Bolt 项目。

实验 - 创建和使用 Bolt 项目

在本实验中,我们将创建一个 Bolt 项目。我们将创建一个任务,在 Windows 和 Linux 节点上运行facter命令。

步骤如下:

  • 使用以下代码行创建一个 Bolt 项目:

    bolt project init packtlab
    
  • 通过在 PECDM Bolt 项目中查找 Windows 和 Linux 客户端并复制输出,创建一个inventory.yaml文件:

    bolt inventory show --targets agent_nodes --detail
    bolt inventory show --targets windows_agent_nodes --detail
    
  • 编写一个任务,覆盖 Windows 和 Linux,运行facter命令,如果只需要返回单一事实,则传递一个参数。

  • 编写一个计划,使用run_command来运行facter并返回计划的结果。

  • 在 Windows 和 Linux 客户端上运行任务和计划。

  • 你可以在github.com/PacktPublishing/Puppet-8-for-DevOps-Engineers/tree/main/ch12找到示例解决方案。

总结

在这一章中,我们展示了 Bolt 如何通过提供执行临时操作的能力来补充 Puppet 的状态管理方法,处理那些不符合 Puppet 声明式强制方法的任务。我们还展示了传输方式如何使 Bolt 能够连接到目标。我们查看了如何通过 Unix 或 PowerShell 使用 Bolt 命令来执行命令、脚本、Puppet 代码和清单,此外还可以上传和下载文件。我们回顾了 Bolt 如何记录到 bolt-debug.log 文件,并且如何配置日志记录以获取更多的日志,以便解决不同的问题。

然后我们展示了 Bolt 项目如何提供目录结构来存储配置和数据。Bolt 项目提供 inventory.yaml 文件来存储目标和传输配置,提供 bolt-project.yaml 文件来存储项目级别的配置设置,并允许将模块依赖项下载到项目中。我们讨论了如何将 Bolt 项目加载到模块路径中,并与其下载的任何模块一起使用。随后,我们强调了项目格式如何随着不同版本的 Bolt 发生变化,以及如何使用 bolt migrate 命令将旧版本项目转换为新格式。

接着我们讨论了任务是单一操作脚本,可以使用任何在目标机器上运行的语言,配合 JSON 文件提供元数据,如参数。我们还展示了如何根据目标列出多个实现。我们查看了如何使用敏感参数使得任务能够使用密码和其他机密信息,而不会在 API 中记录。我们介绍了 noop 选项,它作为标准方式传递参数给任务并以不执行模式运行。我们还展示了如何远程任务包含 remote 参数,设置为 true,并使用远程传输方式,使得 Web 访问服务能够使用任务,即便不能通过传统方式登录。

接着,我们讨论了任务如何能够共享实现中的脚本,并引用其他模块。我们还讨论了一些安全实践,以确保参数能安全地传递给任务。

接着讨论了计划(Plans),它们是将多个任务一起运行并提供逻辑和控制流的方式。我们看到计划可以使用 Puppet 语言或 YAML 编写,目标可以通过 targetspec 数据类型和函数创建。我们还看到了如何在运行计划后返回结构化的结果。

我们接着讨论了 Bolt 插件如何通过引用插件来动态加载数据到 Bolt 运行中,使用引用插件来获取和存储数据,例如从 Terraform 填充数据到清单中。我们还可以使用机密插件提供加密和解密值所需的密钥,以便在 Bolt 运行中使用。我们查看的第三种插件是 Puppet 库插件,目前仅实现了通过 Bolt 安装 Puppet 代理。

在本章中,我们看到如何将 Bolt 与 Puppet 配合使用,结合声明式和有状态的语言方法,发挥两者的优势,从而使 Puppet 配置更加灵活。

在回顾了如何使用 Bolt 和 Puppet Enterprise 之后,在下一章中,我们将探讨如何监控和扩展 Puppet 基础设施、审查性能问题,并使用Puppet 数据服务来实现外部数据模式,允许用户通过自助服务 API 将数据输入到 Puppet 设置中。

第十三章:深入探讨 Puppet 服务器

本章将介绍如何监控、调优并将 Puppet 基础设施与第三方数据源集成。你将了解如何查找我们在前几章讨论的各种服务的日志,并如何查找当前的状态 API。接着,你将学习如何将这些日志和状态集成到像logstash这样的服务中,以提供更大的可见性和告警选项。然后,我们将回顾 Puppet 提供的性能指标,以及如何将这些指标与 Splunk 和 Grafana 等仪表板工具集成,从而为 Puppet 的基础设施提供监控可观测性。我们将为SplunkGrafana设置实验环境,作为 Puppet 操作仪表板的一部分,展示这些仪表板。通过使用这些指标,你将学习如何调整和扩展 Puppet 基础设施的各个组件,以应对 Puppet 扩展时常见的问题和挑战。之后,你将了解外部提供者模式如何使事实、分类和 Hiera 数据能够从外部数据源传输到 Puppet,并允许 Puppet 平台团队提供自助服务,无需完全了解 Puppet 或环境发布程序。本章将展示多个第三方实现,包括ServiceNow1Password。在本章的实验中,我们将实现Puppet 数据服务PDS)来演示这一模式。

在本章中,我们将涵盖以下主要内容:

  • 日志与状态

  • 性能指标、调优与扩展

  • 识别和避免常见问题

  • 外部数据源

技术要求

github.com/puppetlabs/control-repo将控制仓库克隆到你的 GitHub 账户(controlrepo-chapter13),并更新该仓库中的以下文件:

通过下载github.com/PacktPublishing/Puppet-8-for-DevOps-Engineers/blob/main/ch13/params.json中的params.json文件,构建一个包含三个编译器和三个客户端的大型集群,并更新文件以包含你的控制仓库位置和控制仓库的 SSH 密钥。然后,从你的pecdm目录运行以下命令:

bolt --verbose plan run pecdm::provision --params @params.json

首先,我们将查看在哪里可以找到日志以及 Puppet 服务和基础设施的当前状态。这对于如何调优和排除 Puppet 故障至关重要。

日志记录和状态

当我们在本书中之前讨论不同的 Puppet 组件时,我们列出了日志目录,但有一个统一的日志参考点是很有用的。

探索日志位置

本节提供了这些日志的列表,标题包括核心功能、包含目录和该目录中的日志列表:

  • /var/log/puppetlabs/puppetserver/:主要服务器日志目录

  • puppetserver.log:主要的服务器日志,记录其活动

  • puppetserver-access.log:访问端点的请求

  • puppetserver-daemon.log:崩溃报告和致命错误

  • puppetserver-status.log:服务的调试状态日志

  • /var/log/puppetlabs/postgresql/<version>:PostgreSQL 日志目录* pgstartup.log:启动日志* postgresql-<Mon – Sun>.log:每日调试日志* /var/log/puppetlabs/puppetdb/:PuppetDB 日志目录* puppetdb.log:PuppetDB 服务活动日志* puppetdb-access.log:访问端点的请求* puppetdb-status.log:服务的调试状态日志* /var/log/puppetlabs/puppetserver/:主要服务器日志目录* code-manager-access.log:访问代码管理器端点的请求* file-sync-access.log:访问文件同步端点的请求* pcp-broker.log:编译器上的 Puppet 通信协议代理* /var/log/puppetlabs/console-services/:Puppet Enterprise 控制台服务日志目录* console-services.log:控制台服务活动日志* console-services-api-access.log:访问控制台服务 API 端点的请求* console-services-access.log:访问控制台服务端点的请求* console-services-daemon.log:崩溃报告和致命错误日志* /var/log/puppetlabs/nginx/:nginx 日志目录* access.log:访问 nginx 端点的请求* error.log:nginx 错误和一般控制台错误* 代理日志:

当你手动运行 Puppet 时,你在屏幕上看到的代理输出会根据puppet.conf文件中的logdestlogdir设置记录到一个位置。logdest参数可以设置为syslog(发送到 POSIX syslog 服务)、eventlog(发送到 Windows 事件日志)、console(日志发送到控制台)或一个文件名,以便将其输出到logdest设置的位置下的该文件名。syslog是 Unix 系统的默认设置,而eventlog是 Windows 的默认设置。logdest的默认值为 Unix 的/var/log/puppetlabs/puppet和 Windows 的C:\ProgramData\PuppetLabs\puppet\var\log

注意

可以开启服务器配置文件分析,它可以生成详细的目录日志信息。然后可以将其绘制为图形,展示目录编译的深入调试信息。本书不涉及此内容。更多信息请参考 Puppet 的文档:github.com/puppetlabs/puppet/blob/main/docs/profiling.md

经过对不同日志位置的检查,我们可以清楚地看到,将这些服务器日志转发到专用工具中以便索引和处理是非常有用的。

转发服务器日志

随着 Puppet 客户端数量的增加,我们在第十章中完成的日志跟踪工作变得不切实际。在这种情况下,需要更专业的logback.xml,可以将其发送到像 Elastic 的 Logstash 或 Grafana 的 Loki 这样的日志后端。logback.xml文件包含appender定义,它是 Logback 用于写入日志的组件。

通过观察puppetserver.logappender配置,我们可以看到当前的配置:

    <appender name="F1" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>/var/log/puppetlabs/puppetserver/puppetserver.log</file>
        <append>true</append>
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>/var/log/puppetlabs/puppetserver/puppetserver-%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
            <!-- each file should be at most 200MB, keep 90 days worth of history, but at most 1GB total -->
            <maxFileSize>200MB</maxFileSize>
            <maxHistory>90</maxHistory>
            <totalSizeCap>1GB</totalSizeCap>
        </rollingPolicy>
        <encoder>
            <pattern>%d{yyyy-MM-dd'T'HH:mm:ss.SSSXXX} %-5p [%t] [%c{2}] %m%n</pattern>
        </encoder>
    </appender>

在这里,我们可以看到它是如何追加到日志中的,使用的文件名模式,以及日志轮转的日期,并且每个文件的大小不会超过 200 MB,总大小不超过 1 GB,日志将不会保存超过 90 天。编码器展示了日志条目的构成方式。

为了添加日志的 JSON 版本,我们可以做一个类似的条目,仅包含 5 天的日志。在这里,编码器是 Logstash 编码器,用于以 JSON 格式输出。以下是此appender的代码:

<appender name="server_JSON" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <file>/var/log/puppetlabs/puppetserver/puppetserver.log.json</file>
    <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
        <fileNamePattern>/var/log/puppetlabs/puppetserver/puppetserver.log.json.%d{yyyy-MM-dd}</fileNamePattern>
        <maxHistory>5</maxHistory>
    </rollingPolicy>
    <encoder class="net.logstash.logback.encoder.LogstashEncoder"/>
</appender>

要在logback.xml文件的底部启用此appender,请添加以下定义:

    <root level="info">
        <!--<appender-ref ref="STDOUT"/>-->
        <appender-ref ref="${logappender:-DUMMY}" />
        <appender-ref ref="F1"/>
    </root>

在根部分中添加<appender-ref ref="server_JSON"/>将启用我们的 JSON appender。重新启动puppetserver服务将启用新的appender

要设置server-access.log,可以使用github.com/PacktPublishing/Puppet-8-for-DevOps-Engineers/blob/main/ch13/appender_example.xml中的代码,这将添加一个appender,它将配置适当模式的 JSON 输出。

在这种情况下,您需要考虑磁盘空间。可以使用 JSON 日志记录来运行此操作。Logback 是一个强大的库,但本书无法涵盖所有选项。可以在logback.qos.ch/manual/configuration.htmllogback.qos.ch/manual/appenders.html中查看这些选项。

现在日志文件以 JSON 格式存在,可以配置像 Grafana 的 Promtail 或 Elastic 的 Filebeat 这样的工具,将日志文件转发到像 Elastic 的 Logstash 或 Grafana 的 Loki 这样的服务。由 Logrotate 管理的 Ruby 日志也可以被收集,但这需要更多的工作,去为它们设置合适的模式进行处理。

注意

Puppet Enterprise 服务、console-services、PuppetDB 和协调服务都使用 Logback,并可以像这样转发它们的日志。

在回顾了如何将日志发送到外部服务进行处理之后,我们将看到如何将应用 Puppet 目录生成的报告通过报告处理器发送到外部工具。

报告处理器

除了服务器日志记录,如 第十章 所示,每次运行目录都会生成报告。在 第十章 中,您看到如何通过 peadm 配置它,以便使用 reports = puppetdbpuppet.confmaster/server 部分将其存储在 puppetdb 中。设置这个 reports 值告诉服务器使用报告处理器,这是一个在 Puppet Server 接收到报告时运行的 Ruby 脚本。然后该脚本执行操作,将其传递给目标。在 PuppetDB 的情况下,就是将报告发送到 PuppetDB 存储。有三个内置的报告处理器:httplogstorehttp 将报告发送到通过 puppet.conf 中的 reporturl 设置指定的 HTTP 地址,格式为 YAML。log 将报告输出发送到 puppet.conflogdestlogdir 指定的日志文件,而 store 将报告的输出放入 puppet.confreportdir 设置指定的文件。Puppet Forge 中还有其他自定义报告处理器可用,包括 Splunk 集成模块,详细信息请参见 forge.puppet.com/modules/puppetlabs/splunk_hec,以及 Datadog 代理模块,详细信息请参见 forge.puppet.com/modules/datadog/datadog_agent,这使得报告数据可以在这些第三方服务中查看。根据模块的不同,指令有所不同,但通常,添加报告处理器所需的最小操作是 Puppet Server 在环境中部署该模块,并且报告的转发器名称已设置。编写自定义报告处理器超出了本书的范围,但可以在 puppet.com/docs/puppet/latest/reporting_write_processors.html 中找到详细信息。

)

除了日志和报告,我们还可以通过调用状态 API 来查看当前 Puppet 基础架构的状态。

访问状态 API

Puppet 提供了一个可以通过GET /status/v1/services调用的状态端点。该端点返回服务器上所有已知服务的状态。此访问由auth.conf控制,并可以通过 Puppet CA 证书在本地访问,如下所示:

cert="$(puppet config print hostcert)"
cacert="$(puppet config print localcacert)"
key="$(puppet config print hostprivkey)"
uri="https://$(puppet config print server):8140/status/v1/services"
curl --cert "$cert" --cacert "$cacert" --key "$key" "$uri" | jq

最后的 JQ 管道(命令行 JSON 处理器)是可选的,但它使输出更具可读性。以下是 Puppet Server 状态输出的示例:

{
  "server": {
    "service_version": "7.6.0",
    "service_status_version": 1,
    "detail_level": "info",
    "state": "running",
    "status": {},
    "active_alerts": []
  },

可以通过在 URL 中添加服务名称来定位单个服务——例如,GET/status/v1/services/server。对于有额外服务的 PE 安装,应该为每个服务调用特定的端口。PE 服务将在第十四章中讨论。

这些调用的返回代码将是200,表示所有服务正在运行;404表示找不到服务;或503,表示服务状态不在运行状态。

当所有服务都在运行时,服务器状态为running;如果任何服务报告错误,则为error;如果任何服务处于startingstopping状态,则为这两种状态之一;如果任何服务报告未知状态,则为unknown

Puppet Enterprise 提供了一个额外的命令行选项,可以通过puppet infrastructure status命令调用 API,输出类似如下所示:

Puppet Server: Running, checked via https://pe-server-davidsand-0-cffe02.tq2kpafq5bsehkpub4ur5a35ya.xx.internal.cloudapp.net:8140/status/v1/services
  PuppetDB: Running, checked via**https://pe-server-davidsand-0-cffe02.tq2kpafq5bsehkpub4ur5a35ya.xx.internal.cloudapp.net:8081/status/v1/services**

在网页控制台中,您可以通过点击Puppet 服务状态按钮查看 Puppet 服务的状态,如下图所示:

图 13.1 – 网页控制台中的 Puppet 服务状态

图 13.1 – 网页控制台中的 Puppet 服务状态

puppet status命令在 Puppet 5 中已被弃用,并在 Puppet 7 中删除。它没有使用 API 端点。

到目前为止,我们查看的日志、报告和状态让我们了解 Puppet 基础设施和客户端的行为,但它们并未告诉我们关于基础设施及其客户端的整体性能。接下来,我们将查看 Puppet 提供的指标,以及如何使用它们来监控基础设施的性能和容量。

指标、调优和扩展

为了通过服务状态 API 提供更详细的 Puppet 服务性能和健康数据,可以将level标志设置为debug;这将返回指标。例如,要返回 Puppet Server 的指标并使用 JQ 进行过滤,可以运行以下命令:

cert="$(puppet config print hostcert)"
cacert="$(puppet config print localcacert)"
key="$(puppet config print hostprivkey)"
uri="https://$(puppet config print server):8140/status/v1/services/server?level=debug"
curl --cert "$cert" --cacert "$cacert" --key "$key" "$uri" | jq ".status.experimental"

这将输出如下指标的数据,例如puppet-v3-catalog端点:

{
  "http-metrics": [
    {
      "route-id": "puppet-v3-catalog-/*/",
      "count": 41,
      "mean": 4459,
      "aggregate": 182819
    },

这给出了自服务上次重启以来,count表示对该端点进行的调用次数。mean是 5 分钟内的平均响应时间,而aggregate是服务启动以来的总时间。

各种服务中有许多度量指标。要查看这些度量的定义,可以在文档中查看 API 服务(例如,puppet.com/docs/pe/2021.7/status_api.html)。不过,总体来说,它们的文档并不完善,可能需要一些探索,或者你可以在 Puppet 的 Slack 渠道和支持渠道(如果你有合同的话)上提问。不要被大多数度量标题中的实验性标签所困扰——大多数度量数据已经可用了,只是 Puppet 尚未移除实验性标签。

注意

有关 Puppet 的底层度量库如何工作的详细解释,可通过www.youtube.com/watch?v=czes-oa0yik&t=0s观看,由该度量库的作者提供。

现在,让我们来看一下用于展示度量数据的仪表盘。

探索度量仪表盘

Puppet 提供了三种实现方式,自动化收集和展示度量数据的过程:

  • Puppet 操作仪表盘,可在forge.puppet.com/modules/puppetlabs/puppet_operational_dashboards找到,是一个由 Puppet Forge 提供的支持模块,集成了 Telegraf、InfluxDB 和 Grafana,提供机制来发送 API 度量数据、将其存储在时间序列数据库中,并在预配置的仪表盘中可视化数据。操作仪表盘支持 Puppet Enterprise 和开源 Puppet。下图展示了一个此类仪表盘的示例:

图 13.2 – Grafana Puppetserver 性能仪表盘

图 13.2 – Grafana Puppetserver 性能仪表盘

图 13.3 – Splunk Puppet 服务器内存仪表盘

图 13.3 – Splunk Puppet 服务器内存仪表盘

Puppet 团队共同合作,保持 Splunk 和 Grafana 仪表盘的一致性。

对于 Puppet Enterprise,还有一个/opt/puppetlabs/puppet-metrics-collector目录。然后可以使用诸如grepJQ的命令(假设终端处于度量收集器目录中)搜索这些 JSON 文件。两个常见的查询将在average-free-jrubiesqueue_depth中详细解释。可以像这样添加:

grep -oP '"average-free-jrubies.*?,' puppetserver/primary.example.com/*.json puppetserver/pe-server-davidsand-0-cffe02.tq2kpafq5bsehkpub4ur5a35ya.xx.internal.cloudapp.net/*.json
"puppetserver/pe-server-davidsand-0-cffe02.tq2kpafq5bsehkpub4ur5a35ya.xx.internal.cloudapp.net/20220731T220502Z.json":"average-free-jrubies":3
jq '.. |."queue_depth "? | select(. != null)| input_filename , .' -- puppetdb/pe-server-davidsand-0-cffe02.tq2kpafq5bsehkpub4ur5a35ya.xx.internal.cloudapp.net/*.json
"puppetdb/pe-server-davidsand-0-cffe02.tq2kpafq5bsehkpub4ur5a35ya.xx.internal.cloudapp.net/20220731T221001Z.json"
0

为了更方便地共享这些数据,可以通过运行/opt/puppetlabs/puppet-metrics-collector/scripts/create-metrics-archive命令来归档数据,如果希望归档一定天数的数据,可以使用–r标志。

在了解了如何收集和显示度量后,我们将讨论一些常见的性能和容量问题,以及如何使用度量来管理它们。

识别并避免常见问题

设置好日志、状态和度量后,我们需要考虑应该关注什么以及如何检查我们的 Puppet 基础设施。应该有正常的 CPU、内存和磁盘使用情况监控,但有一些关键的功能区域需要关注。我们将在接下来的章节中讨论这些内容。

目录编译

第十章中,我们学习了每次目录编译都需要一个 JRuby 实例来为每个 Puppet 请求编译目录,并且 Puppet 主服务器或编译服务器提供了这个 JRuby 能力。为了计算处理基础设施负载所需的 JRuby 实例数,我们可以将运行间隔(服务器检查频率)除以编译的平均时长。这样就能得出每台服务器所需的 JRuby 实例数。然后,我们可以将预期基础设施中 Puppet 客户端的数量除以这个数字,得到所需的 JRuby 实例总数估算值:

添加此内容:总 JRuby 实例数 = Puppet 客户端数量 / (运行间隔 / 平均 编译时长

选择适当的基础设施大小可能会很复杂。可以通过在puppetserver.conf文件中运行max-active-instances来设置主服务器或编译服务器上的 JRuby 实例数;默认情况下,它设置为 CPU 数量减 1,范围为 1 到 4。每个 JRuby 实例需要在 JVM 堆栈中分配内存。对于 Puppet Enterprise,这个文件是通过设置hiera值为puppet_enterprise::master::puppetserver::jruby_max_active_instances:来控制的。

总 JVM 堆栈内存由 Puppet 服务器启动脚本分配,根据操作系统的不同,可能位于/etc/sysconfig/puppetserver/etc/defaults/puppetserver。这个内存大小由xmx参数设置,可以计算出每个 JRuby 实例默认需要 512MB 的内存,并为其他 Java 任务保留 512MB 的空余空间:

添加此内容:*所需总堆栈大小 = 512MB + 最大活动实例数 ** 512MB

建议你永远不要超过 32 GB 的 JVM 堆栈大小。根据各种现场实验的结果,JRuby 实例的最大有效数量似乎在 11 到 13 之间。这个最大值通常适用于规模更大的环境,并且需要关注以防止编译器故障。在这种情况下,完全依赖水平扩展是不明智的;应该与垂直扩展(增加更多的编译服务器)相结合。

在刚开始时,进行大小调整建议可能会很困难——你可能不清楚你的平均编译时间会是多少,而编译的秒数可能会产生很大的影响,因此,在整个环境逐步扩展时,监控 JRuby 的使用情况和目录编译时间是明智的,并且在出现时寻找异常值。Puppet 为 Puppet Enterprise 大小调整提供了一些指南,详情请参考puppet.com/docs/pe/2021.7/tuning_infrastructure.html

在监控目录性能时,我们有一些关键的关注点。jruby.num-jrubiesjruby.num-free-jrubies指标显示服务器上有多少个 JRuby 实例,以及其中有多少个是空闲的。在查看这些指标时,应计算基础设施的平均使用容量。建议避免超过 80%的使用率,因为性能通常在超过这个值后无法扩展。你还应该确认负载均衡器没有问题,并且空闲的 JRuby 实例在编译器之间使用均衡。一个可能发生的问题是雷鸣群体效应,即许多服务器同时请求目录编译。这可以通过 JRuby 实例使用的巨大峰值在指标中看到。如果你遇到这种情况,可以使用Puppet 运行调度器模块,详情请参见forge.puppet.com/modules/reidmv/puppet_run_scheduler,来分散 Puppet 代理运行的调度。

如果超过了 JRuby 池的容量,请求将会排队并在默认情况下超时,超时为 10 秒。borrow-timeout-count指标提供了等待 JRuby 实例变得可用时超时的请求数量。

目录运行时间

如前所述,目录的运行时间对基础设施中所需的 JRuby 实例数量有巨大影响。查看metrics-time-total指标,它显示报告事件发送的编译时间,我们可以查看编译的平均时间,帮助进行容量计算。我们还可以查看这些数据的分布,看看是否存在任何极端异常值,我们希望对该目录及其 Puppet 代码进行调查。

可以查看以下表格中一些关键区域:

指标定义 指标名称
目录编译时间 Metrics-time-config_retrieval
应用目录的时间 Metrics-time.catalog_application
目录中的资源数量 Metrics-resources-total
生成事实的时间 Metrics-changes-total

表 12.1 – 目录指标

在这些度量指标中,您应该确保每个异常值是有原因的。如果代码或事实较为复杂,或某些资源已知较慢,这可能是正常的,或者可能是需要在应用到更多服务器之前审查的低效代码,从而节省基础设施容量。

PuppetDB 和 PostgreSQL 调优

对于 PuppetD,最佳的监控指标是 jvm-metrics.heap-memory.committedjvm-metrics.heap-memory.used。如果已用内存的大小经常接近已分配内存的大小,最好增加堆栈大小。与编译器类似,这涉及到更新 /etc/sysconfig//etc/default/puppetdb 中的 puppetdbpe-puppetdb 配置文件(取决于操作系统),并更新 JAVA_ARGS 参数。例如,如果你发现 jvm-metrics.heap-memory.committed 设置为 512 MB,但 jvm-metrics.heap-memory.used 经常接近这个限制,可以将最大堆内存大小从 JAVA_ARGS ="-Xmx512m" 更新为 JAVA_ARGS="-Xmx1g"。完成后,需要重启 PuppetDB 服务。但是,请注意,所有因内存不足而排队的任务将在重启后继续执行。对于 Puppet Enterprise,这个文件可以通过将 hiera 值设置为 puppet_enterprise::profile::puppetdb::java_args: 来控制。

另一个好的性能指示是队列深度,表示为 puppetdb-status.status.queue_depth 指标。如果这个值很高且有空闲的 CPU,那么增加 PuppetDB 可用的 CPU 线程数量将是有益的。这可以在 PuppetDB 配置文件 /etc/puppetlabs/puppetdb/conf.d 中完成。如果 PuppetDB 是通过包安装的,可以在 [command-processing] 部分的 threads 键中设置,或者如果使用了 Puppet PuppetDB 模块(在 Puppet Enterprise 中也是如此),则应使用模块的设置来调整类。在 Puppet Enterprise 中,可以在 Web 控制台的 节点组 部分进行调整。任何对线程的更改都需要重启 PuppetDB 服务。

反向场景是,CPU 使用率很高且限流,但 PuppetDB 队列较低,这时应释放线程以提高其他服务的吞吐量。

调优大小

为了帮助根据可用硬件获得正确的服务器设置,Puppet Enterprise 提供了 puppet infrastructure tune 命令。

这会计算出应用于服务器的最佳设置。以下是命令输出中的建议 Hiera 设置示例:

puppet_enterprise::profile::database::shared_buffers: 3176MB
puppet_enterprise::puppetdb::command_processing_threads: 1
puppet_enterprise::profile::puppetdb::java_args:
  Xms: 1588m
  Xmx: 1588m
puppet_enterprise::profile::orchestrator::jruby_max_active_instances: 2
puppet_enterprise::profile::orchestrator::java_args:
  Xms: 1588m
  Xmx: 1588m
puppet_enterprise::profile::console::java_args:
  Xms: 1024m
  Xmx: 1024m
puppet_enterprise::master::puppetserver::jruby_max_active_instances: 2
puppet_enterprise::profile::master::java_args:
  Xms: 1536m
  Xmx: 1536m
puppet_enterprise::master::puppetserver::reserved_code_cache: 192m

这些提取的数据可以放入 Hiera 文件中,并与主服务器进行分类。请注意,如果内存足够大,它会推荐堆大小配置超过 32 GB。这是次优配置,正如我们在查看编译大小和问题时讨论的那样。

以下是一般建议:

处理度量指标的数量可能看起来很困难,尤其是在缺乏明确定义的情况下,但如果使用 Splunk 插件或操作仪表板,这可以为你提供与 Puppet 支持团队使用和监控的一致视图。了解你的环境在这些数值下的常态行为,并查看图表中的尖峰,将它们与其他数据关联起来,可以帮助你发现问题。

使用 Puppet 的知识库,它自 2021 年 4 月起向所有用户开放,可以帮助你搜索问题。查看如 support.puppet.com/hc/en-us/sections/360000926413-Performance-tuning 这样的文章集合,可以帮助你更深入理解你遇到的任何问题。

实验 – 配置度量仪表板

在讨论了度量标准和 Puppet 的两种查看选项之后,我们可以配置 Splunk 仪表板和 Puppet 操作仪表板,以查看提供的仪表板。使用前一部分中描述的关于 PuppetDB 和编译器容量的问题,找到可以帮助你调查的图表。

配置 Puppet 操作仪表板:

  1. 选择一个节点来托管 Puppet 操作仪表板,并在 Web 控制台中将 puppet_operational_dashboards 分类为节点组。

  2. 在节点组的 PE 基础设施代理中,将 puppet_operational_dashboards::enterprise_infrastructure 添加到 Classes(类)标签下的类列表中。

  3. 在所有节点上运行 puppet,直到服务器显示为干净状态。

  4. 登录到 https://<operational_dashboard_node_ip>:3000user=admin password=admin

配置 Splunk:

  1. www.splunk.com/en_us/sign-up.html?301=/page/sign_up 注册一个 Splunk 账户(这是免费的)。

  2. 选择另一个节点来托管 Splunk Enterprise,通过在 Web 控制台上将 splunk::enterprise 分类为节点组并固定所选节点。

  3. 安装 Puppet 报告查看器:

    1. 登录并下载 splunkbase.splunk.com/app/4413/

    2. 登录到 https://<splunk_server_ip>:8000username=adminpassword=changeme

    3. 选择 Apps 栏左上角的齿轮图标。

    4. 在下一个屏幕中,选择右上角的 upload app from file(从文件上传应用)。

  4. 在 Splunk 中将许可证设置为免费:

    1. 点击右上角的 Settings(设置)。

    2. 在系统下拉菜单中,选择 Licensing(许可)。

    3. 选择 change(更改) license group(许可证组)。

    4. 选择 free license(免费许可证)并点击 Save(保存)。

  5. 在 Splunk 中创建 HEC 令牌:

    1. 在 Splunk 控制台中导航到 Settings | Data Input

    2. 添加一个新的 HTTP 事件收集器,命名为您选择的名称。

    3. 确保 Indexer acknowledgement 未启用。

    4. 单击 Next 并将 source type 设置为 Automatic

    5. 确保 App Context 设置为 Puppet Report Viewer

    6. 添加主索引。

    7. 设置 默认索引main

    8. 单击 Review 然后点击 Submit

  6. 在 PE 基础设施节点组中,将 puppet_metrics_collector 分类为 metrics_server_type 参数设置为 splunk_hec

  7. splunk_hec 分类为 enable_reports 设置为 true

  8. events_reporting_enabled 设置为 true

  9. manage_routes 设置为 true

  10. token 设置为 <在第 5 步生成的 token>

  11. url 设置为 https://<public_ip_of_splunk_server>:8088/services/collector

  12. 登录 https://<public_ip_of_splunk_server>:8000,并运行 index=* sourcetype=puppet:summary 以确保数据正在被收集(可能需要一些时间才能开始)。

现在 Puppet 操作面板和 Splunk 已经配置完成,浏览各种图表和面板,找到与目录容量和 PuppetDB 性能相关的图表。

如果可能,请保留 Splunk 设置,以便在下一部分中使用,因为库存和库存趋势视图是 Facter 终端输出的仪表板视图。

现在您已经了解了如何集成 Puppet 的状态、日志和指标,我们可以查看一种模式,该模式允许我们将 Puppet 与其他服务集成,并为 Puppet 用户提供自助服务。

外部数据提供者模式

Puppet 将不再是您系统中唯一的配置和信息来源。很可能会有多个 配置管理数据库CMDBs),例如 ServiceNow 或应用程序团队用于存储信息的内部开发系统。您的几位同事和内部客户可能希望能够创建例外和自定义,而无需了解 Puppet 代码及其部署工作流。同时,也会有需求希望能够将 Puppet 数据反馈到外部系统。外部数据提供者模式使这一切成为可能,允许您执行以下操作:

  • 在分类中进行更改

  • 添加和更改受信任的事实

  • 将现有数据作为事实或 Hiera 数据输入

  • 将 Facter 数据发送到外部源

在介绍了外部数据提供者模式的核心概念后,我们将探讨其中使用的每个技术组件。

了解外部数据提供者组件

该模式的底层组件如下图所示:

图 13.4 – 外部数据提供者模式的核心组件

图 13.4 – 外部数据提供者模式的核心组件

我们将逐一展示这个模式是如何工作的。本书无法展示如何编写每个组件,但我们会详细说明文档在哪里可以找到。在接下来的部分中,将参考示例实现:

后端存储服务BSS)允许你存储数据以供使用。BSS 的技术解决方案并不重要,但它必须具有韧性,并且在读取时提供高吞吐量。

该吞吐量可以通过 2 + <Hiera 层数> 来计算每次 Puppet agent 执行时的读取次数。为了说明如果一个 10,000 台服务器的环境使用默认的 30 分钟 agent 执行时间,并且有 5 层 Hiera 配置时,如何计算该吞吐量,可以这样计算:(2+5) * 10,000 / 1,800 = 39 次查询每秒(四舍五入)。

像 CMDB 或内部应用程序这样的工具可以直接进行查询并充当 BSS,但该工具能够处理工作负载。

trusted 外部命令在 Puppet 6.11 和 7.0 中引入,允许在 Puppet 执行期间运行脚本,并从外部源收集事实和分类信息。此脚本应以客户端的 certname 属性作为参数,返回一个包含事实的 JSON 哈希,并且对于任何未知的 certname 返回错误代码。可以通过在每个主服务器和编译服务器的 puppet.conf 文件的 master/server 部分中使用 trusted_external_command 设置来配置此脚本。此命令返回的事实将包含在 trusted.external.basename 下,其中 basename 是脚本的名称。自 Puppet 6.17 和 7.0 起,还可以通过将 trusted_external_command 设置为包含多个脚本的目录来使用多个受信外部命令。这对于查询多个源非常有用。每个源将获得不同的基本名称。在外部数据提供者模式中,它用于查询 BSS。

Hiera 后端使用 Ruby 或 Puppet 编写的函数在执行 Hiera 查找时查询 API 或其他源。在外部数据提供者模式中,后端查询 BSS 以获取值。有关此内容的文档,请参见 puppet.com/docs/puppet/latest/hiera_custom_backends.html

Puppet 允许使用称为 termini 的可插拔后端,并使用间接器,如 第十章 中所讨论的,允许 Ruby 脚本访问端点上的键值对。事实 termini 是访问事实端点的 Ruby 脚本,允许将数据发送到其他外部系统。有关此内容的更多详细信息,请参见 puppet.com/docs/puppet/latest/indirection.htmlpuppet.com/docs/puppet/latest/man/facts.html

外部数据提供者实现

在撰写本文时,没有单一的外部数据提供者模式实现所有部分,但它们可以结合使用,以集成多个系统和用途。本节中的示例列表并不旨在穷尽所有内容,但应该能展示可以进行调查的集成范围。如果有必要,我们还将提供文档示例,后续可根据需要进行扩展。

Satellite

Red Hat Satellite 可以通过报告处理器接收来自 Puppet 服务器的报告,同时使用可在 forge.puppet.com/modules/puppetlabs/satellite_pe_tools 获取的模块。然而,也可以使用 puppetserver_foreman 模块来配置一个受信任的外部命令,以便从 Satellite 收集各种配置信息,例如智能参数和组织信息作为事实。由于 Puppet 被移除作为默认的配置管理选择,并改为作为可选插件使用,而且 Puppet 版本未能跟上 Satellite 平台的开发进展,参考 www.redhat.com/en/blog/upcoming-changes-puppet-functionality-red-hat-satellite,使用此受信任的外部命令可以将 Puppet 服务器的功能迁移为独立的 Puppet 基础设施,而配置则保存在 Satellite 的 Foreman 组件中。请参阅文件/Satellite,了解受信任的外部命令:github.com/theforeman/puppet-puppetserver_foreman

ServiceNow

已为 Puppet 开发了多个 ServiceNow 集成;CMDB 集成允许使用来自 forge.puppet.com/modules/puppetlabs/servicenow_cmdb_integration 的模块提供的受信任命令。

由于大量节点的使用可能会使 ServiceNow 因查询而不堪重负,因此 ServiceNow 应仅在较小规模的 BSS 中使用。它提供了一个使用受信任命令的有用示例。

已开发出一种更好的方法来确保扩展性,其中 ServiceNow 图连接器连接到 Puppet API 并收集必要的数据:store.servicenow.com/sn_appstore_store.do#!/store/application/42ae987a1b832c10fa34a8233a4bcb0b

Azure Key Vault

Azure Key Vault 的集成是一种调用 Puppet 代码中 azure_key_vault::secret 的功能。可以与 Hiera 后端一起使用来访问 Azure Key Vault 秘密。这是一个经过批准的模块(forge.puppet.com/modules/tragiccode/azure_key_vault)。

1Password

1Password 集成是一个 Hiera 后端,允许在 1Password 设置中进行密钥查找调用:forge.puppet.com/modules/bryxxit/onepassword_lookup

Vault

两种 Vault 解决方案(服务器端和客户端)已在 第九章 中讨论并演示,但为了回顾,服务器端 Vault 查找实现了一个名为 hiera_vault 的 Hiera 后端查找功能。正如在 forge.puppet.com/modules/petems/hiera_vault 中所讨论的,这允许通过 Hiera 调用 Vault 中的密钥并将其编译到代码中。

Puppet 数据服务

Puppet 数据服务 (PDS) 提供了外部数据提供者模式的最完整实现之一,唯一的不同是它实现了一个事实终端。PDS 包含以下组件:

  • 一个允许用户和应用程序交互的 REST API 和 CLI

  • 一个可插拔的后端数据库,用于提供 BSS(在编写时,仅支持 PostgreSQL)

  • 一个用于查询 BSS 的 Hiera 后端

  • 用于查询 BSS 的受信任外部命令

PDS 的设计重点不局限于某一特定的集成,而是允许 Puppet 平台团队利用外部数据提供者模式提供自助服务并减少操作负担。PDS 提供了一个安装模块(github.com/puppetlabs/puppetlabs-puppet_data_service)。构成应用程序和 API 的代码(github.com/puppetlabs/puppet-data-service)打包为 debrpm 格式,这些包由 install 模块使用。在编写时,这两个模块仅为 Puppet Enterprise 安装设计,但基础设置并没有将应用程序局限于 Puppet Enterprise,这意味着它可以适配到开源 Puppet。

Splunk

在本章的 日志记录和状态 部分,我们讨论并演示了如何通过 splunk_hec 模块利用相同的模块事实终端,将通信从 Puppet 服务器发送到 Splunk 服务器的 Splunk Hec URL(github.com/puppetlabs/puppetlabs-splunk_hec/blob/main/lib/puppet/indirector/facts/splunk_hec.rb)。

Puppet 运行的事实可以发送到 Splunk,然后可以通过 Splunk 应用程序中的库存和库存趋势查看这些数据(splunkbase.splunk.com/app/4413/)。

实验室 – 使用 Splunk 和 Puppet 数据服务的动手操作

在讨论了几个集成之后,如果你按照前面的实验步骤进行了 Splunk 安装或重新安装,你可以登录到 Splunk 并查看 inventoryinventory trend 标签。在这里,你将看到 Facter 终端的输出,并可以尝试查看节点中的数据。

在本部分实验中,你将看到如何使用 PDS 来分类节点、更新 Hiera 数据并添加受信事实。

要安装 PDS,你需要执行以下任务:

  1. 查看你克隆的控制仓库中的 hiera.yamlsite.pp 文件,了解 PDS 如何使用它们。

  2. 配置两个必需的应用程序角色。

  3. 对于数据库服务器,执行以下操作:

    1. 从 PE 控制台添加一个新的节点组:
    Parent name: PE Infrastructure
    Group name: PDS Database
    Environment: production
    
    1. puppet_data_service::database 类添加到你在前一步创建的 PDS 数据库组中。

    2. 在执行这些步骤之前,使用规则选项卡节点将现有主服务器添加到该组中。

    3. 提交你的更改。

  4. 对于 PDS API 服务器,执行以下操作:

    1. 选择 puppet_data_service::server 类。

    2. 包括 database_host: <FQDN of your primary server> 参数

    3. 选择 sensitive pds_token 参数。你可以使用 www.uuidgenerator.net/ 来生成一个 token。

    4. 提交你的更改。

  5. 在所有节点上运行 Puppet,直到报告显示没有更改。

  6. 在单独的终端窗口中创建一个到主服务器和一个节点的 SSH 会话。

  7. 在主服务器上,运行 pds-cli node upsert <fqdn_of_node> -c motd -e production 命令。

  8. 在节点上运行 puppet agent –t。在命令输出中,你将看到 motd 已应用默认设置。

  9. 在主服务器上,运行 pds-cli hiera upsert nodes/<fqdn_of_node> motd::content -v '"Hello world its PDS\n"'

  10. 在节点上运行 puppet agent –t。在命令的输出中,你将看到应用了我们设置的 Hiera 覆盖的 motd

  11. 在主服务器上,运行 pds-cli node upsert <fqdn_of_node> -c motd -d '{"status": "Testing"}' -e productionpds-cli hiera upsert nodes/<fqdn_of_name> motd::content -v '"Hello world, I am a PDS %{``trusted.external.pds.data.status} Server\n"'

  12. 在节点上运行 puppet agent –t。观察它应用了带有 Hiera 覆盖的新的 motd,并为受信事实设置了 testing 值。

  13. 登录到控制台,查看节点及其事实,检查是否设置了受信事实 pds.data.status 为 testing。

总结

在本章中,我们总结了各种日志位置,并向您展示了如何将日志转化为 JSON 格式并导出,以便它们能够在如 Elastic 或 Grafana 等日志工具集中处理,这些工具能够更好地对日志进行索引以便查看和分析。我们了解了如何在 Puppet Server 上使用报告处理器,以便通过在客户端应用目录来生成报告。这使得报告能够发送到像 Splunk 这样的工具,并支持高级可视化和搜索。我们还讨论了可用的状态 API,说明了如何通过 API 调用来查找所有正在运行的服务或某个特定服务的状态。展示了 Puppet Enterprise 具有命令行(Puppet Infrastructure status)和 Web 控制台选项来调用该 API。通过这些机制,您学习了如何访问关键日志和指标,以便了解系统的当前状态。

为了更好地利用这些信息并深入了解服务的性能,您学习了如何通过在状态 API 中使用debug标志来获取 Puppet 指标,以及如何使用 Puppet 操作仪表板和 Splunk 插件等工具来收集和可视化这些数据。Puppet Enterprise 具有 Metrics Collector 模块,它会将指标以 JSON 文件格式本地收集,您可以手动查看或导出这些文件。

为了更好地理解如何使用这些指标和仪表板,我们回顾了一些常见问题,讨论了如何为目录编译调整基础设施规模,避免像“雷鸣效应”这样的服务器需求过度集中的问题,并探讨了如何根据需求的增加或减少来调整 PuppetDB。展示了 PE 中各种基础设施调优工具作为优化已部署硬件设置的选项。

接下来,我们介绍了外部数据提供者模式,它提供了自助服务机制,使得 Puppet 数据能够在外部服务上访问,以便更好地集成。展示了一个后端存储服务的核心组件,用于存储能够处理 Puppet 查询的级别的数据,同时展示了受信任的外部命令和 Hiera 后端作为查询这些数据的方法。展示了 Fact 终端作为从 BSS 导出数据到外部服务的方式。

当使用不同的 Hiera 后端时,展示了这些组件的各种实现,其中 1Password、Azure Key Vault 和 Vault 被展示为访问外部秘密管理器的方式,而 Satellite 和 ServiceNow 则展示了可以通过受信任命令将这些应用程序中的数据输入到 Puppet 代码中的方式。

Puppet 数据服务被证明是该模式最完整的实现之一,并提供了一个稳固的设计,使得内部客户能够在不需要全面了解 Git 工作流和 Puppet 语言的情况下,访问适当暴露的 Puppet 选项,进而实现自助服务。

本节介绍了外部数据提供者模式,展示了如何通过 Puppet Enterprise 实现强大的集成,将数据输入和输出到不同的工具,并朝着将 Puppet 作为重要组成部分来构建平台的目标迈进。

在前一部分讲解了 Puppet Server 的组件、如何在大规模监控性能并进行集成后,下一章将讨论 Puppet Enterprise 特定的服务及其组件。它将描述什么是 Puppet Enterprise,它与开源版本有何不同,提供了哪些额外的服务,以及 Puppet 提供的参考架构,这些架构能更轻松地扩展和使用工具自动化部署及其状态。还将讨论专门针对 Puppet Enterprise 的项目和集成。

第四部分 – Puppet Enterprise 与 Puppet 采用方法

本部分将讨论 Puppet Enterprise 及其与开源版本的区别。我们将回顾一些可以扩展 Puppet Enterprise 的 Puppet 相关产品,以及一些 Puppet Enterprise 的具体集成。接着,我们将探讨有助于组织成功采用 Puppet 的方法。我们将探讨如何正确界定用例,从而从常规交付中获益,Puppet 如何在平台工程中发挥作用,以及如何在传统遗留环境和高度监管、变更管理的环境中使用 Puppet。

本部分包含以下章节:

  • 第十四章Puppet Enterprise 简介

  • 第十五章采用方法

第十四章:Puppet Enterprise 简要概述

本章将概述Puppet Enterprise,它是什么以及与开源 Puppet相比提供了哪些功能。尽管本书的作者是 Puppet 员工,但本章并非强行推销,而是介绍如何有效地使用 Puppet Enterprise。它将介绍 Puppet 平台中的额外企业控制台服务,展示代码部署、编排服务、RBAC、Web 控制台以及其他各种服务如何自动配置并协同工作。这将有助于理解 Puppet Enterprise 与开源 Puppet 的区别,以及在开源 Puppet 中需要手动创建的预配置和内建功能。将重点介绍支持的架构模式,帮助理解如何使用 Puppet Enterprise 包装和模块自动部署这些模式,从而部署和扩展 Puppet 基础设施。还将讨论一些相关项目和集成,以及它们如何融入 Puppet Enterprise 环境。

本章将涵盖以下主要内容:

  • 什么是 Puppet Enterprise?

  • 探索 Puppet Enterprise 控制台和服务

  • 在 Puppet Enterprise 中使用 Bolt

  • 自动化部署和参考架构

  • Puppet Enterprise 相关项目和工具

  • 实验—Puppet Enterprise 扩展与配置

技术要求

github.com/puppetlabs/control-repo 克隆控制仓库到你的 controlrepo-chapter14 GitHub 账户,并更新此仓库中的 Puppetfile 文件:github.com/PacktPublishing/Puppet-8-for-DevOps-Engineers/blob/main/ch14/Puppetfile

通过下载来自 github.com/PacktPublishing/Puppet-8-for-DevOps-Engineers/blob/main/ch14/params.jsonparams.json 文件,并根据控制仓库的位置以及控制仓库的 SSH 密钥进行更新,构建一个带有副本的集群,包含三个编译器和三个客户端。然后,从 pecdm 目录运行以下命令:

bolt --verbose plan run pecdm::provision –-params @params.json

什么是 Puppet Enterprise?

在讨论 Puppet Enterprise 时,常见的误解是该产品的某些功能被限制,无法为开源用户提供。Puppet Enterprise 的目标并非限制开源用户可用的功能,而是为那些希望轻松使用 Puppet 的客户提供价值,使他们能够通过减少自身在平台上的开发和自动化工作,专注于获得配置管理的价值。

Puppet 通过确保在 Enterprise 中,组件的打包与自动化安装脚本和模块一同进行版本控制和测试,从而减少用户管理基础设施所需的工作量。Puppet Enterprise 使用两种不同类型的版本发布。Puppet Enterprise 采用 xxxx.y 模式,通常每 3 个月更新一次,截至目前为 2023.0。该版本计划在 2023.3 升级到 Puppet 8.x 版本,并将在其生命周期内不断推出新功能。此版本推荐给那些希望访问最新功能和修复的用户,并且需要定期更新。另一种发布类型是长期支持LTS)版本;此版本遵循 xxxx.y.z 模式。该分支通常每 3 个月更新一次,但更新只包含修复而不包括新功能。LTS 版本的生命周期为 2 年,并与下一个主要的 xxxx 版本重叠 6 个月,因此当前的 2021.7.z LTS 将于 2024 年 8 月 31 日结束主流支持,届时将继续提供重叠支持,直到 2025 年 2 月 28 日,此后用户应迁移到所需版本的 Puppet。2023.y 将成为新的 LTS 发布版本,继续获得 Puppet 的支持。这两种运行中的 Puppet Enterprise 版本通常与两个处于积极开发中的开源 Puppet 版本相对应。2023.0 的发布终止了 Puppet 6,预计 2023 将在 2023.3 版本或稍后转向 Puppet 8。

企业许可证最显著的特点是支持,用户可以向团队提出支持案例,这些团队可以审查基础设施问题并协助解决任何问题或提供支持的模块所需的功能。

Puppet 还提供各种专业服务,如现场服务,提供实操培训和建议。这可能会引导进行架构审查,以了解如何在你的环境中最佳实施,并为产品和解决方案的开发流程提供反馈,如Puppet 数据服务PDS)和Puppet Enterprise 管理模块peadm)。此外,技术账户经理TAMs)将被分配为你提供定期的联系人,帮助你在 Puppet 中取得成功,支持你为你的组织制定成功计划,并专注于确保部署能够实现其目标。

Puppet 提供了 Puppet 产品的参考架构和模式,展示如何在不同的规模和实现类型下工作。构建在 Puppet Server 服务之上的附加应用程序允许进行访问控制、服务器分类、代码部署、可视化和数据搜索,所有操作都可以通过控制台以标准方式完成。我们将在接下来的部分中更详细地探讨这些内容。

探索 Puppet Enterprise 控制台和服务

Puppet Enterprise 主服务器中内置了若干附加服务,如下图所示:

图 14.1 – Puppet Enterprise 组件

图 14.1 – Puppet Enterprise 组件

Puppet 服务器

Puppet 服务与第十章中讨论的相同,证书颁发机构 (CA) 提供证书签名过程以保证通信安全,Puppet 代理联系编译器的 Puppet 服务器服务请求目录编译。 Facter 用于提供服务器配置文件。在 图 14**.1 中,我们选择不显示主服务器本身有一个 Puppet 服务器服务,并且编译器和主服务器都有 Puppet 代理,它们都从主服务器的 Puppet 服务器请求目录编译。

引入 Puppet Web 控制台组件

Puppet Enterprise 服务器最明显的直接差异是提供我们在本书实验室中使用的登录视图的 Web 控制台。几个服务结合在一起形成控制台服务

控制台是一个基于 Jetty 的 Clojure Web 前端服务,具有充当反向代理的 NGINX 服务器。 NGINX 服务器在 HTTPS 端口 443 上监听并将 HTTP 80 重定向到 HTTPS。控制台 UI 提供聚合和翻译 Jetty-based Clojure 服务以生成正确的页面并访问其他控制台服务。

认证 UI 生成登录和重置密码内容页面。展示这一点的最简单方式是使用登录时所需的通信示例,如下图所示:

图 14.2 – 生成登录页面的步骤

图 14.2 – 生成登录页面的步骤

可以从图表中看出,作为第一步,NGINX 服务器接收 GET 请求,并在执行 TLS 协商后重定向到控制台 Jetty 页面。该页面评估 cookie,确认用户未登录,并重定向到 auth/login 页面,该页面从 NGINX 请求并重定向到认证 UI。认证 UI 生成登录页面,并从 RBAC Jetty 页面获取 安全断言标记语言 (SAML) 配置,然后将此登录页面传回用户。

RBAC 服务 具有用户和角色来构建访问策略。它允许在 Puppet Enterprise 中使用本地和远程用户,并可以集成到 轻量目录访问协议 (LDAP) 和 SAML 服务中。所有用户默认被拒绝创建、编辑或查看 Puppet Enterprise 的任何部分的权限,然后通过角色授予权限。

默认情况下,将有一个本地管理员用户作为 Puppet 服务的超级用户以及用于 Puppet 服务内部通信的 API 用户进行身份验证。它不能用于登录,只能通过证书身份验证进行身份验证。有一个允许列表,其中包含可以与 API 用户一起使用的证书的 certname 值。

角色允许将权限分组,以便授予用户执行操作的权限。权限由类型权限对象组成。类型是权限将允许在其上执行操作的对象,例如用户或节点组。权限是创建、编辑或查看的访问级别,而对象是类型的特定实例,例如节点组类型中的 Puppet Enterprise 基础设施节点组。

默认提供五个角色:

  • 管理员:所有权限

  • 操作员:创建和修改节点组、部署代码、运行 Puppet、签署证书并查看控制台的权限

  • 查看者:查看控制台、节点组和作业的权限

  • 代码部署者:使用代码管理器部署代码的权限

  • 项目部署者:部署项目、从项目中运行任务和计划,并在调度器中启动、停止和查看作业的权限

可以通过参考puppet.com/docs/pe/2021.7/rbac_permissions_intro.html#user_permissions来创建自定义角色,以找到正确的用户权限粒度。

LDAP 解决方案,如Active DirectoryAD)可以将用户组映射到角色,而SAML 解决方案,如Okta,可以通过属性对用户组进行类似的映射,以匹配角色。LDAP 和 SAML 都可以在 Web 控制台的访问控制选项卡中配置,通过选择相应的选项来进行映射。

令牌用于所有 Web 会话,且在运行命令时不需要每次都用密码登录,而是可以生成多个用途的令牌。这些令牌是介于02²⁵⁶ – 1之间的字母数字值,存储在数据库中,并根据参数存储在本地文件位置。令牌通过令牌 API 端点、puppet access login命令的 Web 控制台或 CLI 生成。令牌是基于提供的用户凭据生成的,因此将具有该用户设置的权限。默认情况下,puppet access login命令会将令牌写入~/.puppetlabs/token,其生命周期为 30 分钟,除非使用--lifetime选项设置例如5h(5 小时)的生命周期。--print标志会使令牌仅被打印而不存储,这适用于基于服务的 API 访问。

第十一章中讨论了分类服务,我们探讨了它如何使用节点组在 Puppet 中对服务器进行分类,但为了重申关键点,节点组用于通过使用事实或直接固定命名的服务器来将类分类到服务器。节点组是基于继承的,因此节点组的每个子节点都会继承其上方的所有内容。

代码管理器在第十一章中进行了讨论,展示了如何使用r10k从命名的 Git 存储库中的控制仓库下载基于 Puppet 文件的模块,然后通过filesync服务器和filesync客户端保持该代码副本在各个服务之间同步。

活动服务用于记录通过控制台服务发生的所有活动,可以通过 API 端点以及在 Web 控制台的多个位置查看,例如任何用户和角色的活动标签。

Puppet Enterprise 的数据库组件,PuppetDBPostgreSQL,与第十章中讨论的一样,但几个服务需要额外的数据库来存储它们的状态和记录。因此,创建了以下数据库:

  • pe-activity:控制台服务的所有可审计活动

  • pe-classifier:所有节点组信息

  • pe-inventory:无代理客户端详细信息及其访问方式,用于编排器

  • pe-orchestrator:作业运行、作业结果、用户和节点

  • pe-postgres:用于模板和一般访问的 Postgres 数据库。请参阅www.postgresql.org/docs/current/manage-ag-templatedbs.html以进一步了解模板数据库。

  • pe-puppetdb:报告、节点信息和上次运行的清单

  • pe-rbac:用户、角色、组以及 AD/LDAP 信息

注意

所有 PostgreSQL 通信均使用证书进行,包括与副本的通信。

图 14**.1中未涉及的一个组件是编排器服务。现在我们将介绍编排器如何提供在 Puppet Enterprise 中使用 Bolt 计划和任务的能力。

使用 Bolt 与 Puppet Enterprise

第十二章中,介绍了如何在 Bolt 项目中使用bolt二进制文件运行 Bolt,但它可以通过编排器服务与 Puppet Enterprise 集成,允许将计划和任务作为 Puppet Enterprise 的一部分运行。

主要区别在于,目前只能部署包含任务和计划的 Puppet 模块(包括将它们添加到控制仓库);目前没有直接将 bolt 项目部署到 Puppet Enterprise 的方法。

注意

通过模块部署的计划和任务意味着相同的计划或任务可以有多个版本,具体取决于运行环境。

还需要注意,并非 Bolt 原生提供的所有功能都能在 Puppet Enterprise 中使用。

以下列表突出显示了在编排器中运行 Puppet Enterprise 的计划和任务与在原生 Bolt 中运行时的主要区别:

  • 用于计划的各种 Bolt 功能,如promptparallelizefile.upload,尚未实现

  • puppet apply模块只能应用于具有 Puppet 代理的节点

  • 目标和本地主机目标不可用

  • 文件源必须基于模块,不能是绝对路径

这些大部分限制反映了没有从本地机器运行 Bolt,以及缺少运行它们的提示。完整的详情可以在 Puppet 的文档中查看:puppet.com/docs/pe/2021.7/plans_limitations.html

Puppet Enterprise 处理三种类型的节点,包含计划和任务:

  • 安装了 Puppet 代理的节点,使用Puppet 通信协议PCP)和PCP 执行协议PXP

  • 通过Windows 远程管理WinRM)和安全外壳SSH)传输的无代理节点

  • 通过如 F5 和Palo Alto Networks 操作系统PAN-OS)等传输或通过资源 API 提供的传输连接的无代理设备,如交换机或防火墙

在介绍了编排器可以运行的计划和任务之后,我们现在来看一下构成编排器的组件,重点介绍这些服务的目的和关键细节,如日志位置和配置文件。

编排器服务

编排器应用是一个由下图所示服务组成的Clojure应用:

图 14.3 – Puppet 编排器服务的组成部分

图 14.3 – Puppet 编排器服务的组成部分

让我们概览一下这些组件及其相关的服务和日志文件:

  • pe-orchestration-services.service并记录到/var/log/puppetlabs/orchestration-services/orchestration-services.log

  • POST /command/create-connection 清单 API 调用(puppet.com/docs/pe/2021.7/node-inventory-v1-command-endpoints.html#node-inventory-v1-command-endpoints)。这些条目通过一个密钥加密,默认情况下存放在/etc/puppetlabs/orchestration-services/conf.d/secrets/keys.json,尽管这些条目是单独列出的,但清单服务运行在pe-orchestration-services内。它将数据存储在 PostgreSQL 清单数据库中。

注意

添加到清单中的无代理节点会计入 Puppet Enterprise 的整体授权节点数。

  • pe-bolt-server.service并记录到/var/log/puppetlabs/bolt-server/bolt-server.log

  • pe-ace-server.service并记录到/var/log/puppetlabs/ace-server/ace-server.log

  • /var/log/puppetlabs/orchestration-services/pcp-broker-access.log和一般服务日志记录到/var/log/puppetlabs/orchestration-services/pcp-broker.log

  • pxp-agent.service并记录到/var/log/puppetlabs/pxp-agent/pxp-agent.log

orchestrator 服务将通过 RBAC 验证 Puppet Enterprise 控制台用户是否拥有正确的权限。对于计划,只能指定用户或组以及他们可以运行的计划,不限制节点或计划来源的环境。对于任务,任务目标允许指定一组任务和一个 Puppet 查询语言 (PQL) 查询节点或节点组,任务可以针对这些节点执行。这可以通过 API 调用完成,如 www.puppet.com/docs/pe/2021.7/orchestrator_api_commands_endpoint.html#orchestrator_api_post_command_task_target 所示,或者通过 RBAC 图形用户界面,如下图所示:

图 14.4 – 在 web 控制台上创建任务目标

图 14.4 – 在 web 控制台上创建任务目标

在下一节中,我们将学习如何通过 orchestrator 运行任务、计划或 Puppet 运行。

运行作业

当通过 orchestrator 运行任务、计划或 Puppet 运行时,它们被称为 作业。有三种方式运行作业,如下所示:

  • 第一种方法是通过 GUI 选择左侧栏中的相关菜单。

  • 第二种方法是通过主服务器上的 CLI,语法与 puppet task runpuppet plan run Bolt 命令基本相同。与 Bolt 的关键区别在于,--nodes 标志用于代替 targets(反映了您只提供节点名称,而 orchestrator 将查找传输信息),并且提供了额外的标志,如 --node-groups 标志,用于选择要运行的节点组。以下是一个示例:

    puppet task run examplemodule::exampletask paramter1=value1 paramter2=value2 --node-group <node group id>
    puppet plan examplemodule::exampleplan parameter1=value1  --nodes examplehost.com,examplehost2.com
    puppet job run --query 'inventory { facts.os.name = "windows" }'
    
  • 第三种方法是通过 puppet.com/docs/pe/2021.7/orchestrator_api_commands_endpoint.html 中文档化的 API,以下是一些关键调用:

    • POST /command/deploy: 按需运行 Puppet

    • POST /command/plan_run: 运行计划

    • POST /command/task: 在一组节点上运行任务

正在进行的作业可以通过按下 Ctrl + C 在 CLI 中停止,或选择 POST /command/stop API 命令。虽然我们应该小心,注意停止的作业底层进程可能会继续运行直到完成。

在 PE 2021.7.1 中引入了一个 API 命令 POST /command/stop_plan,允许停止计划。

还可以通过 GUI 或 API POST /scheduled_jobs/environment_jobs 在 orchestrator 中调度作业,但需要特别小心,注意使用调度器时的系统负载。由于 orchestrator 无法水平扩展,且任务和计划的队列系统可能会被某些类型的请求轻易阻塞,因此需要特别注意其扩展性限制。

配置性能设置

本节中讨论的设置都可以在 Puppet Enterprise orchestrator 基础设施节点组中配置,通过 Web 控制台或以代码形式在 Hiera 中进行配置。

orchestrator 可以同时运行最多数量的任务;这个最大并发任务数通过 puppet_enterprise::profile::orchestrator::task_concurrency 参数配置(默认值:250),以及 puppet_enterprise::profile::bolt_server::concurrency(默认值:100)和 puppet_enterprise::profile::ace_server::concurrency(默认值:100)一起配置,直接限制 Ace 和 Bolt(它们的数量不应大于 orchestrator::task_concurrency 总数)。它们的大小主要受到 orchestrator 内存的限制,orchestrator 会为你添加的每个实例容量保留大约 ± 1 MB 的 RAM。任务会按照接收到的顺序处理,直到完成;这意味着长时间运行的任务和目标数较多的任务可能会阻塞其他任务的运行并垄断资源。以运行任务需要 10 分钟来完成 1,000 个服务器为例,这将导致任务使用 250 的队列容量四次,执行任务的总时间为 40 分钟,在此期间,所有其他任务需要排队直到完成。强烈建议任务的执行时间不超过 5 分钟,并且需要精心管理任务,分批运行。同时需要注意,任务队列没有限制,并且有可能触及 puppet_enterprise::profile::orchestrator::allowed_pcp_status_requests 参数。理解这一点很重要,这并不意味着任务失败,而是 orchestrator 无法在超时时间内获取任务状态。任务本身可能在此之后已完成。

对于计划,orchestrator 与 Puppet Server 类似,需要 JRuby 实例来编译计划。这个容量由 puppet_enterprise::profile::orchestrator::jruby_max_active_instances 设置,JVM 的堆内存通过 puppet_enterprise::profile::orchestrator::java_args 设置。

在讨论了 Puppet Enterprise 的核心组件和服务后,我们将进一步探讨如何使用自动化工具部署这些组件,并部署到 Puppet 推荐的参考架构上,以确保基础设施能够根据用户需求进行扩展。

自动化部署与参考架构

Puppet Enterprise 专注于创建标准架构和配置以及自动化部署它们。这确保了 Puppet Enterprise 客户能够找到合适的标准架构和模式,并使用提供的工具进行部署,从而减少了设计工作的量。

理解支持的架构

Puppet 为 Puppet Enterprise 提供了三种支持的架构,如下所示:

  • 标准安装仅为一个独立的主服务器,并支持最多 2,500 个客户端。

  • 大型安装是一个主服务器,后面有编译服务器,通过负载均衡器连接,支持最多 20,000 个客户端。

  • 超大型安装包括一个主服务器,一个单独的 PuppetDB 服务器,以及通过负载均衡器连接的编译服务器,支持超过 20,000 个服务器。

以下是通过图示说明:

图 14.5 – 标准架构

图 14.5 – 标准架构

标准架构受到主服务器能为多少客户端运行目录的限制,最多支持 2,500 个节点。超过此数量后,大型架构允许通过编译节点进行横向扩展,但也面临单个主服务器无法承载所有服务负载的限制。因此,在 25,000 个节点时,超大型架构建议将 PuppetDB 作为最重的服务之一分离到单独的服务器上。

在所有这些架构中,可以通过名为灾难恢复DR)的方法,为主服务器和单独的 PostgreSQL 服务器提供副本服务器。如果主服务器或 PostgreSQL 服务器发生故障,灾难恢复可以执行故障转移操作并恢复服务,预期会丢失部分服务,以下是服务的详细表格:

服务名称 复制类型 故障转移方法
Puppet 服务器 主动 / 主动
控制台服务 UI 直到手动提升前为只读
ACE 服务 直到手动提升前为只读
Bolt 服务 直到手动提升前为只读
CA 单向复制 直到手动提升前为只读
RBAC 单向复制 直到手动提升前为只读
分类器 单向复制 直到手动提升前为只读
活动 单向复制 直到手动提升前为只读
编排 单向复制 直到手动提升前为只读
文件同步 单向复制 直到手动提升前为只读
PuppetDB 双向 主动 – 主动

表 14.1 – 服务复制和灾难恢复方法

PuppetDB 在 Puppet Enterprise 中的同步机制是独特的;它在主服务器和副本之间执行读写同步,这也是它在前述列表中唯一可以同步并在提升时可用的服务。其他使用 PostgreSQL 的服务依赖于从主服务器到副本的PGLogical同步,使得副本上的数据为只读。

从这个列表可以看出,在主服务器故障时,副本将只能接管并编译已经注册的服务器的目录、来自 PuppetDB 的查询和报告,以及通过 API 进行的节点分类查询。这意味着无法注册或删除新服务器,无法部署新代码,无法使用网页控制台,无法更改分类,并且大部分 CLI 工具将无法使用,直到通过在副本上执行 puppet infrastructure promote replica 命令进行手动提升操作。

这是一个不可逆的操作,原始的故障主服务器必须重新部署为副本才能再次使用。因此,对于许多试图修复原始主服务器的用户来说,这比通过 DR 过程更省时。

DR 不应与 高可用性 (HA) 混淆,后者是在服务器丢失的情况下预期的连续服务,而这在当前任何 Puppet 架构中都是不可能的。

注意

使用 DR 时,peadm 确保编译器被拆分并配置为两组,PuppetDB 请求在 PuppetDB 副本的两边分发,以最大化容量。如果选择不使用 pecdm,请确保遵循此优化,可在 github.com/puppetlabs/puppetlabs-peadm/blob/main/manifests/setup/node_manager.pp 中查看代码,A 和 B 组设置数据库的参数。

Puppet 架构还定义了一套跨公共云和私有云区域部署的多区域模式,其中区域由云服务提供商定义为具有区域性低延迟连接的数据中心。完整的细节请参见 puppet.com/docs/patterns-and-tactics/latest/reference-architectures/pe-multi-region-reference-architectures.html。最佳实践要求编译器具有低延迟连接,因此最好将它们部署在与主服务器和副本服务器相同的区域;同样,主服务器和副本之间的连接必须是低延迟的。因此,最佳做法是使用集中式部署,其中所有 Puppet 基础设施都位于一个所有区域都可以通信的管理区域,如下图所示:

图 14.6 – 集中式和联合式部署

图 14.6 – 集中式和联合式部署

另外,也可以使用联合模型,在每个区域部署 Puppet 基础设施,缺点是没有一个控制台能够查看整个系统。

讨论了架构和模式之后,接下来是查看可用于部署这些模式的工具。

部署与配置

Puppet 通过多个层次自动化其服务器基础设施的部署。第一层使用 Puppet Enterprise 安装程序,这是一个从 Puppet 下载的 tarball 文件,其中包含所有必要的软件包和脚本来安装 Puppet Enterprise。下载到目标服务器并解压后,可以通过运行./puppet-enterprise-installer来完成基本安装。可以通过创建-c标志并指向其位置,按照puppet.com/docs/pe/2021.7/installing_pe.html上的指导添加自定义配置。一旦 Puppet 服务器配置完成,就可以使用安装脚本自动添加代理;Unix 系统使用 Bash 脚本,Windows 使用 PowerShell 脚本,这些脚本托管在主文件服务器上,确保正确的代理包被安装:

uri='https://<PRIMARY_HOST>:8140/packages/current/install.bash' curl --insecure "$uri" | sudo bash -s -- --puppet-service-ensure stopped agent:environment=production
[Net.ServicePointManager]::ServerCertificateValidationCallback = {$true}; $webClient = New-Object System.Net.WebClient; $webClient.DownloadFile('https://<PRIMARY_HOST>:8140/packages/current/install.ps1', 'install.ps1'); .\install.ps1 -PuppetServiceEnsure stopped agent:environment=production

在这个示例中,选项将environment设置为production,并确保服务未运行。完整的选项范围可用并记录在puppet.com/docs/pe/2021.7/installing_agents.html

这个install脚本仅安装了主服务器,且需要进一步的手动步骤才能根据我们想要的架构添加编译器和副本。为了部署下一个层级,除了直接使用 Enterprise 安装程序外,我们使用peadm模块(forge.puppet.com/modules/puppetlabs/peadm),这是一个受支持的 Puppet 模块,提供了一种自动化方式来运行 Puppet Enterprise 安装脚本,并自动将其配置为支持的架构之一。此模块假设所请求的配置所需的基础设施已准备好,并且可以进一步提升,使用pecdm模块(github.com/puppetlabs/puppetlabs-pecdm)在公共云环境中自动进行配置。关于这些模块的使用示例已在第十二章中详细讨论,并且是我们在本书中部署实验室时一直使用的方法。

peadm模块本身不仅仅是一个简单的部署工具,它还具有显示服务器状态的计划和任务,并允许通过其任务和计划执行版本升级。

Puppet Enterprise 将安装在Enterprise文件夹中的模块与通过分类器或 Hiera 数据配置的模块结合,并与其他文件位置一起放置自定义配置。控制台有许多可以配置的项,这些项可以通过 Web 控制台中的分类或通过 Hiera 设置,例如,通过puppet_enterprise::profile::console::rbac_failed_attempts_lockout设置的失败登录尝试次数和通过puppet_enterprise::profile::console::password_minimum_length设置的密码复杂性规则,如最小密码长度。可以在puppet.com/docs/pe/2021.7/config_console.html#configure_the_pe_console_and_console_services中找到完整的控制台自定义配置列表。

此外,可以通过将文件放置在puppet_enterprise::profile::console::disclaimer_content_path指定的路径中,来为控制台放置文件,默认路径为/etc/puppetlabs/console-services。你可以创建一个登录控制台时显示的消息,例如贵组织可能需要的法律警告。

此外,在控制台中,还可以基于 PQL 搜索节点,并可以选择预定义的 PQL 示例。你可以通过简单地将文件放置在/etc/puppetlabs/console-services/custom_pql_queries.json来将自己的 PQL 示例添加到 Web 控制台,使用/etc/puppetlabs/console-services/custom_pql_queries.json.example作为模板。Web 控制台本身默认使用自签名 CA 证书,可以通过将生成的证书放置在/etc/puppetlabs/puppet/ssl/certs/console-cert.pem/etc/puppetlabs/puppet/ssl/private_keys/console-cert.pem来替换为由贵组织 CA 系统签名的证书。另一个需要考虑的关键文件是许可证密钥,该密钥由 Puppet 颁发,并放置在/etc/puppetlabs/license.key,权限为644 root:root。你可以在 Web 控制台的License选项卡下查看许可证的详细信息。对于这些更改,应该执行一次 Puppet 代理运行,并重启控制台服务。

Puppet Enterprise 的某些领域当前无法通过原生代码进行定义,如 RBAC、分类和 LDAP,但有些 API 和 Puppet 模块可以利用这些 API,从而支持配置存储。对于分类,有一个 API 可以查看分类并配置节点组;这也可以通过node_manager模块实现(forge.puppet.com/modules/WhatsARanjit/node_manager),该模块由 peadm 使用。对于 RBAC 和 LDAP,RBAC API(puppet.com/docs/pe/2021.7/rbac-api.htm)提供了可用于管理组、角色和用户的端点。已开发了一个 Puppet 模块来使用这些 API(forge.puppet.com/modules/pltraining/rbac),并且它有一个 LDAP 端点,同样开发了一个模块来使用这些 API(forge.puppet.com/modules/abuxton/puppet_ds)。

)

在审查完架构和部署建议之后,我们将在下一部分讨论与 Puppet 一起使用的其他支持工具和产品。

Puppet Enterprise 相关项目和工具

Puppet Enterprise 拥有由 Puppet 开发的多个模块和工具,以简化 Puppet 基础设施的管理和支持。最直接的工具是内置的支持脚本;该命令会收集日志和系统信息并进行压缩,从而允许用户将详细的状态信息发送到 Puppet 支持团队的案例中。该命令的简单版本如下:/opt/puppetlabs/bin/puppet enterprise support

详细的选项可以在文档中找到:puppet.com/docs/pe/2021.7/getting_support_for_pe.html#pe_support_script,这些选项允许选择要收集的服务,将归档文件作为命令的一部分直接安全文件传输协议(SFTP)上传,并在有GNU 隐私保护(GPG)密钥的情况下加密归档文件。

注意

可以使用SOScleaner从支持脚本内容中删除主机名和 IP 地址。有关如何安装和运行它的详细信息,请访问 support.puppet.com/hc/en-us/articles/115003312887

在了解了如何部署 Puppet 基础设施之后,了解如何监控和排除发现的任何问题也很重要,因此我们接下来将讨论这个内容。

监控和故障排除 Puppet Enterprise 基础设施

Puppet Enterprise 状态检查模块 (forge.puppet.com/modules/puppetlabs/pe_status_check) 会根据支持案例中常见的问题,检查 Puppet 基础设施服务器和 Puppet 代理的状态,例如确认服务是否正在运行、磁盘空间是否充足以及证书是否即将过期。这些检查可以作为任务运行,Puppet 代码会将问题通知到报告中,或者作为事实——在 第十三章 中显示的 Splunk 插件有一个仪表板用于显示事实输出。使用这些检查意味着如果您在向 Puppet 提交支持案例时遇到问题,您可以引用检查编号。

support_tasks 模块 (forge.puppet.com/modules/puppetlabs/support_tasks/tasks) 提供了执行知识库文章中列出的操作的任务,例如重新生成证书、运行支持脚本和打印 Puppet 数据库表大小。

可以配置一些额外的控制台视图,使其在控制台中可见并可用;值报告只需在 值报告 标签页中输入使用任务、计划、纠正性更改和有意更改时可以回收的时间,系统还会生成统计数据。

Puppet Enterprise 可以收集有关软件包的更多信息,包括未管理的软件包;这些信息会在 puppet_enterprise::profile::agent 类中显示,供您希望从中收集信息的节点组使用,并通过将 package_inventory_enabled 参数设置为 true 来启用此功能。

最终可以启用的额外功能是补丁管理和监控,在 pe_patch 类中进行。

除了核心的 Puppet Enterprise 基础设施,还有其他 Puppet 产品可以管理代码部署管道,将代码部署到 Puppet Enterprise 并在 Puppet 节点上执行合规扫描。

管理部署并确保合规性

有两个额外的 Puppet 产品可以与 Puppet Enterprise 一起使用,v4 目录 API 用于编译新代码的目录,并将其与当前代码的目录进行比较,显示差异,以确保影响符合开发人员预期。这些管道可以在 CD4PE 的 Web 控制台中创建,也可以作为代码通过 YAML 文件插入到模块和控制库中进行部署。

Puppet Comply 是一个基于互联网安全中心CIS)基准的合规性工具。它围绕 CIS 开发的 Java 扫描器、CIS-CAT Pro 访问器(www.cisecurity.org/cybersecurity-tools/cis-cat-pro)构建自动化。这使得可以通过 Puppet Enterprise 中的调度器自动访问主机,并通过任务自动化和安排扫描器运行,从而生成其合规性的仪表板,并在单独的 Puppet Comply 控制台中查看。以下截图展示了 Comply 的主页屏幕:

图 14.7 – Puppet Comply 首页仪表板

图 14.7 – Puppet Comply 首页仪表板

从仪表板可以看到有多少节点已达到合规性,多少节点设置了合规性配置文件,以及节点结果的列表,列出了在特定扫描中分配的配置文件和合规性得分。

它还提供了高级版的cem_linuxforge.puppet.com/modules/puppetlabs/cem_linux)和cem_windowsforge.puppet.com/modules/puppetlabs/cem_windows),以加快 Puppet 的采用,允许基于 CIS 基准通过预制的 Puppet 模块进行基础安全配置。这些模块由 Puppet 维护和支持,确保执行代码与最新的 CIS 基准保持一致。

这两个产品运行在被称为Puppet 应用管理器PAM)的框架中,这是一个基于 Kubernetes 的工具,用于管理 Puppet 应用程序。

实验室 – Puppet Enterprise 扩展和配置

技术要求部分执行 bolt 命令,将部署一个大型的 Puppet Enterprise 2021.5。通过这个基础设施设置,我们将尝试进行我们已讨论的各种扩展和配置,具体如下:

  1. 检查 peadm 中的代码和设置 A 和 B 组的节点组。请注意,github.com/puppetlabs/puppetlabs-peadm/blob/main/documentation/classification.md 提供了组的解释。

  2. 创建一个个人用户,授予其查看控制台、创建节点组以及查看管理员用户活动日志的权限(尝试在没有查看控制台权限的情况下登录)。

  3. 启用所有节点在 Web 控制台上的包管理功能,作为个人用户登录,并查看此操作的活动日志。

  4. 通过使用node_manager模块应用代码,为节点启用补丁管理。

  5. 自定义登录信息。

  6. 使用 peadm 升级计划执行升级至 2021.6 版本。注意:由于 pecdm 包含 peadm,因此可以从开发环境中执行此操作。

示例解决方案可在 github.com/PacktPublishing/Puppet-8-for-DevOps-Engineers/tree/main/ch14 获取。

总结

本章回顾了 Puppet Enterprise 如何在开源工具的基础上构建,提供了确保安全和自动化部署 Puppet 所需的服务。讨论了 Puppet Enterprise 如何将开源软件包打包成一致的版本,并通过 Puppet 架构和服务团队提供支持服务。

我们还讨论了 Puppet Enterprise 的附加服务,通过 RBAC 安全地保护用户和 API 访问,提供了 Web 前端和附加 API,在控制台服务中可以通过 Code Manager 部署代码。

然后介绍了 Puppet Orchestrator,展示了如何在 Puppet Enterprise 中通过 Orchestrator 服务运行任务和计划,使用 PCP 通过 PXP 中介传输 PXP 代理与节点之间的通信。无代理客户端可以被添加到库存服务中,存储它们的传输详情,任务或计划将在 Bolt 服务器上运行,适用于通过 WinRM 或 SSH 连接的节点,而其他网络设备(如交换机或防火墙)则使用 ACE 服务器。我们看到 Orchestrator 如何存储所有作业的细节,并更新活动服务。讨论了 RBAC 访问控制,展示了如何限制哪些计划可供用户使用,同时可以将任务分配给特定用户和特定节点组,使用目标集。还讨论了 Orchestrator 的性能和容量方面的问题,以及如何通过 Web 控制台 GUI 或 CLI 接口运行任务或计划。

回顾了客户可以立即使用的支持架构,以便大规模实现 Puppet 以及满足其区域需求,展示了这些架构的模块和脚本,这些模块和脚本封装起来可以自动部署这些架构,pecdm 模块用于在公共云中部署基础设施,peadm 用于自动化安装和维护的各个步骤,并使用安装脚本。

介绍了可以在 Web 控制台中启用的附加服务,帮助报告 Puppet 提供的价值、补丁管理和打包报告,以及定制化和自动化配置控制台的方法,包括控制台消息的定制、Web 控制台上使用的证书和许可证密钥。随后讨论了几个模块,它们可以帮助报告基础设施状态,并在 support_taskstatus_check 模块中运行标准任务。

随后讨论了两个与 Puppet Enterprise 集成的 Puppet 产品:CD4PE,它提供了一个管道帮助自动化部署代码,以及 Puppet Comply,它提供了预写的模块和仪表板,用于报告 CIS 基准。

虽然所有的架构、工具、打包和一般自动化都可以通过开源 Puppet 实现,但这需要依靠你们团队的开发和支持工作。因此,Puppet Enterprise 应该被视为一个关于团队可用技能、组织已经投资的工具以及可用于工具的资金的决策,同时也考虑到你们组织希望集中精力在哪些工作上的问题。

现在,在全面审查了语言、平台以及 Puppet Enterprise 如何提供预配置的基础设施以减少操作负担和设计要求后,在最后一章中,我们将讨论采用和使用 Puppet 的方法,重点是如何在你的组织中获得最佳应用,因为理解技术只是战斗的一部分,而理解如何与人和流程整合通常是更大的挑战。

第十五章:采用方法

在详细讨论了 Puppet 语言和平台之后,本章将关注采用和实施方法。本章假设 Puppet 的最可能采用者及其观点。因此,这些建议从 Puppet 平台团队的角度出发,但它也会讨论如何让从应用程序到操作系统的所有实施团队共同合作,推动采用。

很多时候,项目或现代化计划的观点是认为仅靠技术就能解决组织的所有问题,而现有的团队和流程只是障碍,必须绕过它们才能交付未来。最成功的采用案例与现有团队合作,并将其嵌入到他们的流程中。本章将通过讨论如何选择正确的范围和焦点来确保实施能够实现其目标,定期交付并展示价值以促进采用,从而覆盖这一点。我们将讨论如何与其他团队和利益相关者合作,确保 Puppet 作为一项技术不是一个争夺空间的孤岛,而是一个可以集成并最大化效益的平台。虽然从头开始部署新服务器通常更为实际,但我们将讨论如何安全、渐进地接触遗产的老旧环境,在这里了解配置漂移的程度并开发自动化来纠正这些漂移,可以在减少昂贵的审计过程方面带来巨大的好处。我们还将详细讨论在受监管环境中使用 Puppet,因为通常认为一个经常进行更改并拥有较高权限的工具根本无法使用。我们将看到如何展示流程和测试,不仅使 Puppet 安全可靠,还能证明它是强制执行任何受监管环境要求的核心部分。最后,我们将看到 Puppet 如何融入云端、其适当的使用场景,以及如何避免公共云迁移中的错误,并不丢失 Puppet 在私有数据中心带来的效益。

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

  • 范围和焦点

  • 平台工程方法

  • 管理遗产遗产的无操作模式

  • 受监管环境中的采用

  • 向云迁移

范围和焦点

范围和关注度的压力将取决于你的组织为什么开始使用 Puppet。如果这是一个单独团队的自动化部署练习,比如 Oracle 团队,压力将比一个购买了大规模 Puppet Enterprise 合同的转型项目小。在大型转型项目中,可能会有诱惑去快速实现大目标,以便尽快回收成本。这是危险的,因为配置问题是复杂的,技术人员往往过于乐观地认为解决方案能很快创建出来。额外的压力可能来自销售团队和决策者,他们可能夸大了变革能多快实施,从而获得必要的资金。这并不是反对有愿景或类似吉姆·柯林斯(Jim Collins)那样的宏大目标。未来的愿景是需要的,但必须表明这将是一个渐进的改善过程,而这些渐进的步骤将带来即时且持续的价值。这将建立来自支持团队和客户的信任和信心,促使他们投资你的平台,因为你可靠地交付的是一些切实可行的成果,而不仅仅是一个遥远的希望。

最佳的交付方法是遵循良好的迭代实践,设定一些大的目标(epics),例如交付核心操作系统角色或 Oracle 角色,这些目标可以拆分成每个迭代的小目标,每个任务应该足够小,能够在正常的迭代周期内完成,通常是两周。每次迭代结束时,这些功能可以展示给利益相关者,以展示进展、收益并获得反馈。

注意

本书并不主张任何特定的敏捷方法论;有大量的书籍和建议教你如何实施敏捷工作实践。适合你组织的做法将取决于当地文化和你的团队。因此,本书的建议是研究各种方法,但要保持灵活,找到适合自己团队且有效的做法,而不是盲目模仿他人的系统。使用如迭代回顾(retrospectives)等技巧,能够确保你的工作方式仍然有效,并且对问题采取相应的行动。

如果忽视这种方法,团队被分散在多个目标之间,就容易导致开发人员各自为战。当开发人员孤立工作时,其他团队成员无法提供帮助或进行有意义的评审,因为他们对工作内容或决策原因没有了解。

如果工作过于庞大和复杂,就会导致开发问题,难以测试并拆解理解。这可能导致管理层和开发人员之间的沮丧,因为交付上没有任何可见的进展。为了交付某些东西的压力可能导致开发人员的疲惫,加上审查和测试大型复杂工作中的困难,可能会导致交付一个有风险或不完整的产物,仅仅是为了交付。这会在一个恶性循环中侵蚀信心和士气,压力和待解决问题不断增加。

受管控环境中的采纳 部分,我们将讨论在你的流程和平台中展示可靠的测试和交付能力,以获得变更和风险团队的信任有多么关键。

经过上述警告,如果团队确实坚持专注和范围的界定,那么团队之间就可以建立对持续开发工作的理解,并强化审查、测试和学习过程。这为开发人员提供了在有趣或具有挑战性的工作部分进行配对的机会,并利用团队分组会议共同做出编码方法的决策。如第一章所讨论的,保持更新关于 Puppet 最佳实践文档中的这些决策,有助于进一步传播知识。最重要的是,在提交代码的审查中,这变成了团队积极讨论和共同工作的成果,而不仅仅是一些在简短的早会更新中听到的内容,也不是团队仅仅依赖开发人员口头描述的内容。所有这些都帮助团队更好地理解代码的预期功能以及为何选择这种方法。

为了说明这种方法,通常基础操作系统需要为核心构建设置一个安全配置文件。这个安全配置文件将包含诸如核心操作系统用户账户、SSH 配置、内核设置以及其他各种重要设置等内容。将这个配置文件作为一个冲刺的焦点,可能会导致开发人员配对并共同处理构成该配置文件的组件模块。开发人员专注于诸如用户账户等元素,并一块一块地构建配置文件,这样就能取得实际进展并分享知识。根据配置文件的大小和开发人员的数量,可能更有意义去处理多个配置文件,但目标仍然是要限制范围并聚焦。

这并不是说任何开发团队应该期待一切按自己的方式进行,并且外部压力不会导致焦点分散,但必须明确表示,这会减慢工作进度并带来风险。

在理解 Puppet 代码的重点和范围之后,下一个需要了解的关键点是代码交付到生产环境的最低验收标准。最小可行产品MVP)这一术语已被误用,成为发布明显不适合生产的内容的借口,通常会表示后续添加测试等项目。事实上,这种情况是不可能发生的,因为总有新的功能需要开发,随着代码的进一步发展,未来会带来更大的运营负担。因此,在你的组织和平台中,Puppet 的最佳实践应该明确代码需要通过哪些测试。一个示例标准可能包含以下内容:

  • 代码必须在 PDK 验证中保持干净,并接受已列出的例外

  • RSpec 测试提供 100%的代码覆盖

  • ServerSpec 测试该模块,并通过核心的 ServerSpec 测试

另一个挑战可能是范围蔓延,这会稀释 Puppet 在组织中使用的更高层次的范围和重点。当投资于一个工具时,很容易通过扩展用例来最大化投资回报率,随着实施的成功,其他团队也会希望加入这一成功,尝试使用提供的工具。因此,需要明确 Puppet 的使用场景;不恰当的使用,如分发二进制文件或大规模同步文件,需要在平台文档中明确指出是不可取的。在这个例子中,这会对基础设施造成很大负担,正如本书所讨论的那样。此外,在聚焦方面,本书强烈建议反对任何鼓励强制重写/重新平台化策略的政策,除非当前的实现存在维护问题或无法按需开发。这种重写几乎没有价值,除非原始实现被充分理解,否则可能导致翻译中的错误,特别是对于声明性代码,因为只有方法是可见的,而最终状态不可见。

在讨论了 Puppet 的范围和重点后,我们现在来看一下如何在传统服务器上管理这种方法,并处理历史遗留问题以及棕地站点的复杂性。

管理无操作模式的遗留资产

遗留系统的实现可能更具挑战性;配置漂移的程度可能使得难以知道从哪里开始。你的组织可能经历了多次并购,这不仅导致了核心组织本身的多个配置标准,也包括了所有接入的系统。

一个常见的采用模式是逐步在传统服务器中建立自动化水平,以建立信心,我们将详细介绍一种常见的方法。

在所有节点上安装代理以收集事实是一个常见的起点,并将这些数据存储在 PuppetDB 中,形成有价值的 CMDB 数据源。然后,如第十三章所讨论的,这些数据可以发送到诸如 ServiceNow 之类的服务,以与中央 CMDB 服务进行集成。在 Puppet Enterprise 中,这让我们可以访问包视图,并具有管理补丁的能力,如第十四章中演示的那样。这个推广立即提供了能力,并且对整个环境有了更好的理解,甚至没有编写任何代码。

下一步是考虑编排。很可能在传统环境中,各个团队手动或半自动地执行了常见的脚本和任务。将这些脚本封装为 Bolt 项目或 Puppet 模块,并使用 Bolt 或 Orchestrator 来运行这些脚本和任务,可以在不需要重新工作的情况下提供更大的控制和流程。

最简单的情况是,如果你正在使用 Puppet Enterprise,并且在第一步中已经部署了代理,那么 Orchestrator 可以直接利用代理的存在,使用 PCP 传输进行通信,并利用 Puppet Enterprise 的 RBAC 和日志系统与 Orchestrator 协同工作。对于不想为传统版本购买许可证的开源 Puppet 或 Puppet Enterprise 用户,可以使用 Bolt 服务器设置带有 SSH 密钥和 WinRM 的黄金主机。有一种折衷方案是使用无代理的 Puppet Enterprise 许可证,但允许使用 Puppet Enterprise 主机,并仍然拥有 RBAC 和访问日志。虽然在前面的章节中没有讨论过,但基于代理的服务器的优点是它们更加集成,能够执行更多操作并收集更多数据,且它们对密钥和安全的管理是由 Puppet 作为产品的一部分来管理的。无代理方法可以在不需要请求安装代理的情况下添加,这可能不适用于所有服务器。无代理方法还避免了 Puppet 代理代码版本的潜在漏洞和更新问题,但确实存在单独的访问管理问题,例如 SSH 密钥的部署和管理。

下一步正是范围和焦点部分讨论的内容:查看基线配置,并理想情况下找到一些非谈判性的起点,这些必须在你的资产上强制执行。例如,必须关闭 root 登录,或者应用程序代理需要升级并管理版本,以避免漏洞。一旦这些简单的配置得到管理,就该查看可能有历史例外的服务器配置。对于遗留服务器而言,即使服务器的预设配置不符合当前的安全和构建标准,也应首先将其标记为问题,然后再进行修复,以避免可能导致服务问题。在没有立即修复的情况下标记配置问题,可以使用在配置文件或模块级别上的无操作标记模式,正如在第八章中讨论的那样。配置漂移可以被理解为是可以接受的例外(记录在 Hiera 数据中),或者通过将模式从无操作模式切换到执行模式来使用 Puppet 进行修复,以应用配置。

一旦基础配置文件完成,就意味着我们在遗留资产中拥有了所有可用的工具,用于自动化审计报告和合规修复。

然后,可以通过与应用程序团队互动,了解他们的配置和审计需求,并按照相同的模式为其应用程序构建自己的角色和配置文件,从而重复这一方法。

提到涉及开发 Puppet 代码的不同团队时,重要的是直接讨论跨团队协作的最佳方法。

一个平台工程方法

正如本章前两部分所明确的,Puppet 的常见采用起点是创建核心基础操作系统配置,然后再与应用程序团队进行接触。这往往会导致一个设置,即 Puppet 成为 Linux/Unix 操作系统团队的工具,这个团队主导着代码库,并且是整个平台的守门人。为了确保有效的跨团队协作,所需的是一种平台工程方法。

注意

关于如何运行平台团队的更深入知识,可以通过书籍和培训获得,例如 teamtopologies.com/,而平台工程通过 platformengineering.org/ 等社区得到了普及。

平台工程的核心概念是拥有一个平台团队,负责管理工具、工作流以及自服务平台的开发。这个平台应该被视为一个产品,用户被视为客户,确保他们的需求得到满足,并在整个组织中推广该平台。正如在第一章中讨论的,Puppet 可能会成为平台的一部分,与其他各种 DevOps 工具和工作流一起使用。图 15.1展示了一个常见的工具集选择:

图 15.1 – 常见的 DevOps 工具集

图 15.1 – 常见的 DevOps 工具集

精确地看 Puppet 将如何适配,这很可能是在第 0 天、第 1 天和第 2 天的方法中,如图 15.2所示。在第 0 天,通过类似 Terraform 的专业工具进行基础设施的配置。接着,在第 1 天,Puppet 代码将被应用于客户端,以便根据基础设施上的操作系统构建和安全标准进行配置。第 2 天,Puppet 的作用将是继续执行配置,以防止配置漂移,避免因外部意外变化或标准的有意更改而导致的代码变动。

图 15.2 – 第 0 天、第 1 天和第 2 天的做法

图 15.2 – 第 0 天、第 1 天和第 2 天的做法

思考 Puppet 代码在这些平台中的关键点是,理想情况下,运行 Puppet 基础设施的责任应该是平台团队的角色之一。这使得各个团队可以开发自己的代码和角色,并通过自服务平台有清晰的部署路径。

这并非总是可能的,通常情况下,Linux 团队仍然需要同时运行他们自己的代码库和 Puppet 基础设施。在这种情况下,最好将其视为两个独立的角色,而不是仅仅优先考虑 Linux 团队的代码库需求,忽略其他使用者的需求。平台团队不应尝试成为所有人 Puppet 代码的把关人,因为这会阻止开发人员将 Puppet 作为自服务平台来使用。你们组织的流程应该涵盖责任和升级机制,下一节将进一步讨论,受监管环境中的采纳

还应该确保负责管理遗留资产的团队承担自动化工作的责任。没有充分了解系统的新团队来自动化它们可能会面临挑战。虽然可能需要更多的时间来培训并让遗留团队参与其中,但让他们主导集成工作,可以帮助更全面地理解系统及其流程。

虽然每个团队负责自己的代码,但合作开发标准和最佳实践仍然很重要,以确保团队具备适当测试和构建工具管道的知识。

跨团队协作不仅仅限于使用 Puppet,还包括其他集成点。在 Puppet 中重写和运行所有内容既不实际也不可取。创建实践社区,让跨部门的各个团队可以见面、讨论并展示他们在自动化方面的思路和进展,能够促进思想的交流。在某些情况下,甚至可以复用其他团队在组织内已经开发的成果。这不应被视为一种竞争,而是相互受益和交流技能与想法的机会。

各个层级的宣传至关重要。参加各种团队会议、午餐会、管理层会议以及外部供应商或行业组织的活动,可以帮助传播关于你平台的消息,并为进一步的发展激发热情。外部供应商活动通常被视为法律复杂,但通过仔细考虑并与法律和营销团队咨询,你可以在组织内部提升平台的知名度,并通过激发对你工作兴趣来吸引外部人才。此外,这些外部活动,如技术顾问委员会,是与志同道合的组织交流最佳实践的绝佳机会。

虽然在范围与重点部分已提到过,但值得强调的是,不要试图解决所有被带给你的问题。如果你能够很好地宣传,大家会变得非常热情,但必须完全诚实地说明你平台的能力和适配性。你应该清楚地传达你能够实际交付的内容,以及如果他们希望参与或接触 Puppet 平台,必须承诺什么。

确定了范围和重点,并理解了协作工作方式后,接下来的重要思考应围绕监管和流程如何影响这些工作方式展开。

在受监管环境中的采用

在高度受监管的环境中工作可能具有挑战性,但这通常是 Puppet 能发挥最大影响的地方。在受监管的环境中实施自动化可能更困难,但执行大规模手动操作则更加具有挑战性,因此潜在的投资回报显著。采用新技术时,最糟糕的做法就是相信“流程只需要改变”。这种态度会导致团队在后期失败,并且可能会造成懒散和忽视流程工作的声誉,最终导致无法在生产环境中正常运行的设置。

最好的做法是在实施 Puppet 之前,先与变革、风险、审计和其他涉及管理流程的团队进行接触。尽管组织中经常抱怨流程问题,但通常没有人去与这些团队互动,而这些团队可能有自己的现代化计划,你可以将你的实施计划与之对接。讨论 Puppet 是什么以及你计划如何在生产环境中使用它,可以提供可信的反馈。即使这些反馈需要缩小你最初的雄心壮志,它也比将这些团队视为守门人要好,因为守门人往往对你的实施缺乏理解,只会拒绝他们未能理解后果或未曾参与的方法。

注意

邀请你的流程团队参加实践社区会议和演示;你们并不站在对立面,反而会发现,在为组织提供价值的过程中,你们有更多的挑战和目标是共同的。

重要的是要围绕 Puppet 能够做什么以及你的开发、测试和发布方法如何运作进行讨论,还要明确其覆盖的范围。Puppet 是一个强大的工具,操作在管理/根级别,因此必须展示你对流程的掌控力,并且理解与这些流程相关的任何风险。

为了赢得所有相关方的信任,包括流程团队,你可以向他们展示 Puppet 的可能性,并讨论它如何为组织带来益处。正如在聚焦与范围一节中讨论的那样,可以通过持续改进流程,特别是通过实践社区讨论,来实现这一点。通过这种方式,多个团队和部门可以就有益于组织的改进达成一致,而不会妥协于安全性和风险。

这种方法看起来可能并不具有革命性,但在受监管的环境中,变革无法快速发生。因此,重要的是要关注在当前限制条件下可以做的事情,展示你的解决方案如何适应这些条件,并与相关方合作来现代化或改进流程。这需要耐心和一致性来赢得团队的支持。在完成对传统私有数据中心环境的视角后,考虑这种方法在云端的不同之处也是非常重要的。

向云迁移

向公共云迁移带来了巨大的机会,尤其是在灵活性方面,提供了使用云特定技术来减少组织运营负担的机会。例如,利用可用区来减少数据中心故障风险,虽然是一个复杂的功能,但在私有数据中心中实现起来非常困难。

不幸的是,云采用方法中有两种常见的反模式。第一个是将所有基础设施、流程和组件完全复制到公共云中,就像它们在私有数据中心中一样工作。这种情况通常发生在“云优先”计划中,这往往是首席信息官CIOs)对公共云资源采用的失望结果。这迫使在组织准备好并理解适合的情况下将部署推入公共云。这导致了意外的账单,因为部署的基础设施并未计划为灵活,并忽略了公共云的租赁性质,而且许多在私有数据中心中有效的解决方案在公共云中的云原生解决方案中实现得更好。

第二种反模式是把一切都抛在脑后,这在那些对内部流程和交付时间感到沮丧的应用团队或部门中可以看到。他们可能有理由感到沮丧,但很少有经验;可悲的是,在私有数据中心中艰难取得的经验教训被遗忘,审计、配置和测试中的最佳实践必须重新建立,因为审计员发现新设置中存在问题。

你应该考虑在公共云中真正执行的内容;当查看如何部署 Puppet 基础设施时,第十三章中提到的多区域模式和策略展示了选择。简单来说,我们可以在私有数据中心通过 Puppet 基础设施管理公共云服务器,或者将 Puppet 基础设施迁移到公共云并管理私有数据中心和公共云,或者为私有数据中心和公共云分别设置独立的 Puppet 基础设施。

这个选择取决于实施目标。例如,公共云是否将用于为私有数据中心提供灵活的容量,例如通过提供一个可以在灾难恢复中构建的备用站点?或者,公共云是否用于开始一种新的工作方式,采用更具云原生的方法和新团队?在第一种情况下,你更有可能希望服务器的配置与共享代码库保持一致,并且拥有一个单一的管理界面对团队管理基础设施可能会有帮助。在这种情况下,决定基础设施是应该私有化还是公共化将归结为成本问题,以及你是否打算利用云原生功能,如可用性集和负载均衡器的灵活性,这些功能可以按需添加编译器。

在第二种情况中,当新团队开始寻找新的方法时,拥有独立的基础设施将最为合理,回顾当前构建中哪些是有用的,哪些仅与私有数据中心相关。如在平台工程方法部分所述,这涉及到了解在云中工作的团队的需求,并确保 Puppet 作为平台的一部分来满足这些需求。云团队随后应该能够使用 API 进行自助服务,同时享受 Puppet 提供的审计和安全性要求,以符合组织在云中的要求。

这也可能是一个机会,从传统组织中使用的高度自定义标准转变,甚至考虑采用合规性来实施 CIS 标准,这在第十四章中提到过。这将是一个成本考虑问题,需要判断是否合理让你的团队来维护这些标准。

摘要

在本章中,我们讨论了如何不仅仅考虑纯技术问题,还要让 Puppet 的采用成功。我们回顾了如何选择重点和范围,以便通过迭代的持续改进交付 Puppet,使用定期的交付节奏以及像小团队集中的冲刺等方法,团队可以一起工作。我们讨论了如何将这个重点分解为可协作完成的交付物,并在定期周期中展示。我们还提到允许 Puppet 团队在做出决策和建立编码实践的过程中建立信心并学习,同时向管理层和利益相关者展示有意义的回报和进展。我们讨论了如何概述 Puppet 的使用案例,并确保避免将任务强行纳入 Puppet 中,以最大化回报,这样的做法可能会破坏 Puppet 基础设施的可靠性和性能,并带来日常维护的难题。

然后回顾了采用遗产资产的方法,展示了即使在由于标准和策略变化以及公司合并收购导致的资产断裂情况下,也可以遵循一个渐进的采用模式,逐步减少配置漂移,并收集有关资产的信息。

我们首先讨论了如何在没有配置的情况下推出代理并收集事实,以创建资产视图,这些视图可以输入到 CMDB 中,在 Puppet Enterprise 的情况下,使用内置集成来管理补丁和打包。接着我们展示了可以考虑使用编排来包装现有脚本,并提供更好的自动化。这可以通过完全授权的 Puppet Enterprise 通过 PCP 或 WinRM/SSH 连接在没有代理的 Puppet Enterprise 许可证下进行,或者使用 Bolt 服务器与 WinRM/SSH 连接来完成。这取决于你的许可证要求以及是否需要 RBAC 和日志记录。随后我们讨论了如何通过构建有状态的 Puppet 代码基准,查找需要强制执行的强制性设置,并在适当情况下使用无操作(no-op)来获取当前视图,逐步构建一个配置文件,允许将例外情况接受到 Hiera 中或对漂移进行修复。建立了这个基准之后,我们讨论了如何与应用团队重复此过程,以便将传统应用纳入控制范围,然后使用配置数据自动化审计报告。

我们讨论了如何跨多个团队协作,成立了一个 Puppet 平台团队,该团队倡导平台并为其他团队提供 API 和自服务,同时为团队设定了采用和使用 Puppet 的标准,但并没有对他们的交付进行限制。

在受管环境中部署证明了 Puppet 的有效性,通过向关键流程团队(如变更和风险管理团队)传达 Puppet 的工作原理,并解决实施过程中使用的流程,如何将代码开发和部署到生产环境中,同时考虑如何最好地与当前流程集成。赢得流程利益相关者和运营团队的信任,未来可能会改变流程,从而推动进一步的自动化。

最后,我们回顾了公共云的采用,讨论了公共云中遇到的两个主要问题:其一是采用云优先策略导致技术和流程的搬迁未经过深思熟虑,另外一个是应用团队单打独斗,忽略了在私有数据中心中为安全性和审计所做的自动化经验教训。我们解释了,应该考虑公共云的目的。它是数据中心的扩展,还是你希望将其纳入内部 Puppet 服务器视图的一部分,或者是你正在迁移的对象?将 Puppet 基础设施迁移到公共云可以开始采用云的灵活性。这是一个全新的方法,旨在通过代码优化来支持云的采用,或者通过遵循行业标准的 CIS 方法来摆脱老旧的定制内部标准,带来新的机会。关键措施是与应用团队会面,确保他们可以访问 API 和平台,从而使他们不必担心核心基础设施构建和安全配置,而只需关心对他们有用的内容,并且能够通过自助服务进行管理。

本书中展示了 Puppet 的有状态方法如何减少漂移和技术债务,自动化审计报告,提供一种标准的变更交付方式,即便在高度监管的环境中,也能为用户提供一个值得信赖的平台,以满足他们的基础设施需求,并释放团队的精力,将注意力集中在为客户交付产品上。配置管理是一个复杂的问题,没有一劳永逸的解决方案,但我们通过一种深思熟虑的迭代方法,展示了通过与组织的流程合作并全员参与,Puppet 如何为你的组织带来变革性的改变。

posted @ 2025-06-26 15:33  绝不原创的飞龙  阅读(1)  评论(0)    收藏  举报