原文:
annas-archive.org/md5/74e7dee08e6c205ecc2a82f2d11edba8译者:飞龙
前言
随着 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_data、json_data和hocon_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 版本。
随时随地、在任何设备上阅读。直接将您最喜欢的技术书籍中的代码搜索、复制并粘贴到您的应用程序中。
优惠不仅仅是这些,您还可以获得独家折扣、时事通讯,并每日收到丰富的免费内容到您的邮箱
按照以下简单步骤获取福利:
- 扫描二维码或访问以下链接
https://packt.link/free-ebook/9781803231709
-
提交您的购买凭证
-
就是这样!我们将直接把免费的 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 工具集
本书不仅专注于技术理解,还将着重于如何利用 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 是否正确?如果不正确,我们将进行更改。这个脚本仅覆盖能够使用 useradd 和 usermod 的操作系统。为了实现跨多个操作系统的兼容性,我们需要检测操作系统类型并为每个操作系统或操作系统组及其所需的命令编写类似的代码段。通常,为了涵盖更广泛的操作系统版本,编写多种语言和脚本是更实用的做法,例如,如果我们想同时支持 Unix 和 Windows。
这与 Puppet 声明相对比,后者无需更改就可以在多个操作系统上工作,因为 Puppet 会检测所需的命令,并作为一部分执行所有必要的状态检查。
这个示例仅仅涉及一个具有单个属性的资源。你可以很快看到,随着检查项和选项的不断增加,shell 脚本示例将变得越来越复杂,并且难以扩展。
Puppet 语言中的关键术语
详细查看 Puppet 语言,Puppet 中最基本的元素是资源。每个资源描述系统的某个部分以及你希望它处于的理想状态。每个资源都有一个类型,它是 Puppet 语言中该资源如何配置的定义,包括哪些属性可以设置,以及可以使用哪些提供者。属性描述的是状态。因此,对于一个用户来说,属性可能是家目录;对于文件而言,属性可能是权限。提供者使得 Puppet 跨操作系统独立工作,因为它们执行底层命令,无论是创建用户还是安装软件包。
所以,让我们以一个公司为例,该公司通常会向环境团队提交构建请求表单,要求配置服务器:
表格 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 – 角色、配置文件和模块的结构
在第八章中,我们将深入探讨更多技术细节,但从本概述中最重要的要点是理解模块提供了可共享和可重用的一次性技术安装。相比之下,角色和配置文件模式为你的组织提供了背景。角色是客户在订购服务器服务时使用的;他们不需要理解技术实现,只需要知道它符合他们的业务需求。你组织技术栈中的配置文件由技术设计师和架构师管理,他们根据组织的标准和配置来组合和指定模块。这些角色负责定义不同组件如何集成,以创建所需的技术栈。因此,虽然一个 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中,示例门户展示了如何向客户展示简化的产品。Puppet 语言的重点应当是为客户提供一致的产品,并让客户、架构师和技术人员专注于他们关心的内容,而无需自己深入技术要求或编码部分。
Puppet 作为平台
到目前为止,本章重点讨论了 Puppet 语言,但现在我们将探讨 Puppet 平台以及它如何将期望的状态应用于客户端服务器。Puppet 可以仅通过安装代理和所有文件本地运行,这在测试中很常见,但本概述将重点介绍客户端-服务器设置。在第 10、13 和 14 章中,我们将详细讨论弹性、可扩展性和更高级的运行选项。然而,目前我们将重点关注 Puppet 客户端如何与服务器通信,以请求并应用其期望的状态。
Puppet 控制下的每个客户端都会安装 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 控制台服务器报告
默认情况下,这个周期每 30 分钟进行一次。在前面的部分中,重点讨论了语言如何自动化构建服务器。在这里,我们可以看到,通过该平台,我们可以确保所有部署的服务器都被强制执行我们设定的状态;无论是安全标准配置文件,还是我们决定更新某个实现中的设置,比如向 IIS 添加额外功能。这可以避免服务器漂移,即当服务器难以保持更新或容易受到手动错误更改或恶意违反标准的影响时。图 1.6显示了 Puppet Enterprise 的仪表板视图,清晰展示了一个服务器群体及其上次运行的状态。这突出显示了服务器是否符合我们的状态要求,或是否在上次运行时做出了更改:
图 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
开发环境所需的软件:
-
Puppet agent (
www.puppet.com/docs/puppet/8/install_agents.html) -
Visual Studio Code (
code.visualstudio.com/) 和以下扩展:-
Visual Studio Code 的 JSON 支持
-
Puppet
-
Rest client
-
Ruby
-
ShellCheck
-
Thunder client
-
VSCode Ruby
-
YAML
-
PowerShell
-
Puppet 模块 PECDM (
github.com/puppetlabs/puppetlabs-pecdm)
-
-
GitHub CLI (
github.com/cli/cli) -
Puppet 开发工具包 (
puppet.com/try-puppet/puppet-development-kit/) -
Azure CLI (
docs.microsoft.com/en-us/cli/azure/install-azure-cli) -
一个 Azure 账户
-
一个 GitHub 账户(免费账户)
-
用于与 GitHub 通信的 SSH 密钥
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 – 实验室设置
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 基础设施的过程。这确保你为后续章节中的实验做好准备。在本实验中,pecdm和peadm模块用于配置标准架构,这是 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.com/blog -
Dev.to 文章:
dev.to/puppetecosystem和dev.to/puppet -
Twitter:
twitter.com/puppetize和twitter.com/PuppetEcosystem
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 问题
使用github.com/rodjek/vim-puppet可以在vim中实现类似效果。
重要说明
本书中将提供有关最佳实践和编码方法的建议,很多建议来自于 Puppet 风格指南等来源。一个组织在开发清晰一致的 Puppet 代码时可以做的最好的一件事,就是编写自己的最佳实践和风格指南,基于 Puppet 风格指南提供的基础,确保在代码审查时遵循该指南。这也可以与风格指南或本书中的某些观点不一致,只要这对你的组织和开发人员最有利并达成共识。
每种类型的资源必须唯一,ntp 资源不可重复命名为两个服务类型资源名为 ntp。在命名时,在字符或空格方面没有其他限制,但出于性能考虑,标题应该保持简短,并且永远不超过 140 个字符。这个 标题 是 Puppet 在生成目录时识别资源的依据。
namevar 属性(也被称为 namevar,默认情况下与标题相同,除非分配了其他属性)。在某些情况下,类型将使用多个属性来定义 namevar,例如一个包同时使用命令和名称。这在通过不同机制安装相同配置的多个副本时使用,比如安装与 Ruby gem 同名的包,以及作为 Red Hat 包管理器 (RPM) 安装的包。
安装 Apache 包可以演示 namevar 与 apache_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 管理,如 mtime 和 ctime。其他属性,例如 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,也可以声明为installed。latest值,顾名思义,确保软件包处于提供者可用的最新版本。对于更灵活的版本控制,可以将值设置为字符串版本,如1.2.3,并且根据提供者的支持,可以使用版本范围,如> 1.0.0 < 2.0.0。使用absent值是 Puppet 的重要部分,在这里,资源不仅确保服务器状态中存在的内容,还包括不应存在的内容。
与在ensure中使用absent值相关的是purged值,这是一个依赖于提供者的选项。如果设置为true,则在删除软件包时会移除配置文件。
providers属性通常保留默认设置,但如果需要通过其他软件包管理系统(如pip或rubygems)安装,可以将其值设置为适当的提供者名称。
要查看可用的提供者,可以在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安装ruby和devkit,并使用/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 属性,有 present 和 absent 选项。选择 present 将默认使用文件值,确保创建的资源是一个普通文件,但仅强制文件路径存在,无论它是符号链接、文件还是目录。
要创建和强制执行一个资源,我们必须选择一个文件的值,并使用 direct 来创建目录或目录嵌套,或使用 link 来创建符号链接。
路径是此类型的 namevar 属性,应为完全限定的路径,或者可以从标题中默认获取。
例如,一个名为Puppet directory的资源,它为位于C:\ProgramData\PuppetLabs的现有directory创建了ensure,如下所示:
file {'Puppet directory' :
ensure => 'directory',
path => 'C:\ProgramData\PuppetLabs'
}
对于我们确保作为文件的资源,content属性为我们提供了多种将内容写入文件的方法。最简单的方式是直接将文本字符串放入文件中,但通过使用函数、文件和模板,我们可以复制存储在 Puppet 模块中的整个文件的内容,或使用模板文件,允许我们将值替换到预先解析的文件中。这些功能将在第七章和第八章中详细介绍。
然后使用三个属性来管理所有权和权限:user、group 和 mode。对于 user 和 group,这很简单,只需输入 UID 和 GID 或用户名和组名。如果未设置,这将默认为 Puppet 正在运行的用户和组。mode 使用 Unix 风格的 4 位权限模式来处理权限,但对于 Windows 系统,输入的模式会有一个非常粗略的转换,最好不要声明 mode,而是使用 ACL 模块来补充文件:forge.puppetlabs.com/puppetlabs/acl。
举个例子,以下声明创建了一个名为config.test的文件,设置了owner和group,并包含两行文本内容:
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 控制之外的文件。
注意
我们将在下一章详细讨论数据类型和变量,但目前请注意,取值为true或false的参数可以不带引号,这也是本书采用的风格。
purge参数只能与ensure设置为directory且recursive设置为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 服务器上,应用此资源声明将会在nfsserver的exampleapp共享下查找第一个匹配项,首先寻找conf.server1,如果找不到,再查找conf.windows,最后查找conf。
不推荐使用backup参数,因为管理和扩展文件桶以存储这些备份是困难的,正如我们在第十一章中看到的,还有更好的方法可以考虑,例如在 Git 中管理我们的代码,以便应对回退场景。
replace 参数应谨慎使用,但如果设置为 true,则仅在文件不存在时强制执行内容。如果文件已存在,则状态已满足。这对于需要初始配置文件但随后会覆盖它的应用程序非常有用。
讨论了许多属性后,尝试通过编写清单文件来满足列出的要求,以实践构造示例:
-
在基于 Unix 的系统中,确保
/etc/sudoers.d目录中仅包含 Puppet 控制的文件。 -
添加一个
/etc/sudoers.d/mongodb文件,内容为robin All=(ALL) NOPASSWD: su – mongo,并使用验证命令visudo -c,文件归root用户所有,属于root组,权限设置为0660。 -
创建一个符号链接,从
/opt/mongodb/mongos到/home/robin/mongos。 -
查看建议的解决方案:
github.com/PacktPublishing/Puppet-8-for-DevOps-Engineers/blob/main/ch03/file_unix_answer.pp。
对于 Windows,请参阅以下内容:
-
在基于 Windows 的系统中,确保
c:\inetpub\wwwroot目录中仅包含 Puppet 控制的文件,但子目录不受影响。 -
添加一个
c:\inetpub\wwwroot\page,内容为nfsshare1:\\publish\page.html,并使用验证命令c:\program files\httpvalidator\httpvlidate.exe文件。 -
创建一个符号链接,从
c:\program files\httpvalidator\httpvlidate.exe到C:\Users\david\Desktop,并使用replace选项在文件存在时替换它。 -
查看建议的解决方案:
github.com/PacktPublishing/Puppet-8-for-DevOps-Engineers/blob/main/ch03/file_windows_answer.pp。
服务类型
安装软件并创建配置文件后,下一步通常是启动具有服务类型的服务。由于系统服务在支持和提供内容上差异很大,我们必须小心提供所有必要的参数。一些服务缺乏正确的状态命令,但可以通过服务的参数提供。
运行并查看 puppet describe service -p 的输出,你将看到各种提供程序,尽管在大多数情况下,默认的服务提供程序是所需的。在某些情况下,如现代 Red Hat 系统上仅提供 init 脚本的旧版软件,我们可能需要选择不同的提供程序。
需要考虑的前两个参数是enable和ensure。ensure接受stopped或running的值,也可以分别表示为false或true。这是一个简单的二进制值,表示服务是否应该运行。enable定义了服务是否应在启动时自动启动,这仅由某些提供程序提供。可以设置为true或false,表示启用或禁用,然后还有一些依赖于提供程序的选项;例如,在 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,在系统术语中,意味着它禁用服务,以至于即使是依赖于它的服务也无法激活它。
注意
请注意,ensure和enabled的默认值完全依赖于底层提供程序的实现。
在没有提供启动脚本的应用程序的情况下,结合start和stop参数,您可以使用 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 服务,因为它可以方便地启动/重启服务,而无需考虑它运行在哪个操作系统上。
关系将在第六章中详细讨论,但为了支持彼此依赖的资源,正如包、文件和服务模式所要求的那样,需要了解 require、before、subscribe 和 notify 这些元参数的基础知识。require 和 before 是镜像关系,它们在两个资源之间创建一个关系,使得当 Puppet 运行一个资源时,它会先运行另一个资源。定义关系的方向在语义上并不重要,尽管在多对一关系中,将依赖元参数应用于多个资源可能会更加合乎逻辑。
类似地,subscribe 和 notify 元参数不仅允许一个资源具有依赖关系,还能在资源状态变化时向支持它们的类型发送刷新事件(可以通过 puppet describe 在类型文档中确认)。这在服务资源中特别有用,因为更新配置文件应导致服务重启。
这些元参数的语法是资源引用,它由一个首字母大写的资源类型和方括号中的资源名称组成。以下是使用 before、notify 和 require 来使包、文件和服务模式的示例:
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.com 或 bolt 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.pp 和 github.com/PacktPublishing/Puppet-8-for-DevOps-Engineers/blob/main/ch03/grafana_windows.pp 找到。
要在 noop 模式下测试运行,可以将 _noop => true 选项应用于 Bolt 命令。
虽然详细讨论每个核心类型可能不切实际,但下一节将高层次地介绍其他核心类型,这些类型对于创建更高级的配置非常有用。
核心资源类型
在本节中,我们将讨论核心资源类型。
用户和组类型
用户类型和组类型是大多数配置的核心,允许将 ensure 属性设置为 present 或 absent。使用 Unix 平台作为提供程序时,用户通常会有最小的 uid 和 gid 属性,而组则最少有 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 属性、unless 或 creates,或者 exec 具有仅刷新属性。
在第一种情况下,如果命令是 exec 报告运行。
使用 onlyif 属性,我们可以声明一个命令,如果它返回 true,则我们的 exec 将会执行。unless 是 onlyif 的反义词,使用一个命令,如果它返回 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 属性,因此通过使用 notify 和 subscribe 属性,我们可以设置 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 运行时的状态。因此,在代码更改的回退过程中,应该始终考虑这一点。
元参数和高级资源
本节将首先介绍元参数,它们是适用于任何资源类型的属性。对于实验部分,我们涵盖了before、required、notify和subscribe,这些用于在资源之间创建依赖关系。接下来,还有几个其他有用的属性,具有不同的效果。要查看类型和提供者上元参数的完整文档,可以在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'
}
}
该组将有group、ubuntu、pci和sox标签,而用户将有user、ubuntu和pci标签。此外,两者还将有一个类名标签,example::access。使用puppet apply --tags pci example.pp命令时,两个资源都会类似地应用;ubuntu标签会应用两个资源,而运行带有sox标签的命令则只会运行该组。
还有一些其他的元参数,如alias和loglevel,尽管它们没有太大使用频率且没有值得详细讨论的风险,但仍可以在www.puppet.com/docs/puppet/8/metaparameter.html查看,或者通过运行puppet describe <any type> -m来了解。
之前展示的资源声明遵循了相同的简单声明模式,但还有其他几种方法可以实现更多的灵活性和高级功能。
资源元类型
Puppet 有一个resources元类型,可以用于确保移除未管理的资源类型。如果把它当作<type> Puppet 资源的输出,可以找到任何没有匹配namevar属性的资源,并标记为absent。它使用四个属性;一个是purge属性,可以为true或false,另两个属性在使用用户类型的资源时特别相关——unless_system_user,该属性接受true、false或指定的最小 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 个用户 exampleappdev、exampleapptest、exampleappprod 和一个组 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.pp 和 github.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 中,我们为重复使用的内容(如文件路径)或条件逻辑(如if或case)分配变量。在这些情况下,必须选择一个值并进行赋值,具体取决于初始状态。
注意
我们在这里稍微简化了过程,因为现在有延迟函数可以在复杂性处理后运行。然而,这仍然不允许我们重新赋值。我们将在第五章中更详细地讲解这一点。
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 属性只能接受 running 或 stopped:
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开始) -
–(假设为正数)*0x或0X(大小写不重要)* 数字字符与大写或小写字母的混合*–(假设为正数)* 数字字符(使用-1 到 1 之间的数字时需要0)* 小数点* 数字字符* 可选的e或E,后面跟着数字(用于科学浮点数)
以下是一些简单且恰当命名的例子,涵盖了上述类型中的每一项:
$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,用于在已知的100到10000的范围内为一个卷组分配百分比:
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配置文件中设置此项。
作为一个简单的例子,以下代码会通知Print,test1尚未声明:
notify {"Print $test1":}
undef数据类型的唯一值是未加引号的undef,并且它本身不用于参数数据类型。这是因为强制要求值的缺失没有意义。
在本章的抽象数据类型,包括敏感类型部分,我们将看到如何接受undef值作为参数的一部分,作为可行选项的选择。
当插入到字符串中时,undef会被转换为空字符串('')。
调用
在第五章和第八章中,我们将学习一些函数,如delete_undef_values和filter,它们可以用于修剪数组和哈希中的undef值。
布尔值
Puppet 中的布尔值代表true或false,在第七章中,当我们查看if/case语句时,你会发现所有 Puppet 的比较都会返回布尔类型。布尔变量应该仅包含一个未加引号的true或false值。因此,这使得数据类型非常简单,没有参数——只有大写的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模块的num2bool和str2bool函数,这将在第八章中讲解。
正则表达式
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,而是使用参数。
这些参数应包括以下内容:
-
源下载:一个默认指向
dl.grafana.com/enterprise/release/grafana-enterprise-8.4.3-1.x86_64.rpm的字符串变量 -
端口:服务监听的端口,类型为整数
-
服务启用:通过布尔值
要实现类的赋值,编写一个类声明,分配变量以确保该类包含在目录运行中。当你在清单上运行bolt时,它将确保你已经包含了变量。解决方案可参考github.com/PacktPublishing/Puppet-8-for-DevOps-Engineers/blob/main/ch04/all_grafana.pp。
数组和哈希
本节将涵盖 Puppet 中的两种核心数据集合:数组和哈希。你将学习如何创建、访问以及执行操作,将值操作到一个新的变量中。
分配数组
Puppet 数组是通过用方括号括起来的以逗号分隔的值列表来创建的。最后一个元素后可以添加可选的逗号,但本书不推荐这样做,以保持样式一致。例如,名为example_array的数组,包含first、second和third字符串,可以如下声明:
$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)接受任何类型的值,并将其作为新元素添加到数组的末尾。这包括将一个数组作为嵌套数组添加。要合并两个数组,必须使用连接操作符(+)。为了演示这一点,我们来看一个包含整数 1 和 2 的数组,它追加 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]]
如果需要连接一个哈希,它会被转换成数组,除非它已经变成只有一个哈希元素的数组。例如,在下面的代码中,转换后的变量将被赋值为一个嵌套数组,元素为 test 和 value,并赋值为 [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,然后继续搜索匹配的three和four字符串,移除第四、第五和第六个元素,同时将['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]}":}
嵌套哈希
与数组一样,通过在哈希中声明一个哈希,可以创建一个嵌套哈希,这样就可以通过链式键来访问。以下示例展示了一个包含packages和services键的变量包列表。packages键包含httpd键,其值为字符串latest,以及cowsay键,其值为浮动值4.0。services键包含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键和字符串oracle、version键和整数11的哈希与包含web_server键和字符串httpd、version键和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键和整数11,httpd键和值12,以及cowsay键和值9。当删除一个键以创建no_cowsay变量时,cowsay和9的键值对被删除。当only_cowsay被赋值时,要删除的哈希中oracle和httpd的值不重要,它将简单地删除键值对。而对于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参数,它必须包含一个包含1到10个键值对(字符串和整数)的哈希:
Class kernel_overrides (
Hash[String,integer,1,10] tunables
)
混合哈希和数组
由于哈希键值或数组值可以是任何数据类型,因此可以进行嵌套操作。但应注意不要让结构过于复杂。
以下示例显示了包含nfs_share_servers哈希的server_cmdb哈希,其中prod和dev键包含字符串数组:
$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.md和www.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>]
例如,下面的类接受 true 和 false 的布尔值,或者 true 和 false 字符串作为 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,要求主机名是以 edi、gla 或 abe 开头的字符串:
Define server_access (
Pattern[/^edi/,/^gla/,/^abe/] hostname
)
数组和哈希
在这一部分,我们将涵盖各种数组和哈希类型。
元组
在上一节中,我们讨论了数组类型可以声明一个类型来包含其所有内容。Tuple 允许在数组的特定索引位置使用任意数量的类型,并支持可选的最小值和最大值。最小值如果小于已分配的类型数量,则这些类型变为可选,而最大值则允许最后一个类型在最大值大于已声明类型数量时重复。最大值需要声明一个最小值:
Tuple[ <type>, *<type>, <minimum size>, <maximum size>]
为了提供一个例子,假设有三个变量:user_declaration、calculation 和 file_download。user_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 数据类型中,单个键类型和值类型被声明,而结构体允许按特定顺序声明字符串键,并且键的值可以选择性地为 optional 或 undef,同时允许声明值类型。与 Tuple 不同,没有最小或最大大小的限制:
Hash[<*optional *undef String name>, <Value type>, *(<*optional *undef String name >,<value type>)
为了说明可选键和值如何影响变量赋值,让我们考虑三个例子:config_file、application_binary 和 application_startup。config_file 变量需要键值对,包括具有字符串值的 mode 键,其值可以是 file 或 link,以及具有字符串值的 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。这有助于我们避免在变量插值时出现资源标题冲突。否则,local 和 also_local 类都会包含名为 Print override 和 Print world 的 notify 资源,且会因重复资源而无法编译。
如前所述,also_local 类可以从 local 类中调用 global 变量,但不能将其赋值给该本地作用域。
总结
在本章中,我们学习了 Puppet 变量与普通过程语言中的变量不同,因为它们只能被赋值一次。我们看到某些词是保留的,不能用作变量命名。我们还看到,Puppet 变量可以进行插值,这取决于字符串的放置方式和位置。
我们介绍了各种核心数据类型及其如何用于限制参数和赋值变量。我们还探讨了 undef 和布尔值,它们在转换值时需要小心管理,以获得预期的结果。
接下来,我们研究了数组和哈希以及如何为它们赋值。尽管它们不能被更改,但我们了解了运算符如何将它们转化为新的赋值。我们还讲解了数组和哈希如何嵌套以及它们如何混合为数组的哈希或哈希的数组。
接着,我们研究了抽象数据类型及其如何通过 Sensitive 类型更加灵活地限制参数,它为日志和报告提供了作用域保护。
之后,我们回顾了如何在不同的作用域中声明 Puppet 变量,以及如何在不同的作用域中共享/查看变量。
在下一章中,我们将介绍事实和函数。我们将查看系统配置工具 Facter,它收集的信息,以及如何定制以收集用户特定的系统配置数据。函数提供 Ruby 代码插件,允许在编译时运行代码,可以执行数据操作或影响目录的运行。我们将讨论内置函数以及来自标准 lib 模块的函数,这些函数可以用于将数据类型转化为我们在本章中讨论的变量。
588

被折叠的 条评论
为什么被折叠?



