精通-Puppet5-全-

精通 Puppet5(全)

原文:annas-archive.org/md5/824fc64c35522ff63cf05c93c55acd70

译者:飞龙

协议:CC BY-NC-SA 4.0

序言

Puppet 5 仍然是首选的软件配置管理工具,尤其适用于大规模配置。

以下是一些当前现实世界中使用 Puppet 的例子:

尽管有 Ansible 和 Salt 等更新的产品,但 Puppet 依然是——我相信——首选工具,尤其适用于大型基础设施(10,000+台服务器)。值得一提的是,Ansible 也变得非常流行,这可能与其较浅的学习曲线以及被 Red Hat 采纳有关。

处理如此规模和复杂度的问题并非易事。在本书《Mastering Puppet 5》中,我们希望为你提供所需的技术,让你能够应对自己的大规模挑战,达到精通级别。

Puppet 5 版本是本书所涵盖的版本,它在去年的 PuppetConf(2017)上由新 CEO Sanjay Mirchandani 盛大发布,被宣布为"Puppet 有史以来最大的一次产品创新"。在本书中,我们详细讲解了这些新技术,包括 Puppet Discovery、Puppet Tasks 和 Puppet Pipelines,帮助你掌握使用 Puppet 5 的技能,让你在实际环境中自信使用。

本书适合的人群

如果你是一名已经使用过 Puppet 的系统管理员或开发人员,并希望在企业环境中、大规模使用 Puppet,掌握更高级的技能和最佳实践,那么这本书适合你。你需要具备一些使用 Puppet 的初级知识。

如果你在开始使用本书之前,希望先获得 Puppet 的入门知识,请查看 Puppet 的免费自学培训课程(learn.puppet.com/category/self-paced-training),或者参加一些由讲师主导的培训或私人培训课程(puppet.com/support-services/training)。

本书内容

第一章,编写模块,将真正帮助你迈上正确的道路,编写更高质量的 Puppet 模块和清单,并介绍了 12 个模块编写的最佳实践。

第二章,角色和配置文件,介绍了两种额外的抽象层和改进的接口,使您的层次化业务数据更容易集成,使系统配置更易于阅读,并且使重构变得更加容易。

第三章,扩展 Puppet,涵盖了生态系统的三个部分,这些部分仍然可以通过 Ruby 层访问,以扩展 Puppet 以适应更高级的使用场景;即自定义事实、自定义函数以及类型和提供者。

第四章,Hiera 5,涵盖了 Hiera 的最新版本,它允许我们将所有特定于站点和业务的数据与我们的清单分开,从而使我们的 Puppet 模块更具可移植性。在本章中,我们还简要查看了配置和数据的三层结构:全局、环境和模块。我们还介绍了如何设置加密的 YAML 后端。最后,我们简要了解了如何使用 Jerakia 扩展 Hiera。

第五章,代码管理,涵盖了 r10k 和 Code Manager 的使用,允许我们将所有 Puppet 代码存储在 Git 仓库中,并提供版本控制和回滚功能。我们讨论了目录环境,它们使我们能够在单个主机上使用多个版本的代码,并且 r10k 和 Code Manager 支持这种方式。我们将构建一个 Puppetfile,并积极将代码部署到我们的 Puppet Master。

第六章,工作流,涵盖了一个基本的 Puppet 工作流。我们将更加深入地将 PDK 融入我们的基本工作流中,使我们能够更高效地编写代码。

第七章,持续集成,涵盖了将 Puppet 与 Jenkins 集成作为持续集成CI)系统。我们将讨论CI/持续部署CI/CD)管道的组件,实现达到这些里程碑所需的条件,并在 CI 系统中积极改进我们的 Puppet 代码。

第八章,通过任务和发现扩展 Puppet,涵盖了 Puppet 任务和 Puppet 发现。Puppet 任务允许我们运行临时命令并将其作为命令式脚本的构建块。我们将构建一个任务来检查日志文件,并计划为我们的 Puppet Master 构建一个聚合日志文件。Puppet 发现允许我们检查现有的基础设施,并确定虚拟机或容器中包、服务、用户以及各种其他组件的真实情况。

第九章,导出资源,介绍了 Puppet 中的虚拟和导出资源。我们将探索如何在清单中导出和收集资源,并介绍导出和收集资源的一些常见使用场景,包括以下内容:动态 /etc/hosts 文件、负载均衡和自动数据库连接。我们还将探索 file_lineconcat 资源,允许我们基于这些导出资源构建动态配置文件。

第十章,应用编排,介绍了多个节点运行的顺序。我们将构建应用编排清单,使我们能够将节点连接在一起,并在多个节点之间提供配置信息,确保我们的多节点应用按所需顺序运行,并且拥有所需的信息。

第十一章,Puppet 扩展性,介绍了 Puppet 的水平和垂直扩展。我们将探索一些常见的调优设置,并检查如何水平扩展 Puppet 服务。

第十二章,故障排除与分析,介绍了在使用 Puppet 时常见的故障排除案例。我们将重点讨论 Puppet 服务错误和清单编译错误,并检查日志文件的调优和配置。

为了充分利用本书的内容

对于有一定 Puppet 经验的用户,本书的收益最大,但每一课都是为了帮助任何阶段的学习者。为了跟随本书的内容,用户应该安装 Puppet Enterprise 的试用版,并将一些节点附加到 Puppet Master。每一章都会帮助配置 Puppet Master,使其能够利用现有的基础设施。

Puppet Enterprise 安装说明可以在 puppet.com/docs/pe/latest/installing_pe.html 找到。

下载示例代码文件

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

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

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

  2. 选择 SUPPORT 标签。

  3. 点击“代码下载与勘误”。

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

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

  • WinRAR/7-Zip for Windows

  • Zipeg/iZip/UnRarX for Mac

  • 7-Zip/PeaZip for Linux

本书的代码包也托管在 GitHub 上,地址为 github.com/PacktPublishing/Mastering-Puppet-5。如果代码有更新,它将在现有的 GitHub 仓库中进行更新。

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

下载彩色图片

我们还提供了一个包含本书中截图/图表彩色图片的 PDF 文件。您可以在这里下载:www.packtpub.com/sites/default/files/downloads/9781788831864_ColorImages.pdf

使用的约定

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

CodeInText:表示文本中的代码词汇、数据库表名、文件夹名称、文件名、文件扩展名、路径名、虚拟网址、用户输入和 Twitter 用户名。以下是一个示例:“Hiera 的早期版本(版本 3 或更早)使用的是单一的、完全全局的 hiera.yaml。”

一段代码如下所示:

lookup({
   'name'  => 'classification',
   'merge' => {
     'strategy'        => 'deep',
     'knockout_prefix' => '--',
   },
 })

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

lookup({
   'name'  => 'classification',
   'merge' => {
     'strategy' => 'deep',
     'knockout_prefix' => '--',
   },
 })

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

$ sudo /opt/puppetlabs/puppet/bin/gem install hiera-eyaml

粗体:表示一个新术语、重要单词或屏幕上显示的单词。例如,菜单或对话框中的词语会像这样出现在文本中。以下是一个示例:“我们可以通过点击屏幕左侧的 Manage Jenkins 进入插件页面。”

警告或重要提示如下所示。

提示和技巧如下所示。

联系我们

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

一般反馈:请通过电子邮件 customercare@packtpub.com 联系,并在邮件主题中提及书名。如果您对本书的任何内容有疑问,请通过电子邮件联系我们:customercare@packtpub.com

勘误表:虽然我们已尽力确保内容的准确性,但错误难免发生。如果您在本书中发现错误,我们将非常感激您向我们报告。请访问 www.packt.com/submit-errata,选择您的书籍,点击勘误表提交表格链接,并输入详细信息。

盗版:如果您在互联网上遇到我们作品的任何非法复制形式,我们将非常感激您提供相关地址或网站名称。请通过 copyright@packt.com 与我们联系,并提供相关材料的链接。

如果您有兴趣成为作者:如果您对某个话题有专业知识,并且有兴趣撰写或参与书籍的编写,请访问 authors.packtpub.com

评价

请留下评价。一旦你阅读并使用了这本书,为什么不在你购买书籍的网站上留下评价呢?潜在的读者可以看到并参考你的客观意见来做出购买决策,我们在 Packt 也能了解你对我们产品的看法,而我们的作者也能看到你对他们书籍的反馈。谢谢!

欲了解更多有关 Packt 的信息,请访问packtpub.com

第一章:编写模块

编写 Puppet 模块和清单是 Puppet 生态系统工作的真正核心。

所以,你可能已经为你的基础设施中的软件组件编写了至少几个模块,而且 Puppet 文档中有一个很好的入门指南:puppet.com/docs/pe/2017.3/quick_start_guides/writing_modules_nix_getting_started_guide.html,所以我不会浪费时间再讲解这些内容。但我敢肯定,在掌握 Puppet v5 的过程中,你真正想要做的是正确地编写这些模块。

让我们在本章中一起迈出提高模块质量的一步。我在过去几年里花了大量时间深入一线,收集来自欧洲一些最佳项目的最佳实践,并应用我从大学教育和 15 年以上的行业经验中学到的实践和软件原则。我希望能向你介绍一些捷径,让你的工作更轻松!

以下是我认为会真正帮助你走上高质量 Puppet 模块和清单正确路径的一些建议:

  • 使用合适的 IDE 和插件

  • 使用良好的模块类结构:

    • 遵循类命名约定

    • 为模块提供一个单一入口点

    • 使用高内聚和低耦合原则

    • 使用封装原则

    • 强类型化你的模块变量

  • 使用新的 Puppet 开发工具包命令:

    • 创建模块框架和元数据

    • 创建 init.pp

    • 创建更多的类

    • 验证你的模块

    • 对模块进行单元测试

  • 持续关注代码气味

  • 确保你没有在处理死代码

  • 与社区合作

  • 使用 Puppet Forge

  • 编写优秀的文档

  • 添加模块依赖

  • 为你的模块添加兼容性数据

    • 操作系统支持

    • Puppet 和 PE 版本支持

  • 使用新的 Hiera 5 模块级别数据

  • 将模板从 ERB 语法升级到 ERP 语法

现在,让我们依次检查这些最佳实践。

使用合适的 IDE 和插件

使用一个合适的文本编辑器并配合插件进行编写,绝对是提高质量的好步骤。有很多选项可以选择,最好选择适合你个人写作风格的工具。就我个人而言,我最成功地使用了 Atom(atom.io),并最近将其安装在我的工作站上。我多年前使用过 Eclipse(这个工具也曾被称为 Geppetto),不过我觉得它由于较大的内存占用显得笨重。保持一定的 Vim 熟练度也很不错,尤其是在服务器端的命令行操作,或者如果你在工作站上使用 Linux 操作系统的话。另外,还有适用于 macOS X 的 TextMate 编辑器,它拥有 Apple 风格的界面。

让我们来看一下作为 Puppet 开发者可用的各种 集成开发环境(IDE) 选项。

Vim

Vim (www.vim.org) 当然仍然是文本文件编辑的主流工具。它在 Unix 世界有着悠久的历史,是一个非常轻量级的命令行文本编辑器。Vim 可以说是最原始的文本编辑器之一。如果你有足够的内存并且有耐心学习各种键盘命令,它可以作为一个闪电般快速且高效的 IDE 使用。我的建议是从几个基本命令开始,每次使用 Vim 时都尽量学习一些新命令。

你可以优化你的 Vim,使其更适合编辑 Puppet 清单。假设你刚安装了一个全新的 Vim,并且已经安装了 Git,我们来看看如何操作。

进入你的主目录并使用以下命令克隆给定的仓库:

cd ~
git clone https://github.com/ricciocri/vimrc .vim
cd .vim
git pull && git submodule init && git submodule update && git submodule status
cd ~
ln -s .vim/.vimrc 

将仓库克隆到你的主目录的 .vim 目录中,将为你配置 Vim 设置。该仓库包含几个子模块,包含以下内容:

  • Pathogen (github.com/tpope/vim-pathogen) 是 Vim 大师 Tim Pope 的通用插件,它让你可以轻松管理 Vim 的 runtimepath,并将 Vim 插件和运行时文件安装到各自独立的目录中,避免文件冲突。

  • Vim-puppet (github.com/rodjek/vim-puppet) 是 Tim Sharpe 编写的原始 Vim 插件,使 Vim 更加适合 Puppet 开发。

  • snipmate.vim (github.com/msanders/snipmate.vim) 是一个 Vim 脚本,它实现了 TextMate 的一些代码片段功能。

  • Syntastic (github.com/vim-syntastic/syntastic) 是一个语法检查插件,它通过外部语法检查器检查文件,并显示任何出现的错误。这可以通过命令行的 pdk validate 命令完成,也可以在文件保存时自动进行检查。

  • Tabular (github.com/godlygeek/tabular) 用于根据 Puppet 风格指南对你的箭头符号 (=>) 进行对齐,以便通过运行 pdk validate 命令(我们稍后会详细介绍 pdk validate 命令)。

  • vim-fugitive (github.com/tpope/vim-fugitive) 提供了 Vim 的深度 Git 集成功能。

我不能保证这将是一个完美符合你个人 Vim 风格的 Vim 设置,但它肯定会让你走上正确的道路,并且你将安装 Pathogen,这样你就可以进一步调整你的 Vim 设置,直到它符合你的需求。

你可能还想在 GitHub 上 fork 这个仓库,这样你可以保存所有的设置并与团队共享。

TextMate

TextMate (macromates.com) 是一个仅适用于 macOS X 的编辑器,且有一个专门的 TextMate bundle 可用于编辑 Puppet 清单 (github.com/masterzen/puppet-textmate-bundle)。首先,安装 TextMate 和 Git(可以通过命令行开发者工具获取),然后按照以下命令设置 Puppet bundle:

$ mkdir ~/temp
$ cd ~/temp
$ git clone https://github.com/masterzen/puppet-textmate-bundle.git Puppet.tmbundle
$ mv ~/temp/Puppet.tmbundle ~/Library/Application\ Support/TextMate/Bundles/
$ rm -fr ~/temp

现在选择一个清单并用 TextMate 打开。在 TextMate 对话框中,选择 Puppet 并安装 Bundle,之后你就准备好开始了。

Atom

这是我基于个人风格推荐的 IDE,使用我的 MacBook 作为主机操作系统。Atom (atom.io) 是一个功能齐全的 IDE,被描述为 一个适用于 21 世纪的可黑客化文本编辑器,它包含了你期望的所有功能:跨平台、包(即插件)管理器、自动补全、文件浏览器、多个面板、查找和替换等。

GitHub 开发了 Atom,并且他们的目标是将一个完整的 IDE 的便利性与像 Vim 这样的经典复杂编辑器的深度可配置性结合起来。

Atom 上有成千上万的开源包,为其增加了新的功能,下面是我特别推荐用于 Puppet 开发的那些:

  • language-puppet(为 Puppet 文件添加语法高亮和片段)

  • linter-puppet-lint(为你的 Puppet 清单提供 linting 支持)

  • aligner-puppet(根据 Puppet 风格指南对胖箭头进行对齐)

  • erb-snippets(用于编写 Puppet ERB 模板的片段和快捷键)

  • linter-js-yaml(使用 JS-YAML 解析你的 YAML 文件)

  • tree-view-git-status(在树视图中显示文件的 Git 状态)

Visual Studio

如果你是 Windows 和 .NET 世界的开发者,那么不妨看看 Visual Studio Code 插件中的 Puppet 语言支持 (marketplace.visualstudio.com/items?itemName=jpogran.puppet-vscode)。

它包含了你在 Visual Studio IDE 中开发 Puppet 时所期望的所有功能:语法高亮、代码片段、文件验证、根据 Puppet 风格指南进行 linting、资源和参数的 IntelliSense、从 puppet resource 命令导入、节点图预览,现已集成 Puppet Development Kit (PDK)。

使用良好的模块和类结构

本节包含一系列关于良好的模块和类设计的建议。请记住,Puppet 开发本质上就像任何其他类型的软件开发一样,我们通过多年的软件开发经验,特别是在 O&O 软件公司,我们学到了一些模块化和类设计的原则,它们使我们的开发变得更好。我还认为,通向 基础设施即代码 的一部分是让我们的 Puppet 代码与其他应用程序代码一样设计良好、结构清晰且经过充分测试。

遵循类命名约定

在 Puppet 社区中,随着时间的推移,已经形成了一定的类命名约定,结构化类时考虑这些约定非常重要:

  • init.ppinit.pp 包含与模块同名的类,是模块的主要入口点。

  • params.ppparams.pp 模式(稍后将在本章中详细介绍)是一个优雅的小技巧,利用了 Puppet 的类继承行为。模块中的其他任何类都会继承 params 类,因此可以适当地设置它们的参数。

  • install.pp:与安装软件相关的资源应放在 install 类中。install 类必须命名为 <modulename>::install,并且必须位于 install.pp 文件中。

  • config.pp:与配置已安装软件相关的资源应放在 config 类中。config 类必须命名为 <modulename>::config,并且必须位于 config.pp 文件中。

  • service.pp:与管理软件服务相关的资源应放在 service 类中。服务类必须命名为 <modulename>::service,并且必须位于 service.pp 文件中。

对于配置为客户端/服务器风格的软件,请参见以下内容:

  • <modulename>::client::install<modulename>::server::install 将是分别放置在 clientserver 目录中的 install.pp 文件的类名

  • <modulename>::client::config<modulename>::server::install 将是分别放置在 clientserver 目录中的 config.pp 文件的类名

  • <modulename>::client::service<modulename>::server::service 将是分别放置在 clientserver 目录中的 service.pp 文件的类名

拥有一个单一的入口点来访问模块

init.pp 应该是模块的唯一入口点。这样,尤其是审查文档的人,以及查看 init.pp 代码的人,都能对模块的行为有一个完整的概览。

如果你有效地使用了封装并且使用了描述性的类名,那么仅通过查看 init.pp 你就可以很好地了解模块是如何实际管理软件的。

具有可配置参数的模块应该以单一方式并在此单一位置进行配置。唯一的例外是,例如,像 Apache 模块这样的模块,可能还需要配置一个或多个虚拟目录。

理想情况下,你可以通过一个简单的包含语句来使用你的模块,如下所示:

include mymodule

你也可以通过使用类声明来使用它,如下所示:

class {'mymodule':
  myparam => false,
}

Apache 虚拟目录 风格的配置方式将是使用你的新模块的第三种方式:

mymodule::mydefine {‘define1':
  myotherparam => false,
}

与此建议相反的反模式是,除了 init.pp 和你的定义类型外,还存在其他类,且这些类的参数期望被设置。

使用高内聚和低耦合原则

尽可能地,Puppet 模块应该由具有单一责任的类组成。在软件工程中,我们称之为高内聚性。软件工程中的内聚性是指一个模块的各个元素在多大程度上属于同一类别。尽量确保每个类只有一个责任,避免在类中随意混合不相关的功能。

使用封装原则

尽可能地,这些类应使用封装来隐藏实现细节,避免用户了解具体的资源名称。例如,你模块的用户无需了解个别资源的名称。在软件工程中,我们称之为封装。例如,在 config 类中,我们可以使用多个资源,但用户不需要知道所有细节。相反,他们只需要知道应该使用 config 类来正确配置软件。

将类包含在其他类中非常有用,尤其是在大型模块中,你希望提高代码的可读性。你可以将功能块移动到独立的文件中,然后使用 contain 关键字来引用这些分离的功能块。

请参阅 puppet.com/docs/puppet/5.3/lang_containment.html 网站,获取关于 contain 关键字的提醒。

提供合理且经过深思熟虑的参数默认值

如果绝大多数使用你模块的人都将使用某个特定参数设置,那么当然有必要为该参数设置默认值。

仔细考虑你的模块是如何使用的,并站在非专家用户的角度来审视自己的模块。

按照合理的顺序展示可用的模块参数,将常用的设置放在最前面,而不是随意的顺序(如字母顺序)。

强烈类型化你的模块变量

在 Puppet 的早期版本中(在版本 4 发布之前的版本),我们会创建没有定义数据类型的 class 参数,然后,如果我们非常友好的话,我们会使用 stdlib validate_<datatype> 函数来检查这些变量的适当值:

class vhost (
  $servername,
  $serveraliases,
  $port
)
{ ...

Puppet 4 和 5 内建了定义参数化类接受的数据类型的方法。请参阅以下示例:

class vhost (
  String  $servername,
  Array   $serveraliases,
  Integer $port
)
{ ...

使用新的 Puppet 开发工具包命令

一些可以提高 Puppet 开发质量的功能,例如 puppet-lintpuppet-rspec 和像 puppet module create 这样的命令,已经存在了一段时间,但之前,你必须自己去发现这些工具,安装它们,并弄清楚如何有效地使用它们。

Puppet 在 2017 年 8 月决定将这些工具整合到客户端,并通过新的 Puppet 开发工具包版本 1.0 使其变得易于使用。我清楚地记得,puppet-rspec 总是需要一些时间来设置并使其正常工作。现在一切都变得非常简单。

让我们快速浏览一下使用新版本 PDK 1.0 的模块开发流程。

  • 创建模块框架和元数据pdk new module 命令的运行方式与旧的 puppet module create 命令相同,具体如下:
$ pdk new module zope –-skip-interview

所以,只需使用模块的名称来创建 init.pp

$ pdk new class zope

这些命令现在可以完全避免在文本编辑器中使用片段来创建注释、声明和其他样板代码。

  • 创建更多类:使用相同的命令创建任何更多的类。请参见以下示例:
$ pdk new class params

验证您的模块

在工作过程中,您可以使用新的 pdk validate 命令(puppet.com/docs/pdk/1.0/pdk_reference.html#pdk-validate-command)来帮助检查模块是否能编译、是否符合 Puppet 风格指南,以及是否拥有有效的元数据:

$ pdk validate

对您的模块进行单元测试

提升模块质量的最重要的一件事就是对它们进行测试!测试确实是任何软件开发领域中软件质量保证的一个重要方面。在敏捷开发社区,我们已经对自动化测试坚持了超过 10 年!

Puppet RSpec(rspec-puppet.com/tutorial)已经让 Puppet 社区能够对其模块进行单元测试有一段时间了,但现在使用新版本 PDK 1.0 更加容易,因为所有设置都已准备好,您只需添加测试代码并运行测试。

从 Puppet 的角度来看,单元测试意味着 检查编译器的输出。编译后的关系资源目录中是否包含资源?它们的顺序是否符合预期,具体取决于传递的参数和/或存在的事实?

当你开始在 Puppet-RSpec 中编写测试时,最初看似你只是在用另一种类似 Ruby 的语言重写 Puppet 清单。然而,实际上远不止如此。如果模块的功能具有一定复杂性,例如测试 Puppet 模板生成的动态内容,支持多个操作系统,或根据传入的参数执行不同的操作,那么这些测试实际上形成了一张安全网,在编辑或添加新功能时可以防止回归,或者在重构或升级到新版本的 Puppet 时提供保护。

让我们继续前两个部分,并使用开发工具包对我们的模块进行单元测试。每当你使用pdk new class命令生成一个类时,PDK 会创建一个对应的单元测试文件。这个文件位于模块的/spec/classes文件夹中,已经包含了编写单元测试的模板(参见 rspec-puppet.com/tutorial)。你可以使用以下命令运行测试:

$ pdk test unit

注意代码异味

在你的 Puppet 代码库逐渐增大时,要特别注意代码异味!以下链接是一个研究项目,描述了一些 Puppet 代码异味,这是一种极限编程(XP)术语,指的是代码问题——通常意味着设计或实现不佳:www.tusharma.in/wp-content/uploads/2016/03/ConfigurationSmells_preprint.pdf

让我们快速回顾一下如何使用前面研究项目中使用的基于 Python 的Puppeteer工具:

  1. 确保你已经安装了最新的 Java SDK。

  2. 进入你的workspace目录~/workspace,并克隆以下 Git 仓库:

$ git clone https://github.com/tushartushar/Puppeteer
$ cd Puppeteer
  1. 下载 PMD 工具(github.com/pmd/pmd),并在 shell 脚本中更新路径。PMD 是一个可扩展的静态代码分析器,内置 复制粘贴检测器CPD)。

  2. 更新所有 Puppet 仓库存放的文件夹路径。

  3. 执行cpdRunner.sh shell 脚本,使用 PMD-CPD 工具进行克隆检测。

  4. 更新SmellDetector/Constants.py中的REPO_ROOT常量,它表示存放所有 Puppet 仓库的文件夹路径。

  5. 执行Puppeteer.py

  6. 使用puppet-lint分析 Puppet 仓库(可选)。

  7. 在设置好仓库根目录后,执行puppet-lintRunner.py

  8. Puppet-lint_aggregator/PLConstants.py中设置仓库根目录。

  9. 执行PuppetLintRules.py,它将生成所有分析项目的汇总总结。

处理死代码

另一个常见问题是,随着 Puppet 代码库的增长,代码中可能存在未使用的代码。但幸运的是,我们可以使用一个工具来解决这个问题。

puppet-ghostbuster 实质上将实际使用的内容(存储在 PuppetDB 中)与您认为正在使用的内容(在您的代码库目录中)进行比较。这给了您一个机会,删除和清理那些真正未使用的内容。从软件可维护性的角度来看,这是非常有益的。较小的代码库显然更便宜、更容易维护!

让我们快速了解如何使用这个 Ruby gem。

在您的环境变量中进行以下设置:

  • HIERA_YAML_PATHhiera.yaml 文件的位置。默认值为/etc/puppetlabs/code/hiera.yaml

  • PUPPETDB_URL:PuppetDB 的 URL。默认值为 http://puppetdb:8080

  • PUPPETDB_CACERT_FILE:您站点的 CA 证书。

  • PUPPETDB_CERT_FILE:由您站点的 Puppet CA 签名的 SSL 证书。

  • PUPPETDB_KEY_FILE:该证书的私钥。

按如下方式运行命令:

$ find . -type f -exec puppet-lint --only-checks ghostbuster_classes,ghostbuster_defines,ghostbuster_facts,ghostbuster_files,ghostbuster_functions,ghostbuster_hiera_files,ghostbuster_templates,ghostbuster_types {} \+

你可以向逗号分隔的项中添加或删除内容,以检查未使用的类、定义的类型、事实、文件、函数、Hiera 文件、模板和类型。

使用 Puppet Forge

也许不言而喻,当你编写 Puppet 模块时,完全没有必要重新发明轮子。在 Puppet Forge 上花几分钟时间(forge.puppet.com)真的可以为你节省几天的编辑工作。到目前为止,Forge 上已经有超过 5000 个模块,因此充分利用 Puppet 社区所做的所有辛勤工作是非常有意义的。首先在 Forge 上搜索该软件,很可能已经存在类似的模块。

根据我的经验,我发现通常总有一些几乎能够完成任务的东西。可能有一个模块(通常是一个不受支持且未经批准的模块),也许它能够执行你所需要的软件管理,但它只适用于 Ubuntu,而你正在使用的是 Red Hat。通常来说,更好的做法是对该模块进行分叉,不管它的状态如何,然后在此基础上进行修改,而不是从头开始。

与社区合作

我最好的描述这个最佳实践的方法是用反模式作为例子。

我曾遇到一个 Puppet 开发者,他会从头开始编写模块,然后将 Forge 模块中的代码行复制并粘贴到新模块中。从那时起,该模块完全存在于社区之外!它甚至不是一个分叉,因此将时间积累下来的社区更改集成进来变得非常麻烦。你必须挑选这些更改,以将功能集成到自己的模块中,而且你可能仍然会遇到回归问题。通常,最佳实践是至少要分叉 Forge 模块!这样你就能获得 Git 历史记录,通常这些记录包含了在制作该模块过程中所付出的思考和努力。

你看,如果你曾经读过那本伟大的书《教堂与集市:一位偶然革命者的 Linux 与开源思考》(www.amazon.com/Cathedral-Bazaar-Musings-Accidental-Revolutionary/dp/0596001088),你就会明白,Linux 导向的软件开发哲学通过一个集市,协作的工作方式,超越了将开发工作分割成一个教堂式的独立工作方式。好吧,这是我对这个开发者工作风格的看法。他是在教堂式工作,而不是集市式工作。实际上,你是在做出决定,把你的教堂团队与集市中的大量人员对抗,在我看来,这在互联网时代,作为项目管理并没有给你带来竞争优势,是不明智的。

有时,Forge 上的模块会有些过时。如果模块的元数据过时了,你可以通过使用 PDK 的new module命令 (puppet.com/docs/pdk/1.0/pdk_generating_modules.html#create-a-module-with-pdk) 来重新生成,并提交新的元数据。

当然,作为一个优秀的 Puppet 社区成员,做出的更好实践是将你所做的更改提交为 pull 请求,并为社区的工作做出贡献。

撰写优秀文档

另一个重要的建议是:简单地写出优秀的文档。作为开发者,我认为没有什么比需要深入代码才能理解一个模块如何工作更糟糕了;这就像必须掀开汽车引擎盖来理解如何驾驶汽车一样!

提高用英语表达技术思想的能力!我真的认为这是每个优秀开发者必须掌握的技能。

获取一个 Markdown 编辑器

Puppet 模块使用 markdown 格式来编写文档。因此,使用独立的 Markdown 编辑器或一些 IDE 插件是有意义的,这样你就可以恰当地创建高质量的文档。接下来是我们在本章中考虑过的代码 IDE 的选择,随之而来的对应 markdown 插件如下。

Vim

如果你是 vim 粉丝,可以使用 vim-instant-markdown 插件 (github.com/suan/vim-instant-markdown)。

TextMate

如果你喜欢 TextMate 的苹果风格,可以使用 TextMate markdown 包 (github.com/textmate/markdown.tmbundle)。

Atom

如果像我一样,你喜欢使用 Atom,你可以使用 Markdown Preview Plus 包 (atom.io/packages/markdown-preview-plus)。

Visual Studio

如果你是 Windows 和 .NET 世界的开发者,那么不妨试试 Markdown 编辑器扩展(marketplace.visualstudio.com/items?itemName=MadsKristensen.MarkdownEditor)。

独立的 Markdown 编辑器

如果你更喜欢使用独立的 Markdown 编辑器,我个人推荐 macOS X 上的 MacDown。以下是我为各种操作系统列出的独立 Markdown 编辑器的(非常)简短列表。

Remarkable

如果你使用 Linux,Remarkable 可能是最好的独立编辑器。它也支持 Windows。它的一些功能包括实时预览、导出为 PDF 和 HTML、GitHub Markdown、定制 CSS、语法高亮和快捷键。

MacDown

如果你更喜欢使用独立的 Markdown 编辑器,我可以推荐 MacDown for macOS X,它是免费的(开源)。它深受 Mou 启发,并且专为 web 开发者设计。它具有可配置的语法高亮、实时预览和自动补全功能。如果你正在寻找一个精简、快速、可配置的独立 Markdown 编辑器,这可能是适合你的选择。

添加模块依赖

编辑模块的 metadata.json 文件以添加模块依赖。请参见以下示例:

"dependencies": [
    { "name":" stankevich/python",
       "version_requirement":">= 1.18.x"
     }
  ]

name 键是需求的名称,即 "pe""puppet"version_requirement 键是 semver(semver.org)值或范围。参见以下示例:

  • 1.18.0

  • 1.18.x

  • >= 1.18.x

  • >=1.18.x <2.x.x

这些都是有效的 version_requirement 值。

之后,请使用新的 PDK 命令检查 metadata.json 文件的有效性,命令如下:

$ pdk validate metadata

添加模块依赖的好处是,当你运行 puppet module download 命令时,Puppet 会相应地下载所有模块依赖。

为你的模块添加兼容性数据

本节介绍了如何为适用于你所使用版本的 Puppet 或 Puppet Enterprise 及操作系统的模块添加兼容性数据。首先,编辑模块的 metadata.json 文件以添加兼容性数据。

操作系统支持

在模块的 metadata.json 中表达该模块支持的操作系统,示例如下:

"operatingsystem_support": [
       { "operatingsystem": "RedHat", },
       { "operatingsystem": "Ubuntu", },
]

预期会有 Facter facts operatingsystemoperatingsystemrelease。以下是一个更完整的示例:

"operatingsystem_support": [
       {
           "operatingsystem":"RedHat",
           "operatingsystemrelease":[ "5.0", "6.0" ]
       },
       {
           "operatingsystem": "Ubuntu",
           "operatingsystemrelease": [
               "12.04",
               "10.04"
           ]
       }
   ]

之后,请使用新的 pdk 命令检查 metadata.json 文件的有效性:

$ pdk validate metadata

Puppet 和 PE 版本支持

metadata.json 文件中的 requirements 键是一个外部需求的列表,格式如下:

"requirements": [ {“name”: “pe”, “version_requirement”: “5.x”}]

name 是需求的名称,例如 "pe""puppet"version_requirement 可以是一个 semver(semver.org)版本范围,类似于依赖项。

同样,你可以使用新的 PDK 命令检查 metadata.json 文件的有效性,命令如下:

$ pdk validate metadata

使用新的 Hiera 5 模块级数据

在模块编写过程中,我们已经使用了相当长一段时间的params.pp模式。模块中的一个类,按照惯例被称为<MODULENAME>::params,为其他任何类设置变量:

class zope::params {
  $autoupdate = false,
  $default_service_name = 'ntpd',

  case $facts['os']['family'] {
    'AIX': {
      $service_name = 'xntpd'
    }
    'Debian': {
      $service_name = 'ntp'
    }
    'RedHat': {
      $service_name = $default_service_name
    }
  }
}

如你所见,我们在这里使用了一些条件逻辑,这取决于os::family事实,从而可以适当地设置service_name变量。我们还暴露了autoupdate变量,并给它设置了默认值。

这个params.pp模式是一个优雅的小技巧,它利用了 Puppet 特有的类继承行为(在 Puppet 中通常不建议使用继承)。然后,模块中的任何其他类都继承自params类,以适当地设置它们的参数,如下例所示:

class zope (
  $autoupdate   = $zope::params::autoupdate,
  $service_name = $zope::params::service_name,
) inherits zope::params {
 ...
}

自从 Hiera 5 发布以来,我们能够大大简化模块的复杂性。通过使用基于 Hiera 的默认值,我们可以简化模块的主类,它们不再需要继承自params.pp。此外,你也不再需要在参数声明中显式地使用=运算符设置默认值。

让我们来看一下使用 Hiera 5 与params.pp模式等效的配置。

首先,为了使用这个新功能,需要在模块的metadata.json文件中将data_provider键设置为heira值:

...
"data_provider": "hiera",
...

接下来,我们需要将hiera.yaml文件添加到模块的根目录:

---
version: 5
defaults:
  datadir: data
  data_hash: yaml_data
hierarchy:
  - name: "OS family"
    path: "os/%{facts.os.family}.yaml"

  - name: "common"
    path: "common.yaml"

然后,我们可以将三个文件添加到/data目录中(请注意hiera.yaml文件中的datadir设置)。这三者中的第一个文件用于设置 AIX 的service_name变量:

# zope/data/os/AIX.yaml
---
zope::service_name: xntpd

第二个文件用于设置 Debian 的service_name变量:

# zope/data/os/Debian.yaml
zope::service_name: ntp

最后是公共文件,如果 Hiera 在查找service_name设置或autoupdate值时没有找到对应的操作系统文件,它会退回到此文件来查找其值:

# ntp/data/common.yaml
---
ntp::autoupdate: false
ntp::service_name: ntpd

我们将在第四章中更加详细地探讨 Hiera 5,Hiera 5

总结

在本章中,我们涵盖了很多内容,我介绍了一些最佳实践,你可以利用它们来编写更高质量的组件模块。

在下一章中,我们仍然会讨论 Puppet DSL 的开发,并将注意力转向两个特殊模块:角色和配置文件,它们可以帮助我们构建可重用、可配置、可重构的全站配置代码。

第二章:角色与配置文件

角色和配置文件模式在 Puppet 社区中成为了常识,起初是由 Craig Dunn 的开创性博客文章(www.craigdunn.org/2012/05/239/)普及的,并且迅速被社区的其他成员采纳。它现在已经成为一个广泛采用的模式或最佳实践。这是一种可靠的方法,用于构建可重用、可配置和可重构的全站配置代码,同时它也是处理基础设施接口的一种方式——使用封装抽象的 软件开发范式。

在模式发展之前,Puppet 语言本身只提供了两个抽象层级,如下所示:

但很快就清楚了,需要进一步的中间抽象来拆解、重构并澄清这两者。

让我们考虑一下整体任务:我们希望将类(及其对应的业务数据)分配给节点,并且我们希望以一种方式来实现,在这个抽象过程的每个阶段都能够封装并隐藏复杂性:从在整个基础设施中的上下文中查看节点,作为一个软件栈,并深入了解其中包含的技术组件及其配置,这些组件构成了该软件栈的一部分。

我见过节点仅使用这两层抽象的非常详细的定义方式。我也见过其他方法,例如使用基于 Hiera 的微型外部节点分类器ENC)。我曾帮助公司过渡到使用角色和配置文件模式,并且我曾使用 Puppet Enterprise 控制台和 Foreman 作为 ENC。我在 Puppet 代码中定义节点,使用 Hiera 的多种方式来辅助节点分类,甚至使用 PE 控制台 API 进行节点分类,因此我希望我在这个过程中能积累一些最佳实践,现在能够传授给你。

在本章中,让我们一起看看角色和配置文件模式,以及它是如何帮助你专业地管理基础设施并实现我们在Mastering Puppet 5中的下一个里程碑的。

模式总结

角色和配置文件模式在你的节点分类(在最高层)和组件模块(在最低层)之间增加了两个额外的抽象层,从而在你的 Puppet 模块中提供了三个抽象层。以下描述从最复杂到最简单:

  • 组件模块:这些是用于管理您业务的软件的模块。毫无疑问,您从 Forge 下载了一些(例如,puppetlabs/apache、puppetlabs/mysql、hunner/wordpress 等),也可能有一些是您为自己特定业务目的开发的模块。

我们已经在第一章中详细讨论过这些内容,编写模块,所以问题是:

  • 配置文件:一组封装的技术特定类,这些类使用一个或多个组件模块和相应的业务数据来配置解决方案堆栈的一部分。

  • 角色:一组封装的业务特定类,这些类包含配置文件,用于构建完整的系统配置。

这两层额外的抽象和改进的接口使得层次化的业务数据更容易集成,使系统配置更容易被业务人员和技术人员阅读,并且使得重构变得更加容易。

以下 UML 图展示了模式中各元素之间的关系,更加清晰:

从前面的图中,我们可以看到以下内容:

  • 一个节点恰好包含一个角色。

  • 一个角色包含一个或多个配置文件。

  • 配置文件由一个或多个组件模块和相应的层次化业务数据组成。

  • 组件模块由许多资源组成。

Puppet 资源应该已经对您非常熟悉,我们已经在第一章中讨论过组件模块,编写模块,所以在接下来的两个部分,我们将深入探讨配置文件角色部分的模式。

配置文件

首先,让我们后退一步,考虑一下我们想通过配置文件实现什么目标。

总体任务是产生可用的技术块,这些技术块可以像拼装积木一样组合在一起,形成我们今天业界所称的技术堆栈解决方案堆栈。最著名的堆栈示例是 LAMP 堆栈(Linux、Apache、MySQL、PHP),最近,Ruby 或 Python 有时取代了 PHP 成为主要的脚本语言。Node.js 在业界也被迅速采用。

考虑到 LAMP 堆栈,我们希望做的是为 Apache、MySQL 和 PHP 组件创建技术块。因此,配置文件是这些较小的技术块,它们最终将组成这些完整的解决方案堆栈。配置文件是我们拼接在一起的三个构建砖块,如下所示:

让我们看看这个 LAMP 堆栈,使用一些完全功能的 Puppet 领域特定语言DSL)代码:

# LAMP stack profiles

# apache profile
class profile::web::apache (
  String $directory = '/var/www',
  String $vhost,
) {
  include apache
  apache::vhost { $vhost:
    port    => '80',
    docroot => "/var/www/${vhost}",
  }
}

# mysql profile
class profile::db::mysql (
  String $username = '/var/www',
  String $password,
) {

  include mysql::server
  mysql::db{ 'mysqldb':
     user     => $username,
     password => $password,
     grant    => 'ALL',
  }
}

# php profile
class profile::programming::php
{
  class { '::php':
    ensure       => latest,
    manage_repos => true,
    fpm          => true,
    dev          => true,
    composer     => true,
    pear         => true,
    phpunit      => false,
    settings   => {
      'PHP/max_execution_time'  => '90',
      'PHP/max_input_time'      => '300',
      'PHP/memory_limit'        => '64M',
      'PHP/post_max_size'       => '32M',
      'PHP/upload_max_filesize' => '32M',
      'Date/date.timezone'      => 'Europe/Berlin',
    },
  }
}

如你所见,在这些类中,我们为 LAMP 堆栈中剩余的 AMP 部分创建了一个抽象,并封装了底层组件模块的功能。当然,Linux 已经安装好了!

配置文件最佳实践总结

以下是你在开发自己配置文件时应注意的最佳实践,以下 LAMP 栈为例:

  • 设计时使用 include 关键字

  • 使用子目录来组织合理且易读的配置文件类组

  • 通过参数、默认值和抽象来隐藏复杂性

  • 决定如何设置组件类的参数

  • 决定使用自动类参数查找或 lookup 函数

现在我们来逐一讨论这些最佳实践。

设计时使用 include 关键字

配置文件的单一接口应该是它们在使用 Puppet include 关键字的相应角色部分中的应用。写配置文件时请记住这一点。我们只需在任何需要安装 PHP 的角色中写入如下内容:

...
 include profile::programming::php
 ...

关于 Puppet 的 include 关键字:

  • 多个声明是可以的

  • 它依赖于外部数据来获取参数

语法:接受单个类名(例如,include apache)或类引用(例如,include Class['apache']

使用子目录来组织合理且易读的配置文件类组

我们正在使用组件模块 puppetlabs/apache、puppetlabs/mysql 和 mayflower/php,并将它们分别封装为配置文件 classes web::apache database::mysqlprogramming::php。你可以看到,我使用了一些合理的子目录和类名来反映它们对栈的贡献,即 webdbprogramming 子目录位置分别对应 Apache、MySQL 和编程配置文件。

通过参数、默认值和抽象来隐藏复杂性

你可以看到,在 Apache 配置文件中,我们已经相当大程度地隐藏了 vhost 定义类型的复杂性,因此你只需要提供 vhost 的名称作为字符串。此外,你可以覆盖根internet目录的值。我相信这个位置在所有 Linux 操作系统中都是相同的。接口大小的减少确实降低了复杂性,并提供了一个简单、整洁的抽象,如果你不需要多个 Apache vhost,这完全没问题。

决定如何设置组件类的参数

正如 Puppet 文档中关于角色和配置文件的说明所说(puppet.com/docs/pe/2017.3/managing_nodes/roles_and_profiles_example.html#the-rules-for-profile-classes),在如何设置提供给组件模块的参数时,存在一定的权衡,我们应该根据代码的可读性业务数据的灵活性需求来做出决策。

也就是说,如果我们始终为某个参数使用相同的值,我们可以将其硬编码(高度可读),我们可以根据例如事实来计算参数的值(相当可读且具有一定灵活性),或者我们可以在业务数据层次结构中查找参数的值(高度灵活)。

决定使用自动类参数查找还是使用lookup函数

对于前面最佳实践中的第三个考虑点,还有另一个关于如何将数据从业务数据层次结构传入profile类的决策:

  • 在这些profile中,我们使用了自动类参数查找(puppet.com/docs/puppet/5.3/hiera_automatic.html)从我们的业务数据层次结构中请求数据。使用profile的参数接口是一种可靠且广为人知的方式来查找profile的配置设置,并且允许更好地与外部工具集成,比如 Puppet Strings(github.com/puppetlabs/puppet-strings),这是一个基于 YARD 的(yardoc.org)文档提取和展示工具。

  • 当我们为profile类编写代码时,我们本来也可以省略所有参数,改用lookup函数:

$jenkins_port = lookup('profile::jenkins::jenkins_port', {value_type => String, default_value => '9091'})
$java_dist    = lookup('profile::jenkins::java_dist',    {value_type => String, default_value => 'jdk'})
$java_version = lookup('profile::jenkins::java_version', {value_type => String, default_value => 'latest'})
# ...

如果你不习惯自动类参数查找的自动化特性,这种方法是一个替代选择。我个人发现,做一个显式的数据查找,并在更强大的 Puppet DSL 中直接处理返回值,更让我感到舒适。早期版本的 Hiera 在追踪错误时经常令人困惑(puppet.com/blog/debugging-hiera),而这种方法确实有所帮助。你可以直接检查数据类型并进行进一步的验证。通过在profile中完整地写出查找键,我们可以在整个 Puppet DSL 代码库中全局grep它,从而在 Puppet 清单和为其提供服务的业务数据之间建立明确的联系:

grep -nr 'profile::web::apache::vhost*' .

然后,你可以使用新的Puppet lookuppuppet.com/docs/puppet/5.3/man/lookup.html)命令(之前是hiera命令行调用)。由于它是lookup函数的命令行等效物,你可以在调试时确信你得到了完全符合要求的业务数据值:

Puppet lookup ' profile::web::apache::vhost *' .

实际上,我对 YAML 作为一种语言本身也有一些问题(例如,参见 arp242.net/weblog/yaml_probably_not_so_great_after_all.html),而能够依赖于更明确的 Puppet DSL 的鲁棒性弥补了我在调试过程中认为 YAML 本身存在的弱点。

仔细阅读这篇博客文章:puppet.com/blog/debugging-hiera-redux,它更新了使用最新命令来调试 Hiera 的方法,当然也确保你至少在使用一个 YAML 解析器。

同时,请记住,Hiera 确实有它的局限性,特别是在大型且多样化的基础架构中(www.craigdunn.org/2015/09/solving-real-world-problems-with-jerakia)。

接下来,让我们看看模式中的更高层次抽象:角色

角色

让我们再退一步,考虑一下我们希望通过角色部分的模式实现什么。总的任务是将这些像积木一样的配置文件类组合成完整的技术堆栈,这些我们称之为角色,它们现在是我们完整模式的第二部分:

在这里,你可以看到我们已经将之前示例中的组合配置文件堆叠起来,生成了完整的技术堆栈。我们还使用了两个额外的共享配置文件:

  • profile::base被包含在所有机器中,包括工作站。它管理安全基准等,并使用条件逻辑处理特定操作系统的配置文件;例如,profile::base::ubuntuprofile::base::redhat等,按需使用。

  • profile::server被包含在所有提供网络服务的机器中,并配置诸如 NTP、防火墙、监控、日志记录等服务。

让我们再看一遍 Puppet DSL 中完整功能的 LAMP 堆栈作为例子:

# LAMP stack

class role::lamp {
  include profile::web::apache
  include profile::db::mysql
  include profile::programming::php
  include profile::server
  include profile::base
}

角色最佳实践总结

在开发你自己角色时,以下是你应该注意的最佳实践,以之前提到的 LAMP 堆栈为例:

  • 仅使用include关键字构建角色

  • 使用你业务的通用命名来命名角色

  • 决定节点角色的粒度

现在让我们逐一检查这些最佳实践。

仅使用include关键字构建角色

如同 Puppet 文档中关于角色的说明,规则(puppet.com/docs/pe/2017.2/r_n_p_full_example.html#the-rules-for-role-classes)中指出,角色应该做的唯一事情就是使用 Puppet 的include关键字声明配置文件类。也就是说,角色本身不应该拥有任何类参数。角色也不应该声明任何组件类或资源——这正是配置文件的目的。

使用你业务的通用命名来命名角色

角色的名称应该基于你业务中用于管理该类型节点的通用名称。因此,如果你通常称机器为Web 服务器,你应该使用诸如role::web这样的名称,而不是根据任何底层的配置文件技术命名,如web::apacheweb::nginx。这样可以增加抽象层次,隐藏配置文件代码的复杂性,符合良好的编程实践。

这种最佳实践的另一个优势是它能促进组织内部的沟通:测试人员、项目经理甚至业务人员可以理解角色的简单语言,而 Puppet 开发人员则更容易在更深层次的配置文件抽象级别进行沟通。

配置文件向角色暴露了适当的接口。相应地,角色也向你的 ENC 暴露了整洁的接口,这使得即使是技术背景较少的公司人员也能负责节点分类。

决定节点角色的粒度

你应该从完全细粒度的角色开始,每个角色只是包含它所包含的简单配置文件列表。

如果你有很多只略有不同的节点,可以开始引入更复杂的角色,每行仅包含一个配置文件,例如条件逻辑甚至嵌套角色。

摘要

在本章中,我们拓宽了编写 Puppet 模块的技能,涵盖了角色和配置文件模式,并参考了两个特殊案例,这些案例提供了一种可靠的方式来构建可重用、可配置和可重构的全站配置代码。

接下来,我们保持开发的思维方式,但看看如何处理那些可能需要扩展 Puppet 超出常规使用场景的边缘情况。

第三章:扩展 Puppet

Puppet 生态系统已有超过 10 年的历史,最初是用 Ruby 编写的。

尽管已经在将主代码库迁移到 Clojure 语言方面取得了很大进展(特别是主 Puppet 服务器和 PuppetDB 组件),但生态系统的某些部分仍然可以通过 Ruby 层进行访问,以便根据更高级的使用案例扩展 Puppet,具体如下:

  • 自定义 Facts

  • 自定义函数

  • Types 和 Providers

让我们逐一考虑这些内容,看看如何利用最初的一些基础知识以及稍后的 Ruby 代码的更深入理解,在客户端和服务器端扩展 Puppet。

自定义 Facts

自定义 Facts 是一种客户端技术,用于在执行代理运行期间从节点提取任意信息,并且可以在 Puppet 清单或模板中与任何其他分发的 facts 一起使用。Facts 在 Puppet 代理上执行。

创建和分发新的自定义 fact 的最佳方式是将其放置在一个模块中,放在lib目录的facter子目录中,然后通过pluginsync将其分发到代理机器。

这篇文档页面puppet.com/docs/puppet/5.3/plugins_in_modules.html#adding-plug-ins-to-a-module准确地展示了在模块中放置代码的位置,而同一文档中的puppet.com/docs/puppet/5.3/plugins_in_modules.html#installing-plug-ins部分则展示了pluginsync的技术细节。

下图展示了在正常目录请求之前的pluginsync过程。通常,通过使用 FQDN 在 Puppet 服务器上调用GET方法,然后启动pluginsync过程,将适当的 facts、types 和 providers 分发回代理:

你可以查看 Puppet 代理和 Puppet 服务器之间所有 HTTPS 通信的详细信息,访问puppet.com/docs/puppet/5.3/subsystem_agent_master_comm.html

大多数时候,我发现 fact 通常只是执行一个任意的命令行表达式,这也是一般理解 facts 的好方法:它们实际上是一个 Ruby 包装器,通常围绕一个命令行表达式,使其通过 Facter 对 Puppet 生态系统可用。

以下代码是一个不错的代码片段,可以作为进一步开发的模板:

# <modulepath>/lib/facter/mycustomfact.rb
Facter.add(:mycustomfact) do
   confine :kernel => "Linux"
   ...
   myvar = Facter::Core::Execution.exec("foo")
  ...
 end

确保你适当地confine你的事实。没有什么比当你为基础设施引入一个新操作系统时,发现因为它们没有使用特定的命令语法而执行失败的事实更糟糕了。或者,如果我们突然引入一批 Windows 节点,结果发现 Windows 当然不理解大多数 Linux 命令,那该怎么办?

在编写自定义事实时,请记住这一点。

调试事实

你可以通过在自定义事实的 Ruby 代码中使用facter.debug语句来调试 Facter,如下所示:

Facter::Type.newtype(:mycustomfact) do
   ...
   Facter.debug "foo is the value: #{foo}"
   ...
 end

在调试过程中,单独运行 Facter 不会识别你新增的自定义事实,因为它通常需要pluginsync过程来分发。你必须设置FACTERLIB环境变量,以便在开发和调试新代码时简化这个过程。假设你在个人工作目录中有some_factssome_other_facts子目录,里面是你正在编辑的新事实的 Ruby 代码。你可以按如下方式设置代码:

$ ls ~/some_facts
 mycustomfact.rb
$ ls ~/some_other_facts
 myothercustomfact.rb
$ export FACTERLIB="~/some_facts: ~/some_other_facts"
$ facter mycustomfact myothercustomfact –debug

自定义函数

自定义事实允许我们在客户端运行任意代码。这是一个服务器端技术,帮助你编译目录。函数在 Puppet 服务器上执行。Puppet 已经包含了多个内置函数,额外的函数可以通过 Puppet Forge 模块获得,尤其是stdlib模块(参见forge.puppet.com/puppetlabs/stdlib)。

事实上,有三种可能的方式来创建自定义函数,尽管你不太可能使用前两种方式,所以我会提供一些链接到 Puppet 文档,以供参考这些选项:

创建和分发新自定义函数的最佳方式是将其放在模块中,在lib目录下的puppet/functions/<modulename>子目录中,然后通过pluginsync进行分发,如下所示:

#<modulepath>/lib/puppet/functions/mymodule/myfunction.rb
Puppet::Functions.create_function(:'mymodule::myfunction') do
  dispatch :up do
    param 'String', :a_string
  end
  def up(a_string)
    a_string.upcase
  end
end

类型和提供者

Puppet 已经有一个非常丰富的内置资源类型词汇表(请参见 puppet.com/docs/puppet/5.3/type.html),这些词汇表也通过额外的模块得到了扩展。Windows 特定的资源类型就是 Puppet 成功扩展其资源类型的一个很好的例子(请参见 puppet.com/docs/puppet/5.3/resources_windows_optional.html)。

以下是一些你可能需要考虑编写类型和提供程序,作为 Puppet DSL 中常规模块和清单的替代方案的迹象:

  • 你的 Puppet DSL 中有多个 exec 语句,带有复杂的 onlyifunless 条件属性

  • Puppet 在以下情况的处理并不理想:

    • 你的 Puppet DSL 不是一个足够强大的 API,你需要访问纯 Ruby 来操作数据。

    • 你的 Puppet DSL 代码具有复杂且相当复杂的条件逻辑。

类型

按照以下步骤创建你的类型:

  1. 创建并分发类型

  2. 添加 namevar 特殊属性

  3. 添加额外的类型属性

  4. 添加可选的ensure属性

  5. 添加类型参数

  6. 设置属性和参数的默认值

  7. 使用验证块检查输入值

  8. 检查输入值是否与newvalues数组匹配

  9. 使用 munge 检查数据类型兼容性

  10. 使用AutoRequire来处理隐式关系

  11. 使用Arrays来列出属性的值。

  12. 使用desc方法添加内联文档

查看官方文档页面上的 Puppet 类型:puppet.com/docs/puppet/5.3/custom_types.html。Gary Larizza 的博客也提供了一组有用的类型示例:garylarizza.com/blog/2013/11/25/fun-with-providers/

现在,让我们逐步查看每个步骤,在接下来的部分中更详细地创建你的新类型。

创建并分发类型

创建和分发一个新的自定义类型的最佳方式是将其放入模块中,放在 lib 目录下的 puppet/type 子目录中,然后通过 pluginsync 将其分发到代理机器,就像我们在上一节中看到的自定义事实一样。

文件名应与正在开发的类型名称相匹配,如下方代码所示:

 <modulepath>/lib/puppet/type/mynewtype.rb

 Puppet::Type.newtype(:mynewtype) do
 ...
 end

添加 namevar 特殊属性

在使用类型的特殊属性后,也就是它的namevar,我们就可以实际使用 Puppet DSL 来声明我们的资源。namevar 应该唯一地标识底层操作系统中的资源,并且必须是可以预先指定的,如下方代码所示:

Puppet::Type.newtype(:mynewtype) do

   mynewparam(:name, :namevar => true) do
   end

 end

现在,我们可以在 Puppet DSL 中声明我们的资源。在这种情况下,namevar 默认为资源标题,如下方代码所示:

mynewtype { ‘foo': }

资源标题用于在 Puppet 目录中唯一地引用该资源。因此,namevar 表示底层系统中该资源的名称,如以下代码所示:

mynewtype { 'foo':
   name => 'bar',
 }

然后,运行以下命令:

$ puppet apply -e "mynewtype { 'foo': }"
notice: Finished catalog run in 0.09 seconds

添加额外的类型属性

类型属性是反映底层操作系统中该资源当前状态的属性。

在 Puppet 执行过程中,这些值会被积极执行,因此它们应该是可发现的可更新的。如果属性不能更新,它可以作为只读属性来实现。在以下代码中,我们正在扩展示例类型的接口以定义一个版本属性:

Puppet::Type.newtype(:mynewtype) do
   ...
   mynewproperty(:version) do
   end
   ...
 end

现在我们开始在 Puppet DSL 中使用该属性,如以下代码所示:

mynewtype{ 'foo':
   version => '2.2',
 }

但它还无法使目录编译,因为在任何相应的提供者中都没有该属性的实现,如以下命令所示:

$ puppet apply -e "mynewtype { 'foo': version => '2.2' }"
err: /Stage[main]// Mynewtype[foo]: Could not evaluate: undefined method 'version' for nil:NilClass
notice: Finished catalog run in 0.04 seconds

添加可选的 ensure 属性

尽管是可选的,大多数原生 Puppet 资源类型确实有 ensure 属性,但也有一些例外——例如 . execnotify。你只需通过立即调用 ensurable 来为资源类型添加 ensure 属性:

 Puppet::Type.newtype(:mynewtype) do
   ensurable
   ...
 end

该类型的相应提供者随后将通过使用 createexists?destroy 方法实现 ensure 属性。

在 Puppet DSL 中,ensure 属性应该是资源中的第一个属性(根据 Puppet 风格指南),它支持 presentabsent 关键字(present 是默认值,因此为了简洁可以省略),如以下代码所示:

mynewtype { 'foo':
   ensure => absent,
 }

添加类型参数

Type 参数不同于属性,因为它们不直接与底层系统上的实际可发现和可更新资源相关联。相反,它们执行以下两种操作之一:

  • 允许你为与底层系统上的属性和资源交互提供额外的信息上下文

  • 提供一个抽象层,允许你覆盖底层系统上的预期行为

让我们使用 newparam 方法向我们的新类型添加一个 source 参数:

Puppet::Type.newtype(:mynewtype) do
   ...
   newparam(:source) do
   end
   ...
 end

设置属性和参数默认值

假设我们想要添加一个额外的 override 参数,我们希望它的默认值为 false。以下是表达该功能的 Ruby 代码:

Puppet::Type.newtype(:mynewtype) do
   ...
   newparam(:override) do
     defaultto :false
   end
   ...
 end

使用验证块检查输入值

我们可以使用 validate 块和例如 regex 表达式来验证新属性 version 的提供值,如以下代码所示:

Puppet::Type.newtype(:mynewtype) do
   ...
   newproperty(:version) do
     validate do |value|
       fail("Invalid version specified") unless value =~
         /^(\d+\.)?(\d+\.)?(\*|\d+)$/
     end
   end
   ...
 end

将输入值与 newvalues 数组进行检查

我们还可以使用 newvalues 方法,用值数组验证属性提供的值,如以下代码所示:

Puppet::Type.newtype(:mynewtype) do
   ...
   newparam(:override) do
     defaultto :true
     newvalues(:true, :false)
   end
   ...
 end

使用 munge 检查数据类型兼容性

为了决定是否更新底层提供程序的属性,会对提供的值与通过提供程序获取的值进行简单的相等性比较。

munge方法可以确保用户提供的数据与预期返回的提供程序数据类型一致。例如,我们可以调用munge方法,确保用户提供的integernumeric string数据类型与提供程序要求的integer兼容,如以下代码所示:

Puppet::Type.newtype(:mynewtype) do
   ...
   newparam(:identifier) do
     munge do |value|
       Integer(value)
     end
   end
   ...
 end

使用autorequire来处理隐式关系

为了使您的类型用户更方便,您可以使用autorequire来避免在资源之间冗长地指定许多显式关系。autorequire方法在目录中建立了资源之间的隐式排序。一个典型的例子是文件资源依赖于其父目录。

例如,在我们的类型中,如果source参数是文件路径,那么我们应该确保首先管理相应的file资源,如以下代码所示:

Puppet::Type.newtype(:mynewtype) do
   ...
   autorequire(:file) do
     self[:source]
   end
   ...
 end

在 Puppet DSL 中手动指定的依赖关系比通过autorequire方法建立的隐式依赖关系具有更高的优先级。

使用数组将值列表分配给属性

当属性的预期值是数组时,array_matching选项应包含在调用newproperty时,并且其值应为all。然后,数组中的所有值都将用于该属性,如以下代码所示:

Puppet::Type.newtype(:mynewtype) do
   ...
   newproperty(:myarray, :array_matching => :all) do
   end
   ...
 end

使用desc方法添加内联文档

您的新类型的用户可以使用puppet describepuppet doc命令来获取您配置的内联文档。要获取当前环境中所有已配置类型的完整描述,包括自定义资源,请运行以下命令:

$ puppet describe –list

现在,让我们通过使用desc方法添加一些内联文档来完成我们的类型示例:

Puppet::Type.newtype(:mynewtype) do

   ensurable

   newparam(:override) do
     desc 'whether or not to override'
     defaultto :true
     newvalues(:true, :false)
   end

   newproperty(:version) do
     desc 'the version to use for mynewtype'
     validate do |value|
       fail("Invalid version") unless value =~
         /^(\d+\.)?(\d+\.)?(\*|\d+)$/
     end
   end

   newparam(:identifier) do
     desc 'the identifier for mynewtype'
     munge do |value|
       Integer(value)
     end
   end

 end

提供程序

提供程序是资源在系统上的实现。类型表达了描述资源时使用的接口,而提供程序则提供了资源如何与底层系统交互的实现。

接口与其实现之间的分离使得可以为一个类型开发多个提供程序。

作为 Puppet 安装的一部分提供的package类型,包含许多与系统交互的独立提供程序,例如rpmaptyumzipperchocolatey等。开发新的提供程序所需的唯一条件是它遵循在其类型中定义的接口。

您可以查看 Puppet 提供者的官方文档页面:puppet.com/docs/puppet/5.3/custom_types.html#providerspuppet.com/docs/puppet/5.3/provider_development.html。Gary Larizza 的博客也提供了一组有关提供者的有用示例:garylarizza.com/blog/2013/11/26/fun-with-providers-part-2/

按照以下步骤为您的类型创建新提供者:

  1. 创建并分发您的提供者

  2. 以下方式可以指示提供者是否适合该类型:

    • 使用confine方法

    • 使用defaultfor方法

    • 使用commands方法

  3. 实现ensure属性

    • 使用exists?方法

    • 使用createdestroy方法

  4. 使用GETSET方法来管理类型属性

  5. 实现self.instances方法

接下来,我们将详细介绍创建新提供者的每个步骤。

创建并分发提供者

创建和分发新提供者的最佳方式是将其放入相同模块中,位于lib目录下的puppet/provider/<typename>子目录中,然后通过pluginsync将其分发到代理机器。请注意,文件名应与提供者的名称匹配,如下代码所示:

# <modulepath>/lib/puppet/provider/mynewtype/myprovider.rb

 Puppet::Type.type(:mynewtype).provide(:myprovider) do
   ...
 end

指示提供者是否适合该类型

confinecommands方法用于确定哪些提供者对该类型有效,而defaultfor方法用于在有多个提供者时指示默认提供者。我们来看看这些方法的使用。

使用confine方法

confine方法可以与 fact 一起使用,如下代码所示:

Puppet::Type.type(:mynewtype).provide(:myprovider) do
   ...
   confine :osfamily => :redhat
   ...
 end

confine方法也可以使用exists来基于系统管理下是否存在特定文件作为条件。以下示例演示了如何将提供者限制为仅在 Puppet 的.config文件存在的系统上:

Puppet::Type.type(:mynewtype).provide(:myprovider) do
   ...
   confine :exisits => Puppet[:config]
   ...
 end

另一种可能性是基于某些 Puppet 特性来设置confine方法的条件(这些特性都列在源码目录的github.com/puppetlabs/puppet/tree/master/lib/puppet/feature中),如下代码所示:

Puppet::Type.type(:mynewtype).provide(:myprovider) do
   ...
   confine :feature => :selinux
   ...
 end

最后,confine可以接受一个布尔表达式来限制您的提供者,如下代码所示:

Puppet::Type.type(:mynewtype).provide(:myprovider) do
   ...
   confine :exisits =>  Puppet[:config]
   ...
   confine :true => begin
     if File.exists?(Puppet[:config])
       File.readlines(Puppet[:config]).find {|line| line =~ /^\s*\[agent\]/ }
     end
   end
   ...
 end

使用defaultfor方法

confine方法很好,但其使用可能仍会导致特定资源类型有多个有效的提供者。在这种情况下,类型应该使用defaultfor方法指定其首选提供者。

defaultfor方法使用事实名称和值作为其参数,随后用于确定某些类型底层系统的默认提供者。

例如,在 Red Hat 系统上,yumrpm都可以作为包资源类型的有效提供者,但defaultfor方法将用于指示对于 Red Hat 系统,yum实际上是默认提供者,如以下代码所示:

Puppet::Type.type(:mynewtype).provide(:yum) do
   ...
   confine :osfamily =>  :redhat
   defaultfor: osfamily => :redhat
   ...
 end

使用commands方法

限定提供者也可以基于系统路径中某些命令的可用性,通过commands方法来实现。

更重要的是,通过使用commands生成的特殊方法,我们还可以告知 Puppet 与底层系统交互的正确命令。这比使用 Ruby 自带的命令执行方法(如%x{cmd}cmd)更为可取,原因如下:

  • 当设置--debug标志时,Puppet 会显示以这种方式调用的命令

  • 它们作为提供者的要求已被记录

  • 异常通过抛出Puppet::ExecutionFalure一致地处理

以下代码展示了这一点:

Puppet::Type.type(:mynewtype).provide(:yum) do
   ...
   commands :yum => 'yum', :rpm => 'rpm'
   ...
 end

实现ensure属性

为了实现ensure属性,提供者需要能够判断资源是否存在,在不存在时创建资源,并在存在时销毁资源。这是通过exists?createdestroy方法实现的,接下来的部分将会详细介绍这些方法。

使用exists?方法

exists?方法检索资源的ensure状态。返回布尔值,如以下代码所示:

Puppet::Type.type(:mynewtype).provide(:yum) do
   ...
   confine :osfamily =>  :redhat
   defaultfor: osfamily => :redhat
   ...
   def exists?
     begin
       rpm('-q', resource[:name])
     rescue Puppet::ExecutionFailure => e
       false
     end
   end
   ...
 end

使用createdestroy方法

资源的存在状态通过参考用户在 Puppet DSL 中声明的带有ensure属性的资源声明,使用createdestroy方法进行修改。

当满足以下两个条件时,调用create方法:

  • 在资源声明中,ensure属性已设置为present

  • false值由exists?方法返回(表示资源不存在)

当满足以下两个条件时,调用destroy方法:

  • 在资源声明中,ensure属性已设置为absent

  • true值由exists?方法返回(表示资源已存在)

以下代码展示了如何使用这些方法:

Puppet::Type.type(:mynewtype).provide(:yum) do
   ...
   def create
     package=resource[:version] ?
       “#{resource[:name]}-#{resource[:version]}]” : resource[:name]
     yum(‘install', ‘-y, package')
   end
   ...
   def destroy
     yum(‘erase', ‘-y', resource[:name])
   end
   ...
 end

使用GETSET方法来管理类型属性

类型中定义的每个属性应在提供者中实现GETSET方法。

Puppet 随后会在运行期间调用这些方法来管理属性,如下所示:

  1. GET方法最初被调用以检索当前值

  2. 这随后与用户在 Puppet DSL 中声明的值进行比较

  3. 如果值不同,则调用SET方法根据需要更新该值。

这在以下代码中有所展示:

Puppet::Type.type(:mynewtype).provide(:yum) do
   ...
   def version
     version = rpm('-q', resource[:name])
     if version =~ /^#{Regexp.escape(resource[:name])}-(.*)/
       $1
     end
   end

   def version=(value)
     yum('install', "#{resource[:name]}-#{resource[:version]}")
   end
   ...
 end

实现self.instances方法

Puppet 提供了额外的操作模式,即使用 puppet resource 命令来发现资源。self.instances 方法应实现返回提供程序能够在底层系统上找到的特定资源类型的任何实例。

以下示例演示了如何使用 rpm -qa 命令查询底层 Red Hat 系统上安装的所有软件包:

Puppet::Type.type(:mynewtype).provide(:yum) do
   ...
   def self.instances
     pkgs = rpm('-qa','--qf','%{NAME} %{VERSION}-%{RELEASE}\n')
     pkgs.split("\n").collect do |entry|
       name, version = entry.split(' ', 2)
       new( :name => name,
         :ensure => :present,
         :version => version
       )
     end
   end
   ...
 end

self.instances 返回的每个资源都会将属性存储在 @property_hash 实例变量中。提供程序中的所有其他方法都可以访问该属性哈希,因此我们可以以更简单的方式实现 exists?version 方法,如以下代码所示:

Puppet::Type.type(:mynewtype).provide(:yum) do
   ...
   def exists?
     @property_hash[:ensure] == :present
   end

   def version
     @property_hash[:version]
   end
   ...
 end

总结

在本章中,我们探讨了如何通过客户端事实、服务器端自定义函数以及自定义类型和提供程序来扩展 Puppet。你可以看到,凭借一些 Ruby 知识,你可以轻松地扩展 Puppet 生态系统,以满足你自己独特的需求。

在下一章中,我们将探讨 Hiera 5,我们将使用它在代码与业务数据之间创建分离。

第四章:Hiera 5

Hiera 5 现在已成为 Puppet 生态系统的一个完全成熟的成员。我们已经使用 Hiera 好几年了,用于实现 Puppet 代码和配置数据之间的所谓关注点分离。本质上,Hiera 使我们能够将如何做(Puppet 模块和清单)与做什么(配置数据)分离。这使我们能够将所有特定站点和业务的数据与我们的清单分开,从而使我们的 Puppet 模块更加可移植。我记得很久以前,Kelsey Hightower 在 Puppet 社区中首次给我们做了一个关于将清单与数据分离的演讲。好吧,Hiera 5 在这个版本中终于成熟了,现在可以让我们完全掌握这一基础设施设计方面的工作。

Hiera 提供了一个用于配置数据的键/值查找功能,允许外部查找值,然后将这些数据暴露给 Puppet DSL,从而传递给 Puppet 编译器。Hiera 数据保存在一个可插拔的数据库中,通常只包含简单的文本文件。我们应该努力实现的是设计一个数据层次结构,基本上通过我们的服务器类别级联。Hiera 然后会在这个层次结构中的所有层级中搜索,将所有结果合并成一个单一值、数组或哈希。

尽管 Hiera 通常具有可插拔设计,但 Hiera 数据的来源是用易读的 YAML 编写的。这意味着 Puppet 开发人员通常不需要始终参与站点配置,因此一些服务器配置现在可以由你组织中的其他、技术要求较低的专业人员来完成。

代码和数据的关注点分离

Hiera 将 Puppet DSL 与业务数据分开,使我们能够反复使用一些相同的通用 Puppet DSL。事实上,大多数组织使用的 Puppet DSL 中,约 80% 是完全通用的;只有业务数据有所不同。Hiera 使我们能够实现功能和业务数据之间的完全关注点分离,而是将业务数据方便地作为参数传递给我们的模块。

Hiera 的工作原理是首先在最广泛的范围内设置业务值(即在整个站点范围内,或在 Puppet 术语中称为common),然后向上移动层次结构,在适当的级别覆盖这个全局值。

专门针对基础设施的数据非常适合使用层次模型。基础设施通常由一组可配置的属性组成:IP 地址、端口、主机名和 API 端点。我们在基础设施中配置了大量设置,其中大多数最好通过层次结构表示。

很多基础设施数据都有一个默认值,例如,数据中心使用的 DNS 解析器。你首先将其作为键值对设置在 common.yaml 数据文件中。在 Puppet 初次安装后,hiera.conf 中的层次结构哈希最初仅提供这个常见(默认)级别:

---
version: 5
hierarchy:
  - name: Common
    path: common.yaml
defaults:
  data_hash: yaml_data
  datadir: data

引入环境框架

这是 Hiera 的一个典型场景:你可能需要覆盖开发环境的 DNS 设置,因为该环境无法连接到网络中的生产解析器。然后你在第二数据中心部署生产环境,并且需要该位置有所不同。Hiera 允许我们建模像是 生产 DNS 解析器是 10.20.1.3,而开发 DNS 服务器是 10.199.30.2 的设置。

为了适应这种场景,我们可以在 Hiera 层次结构中引入最好的环境 框架,如下所示:

---
version: 5
 hierarchy:
   - name: "Per-node data"
     path: "nodes/%{trusted.certname}.yaml"

   - name: "Per-environment data"
     path: "%{server_facts.environment}.yaml"

 - name: Common
     path: common.yaml

百分号括号 %{variable} 语法表示 Hiera 插值标记。无论你在哪里使用这些插值标记,Hiera 都会评估变量的值,并将其适当地插入到层次结构中。

请查看 Puppet 文档,了解 Hiera 5 配置语法的具体细节:puppet.com/docs/puppet/5.3/hiera_config_yaml_5.html#config-file-syntax-hierayaml-v5

如果我们使用数据 datadir 并且默认使用 YAML 后端,我们可以完全省略 defaults 哈希,因为这些是默认设置。

一个更完整的层次结构

我们只处理简单的层次结构,因此我们可以构建一个最能代表我们基础设施的层次结构,例如以下内容,而不是在 Puppet DSL 中编写复杂的条件语句来确定如何解析 DNS 解析器:

这个示例层次结构将通过以下 hiera.yaml 表示:

---
version: 5
  hierarchy:
    - name: "Per-node data"
      path: "nodes/%{trusted.certname}.yaml"

    - name: "Per application data"
      path: "%{facts.application}.yaml"

    - name: "Per environment data"
      path: "%{server_facts.environment}.yaml"

    - name: "Per datacenter data"
      path: "%{facts.datacenter}.yaml"

    - name: "Common data"
      path: common.yaml

factstrustedserver_facts 哈希是最有用的可在 hiera.yaml 中插值的哈希。

注意,如果你需要引用节点的 fqdn,请使用 trusted.certname。为了引用节点的环境,server_facts.environment 事实是可用的。

请查看 Puppet 文档,了解关于 Hiera 插值的更多细节:puppet.com/docs/puppet/5.3/hiera_merging.html#interpolation

Hiera 5 概要

让我们现在一步步来看一下 Hiera 3 和 Hiera 5 之间的一些关键区别,具体如下:

  • 全局、环境和模块层

  • 加密的 YAML 后端

  • 查找函数

  • 调试 Hiera

全局、环境和模块层

Hiera 的早期版本(版本 3 或更早)使用单一的、完全全局的 hiera.yaml。由于其层次结构完全是全局的,实际上不可能在不同时改变所有环境的情况下更改它。环境通常用于控制代码更改,因此这使得单一的 hiera.yaml 文件非常不适合。Hiera 5 使用三层配置和数据:

  • 全局层:

    • 在 Hiera 3 中,这是唯一的层次

    • 对于非常临时的覆盖非常有用,例如,当你的运维团队必须绕过常规的变更流程时

    • 仍然支持遗留的 Hiera 3 后端——因此在迁移到 Hiera 5 的过程中可以继续使用。

    • 现在应该避免使用这个层。所有常规数据应该在环境层中指定。

  • 环境层:

    • 环境层现在是大多数 Hiera 数据定义发生的地方。

    • 在环境中的所有模块可用

    • 覆盖模块层

  • 模块层:

    • 正如我们在第一章《模块编写》中讨论的那样,模块层现在可以为模块的类参数配置默认值和合并行为。这是一个非常方便的替代方案,代替使用params.pp模式。

    • 为了实现我们习惯的params.pp模式的相同行为,建议使用default_hierarchy设置,因为这些绑定在合并中并不包含。

    • 在环境层中的数据集会覆盖模块作者配置的默认数据。

加密的 YAML 后端

在 Puppet 4.9.3 中,添加了hiera-eyaml后端到 Hiera 功能中,这样你就可以存储加密的数据值。这样,你现在可以隐藏所有的秘密值,如密码、证书等,而不是在 Hiera 数据文件中使用纯文本。接下来,让我们一步步介绍如何启用这一功能。

安装 hiera-eyaml

使用 Puppet Server 设置eyaml,可以通过以下命令安装hiera-eyaml gem:

$ sudo /opt/puppetlabs/bin/puppetserver gem install hiera-eyaml

你还需要第二次使用以下命令安装 Ruby gem:

$ sudo /opt/puppetlabs/puppet/bin/gem install hiera-eyaml

创建加密密钥

使用eyaml createkeys命令创建公钥和私钥,如下所示:

$ eyaml createkeys

此命令将在默认的./keys目录中创建公钥和私钥,并使用默认名称。

安全地存储加密密钥

现在让我们将两个密钥复制到/etc/puppetlabs/puppet/eyaml目录,并设置适当的权限,赋予 Puppet 用户所有权,并排除所有其他用户无法访问这两个密钥:

$ mv -t /etc/puppetlabs/puppet/eyaml ./keys/*.pem
$ chown -R puppet:puppet /etc/puppetlabs/puppet/eyaml
$ chmod -R 0500 /etc/puppetlabs/puppet/eyaml
$ chmod 0400 /etc/puppetlabs/puppet/eyaml/*.pem
$ ls -lha /etc/puppetlabs/puppet/eyaml
-r-------- 1 puppet puppet 1.7K Apr 25 08:08 private_key.pkcs7.pem
-r-------- 1 puppet puppet 1.1K Apr 25 08:08 public_key.pkcs7.pem

更改 hiera.yaml

hiera.yaml中进行以下设置,以启用hiera-eyaml后端,并提供对密钥和数据文件的访问:

  • 设置lookup_key属性为eyaml_lookup_key,以便使用新的eyaml后端。

  • 将加密密钥的位置添加到options哈希中

  • 将所有的文件路径更改为eyaml,而不是 YAML 文件扩展名:

---
 version: 5
 hierarchy:
   - name: "Encrypted and regular data"
     lookup_key: eyaml_lookup_key    paths:
       - “nodes/%{trusted.certname}.eyaml”
       - “%{facts.application}.eyaml”
       - “%{server_facts.environment}.eyaml”
       - “%{facts.datacenter}.eyaml”
       - "common.eyaml"
     options:
       pkcs7_private_key: /etc/puppetlabs/puppet/eyaml/private_key.pkcs7.pem
       pkcs7_public_key:  /etc/puppetlabs/puppet/eyaml/public_key.pkcs7.pem
 defaults:
   datadir: data

使用此配置,你可以将加密和纯文本的密钥和值存储到eyaml数据文件中。

查找函数

值得一提的是,我们现在应该在 Puppet DSL 中使用新的lookup()函数来获取 Hiera 值。lookup()函数替代了现在已废弃的一组 Hiera 函数:

  • hiera( )

  • hiera_hash( )

  • hiera_array( )

  • hiera_include( )

这些每一个都有一种等效的方式来实现相同的结果,因此对 Puppet DSL 代码库做一些简单的查找和替换工作,很快你就能摆脱已废弃的路线。

查找函数语法

lookup 函数语法有三种特定的使用方式,如下所示:

  • 必须包含强制性的 <name> 和三种可选参数:<value type><merge behavior><default value>,按照给定顺序并以逗号分隔。例如,lookup( <name>, [<value type>], [<merge behavior>], [<default value>] )

  • 必须包含可选的 <name> 和强制性的 <options hash> 参数。例如,lookup( [<name>], <options hash> )

  • 必须包含 <name><lambda expression> 参数。例如,lookup( <name>, <lambda expression> )

查找函数参数

显示在 [ ] 中的 lookup 函数参数不是强制性的,如前一部分所述。

  • <name>

    • 必须是 stringarray 类型。

    • 在 Hiera 层级中要检索的键名。

    • 也可以提供一个包含多个键的数组。如果最终的 Hiera 查找无法为第一个键返回结果,它将依次尝试后续给定键的查找,最后如果数组中的键都没有返回值,则回退到默认值。

  • <value type>

    • 必须是有效的数据类型

    • 如果返回值的数据类型与此处给定的数据类型不匹配,Hiera 查找(因此目录编译)将会失败。

    • 默认为 Data(即任何正常值不会使 Hiera 查找失败)

  • <merge behavior>

    • 必须是 stringhash 类型(请参阅以下 深度合并查找设置说明 部分)。

    • 解释是否以及如何合并在不同层级遇到的多个值。这将覆盖在 Hiera 数据源中指定的合并行为。

    • 默认为无值,意味着如果存在,Hiera 将首先使用在数据源中定义的合并行为;否则,它将简单地使用第一个查找策略(请参阅以下 查找策略 部分)。

  • <default value>

    • 如果提供,当 Hiera 查找无法在 Hiera 层级找到值时,查找将返回此处提供的值。

    • Hiera 查找找到的值从不与给定的默认值合并。

    • default typevalue type 必须匹配

    • no value 是默认值;意味着每当 Hiera 查找无法检索到正常值时,Hiera 查找(因此编译)将会失败。

查找函数语法 部分所述,也可以使用 <options hash> 提供查找函数参数:

  • <options hash>

    • 必须是 hash 类型

    • 如果使用这种替代的 <options hash> 语法样式,则不能与任何前面的常规参数结合使用,除了 <name>

    • 可用的选项哈希键如下:

      • name:与前面描述的第一个 <name> 参数相同。你可以将其作为参数传递或放在选项 hash 中,但不能同时使用两者。

      • value_type:与前面描述的第二个 <value type> 参数相同。

      • merge:这与之前描述的第三个 <merge behavior> 参数相同。

      • default_value:这与之前描述的第四个 <default_value> 参数相同。

      • default_values_hash:这是一个查找键及其相应默认值的哈希表。如果无法从 Hiera 查找中检索到正常值,则在 Hiera 放弃之前会检查该哈希表中的键。可以将其与 default_value 或 lambda 表达式结合使用,如果无法从 Hiera 层次结构中检索到值,将替换为该值。空哈希表是默认值。

      • override:该值是一个包含 Hiera 查找键及其相应重写设置的哈希表。Hiera 在重写哈希表中查找该键;如果找到,它最终返回该值,忽略任何合并行为。空哈希表是默认值。

此外,正如在 查找函数语法 部分所解释的,提供 lookup() 函数参数的第三种方式是使用单个 lambda 表达式。如果 Hiera 查找无法检索到值,请将请求的键传递给 lambda 表达式,lambda 表达式的结果将成为 default_value

lookup(‘my::key’) |$my_key| {"Hiera couldn't find '${my_key}'. Did you forget to add this key-value pair to your hierarchy?"}

在这里,<lambda_expression> 返回一个自定义字符串,向用户提供反馈,并优雅地处理 Hiera 无法检索所需键的情况,而在 Hiera 的早期版本中,如果检索失败,它会静默失败,导致各种问题。

我们还可以添加我们的事实值等,帮助用户找到合适的位置来插入他们的键值对(请参考本章开头的 更完整的层次结构 部分):

lookup(‘my::key’) |$my_key| {"Hiera couldn't find '${my_key}' using certname  ‘${trusted.certname}’, application ‘${facts.application}’, environment ${server_facts.environment}, and datacenter ${facts.datacenter}. Did you forget to add this key-value pair to your hierarchy?"}

查找函数示例

让我们快速浏览一下 lookup() 函数的主要用例,同时展示旧的 hiera() 函数的等效用法:

  • 以下用法是一个完全常规的查找:
lookup('ntp::user')
 # equivalent to hiera('ntp::user')
  • 以下用法是常规查找,同时提供默认值:
lookup('ntp::user','root')
 # equivalent to hiera('ntp::user','root')
  • 以下用法是数组查找:
lookup('my_ntp_servers', Array, 'unique')
 # equivalent to hiera_array('ntp_servers')
  • 以下是一个深度合并查找:
lookup('users', Hash, 'deep')
 # equivalent to hiera_hash('users') with deep
  • 以下是一个分类查找:
lookup('classes', Array[String], 'unique').include
 # equivalent to hiera_include('classes')

查找策略

合并策略不再像早期版本的 Hiera 那样在全局范围内设置,这是一个很大的改进。有效的合并策略如下:

  • first:检索第一个匹配项;这等同于传统的 hiera() 默认行为。

  • unique:这是一个数组合并,相当于旧的 hiera_array() 函数。

  • hash:这相当于没有启用深度合并的旧 hiera_hash() 函数。

  • deep:这等同于启用了 deeper 合并的旧 hiera_hash() 函数(deep 不再被支持)

查阅官方的 Hiera 3.3 文档,深入理解深度合并和更深合并的概念:puppet.com/docs/hiera/3.3/lookup_types.html#example。请注意,Hiera 3 中的 deeper 合并相当于 Hiera 4+ 中的 deep 合并。

deep 合并不再被支持。

深度合并查找设置说明

现在,让我们一起看看这些常常被误解的合并设置,以确保我们掌握 Hiera 的使用技巧。

knockout_prefix 设置

这是一个深度合并的示例,使用knockout_prefix设置指定一个前缀,用于指示某个值应该从结果中移除:

# common.yaml
 ---
 classification:
   classes:
     - paessler
     - other
# mynode.myorg.net.yaml
 classification:
   classes:
     - -- paessler
     - nagios
     - webserver

在这里,我们表示mynode.myorg.net.yaml并没有使用 Paessler 进行监控,而是使用了 Nagios。这个查找会返回正确的值,如下所示:

lookup({
 'name' => 'classification',
 'merge' => {
 'strategy' => 'deep',
 'knockout_prefix' => '--',
 },
 })

sort_merge_arrays 设置

我们还可以通过sort_merge_arrays设置对合并后的数组进行排序,并移除匹配knockout_prefix的数据。可以从结果哈希中移除数组成员或整个键:

 lookup({
   'name'  => 'classification',
   'merge' => {
     'strategy'          => 'deep',
     'knockout_prefix'   => '--',
     ‘sort_merge_arrays’ => true,
   },
 })

merge_hash_arrays 设置

如果某个数组成员包含一个哈希,并且你希望将这些内容合并在一起,可以通过使用merge_hash_arrays设置来实现。

unpack_arrays 设置

最后,还有unpack_arrays设置。让我们再次修改节点的数据,保持公共数据不变,修改后的数据如下:

# mynode.myorg.net.yaml
 classification:
   classes:
     - --paessler,nagios
     - webserver

unpack_arrays设置会将每个字符串根据,分隔符拆分,创建一个数组,例如[“–-paessler”, “nagios”],然后将其合并;在我们的示例中,paessler值会被移除,因为它是通过knockout_prefix设置标记的,如下所示:

lookup({
   'name'  => 'classification',
   'merge' => {
     'strategy'        => 'deep',
     'knockout_prefix' => '--',
     unpack_arrays     =>’,’,
   },
 })

调试 Hiera

Hiera 的数据查找都是基于被配置节点的详细信息进行的,正是这个节点的作用域告知 Hiera 应选择哪些数据集,如何排序数据,以及如何插值某些值。

查看 Hiera 文档,获取更多关于调试和查找功能的详细信息:puppet.com/docs/puppet/5.3/hiera_quick.html#testing-hiera-data-on-the-command-linepuppet.com/docs/puppet/5.3/man/lookup.html

旧的调试技巧

之前,我们曾通过命令行运行hiera并使用–debug参数,提供了设置(例如,mysetting)来查找,如下所示:

$ hiera -c /etc/puppetlabs/puppet/hiera.yaml --debug mysetting

上述命令以所需的调试详细级别运行hiera,但我们还需要收集节点的事实信息以及其他相关数据(特别是环境和fqdn):

$ hiera -c /etc/puppetlabs/puppet/hiera.yaml --debug --json facts.json mysetting environment=production fqdn=mynode.example.local

另一种早期的调试方法是使用hiera查找功能,通过puppet apply使用-e(执行)参数:

$ puppet apply --debug -e '$foo = hiera(mysetting) notify { $foo: }'

等效的调试技巧

hiera命令现已完全被puppet lookup命令取代,因此我们可以运行以下命令,并使用--node参数来提供查找所涉及的节点:

$ puppet lookup --node mynode.example.local --debug mysetting

这里的主要区别是,puppet lookup功能现在会查询puppetdb,以收集与给定node参数相关的所有事实数据。

我们现在还可以使用 --explain 参数来详细描述 Hiera 如何在其层级中获取数据。

超越 Hiera 使用 Jerakia

如果你想超越单一客户和小规模的层级数据分类,并开辟更大、更复杂、多样化环境建模的可能性,你应该考虑使用 Jerakia(jerakia.io),将 Jerakia 用作 Hiera 后端,或者配置 Puppet 接受 Jerakia 作为数据绑定终端。

Jerakia 高级用例

以下是关于 Jerakia 高级用例的一些问题:

  • 我该如何仅为某个模块使用不同的 Hiera 后端?

  • 我该如何允许某个团队使用独立的层级,仅用于他们自己的应用程序?

  • 我该如何允许某个用户或团队访问更小范围的数据?

  • 我该如何在不强制使用 YAML 的情况下使用 eyaml 加密?

  • 我该如何实现一个动态层级,而不是硬编码它?

  • 我该如何将应用程序特定的数据分组到单独的 YAML 文件中?

Jerakia 使我们能够实现一些特殊的情况。

安装 Jerakia

Jerakia 是通过 RubyGem 安装的。只需运行以下命令:

$ gem install jerakia

配置 Jerakia

按如下方式设置 jerakia.yaml 配置文件:

$ mkdir /etc/jerakia
$ vim /etc/jerakia/jerakia.yaml

这是最简单的配置:

--- 
policydir: /etc/jerakia/policy.d 
logfile: /var/log/jerakia.log 
loglevel: info 
eyaml:   
 private_key: /etc/puppetlabs/puppet/eyaml/private_key.pkcs7.pem         
 public_key: /etc/puppetlabs/puppet/eyaml/public_key.pkcs7.pem

如果你打算使用加密,你还应该在 private_keypublic_key 设置中提供密钥,如所示。

创建你的默认 Jerakia 策略

所有来自 Jerakia 的数据请求都是按照所谓的 policy 处理的。策略的文件名应与策略的实际名称相同,并从 jerakia.yaml 配置文件中 policydir 设置所指定的目录加载。如果查找请求中未指定某个策略名称,则使用 default 作为默认名称。我们来创建默认策略,如下所示:

$ mkdir /etc/jerakia/policy.d
$ vim /etc/jerakia/policy.d/default.rb

Jerakia 的 policy 是一个容器,包含所谓的 lookup,按指定的顺序执行。一个查找包括一个应当用于数据查找的 datasource,以及任何插件功能。

以下是一个简单的示例,使用 file 数据源从简单的 YAML 文件提供数据:

policy :default do

   lookup :default do
     datasource :file, {
       :format     => :yaml,
       :docroot    => "/var/lib/jerakia",
       :searchpath => [
         "hostname/#{scope[:fqdn]}",
         "environment/#{scope[:environment]}",
         "common",
        ],
     }
   end

 end

让我们修改默认策略,以适应另一个配置团队的设置,假设该团队位于 denmark

policy :default do

   lookup :denmark do
     datasource :file, {
       :format     => :yaml,
       :docroot    => "/var/external/data/ie",
       :searchpath => [
         "project/#{scope[:project]}",
         "common",
       ]
     }

     confine scope[:location], "dk"

     confine request.namespace[0], [
       "apache",
       "php",
     ]
     stop

   end

   lookup :default do
     datasource :file, {
       :format     => :yaml,
       :docroot    => "/var/lib/jerakia",
       :searchpath => [
         "hostname/#{scope[:fqdn]}",
         "environment/#{scope[:environment]}",
         "common",
        ],
     }
   end

 end

使用 Vault 作为加密后端

Jerakia 的 2.0 版本现在支持通过 transit secret 后端与 Vault 集成。

Vault 是一个开源平台,用于加密、保护、存储并严格控制密码、令牌、证书以及基础设施的其他秘密设置的访问权限。Vault 还处理秘密管理中的一些复杂问题,例如租赁、轮换、撤销和审计。

因此,Vault 提供了类似 加密即服务 的后端供 Jerakia 使用。

安装和配置 Vault

查看 Vault 文档以安装和配置 Vault:www.vaultproject.io/docs/install/index.html

解封 Vault

按照 Vault 文档中的步骤解封 Vault:www.vaultproject.io/docs/concepts/seal.html

启用传输后端

通过如下方式挂载来启用传输后端:

$ ./vault mount transit

创建加密密钥

让我们创建一个 Jerakia 用于加密和解密的密钥。默认情况下,密钥被称为 jerakia

$ ./vault write -f transit/keys/jerakia

创建加密和解密策略

现在我们需要创建一个策略,限制 Jerakia 仅能使用加密和解密端点。

为了创建这个策略,我们将创建一个新的文件 jerakia_policy.hcl,然后使用 policy-write Vault 命令将其导入 Vault:

# jerakia_policy.hcl 
path "transit/decrypt/jerakia" {
   policy = "write"
 }
 path "transit/encrypt/jerakia" {
   policy = "write"
 }
$ ./vault policy-write jerakia jerakia_policy.hcl

检查加密是否正常工作

现在,我们可以尝试使用 Jerakia 传输密钥和刚刚创建的策略在命令行中加密一个值:

$ echo -n "Lorem ipsum dolor sit amet" | base64 | ./vault write transit/encrypt/jerakia plaintext=- -policy=jerakia
vault:v1:Xv3R5CugxnCLhL/T2eJ+rN+UilHzo78evxd0tf5efx0M2U2qIgaI

查看 Vault 文档,了解 readwrite 命令的更多细节:www.vaultproject.io/docs/commands/read-write.html

允许 Jerakia 使用我们的 Vault 进行身份验证

AppRole 身份验证是与 Vault 认证的推荐方法。

使用这种身份验证方法时,Jerakia 配置了角色 ID(role_id)和密钥 ID(secret_id),并使用这些值从 Vault 获取一个有限生命周期的令牌,以便与传输后端的 API 进行交互。

当令牌过期时,Jerakia 将再次使用 role_idsecret_id 请求新的令牌。

首先,我们将为 Jerakia 创建一个 AppRole,并为其设置 15 分钟的 TTL。这个角色必须与我们之前创建的访问策略相关联,使用 policies 参数:

$ ./vault write auth/approle/role/jerakia token_ttl=15m policies=jerakia

现在,我们可以检查 Jerakia 的 AppRole 并确认 role_id

$ ./vault read auth/approle/role/jerakia/role-id
Key     Value
 ---     -----
 role_id bfce3860-0805-43dc-ab6d-fe789559fe32

我们还需要创建一个 secret_id

$ ./vault write -f auth/approle/role/jerakia/secret-id
Key                 Value
 ---                 -----
 secret_id           94f23dba-7355-426c-ae1e-5768dbb70280
 secret_id_accessor  f7b0f10a-99f4-4c7e-b69d-7bbd27a3c016

现在我们拥有了 role_idsecret_id,可以继续将 Jerakia 与 Vault 集成。

配置 Jerakia 进行加密

jerakia.yaml 配置文件中,我们使用 Vault 作为加密提供者,并配置该提供者所需的特定配置:

encryption:
   provider: vault
   vault_addr: http://127.0.0.1:8200
   vault_use_ssl: false
   vault_role_id: bfce3860-0805-43dc-ab6d-fe789559fe32
   vault_secret_id: 8a2fa99c-7811-5e65-a74a-8ab2ba9b6389
   vault_keyname: jerakia

我们现在应该能够使用 Jerakia 进行 加密解密

$ jerakia secret encrypt mySecret
 vault:v1: d3HftM8HAJDwWeSfLkBcdpAdTFy8fBu3mj4Kf3mHADSLuevwCbjZ
$ jerakia secret decrypt vault:v1:d3HftM8HAJDwWeSfLkBcdpAdTFy8fBu3mj4Kf3mHADSLuevwCbjZ
 mySecret

启用 Jerakia 查找加密

我们通过在策略中使用 output_filter 方法来启用加密:

policy :default do

   lookup :default do
     datasource :file, {
       :format     => :yaml,
       :docroot    => "/var/lib/jerakia",
       :searchpath => [
         "hostname/#{scope[:fqdn]}",
         "environment/#{scope[:environment]}",
         "common",
        ],
     }
     output_filter :encryption
   end

 end

这指示 Jerakia 将所有内容传递给加密过滤器,并将所有检索到的值与加密提供者的签名进行匹配。如果匹配成功,加密提供者将在返回值之前用于解密。

总结

在本章中,我们详细探讨了 Hiera 5 与其早期版本之间的主要区别。我们还描述了如何快速设置加密的 YAML 后端,这样您就不再需要以明文方式保存您的秘密 Hiera 值。

我们还看了 Jerakia,您可以使用它来处理更高级的用例,比如为不同的团队提供不同的层级结构,并集成 Vault,为 Jerakia 提供类似 加密即服务 的后端。

在下一章中,让我们继续通过研究 Puppet 代码的管理来进行我们的高级课程。

第五章:管理代码

代码管理在 Puppet 的生命周期中经历了很多变化。在 Puppet 的早期版本中,代码管理主要由个人用户负责。大多数用户开始时只是直接在 Puppet Master 上编辑代码。我曾为一个组织工作过,该组织为每个模块创建了 Yum RPM 包,在引入 Puppet 环境之前,允许我们在多个 Puppet Master 之间回滚和前进。许多用户将 Puppet 代码存储在 Git 或 Subversion 中,并将代码签出到 Puppet Master 的目录中。

这些模型每个都有显著的开销管理,在从 Puppet 2 到 Puppet 3 的过渡过程中,两个解决方案脱颖而出,成为 Puppet 社区的首选:Puppet Librarian 和 r10k。Puppet Librarian 像 Ruby 的 bundle 文件一样管理代码,使用一个命令就能引入所有列出的模块和依赖项。来自 Forge 的自动依赖管理也有一些问题。一些模块包括所有操作系统的依赖列表,包括那些不在你基础设施中的操作系统。一些模块在一段时间内没有收到更新,导致它们链接到依赖的旧版本,而你的组织却使用了较新的版本。最后,Puppet 模块中的依赖项通常列出一个版本范围而不是单一版本,如果这些模块跨多个清单使用,解决冲突可能会变得非常困难。

一些 Puppet Librarian 的用户使用puppet-librarian-simple,它不管理依赖项。尽管puppet-librarian-simple比 r10k 更容易安装,但它与 r10k 的功能不完全匹配;r10k 已经成为最常用的代码管理解决方案,无论是企业用户还是开源用户。r10k 允许用户指向一个包含构建 Puppet 环境的指令集的远程仓库。Puppet Enterprise 带有 r10k 的扩展,称为 Puppet 代码管理器。

本章将涵盖以下主题:

  • 高效管理代码

  • 代码管理器

  • Git

  • r10k

  • 控制库

  • 安装并使用 r10k

  • 多租户控制库

高效管理代码

尽管直接在 Puppet Master 上编写代码是开始使用 Puppet 的最简单方式,但它是管理基础设施变化时最低效的模型。手动更改让用户单独管理以下问题:

  • 备份与恢复

  • 变更管理

  • Puppet Masters 的复制

  • Puppet 环境的复制

没有代码管理时,备份通常通过磁盘快照进行,或者通过简单地将代码打包并移到一个独立的位置以防紧急情况。手动放置代码使得组织需要负责维护备份和恢复的节奏与流程,并进行变更管理。没有任何代码管理时,将代码复制到 Puppet Masters 和 Puppet 环境的过程完全是手动的,这使得所有 Puppet 代码的测试和实现都依赖于危险的手动流程,而不是受控环境中的流程。

尽管将代码放入 RPM 中可以解决备份和恢复问题、变更管理和 Puppet Masters 的复制问题,但它在 Puppet 环境中存在困难。每个 Puppet 环境都需要创建一个 RPM,这会导致一组混乱的构建文件,这些文件会不断将代码放入多个环境中。此外,RPM 不适用于用来测试单独代码功能的短期环境。

使用 Code Manager 或 r10k 来管理代码能大大简化这些问题。代码从不直接写入磁盘;相反,它从一个远程仓库拉取需求列表,所有相关代码会被放置到 Puppet Master 上。这个模型的主要好处之一是每个代码变更都可以在 Git 中版本化,每次变更都可以通过标签、分支或提交哈希进行明确引用并放置到主服务器上。所有代码始终存储在远程,并且不依赖 Puppet Master 本身来进行备份和恢复。回滚现在只需在远程仓库中更改一个文件。代码管理还允许多个 Puppet Masters 的扩展,无论是长期存在的环境还是短期环境。

Code Manager

Code Manager 提供了企业级 RBAC 和额外的代码分发功能给 r10k。Code Manager 会自动为你设置 r10k,但使用它要求你了解 r10k 如何调用代码,以及如何将代码存储在 Git 仓库中。

Git

本书并非旨在成为 Git 的完整资源,但要有效使用 Code Manager,你应该了解一些 Git 的基础知识。

Git 是一个现代的代码仓库,它允许多个用户在同一代码集上进行异步工作。它通过将每次代码提交区分为与上次提交之间的差异来实现这一点。每次提交都是上次提交和当前更改之间的独特差异。第一次提交可能会向代码库添加数百行代码,但接下来的提交可能仅仅是删除一行并替换成另一行。当这段代码被另一个用户克隆(或复制)时,它会下载最新的代码,并允许用户回滚到之前的提交。

作为 Git 的入门,我们通过一个场景来演示。假设你使用 Git 来重新装修你的客厅。当前的提交就是你客厅现在的样子。如果你喜欢去年夏天的装修,替换沙发之前的样子,你可以回退到之前的提交,将客厅恢复到以前接受的状态。提交应该被视为代码的已接受状态,或者在这个例子中,是你客厅的已接受状态。

首先,我们最不希望在打造新客厅的同时破坏现有的客厅,因此我们会使用 git clone 克隆现有客厅。这样就会复制当前的客厅,并将整个更改历史一并打包。为了简化操作,我们将使用最新版本的客厅。如果我们想要对客厅进行更改,我们可以购买新的沙发、电视和两盏新的灯具。假设我们非常喜欢这些灯具,但对沙发和电视不太确定。如果我们对灯具使用 git add,它将把这些灯具添加到暂存目录中。Git 将报告以下信息:

$ git status
On branch master
Changes to be committed:
 (use "git reset HEAD <file>..." to unstage)

new object: Lamps

Changes not staged for commit:
 (use "git add <file>..." to update what will be committed)
 (use "git checkout -- <file>..." to discard changes in working directory)

 modified: Couch
 modified: Television

我们已经要求 Git 跟踪我们喜欢的新的灯具的更改。当我们输入 git commit 时,系统会要求我们写下更改的内容,然后 Git 会将新的客厅状态提交到记忆中:

$ git commit -m 'Beautiful New Lamps'

[master 0b1ae47] Beautiful New Lamps
 1 object changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 Lamps

请注意,沙发和电视并未包含在这次更改清单中。在我们的工作目录中,或者说我们的当前客厅,沙发和电视仍然存在,但它们并不是永久的更改,直到我们也将它们添加并提交。我们还可以选择将新的提交(灯具)推送回远程仓库以备份,并让其他装饰者使用我们最新的客厅组合,使用命令 git push

简而言之,我们克隆(复制)一个客厅的格式。我们随意地对这个格式进行更改。我们确定喜欢的更改会被添加并提交。那些不确定的更改仍然存在,但只在当前的工作目录中(或当前的客厅状态)。我们可以选择添加并提交沙发和电视,或者简单地使用 git stash 将客厅恢复到最后一个已知的良好状态,也就是我们之前的客厅,再加上新的灯具。这种模式给了我们尝试大幅度更改的选项,并且只提交我们确定的那些更改作为时间的检查点。一旦我们有了一个愿意支持的提交(检查点),我们就可以将其推送到所有人都能看到的客厅版本中。

让我们来讲解如何在代码上使用 Git,而不是在客厅里。第一步是克隆,或复制一个仓库。命令 git clone 会复制整个仓库及其历史记录,并将其带到本地工作站。这份代码的副本与它被克隆的地方(原始位置)完全独立。git clone 会创建一个完全独立的原始仓库副本。

当用户第一次进入代码仓库时,所有的代码都在工作目录中。用户可以在这里随意更改代码,Git 会跟踪上次提交与当前仓库之间的差异。Git 有一个叫做git status的命令,可以让用户查看哪些文件与上次提交不同。在以下示例中,一个模块已被克隆,init.pp中的值已被更改,用户在模块目录内运行了git status

Changes not staged for commit:
 (use "git add <file>..." to update what will be committed)
 (use "git checkout -- <file>..." to discard changes in working directory)

 modified: init.pp

你可能已经注意到Changes not staged for commit。Git 会识别工作目录、已暂存的更改以及仓库历史中的每次提交。标准工作流是克隆一个仓库,进行更改,将其暂存,创建一个新的原子提交,然后再推送回中央仓库。

尽管我们通常不会对从 Puppet Forge(Puppet 代码的主要外部仓库)获得的模块进行修改,但我们还是来看看克隆、修改、提交和(可选地)将代码推送回原始仓库的过程,Git 会自动将原始仓库标记为origin

首先,我们将克隆并在本地创建puppetlabs/ntp的副本:

$ git clone git@github.com:puppetlabs/puppetlabs-ntp.git
Cloning into 'puppetlabs-ntp'...
remote: Counting objects: 7522, done.
remote: Compressing objects: 100% (13/13), done.
remote: Total 7522 (delta 5), reused 18 (delta 5), pack-reused 7504
Receiving objects: 100% (7522/7522), 1.64 MiB | 0 bytes/s, done.
Resolving deltas: 100% (4429/4429), done.

请注意,它克隆了仓库并应用了 4,429 个差异。现在我们在 GitHub 上有了一个完整的本地仓库副本。它会创建一个名为puppetlabs-ntp的目录,我们必须通过使用cd命令进入该目录,才能继续在本地仓库中操作。

接下来,我们将编辑我们打算修改的文件。在这种情况下,我在仓库的manifests/init.pp中添加了一个注释。我可以通过运行命令git status来查看 Git 如何查看仓库:

$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
Changes not staged for commit:
 (use "git add <file>..." to update what will be committed)
 (use "git checkout -- <file>..." to discard changes in working directory)

 modified: init.pp

 no changes added to commit (use "git add" and/or "git commit -a")

Git 现在看到了本地仓库的更改。我想确保将这个更改提交到仓库,所以接下来,我会将其添加到暂存目录,使用git add manifests/init.pp将其标记为提交。如果我们再次运行git status命令,我们会注意到代码不再是not staged for commit,而是进入了Changes to be committed状态:

$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
Changes to be committed:
 (use "git reset HEAD <file>..." to unstage)

 modified: manifests/init.pp

在暂存目录中有了init.pp后,我可以将这段代码提交为一个新版本。运行命令git commit会打开默认编辑器,允许你对提交进行注释和命名。我将带上-m参数运行命令,这样我就可以直接在命令行中传递提交信息,而无需打开默认编辑器:

$ git commit -m 'Simple Clarification Comment added to init.pp feature'
[master 4538890] Simple Clarification Comment added to init.pp feature
 1 file changed, 1 insertion(+)

现在,我的本地仓库中已经有了新的提交。我可以使用命令git log查看这个提交:

commit 45388902ef5cf125ea2109197e115f050d603406 (HEAD -> master)
Author: Ryan Russell-Yates <rary@packt.com>
Date: Sun Apr 8 16:28:26 2018 -0700

Simple Clarification Comment added to init.pp feature

最显著的是,这个更改仅存在于我笔记本上的本地仓库中。为了共享这段代码,我需要将我的提交推送回原始代码所在的位置。当你在本地运行git clone时,它也会记录代码的来源,并默认将远程仓库命名为origin。如果我运行命令git remote -v,我实际上可以看到仓库的来源 URL:

$ git remote -v
origin git@github.com:puppetlabs/puppetlabs-ntp.git (fetch)
origin git@github.com:puppetlabs/puppetlabs-ntp.git (push)

如果我有权限直接推送到这个仓库,我可以使用简单的命令git push origin master将我的新提交推送到源代码中。Master 是分支的名称,或者说是我在仓库中所工作的特定代码集。

分支是 Git 中的一个概念,它允许我们创建代码的副本并在类似独立目录的地方进行工作。默认情况下,Git 创建一个主分支,这是存放最新功能代码的地方。我们可以在 Git 中创建一个新分支并修改代码,而不会影响它原来的分支。Git 最有效的使用方式是基于主干的开发模型,我们从主分支开始,创建一个包含新特性的分支,测试这些特性,最终将分支合并回主分支。这个模型使我们能够在不影响原始代码集的情况下进行工作、共享、测试,甚至实现代码。

当我们输入git checkout -b new_branch时,我们创建了一个新的分支,它基于我们之前所在的原始分支。我们可以在这里进行工作,添加额外的提交,甚至将其推送回源代码,而不会影响原始代码。只有当代码被合并回原始分支时,才会对该分支产生影响。可以将其视为 Git 中相当于将一套代码复制到新目录中,进行工作、测试,然后在完成后将其复制回原始源代码的操作。

r10k

r10k 是 Puppet Enterprise Code Manager 的主要驱动程序。它围绕一个单一的仓库展开,称为控制仓库。控制仓库包含描述整个 Puppet 环境的文件。这些文件的集合整体上构成了一个版本的 Puppet 代码,旨在推送到特定的一组节点。每次运行 r10k 时,它会重新部署控制仓库中的所有内容。

控制仓库

控制仓库是 r10k 和 Code Manager 代码管理的核心。它是一个单一的入口点,作为 Git 仓库表示,描述了一个或多个 Puppet Master 的一个或多个环境。

r10k 旨在为 Puppet 环境提供以下内容,来源于控制仓库:

  • 通过 Puppetfile 创建代码集所需的每个 Puppet 模块

  • 一个 Hiera 层级结构

  • Hiera 数据

  • 环境特定的配置

  • 任何额外的代码(例如 site.pp、角色或配置文件)

在单个 Puppet Master 上实现多个状态,可以通过使用 Puppet 3 中推出的一个概念来实现:Puppet 环境。在 Puppet 3 中,我们获得了使用多个目录来存储代码的能力,并可以为每个代理选择单独使用的代码目录。Code Manager 和 r10k 在此基础上扩展了这个概念,将控制仓库的每个分支视为完全独立的环境。

如果一个控制仓库包含多个分支,r10k 可以单独部署每个分支,作为一个独立的环境。这确实使得我们的控制仓库分支与标准的 Git 仓库有所不同。传统上,最佳模型是基于主干的开发,这使得我们有一个主分支,用于接收所有完成的代码更改。一个 Puppet 控制仓库通常包含多个长期和短期存在的分支,且分支之间有不同程度的代码合并意图。在最佳情况下,我们将代码与不同层次的环境合并,直到最终进入生产环境。我们将在本章后面介绍的 Puppetfile 通常是各个环境之间差异最大的文件。

在一个组织拥有正式的生产、预生产和开发环境,并且用户积极编写 Puppet 代码的情况下,我们可能会看到以下几个分支:

  • 生产环境

  • 预生产环境

  • 开发环境

  • 功能 1

  • 功能 2

功能 1功能 2 被认为是短期分支,修改的目的是为了合并到开发环境中。Puppet 环境不需要与组织所认为的环境一一对应,且通常不应当如此。不要觉得需要将 Puppet 环境完全符合服务器的组织边界。

查看这些环境的最简单方法之一是将你的 control-repo 分支内部分类为 生产环境相似非生产环境相似

生产环境相似的环境

生产环境相似的环境是组织可以预期获取并为各个 Puppet 代理提供稳定代码集的正式代码通道。当我与组织首次设置这些环境时,我常常将它们描述为,如果在夜间或周末出现故障,需要你前去处理的任何环境。一个组织可能有一个开发环境,但如果需要基础设施团队的支持来维护,该环境应当被视为生产环境。任何被其他团队在组织内日常使用的环境,应当比非生产环境相似的环境受到更严格的控制。

管理 生产环境相似 分支的一些关键要点如下:

  • 如果你在 CI/CD 方面很强,并且经常将代码部署到生产环境中,请通过分支部署你的模块。

  • 如果你在常规周期(例如季度)中部署更新,请通过标签部署模块,作为版本号。

  • 将这些分支设置为 Git 仓库中的受保护分支。

  • 确定组织的 RBAC(基于角色的访问控制)和治理策略。

更多关于通过标签和分支部署模块的信息将在本章的 Puppetfile 部分详细讲解。

如果你正在使用托管 Git 解决方案,如 Bitbucket、GitLab 或 GitHub,启用控制库中生产环境分支的受保护分支。受保护分支确保只有提升了权限的管理员账户可以直接推送到该分支,或批准从其他分支生成的合并请求。这确保了代码在接受进入这些受控环境之前,已经经过同行评审。

组织应当决定一个 RBAC(角色基础访问控制)和治理政策,围绕这些受保护的分支,并应选派技术人员来审查代码并正式将代码接受到这些类似生产环境中。像开源项目一样,这允许任何组织成员通过 Git 向受控环境推荐更改,但需要可信任的个人将代码接受到受控代码库中。

另一方面,非生产环境的管理需求显著较低,可以在将代码合并到支持直接业务需求的环境之前,用于测试新特性。

非生产环境

我们对待非生产环境生产环境的方式不同。生产环境需要管理以确保仅有受信任的代码被部署,而我们的非生产环境分支则受限于这些相同的保护措施。

这些非生产环境分支的主要目标是促进快速的代码部署和测试周期。像受保护分支和治理政策这样的模式故意放慢开发速度,以增加稳定性,但不应在这些“西部片”风格的开发分支上使用。

非生产环境最常见的两个例子是 Puppet 暂存环境和功能分支。Puppet 暂存环境旨在允许所有 Puppet 用户在一个环境中集成和测试更改,然后再将代码发送到生产环境

如果你的组织需要一个暂存环境,你应只使用一个暂存环境,因为在多个暂存环境之间合并代码可能会比较困难。功能分支专门用于在隔离的环境中构建和测试新代码,然后再将其发送到暂存环境,或者在没有暂存环境的情况下,直接将其发送到生产环境分支,对于拥有强大 CI/CD 实践的组织。我们希望减少这些分支的开销,以便于异步提交代码和测试,而不需要一个受信任的代理来批准每个变更。

在大型组织中开发 Puppet 代码的常见工作流如下:

  • 克隆控制库

  • 检出一个新分支,基于你打算修改的分支(通常是 staging 分支)

  • 通过 PE 控制台将一个或多个节点添加到该环境,或者在代理的puppet.conf中设置环境。

  • 对代码进行迭代:编写代码并测试

  • 将你的代码与暂存环境合并,并删除短期分支

  • 通过多级类似生产环境的分支来推广暂存环境

在了解这些概念之后,接下来我们来检查 Puppet 控制仓库中包含的内容。

Puppetfile

控制仓库的核心是 PuppetfilePuppetfile 作为一个 Puppet 模块的列表,在每次运行 r10k 时都会被导入,并将模块部署到与控制仓库分支名称相匹配的 Puppet 环境中。它允许我们从两个地方导入模块:Puppet Forge 和远程 Git 仓库。

从 Puppet Forge 拉取模块可以使用简写形式,在文件的最顶部,你可以选择一个位置来搜索 Forge 模块。默认情况下,控制仓库会将我们指向 forge.puppet.com,这使得我们可以以简写形式编写我们想要引入的模块。在 Puppetfile 中输入 mod "puppetlabs/ntp" 会拉取最新版本。通过简单地添加一个版本号,例如 mod "puppetlabs/ntp", "7.1.1",r10k 将确保只部署 Forge 中的特定版本到环境中。通常认为最佳实践是始终包含 Forge 模块的版本,以避免意外地将新的主要版本部署到环境中。

此外,我们还可以直接指向 Git 仓库。这种用法最常见于用户或组织内部开发的 Puppet 模块。像 Forge 一样,我们可以专门指定一个 Git 仓库的版本,并将其部署到一个环境中。以下是一个示例:

mod 'ourapp',
  :git => 'git@git.ourcompany.com:ourapp.git',
  :ref => '1.2.2',

这个 Puppetfile 中的每一行实际上都代表着 r10k 的某个指令。第一行,mod 'ourapp',告诉 r10k 以 'ourapp' 这个名称部署该仓库,并将该模块作为该名称进行部署。这个名称必须与模块的命名空间匹配,在本例中,config.pp 需要包含 class ourapp::config

:git 引用告诉 r10k 到哪里去获取代码。r10k 必须具备 SSH 密钥才能访问这个仓库,除非仓库允许匿名克隆。ref 标签实际上会搜索提交、git 标签和分支,直到找到与引用匹配的一个。如果这个仓库包含名为 1.2.2git 标签,r10k 将使用该特定版本的代码。请注意,如果存在名为 1.2.2 的分支和标签,调用仓库的这种方法可能会引起问题。ref 是一种简写方式,允许你调用标签、分支或提交,但它们也可以通过 :tag:branch:commit 直接调用。

以下代码是一个 Puppetfile 的示例,它提供了以下内容:

  • 将 Forge 设置为 forge.puppet.com 的 HTTPS 版本

  • 包含最新的 puppetlabs/ntp

  • 包含 puppetlabs/stdlib 版本 4.25.1

  • 包含 puppetlabs/nginx 版本 0.11.0

  • 包含三个内部应用程序,可以通过分支、标签或提交进行调用

forge "https://forge.puppet.com"

# Forge Modules
# Always take latest version of NTP, notice no version listed
mod "puppetlabs/ntp"

# Specific versions of stdlib and nginx.
mod "puppetlabs/stdlib", "4.25.1"
mod "puppetlabs/nginx", "0.11.0"

# Modules from Git

# Pointing to Master Branch
mod 'ourapp',
  :git    => 'git@git.ourcompany.com:ourapp.git',
  :branch => 'master',

# Pointing to the 1.2.2 tag
mod 'ourapp2',
  :git => 'git@git.ourcompany.com:ourapp2.git',
  :tag => '1.2.2',

# pointing to an explicit git commit
mod 'ourapp3',
  :git    => 'git@git.ourcompany.com:ourapp3.git',
  :commit => '0b1ae47d7ff83489299bb7c9da3ab7f4ce7e49a4',

hiera.yaml

Hiera 在 Puppet 5 中的最佳功能之一是它默认包含在内,不需要额外安装。正如上一章所述,Puppet 5 为我们提供了三个 Hiera 层级:全局、环境和模块中的数据。环境级别的 Hiera 包含在控制仓库中,为我们提供每个环境的独立数据,并允许我们将所有 Hiera 数据存储在一个仓库中。

这种模型允许我们轻松地在 Puppet 5 中进行数据层的版本控制,甚至可以在不同分支之间合并我们的数据。如果我们希望像迭代开发 Puppet 代码一样迭代开发 Hiera 数据,我们可以使用第四章中展示的相同的 Hiera v5 配置,Hiera 5,如下所示,在各个环境中设置我们的数据:

---
version: 5
 hierarchy:
 - name: "Per-node data"
 path: "nodes/%{trusted.certname}.yaml"
- name: "Per-environment data"
 path: " %{server_facts.environment}.yaml"
- name: Common
 path: common.yaml

这将使用control-repo中的默认datadir(数据目录)来存储我们的 Hiera 数据。如果我们使用这个层级结构,我们的控制仓库可能包含以下内容:

├── data
│   ├── common.yaml
│   ├── development.yaml
│   ├── nodes
│   │   ├── server1.ourcompany.net.yaml
│   │   └── server2.ourcompany.net.yaml
│   ├── preprod.yaml
│   ├── production.yaml
│   └── staging.yaml
└── hiera.yaml

site.pp

site.pp是现代 Puppet Master 中最古老的文件之一。site.pp的最初目的是对节点进行分类,将类和资源分配给节点以创建目录。它接受正则表达式和字符串匹配名称,如果用于直接在系统上放置代码和资源,它将包含如下代码:

node 'application.company.com' { include role::application }

如今,大多数用户不再将分类存储在site.pp中。分类由外部节点分类器ENC)处理,例如 Puppet Enterprise 控制台。Hiera 也成为了一种常见的分类方法,取代了 ENC。任何未在site.pp中限定为节点的代码,都将应用于 Puppet 环境中的所有节点。以下代码,在节点规格外部放置时,会在节点的 Hiera 层级结构中搜索名为classes的数组中的唯一类,移除任何包含在名为class_exclusions的数组中的内容,并将它们应用于每个节点。这使得 Hiera 能够充当 Puppet 节点的分类器。

以下代码启用 Hiera 作为分类策略,当它放置在site.pp中时:

#This section ensures that anything listed in Hiera under classes can be used as classification

$classes = lookup('classes', Array[String], 'unique')
$exclusions = lookup('class_exclusions', Array[String], 'unique')
$classification = $classes - $exclusions

$classification.include

如果我们有一个名为snowflake.ourcompany.com的服务器,且以下内容包含在我们的 Hiera 层级结构中,我们将包括role::ourappprofile::partners::baseline,但排除profile::baseline,即使它在common.yaml中列为一个类。这确保了profile::baseline会在整个基础设施中应用,除了那些被显式排除的地方:

# common.yaml
---
classes:
  - profile::baseline

我们还可以使用上面的类排除来从特定节点中移除基线:

# nodes/snowflake.ourcompany.com.yaml
---
classes:
  - profile::partners::baseline
  - role::ourapp
class_exclusions:
  - profile::baseline

site.pp 还允许我们为整个环境的 Puppet 代码设置一些合理的默认值。在以下示例中,任何 Windows 机器将默认使用 Chocolatey 作为包管理器。Chocolatey 是一个免费的开源解决方案,类似于 Linux 中的 Yum 包管理器。如果你还没有在 Windows 环境中试过它,它是一个比直接从 .msi.exe 安装要好得多的选择:

# Set Default Package Provider to Chocolatey on Windows

if $::kernel = 'windows' {
  Package {
    provider => 'chocolatey'
  }
}

environment.conf

environment.conf 文件是控制库中的一个可选文件,允许你覆盖 Puppet 环境中的某些设置。从 5.5 版本开始,environment.conf 提供了五个设置,如下所示:

  • modulepath:搜索 Puppet 模块的位置。

  • manifest:搜索 site.pp 的位置,或按字母顺序解析的节点清单文件目录。

  • config_version:用户定义的脚本,用于生成通过运行 Puppet 代理所产生的版本。

  • environment_timeout:Puppet 环境缓存环境数据的时间长度。

  • static_catalogs:一个高级配置,内部版本化从 Puppet 主服务器提供的文件。默认启用。

此外,environment.conf 能够使用从 Puppet 配置生成的变量。在以下示例中,我们设置了 environment.conf 文件中最常见的两个设置:

# Extend Modulepath
# Using $basemodulepath to ensure all default modulepaths are still preserved
# This will now search for modules at $codedir/site, allowing us to place modules
# directly into the control repo. Often used for Roles and Profiles
modulepath = site:$basemodulepath
# Set version that appears during a Puppet run with a custom script
# Contained in base on control repo config_version = 'scripts/version.sh'

角色与配置文件

在前一章中,我们讨论了角色与配置文件。对于许多小型组织来说,将角色和配置文件放在控制库中是一个常见做法,因为这是一个简单的地方,可以开始为组织编写 Puppet 代码。使用之前的 environment.conf,我们的角色和配置文件将位于 /etc/puppetlabs/code/environments/<environment>/site,作为角色目录和配置文件目录。这些将包含在 Git 仓库中,位于仓库根目录下的 site 文件夹中。

对于许多较大的组织,接受单独角色和单独配置文件模块的提交,比将它们打包到控制库中更容易维护。这为每个环境提供了调用特定标记版本的角色和配置文件模块的能力。这两种方法都是有效的,并且在使用代码的代理上产生相同的结果。

本章结束时,你将找到一个关于多租户控制库的指南,如果角色和配置文件模块与控制库分开,将更容易管理。

控制库示例

如果我们按照前面示例中的设计使用控制库中的所有内容,我们的控制库的单个分支将如下所示:

$ tree control-repo
control-repo
├── data
│   ├── common.yaml
│   ├── development.yaml
│   ├── nodes
│   │   ├── server1.ourcompany.net.yaml
│   │   └── server2.ourcompany.net.yaml
│   ├── preprod.yaml
│   ├── production.yaml
│   └── staging.yaml
├── environment.conf
├── hiera.yaml
├── manifests
│   └── site.pp
└── site
 ├── profile
 │   └── manifests
 │   └── application.pp
 └── role
 └── manifests
 └── webserver.pp

安装和使用 r10k

通常,如果你拥有 Puppet Enterprise,你应该使用 Code Manager 而不是 r10k。如果你是 Puppet 开源用户,或者你的环境是开源和企业节点的混合体,可以考虑直接安装 r10k。Forge 上有一个 Puppet 模块,Vox Pupuli 提供的该模块可以在现有的 Puppet Master 上安装 r10k,地址是 forge.puppet.com/puppet/r10k

一旦安装了 r10k,可以通过以 root 用户或具有 sudo 权限的用户身份在每个 master 上运行 r10k deploy environment <branch> -p 来部署环境。通常,当 r10k 代替 Code Manager 使用时,CI/CD 系统会被用来自动化通过 r10k 的部署。

Code Manager

现在已经详细介绍了 r10k,我们来探讨一下它的 Puppet Enterprise 版本:Code Manager。Code Manager 为 r10k 添加了以下四个主要特性:

  • 文件同步与从 Master of Masters (MoM) 的 Rsync 跨主机同步

  • RBAC 和 pe-client-tools 提供 RBAC 访问

  • 自动环境隔离

  • 简单安装

在 Puppet Enterprise 中使用 Code Manager 而不是 r10k 的主要原因是 Puppet Enterprise 提供的强大 RBAC 模型。没有 Git 时,r10k 钩子要求你通过 SSH 或控制台登录到 Puppet Master,运行命令部署一个或多个环境。Puppet 提供的 PE 客户端工具允许用户生成一个短期有效的 RBAC 访问令牌,并通过远程检查与 Puppet Enterprise Console 中的 RBAC 进行匹配。这个远程 RBAC 模型不仅可以为不同的人员分配不同级别的环境部署权限,而且完全不需要用户登录到 Puppet Master。PE 客户端工具可以从本地工作站运行,并通过 Puppet Enterprise Web API 部署环境。

第二个主要特性是文件同步。r10k 直接将代码部署到单个 Puppet Master 的代码目录中。如果一个组织有多个 Puppet Masters,并由 Master of Masters(MoM)进行控制,使用单一命令即可将代码库部署到 MoM 上的代码暂存目录中,然后该代码会同步部署到环境中的所有 Puppet Masters。这样,你无需登录到多个 Puppet Masters,而是可以远程运行一次命令,让 MoM 将代码分发到所有的 Puppet Masters 上。

Code Manager 还确保在系统中运行所有环境隔离命令,确保类型资源不会意外地溢出到其他环境。该命令的开源等效命令是 puppet generate types --environment <environment>

Code Manager 的最后一个主要特性是简易安装。启用 Code Manager 所需的一切都包含在 Puppet Enterprise 中。

启用 Code Manager

在 Puppet Enterprise 中启用 Code Manager 非常简单,因为它已经预先捆绑在系统中。每个主节点上必须生成的唯一制品是用于访问控制库和任何其他 Puppetfile 中 Git 仓库的 SSH 密钥。这些 SSH 密钥应该在没有密码的情况下创建,并应在 Puppet Master 上进行保护。此外,如果你使用的 Git 服务支持它,请将此密钥作为部署密钥,而不是用户密钥。部署密钥仅具有检出代码的权限,不能将代码提交回 Git 服务器。对于单个主节点,可以作为 root 用户或使用 sudo 运行以下命令来生成 SSH 密钥:

# Create SSH Directory
$ sudo mkdir -p /etc/puppetlabs/puppetserver/ssh

# Generate SSH Key - With No Password
$ sudo ssh-keygen

Generating public/private rsa key pair.
Enter file in which to save the key (/var/root/.ssh/id_rsa): /etc/puppetlabs/puppetserver/ssh/id-control_repo.rsa
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /etc/puppetlabs/puppetserver/ssh/id-control_repo.rsa.
Your public key has been saved in /etc/puppetlabs/puppetserver/ssh/id-control_repo.rsa.pub.
The key fingerprint is:
SHA256:Random key root@server
The key's randomart image is:
+---[RSA 2048]----+
Random Art
+----[SHA256]-----+

# Ensure pe-puppet owns the directory and the keys
$sudo chown -R pe-puppet:pe-puppet /etc/puppetlabs/puppetserver/ssh

启用 Code Manager 的最简单方法是在生成密钥后,进入 Puppet Enterprise 控制台中 PE 基础设施下的 PE Master 分类。在 puppet_enterprise::profile::master 类下添加以下参数:

  • r10k_private_key: 生成并在 Puppet Master 上提供的私钥的位置。

  • r10k_remote: 控制库的位置——应为 Git URL。

  • code_manager_auto_configure: 设置为 true。这让 Puppet 自动进行配置。

  • r10k_proxy(可选):设置访问 Forge 的代理 URL,如果你的主节点只能通过代理访问互联网。

没有代理的此分类示例如下:

一些组织更愿意将他们对 Puppet 的更改存储在代码中,而不是在 PE 控制台中。以下代码也代表了前述更改,但 Puppet Master 在删除 puppet_enterprise::profile::master 类后,才会成功编译清单。要通过配置文件而不是通过控制台启用 Code Manager,请在删除控制台中的相同类后,将以下内容应用到主节点:

class profile::pe_master {

  sshkey {'codemanager':
    ensure => present,
    key    => 'Long String of Private Key',
    target => '/etc/puppetlabs/puppetserver/ssh/id-control_repo.rsa',
    type   => 'ssh-rsa',
  }

  class puppet_enterprise::profile::master {
    code_manager_auto_configure => true,
    r10k_remote                 => 'git@git.ourcompany.com:control-repo.git',
    r10k_private_key            => '/etc/puppetlabs/puppetserver/ssh/id-control_repo.rsa',
  }

}

这些方法中的每一种都可以在主节点上启用 Code Manager,允许远程 PE 客户端工具从单独的工作站部署环境。

Code Manager RBAC

启动 Code Manager 和 RBAC 的最简单方法是将用户添加到现有的用户角色 Code Deployers。Code Deployers 可以使用 PE 客户端工具部署任何环境。虽然一开始这看起来限制过于宽松,但请记住,Code Manager 仅部署控制库的现有分支。强烈建议不要在 Git 中预先存储代码,指望用户不会执行代码部署并部署最新版本的代码。代码部署应被视为幂等的,用户应该可以随意部署环境,通常在出现错误时不会覆盖任何代码。

在以下示例中,我已经将自己添加为用户,并将该用户添加到 Code Deployer 角色中,保持能够部署任何环境的权限:

你可以在以下截图中查看权限详情:

PE 客户端工具

Code Manager 是通过 PE 客户端工具来使用的。这些工具默认安装在 Puppet Master 上,但出于安全考虑,我们更倾向于将它们安装在用户工作站上,以便进行远程代码部署,并避免用户直接使用 Puppet Master。PE 客户端工具为我们提供了两个新命令:puppet-access loginpuppet-code deploy <environment>

puppet-access login 为我们提供一个 RBAC 令牌,默认生命周期为 5 分钟。用户可以通过为 puppet-access 添加 --lifetime=<time> 标志来覆盖这个生命周期。时间可以用分钟、小时、天或年表示,分别以 mhdy 为单位。例如,要设置半天的登录时间,用户可以运行 puppet-access login --lifetime=4h。这些令牌的最大值和默认生命周期由 puppet_enterprise::profile::console 类来决定。rbac_token_auth_lifetime 参数设置用户接收到的默认令牌生命周期。rbac_token_maximum_lifetime 设置用户通过 --lifetime 标志请求的令牌最大生命周期。组织在设置此值之前应考虑其标准登录安全实践。

puppet-code deploy <environment> 从控制仓库部署特定的环境,并且只能使用 puppet-access 提供的有效令牌执行。一旦令牌过期,用户需要重新通过 puppet-access 请求访问权限。通过为 puppet-code deploy 添加 -w 标志,可以让部署等待并返回部署状态的消息。建议用户在手动部署时使用 -w 标志,而在系统自动执行部署时,如 CI/CD 系统或 Git 钩子,可以省略该标志。

第一步是从 Puppet 的下载页面下载 PE 客户端工具。它支持多种操作系统,包括 Linux、macOS X 和 Windows。

PE 客户端工具可以设置系统级配置文件和用户级配置文件。用户配置会覆盖系统配置。我们必须管理两个 PE 客户端工具的文件:puppet-access.confpuppet-code.conf

系统级配置文件位于 Windows 的 C:/ProgramData/PuppetLabs/client-tools/ 和所有其他操作系统的 /etc/puppetlabs/client-tools。用户配置文件位于所有操作系统的 ~/.puppetlabs/client-tools,并会覆盖系统级配置。

puppet-accesspuppet-login 都需要一个有效的 CA 来进行 Web API 调用。默认情况下,这个文件可以在任何连接到相应 Puppet Master 的代理上找到,路径为 /etc/puppetlabs/puppet/ssl/certs/ca.pem。如果在非 Puppet 管理的机器上进行开发,你应该将此文件复制到本地。

puppet-access.conf 用于提供 puppet-access login 命令的配置,该命令连接到 Puppet Enterprise 的 RBAC API,并授予一个临时登录令牌,用于部署代码。puppet-access.conf 通常包含至少以下两个属性:

  • service-url:Puppet Enterprise 安装的 RBAC API URL

  • certificate-file:由主节点提供的有效 SSL 证书

#puppet-access.conf
{
  "service-url": "https://pemaster.ourcompany.com:4433/rbac-api",
  "certificate-file": "/etc/puppetlabs/puppet/ssl/certs/ca.pem"
}

puppet-code.confpuppet-access.conf 类似,都需要证书和 service-url 进行调用。有两点需要注意的是,与 puppet-access.conf 相比,puppet-code.conf 的服务 URL 会有所不同。puppet-access 调用 RBAC API,而 puppet-code 调用代码管理 API。此外,尽管两者都使用来自 Puppet Master 的相同证书,但你会注意到,puppet-code.conf 使用的是 cacert,而不是 certificate-file

#puppet-code.conf
{
  "service-url": "https://pemaster.ourcompany.com:8170/code-manager",
  "cacert": "/etc/puppetlabs/puppet/ssl/certs/ca.pem"
}

一旦设置完成,用户可以使用 Code Manager 工作流执行以下操作:

  • 查看代码

  • 进行更改

  • 推送回源仓库

  • 运行 puppet-access login 获取令牌

  • 运行 puppet-code deploy 部署环境

  • 检查结果

  • 如有必要,重复操作

多租户控制仓库

较大的组织可能需要 Puppet Enterprise Code Manager 的多租户设置。虽然从根本上说,工作流是相同的,但我们结构化控制仓库的方式略有不同。

我们尽量减少控制仓库的影响,将其转化为类似库的调用。我们希望将控制仓库定位为存储代码引用,而非代码本身。将角色和配置文件清单移动到外部仓库,可以让我们将它们作为版本化的工件进行管理,并直接声明每个环境可以使用的版本。我们的控制仓库仅包含 Puppetfile、使用 site.pp 全局应用的内容,以及我们希望对整个组织开放的值,以供在 Hiera 中使用。

我们对工作流做了一些小改动,以便支持更大的团队,具体如下:

  • 角色和配置文件会被导出到独立模块中,标记上版本,并通过 Puppetfile 导入。

  • 只有用于多个模块之间共享的值,如 LDAP 设置,才会保存在环境级别的 Hiera 中。所有直接调用类的操作,例如 profile::ntp::servers,都会存储在数据中,位于适当仓库中的模块中(在本例中是配置仓库)。

角色和配置文件被迁移为独立模块,并且每个团队也会获得自己的模块。这些模块然后在模块中加入自己的强大 Hiera 层次,并可用于为每个团队提供角色和配置文件。如果我们有一个开发名为myapp的应用程序的团队,他们将创建一个名为myapp的模块,并包含roleprofile文件夹。我们的命名空间会发生一些变化,但这使得我们可以将模块视为每个团队的角色和配置文件集合。原始的roleprofile仓库变成了整个组织常用代码的存放地,如安全基线或 Web 服务器默认配置。

以下代码可以由myapp团队生成,它为这些仓库提供了 Hiera、角色和配置文件的强大功能:

class myapp::role::app_server {
  # Global Baseline used by entire organization
  include profile::baseline
  # Profile generated specifically by myapp team
  include myapp::profile::application
}

class myapp::profile::application {
  # Profile has some custom code from the Myapp Team
  include myapp::application
  # Profile also uses the standard Webserver profile of the organization
  include profile::webserver
}

这种方法论结合了本章中的其他实践,如受保护分支,使得团队可以在不同的项目上以不同的速度工作,同时不会拖慢组织中其他团队的进度。它将控制库限制为仅描述环境,并开放角色和配置文件,以便从组织的任何地方接收代码贡献,同时通过 RBAC 和治理措施确保在接受代码之前进行适当的代码审查。

我们大大缩小后的控制库现在如下所示:

$ tree control-repo
control-repo
├── hiera.yaml
├── environment.conf
├── Puppetfile
├── data
│   ├── common.yaml
│ . └── datacenter
│       ├── us.yaml
│       ├── uk.yaml
│       └── can.yaml
└── manifests
 └── site.pp 

我们的团队模块就像是一个小型的控制库,具有 Hiera 层次结构、角色和配置文件:

$ tree team
team
├── README.md
├── hiera.yaml
├── data
│   ├── common.yaml
│   └── os
│       ├── RedHat.yaml
│       ├── Ubuntu.yaml
│       └── Windows.yaml
│   └── datacenter
│       ├── us.yaml
│       ├── uk.yaml
│       └── can.yaml
├── files
├── manifests
│ ├── profile
│ │ └── myapp.pp #team::profile::myapp
│ └── role
│ └── myapp.pp #team::role::myapp which includes team::profile::myapp
├── metadata.json
└── templates

总结

在本章中,我们讨论了 Git、r10k 和 Code Manager。我们强调了生产环境类非生产环境类的逻辑分离。我们列出了控制库的内容:Puppetfilehiera.yamlenvironment.confsite.pp以及各种类型的代码,如rolesprofiles。我们介绍了启用 Code Manager 并使用 PE 客户端工具与 Puppet Code Manager 进行交互的过程。最后,我们讨论了一种多租户、面向企业的控制库格式,该格式将角色和配置文件导出为独立的模块,并使用模块中的数据为组织中的每个团队提供 Hiera 层次结构。

在下一章中,我们将专注于将工作流整合到我们的代码开发中。我们将扩展我们的工作到 PDK,并检查良好的开发实践。

第六章:工作流程

在本章中,我们将讨论 Puppet 中的工作流程。我们将介绍什么是好的技术工作流程,如何将其应用到 Puppet 上,以及如何使用Puppet 开发工具包PDK)来改进我们的工作流程。我们将探讨一个好的工作流程的以下特性:易用性、快速反馈、入职容易性和质量控制。我们将使用 Puppet Git 仓库提供一个基本的 Puppet 工作流程,该流程可以调整以适应任何管理系统。我们还将探索 Puppet 发布的新的 PDK,它可以改进我们的工作流程。

本章将涵盖以下主题:

  • Puppet 工作流程

  • 设计 Puppet 工作流程

  • 使用 PDK

Puppet 工作流程

工作流程是一系列处理流程,从启动到完成。随着组织中 Puppet 环境变得更加复杂,一个值得信赖的共享工作流程将使得共享工作变得更加容易。Puppet 工作流程应该允许我们访问代码、编辑代码、测试代码,并最终将代码部署回 Puppet Master。虽然这不是强制要求,但强烈建议组织或团队采用共享工作流程。共享工作流程有几个主要好处,如下所示:

  • 可衡量的易用性

  • 快速反馈

  • 入职容易性

  • 质量控制

易用性

设计并开始工作流程的主要原因是为了提供易用性。团队应该围绕他们的代码库设计工作流程,使他们能够了解如何检索特定的代码、如何编辑这些代码以及新的编辑会带来哪些影响。工作流程还提供了一种标准化的方式来打包代码,以便现有代码库使用。工作流程中的每个步骤应该是清晰、简洁、易于沟通和可重复的。重要的是,团队中的每个人不仅要了解工作流程的运作方式,还要理解每个步骤存在的原因,这样他们才能在组织中发生变化时进行故障排除并为工作流程做出贡献。

相对于个性化的工作流程,共享工作流程的主要好处之一是能够衡量工作流程对组织的影响。为了衡量我们的工作流程,我们首先将标准和非标准的工作单位分开。我们对代码所做的编辑通常在大小和复杂性上有所不同,且不容易用标准单位衡量。另一方面,代码通常是以相同的方式进行检出、测试和部署的,这使我们能够很好地估算出完成工作流程所需的时间,减去代码编辑的部分。

如果我们的工作流需要大约 30 秒来克隆代码库,编辑代码的时间不确定,5 分钟来运行测试,另加 30 秒来在我们的环境中部署代码,那么我们的工作流,进行一次测试时,约需 6 分钟。如果我们的团队有 8 名成员,每人平均每天执行这个工作流 10 次,那么我们的工作流实际上每周占用了大约 8 小时的团队工作时间(8 x 10 x 6 = 480 分钟,或 8 小时)。将测试时间减半,可以使我们团队在工作流上花费的总时间减少大约 3 1/3 小时每周。因此,由于工作流中可以节省的这部分可量化时间,团队应尽可能考虑优化工作流。

通常,你不需要比估算执行工作流中标准功能所需时间更详细的估算,但你需要知道哪些部分可能会被多次执行。在使用 Puppet 时,用户很可能会写代码、推送代码并进行测试,而不是从服务器拉取代码。你可以分别检查工作流的每个部分,尝试改进其中的一部分,但你也应该考虑到修改对工作流其他部分的影响。

快速反馈

一个好的工作流应该为用户提供持续的反馈。每个步骤都应该有明确的定义,并具有严格的通过或失败标准。例如,Git 会在检测到问题时发出警告,比如无法拉取代码或推送代码到原始仓库。我们可以通过扩展 Git 提交钩子(无论是服务器端还是客户端)来进行检查,确保代码在被接纳到组织的 Git 服务器之前是处于正确状态的。在我们的测试标准中运行 Puppet 时,我们期望它能进行干净且幂等的运行。Puppet 目录不应生成失败的资源,也不应在每次 Puppet 运行时管理相同的资源。

使用 Puppet 解决问题所需的时间会随着工作流对工程师提供的反馈量增加而缩短。如果你在一个需要将代码推送到 Puppet Master 环境并在真实代理上进行测试的工作流中,简单地运行 puppet parser validate 可以节省大量时间。解析器验证会快速告诉你 Puppet 代码是否可以编译,而不是告诉你它将会做什么。这个简单的命令可以减少我们在代码上执行 git commit、推送到 Git 仓库、部署到环境、登录测试机并等待 Puppet 代理触发目录错误的次数。我们甚至可以确保每次提交前都会运行这个命令,使用 Git 的预提交钩子。自动化测试工具,如 RSpec 和 Beaker,可以扩展这种方法,并且与 CI/CD 管道(将在下一章讨论)结合使用,可以为代码开发者提供更快速的反馈。

容易上手

一个良好的工作流自然能够促进新成员加入项目的便利性,无论是开源项目还是组织的一部分。一个简单的工具套件和指南对新成员来说是无价的,能帮助他们克服首次提交的难关。即使是一个简单的README,如果维护得当,也能发挥很大作用。将新成员引入项目是昂贵的,而高质量的工作流可以减少新成员所花费的时间。加入新项目成员还需要现有项目成员提供一些信息和时间。如果你的项目是一个持续开发的工作,成员流动很可能会发生,因此,在工作流中节省现有成员的时间,并缩短新成员达到有效性所需的时间,应当是优先事项。

质量控制

一个好的工作流应始终致力于减少错误并提高代码质量。工作流中的每一个内置安全机制都可以让团队更快速地在复杂功能上进行迭代。简单的措施,比如防止直接推送到生产分支并将生产环境基于语义版本化的代码,可以实现快速开发,而不必担心破坏关键基础设施。

以下列出了一些围绕安全性和稳定性设计的工作流改进示例:

  • 防止直接向控制仓库的生产环境推送代码

  • 防止直接向单个模块的主节点推送代码

  • 在将所有清单推送回源仓库之前,运行 Puppet 解析器验证所有清单

  • 在将代码合并到主分支或类似生产环境的控制仓库分支之前,进行代码审查

  • 自动化测试

设计 Puppet 工作流

自 Puppet 开始以来,代码管理经历了很多变化。即使是整体工作流也发生了巨大变化。本节将帮助你了解 Puppet 代码管理的一些历史,面临的一些挑战,最重要的是,一些设计和使用强大 Puppet 工作流的解决方案。

最初,我们直接将 Puppet 清单写入磁盘。我们通过 SSH 登录到 Puppet Master 并直接编辑清单,将大部分代码视为远程机器的配置文件。这种模式需要为应用到代理的代码提供自定义备份和恢复,并且不提供便捷的回滚功能。如果部署出现问题,你不得不手动从备份中提取代码片段并将其部署到系统中。一些社区成员开始将 Puppet 代码存储在 Git 中。随着组织中独立仓库数量的增加,手动一个个引入 Git 仓库变得更加麻烦,一些社区开源项目也开始形成,专注于 Git 代码的分阶段管理。

Puppet 工作流的组成部分

虽然 r10k 不是唯一的 Puppet 代码管理工具,但它已成为企业组织中部署的标准代码管理工具。我们将把工作分解为任务和仓库,如下所示:

  • 仓库:

    • 控制仓库

    • 模块仓库

  • 任务:

    • 克隆

    • 创建新分支

    • 编辑相关代码

    • 添加并提交

    • 推送

    • Puppet 登录并部署

    • 分类

    • 测试(自动或手动)

仓库

代码管理要求所有代码都存储在 Git 中。将代码拆分到多个仓库并将其置于主分支上,可以引用不同版本的代码。每个模块应独立存放在单独的仓库中,以便进行版本控制和治理。Puppetfile 将通过 Puppet Forge 或指向您自己本地 Git 实例来调用这些仓库。

控制仓库

如前一章所述,我们的控制仓库不过是一个 Git 代码仓库。与其打交道时需要特别注意的一点是,分支名称对应于 Puppet 环境。如果您创建一个名为 feature 的 Git 分支并部署代码,Puppet Master 将把代码部署到 /etc/puppetlabs/code/environments/feature。通常,主分支会被另一个名为 production 的保护分支取代,以便代理默认将代码提交到生产分支。

模块仓库

模块仓库是标准的 Git 仓库。通常,我们希望保护主分支,避免其接收直接提交。组件模块的贡献者应通过提交拉取请求(pull request)来提交代码,并在将代码合并到主分支之前进行代码审查。主分支应该始终是模块的功能性版本,尽管它不需要是一个准备好部署到生产环境的版本。将主分支视为稳定代码,可以让非生产环境可靠地指向所有仓库的主分支,以在开发过程中获取最新的接受代码。在部署到生产环境时,我们将使用 Git 标签来创建一个版本,比如 1.2.0。然后,我们可以将最新的代码部署到非生产环境,并正式将代码接受到生产环境中。

任务

在基于 Code Manager 或 r10k 的系统中,工作流的主要驱动因素是 Git 工作流。Git 工作流有多种模型,比如 GitHub flow 和 Git flow,但本书的主要重点不是 Git,因此我们将从一组最基本的命令和流程开始。最有效的开始方式是使用我们的控制仓库提供的临时环境。在这个工作流中,我们假设已经在现场实现了 Git 解决方案,或者由托管服务提供商提供,并且 Puppet Master 正在使用 Code Manager 部署环境。

工作流的第一步是识别需要更改的组件。在这个工作流示例中,我们假设正在对一个组件模块和嵌入在控制仓库中的配置文件进行更改。在手动测试阶段,我们会包含修复步骤,包含新的代码部署和新的推送到 Git 仓库。

克隆并编辑组件仓库

首先,我们将克隆组件模块,切换到新特性分支,并对仓库中的文件进行编辑。我们将确保在开发过程中使用 Git 分支,这样可以将我们的代码推送到上游 Git 仓库,而不影响原始代码。我们将以在现有模块的单独分支上创建新的代码快照来结束这一步,以便我们可以独立地测试这段代码。这一系列步骤是以下操作的通用工作流程:

  1. 为单个模块制作上游仓库的副本(git clone/pull

  2. 创建一个与主分支分开的模块分支(git checkout

  3. 对代码进行任何和所有编辑(选择的 IDE)

  4. 创建代码当前状态的快照(git addcommit

  5. 将快照推送回上游仓库(git push

实际操作中的代码如下:

# Clone the remote git repository for the module. You can skip this step if the
# repository is already present on your local system
git clone git@gitserver.com:puppet/module.git

# If the repository is already local on the system, we'll just want to update our
# local master branch
git pull origin master

# Check out a new environment based on the existing master branch, which is the
# default branch of a git repository, and the branch we should start on on a clone.
git checkout -b new_feature

# We'll edit some files to add new features

# Adding new paramters to init
vim manifests/init.pp - Adding new parameters to init
# Adding a new feature to config
vim manifests/config.pp
# Ensuring the new feature is represented in the deployed template
vim templates/file.epp

# Add all edited files to git staging, assuming you're at the base of the repository
git add .

# Add an atomic commit, not only describing what the commit is, but why it was done
git commit -m 'Added new code to support feature requested by client'

# Push this code back to the origin repository as the new branch
git push origin new_feature

我们的编辑现在已经在上游仓库的new_feature分支中。主分支将继续作为其他人进一步开发的参考点,并用于在暂存环境中的测试。为了开始测试这段代码,我们将创建一个新的 Puppet 环境,专门用于测试和对这段代码进行迭代。

克隆控制仓库

第一步和上一步一样:克隆 Git 仓库。关于 Puppet 环境需要记住的一点是,这个仓库的分支对应一个 Puppet 环境。大多数 Puppet 用户没有主环境,而是使用 Puppet 默认放置节点的生产环境。如果你的组织有任何生产环境之前的环境(许多组织都有),你需要确保在创建新分支之前,先从现有分支开始。git checkout -b命令会创建一个新分支,起始于你当前所在的分支。以下是创建新环境的步骤,模仿现有环境:

  1. 从上游仓库克隆控制仓库的副本(git clone)。

  2. 检出你希望基于其编写新代码的环境(git checkout)。

  3. 检出一个新分支,基于当前分支(git checkout -b):

# This step is not needed if the repository is already on the local file system
git clone git@gitserver.com:puppet/control-repo.git

# We'll assume integration is the pre-production branch used by the organization
# to stage changes before moving into production-like branches
# Remember, there usually is no master branch in a control repository, so we want
# to target a specific branch to work against.
git checkout integration

# If this repo has been freshly cloned, git pull shouldn't provide any new updates,
# but it's safe to run either way. If the repository has already been cloned in the
# past, you definitely want to run this command to pull the latest commits from 
# upstream.
git pull origin integration

# We'll perform a second checkout, with the -b flag to indicate a new branch based on the existing branch
git checkout -b new_feature

就像我们为组件模块仓库所采取的步骤一样,这一系列命令确保我们拥有最新的提交的集成分支的本地仓库副本,并且我们基于现有代码启动了一个新分支。我们已经准备好直接在控制仓库中编辑文件,例如 Puppetfilehieradata 以及嵌入的 rolesprofiles(如果你将它们保存在控制仓库中,而不是作为独立的仓库)。一旦我们拿到代码,我们将编辑相关文件,创建新提交,将代码推送回源仓库,并部署环境。

编辑控制仓库

一旦我们进入目标环境的本地副本,就可以开始对代码进行更改了。我们通常会创建这些额外的短期环境,以便通过简单的命令部署新代码。我们有一些文件需要关注,因为我们将控制仓库视为整个环境的配置文件。Puppetfile 用于管理依赖项,包括任何组件模块(来自 Forge 或你自己的环境)。rolesprofiles 通常也保存在控制仓库中,代码可以直接在这些环境中进行编辑。对控制仓库进行更改的工作流程如下:

  1. 编辑文件(使用你选择的 IDE)。

  2. 创建当前代码状态的快照(git addcommit)。

  3. 将环境推送回远程仓库(git push):

# Edit our files

# Change the branch of the component module to new_feature
vim Puppetfile

mod 'module',
 git => git@gitserver.com:puppet/module.git,
 branch => 'new_feature'

# Make a change in the profile that utilizes the component modules
vim site/profiles/manifests/baseline.pp

# Add our new changes, to be staged for a commit
git add .

# Commit our changes
git commit -m 'Supporting new Feature to support <effort>'

# Push our code back to the control repository as a new branch intended to be
# realized as a new environment on the Puppet Master
git push origin new_feature

此时,我们已经编辑了控制仓库中的模块和文件并推送回源仓库。接下来,我们将部署前面代码中创建的分支,并调整我们的配置以使用模块更改。除非你已经设置了 Git 钩子或 CI/CD 解决方案,否则你还需要在 Puppet Master 上触发环境部署。

在 Puppet Master 上部署新环境

Puppet 提供了 PE 客户端工具,如 第五章 管理代码 中所述,专门用于部署代码。如果这些工具在你的工作站上不可用,你也可以登录到 Puppet Master,那里已经可以使用它们。假设你正在使用 Code Manager,无论是在本地工作站还是远程服务器上,接下来的步骤都是相同的。

  1. 从 Puppet Enterprise 获取登录令牌(puppet-access login)。

  2. 从上游仓库分支部署环境(puppet-code deploy):

# If PE Client Tools are not installed locally, the Puppet Master comes with them
# installed by default. We'll assume that the PE client tools are not already
# installed and log in to the Puppet Master
ssh user@puppet.org.net

# Generate an authorization token to allow your PE Console user to deploy code
puppet-access login

# Use our access token to deploy our new environment. Notice the -w flag, which
# triggers the client tools to wait and give you a pass or fail message on the
# status of the deployment.
puppet-code deploy new_feature -w

现在我们的代码已作为一个全新的环境部署在 Puppet Master 上。我们仍然缺少一个步骤,就是对我们的测试系统进行分类并确保它被放置在正确的环境中。对于 Puppet Enterprise 用户,你可以使用 PE 控制台中的节点分类器组来同时分类和声明一个环境。要创建一个新的节点组,选择一个环境,勾选环境组框,命名并点击创建。进入你的新环境组,将测试节点固定到该组,并向分类页面添加任何相关的类。

你也可以通过 manifests/site.pp 在控制库中进行分类,方法如下:

node 'test.node' {
  include relevant_role_or_profile
  include new_feature
}

通过 Hiera 进行分类的代码如下:

# data/host/test.node.yaml
---
classification:
  - relevant_role_or_profile
  - new_feature

# manifests/site.pp

# Notice the lack of a node group around the include statement
include $::classification

Puppet 用户常用的分类方法有多种,但没有自动化测试的话,我们需要做一些分类并运行代理来检查测试结果。

测试更改

在你的测试节点正确连接到环境组后,你可以登录到节点并触发代理运行,使用 puppet agent -t。或者,你也可以通过 PE 控制台运行 Puppet 代理并查看日志。如果没有看到任何变化,可能有以下几种原因:

  • 代理已经运行,在你在控制台中对节点进行分类和运行 Puppet 代理之间。

  • 漏掉了一个步骤,代码未正确部署。

  • 你的代码没有在系统上触发任何新的更改,你应该修改系统以查看 Puppet 是否会修正该更改。

确保检查你的更改所针对的资源,查看代理是否已经部署了新的更改。你可能还需要验证代码部署是否已正确完成,并确保你已将代码推送回 Git 库。如果你的代码没有触发系统上的任何更改,或者触发了不期望的更改,你可以执行以下更短的工作流程,直到代码正确解决:

  1. 在目标库中编辑代码:控制库或模块库(使用你选择的 IDE)

  2. 创建代码的快照(git addcommit

  3. 将代码推送回远程库(git push

  4. 重新部署环境(puppet-access loginpuppet-code deploy

  5. 在测试机器上触发代理运行(puppet agent 或 PE 控制台)

  6. 检查目标系统上的变化

  7. 重复直到达到预期状态:

# Start in the repository with the change. This could be a component module
# or the control repository. We're assuming each repository is still on the
# branch from the last step, and no pulls or branch changes are necessary.

# Edit the file with the targeted changes
vim manifests/manifest.pp

# Add the file to the git staging area
git add manifests/manifest.pp

# Commit the file to the repository
git commit -m 'Fixing specific bug'

# Push the repository back to upstream origin
git push origin new_feature

# From the Puppet Master, or a workstation with PE Client Tools

# Log in with RBAC
puppet-access login

# Deploy the environment
puppet-code deploy new_feature -w

# On the test node

# Run the agent, observe the results
puppet agent -t

# Repeat as necessary until issues are solved

一旦我们的代码达到预期状态,我们就可以开始将它放回 Puppet Master 上的长期环境中。模块应该将其代码合并回主分支,并且对控制库的更改需要与长期存在的分支合并。

合并分支

在之前的步骤中,我们将工作代码隔离到了一个功能分支和一个短期的、非生产环境中。虽然团队和组织应当选择一些合并的保护措施和策略,例如同行代码审查和自动化测试,但本节将重点介绍将分支合并到 master 或长生命周期分支的步骤。企业和开源的基于 Web 的 Git 解决方案通常包含一些额外的控制,来指示谁可以合并到仓库,以及合并到哪些分支。一般的最佳实践是允许进行同行代码审查,审查者可以将代码接受到长生命周期分支或 master 分支中。通过命令行合并代码是一个简单的过程,步骤如下:

  1. 切换到你想要合并的分支(git checkout

  2. 将另一个分支合并到当前分支(git merge

  3. 将合并后的分支推送到上游仓库(git push):

# Many Enterprise-focused git repositories have built in merge features, that ar
# likely more robust and easier to use than a simple git merge. If you have an in
# house git solution, follow the program documentation on a merge request

# On Module
# We'll change to target branch, in this case master
$ git checkout master

$ git merge feature_branch
Updating 0b3d899..227a02e
Fast-forward
 README.md | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 README.md

# Push the branch to upstream repository so Puppet can find it.
$ git push origin master

在控制仓库中进行合并有时会遇到麻烦,因为 Puppetfile 在不同版本间(有意)不同。我们的 生产型 分支应该使用 Git 标签来声明要部署的代码的目标版本,并将其推广到各个环境中。我们的 非生产型 环境通常指向每个模块的 master 分支,提供最新的稳定代码供环境测试和开发。合并的方式与组件模块相同;只需确保不要将 生产型 分支上的 Puppetfile 覆盖为 非生产型 分支中的、控制较少的 Puppetfile。生产分支应参考 Git 标签来部署代码。

Git 标签与版本控制

Git 标签用于创建代码的一个永久状态,并将其与现有分支区分开。标签不是用来迭代的,而是作为代码状态的时间标记。这使得标签非常适合用于 Puppet 代码的发布版本控制。我们可以从任何分支创建标签,但 master 是最常用的创建发布标签的分支。我们可以简单地在模块仓库中使用 git tag 命令来创建一个带有语义版本号的快照,并将其推送到 origin 仓库,供 r10k 或 Code Manager 调用。Git 标签的工作流也很简洁,步骤如下:

  1. 检出目标分支(通常是 master)以便打标签(git checkout

  2. 对代码进行版本控制(git tag

  3. 推送标签到远程仓库(git push):

rary at Ryans-MBP in ~/workspace/packt/module (master)
$ git tag 'v1.4'

$ git tag -l
v1.4

$ git push origin v1.4

在我们的模块成功版本控制后,我们可以编辑 生产型 的 Puppetfile 来使用我们的标签,而不是指向某个特定的开发分支或 master 分支:

# Production-like branch, tagged with a solid version number
forge https://forge.puppetlabs.com

mod 'module',
 git => 'git@gitserver.com:puppet/module.git',
 tag => 'v1.4'

这是 Puppet 工作流的简化版本,但仍有改进的空间。Puppet 最近发布了一款名为 PDK 的工具,旨在帮助将高质量的 Puppet 工具整合进你的工作流中。

使用 PDK

一个好的工作流程应当提供易用性、快速反馈、便捷的上手过程和质量控制。PDK 旨在提高这一领域的生产力。PDK 中的许多工具已经存在了一段时间,但它们通常很难使用且配置复杂,尤其是在工作站开发环境下。

PDK

Puppet 在其官网上免费提供 PDK,并且为每个主要操作系统提供了相应的版本。它使用完全隔离的环境提供 Puppet 二进制文件和 RubyGems,这使得开发变得更加简便。PDK 中包含的工具,从版本 1.5.0 开始,如下所示:

  • 创建新的 Puppet 工件:

    • 模块

    • 已定义类型

    • 任务

    • Puppet Ruby 提供程序

  • PDK 验证——简单的健康检查:

    • Puppet 解析器验证(Puppet 语法)

    • Puppet 风格检查(Puppet 风格)

    • Puppet 元数据语法

    • Puppet 元数据样式

    • RuboCop(Ruby 风格)

  • PDK 单元测试(Puppet RSpec—单元测试)

创建新的 Puppet 工件

PDK 允许用户使用最佳实践创建新的工件。每个 pdk new 命令都会构建一个已经按照 Puppet 结构化的工件。这些工件旨在符合 Puppet 的最佳实践。如果你是第一次在隔离的环境中测试 PDK,从新模块开始是最简单的方法。

pdk new 命令

命令pdk new module会引导用户进入一个提示,要求用户指定 Puppet Forge 用户名、作者全名、模块许可证以及支持的操作系统。如果你没有 Forge 用户名或模块许可证,可以随便输入任何值。输入提示后,你会找到一个新目录,其中包含代码。如果你希望将这些代码推送到上游仓库,可以在命令行中按以下步骤操作:

# From directory pdk new module was run in, enter the module, create a
# git repository and add all files to staging
$ cd module
$ git init
$ git add .

# Initial Commit is a good common message as a starting point
$ git commit -m 'Initial Commit'

# Add the upstream remote
$ git remote add origin git@gitserver.com:puppet/module.git

# Push to master and begin regular module development workflow
$ git push origin master

如果你正在使用一个之前创建的模块,可以使用 pdk convert 命令将模板中缺失的任何项加入到现有模块中。默认情况下,PDK 部署位于 github.com/puppetlabs/pdk-templates 的模板。如果你需要修改这里找到的任何文件,可以从官方仓库克隆 pdk-templates 的副本,并将其推送到一个中心 Git 仓库。你需要使用 pdk convert --template-url <https> 来选择新的模板并将其部署到现有模块中。--template-url flag 命令还将把新 URL 设置为工作站上的默认 URL。

你可以随意制作这个模板的副本,因为 Puppet 提供的模板相当全面,并且有较强的主观看法。它甚至包括一些启动 CI/CD 系统的方法,例如 gitlab-ci。你可以删除不使用系统的相关文件,并确保模板提供的内容对你的组织是有意义的。

模板仓库为 PDK 提供了三个目录和配置文件,如下所示:

  • moduleroot:此目录中的 Ruby 模板将覆盖现有文件。若您需要强制使用某个特定文件(例如 CI/CD 流水线文件),这非常有用。

  • moduleroot_init:此目录中的 Ruby 模板不会覆盖现有文件。这对于起始文件(如模块模板)非常适用。

  • object_templates:Ruby 模板,决定在执行诸如 pdk new class 等命令时文件的输出。

  • config_defaults.yaml:为 PDK 模板中的所有 Ruby 模板提供默认值和变量。

一旦获得新的模块模板,您可以开始在模块内创建 Puppet 代码清单。通过在新模块内使用 pdk new class,我们可以开始创建清单。该命令会按照自动加载布局创建清单,因此运行 pdk new class server::main 会在 manifests/server/main.pp 位置创建一个文件。使用默认模板创建的类将是一个空的、无参数的类,文件顶部会有 Puppet 字符串风格的文档注释。pdk new defined_type 命令会创建一个类似的文件,但会使用定义声明而不是类声明:

$ pdk new class config
pdk (INFO): Creating '/Users/rary/workspace/packt/module/manifests/config.pp' from template.
pdk (INFO): Creating '/Users/rary/workspace/packt/module/spec/classes/config_spec.rb' from template

# Sample with folders
$ pdk new class server::main
pdk (INFO): Creating '/Users/rary/workspace/packt/module/manifests/server/main.pp' from template.
pdk (INFO): Creating '/Users/rary/workspace/packt/module/spec/classes/server/main_spec.rb' from template.

pdk new task 命令将基于模板在 tasks 目录中创建文件,用于 Puppet 任务。Puppet 任务是通过 Puppet 自动化管理基础设施中临时脚本和命令的一种方式。pdk new provider 是用于设计新的自定义 Ruby 提供程序到 Puppet 的实验性功能。

一旦创建并开发了新的对象,PDK 还将提供一套工具集来进行语法和样式检查,使用 pdk validate

pdk validate 命令

PDK 提供了 pdk validate 来检查语法和样式。语法检查确保代码可以编译,并且确保清单文件或 JSON 元数据中没有遗漏逗号或闭括号等内容。语法检查还可以通过 puppet parser validate 手动执行。样式检查会检查代码是否符合标准的样式指南。Puppet-lint 用于提供 Puppet 的样式检查,所有规则可以在 puppet-lint.com/ 查找。当模块处于健康状态时,PDK 将会对所有任务显示勾选标记:

$ pdk validate
pdk (INFO): Running all available validators...
pdk (INFO): Using Ruby 2.4.4
pdk (INFO): Using Puppet 5.5.1 ![] Checking metadata syntax (metadata.json tasks/*.json).
![] Checking module metadata style (metadata.json).
![] Checking task metadata style (tasks/*.json).
![] Checking Puppet manifest syntax (**/**.pp).
![] Checking Puppet manifest style (**/*.pp).
![] Checking Ruby code style (**/**.rb).

无效的 metadata.json 文件将阻止将模块上传到 Forge,并且无法运行 RSpec 测试。该文件详细列出了模块的作者信息以及其他信息,如依赖关系和支持的操作系统:

#Invalid Metadata.json

$ pdk validate
/opt/puppetlabs/pdk/private/ruby/2.4.4/lib/ruby/gems/2.4.0/gems/pdk-1.5.0/lib/pdk/module/metadata.rb:142:in `validate_name': Invalid 'name' field in metadata.json: Field must be a dash-separated user name and module name. (ArgumentError)

pdk validate 还会在模块中的每个清单文件上运行 Puppet 解析器验证。在以下示例中,init.pp 文件末尾忘记了一个大括号,PDK 提示我们该代码无法编译:

# Failed Parser Validation
# Can be ran alone with puppet parser validate

$ pdk validate
pdk (INFO): Running all available validators...
pdk (INFO): Using Ruby 2.4.4
pdk (INFO): Using Puppet 5.5.1
![] Checking metadata syntax (metadata.json tasks/*.json).
![] Checking module metadata style (metadata.json).
![] Checking Puppet manifest syntax (**/**.pp).
![] Checking Ruby code style (**/**.rb).
info: task-metadata-lint: ./: Target does not contain any files to validate (tasks/*.json).
Error: puppet-syntax: manifests/init.pp:9:1: Could not parse for environment production: Syntax error at '}'

如果 Puppet 解析器验证通过,puppet-lint 将在所有清单上运行。它将根据 Puppet 风格指南打印代码中的错误和警告。在以下示例中,我们对一个清单运行 pdk validate,清单中第 10 行有一行超出了 140 字符,并且第 9 行后有多余的空格:

$ pdk validate
pdk (INFO): Running all available validators...
pdk (INFO): Using Ruby 2.4.4
pdk (INFO): Using Puppet 5.5.1
![] Checking metadata syntax (metadata.json tasks/*.json).
![] Checking module metadata style (metadata.json). ![]Checking Puppet manifest syntax (**/**.pp).
![] Checking Puppet manifest style (**/*.pp).
![] Checking Ruby code style (**/**.rb).
info: task-metadata-lint: ./: Target does not contain any files to validate (tasks/*.json).
warning: puppet-lint: manifests/init.pp:10:140: line has more than 140 characters
error: puppet-lint: manifests/init.pp:9:28: trailing whitespace found

在某些情况下,我们希望禁用某个警告或错误,而不是打印出来。可以在 puppet-lint.com/checks/ 上找到检查列表,并可以用于禁用单个检查。在以下示例中,注意消息语句后的注释,告诉 lint 忽略 140 字符限制:

# A description of what this class does
#
# @summary A short summary of the purpose of this class
#
# @example
# include module
class module {

  notify {'String-trigger':
    message =>'This is the string that never ends. Yes it goes on and on my friends. Some developer just started writing without line breaks not knowing what they do, so this string will go on forever just because...' # lint:ignore:140chars
  }

}

如果我们希望在一个清单中忽略多个地方,可以通过将注释放在单独的一行并以 # lint:endignore 结束,使用 lint 块 ignore。在以下示例中,我们有两个大字符串,在 puppet-lint 检查时不会触发警告:

class module::strings {

# lint:ignore:140chars
  notify {'Long String A':
    message =>'This is the string that never ends. Yes it goes on and on my friends. Some developer just started writing without line breaks not knowing what they do, so this string will go on forever just because this is the string that never ends...'
  }

  notify {'Long String B':
    message =>'This is another string that never ends. Yes it goes on and on my friends. Some developer just started writing without line breaks not knowing what they do, so this string will go on forever just because this is the string that never ends...'
  }

# lint:endignore

}

如果你希望禁用某些检查,可以创建一个 puppet-lint.rc 文件。该文件可以放置在 /etc 目录下作为全局配置,也可以放置在主目录下的 .puppet-lint.rc 文件作为用户配置,或者放置在模块的根目录下作为 .puppet-lint.rc。如果你的团队使用本地开发工作站,可以考虑将 .puppet-lint.rc 添加到 PDK 模板中,以便在每个仓库中强制执行标准:

# Permanently ignore ALL 140 character checks
$ cat puppet-lint.rc
--no-140chars-check

最后,任何 Ruby 代码都将通过 RuboCop 进行验证。RuboCop 将检查模块中所有 Ruby 文件的样式。这为自定义 facts、types、providers 以及用 Ruby 编写的任务提供了样式检查:

$ pdk validate
pdk (INFO): Running all available validators...
pdk (INFO): Using Ruby 2.4.4
pdk (INFO): Using Puppet 5.5.1
![] Checking metadata syntax (metadata.json tasks/*.json).
![] Checking module metadata style (metadata.json).
![] Checking Puppet manifest syntax (**/**.pp).
![] Checking Puppet manifest style (**/*.pp).
![] Checking Ruby code style (**/**.rb).
info: task-metadata-lint: ./: Target does not contain any files to validate (tasks/*.json).
error: rubocop: spec/classes/config_spec.rb:8:38: unexpected token tRCURLY
(Using Ruby 2.1 parser; configure using `TargetRubyVersion` parameter, under `AllCops`)

pdk validate 提供了一个快速检查代码样式和语法的工具,但不会检查代码的功能。PDK 还提供了一个开箱即用的 RSpec 测试模板,因此,当使用 pdk new class 创建一个新类时,系统会自动创建一个简单的对应 RSpec 测试。

pdk test unit 命令

使用 pdk new class 创建的新清单也会提供一个默认的 RSpec 测试。编写单元测试以确保清单在运行时执行预期的操作。Puppet 提供的默认单元测试确保代码在 metadata.json 中列出的每个操作系统上都能成功编译,并使用这些操作系统的默认 facts。这可以扩展以创建更强大的单元测试。在以下示例中,已添加一个检查,表示模块的 init.pp 应提供一个名为 /etc/example 的文件,而该文件在清单中并未提供:

$ pdk test unit
pdk (INFO): Using Ruby 2.4.4
pdk (INFO): Using Puppet 5.5.1
![] Preparing to run the unit tests.
![] Running unit tests.
 Evaluated 45 tests in 2.461011 seconds: 9 failures, 0 pending.
![] Cleaning up after running unit tests.
failed: rspec: ./spec/classes/module_spec.rb:9: expected that the catalogue would contain File[test]
 module on centos-7-x86_64 should contain File[test]
 Failure/Error:

 it { is_expected.to compile }
 it { is_expected.to contain_file('/etc/example') }
 end
 end

默认的 PDK 提供的简单测试仅为每个模块提供 it { is_expected.to compile } 作为 RSpec 测试。在下一章中,我们将扩展初始的 RSpec 模块,涵盖单元测试并为 Puppet 模块提供一些基本的代码覆盖率测试。

摘要

本章开始时,我们详细描述了什么构成一个良好的工作流程。将工作流程与持续集成和持续交付策略结合后,许多工作变得更加容易,这些内容将在下一章中介绍。我们将扩展由 Puppet PDK 构建的 RSpec 测试,并讨论验收测试策略。我们还将介绍一些新的工作流程和工具,以便在开发 Puppet 代码和清单时提供更及时的反馈。

第七章:持续集成

持续集成作为一种实践,确保每次提交代码时,代码都能以一致的方式进行构建和测试。我们使用持续集成系统来自动化这一实践,使其可以在每次提交时都能实际使用。一些持续集成管道最终会演变为持续交付或持续部署管道。持续集成与交付的关键区别在于,交付确保每次提交代码时,代码也会被打包并交付到所需运行的服务器上。持续交付要求能够通过单一的协调命令一致地部署整个基础设施和应用程序。持续部署要求为基础设施中的每个组件提供端到端的测试套件,但它的任务非常简单,就是在每个测试通过时自动执行该单一协调命令。

这些系统如何为单个应用程序和基础设施提供价值,对每个公司和组织来说都是独一无二的,就像任何其他业务规则一样。有一些常见的使用案例和几乎在每个人的持续集成管道中普遍存在的业务规则,也有一些是团队所追求的目标。

在本章中,我们将做以下几件事:

  • 使用 Puppet 设置一个持续集成系统(Jenkins)

  • 为配置模块创建一个作业

  • 设置我们的第一个测试

  • 集成Puppet 开发工具包PDK)测试套件

  • 编写 RSPec 单元测试

  • 使用 Test Kitchen 设置 Puppet 集成测试

持续集成系统

我们的持续集成系统是一个面板,用于跟踪我们的代码仓库。对于每个这些仓库,你将看到通常所称的作业。作业是一系列步骤,通常是用代码编写的,用来告诉系统在通过按钮或命令行触发构建时应该做什么。构建只是该作业的一个实例,正在运行或已运行。最后,构建包含日志文件、关于构建的关键信息以及任何你希望系统存储或发送到终端的工件(对象)。

我们将使用 Puppet 构建我们的 CI 系统,最终由它来管理我们的 Puppet 代码。这是当你在一个已有环境中开始使用 CI 时的常见场景。

Puppet Pipelines

Puppet Pipelines 是 Puppet 推出的新产品。2017 年 9 月,Puppet 收购了 Distelli,以便开发新的 Puppet Pipelines 程序。这个 CI 系统仍然主要面向容器和应用程序,但也在努力改善其 Puppet 功能。Puppet Pipelines 仍然可以用于 Puppet 代码的持续集成系统,但在未来一年中,关于 Puppet 代码的功能可能会经历很多变化。本章中,我们将使用一个非常流行的开源持续集成系统:Jenkins。

Jenkins

Jenkins 是目前最古老、最常用的持续集成系统之一。它最初是作为 Hudson 于 2005 年开始的,并逐渐发展成我们今天看到的 Jenkins 分支。与大多数其他持续集成系统相比,Jenkins 因其高度插件化的特性,既强大又复杂。有大量的 Jenkins 插件用于为持续集成系统添加功能,从源代码管理、图表和视图,到编排、自动化测试和几乎所有编程语言的代码风格检查。凭借这些广泛的功能集,Jenkins 也常常变得复杂。开箱即用的 Jenkins 除了在系统上运行 shell 命令外并没有太多功能。在本节中,我们将探索如何使用 Puppet 构建一个基本的 Jenkins 配置,以便管理我们的 Puppet 代码。

使用 Puppet 管理 Jenkins

我们使用 Puppet 来管理持续集成系统,因为它是一个系统。我们使用 Jenkins 来管理我们的配置管理代码,因为它是代码。这就是为什么我们要用 Puppet 来构建 Jenkins,然后将我们的 Puppet 代码提交到 Jenkins。

rtyler/jenkins

在构建新软件时,我们应该始终寻找现成的模块,因此我打算从 Forge 中使用 rtyler/jenkins 模块。这个模块将覆盖我们安装 Jenkins LTS 服务器、安装 Jenkins 插件和每个运行构建所需包的基本需求。

在较大的基础设施中,我们不会在 Jenkins 服务器上运行构建,而是在附加的 Jenkins 代理上运行构建。由于此设置没有代理,Jenkins 将充当我们的构建代理并为我们运行作业。因此,我们需要安装 Git 和 PDK,以便它可以为我们运行命令。我们使用 Git 插件来直接连接到我们的代码,而 Pipelines 插件为我们提供了一种 DSL 来编写我们的步骤。

我们将通过创建一个配置文件目录、在其中创建一个 manifests 目录,并在该文件夹中创建一个 jenkins.pp 来使用 PDK 构建一个新模块:

#profile/manifests/jenkins.pp
class profile::jenkins {

  class { 'jenkins': lts => true }

  package {'git': ensure => latest }

  file {'/tmp/pdk.rpm':
    ensure => file,
    source => 'https://puppet-pdk.s3.amazonaws.com/pdk/1.7.0.0/repos/el/7/puppet5/x86_64/pdk-1.7.0.0-1.el7.x86_64.rpm',
  }

# Install latest PDK directly from Puppet Source
  package {'pdk':
    ensure => installed,
    source => '/tmp/pdk.rpm',
    require => File['/tmp/pdk.rpm'],
  }

}

我们将手动安装插件。rtyler/jenkins 确实支持 Jenkins 插件,但不支持插件依赖性。构建流水线中有相当多的依赖关系,因此我们将手动安装插件,以突出显示两个主要插件。

在我们的配置文件应用到节点后,我们就拥有了一个全新的 Jenkins 安装,并且安装了我们所需的插件。我们可以通过 8080 端口的网页 URL 访问我们的新 Jenkins 节点:

管理我们的插件

如果你想为每个插件实现 Puppet 化,你可以使用这个 Jenkins 模块提供的jenkins::plugin资源。你可以在/var/lib/jenkins/plugins文件中,或者在 Jenkins 实例的已安装插件选项卡中找到每个安装的插件。

资源语法如下:

jenkins::plugin {'<plugin': version => 'version' } 

在本节中,我们将获取 CI/CD 工作流的两个关键插件:Git 和 Pipeline。我们可以通过点击屏幕左侧的“管理 Jenkins”,然后在菜单底部点击“管理插件”来进入插件页面。Jenkins 的插件数量不断增加,我们需要选择合适的插件:

仅通过名称定位插件可能会很困难,因此可以尝试使用一些描述信息在列表中查找这些插件。

一旦我们选择了这些插件,并点击“下载并安装(重启后)”,我们将被带到一个页面,列出所有待安装、正在安装或安装成功的插件。在这个页面的底部有一个复选框,允许我们在整个下载完成后重启服务器。确保勾选该复选框:

创建我们的第一个构建

在 Jenkins 中安装了所需的插件后,我们可以开始构建我们的第一个构建。我们将从代码仓库的最低要求开始,然后演示如何让 Jenkins 读取该仓库并在新的代码被提交时自动运行构建。

这个项目将需要一个可供 Jenkins 访问的 Git 仓库。如果你没有现成的 Git 仓库,可以在 GitHub 上开设一个账户并使用公共仓库。我们没有写任何敏感信息,所以让世界看到你的仓库是可以的。

构建我们的个人资料模块

我们在本章开始时写了一些代码,通过个人资料的形式定义了我们的 Jenkins 服务器。首先,让我们检查一下我们当前工作目录的结构,看看我们现有代码所在的目录:

profile/
└── manifests
    └── jenkins.pp

这是一个非常简化的个人资料模块,只有一个清单。我们将首先把这个简单的模块转换为 Git 仓库:

[rary@workstation ~]# cd profile/
[rary@workstation profile]# git init
Initialized empty Git repository in ~/profile/.git/

如果我们运行git status,我们会看到manifests目录已经被检查。此仓库中的每个文件目前都是新的,因此我们需要将每个文件添加并提交到我们的第一次提交中,这通常被称为'initial commit'

[rary@workstation profile]# git add -A
[rary@workstation profile]# git commit -m 'initial commit'
[master (root-commit) 64f24a1] initial commit
 1 file changed, 19 insertions(+)
 create mode 100644 manifests/jenkins.pp
[root@pe-puppet-master profile]# git status
# On branch master
nothing to commit, working directory clean

然后我们准备将初始提交发送到远程仓库:

[rary@workstation profile]# git remote add origin git@github.com:RARYates/cicd-walkthrough-profile.git
[rary@workstation profile]# git push origin master
Counting objects: 4, done.
Delta compression using up to 2 threads.
Compressing objects: 100% (2/2), done.
Writing objects: 100% (4/4), 519 bytes | 0 bytes/s, done.
Total 4 (delta 0), reused 0 (delta 0)
To git@github.com:RARYates/cicd-walkthrough-profile.git
 * [new branch] master -> master

构建我们的 Jenkinsfile

我们在 Jenkins 节点上安装的 Pipeline 插件允许我们直接在与代码相同的仓库中声明流水线,作为一个名为 Jenkinsfile 的脚本。这个 Jenkinsfile 描述了我们的构建步骤的细节,Jenkins 可以自动读取它来执行我们的构建。我们将从一个非常简单的 Jenkinsfile 开始,确保我们的所有清单都通过puppet parser validate

pipeline {
    agent any

    stages {
        stage('Test') {
            steps {
                sh 'find manifests -name *.pp -exec /usr/local/bin/puppet parser validate {} +;'
            }
        }
    }
}

这个 Jenkinsfile 描述了一个可以在任何代理上运行的流水线(我们只有一个代理:我们的 Jenkins 节点)。它有多个阶段,但只有一个名为Test的阶段,并且只有一个步骤,该步骤在每个以.pp结尾的文件上运行puppet parser validate(每个清单)。

然后,我们将这个文件发送到远程仓库,以便 Jenkins 通过我们一直在使用的正常 Git 工作流程找到它。

将 Jenkins 连接到我们的仓库

现在我们在 Jenkinsfile 中声明了构建任务,可以开始构建我们的第一个作业。我们从点击左上角的“新建项目”开始,并创建一个新的多分支流水线作业,命名为 profile:

对于我们的构建,我们需要通过添加项目仓库来编辑分支源,并将扫描间隔设置为每分钟运行一次。这个对我来说是一个公共仓库,所以我不需要附加任何凭证。我将使用默认行为和属性策略:

一些托管的 Git 仓库,例如 GitHub Enterprise,允许扫描组织中的所有仓库。如果所有仓库都能被自动发现,这将节省大量管理 Jenkins 的时间。

在我点击扫描后,将立即运行一个任务来发现该仓库中的分支。尽管这个界面看起来就像是 Jenkins 构建,它的通过或失败状态完全取决于是否能够连接到 Git 仓库并在分支上找到 Jenkinsfile。让我们返回主页查看我们的第一次构建:

我们的启动页面展示了我们的第一次构建!太阳图标代表一次成功的构建,表示构建中的每一步都返回了正向的退出状态。在构建的最右侧是一个运行构建按钮,点击它可以再次运行构建。现在,点击名称档案并进入构建的详细信息。因为这是一个多分支流水线,我们还需要点击主分支以查看我们的状态。你会看到我们的构建已经运行,并且你可以从这个菜单检查每一步的详细情况。

为了确保这个操作不需要将我们的 Jenkins 放到一个公开可访问的地方,我们将使用仓库轮询。虽然这对于大多数情况有效,但最有效的策略是使用 Git 钩子,在每次构建后触发 Jenkins 运行。

在这个阶段,我们有一组可以按需执行的命令。为了让持续集成真正发挥作用,我们需要让我们的代码自行测试。在我们的作业中,我们可以选择查看配置来进入配置页面。我们将设置我们的构建触发器,以每分钟轮询 SCM:

一旦我们保存了这个配置,Jenkins 将自动每分钟检查一次我们的远程仓库是否有变化。现在我们拥有了最简单形式的持续集成:每次提交时,代码都会进行自我测试。由于代码覆盖范围很小,我们的持续集成流水线并未为我们提供太多价值,除了在我们创建了格式错误的清单时提醒我们。

集成 PDK

Puppet PDK 为我们提供了一个可重复的持续集成框架。我们将把我们的基础模块转换为 PDK 模块,然后我们将开始使用 PDK validate 来替代我们基本的 puppet parser validate 命令。因为 PDK 已经在我们的 Jenkins 主节点上可用,所以所有 PDK 命令也可以使用。

我们的第一步是切换分支,以便在添加新代码时不会影响主分支:

[root@pe-puppet-master profile]# git checkout -b pdk
Switched to a new branch 'pdk'

接下来,让我们使用 PDK convert 命令转换我们现有的模块。我们会被提示一系列问题,这些问题主要用于将模块发布到 Forge。最后一个问题询问该模块适用于哪个操作系统,实际上有助于形成我们的测试绑定,因此我们将其最小化为仅针对目标操作系统:基于 Red Hat 的 Linux。只需运行 pdk convert 并按照提示进行操作。

默认的 PDK 模板包含三个与我们无关的文件:.gitlab-ci.yml.travis.ymlappveyor.yml,这些文件用于其他 CI 系统。接下来,我们将添加我们的新文件并将其提交到新的代码提交中:

[rary@workstation profile]# rm .gitlab-ci.yml .travis.yml appveyor.yml
rm: remove regular file ‘.gitlab-ci.yml’? y
rm: remove regular file ‘.travis.yml’? y
rm: remove regular file ‘appveyor.yml’? y
[rary@workstation profile]# git add -A
[rary@workstation profile]# git commit -m 'Initial PDK integration'
[pdk 7eb5009] Initial PDK integration
 10 files changed, 350 insertions(+)
 create mode 100644 .gitignore
 create mode 100644 .pdkignore
 create mode 100644 .rspec
 create mode 100644 .rubocop.yml
 create mode 100644 .yardopts
 create mode 100644 Gemfile
 create mode 100644 Rakefile
 create mode 100644 metadata.json
 create mode 100644 spec/default_facts.yml
 create mode 100644 spec/spec_helper.rb

然后,我们将修改我们的 Jenkinsfile 中的 Test 阶段,使用 pdk validate 工具:

pipeline {
  agent any
    stages {
        stage('Test') {
            steps {
                sh '/usr/local/bin/pdk validate'
            }
        }
    }
}

我们将通过 Git 工作流将其推送回我们的远程仓库,Jenkins 实例将在通过 git push origin pdk 远程发送后自动接收我们在新 PDK 分支上的作业。在我们的配置文件页面上,我们现在会看到一个新的分支:

这个 PDK 分支的内部应该与我们之前的分支类似,但我们想查看我们测试的日志。在日志中,我们会看到一些 puppet-lint 警告被触发,但并未导致构建失败。默认情况下,Puppet lint 警告的退出状态为 0,这允许构建仍然通过:

warning: puppet-lint: manifests/jenkins.pp:1:1: class not documented
warning: puppet-lint: manifests/jenkins.pp:14:12: indentation of => is not properly aligned (expected in column 13, but found it in column 12)
warning: puppet-lint: manifests/jenkins.pp:15:12: indentation of => is not properly aligned (expected in column 13, but found it in column 12)

我喜欢使用 Warnings 插件来查看 lint 语法。它展示了随时间变化的趋势,但对于适当的持续集成来说,它并不是必需的。

在我们将这段代码通过拉取请求合并到 master 之前,先通过在清单的顶部添加注释并对齐 PDK 包中的箭头来清理我们的 lint 警告:

# Jenkins Profile
class profile::jenkins {

  class { 'jenkins': lts => true }

  package {'git': ensure => latest }

  file {'/tmp/pdk.rpm':
    ensure => file,
    source => 'https://puppet-pdk.s3.amazonaws.com/pdk/1.7.0.0/repos/el/7/puppet5/x86_64/pdk-1.7.0.0-1.el7.x86_64.rpm',
  }

# Install latest PDK directly from Puppet Source
  package {'pdk':
    ensure  => installed,
    source  => '/tmp/pdk.rpm',
    require => File['/tmp/pdk.rpm'],
  }

}

然后,我们可以添加这些更改并将其推送回远程仓库。我们的 Jenkins 扫描将在一分钟内捕获这些更改并给出清晰信号。一旦我们对这些结果感到满意,就可以通过远程仓库上的拉取请求将代码合并回主分支,并再次观察该测试在主分支上的运行。

现在我们已经有了一些基本的验证,可以开始构建一些基本的测试覆盖率,以确保我们的配置文件不会随着时间的推移而失去功能,或者发生回归。

使用 Puppet RSpec 进行单元测试

单元测试是围绕最小的代码单元进行的测试。在 Puppet 中,最小的功能单元是清单。RSpec 为我们提供了一个用于 Puppet 代码的单元测试框架,它能够快速有效地检查我们的 Puppet 代码是否生成了我们预期的 Puppet 清单。不论我们在 RSpec 中编写什么测试,实际上我们是在问:当我执行这段代码时,所需的内容是否会出现在 Puppet 清单中?

RSpec 作为一个系统是在命令行上运行的,并不涉及新的虚拟机或容器。它现在已经包含在 Puppet PDK 中,通过命令 pdk test unit 运行。我们将查看运行单元测试和使用 PDK 提供的模板编写简单单元测试所涉及的文件。

我们正在开始一个新的功能集,因此我们需要从主分支开始,拉取远程提交,并开始一个新分支:

[rary@workstation profile]# git checkout master
Switched to branch 'master'

[rary@workstation profile]# git pull origin master
remote: Counting objects: 1, done.
remote: Total 1 (delta 0), reused 0 (delta 0), pack-reused 0
Unpacking objects: 100% (1/1), done.
From github.com:RARYates/cicd-walkthrough-profile
 * branch master -> FETCH_HEAD
Updating 1b91eec..639f8f6
Fast-forward
 ...

[rary@workstation profile]# git checkout -b rspec
Switched to a new branch 'rspec'

在我们开始使用 RSpec 之前,我们需要一组可以操作的示例文件。在撰写本书时,PDK 中没有命令可以创建单元测试而不创建新的清单。为了克服这个限制,我们只需重命名我们的jenkins.pp文件,使用 PDK 创建一个新类,然后将现有文件重新放回原位覆盖它:

[rary@workstation profile]# mv manifests/jenkins.pp manifests/jenkins.pp.bak;pdk new class jenkins;mv manifests/jenkins.pp.bak manifests/jenkins.pp
pdk (INFO): Creating '/root/profile/manifests/jenkins.pp' from template.
pdk (INFO): Creating '/root/profile/spec/classes/jenkins_spec.rb' from template.
mv: overwrite ‘manifests/jenkins.pp’? y

现在,我们的 jenkins_spec.rb 文件已经根据模板构建完成,准备开始编写 RSpec 单元测试。

相关的 RSpec 文件

文件就位后,让我们检查一下在测试类时,我们将使用的最相关的文件:

  • .fixtures.yml

  • spec/classes/jenkins_spec.rb

spec/spec_helper.rb 为测试套件中的每个测试提供配置和变量。我们在本例中不会编辑它,但请注意,这基本上是所有模块测试的全局配置文件。

.fixtures.yml

我们的 fixtures 文件让我们的测试知道清单所需的依赖项。它位于仓库的根目录,命名为 profile/.fixtures.yml。对于我们特定的配置文件,我们将构建一个包含 rtyler/jenkins 及其所有依赖项的配置文件,以支持我们的测试:

#profile/.fixtures.yml
fixtures:
  repositories:
    jenkins:
      repo: "git://github.com/voxpupuli/puppet-jenkins.git"
      ref: "1.7.0"
    apt: "https://github.com/puppetlabs/puppetlabs-apt"
    stdlib: "https://github.com/puppetlabs/puppetlabs-stdlib"
    java: "https://github.com/puppetlabs/puppetlabs-java"
    zypprepo: "https://github.com/voxpupuli/puppet-zypprepo.git"
    archive: "https://github.com/voxpupuli/puppet-archive.git"
    systemd: "https://github.com/camptocamp/puppet-systemd.git"
    transition: "https://github.com/puppetlabs/puppetlabs-transition.git"

我们使用这个文件在测试中声明一个模块,并通过指向仓库的引用来找到它。在前面的例子中,我们获取了每个模块的最新版本,除了 Jenkins,我们将其固定在 1.7.0,因为我们在 Puppetfile 中使用了这个版本。根据你的代码策略,你可能会选择或不选择指定特定版本的引用,就像我之前所做的那样。

关于配置文件的文档可以在 spec_helper.rb GitHub 仓库中找到,地址是 github.com/puppetlabs/puppetlabs_spec_helper#fixtures-examples

jenkins_spec.rb

在我们的配置文件就位之后,让我们检查一下 PDK 提供的 jenkins_spec.rb 文件:

# Brings in our Global Configuration from spec/spec_helper.rb
require 'spec_helper'

# Tells RSpec with manifest to check, in this case: profile/manifests.jenkins.pp
describe 'profile::jenkins' do

# Runs the test once for each operating system listed in metadata.json, with a suite of default facts
  on_supported_os.each do |os, os_facts|
    context "on #{os}" do
      let(:facts) { os_facts }

# The manifest should compile into a catalog
      it { is_expected.to compile }
    end
  end
end

前面的简单测试只是确保目录在metadata.json中列出的每个操作系统上都能编译。通常,我们会运行这个测试,并收到一个通过的状态。在这个特定的情况下,rtyler/jenkins要求我们提供一个额外的systemd事实,而这个事实在基础的on_supported_os函数中不可用。

在 forge 上检查流行模块的代码示例,尤其是在你将配置文件与现有模块进行测试时。通常,上游模块已经有了修复,就像我们即将实现的修复一样。

我们将编辑现有的spec类,向我们的系统引入一个新事实,以支持systemd

require 'spec_helper'

describe 'profile::jenkins' do
  on_supported_os.each do |os, os_facts|
     context "on #{os}" do

# Add a new ruby variable that returns true when the OS major release version is 6
      systemd_fact = case os_facts[:operatingsystemmajrelease]
                     when '6'
                       { systemd: false }
                     else
                       { systemd: true }
                     end
# Change our facts to merge in our systemd_fact
      let :facts { os_facts.merge(systemd_fact) }

      it { is_expected.to compile }
    end
  end
end

现在,我们的测试将能够编译,因为上游的 Jenkins 模块将拥有它需要的systemd事实。我们来编译一下我们的测试:

[root@pe-puppet-master profile]# pdk test unit
pdk (INFO): Using Ruby 2.4.4
pdk (INFO): Using Puppet 5.5.2
![] Preparing to run the unit tests.
![] Running unit tests.
 Evaluated 4 tests in 3.562477833 seconds: 0 failures, 0 pending.

你可能已经注意到,我们有四个通过的测试。尽管我们只写了一个测试,但我们的on_supported_os函数查看了我们的metadata.json文件,并为每个列出的操作系统提供了一个测试,所有这些操作系统都属于红帽(Red Hat)家族。

扩展我们的 Jenkinsfile

我们将更改我们的 Jenkinsfile,以支持我们新的 RSpec 测试。我们将删除原来的测试阶段,并通过创建验证单元测试阶段来更加清晰。我们将这两个阶段简单地整合为pdk validatepdk test unit

pipeline {
    agent any

    stages {
        stage('Validate') {
            steps {
                sh '/usr/local/bin/pdk validate'
            }
        }
        stage ('Unit Test') {
            steps {
                sh '/usr/local/bin/pdk test unit'
             }
        }
    }
}

这将把我们的流水线分为三个独立的阶段:SCM检出、验证单元测试。我们将能够看到在 Jenkins 中每个步骤的构建是通过还是失败。

现在我们已经为测试奠定了基本框架,让我们将代码推送回远程仓库:

[root@pe-puppet-master profile]# git commit -m 'Initial RSpec Framework'
[rspec 2bc4765] Initial RSpec Framework
 3 files changed, 37 insertions(+), 1 deletion (-)
 create mode 100644 .fixtures.yml
 create mode 100644 spec/classes/jenkins_spec.rb
[root@pe-puppet-master profile]# git push origin rspec
Counting objects: 8, done.
Delta compression using up to 2 threads.
Compressing objects: 100% (5/5), done.
Writing objects: 100% (6/6), 892 bytes | 0 bytes/s, done.
Total 6 (delta 1), reused 0 (delta 0)
remote: Resolving deltas: 100% (1/1), completed with 1 local object.
To git@github.com:RARYates/cicd-walkthrough-profile.git
 * [new branch] rspec -> rspec

回到我们的 Jenkins 实例,我们可以看到新的 RSpec 分支和我们测试的日志。请注意每个部分,此外,我们还可以看到我们的 Jenkins 实例通过了我们的四个 RSpec 测试。

扩展我们的测试

现在我们已经能够编写测试了,我们将编写一个简单的测试,这个测试只是简单地镜像我们的清单。这个测试将帮助我们防止回归,因为更改现有的值或删除现有资源会导致测试失败。如果这是一个预期的更改,测试也必须进行相应的修改。虽然直观上感觉这会拖慢开发速度,但在集成时它能节省更多的时间,因为你可以确保没有引入新的错误。

这是我们的 RSpec 测试,包含我们原始配置文件的镜像:

require 'spec_helper'

describe 'profile::jenkins' do
  on_supported_os.each do |os, os_facts|
    context "on #{os}" do
      systemd_fact = case os_facts[:operatingsystemmajrelease]
                     when '6'
                       { systemd: false }
                     else
                       { systemd: true }
                     end
      let :facts do
        os_facts.merge(systemd_fact)
      end

      ####  NEW CODE  ####

      context 'With Defaults' do
        it do
          # Jenkins must be the LTS
          is_expected.to contain_class('jenkins').with('lts' => 'true')

          # We're unsure if we want latest git, but we want to make sure it's installed
          is_expected.to contain_package('git')

          # Download this particular version of the PDK
          is_expected.to contain_file('/tmp/pdk.rpm').with('ensure' => 'file',
                                                           'source' => 'https://puppet-pdk.s3.amazonaws.com/pdk/1.7.0.0/repos/el/7/puppet5/x86_64/pdk-1.7.0.0-1.el7.x86_64.rpm')

          # Install PDK from Disk. We'll change this test if we place this in a proper yumrepo one day
          # Also not that that_requires, and  the lack of quotes within the File array
          is_expected.to contain_package('pdk').with('ensure'  => 'installed',
                                                     'source'  => '/tmp/pdk.rpm').that_requires('File[/tmp/pdk.rpm]')
        end
      end

      ### END NEW CODE ###

      it { is_expected.to compile }
    end
  end
end

当我们创建一个包含此新测试的提交,并将其推送到 Jenkins 时,我们将看到构建实际上会执行这个测试。到目前为止,我们从未故意让测试失败。现在我们来证明我们的测试。注释掉原始清单中的一个资源,或者在将该仓库提交到远程服务器之前更改某些配置。推送后,你应该能在 Jenkins 中看到一个失败的测试!只需取消注释你的资源,并向远程仓库推送一个新的提交,你将看到 Jenkins 通过这个构建。一旦构建通过,继续合并到主分支,以便我们可以继续进行下一部分的集成测试。

关于编写 RSpec 测试的文档可以参考 rspec-puppet.com/

使用 Test Kitchen 进行验收测试

验收测试是为了验证是否满足需求而执行的测试。虽然 RSpec 是一种快速检查目录是否按预期编译的方式,但它并不实际在系统上运行目录,也无法验证是否能够看到预期的结果。在 Puppet 中,验收测试是将你选择的清单应用到系统上,并验证在目录应用后系统是否满足要求,最好使用一种不是 Puppet Agent 本身的方法。

在本章中,我们将为我们的 Jenkins 配置文件构建一个验收测试,确保 Jenkins 正在运行,并且我们可以通过端口8080访问它,以便查看网页。这超出了 RSpec 的能力,因为 RSpec 实际上并没有构建一个我们可以验证的节点。当我们在 Puppet 中使用验收测试工具时,我们还将它与一个虚拟机管理器(hypervisor)绑定,以便它能够管理一个节点,或称为被测试系统SUT)。

Beaker

Puppet 提供了一个完全足够的验收测试工具——Beaker。Beaker 旨在连接到虚拟机管理器并根据配置文件中的定义启动节点,应用 Puppet 测试。它使用一种简单的语言叫做 Serverspec 来定义测试。它还有一个优点是通过再次运行测试来检查幂等性。Puppet 本身也将它与另一个名为 VMPooler 的应用程序连接,VMPooler 会预先启动一池虚拟机作为 SUT,并在测试完成后替换它们,从而为验收测试提供快速响应时间。如果你所在的组织已经在 CI/CD 流程中走得很远,并且需要虚拟机,我强烈推荐 Beaker。在本节中,我们将在 Test Kitchen 中进行验收测试,仅仅因为我认为它更容易使用,并且提供更多的工作站开发选项。

Test Kitchen 和 kitchen-puppet

测试厨房实际上是 Chef 构建的测试框架。它非常简单易用,而且使用一种比 Serverspec 更容易操作的语言,叫做 Inspec。我们将扩展测试厨房,使用 rubygem kitchen-puppet 来支持 Puppet,该项目可以在 github.com/neillturner/kitchen-puppet 找到。我们需要准备我们的 Jenkins 节点,以便开始利用测试厨房并运行另一组验证测试。

在我们的 Jenkins 节点上准备测试厨房

测试厨房直接支持我们 Puppet 代码的开发活动。在我们的 CI/CD 运行中,我们将使用来自测试厨房的一个复合命令:kitchen test。Kitchen test 是 destroy、create、converge、setup、verify 等命令的编排,带领我们完成清理、构建、应用代码以及测试每一次运行。你可以在本地运行测试厨房,也可以在我们的 CI/CD 系统上运行,这是使用 kitchen-puppet 的最大优势之一。在本节中,我们将添加大量代码,从更新我们的 Jenkins 配置文件到支持测试厨房,再到构建测试和测试厨房配置。

Jenkins 配置文件

我们首先将修改我们的配置文件。在以下示例中,我们将添加以下资源和功能:

  • 如果节点尚未是 Docker 容器,则安装 Docker

  • 安装 RVM、Ruby 2.4.1 及所有测试厨房所需的 RubyGems

我们已经在以下代码中添加了前述资源和功能:

# Jenkins Profile
class profile::jenkins {

  class {'jenkins':
    lts => true,
  }

  package {'git': ensure => latest }

  file {'/tmp/pdk.rpm':
    ensure => file,
    source => 'https://puppet-pdk.s3.amazonaws.com/pdk/1.7.0.0/repos/el/7/puppet5/x86_64/pdk-1.7.0.0-1.el7.x86_64.rpm',
  }

# Install latest PDK directly from Puppet Source
  package {'pdk':
    ensure => installed,
    source => '/tmp/pdk.rpm',
    require => File['/tmp/pdk.rpm'],
  }

  if $::virtual != 'docker' {
    class {'docker':
      docker_users => ['jenkins']
    }
  }

  include rvm

  rvm::system_user { 'jenkins':}

  rvm_system_ruby {'ruby-2.4.1':
    ensure => 'present',
    default_use => true,
  }

  rvm_gem {['ruby-2.4.1/librarian-puppet',
            'ruby-2.4.1/test-kitchen',
            'ruby-2.4.1/executable-hooks',
            'ruby-2.4.1/kitchen-inspec',
            'ruby-2.4.1/kitchen-puppet',
            'ruby-2.4.1/kitchen-docker']:
    ensure => installed,
    require => Rvm_system_ruby['ruby-2.4.1'],
    notify => Service['jenkins'],
  }

}

在我们继续执行本节的其余部分之前,我们需要将这个新配置文件部署到我们的 Jenkins 节点。确保在继续编辑构建之前,将其部署到你的 Puppet Master 上。与 CI/CD 系统的工作有时会让人感觉像是一系列“先有鸡还是先有蛋”的情境。这是正常现象,但这些概念不仅仅局限于我们的 CI/CD 系统。

.kitchen.yml

我们将处理的第一个文件是 .kitchen.yml。这个文件决定了测试厨房如何执行构建。这个 YAML 文件为我们提供了以下内容:

  • Driver:用于以特权用户身份在 Docker 中运行构建,启动时从 init 进程开始。如果你不熟悉容器的工作方式,我们这样设置是为了让它更像传统的虚拟机,而不是仅仅作为一个应用程序的包装器。

  • Provisioner:我们正在设置测试厨房,使用 Puppet 配置器并指定本地清单和模块路径来构建。

  • Verifier:使用 Inspec 进行测试。

  • Platforms:我们将配置我们的容器以使用 CentOS SystemD 容器。我们传递额外的命令以确保 SSH 正常工作,并且初始化脚本可供 Jenkins 运行使用。

  • 套件:这个术语用于描述我们运行的每一个测试套件。第一个套件是通过我们测试目录中的 jenkins.pp 定义的,这是一个简单的 include profile::jenkins,就像我们在 example.pp 中可能看到的那样。请注意这个套件中的预验证阶段,它给我们的 Jenkins 实例 30 秒的时间来完成启动,然后再进行测试:

---
driver:
  name: docker
  privileged: true
  use_sudo: false
  run_command: /usr/sbin/init

provisioner:
  name: puppet_apply
  # Not installing chef since inspec is used for testing
  require_chef_for_busser: false
  manifests_path: test
  modules_path: test/modules

verifier:
  name: inspec

platforms:
- name: centos
  driver_config:
    image: centos/systemd
    platform: centos
    run_command: /usr/sbin/init
    privileged: true
    provision_command:
      - yum install -y initscripts
      - sed -i 's/UsePAM yes/UsePAM no/g' /etc/ssh/sshd_config
      - systemctl enable sshd.service

suites:
  - name: default
    provisioner:
      manifest: jenkins.pp
    lifecycle:
      pre_verify:
      - sleep 30

.kitchen.yml 也适用于本地环境,让我们可以在将代码推送到远程代码库之前运行测试并验证它们。如果我们想检查本地系统上的最终状态,我们也可以使用 kitchen converge 来构建机器并应用代码。

Puppetfile

kitchen-puppet gem 通过 Puppetfile 运行。在后台,它使用一个叫做 librarian-puppet 的工具来拉取 Puppetfile 中的所有模块和依赖项。Librarian 和 r10k 是同时出现的,r10k 不提供自动依赖解析,更倾向于显式命名。由于我们使用了 Puppet Librarian,我们明确添加了对 Java 和 Apt 的排除,因为我们两年前的 Puppet 模块锁定了旧版本。我们的 Jenkins 模块在现代 Java 和 Apt 版本下运行得很好,但必须禁止自动依赖解析,以避免构建失败:

forge 'https://forge.puppetlabs.com'
mod 'rtyler/jenkins',
  :git => 'https://github.com/voxpupuli/puppet-jenkins.git',
  :ref => 'v1.7.0'

#mod 'puppetlabs-stdlib'
mod 'darin-zypprepo'
mod 'puppet-archive'
mod 'camptocamp-systemd'
mod 'puppetlabs-transition'
mod 'maestrodev-rvm'
mod 'puppetlabs-docker'

mod 'puppetlabs-java'
mod 'puppetlabs-apt'

exclusion 'puppetlabs-apt'
exclusion 'puppetlabs-java'

Jenkinsfile

我在我们的 Jenkinsfile 中添加了两个新对象:一个由 shell 脚本提供的集成测试,以及一个后置操作,告诉 Jenkins 清理我们的工作空间。我们使用外部脚本而不是内联执行,是为了更容易进行管理,因为每个 sh 步骤都是 Jenkins 中的独立 shell。我们的后置清理操作确保我们不会保留来自上次构建的任何遗留物:

pipeline {
    agent any

    stages {
        stage('Validate') {
            steps {
                sh '/usr/local/bin/pdk validate'
            }
        }
        stage ('Unit Test') {
            steps {
                sh '/usr/local/bin/pdk test unit'
             }
        }
        stage ('Integration Test') {
            steps {
                sh './acceptance.sh'
             }
        }
    }
post {
        always {
            deleteDir()
        }
    }
}

acceptance.sh

我们的接受脚本相对较小,但允许 Jenkins 在运行 Kitchen 测试之前,在 RVM 中为这个构建和源代码提供路径。我们希望确保构建保持一致,因此也希望控制构建周围的环境:

#profile/acceptance.sh
#!/bin/bash
PATH=$PATH:/usr/local/rvm/gems/ruby-2.4.1/bin/:/usr/local/bin
source /usr/local/rvm/bin/rvm
/usr/local/rvm/gems/ruby-2.4.1/wrappers/kitchen test

测试

我们的实际测试本身是我们新迭代中最简单的文件之一。我们将其放置在默认文件夹中,这样它就能被我们之前提到的默认套件找到。我们正在构建一个单一的控制或测试集,共包含三个测试:

  • 确保 Jenkins 包已安装

  • 确保 Jenkins 服务正在运行

  • 确保 Jenkins 可以在本地主机的 8080 端口访问,并返回 200 的退出状态:

# profile/integration/default/jenkins_spec.rb
control 'Jenkins Status' do
  describe package('jenkins') do
    it { is_expected.to be_installed }
  end

  describe http('http://localhost:8080', open_timeout: 60, read_timeout: 60) do
    its('status') { is_expected.to cmp 200 }
  end

describe service('jenkins') do
    it { is_expected.to be_running }
  end
end

执行测试

现在我们已经把所有组件准备好,接下来让我们把代码部署到我们的代码库,并让 Jenkins 运行任务。如果你还没有运行新的 Jenkins 配置文件,你需要确保它已经部署到主机,并且 Jenkins 节点已经同步完毕。一旦我们将测试推送到 CI/CD 系统,它将读取我们的代码并开始测试。特别需要注意的是,这个测试将比我们之前编写的测试花费显著更长的时间,因为容器需要被下载、构建、启动、同步和测试,而不像我们的 PDK 命令仅仅检查语法或编译快速目录。

本章我们创建了许多文件,接下来快速回顾一下我们管理过的文件,忽略软件自动生成的文件:

rary at Ryans-MacBook-Pro-3 in ~/workspace/packt
$ tree cicd-walkthrough-profile
cicd-walkthrough-profile
├── Jenkinsfile # Test to be Performed
├── Puppetfile # Dependencies for Kitchen Tests
├── acceptance.sh # Command to run Test Kitchen for Jenkins
├── manifests
│   └── jenkins.pp # Jenkins Profile
├── spec
│   ├── classes
│   │   └── jenkins_spec.rb # Our Inspec test for the Kitchen Phase
└── test
 ├── integration
 │   └── default
 │       └── jenkins_spec.rb # Our RSpec Test, checking the Catalog
 └── jenkins.pp # Our example manifest that applies the Profile for Kitchen

13 directories, 18 files

总结

本章我们专注于构建 CI 系统(Jenkins)并执行验证检查、单元测试和验收测试。CI/CD 是一个持续的过程,我们的工作流总是有改进的空间。持续集成为我们提供了一个宝贵的安全网,让我们在开发过程中无需担心功能丢失或回归问题。

从这里开始,有哪些地方可以去呢?通过使用 Git 钩子将 Git 系统与 Jenkins 更紧密地集成,部署代码并在拉取请求添加之前提供状态反馈。你还可以向开发者发送通知,提醒他们测试结果从通过变为失败。如果你觉得这些警告有些过多,可以调整提供警告的系统,以避免某些错误的出现。每个人的 CI/CD 之路都是不同的,所以自己去探索,找出适合你的方法!

下一章将介绍 Puppet 任务和 Puppet 探索。Puppet 任务让我们可以运行临时命令,并将其作为命令式脚本的构建块。我们将构建一个任务来检查日志文件,并计划为我们的 Puppet 主机构建一个汇总的日志文件。Puppet 探索让我们能够检查现有的基础设施,并确定虚拟机或容器中的软件包、服务、用户以及其他各种组件的真实情况。

第八章:通过任务和发现扩展 Puppet

自从 Puppet 5 发布以来,Puppet 宣布了三项新服务:任务、发现和流水线。Puppet 任务为我们提供了一个命令式的解决方案,用于自动化临时任务。Puppet 发现使我们能够发现基础设施的状态。Puppet 流水线将在下一章简要讨论,涵盖应用程序级别的 CI/CD。

在本章中,我们将研究并使用 Puppet 任务来帮助管理 Web 服务器。我们将介绍一些最佳实践,并探讨使用 Puppet 任务的合适时机。接下来,我们将深入了解 Puppet 发现并检查我们的基础设施。我们将使用 Puppet 发现来做出智能决策,决定在基础设施中自动化哪些内容。

Puppet 任务

Puppet 旨在在基础设施中的节点上持续执行最终状态的强制执行。虽然 Puppet 可以涵盖大多数基础设施任务,但有些任务最好留给临时任务来处理。Puppet 任务是可以在节点和容器上按需运行的操作。你可以像编写脚本一样编写任务,并且它们可以用目标节点上任何可用的语言编写。在选择任务还是 Puppet 清单时,我有一个简单的思考过程:这是我希望永久存在的东西,还是一个单次执行的操作?

让我们考虑一些在正常工作场所中是永久性或状态性的事物。我工作地点的物理地址、建筑物、房间和家具是我希望永久强制执行的物理事物的例子。像每周会议或每日站会也应该是持续强制执行的内容,作为业务规则。所有这些事物都有组成部分,从砖瓦到每周站会的时间和地点。如果我们能用 IT 工具管理现实世界,Puppet 将是描述我们办公室和业务规则的完美工具,这些规则预计会保持不变。

在同样的背景下,一次临时会议或下班后的活动由一系列任务组成,这些任务只执行一次,但每次大多数情况下都以相同的方式执行(带有变量)。如果客户订购某些东西,我们会使用任务来交付请求。如果请求是定制的,我们将使用一系列任务来构建整个复合体。这些是我们一贯做的事情,但会有变化,并且发生在不确定的时间点。一个外部事件或人推动了这项工作的创建,但我们尝试通过自动化的方式来重复这些操作,以节省时间并提高一致性。

任务和 Puppet 在管理上的主要区别在于命令式和声明式模型。在本节中,我们将设置 Bolt(驱动任务的技术),使用 Puppet 构建一个 Web 服务器,然后通过 Bolt 根据需求部署我们的网站。

Bolt

Bolt 是 Puppet 任务的主要驱动程序,它是一个用 Ruby 编写的开源项目,用于通过 SSH 和 WinRM 远程执行任何语言的脚本。你可以用任何受目标主机支持的语言编写任务,例如在 Windows 和 Linux 上使用 PowerShell 和 Bash,或者如果有解释器的话,可以使用 Ruby 和 Python。Bolt 被设计为一个无代理的系统,通过标准协议分发脚本并执行远程命令,使用 SSH 公钥加密或用户名和密码。此外,还有一个内置的命令行工具,用于通过 PuppetDB 查询构建库存文件。Bolt 还支持任务计划,打包在 forge 模块中,将多个任务连接在一起,提供更复杂的任务。

安装 Bolt

可以通过多种方法安装 Bolt,所有方法的描述都可以在puppet.com/docs/bolt/0.x/bolt_installing.html中找到:

  • downloads.puppet.com/下载的软件包

  • 一个公共的 Chocolatey 软件包

  • OSX Homebrew 安装

  • Linux 本地软件包仓库

  • Rubygems

Bolt 通过标准连接协议远程工作。尝试在本课中将其安装并在工作站上使用,而不是在 Puppet Master 上使用。

在我的 MacBook 上,我将使用 Homebrew 安装 Bolt:

rary at Ryans-MacBook-Pro in ~/workspace/packt
$ brew cask install puppetlabs/puppet/puppet-bolt
==> Tapping puppetlabs/puppet
Cloning into '/usr/local/Homebrew/Library/Taps/puppetlabs/homebrew-puppet'...
remote: Counting objects: 15, done.
remote: Compressing objects: 100% (14/14), done.
remote: Total 15 (delta 1), reused 8 (delta 1), pack-reused 0
Unpacking objects: 100% (15/15), done.
Tapped 3 casks (49 files, 54.9KB).
==> Satisfying dependencies
==> Downloading https://downloads.puppet.com/mac/puppet5/10.13/x86_64/puppet-bolt-0.22.0-1.osx10.13.dmg
######################################################################## 100.0%
==> Verifying SHA-256 checksum for Cask 'puppet-bolt'.
==> Installing Cask puppet-bolt
==> Running installer for puppet-bolt; your password may be necessary.
==> Package installers may write to any location; options such as --appdir are ignored.
Password:
installer: Package name is puppet-bolt
installer: Installing at base path /
installer: The install was successful.
puppet-bolt was successfully installed!

然后,我会关闭我的终端,重新打开并验证bolt命令是否在我的路径中:

$ bolt
Usage: bolt <subcommand> <action> [options]

Available subcommands:
 bolt command run <command> Run a command remotely
 bolt file upload <src> <dest> Upload a local file
 bolt script run <script> Upload a local script and run it remotely
 bolt task show Show list of available tasks
 bolt task show <task> Show documentation for task
 bolt task run <task> [params] Run a Puppet task
 bolt plan show Show list of available plans
 bolt plan show <plan> Show details for plan
 bolt plan run <plan> [params] Run a Puppet task plan
 bolt puppetfile install Install modules from a Puppetfile into a Boltdir

Run `bolt <subcommand> --help` to view specific examples.

管理节点

在 Bolt 中,我们必须明确列出要管理的节点。我们可以通过--nodes命令标志来实现,或者提供一个库存文件。库存文件是一个包含节点组和已经设置好的配置选项的 YAML 文件。默认情况下,Bolt 会使用放在~/.puppetlabs/bolt/inventory.yamlinventory文件。在这一部分中,我们将只针对 Puppet Master,因此我会确保它出现在inventory文件中:

# ~/.puppetlabs/bolt/inventory.yaml
---
groups:
 - name: puppetserver
 nodes:
 - pe-puppet-master.puppet.net
 config:
 transport: ssh
 ssh:
 user: root

在我可以将 Bolt 运行到服务器之前,我需要确保我的 SSH 密钥作为 root 用户在该系统上可用。我将使用ssh-copy-id工具将其从我的 UNIX 系统传输到 root 用户:

rary at Ryans-MacBook in ~/workspace/packt
$ ssh-copy-id root@pe-puppet-master.puppet.net

/usr/bin/ssh-copy-id: INFO: attempting to log in with the new key(s), to filter out any that are already installed
/usr/bin/ssh-copy-id: INFO: 1 key(s) remain to be installed -- if you are prompted now it is to install the new keys
root@pe-puppet-master.puppet.net's password:

Number of key(s) added: 1

Now try logging into the machine, with: "ssh 'root@pe-puppet-master.puppet.net'"
and check to make sure that only the key(s) you wanted were added.

临时命令

在 Puppet Bolt 的核心功能中,我们执行远程命令、发送脚本并运行脚本。Bolt 提供了三个简单的命令来实现这一点:bolt command runbolt file uploadbolt script run。为了测试我们之前的 SSH 密钥,让我们使用bolt command run运行一个简单的命令:

rary at Ryans-MacBook-Pro-3 in ~/workspace/packt/bolt
$ bolt command run "echo 'Hello World'" --nodes puppetserver --no-host-key-check
Started on puppetserver.puppet.net...
Finished on puppetserver.puppet.net:
 STDOUT:
 Hello World
Successful on 1 node: pe-puppet-master.puppet.net
Ran on 1 node in 0.40 seconds

对于简单的临时任务,运行bolt命令是检查系统的好方法。当我们需要发送更多的指令时,我们就需要编写脚本并远程运行它。这里是一个简单的脚本,返回用户和所有开放端口:

#./inspect.sh

#!/bin/bash

echo 'Users:'
cat /etc/passwd | cut -f 1 -d ':'
echo 'Ports:'
netstat -tulpn

当我们通过bolt script run运行这个脚本时,我们会看到如下输出:

$ bolt script run inspect.sh --nodes puppetserver --no-host-key-check
Started on puppetserver.puppet.net...
Finished on puppetserver.puppet.net:
 STDOUT:
 Users:
 root
 ...
 vboxadd
 vagrant
 Ports:
 Active Internet connections (only servers)
 Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
 tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN 1258/sshd
 ...
Successful on 1 node: puppetserver.puppet.net
Ran on 1 node in 0.85 seconds

最后,如果我想将这个脚本提供给 Puppet 服务器上的本地用户,我可以通过bolt script upload将其发送过去:

rary at Ryans-MacBook in ~/workspace/packt/bolt
$ bolt file upload inspect.sh /tmp/inspect.sh --nodes puppetserver --no-host-key-check
Started on puppetserver.puppet.net...
Finished on puppetserver.puppet.net:
 Uploaded 'inspect.sh' to 'puppetserver.puppet.net:/tmp/inspect.sh'
Successful on 1 node: puppetserver.puppet.net
Ran on 1 node in 0.66 seconds

Bolt 任务

Bolt 任务允许我们编写并扩展脚本,加入额外的元数据参数。这些参数可以通过环境变量、PowerShell 命名参数或在更复杂的情况下作为 JSON 输入提供。Bolt 任务类似于 Puppet 中的资源,允许我们参数化某个操作,并以可重复的方式使用该命令。我们将编写一个简单的任务,允许我们通过名称检查 Puppet Master 上的特定日志文件。这个任务将是名为 puppetserver 的日志模块的一部分。

task.json

这个 JSON 参数文件是任务的可选组件,允许将参数作为环境变量传递给我们的脚本。我们也可以使用这个文件限制用户输入,如果需要,可以仅提供少数几个选项。在下面的示例中,我们的脚本将接受一个日志并存储该参数。日志参数将只允许三种选择,用于确定用户要查找的日志文件位置。store 参数默认关闭,但将允许我们聚合日志,用于我们将在下一节中构建的计划:

#logs/tasks/puppetserver.json
{
  "puppet_task_version": 1,
  "supports_noop": false,
  "description": "Retrieve a log file from the puppetserver",
  "parameters": {
    "log": {
      "description": "The Puppetserver log you want to read",
      "type": "Enum[console,puppetdb,puppetserver]"
    },
    "store": {
      "description": "Store logfile in /tmp/puppetlog.log",
      "type": "Optional[Boolean]"
    }
  }
}

参数使用与 Puppet 相同的数据类型。你可以使用 Puppet 支持的任何数据类型作为 Puppet 任务的数据类型。

任务

我们的任务将是一个简单的 shell 脚本,基于我们的输入参数读取一个指定的文件,决定是否存储输出,然后将输出以 JSON 格式返回给 Bolt。返回为 JSON 格式非常重要,这样它才能被 Bolt 接收。在更复杂的用例中,我们甚至可以使用这个 JSON 将键值对传递给计划中的后续任务,这部分将在下一节中讲解。

任务可以用系统支持的任何语言编写。这个示例将使用 Bash,因为几乎每个管理员都使用过它。如果你还没有尝试过用 Python、Ruby、Golang 或任何其他脚本语言(除了 shell)写脚本,试试看。这些任务在更高级的语言中实际上更容易编写。

在我们的 shell 脚本中有几个需要注意的事项:

  • 从我们的 JSON 参数文件返回的值会成为环境变量,并以 PT_ 开头。我们的脚本通过 $PT_log$PT_store 来检查通过命令行传递的值。

  • 我们使用了一个 case 语句将 $PT_log 映射到一个日志文件。这种用法类似于 Puppet 中的选择语句。

  • 如果 $PT_store 为真,我们将生成一个可以追加的日志文件。

  • 最后一行的日志以 JSON 格式打印出来,以便 Puppet Tasks 知道它是一个有效的命令行输出:

# logs/tasks/puppetserver.sh
#!/bin/sh

# Map $PT_log to a $logfile variable
case "$PT_log" in
 'console') logfile='/var/log/puppetlabs/console-services/console-services.log' ;;
 'puppetdb') logfile='/var/log/puppetlabs/puppetdb/puppetdb.log' ;;
 'puppetserver') logfile='/var/log/puppetlabs/puppetserver/puppetserver.log' ;;
esac

# Variable that stores all the text from inside the logfile
log=`cat $logfile`

# If store is true, build a header and then print out $log
if [ $PT_store == 'true' ]
then
 echo "${PT_log}\r============" >> /tmp/puppetlog.log
 echo $log >> /tmp/puppetlog.log
fi

# print out the key value of "<chosen log>":"all log contents" in JSON to be
# read by the Bolt interpreter
echo -e "{'${PT_log}':'$log'}"

在运行命令之前,让我们再检查一下我们写的文件是否在正确的位置:

logs
├── files
├── manifests
├── tasks
│   ├── puppetserver.json
│   └── puppetserver.sh
└── templates

然后我们可以在命令行上运行我们的命令。我们添加了一些有助于执行的参数:

  • nodes:这决定了基于我们的清单文件,在哪些节点上执行任务。

  • modulepath:查找模块的路径。由于我们直接在这个模块上工作,因此我们将 modulepath 设置为模块上级目录。

  • --no-host-key-check:你可能不需要这个标志,但为了简化本节中 SSH 故障排除的过程,我们将使用这个标志。

  • log=puppetdb:这是我们在 JSON 文件中写入的参数。它将被转换为 $PT_log 并用于我们的 shell 脚本:

$ bolt task run logs::puppetserver --nodes puppetserver --modulepath .. log=puppetdb --no-host-key-check

Started on pe-puppet-master.puppet.net...
Finished on pe-puppet-master.puppet.net:
 {'puppetdb':'2018-09-23T00:20:55.115Z INFO [p.p.command] [8-1537662054876] [212 ms] 'replace facts' command processed for pe-puppet-master
 2018-09-23T00:21:12.077Z INFO [p.p.command] [9-1537662071679] [370 ms] 'store report' puppet v5.5.2 command processed for pe-puppet-master
 2018-09-23T00:21:53.936Z INFO [p.p.c.services] Starting sweep of stale nodes (threshold: 7 days)
 ...'}
 {
 }
Successful on 1 node: pe-puppet-master.puppet.net
Ran on 1 node in 0.89 seconds

尝试自己运行这个命令。它会为每个命令返回不同的日志文件,如果你传递 store=true,它甚至会开始将这个日志附加到 /tmp 中名为 puppetlog.log 的文件里。

Bolt 计划

如果 Puppet 任务是我们的命令资源,那么 Puppet 计划就是我们的 Puppet 清单。在这里,我们结合多个任务和命令来形成一个协调的计划。这些计划采用与 Puppet 代码相同的 DSL 编写,尽管在写这本书时,只能使用 puppet 函数,而且没有包含像资源或类这样的对象。

在我们的示例计划中,我们将引入两个参数:

  • $enterprise:这个参数用于确定是否在计划中检查 pe-console-services(也可以使用目标或 PuppetDB 中的 facts)。

  • $servers:这是一个服务器列表,作为一个以逗号分隔的列表传递。

我们的任务将清理现有的日志并生成一组新的日志。这个脚本会为每个部分运行我们在上一节中构建的日志抓取任务,并将所有日志聚合在一起。Enterprise 作为一个可选标志,将决定是否也包含 pe-console-services.log。在我们构建完日志之后,我们将简单地读取日志文件,并确保它通过 return 函数返回到命令行。最后,我们会清理我们自己,并清理我们刚刚在 /tmp 中生成的聚合日志:

# logs/plans/puppetserver.pp
plan logs::puppetserver (
  Boolean $enterprise,
  TargetSpec $servers,
) {

  run_command('rm -f /tmp/puppetlog.log', $servers)
  run_task('logs::puppetserver', $servers, log => 'puppetserver', store => true)
  run_task('logs::puppetserver', $servers, log => 'puppetdb', store => true)

  if $enterprise == true {
    run_task('logs::puppetserver', $servers, log => 'console', store => true)
  }

  return run_command('cat /tmp/puppetlog.log', $servers)
  run_command('rm -f /tmp/puppetlog.log', $servers)

}

一旦我们构建好计划,就可以运行 bolt plan run,并传入我们的 modulepath 和参数:

rary at Ryans-MacBook-Pro-3 in ~/workspace/packt/logs
$ bolt plan run logs::puppetserver --modulepath .. --no-host-key-check enterprise=false servers=root@pe-puppet-master
Starting: plan logs::puppetserver
Starting: command 'rm -f /tmp/puppetlog.log' on root@pe-puppet-master
Finished: command 'rm -f /tmp/puppetlog.log' with 0 failures in 0.38 sec
Starting: task logs::puppetserver on root@pe-puppet-master
Finished: task logs::puppetserver with 0 failures in 0.39 sec
Starting: task logs::puppetserver on root@pe-puppet-master
Finished: task logs::puppetserver with 0 failures in 0.45 sec
Starting: command 'cat /tmp/puppetlog.log' on root@pe-puppet-master
Finished: command 'cat /tmp/puppetlog.log' with 0 failures in 0.15 sec
Finished: plan logs::puppetserver in 1.39 sec
[
 {
 "node": "root@pe-puppet-master",
 "status": "success",
 "result": {
 "stdout": "puppetserver\n============\n2018-09-23T00:20:54.905Z INFO [qtp417202273-69] [puppetserver] Puppet 'replace_facts' command for pe-puppet-master submitted to PuppetDB with UUID fc691079-debf-4c99-896b-3244f353a753\n2018-09-23T00:20:55.268Z ERROR [qtp417202273-69] [puppetserver] Puppet Could not find node statement with name 'default' or 'pe-puppet-master' on node pe-puppet-master\n ...",
 "stderr": "",
 "exit_code": 0
 }
 }
]

你可能会注意到日志以一个大的 JSON 对象的形式返回,且没有显示行间断。如果你想查看这个聚合的日志文件,可以尝试运行以下命令并检查新的 puppetlog.log 文件:

$ rm -f *.log;bolt plan run logs::puppetserver --modulepath .. --no-host-key-check enterprise=false servers=root@pe-puppet-master > compressed.log; head -n 6 compressed.log | tail -n 1 | awk '{gsub("\\\\n","\n")};1' > puppetlog.log

Puppet Enterprise 任务管理

Bolt 是一个功能齐全的开源产品。它在你的环境中运行良好,并不需要 Puppet Enterprise。话虽如此,Puppet Enterprise 的控制台与 Bolt 非常契合。在控制台的左侧有一个任务页面,点击后将带你进入主要的任务页面。一旦进入,你将看到“运行任务”页面,如果你在组织内共享任务,这些页面会为你提供一些便利的功能。

本节仅对 Puppet Enterprise 用户相关。此模块需要通过 r10k 或手动放置到 /etc/puppetlabs/code/environments/production/modules 目录下,才能被 Puppet Enterprise 控制台读取。

第一个主要特性是能够在运行任务之前直接查看支持的 JSON 参数文件。请注意,当我们添加logs::puppetserver时,描述和可选参数会在任务中呈现,这样能方便其他用户查看文档:

每个参数也以下拉菜单的形式呈现。由于我们在puppetserver.json中选择了 Enum[console,puppetdb,puppetserver] 作为我们的类型,因此在控制台中,用户只能选择这些选项。Store 也仅是一个真假值的下拉选项,这要归功于我们的布尔选择:

一旦我们运行任务,我们将得到一个清理过的日志版本,如果您所在的组织较大,您可以将该任务加入库存,并允许管理员远程查看日志文件,而无需登录到服务器或管理代码:

这个任务本意是一个简单的示例。对于复杂的任务和计划,您可以在基础设施中使用 SSH 或 WinRM 自动化执行任何类型的操作,且支持任何语言。我们的任务具有导入和导出 JSON 变量的能力,这使得我们能够在任务之间构建更复杂的依赖关系。Puppet Tasks 对 Puppet 生态系统来说仍然相对较新,但它是一个有前景的新功能,允许在组织内部快速共享管理自动化任务。

Puppet Discovery

Puppet Discovery 是 Puppet 推出的新产品。Puppet Discovery 是一个独立的容器化应用程序,旨在实时发现关于容器和虚拟机的信息。该平台的设计目标是拥有所有 IT 资源的库存,发现每个资源的详细信息,并对这些机器采取行动。尽管仍处于开发初期阶段,但我预计 Discovery、Puppet Tasks 以及更广泛的 Puppet 生态系统之间会有更紧密的集成。

Puppet Discovery 一般是安全的,可以用于检查生产级别的系统。Puppet Discovery 会对所有来源进行主动扫描,可能会在您的组织中触发安全警告。如果您决定在公司资源上使用 Puppet Discovery,请确保与安全团队协调。

在本节中,我们将安装 Puppet Discovery,并查看我们可以使用的功能。我们将首先安装系统,然后添加我们基础设施机器的 IP CIDR 块,接着使用凭据连接到机器。然后,我们将探索 Puppet Discovery,查看我们基础设施中各个节点和包的详细信息。

如果没有提前通知安全团队,这可能会在生产环境中触发安全警报。

安装 Puppet Discovery

Puppet Discovery 不是自由开源软件FOSS)。我们需要从 Puppet 获得一个许可证,可以在licenses.puppet.com获取。选择一个可用的 Puppet Discovery 许可证以开始使用,然后将其下载到你运行 Puppet Discovery 的目标机器上。此 JSON 文件将在 Puppet Discovery 应用程序的安装过程中使用。

你需要在机器上安装 Docker。为了安装 Puppet Discovery,主机上必须安装 Docker。

准备 Puppet Discovery

puppet.com/download-puppet-discovery下载适用于你的操作系统的 Puppet Discovery。本节将帮助我们将二进制文件放入路径中,并首次设置 Puppet Discovery。

下载 Puppet Discovery 后,我们需要将二进制文件移动到路径中。在大多数基于 Unix 的操作系统中,/usr/local/bin 已经包含在路径中。我们需要将二进制文件放入路径,确保它可执行,并确保我们可以作为本地用户运行它:

如果 /usr/local/bin 不在你的路径中,你可以使用 echo $PATH 查看系统路径中包含的目录。返回的结果将是以冒号分隔的目录列表。

rary at Ryans-MacBook-Pro in ~/workspace
$ mv ~/Downloads/puppet-discovery /usr/local/bin

rary at Ryans-MacBook-Pro in ~/workspace
$ chmod a+x /usr/local/bin/puppet-discovery

rary at Ryans-MacBook-Pro in ~/workspace
$ puppet-discovery
A discovery application for cloud-native infrastructure

 Find more information at https://puppet.com/products/puppet-discovery

Usage:
 puppet-discovery [command]

...

一旦验证二进制文件工作正常,我们将运行 puppet-discovery start 启动服务。系统会提示我们提供许可证密钥,阅读最终用户许可协议(该协议将在浏览器中弹出)并生成管理员密码:

rary at Ryans-MacBook-Pro in ~
$ puppet-discovery start
Please enter the path to your Puppet Discovery license: Documents/License-puppet-discovery-trial-2018-10-23.puppet_discovery.json

By continuing with installation, you agree to terms outlined in the Puppet Discovery End User License Agreement located here: /Users/rary/.puppet-discovery/data/puppet-discovery-eula-1537730629.html

Do you agree? [y/n]: y

*************************************************************************
* NOTE: If you forget your password you lose all of your discovery data *
*************************************************************************

Password requirements:
* Password must have at least 6 characters
* Password must use at least 3 of the 4 character types: lowercase letters, uppercase letters, numbers, symbols
* Password cannot be the same as current password

Please create an admin password: **************
Verify by entering the same password again: **************

Puppet Discovery: started 15s [====================================================================] 100%
Puppet Discovery: pulled [8/8] 1m3s [====================================================================] 100%
Opening Puppet Discovery at https://localhost:8443 ...

一旦完成此步骤,Puppet Discovery 将在目标机器上的 8443 端口以 Docker 容器的形式运行。

在撰写本书时,许可证提示使用的是相对路径,而不是绝对路径,因此确保你从能够找到该 JSON 文件的位置运行此命令。

管理源

我们初次登录时,除非提供了目标机器的基本列表和凭证,否则不会进入欢迎界面。Puppet Discovery 能够连接到整个 Amazon Web Services、Google Compute Platform、Microsoft Azure 或 VMWare VSphere 帐户,并执行可用资源的自动发现。如果没有可用的 API 驱动平台,我们也可以提供一个直接的 IP 地址列表。

在本节中,我们将向 Discovery 添加一个 CIDR 地址块,该地址块将对所有用户可用,无论平台和虚拟化技术如何。

通过 IP 地址添加源

如果你使用云服务提供商进行测试,可以直接使用云提供商的服务。此节的其余部分将不依赖于我们连接到机器的具体方法。

在本书编写期间,我的 Puppet 基础设施中创建了多个节点,以便我们能够检查它们。我使用的是 Vagrant 和 VirtualBox 作为平台,并且我将使用我的本地网络 10.20.1.0/24 来发现我所有的 Puppet 基础设施。在选择你将用来演示这一部分的 IP 地址时,确保你安装了 Docker 的机器能够在提供的网络上找到节点:

管理凭证

在我们第一次列出要发现的节点后,Puppet Discovery 会自动带我们进入一个欢迎页面,让我们选择一个身份验证方法。在编写本书时,有三种方法可用:SSH 私钥、SSH 凭证和 WinRM 凭证。SSH 私钥通常是最安全的方法,但如果远程系统上没有 SSH 密钥,则可以通过 SSH 凭证(用于 Linux)或 WinRM 凭证(用于 Windows)输入用户名和密码。

在这一部分,我们将使用 SSH 密钥提供与我们在前一步中发现的机器的连接。

SSH 密钥文件

如果你在测试中使用 vagrant,而不是云服务提供商,我只是使用 vagrant 提供的默认不安全密钥。这个密钥可以在github.com/hashicorp/vagrant/tree/master/keys找到。

在添加凭证时,我们也在限定凭证的作用范围。在 SSH 私钥凭证中,你需要从本地硬盘选择你希望使用的 PEM 文件。我们有三种可用的 RBAC 选项:

  • 在主机上发现数据:这个密钥是否应该用于发现信息?

  • 在目标主机上运行任务:这个密钥是否应该能够运行和执行任务?

  • 提升权限至 root:这个用户是否应该成为发现和任务的 root 用户?

最后,我们有一个用户名和密码短语。我们的用户名是我们希望连接到远程机器的用户。由于我的机器都是在 vagrant 上,我也会使用 vagrant 作为我连接的用户。密码短语用于解密 SSH 密钥,如果你的密钥没有密码短语,就像我的一样,则可以选择不使用:

一旦我们设置了第一组主机和凭证,我们就可以开始使用 Puppet Discovery。

正在发现

Puppet Discovery 可能需要一些时间来收集你基础设施的所有信息。此外,浏览器缓存可能会导致发现后该页面无法显示数据。你可能需要等待并清除缓存,然后才能在仪表盘上看到数据填充。

我们的欢迎页面现在显示了所有主机、软件包和容器,这些可以在我们提供的所有来源和所有输入的身份验证方法下找到。这个仪表盘是交互式的,点击任意框将带你进入一个视图,展示所有代表仪表盘上信息的节点:

如果你对发现这些节点的过程感兴趣,你可以点击发现页面左上角的“历史事件”图标,查看发现日志。

查看发现结果

在我的原始示例中,我提供了 10.20.1.0/24 的 CIDR 块进行扫描。Puppet Discovery 使用我提供的凭据尝试连接整个 IP 范围,并返回了我的所有节点。你可能已经注意到我有一个失败的节点,实际上它是我的网关,不能使用我的凭据登录:

发现主机

返回仪表板后,我们可以选择“主机”来查看所有主机的列表,而不限定具体信息。我们将看到关于这些主机的基本信息,包括操作系统和机器本身的运行时间:

如果我们选择任何单个节点的超链接,我们将获得一个更有用的对象列表,可以帮助我们获得每个主机的详细信息。每个标签页将向我们展示不同的信息:

  • 属性:Puppet Discovery 本身使用的主要属性,包括主机名、DNS 名称和操作系统详细信息

  • 服务:节点上的所有服务及其当前状态(运行中,已停止)

  • 用户:系统上的所有用户及其主目录

  • :系统上所有可用的组

  • 软件包:系统上的每个软件包、它们的版本以及安装它们所使用的方法

  • 标签:云提供商列出的任何标签

  • 容器:主机系统上运行的所有容器

发现软件包

我们可以在 Puppet Discovery 中整体查看软件包。当你从仪表板选择软件包时,将会跳转到一个页面,列出所有软件包、它们的版本、包管理器,以及最重要的,它们运行的实例数量。我们可以利用这些信息查看软件是否已经在我们的基础设施中广泛安装,或者跟踪基础设施中的版本变化。这些信息在安全修复中尤其有用,特别是在尝试确定基础设施中存在漏洞的系统时:

执行动作

Puppet Discovery 还允许我们对基础设施执行一小部分操作:安装 Puppet 代理和管理服务。在未来,Puppet Discovery 可能还会包括通过基础设施联合执行任务的能力。你可以通过选择 Puppet Discovery 顶部栏中的 Act + 图标来访问这些操作。你将被重定向到选择任务页面:

安装代理

使用 Puppet Discovery 安装代理是通过基础设施安装 Puppet 代理的最简便方法之一。你现在可以提供以下参数,并将任务应用于主机列表:

  • master:要使用的 Puppet 主机。这是唯一的必选参数。

  • cacert_content:主机应返回的预期 CA 证书。

  • certname:代理的证书名称。

  • environment:节点应运行的环境。

  • dns_alt_names:内置在代理证书中的 DNS 替代名称。

  • custom_attributes:任何自定义的 CSR 属性。

  • extension_request:任何特定的扩展请求(如 pp_role),用于添加到证书中。

管理服务

Puppet Discovery 也提供了服务管理功能,并仅提供两个字段供我们使用:操作名称。使用这两个字段可以在机器上查找服务,并启动、停止或重启节点上的任何服务。这是一种在每个节点上引入 Puppet 之前,进行基础基础设施管理的便捷无代理方式。

Discovery 的用途

Discovery 在 Puppet 生态系统中仍然是一个相对较新的工具。它应该是安装的第一步,帮助你决定如何继续推进更大的 Puppet 基础设施部署。也就是说,Puppet Discovery 有几个关键用途:

  • 确定你在环境中已经拥有的资源

  • 确保目标机器正确安装了安全补丁

  • 在更高层次上检查资源,而不是直接对其执行操作

总结

在本章中,我们介绍了 Puppet 任务和 Puppet Discovery。Bolt 和 Puppet 任务让我们能够在目标机器上执行远程临时命令。我们可以对这些临时命令进行参数化,并构建高度可共享的任务,这些任务可以在我们组织中被广泛使用。我们甚至可以将这些任务串联成 Puppet 计划,构建更复杂的操作,并在整个基础设施中共享。我们检查了 Puppet Discovery,安装了它并查看了现有的基础设施。我们学习了如何查看和部署代理,以及如何通过 Puppet Discovery 管理服务。

下一章我们将介绍 Puppet 中的虚拟资源和导出资源。

第九章:导出资源

导出资源提供了一种让系统声明资源的方式,但不一定要实现它。它们的设计目的是让节点将有关自己信息发布到一个中央数据库(PuppetDB),这样另一个节点就可以收集 Puppet 资源并在系统上实现它。导出资源主要为创建一个能够感知环境中其他基础架构的基础架构提供了方法。它们对于必须最终与由自动化过程动态创建的基础架构中的信息进行合并的基础架构提供了最大的价值。

本章将涵盖以下主题:

  • 虚拟资源和导出资源

  • 一些用例

虚拟资源和导出资源

要理解导出资源,首先需要理解虚拟资源。虚拟资源声明了一种状态,该状态可能会被提供,但只有在使用realize函数声明时才会强制执行。这些资源的设计目的是让你提前发布资源,但只有在满足其他条件时才会强制执行。虚拟资源有助于克服 Puppet 代码中可能出现的单一声明挑战,特别是当你有多个清单需要生成相同资源时——你可能需要在单一节点上包括多个这样的清单。如果多个模块需要管理同一文件,可以考虑将资源虚拟化,并使该资源在多个模块中可用。

虚拟资源

虚拟资源的一个常见使用示例是使用特定访问权限的管理员用户。通过健全的安全策略,你可能不希望任何单一用户拥有基础架构中所有系统的管理员访问权限。此时,你可能希望将管理员用户声明为虚拟资源,并允许在适当的情况下由配置文件实现这些用户。在以下示例中,我将把自己添加为 Linux 系统的管理员用户,然后在多个清单中实现该资源,既不会导致资源冲突,又能让我将自己精确地放置在适当的系统上。

首先,我需要将自己以及可能的其他用户声明为虚拟资源:

# modules/admins/manifests/infrastructure.pp
# This manifest declares the virtual resource for my administrative user
class admins::infrastructure {
  @user {'rrussellyates':
    ensure => present,
    comment => 'Ryan Russell-Yates',
    groups => ['wheel']
  }
}

我希望在多个配置文件中使用realize函数从目录调用用户对象,并确保它已经存在于系统上。注意引用中的大写字母:User['rrussellyates']。这个对象已经在目录中存在,因此我调用的是一个已经存在的对象。我需要确保包含声明该虚拟用户的清单,以便该虚拟用户已经在目录中并由配置文件实现:

# modules/profile/manifests/monitoring_support.pp
# Assume I'm a member of a monitoring team, that monitors critical applications
class profile::monitoring_support {
  include admins::infrastructure
  include profile::nagios
  include profile::monitoring_baseline

  realize User['rrussellyates']
}

# modules/profile/manifests/team/baseline.pp
# This profile combines our multiple required classes for the application
class profile::my_app
 {

  include admins::infrastructure
  include security
  include ntp
  include dns

  realize User['rrussellyates']
}

现在,我的生产级应用程序需要两个清单来调用我作为管理员用户。由于这是一个只声明过一次的虚拟资源,因此这两个清单可以独立或一起调用该用户而不会发生冲突:

# The role for our production application with special SLA monitoring
# Notice that both my_app and monitoring support require me as an administrative
# user. A development version of the application needs my support, as well as
# anything with a production-level SLA for monitoring. If I attempted to declare
# myself as a resource in both of these profiles, we'd have a duplicate resource
# declaration.
class role::production_app {
  include profile::my_app
  include profile::monitoring_support
}

然后,我们将把这个角色应用到我们的节点,使用我们的site.pp

# site.pp

node 'appserver' {
  include role::production_app
}

当我们在系统上运行这个时,管理员用户会被实现而不会出现重复的资源声明错误,即使该用户在两个个人资料中都被实现。我们可以在多个地方成功调用此用户,而不会发生资源冲突:

[root@wordpress vagrant]# puppet agent -t
Info: Using configured environment 'production'
Info: Retrieving pluginfacts
Info: Retrieving plugin
Info: Retrieving locales
Info: Loading facts
Info: Caching catalog for wordpress
Info: Applying configuration version '1529120853'
Notice: /Stage[main]/Admins::Infrastructure/User[rrussellyates]/ensure: created
Notice: Applied catalog in 0.10 seconds

标签

不是所有虚拟资源都是相互独立的。有时,我们希望生成一组可以一起实现的资源。Puppet 提供了一个叫做标签(tag)的元参数,使我们能够将资源分类在一起。标签允许我们使用 puppet agent -t --tags <tag> 运行一部分 Puppet 代码。它们为资源提供了用户特定的标记,以便构建类似对象的集合。标签是一个数组,因此你可以为一个资源应用多个标签,但仍然分别调用它们。带有标签的虚拟资源可以通过资源收集器调用,这个收集器有时被称为 飞船操作符。通过标签调用资源的简单格式是 Resource <| |> 。在两个竖线之间,你可以在目录中搜索任何参数或元参数。

通过使用标签,我们可以用 User <| tag == 'monitoring_admin' |> 调用这个管理员用户。这使我们能够将资源捆绑在一起并作为一个组调用,而不是作为一个独立的部分。让我们以之前的示例为基础,扩展成一个基于标签的系统:

class admins::infrastructure {
  @user {'rrussellyates':
    ensure  => present,
    comment => 'Ryan Russell-Yates',
    groups  => ['wheel'],
    tag     => ['infrastructure_admin','monitoring_support'],
  }
  @user {'jsouthgate':
    ensure  => present,
    comment => 'Jason Southgate',
    groups  => ['wheel'],
    tag     => ['infrastructure_admin'],
  }
  @user {'chuck':
    ensure  => present,
    comment => 'Our Intern',
    groups  => ['wheel'],
    tag     => ['monitoring_support'],
  }
}

现在,我们已经将 chuck 标记为监控支持的成员,将 Jason 标记为基础设施管理员的成员,而我自己则同时是这两个团队的成员。我的清单将调用一个组的用户,而不是单独调用每个用户:

class profile::my_app {
  include admins::infrastructure
  include security
  include ntp
  include dns

  # This line calls in all Monitoring Support and Infrastructure Admin users.
  User <| tag == 'monitoring_support' or tag == 'infrastructure_admin' |>
}

当我们更改个人资料以使用这两个标签时,将会添加两个额外的用户:jsouthgatechuck。管理员用户 russellyates 已经在系统中,因此没有重新创建他:

[root@wordpress vagrant]# puppet agent -t
Info: Using configured environment 'production'
Info: Retrieving pluginfacts
Info: Retrieving plugin
Info: Retrieving locales
Info: Loading facts
Info: Caching catalog for wordpress
Info: Applying configuration version '1529120940'
Notice: /Stage[main]/Admins::Infrastructure/User[jsouthgate]/ensure: created
Notice: /Stage[main]/Admins::Infrastructure/User[chuck]/ensure: created
Notice: Applied catalog in 0.11 seconds

导出的资源

虚拟资源允许我们将资源分配到它们所创建的节点上。进一步来说,导出的资源允许一个节点创建虚拟资源并与基础设施中的其他节点共享。导出的资源是设计自动化系统的一种有用方式,尤其是当这些系统需要来自其他自动化系统的信息时。在实现中,你可以将导出的资源视为已虚拟化并宣布的。该资源并未在系统上实现(尽管它可能会),而是与基础设施的其他部分共享。这使我们能够构建基于了解其他节点状态的系统来管理事物。

声明一个导出的资源与声明虚拟资源的方式相同,不同的是我们使用两个 @ 符号而不是一个:

class profile::fillsuptmp 
{
  # Exported Resource. Virtual and Shared
  # Notice that only the @@ is different!
  @@file {
"/tmp/${::fqdn}":
    ensure  => present,
    content => $::ipaddress,
    tag     => ['Fillsuptmp']
  }
}

虽然我们可能不希望使用这个特定的示例,但我们可以使用文件<| |>在本地系统上实现这个资源,并仅接收本地资源。通过添加一组额外的括号,<<| |>>,我们的基础设施会将此文件作为每个节点所描述的内容来接收。以下示例展示了如何从导出的目录中检索我们的资源:

class profile::filltmp {
# This simple declaration will call our file from above and place one from
# each machine on the system. Notice the title containing $::fqdn, so a new
# file in /tmp will be created with the FQDN of each machine known to PuppetDB.

  include profile::fillsuptmp

  File <<| tag == 'Fillsuptmp' |>>
}

我们将在 site.pp 中添加这个配置文件,而不放在节点定义内部,这样所有节点都能使用它:

# site.pp

include profile::filltmp

当我们运行代理时,我们会看到每个节点在基础设施中的 /tmp 目录下生成一个文件。对于每个检查到 master 的节点,其他所有节点也将获得一个新的文件:

[root@wordpress vagrant]# puppet agent -t
Info: Using configured environment 'production'
Info: Retrieving pluginfacts
Info: Retrieving plugin
Info: Retrieving locales
Info: Loading facts
Info: Caching catalog for wordpress
Info: Applying configuration version '1529121275'
Notice: /Stage[main]/Profile::Fillsuptmp/File[/tmp/wordpress]/ensure: defined content as '{md5}c679836c51e9e0e92191c7d2d38f5fe5'
Notice: /Stage[main]/Profile::Filltmp/File[/tmp/pe-puppet-master]/ensure: defined content as '{md5}c679836c51e9e0e92191c7d2d38f5fe5'
Notice: Applied catalog in 0.10 seconds

导出的资源是从最后一次报告到 PuppetDB 的目录存储中收集的。如果一个节点在目录中发生了变化,而导出的资源在上次运行中不再可用,则该资源将无法用于资源收集。

需要注意的是,关于导出的资源:实现这些资源的节点将最终趋向于基础设施的预期状态。直到节点将该资源报告给 Puppet Master,它才会意识到该节点的存在。假设你有一个新节点,并且已将其与外部资源分类到目录中,直到该节点第一次检查到 master,这个资源才会出现在 PuppetDB 中。即使它检查到 PuppetDB,执行该资源的节点也必须再次运行 puppet agent run。这意味着,对于刚刚运行了默认 30 分钟计时器的节点,它可能需要 30 分钟才能将其导出资源报告给 Puppet Master。然后,你的节点收集这些资源可能需要再等待 30 分钟才能与 master 检查并接收这些更改。导出的资源不是即时的,但你的基础设施最终会根据提供给 PuppetDB 的新信息进行收敛。

Puppet 代理的内置默认设置是每 30 分钟与 Puppet Master 进行一次检查。如果你有一台简单配置的机器,像负载均衡器,应该更快地找到此信息,可以考虑让它更频繁地检查与 master 的连接。每 5 分钟检查一次的负载均衡器,在通过 Puppet 完成初始配置后,应该很快就能将节点报告为在线。

使用案例

导出的资源最好在基础设施中谨慎使用。我们将讨论一些使用案例,并在过程中介绍可能使用这些信息的类似应用程序。我们将使用 Forge 模块,在合理的地方使用,但也会构建一些自定义导出的资源,以便提供一个功能性示例。在这一部分,我们将讨论几个导出资源的示例:

  • 动态的 /etc/hosts 文件

  • 将一个节点添加到 haproxy 负载均衡器

  • 在应用服务器上为数据库服务器构建外部数据库

  • 使用 concatFile_line Puppet 资源的自定义配置文件

主机文件

第一个示例易于理解和解释,但绝对不应替代真正的域名服务器(DNS)。几年前,我有一个客户正在使用公共云,但该云被一家非常大的公司收购,该公司有一个专门的团队来管理公司 DNS。DNS 记录的处理时间通常为 4 天,而他们启动的许多应用程序的生命周期只有几天,然后就会被新节点替换。如果节点的网络信息在一段时间内发生变化,这种解决方案会遇到一些问题,但它在 DNS 政策放宽之前为客户提供了有效的短期解决方案。

在下面的示例中,我们将使用一个单独的配置文件,导出每个由profile::etchosts分类的系统的 FQDN 和 IP 地址,这些信息将被环境中的每个节点(包括源节点)消费和处理:

class profile::etchosts {
# A host record is made containing the FQDN and IP Address of the classified node
  @@host {$::fqdn:
    ensure => present,
    ip     => $::ipaddress,
    tag    => ['shoddy_dns'], }
# The classified node collects every shoddy_dns host entry, including its own,
# and adds it to the nodes host file. This even works across environments, as
# we haven't isolated it to a single environment.
  Host <<| tag == 'shoddy_dns' |>>
}

如果我们希望确保仅收集处于相同 Puppet 环境中的主机条目,我们可以简单地将清单更改为读取Host <<| tag == 'shoddy_dns'environment == $environment |>>

这个清单包含了导出的资源和资源收集调用。我们在其上包含此清单的任何节点将报告其主机记录并检索所有主机记录(包括它自身)。由于我们希望将此代码应用于基础设施中的所有节点,因此我们会将其放在site.pp中的任何节点定义之外,使其适用于基础设施中的所有节点:

# site.pp
include profile::etchosts
# Provided so nodes don't fail to classify anything
node default { }

当我们运行代理时,我们会单独检索每个主机条目并将其放入/etc/hosts。在下面的示例中,我将这个目录应用于我的 Puppet Master。Puppet Master 从 PuppetDB 中检索每个主机条目并将其放入/etc/hosts。报告的节点是haproxyappservermysql。这些节点将在本章其余的示例中使用:

[root@pe-puppet-master vagrant]# puppet agent -t
Info: Using configured environment 'production'
Info: Retrieving pluginfacts
Info: Retrieving plugin
Info: Retrieving locales
Info: Loading facts
Info: Caching catalog for pe-puppet-master
Info: Applying configuration version '1529033713'
Notice: /Stage[main]/Profile::Etchosts/Host[mysql]/ensure: created
Info: Computing checksum on file /etc/hosts
Notice: /Stage[main]/Profile::Etchosts/Host[appserver]/ensure: created
Notice: /Stage[main]/Profile::Etchosts/Host[haproxy]/ensure: created
Notice: Applied catalog in 14.07 seconds

[root@pe-puppet-master vagrant]# cat /etc/hosts
# HEADER: This file was autogenerated at 2018-06-15 03:36:02 +0000
# HEADER: by puppet. While it can still be managed manually, it
# HEADER: is definitely not recommended.
127.0.0.1 localhost
10.20.1.3 pe-puppet-master
10.20.1.6 mysql
10.20.1.5 appserver
10.20.1.4 haproxy

如前所述,这不应被视为真正的 DNS 的替代品。它是一个简单且实用的示例,展示了如何构建和使用 Puppet 导出的资源。

负载均衡

负载均衡器是一个常见的系统,它在 Puppet 中使用导出的资源模式。负载均衡器用于将流量转发到多个节点,提供通过冗余实现的高可用性,以及通过水平扩展提供的性能。像 HAProxy 这样的负载均衡器还允许设计将用户引导到距离他们更近的数据中心的应用程序,以提高性能。

负载均衡器本身将接收传统配置,而负载均衡器的每个成员将导出一个资源供负载均衡器消费。负载均衡器随后使用每个导出资源中的每个条目来构建一个合并的配置文件。

以下示例使用了 Puppet Forge 中的puppetlabs-haproxy模块(更多信息请访问forge.puppet.com/puppetlabs/haproxy)。HAProxy 是一个免费的开源负载均衡器,可以在家中无需许可证使用。Forge 上还有其他负载均衡器的模块可用,用户可以根据企业需求创建自定义模块。

我们需要创建两个配置文件来支持这个用例:一个用于负载均衡器,一个用于负载均衡成员。负载均衡成员配置文件是一个简单的导出资源,声明它将使用的监听服务,并将其主机名、IP 地址和可用端口报告给 HAProxy。loadbalancer配置文件将配置一个非常简单的默认loadbalancer,提供一个监听服务用于转发流量,最重要的是收集所有从导出资源来的配置:

class profile::balancermember {
  @@haproxy::balancermember { 'haproxy':
    listening_service => 'myapp',
    ports             => ['80','443'],
    server_names      => $::hostname,
    ipaddresses       => $::ipaddress,
    options           => 'check',
  }
}
class profile::loadbalancer {
  include haproxy

  haproxy::listen {'myapp':
    ipaddress => $::ipaddress,
    ports => ['80','443']
  }

  Haproxy::Balancermember <<| listening_service == 'myapp' |>>
}

然后,我们将这些配置文件放置在两个不同的主机上。在以下示例中,我将balancermember配置文件放置在appserver上,而loadbalancer配置文件放置在haproxy上。我们将继续扩展之前的site.pp,并逐步添加代码:

#site.pp
include profile::etchosts

node 'haproxy' {
  include profile::loadbalancer
}

node 'appserver' {
  include profile::balancermember
}

# Provided so nodes don't fail to classify anything
node default { }

在以下示例中,负载均衡器已经配置为负载均衡器,但没有转发的负载均衡成员。appserver也已经完成了一次运行,并将其导出的haproxy配置报告到 PuppetDB。最后,HAProxy 服务器收集并实现这个资源,并将其作为一行放入配置文件中,使得haproxy能够将流量转发到appserver

root@haproxy vagrant]# puppet agent -t
Info: Using configured environment 'production'
Info: Retrieving pluginfacts
Info: Retrieving plugin
Info: Retrieving locales
Info: Loading facts
Info: Caching catalog for haproxy
Info: Applying configuration version '1529036882'
Notice: /Stage[main]/Haproxy/Haproxy::Instance[haproxy]/Haproxy::Config[haproxy]/Concat[/etc/haproxy/haproxy.cfg]/File[/etc/haproxy/haproxy.cfg]/content:
--- /etc/haproxy/haproxy.cfg 2018-06-15 04:27:25.398339144 +0000
+++ /tmp/puppet-file20180615-17937-6bt84x 2018-06-15 04:28:05.100339144 +0000
@@ -27,3 +27,5 @@
 bind 10.0.2.15:443
 balance roundrobin
 option tcplog
+ server appserver 10.0.2.15:80 check
+ server appserver 10.0.2.15:443 check

Info: Computing checksum on file /etc/haproxy/haproxy.cfg
Info: /Stage[main]/Haproxy/Haproxy::Instance[haproxy]/Haproxy::Config[haproxy]/Concat[/etc/haproxy/haproxy.cfg]/File[/etc/haproxy/haproxy.cfg]: Filebucketed /etc/haproxy/haproxy.cfg to puppet with sum dd6721741c30fbed64eccf693e92fdf4
Notice: /Stage[main]/Haproxy/Haproxy::Instance[haproxy]/Haproxy::Config[haproxy]/Concat[/etc/haproxy/haproxy.cfg]/File[/etc/haproxy/haproxy.cfg]/content: content changed '{md5}dd6721741c30fbed64eccf693e92fdf4' to '{md5}b819a3af31da2d0e2310fd7d521cbc76'
Info: Haproxy::Config[haproxy]: Scheduling refresh of Haproxy::Service[haproxy]
Info: Haproxy::Service[haproxy]: Scheduling refresh of Service[haproxy]
Notice: /Stage[main]/Haproxy/Haproxy::Instance[haproxy]/Haproxy::Service[haproxy]/Service[haproxy]: Triggered 'refresh' from 1 event
Notice: Applied catalog in 0.20 seconds

这个模块使用Concat片段来构建整个文件。如果一个节点在某次运行中没有报告其haproxy::balancermember导出资源,它将在下一次运行时从loadbalancer中移除,后者会实现这些资源。

当用户请求 HAProxy 服务器的端口80http)或端口443https)时,它将自动从我们的appserver获取并转发流量。如果我们有多个应用服务器,它甚至会在两个服务器之间分配负载,实现横向扩展。

数据库连接

许多应用程序需要数据库来存储会话间的信息。在许多大型组织中,通常将这些数据库集中化,并将应用程序指向它们。以下示例将包含两个配置文件:一个用于数据库,一个用于需要使用该数据库的应用程序。

以下示例使用了 Puppet Forge 中的puppetlabs-mysql模块。MySQL 是一个免费的开源 SQL 数据库,可以在家中无需许可证使用。还有其他数据库的模块可用,如 SQL Server、OracleDB、Kafka 和 MongoDB。这些模块可以以类似的方式使用,为外部数据库提供导出资源。

在下面的示例中,appserver::database配置文件提供了一个非常简单的 MySQL 安装。它还获取所有标记为ourappmysql::db资源,并在中央服务器上实现它们。appserver配置文件接受一个密码参数,该密码可以通过hiera提供,或使用eyaml加密。使用此密码,它将导出一个数据库资源,并将在数据库服务器上进行收集和实现。还可以对该服务器上的应用程序进行其他配置,以确保它使用这个导出的资源提供的数据库:

class profile::appserver (
  $db_pass = lookup('dbpass')
) {
  @@mysql::db { "appdb_${fqdn}":
    user     => 'appuser',
    password => $db_pass,
    host     => $::fqdn,
    grant    => ['SELECT', 'UPDATE', 'CREATE'],
    tag      => ourapp,
  }
}

class profile::appserver::database {

  class {'::mysql::server':
    root_password => 'suP3rP@ssw0rd!',
  }

  Mysql::Db <<| tag == 'ourapp' |>>
}

我们将把这些配置文件插入到appserver节点的定义中,并为mysql创建一个新的节点定义。此配置将确保我们的appservermysql服务器上有一个相关的数据库,并且它的应用程序能够通过haproxy正确地转发。注意在appserver节点上传递密码:

#site.pp

include profile::etchosts

node 'haproxy' {
  include profile::loadbalancer
}
node 'appserver' {
  include profile::balancermember
  class {'profile::appserver': db_pass => 'suP3rP@ssw0rd!' }
}
node 'mysql' {
  include profile::appserver::database
}
# Provided so nodes don't fail to classify anything
node default { }

当应用于一个已配置的数据库服务器时,使用从appserver新导出的资源,数据库服务器上会创建一个新的数据库、数据库用户和一组权限:

[root@mysql vagrant]# puppet agent -t
Info: Using configured environment 'production'
Info: Retrieving pluginfacts
Info: Retrieving plugin
Info: Retrieving locales
Info: Loading facts
Info: Caching catalog for mysql
Info: Applying configuration version '1529037526'
Notice: /Stage[main]/Profile::Appserver::Database/Mysql::Db[appdb_appserver]/Mysql_database[appdb_appserver]/ensure: created
Notice: /Stage[main]/Profile::Appserver::Database/Mysql::Db[appdb_appserver]/Mysql_user[appuser@appserver]/ensure: created
Notice: /Stage[main]/Profile::Appserver::Database/Mysql::Db[appdb_appserver]/Mysql_grant[appuser@appserver/appdb_appserver.*]/ensure: created
Notice: Applied catalog in 0.34 seconds

该系统的一个缺陷是从启动appserver到数据库在系统上实现之间有长达 30 分钟的间隙。在下一章中,我们还将讨论应用程序编排,它通过将节点链接在一起并协调代理运行来帮助解决这个问题。如果你有时间让基础设施收敛,这个导出的资源可以仅用于数据库和应用程序。

Concat,文件行,以及你!

之前的示例依赖于现有资源,例如主机,或现有的 forge 模块,如haproxymysql。在某些情况下,我们需要使用导出的资源在系统上构建自定义配置文件。我们将展示使用concatfile_line的示例。concat用来声明文件的全部内容,使用一个有序字符串列表。文件行是puppetlabs-stdlib的一部分(更多信息请访问forge.puppet.com/puppetlabs/stdlib),它将缺失的行插入到现有文件中,也可以使用regex匹配现有行。

Concat – 锤子

我们将构建一个名为/tmp/hammer.conf的文件,该文件由一个头部部分和由导出的资源提供的可变数量的部分组成。这个类设计用来在基础设施中的所有机器上使用,但也可以通过将导出的资源与concat资源和头部concat::fragment分开,轻松转变为单个服务器配置文件。

以下示例使用了 Puppet Forge 中的puppetlabs-concat。(更多信息请访问forge.puppet.com/puppetlabs/concat)。concat模块允许我们声明一个由多个片段组成的文件,并按顺序在系统上创建该文件。这使得我们能够定义一个或多个头部和尾部,同时为动态行的添加留出空间。

在以下示例中,我们正在构建一个清单来构建hammer.confconcat资源为每个片段提供了构建的位置。hammer timeconcat::fragment用于管理头部,如参数中的顺序01所示。每台机器将导出一个concat片段,详细描述 FQDN、IP 地址、操作系统和版本,并作为文件中的一行,以模拟一个全局配置文件或清单文件。最后,每台机器将使用concat片段的资源收集器实现所有这些导出的片段:

class files::hammer {

  $osname = fact('os.name')
  $osrelease = fact('os.release')

  concat {'/tmp/hammer.conf':
    ensure => present,
  }

  concat::fragment {'Hammer Time':
    target => '/tmp/hammer.conf',
    content => "This file is managed by Puppet.It will be overwritten",
    order => '01',
  }

  @@concat::fragment {"${::fqdn}-hammer":
    target => '/tmp/hammer.conf',
    content => "${::fqdn} - ${::ipaddress} - ${osname} ${osrelease}",
    order => '02',
    tag => 'hammer',
  }

  Concat::Fragment <<| tag == 'hammer' |>>

}

我们将在任何节点定义之外添加files::hammer,这样这个清单文件将在我们基础设施中的所有机器上创建:

include profile::etchosts
include files::hammer

node 'haproxy' {
  include profile::loadbalancer
}

node 'appserver' {
  include profile::balancermember
  class {'profile::appserver': db_pass => 'suP3rP@ssw0rd!' }
}

node 'mysql' {
  include profile::appserver::database
}

# Provided so nodes don't fail to classify anything
node default { }

当在我们基础设施中的mysql节点上运行时,作为基础设施中的最后一个节点,/tmp/hammer.conf文件将被创建,并包含每个节点提供的 Facter facts:

root@mysql vagrant]# puppet agent -t
Info: Using configured environment 'production'
Info: Retrieving pluginfacts
Info: Retrieving plugin
Info: Retrieving locales
Info: Loading facts
Info: Caching catalog for mysql
Info: Applying configuration version '1529040374'
Notice: /Stage[main]/Files::Hammer/Concat[/tmp/hammer.conf]/File[/tmp/hammer.conf]/ensure: defined content as '{md5}f3f0d7ff5de10058846333e97950a7b9'
Notice: Applied catalog in 0.33 seconds

# /tmp/hammer.conf
This file is managed by Puppet. It will be overwritten
haproxy - 10.0.2.15 - CentOS 7
mysql - 10.0.2.15 - CentOS 7
pe-puppet-master - 10.0.2.15 - CentOS 7
appserver - 10.0.2.15 - CentOS 7

大多数配置文件应该以这种方式构建,并由 Puppet 整体管理。如果某个文件的全部内容未被管理,我们可以使用stdlib提供的file_line来代替。

file_line – 手术刀

在本练习中,我们将使用 Puppet 文件资源来构建文件,但仅在系统中尚未存在该文件时才会构建。然后,我们将使用文件行将我们想要的值插入到文件中,这些值是从所有系统的导出资源中收集的。

以下示例使用了 Puppet Forge 中的puppetlabs-stdlib(更多信息请访问forge.puppet.com/puppetlabs/stdlib)。stdlib包含了大量可以在清单中使用的函数,以及资源file_linefile_line允许单独管理文件中的一行,也可以作为查找和替换未管理文件时使用的正则表达式匹配。如果是针对 INI 文件,建议改用puppetlabs-inifile。(更多信息请访问forge.puppet.com/puppetlabs/inifile)。

在这个示例中,我们将首先创建一个名为/tmp/scalpel.conf的文件。我们会确保这个文件存在,并且其所有权属于 root。我们将设置替换标志,告知 Puppet 如果该文件已经存在于系统中则不要替换其内容,从而确保文件中已有的内容不会被覆盖。如果文件当前不在系统中,内容行将提供默认值。接下来,我们将创建一个导出的file_line来模拟一个配置行,并使用匹配语句确保我们替换的是配置错误的行,而不是创建新行。最后,我们将在所有分类了此文件的节点上实现这些资源:

class files::scalpel {

  $arch = fact('os.architecture')
  file {'/tmp/scalpel.conf':
    ensure => file,
    owner => 'root',
    group => 'root',
    content => 'This file is editable, with individually managed settings!',
    replace => false,
  }
  @@file_line {"$::fqdn - setting":
    path => '/tmp/scalpel.conf',
    line => "${::fqdn}: $arch - ${::kernel} - Virtual: ${::is_virtual}",
    match => "^${::fqdn}:",
    require => File['/tmp/scalpel.conf'],
    tag => 'scalpel',
  }
  File_line <<| tag == 'scalpel' |>>
}

scalpel 配置文件旨在在基础设施中的每台机器上使用,因此它也被放置在site.pp中的节点定义外:

include profile::etchosts

include files::scalpel

node 'haproxy' {
  include profile::loadbalancer
}

node 'appserver' {
  include profile::balancermember
  class {'profile::appserver': db_pass => 'suP3rP@ssw0rd!' }
}

node 'mysql' {
  include profile::appserver::database
}

# Provided so nodes don't fail to classify anything
node default { }

最后,我们的节点捕捉到配置更改,并创建了文件。请注意,文件已被创建,然后通过我们在导出file_line资源中使用的require参数插入了文件行:

[root@haproxy vagrant]# puppet agent -t
Info: Using configured environment 'production'
Info: Retrieving pluginfacts
Info: Retrieving plugin
Info: Retrieving locales
Info: Loading facts
Info: Caching catalog for haproxy
Info: Applying configuration version '1529041736'
Notice: /Stage[main]/Files::Scalpel/File[/tmp/scalpel.conf]/ensure: defined content as '{md5}2d3ebc675ea9c8c43677c9513f820db0'
Notice: /Stage[main]/Files::Scalpel/File_line[haproxy - setting]/ensure: created
Notice: /Stage[main]/Files::Scalpel/File_line[mysql - setting]/ensure: created
Notice: /Stage[main]/Files::Scalpel/File_line[appserver - setting]/ensure: created
Notice: /Stage[main]/Files::Scalpel/File_line[pe-puppet-master - setting]/ensure: created
Notice: Applied catalog in 0.18 seconds

# /tmp/scalpel.conf
This file is editable, with individually managed settings!
haproxy: x86_64 - Linux - Virtual: true
mysql: x86_64 - Linux - Virtual: true
appserver: x86_64 - Linux - Virtual: true
pe-puppet-master: x86_64 - Linux - Virtual: true

与我们的concat示例不同,除由清单管理的单独行外,这个文件在 Puppet 之外仍然是可编辑的。在下面的示例中,我已将文件顶部添加了注释,并将haproxy的虚拟设置更改为 false:

# Our comments now stay in this file, because we're not managing
# The whole file, just individual lines. This methodology can
# come in useful once in a great while. This is still configuration
# drift, so make sure to use it sparingly!
This file is editable, with individually managed settings!
haproxy: x86_64 - Linux - Virtual: false
mysql: x86_64 - Linux - Virtual: true
appserver: x86_64 - Linux - Virtual: true
pe-puppet-master: x86_64 - Linux - Virtual: true

当代理再次运行时,haproxy行会被修正,但我们的注释仍然保持在文件顶部。用户甚至可以向该文件添加自己的配置行,只要这些配置没有被 Puppet 导出的资源报告,它们就会保留在配置文件中:

[root@haproxy vagrant]# puppet agent -t
Info: Using configured environment 'production'
Info: Retrieving pluginfacts
Info: Retrieving plugin
Info: Retrieving locales
Info: Loading facts
Info: Caching catalog for haproxy
Info: Applying configuration version '1529042980'
Notice: /Stage[main]/Files::Scalpel/File_line[haproxy - setting]/ensure:
created
Notice: Applied catalog in 0.15 seconds

# Our comments now stay in this file, because we're not managing
# The whole file, just individual lines. This methodology can
# come in useful once in a great while. This is still configuration
# drift, so make sure to use it sparingly!

This file is editable, with individually managed settings!
haproxy: x86_64 - Linux - Virtual: true
mysql: x86_64 - Linux - Virtual: true
appserver: x86_64 - Linux - Virtual: true
pe-puppet-master: x86_64 - Linux - Virtual: true

这种方法确实允许基础设施中存在大量的配置漂移。如果你的团队致力于提供受控的自服务资源,这是一个有效的方式,允许你的客户修改配置文件,前提是这些设置没有被你的基础设施团队专门管理。

总结

在这一章中,我们讨论了虚拟资源和导出资源。虚拟资源允许我们描述一个资源应该是什么,并在其他条件下实现它。导出资源允许我们通过使用 PuppetDB 将我们的虚拟资源公布给基础设施中的其他节点。我们探讨了如何为管理用户编写虚拟资源,并为基础设施中的所有其他节点在/tmp目录中放置一个文件。接着我们使用导出资源创建了一个/etc/hosts文件,一个负载均衡器,一个数据库,以及使用concatfile_line构建自定义配置文件的示例。

当我们将这些导出资源应用到我们的系统时,我们注意到这些资源的主要限制是时间。我们的基础设施最终会收敛,但它并不会以协调和及时的方式发生。我们的下一章将讨论应用程序编排,它允许我们将多层应用程序连接起来,并编排 Puppet 在这些节点上的运行顺序。

第十章:应用程序编排

应用程序编排为 Puppet 语言提供了几个关键特性。应用程序编排扩展了导出资源的概念,针对更具体的应用程序,允许节点之间共享配置项。此外,该特性还提供了一种方式,按顺序安排 Puppet 运行,确保依赖节点在需要它们的节点之前完成构建或汇聚。应用程序编排允许我们将多个节点按顺序组合在一起运行。最重要的是,配置更新不是在检查时随机应用的,而是按照特定的、顺序的模式应用的。

应用程序编排仅适用于 Puppet Enterprise。Puppet 开源用户可以使用语言构造,但有序运行是 Puppet Enterprise 提供的。

应用程序编排有三个新的语言构造,我们需要使用它们来创建有序运行,并自动共享彼此的信息:

  • 应用程序定义:描述整个应用程序堆栈的组件集合的端到端描述

  • 应用程序组件:整个应用程序堆栈的单个组件

  • 服务资源:旨在跨应用程序组件共享信息的资源

应用程序定义

应用程序定义看起来很像定义的资源,但也类似于传统的 Puppet 配置文件。它们描述了一组组件,构成整个系统,但与配置文件不同,它们不局限于单一节点。这些应用程序定义描述了一个或多个节点的已配置状态,按应用程序组件拆分。

应用程序定义将类似于定义类型,但有几个关键区别:

  • 它们被命名为application,而不是define

  • 每个资源必须在模块内进行命名空间划分:

# Application used instead of 'class' or 'define'
application 'example' (
  $var,
) {

# app1 exports its database configuration items
  example::app1 {
    config => $var,
    export => Database['app1'],
  }

# app2 both imports the previous database and exports its own type: Application
  example::app2 {
    config  => $var,
    consume => Database['app1'],
    export  => Application['app1'],
  }

需要注意的是,应用程序中的每个资源都可以与我们网站定义中的一个完全不同的节点相关联。我们还可以使用我们的网站定义来传递这些共享的配置项,这些项在前面的代码中由$var表示:

#/etc/puppetlabs/code/environments/production/manifests/site.pp
site {
  example {'app1':
    var => 'config',
    nodes => {
      Node['database'] => [ Example::App1['app']],
      Node['app']      => [ Example::App2['app']],
    }
  }
}

在节点的哈希值中,注意到Node对象和Example::App<X>对象是大写的。

应用程序组件

应用程序组件提供了多节点应用程序的单个部分。它们通常是定义类型(以便重用),但在非常简单的情况下,也可以是类或甚至本地资源,例如文件。应用程序组件由exportconsumerequire元参数在应用程序声明中创建。

应用程序组件写成通用类或定义类型。它们遵循与所有其他 Puppet 代码相同的自动加载格式。example::app2的清单仍然位于manifests/example/app2.pp。应用程序组件可以通过在清单底部添加一个额外的语句,显式列出它们在各自清单中导出和消费的值:

class example::app2 (
# $db_host is provided by the consume of the Database
  $db_host,
) {
# Any resources, defined types or class calls in a regular manifest would be placed here.
}
# Note that the consume is outside of the class declaration
Example::App2 consumes Database {
  db_host => $host,
}
# Note that the produces is outside of both the class declaration, and above consume
Example::App2 produces Http {
  host => $::fqdn,
  port => '80',
}

在上面的示例中,$db_host是一个可以传递给清单中任何资源的值。我们不是通过 Hiera 或 Puppet DSL 传递它,而是通过另一个应用程序提供的host参数来消费该值。我们还导出节点自身的 FQDN 和主机名,以便后续应用程序可以使用这些值指向由example::app2创建的 Web 服务。DatabaseHttp都是服务资源,描述在应用程序之间共享的信息。

服务资源

服务资源是由应用组件填充和查看的环境范围信息池。服务资源类似于导出资源,提供关于 PuppetDB 中其他节点的信息。服务资源的独特之处在于它们建立节点之间的依赖关系。服务资源被声明为 Puppet 类型,用 Ruby 编写。提供程序是可选的,并允许进行导出资源的可用性测试。

服务资源类型提供了通过应用编排的consumeexport元参数存储和传输信息的框架。服务资源需要一个类型,使用 Ruby 代码声明信息的结构。它们始终存储在模块中,路径为lib/puppet/type/<resource>.rb,并在部署时发送到环境中的所有节点,但未使用该资源的节点不会执行操作。以下示例类型可以包含由app1导出并由app2消费的数据库资源:

#lib/puppet/type/database.rb

# Notice the :is_capability => true. This property creates this type as an
# environment-wide service, to be produced and consumed.
Puppet::Type.newtype :database, :is_capability => true do
  newparam :host
end

这个简单的示例为我们提供了一种导出关于数据库信息的方式,特别是host参数。这可以通过export参数填充,并通过consume参数读取。

建模应用程序

在本章的其余部分,我们将专注于构建一个简单和一个更复杂的编排应用程序示例。我们的第一阶段将是创建一个单一的数据库和一个单一的 Web 服务器。

应用程序和数据库

在我们的第一个示例中,我们将从一个节点的数据库导出信息,并在 WordPress 实例上检索它。这个简单的示例将允许我们成对部署节点,并确保在依赖于它的 Web 应用程序之前构建数据库。

依赖关系

在开始编写代码之前,我们将查找 Forge 上的相关支持或开源模块。WordPress 需要一个 SQL 服务器和一个 Web 主机,我们将通过 Apache HTTPD 提供。在开始之前,我们需要从 Forge 安装以下模块:

  • puppetlabs-mysql

  • puppetlabs-apache

  • hunner-wordpress

[root@pe-puppet-master myapp]# puppet module install puppetlabs-mysql
Notice: Preparing to install into /etc/puppetlabs/code/environments/production/modules ...
Notice: Downloading from https://forgeapi.puppet.com ...
Notice: Installing -- do not interrupt ...
/etc/puppetlabs/code/environments/production/modules
└─┬ puppetlabs-mysql (v5.4.0)
 ├── puppet-staging (v3.2.0)
 ├── puppetlabs-stdlib (v4.25.1)
 └── puppetlabs-translate (v1.1.0)
[root@pe-puppet-master myapp]# puppet module install puppetlabs-apache
Notice: Preparing to install into /etc/puppetlabs/code/environments/production/modules ...
Notice: Downloading from https://forgeapi.puppet.com ...
Notice: Installing -- do not interrupt ...
/etc/puppetlabs/code/environments/production/modules
└─┬ puppetlabs-apache (v3.1.0)
 ├── puppetlabs-concat (v4.2.1)
 └── puppetlabs-stdlib (v4.25.1)
[root@pe-puppet-master myapp]# puppet module install hunner-wordpress
Notice: Preparing to install into /etc/puppetlabs/code/environments/production/modules ...
Notice: Downloading from https://forgeapi.puppet.com ...
Notice: Installing -- do not interrupt ...
/etc/puppetlabs/code/environments/production/modules
└─┬ hunner-wordpress (v1.0.0)
 ├── puppetlabs-concat (v4.2.1)
 ├── puppetlabs-mysql (v5.4.0)
 └── puppetlabs-stdlib (v4.25.1)

构建

我们将从顶部开始编写代码。在学习代码的同时,想象代码的最终状态,并学习在此过程中启用它的各个部分是很有帮助的。

节点声明

我们的第一步将是节点声明。这将放在我们的site.pp中,每个应用程序将放在特定的站点调用下。在下面的示例中,请注意以下内容:

  • 所有应用都在顶级 site{} 声明中声明。

  • myapp {'myapp': } 只是可以放入 site.pp 中的一种应用。我们还可以在它下面再添加一个,如 myapp.{'myapp2': },它位于站点中,并且拥有此应用的第二个独立实例。

  • Node['<nodename>']Myapp::<app> 是大写的。

  • 我仍然可以使用 site.pp 来处理其他事项,正如 Puppet Master 的分类所示,具体如下:

site {
  myapp { 'myapp':
      nodes => {
        Node['mysql'] => [ Myapp::Db['myapp']],
        Node['appserver'] => [ Myapp::Web['myapp']],
        Node['haproxy'] => [ Myapp::Lb['myapp']],
    }
  }
}

node 'puppetmaster' {
  include role::puppetmaster
}

# To keep the sample simple, firewalls have been disabled on all machines.
service {'firewalld': ensure => stopped }
service {'iptables':  ensure => stopped }

这个特定配置将确保 mysql 节点获取数据库,appserver 获取 WordPress,而 HAProxy 获取负载均衡器配置。

应用声明

在前面的示例中,我们在 site{} 声明下方调用了一个名为 myapp 的资源。此清单位于 myapp 模块中的 manifests/init.pp,声明了该应用,描述了一些可覆盖的参数,并使用 exportconsume 元参数来编排应用。请注意以下内容:

  • 在第一行中,应用 myapp 被用作类或定义的替代。

  • myapp::db 导出 SQL 资源。

  • myapp::web 消耗 SQL 资源。

  • myapp::db 会在 myapp::web 之前运行,因为 myapp::web 通过 consume 依赖于它。

  • 我们使用 $name 变量,以便每个组件接收 myapp 作为名称,该名称来自 myapp {'myapp':}

application myapp (
  $dbuser = 'wordpress',
  $dbpass = 'w0rdpr3ss!',
  $webpath = '/var/www/wordpress',
  $vhost = 'appserver',
) {
  myapp::db { $name:
    dbuser => $dbuser,
    dbpass => $dbpass,
    export => Sql[$name],
  }
  myapp::web { $name:
    webpath => $webpath,
    consume => Sql[$name],
    vhost => $vhost,
  }
}

DB 服务资源

我们将为这个简单的用例构建自己的自定义 DB 类型。它将允许我们从数据库向 WordPress 应用传递值。这个简单的示例确保类型命名为 db,并将其标记为服务资源,向数据库服务资源提供五个可用参数。此文件放置在 lib/puppet/type/db.rb 中:

# lib/puppet/type/db.rb
# Adding :is_capability to the custom type marks the resources as service resources
Puppet::Type.newtype :db, :is_capability => true do
  newparam :name, :is_namevar => true
  newparam :user
  newparam :password
  newparam :port
  newparam :host
end

应用组件

我们的 myapp 定义类型将利用我们在上一节创建的 db 资源,将四个值传递给 PuppetDB,直接从 myapp::db 中。我们将使用此清单来构建 MySQL 服务器,并向另一个节点上的 WordPress 实例提供信息。请注意以下示例中的内容:

  • 一个常规的定义类型,使用标准的 Puppet DSL。我们构建了一个服务器和一个数据库来支持该应用。

  • $host 在清单中没有使用,但会传递给产生的 Db 资源。

  • Myapp::Db 产生 Db 直接放置在定义之后,在同一个清单中:

define myapp::db (
  $dbuser,
  $dbpass,
  $host = $::fqdn,
){

  class {'::mysql::server':
    root_password => 'Sup3rp@ssword!',
    override_options => {
      'mysqld' => {
        'bind-address' => '0.0.0.0'
      }
    }
  }

  mysql::db { $name:
    user => $dbuser,
    password => $dbpass,
    host => '%',
    grant => ['ALL PRIVILEGES'],
  }
}
Myapp::Db produces Db {
  dbuser => $dbuser,
  dbpass => $dbpass,
  dbhost => $host,
  dbname => $name,
}

Myapp::Web 是一个定义类型,用于消耗由 Myapp::Db 产生的 Db。它安装所需的包,安装 Apache,构建 vhost,并将 WordPress 部署到 vhost 的 docroot。请注意以下内容:

  • $vhost$webpath 是由应用 myapp 提供的。

  • $dbuser$dbpass$dbhost$dbname 由消耗 Db {} 提供。

  • 因为我们的清单使用了 dbpassdbhostdbuserdbname 的值,我们的映射不需要再声明。以下示例将直接声明变量:

define myapp::web (
  $webpath,
  $vhost,
  $dbuser,
  $dbpass,
  $dbhost,
  $dbname,
  ) {

    package {['php',
              'mysql',
              'php-mysql',
              'php-gd'
             ]:
      ensure => installed,
    }

    class {'apache':
      default_vhost => false
    }

    include ::apache::mod::php

    apache::vhost { $vhost:
      port => '80',
      docroot => $webpath,
      require => File[$webpath],
    }

    file { $webpath:
      ensure => directory,
      owner => 'apache',
      group => 'apache',
      require => Package['httpd'],
    }

    class { '::wordpress':
      db_user => $dbuser,
      db_password => $dbpass,
      db_host => $dbhost,
      db_name => $dbname,
      create_db => false,
      create_db_user => false,
      install_dir => $webpath,
      wp_owner => 'apache',
      wp_group => 'apache',
    }
  }
Myapp::Web consumes Db { }

我们可以使用前面的代码集合来排序和部署我们的多层应用程序。我们当前的模块应如下所示:

myapp
├── lib
│   └── puppet
│       └── type
│           ├── sql.rb
├── manifests
    ├── db.pp
    ├── init.pp
    └── web.pp

然后我们可以使用puppet apppuppet job命令来部署我们的应用程序。

部署

要查看列在site.pp中的应用程序,我们可以使用命令puppet app show。此命令读取我们的主清单,并列出所有应用程序及其组件。在以下示例中,从前面的代码来看,我们正在将Myapp::Db部署到mysql,并将Myapp::Web部署到appserver

在运行此实验时,可能会收到一条消息:应用程序管理已禁用。要启用它,请在 orchestrator 服务配置中设置app-management: true。为了解决这个问题,你可以登录到 Puppet Enterprise 控制台,进入 Puppet Master 配置,并将puppet_enterprise::profile::master::app-management的值更改为true

[root@pe-puppet-master manifests]# puppet app show
Myapp[myapp]
 Myapp::Db[myapp] => mysql
 + produces Sql[myapp]
 Myapp::Web[myapp] => appserver
 consumes Sql[myapp]

为了模拟部署,我们可以使用puppet job plan命令。我们为它提供applicationenvironment标志,以便应用程序协调器知道使用哪个版本的site.pp。此命令主要显示排序,你可以从以下结果中看到,mysql将在appserver之前进行配置:

[root@pe-puppet-master manifests]# puppet job plan --application Myapp --environment production

+-------------------+------------+
| Environment | production |
| Target | Myapp |
| Concurrency Limit | None |
| Nodes | 2 |
+-------------------+------------+

Application instances: 1
 - Myapp[myapp]

Node run order (nodes in level 0 run before their dependent nodes in level 1, etc.):
0 -----------------------------------------------------------------------
mysql
 Myapp[myapp] - Myapp::Db[myapp]

1 -----------------------------------------------------------------------
appserver
 Myapp[myapp] - Myapp::Web[myapp]

Use `puppet job run --application 'Myapp' --environment production` to create and run a job like this.
Node catalogs may have changed since this plan was generated.

通过从puppet job plan切换到puppet job show,我们实际上是按顺序部署我们的代码。运行首先发生在mysql服务器上,产生的信息将被appserver节点使用。此运行确保在尝试部署依赖于它们的应用程序之前,必要的组件已完全部署:

Use `puppet job run --application 'Myapp' --environment production` to create and run a job like this.
Node catalogs may have changed since this plan was generated.
[root@pe-puppet-master manifests]# puppet job run --application 'Myapp' --environment production
Starting deployment ...

+-------------------+------------+
| Job ID | 8 |
| Environment | production |
| Target | Myapp |
| Concurrency Limit | None |
| Nodes | 2 |
+-------------------+------------+

Application instances: 1
 - Myapp[myapp]

Node run order (nodes in level 0 run before their dependent nodes in level 1, etc.):
0 -----------------------------------------------------------------------
mysql
 Myapp[myapp] - Myapp::Db[myapp]

1 -----------------------------------------------------------------------
appserver
 Myapp[myapp] - Myapp::Web[myapp]

New job created: 8
Started puppet run on mysql ...
Finished puppet run on mysql - Success!
 Resource events: 0 failed 4 changed 32 unchanged 0 skipped 0 noop
 Report: https://pe-puppet-master/#/run/jobs/8/nodes/mysql/report
Started puppet run on appserver ...
Finished puppet run on appserver - Success!
 Resource events: 0 failed 3 changed 130 unchanged 0 skipped 0 noop
 Report: https://pe-puppet-master/#/run/jobs/8/nodes/appserver/report

Success! 2/2 runs succeeded.

我们现在已经部署了一个非常简单的有序应用程序。在配置我们的wordpress服务器之前,数据库将完全启动。在下一个示例中,我们将允许多个wordpress服务器和多个负载均衡器来为我们的应用程序提供扩展。

添加负载均衡器并提供水平扩展

在许多情况下,我们希望我们的应用程序能够水平扩展。构建更多的节点可以让我们为更多的客户提供服务。这将是对之前应用程序的完全重写,并且还将整合puppetlabs/app_modeling模块来自 Forge。

依赖项

为了提供新的功能,我们需要从 Forge 获取puppetlabs-haproxy模块和puppetlabs/app_modeling模块。如果你正在使用Puppetfile,只需将它们添加到Puppetfile中。在以下示例中,我正在手动安装这些依赖项到现有的主服务器上:

[root@pe-puppet-master myapp]# puppet module install puppetlabs-haproxy
Notice: Preparing to install into /etc/puppetlabs/code/environments/production/modules ...
Notice: Downloading from https://forgeapi.puppet.com ...
Notice: Installing -- do not interrupt ...
/etc/puppetlabs/code/environments/production/modules
└─┬ puppetlabs-haproxy (v2.1.0)
 ├── puppetlabs-concat (v4.2.1)
 └── puppetlabs-stdlib (v4.25.1)
[root@pe-puppet-master myapp]# puppet module install puppetlabs/app_modeling
Notice: Preparing to install into /etc/puppetlabs/code/environments/production/modules ...
Notice: Downloading from https://forgeapi.puppet.com ...
Notice: Installing -- do not interrupt ...
/etc/puppetlabs/code/environments/production/modules
└─┬ puppetlabs-app_modeling (v0.2.0)
 └── puppetlabs-stdlib (v4.25.1)

我们现在可以通过app_modeling构建haproxy节点和新的应用程序编排功能。

构建

我们将从site.pp开始,再次从端点建模我们的应用程序。我添加了两条额外的服务行,确保为了本节课的目的防火墙被禁用。我们还可以考虑使用puppetlabs/firewall来管理我们的防火墙,并为防火墙生成和使用 FQDN。在以下示例中,你会注意到一些事情:

  • 我们将dbpass变量传递给应用程序。这个变量可以存储在 Hiera 中,并通过 EYAML 加密。

  • 我们有两个wordpress节点和两个haproxy节点,它们在appserver中都有各自独特的名称。

# For the purposes of this demo, the next two lines can be used to ensure firewalls
# are off for all CentOS nodes.

service {'iptables': ensure => stopped }
service {'firewalld': ensure => stopped }

site {
  myapp { 'myapp':
    dbpass => 'rarypass',
    nodes => {
      Node['mysql']       => [ Myapp::Db['myapp']],
      Node['wordpress']   => [ Myapp::Web['myapp-1']],
      Node['wordpress-2'] => [ Myapp::Web['myapp-2']],
      Node['haproxy']     => [ Myapp::Lb['myapp-1']],
      Node['haproxy-2']   => [ Myapp::Lb['myapp-2']],
    }
  }
}

在声明应用程序后,我们可以建模我们的init.pp来声明整个应用程序。这个应用程序包含了很多内容,因此请注意以下几点:

  • 提供了五个变量,db变量在DBApp中都被使用。

  • Myapp::Db生成一个数据库。

  • Myapp::Web消耗一个数据库并生成一个 HTTP 服务资源。

  • 我们使用来自puppetlabs/app_modelingcollect_component_titles函数提供一个我们可以迭代的数组。我们通过$nodes收集附加到Myapp::WebMyapp::Lb的节点。这些值被命名为allwebsalllbs

  • 我们使用来自puppetlabs/stdlibmap函数操作$allwebs。在这个map函数中,我们将每个节点名称转化为Http["web-${wordpress_name}"]的值,其中$wordpress_name是附加到Myapp::Web应用程序的每个节点的名称。我们将这个值作为每个MyApp::Web声明的导出。

  • 我们将$http (Http["web-${wordpress_name}"])的值返回给$https数组,以便我们可以在负载均衡器上使用这些值。

  • 我们的负载均衡器使用each语句代替map语句,因为我们不需要转换任何数据:

application myapp (
  $dbuser = 'wordpress',
  $dbpass = 'w0rdpr3ss!',
  $dbname = 'wordpress',
  $webpath = '/var/www/wordpress',
  $webport = '80'
) {

  myapp::db { $name:
    dbuser => $dbuser,
    dbpass => $dbpass,
    dbname => $dbname,
    export => Database["db-${name}"],
  }

# This section can be confusing, but here is essentially what's going on
# $allwebs is an array full of every node assigned to Myapp::Web in our application
# $https takes that $allwebs array of every node, creates a service resource,
# adds myapp::web to each node providing values for that service resource, and then
# returns all transformed service resource names back to the array.

# We're transforming each node listed in our site.pp into an array of Http[<nodename>]
# resource calls. And on each node we'll apply our defined type inside of the
# same map.

  $allwebs = collect_component_titles($nodes, Myapp::Web)

  $https = $allwebs.map |$wordpress_name| {

    $http = Http["web-${wordpress_name}"]

    myapp::web { "$wordpress_name":
      dbuser => $dbuser,
      dbpass => $dbpass,
      dbname => $dbname,
      webport => $webport,
      webpath => $webpath,
      consume => Database["db-${name}"],
      export => $http,
    }

    $http

  }

# We'll use an each statement here instead of a map, because we don't need
# any Load balancer values returned. They're the end of the chain. Our each
# statement covers each node, and $https from before is used to add nodes
# to the load balancer

  $alllbs = collect_component_titles($nodes, Myapp::Lb)

  $alllbs.each |$load_balancer| {

    myapp::lb { "${load_balancer}":
      balancermembers => $https,
      require => $https,
      port => '80',
      balance_mode => 'roundrobin',
    }

  }

}

我们的myapp::db生成一个 MySQL 服务器,并创建一个数据库来为我们的应用程序提供服务。我们在init.pp中使用dbuserdbpassdbname的值。请特别注意生成行,使用app_modeling服务资源来生成清单底部的数据库:

  • 生成一个主机,从将被 Web 应用程序消耗的机器的 FQDN 中提取。

  • 生成一个端口,虽然我们的 Web 清单中不使用该端口,但它提供了一个可用性测试,用于我们的应用程序编排节点。在能够通过 FQDN 上的端口3306连接到数据库之前,Web 的应用程序编排不会触发。如果没有这个声明,它将默认为5432,这是 Postgres 服务器的默认端口:

define myapp::db (
  $dbuser,
  $dbpass,
  $dbname,
){

  class {'::mysql::server':
    root_password => 'Sup3rp@ssword!',
    override_options => {
      'mysqld' => {
        'bind-address' => '0.0.0.0'
      }
    }
  }

  mysql::db { $dbname:
    user => $dbuser,
    password => $dbpass,
    host => '%',
    grant => ['ALL'],
  }
}
# This produces line is producing 2 values: host and port. We'll use host directly
# on Myapp::Web, but the port designator is used to pass the Resource Type test for
# Database using puppetlabs/app_modeling. Without the port, the test will fail to find
# the upstream Database and won't finish the agent run.
Myapp::Db produces Database {
  host => $::fqdn,
  port => '3306',

}

我们的Myapp::Web调用将使用来自初始应用程序的五个变量,但从所消耗的资源中接收数据库主机。请注意以下几点:

  • $dbhost的值由消耗的数据库填充。在底部,我们明确地将$dbhost的值映射到Myapp::Web消耗的$host值,并将其传递给Myapp::Db

  • 我们将由消费提供的$dbhost传递给wordpress类,实现与远程数据库的自动连接。

  • Myapp::Web生成一个 HTTP 资源,提供主机、端口、IP 和状态码。我们将使用主机、端口和 IP 为我们的负载均衡器提供服务,而status_codes则是另一个可用性测试,以确保由haproxy提供服务的网站状态码为302200

define myapp::web (
  $webpath,
  $webport,
  $dbuser,
  $dbpass,
  $dbhost,
  $dbname,
  ) {

    package {['php','mysql','php-mysql','php-gd']:
      ensure => installed,
    }

    class {'apache':
      default_vhost => false
    }

    include ::apache::mod::php

    apache::vhost { $::fqdn:
      port => $webport,
      docroot => $webpath,
      require => [File[$webpath]],
    }

    file { $webpath:
      ensure => directory,
      owner => 'apache',
      group => 'apache',
      require => Package['httpd'],
    }

    class { '::wordpress':
      db_user => $dbuser,
      db_password => $dbpass,
      db_host => $dbhost,
      db_name => $dbname,
      create_db => false,
      create_db_user => false,
      install_dir => $webpath,
      wp_owner => 'apache',
      wp_group => 'apache',
    }
  }
Myapp::Web consumes Database {
  dbhost => $host,
}
Myapp::Web produces Http {
  host => $::clientcert,
  port => $webport,
  ip => $::networking['interfaces']['enp0s8']['ip'],
  # Like the port parameter in the Database provider, we'll need to send the status_codes
  # flag to the Http provider to ensure we don't only accept a 302 status code.
  # A new wordpress application sends status code 200, so we'll let it through as well.
  status_codes => ['302','200'],
}

Myapp::Lb 实际上并不会消费或导出任何资源。我们构建了一个haproxy::listen服务,然后对于每个balancermember,我们导入了上述的值。在我们的应用声明中,我们将每个语句应用于$https数组中的每个成员,以下代码将这些数据转化为相关的负载均衡器。我们从每个myapp::web中获取主机、端口和 IP,并将其作为成员添加到我们的haproxy::listen中:

define myapp::lb (
  $balancermembers,
  String $ipaddress = '0.0.0.0',
  String $balance_mode = 'roundrobin',
  String $port = '80',
) {

  include haproxy

  haproxy::listen {"wordpress-${name}":
    collect_exported => false,
    ipaddress => $::networking['interfaces']['enp0s8']['ip'],
    mode => 'http',
    options => {
      'balance' => $balance_mode,
    },
    ports => $port,
  }

  $balancermembers.each |$member| {
    haproxy::balancermember { $member['host']:
      listening_service => "wordpress-${name}",
      server_names => $member['host'],
      ipaddresses => $member['ip'],
      ports => $member['port'],
    }
  }

}

部署

部署我们的新应用程序使用与之前相同的命令。我们将使用puppet app show来提供带排序的节点列表。你会看到,我们的单个数据库会产生一个数据库;每个 webapp 使用该数据库并生成一个 HTTP 服务资源,最终被每个负载均衡器使用:

[root@pe-puppet-master manifests]# puppet app show
Myapp[myapp]
 Myapp::Db[myapp] => mysql
 + produces Database[db-myapp]
 Myapp::Web[myapp-1] => appserver
 + produces Http[web-myapp-1]
 consumes Database[db-myapp]
 Myapp::Web[myapp-2] => appserver2
 + produces Http[web-myapp-2]
 consumes Database[db-myapp]
 Myapp::Lb[myapp-1] => haproxy
 consumes Http[web-myapp-1]
 consumes Http[web-myapp-2]
 Myapp::Lb[myapp-2] => haproxy2
 consumes Http[web-myapp-1]
 consumes Http[web-myapp-2]

在我们启动应用程序之前,我们可以运行一个puppet job plan来了解运行过程中排序的情况:

[root@pe-puppet-master manifests]# puppet job plan --application Myapp --environment production

+-------------------+------------+
| Environment | production |
| Target | Myapp |
| Concurrency Limit | None |
| Nodes | 5 |
+-------------------+------------+

Application instances: 1
 - Myapp[myapp]

Node run order (nodes in level 0 run before their dependent nodes in level 1, etc.):
0 -----------------------------------------------------------------------
mysql
 Myapp[myapp] - Myapp::Db[myapp]

1 -----------------------------------------------------------------------
wordpress
 Myapp[myapp] - Myapp::Web[myapp-1]
wordpress2
 Myapp[myapp] - Myapp::Web[myapp-2]

2 -----------------------------------------------------------------------
haproxy
 Myapp[myapp] - Myapp::Lb[myapp-1]
haproxy2
 Myapp[myapp] - Myapp::Lb[myapp-2]

Use `puppet job run --application 'Myapp' --environment production` to create and run a job like this

最后,我们运行我们的应用程序,看到 MySQL 被先配置,然后是我们的wordpress实例,接着是负载均衡器。感谢puppetlabs/app_modeling提供的服务资源,我们也知道,在wordpress服务器之前,数据库已经被主动访问,而且我们的wordpress服务器在负载均衡器配置之前会产生 302 状态码:

[root@pe-puppet-master production]# puppet job run --application Myapp --environment production --verbose
Starting deployment ...

+-------------------+------------+
| Job ID | 42 |
| Environment | production |
| Target | Myapp |
| Concurrency Limit | None |
| Nodes | 5 |
+-------------------+------------+

Application instances: 1
 - Myapp[myapp]

Node run order (nodes in level 0 run before their dependent nodes in level 1, etc.):
0 -----------------------------------------------------------------------
mysql
 Myapp[myapp] - Myapp::Db[myapp]

1 -----------------------------------------------------------------------
wordpress
 Myapp[myapp] - Myapp::Web[myapp-1]
wordpress-2
 Myapp[myapp] - Myapp::Web[myapp-2]

2 -----------------------------------------------------------------------
haproxy
 Myapp[myapp] - Myapp::Lb[myapp-1]
haproxy-2
 Myapp[myapp] - Myapp::Lb[myapp-2]

New job created: 42
Started puppet run on mysql ...
Finished puppet run on mysql - Success!
 Resource events: 0 failed 9 changed 27 unchanged 0 skipped 0 noop
 Report: https://pe-puppet-master/#/run/jobs/42/nodes/mysql/report
Started puppet run on wordpress-2 ...
Started puppet run on wordpress ...
Finished puppet run on wordpress-2 - Success!
 Resource events: 0 failed 81 changed 66 unchanged 0 skipped 0 noop
 Report: https://pe-puppet-master/#/run/jobs/42/nodes/wordpress-2/report
Finished puppet run on wordpress - Success!
 Resource events: 0 failed 81 changed 66 unchanged 0 skipped 0 noop
 Report: https://pe-puppet-master/#/run/jobs/42/nodes/wordpress/report
Started puppet run on haproxy-2 ...
Started puppet run on haproxy ...
Finished puppet run on haproxy - Success!
 Resource events: 0 failed 4 changed 30 unchanged 0 skipped 0 noop
 Report: https://pe-puppet-master/#/run/jobs/42/nodes/haproxy/report
Finished puppet run on haproxy-2 - Success!
 Resource events: 0 failed 4 changed 30 unchanged 0 skipped 0 noop
 Report: https://pe-puppet-master/#/run/jobs/42/nodes/haproxy-2/report

Success! 5/5 runs succeeded.
Duration: 58 sec

总结

在本章中,我们学习了如何使用应用程序编排来排序我们的应用程序。这建立在我们编写 Puppet 代码时学到的基础知识上,甚至在使用导出资源时也是如此。当我们构建更多应用程序和对象进行配置时,我们需要确保我们的 Puppet Master 能够为所有这些节点提供服务。

在下一章中,我们将讨论如何水平和垂直扩展 Puppet Enterprise。

第十一章:扩展 Puppet

Puppet 用于集中管理组织中的所有服务器。在一些组织中,节点总数可能达到数百个。其他组织则拥有成千上万的服务器。对于较少的服务器,我们可以在一台服务器上配置单一的整体 Puppet Master(Puppetserver、PuppetDB 或 PE Console)。一旦达到一定规模,我们可以将 Puppet Enterprise 的组件导出到独立的服务器上。随着服务器规模的增大,我们可以开始逐个扩展每个组件。本章将介绍安装 Puppet Enterprise 的模型、扩展到三台服务器,并最终通过负载均衡多个 Puppet 组件来支持非常大的 Puppet 安装。

在支持较小子集的服务器时,第一阶段是优化我们在单一主节点上的设置。

本章将主要讲解如何扩展 Puppet Enterprise。开源技术也将在扩展的背景下讨论,但完整的实施方法将留给 Puppet 开源的用户自行完成。

检查

在我们开始扩展服务之前,首先了解如何收集和理解这些系统的指标。PuppetDB 和 Puppet Enterprise 控制台都提供了仪表板。我们可以使用这些仪表板检查系统的指标并识别问题。随着环境的增长,我们希望确保 Puppet 有足够的系统资源,以确保能够编译并提供目录给代理。在 PuppetDB 和 Puppetserver 上分别提供了独立的仪表板。

Puppetserver

Puppetserver 是 Puppet 的主要驱动程序,是开源 Puppet 中唯一必需的组件。Puppetserver 开发者仪表板用于跟踪 Puppet Master 为代理提供目录的能力。该仪表板的主要关注点是 Puppetserver 上的 JRuby。Puppetserver 中的 JRuby 只是包含在Java 虚拟机JVM)中的小型 ruby 实例,专门用于为代理编译目录。

你可以通过以下链接访问 Puppetserver 开发者仪表板:https://<puppetserver>:8140/puppet/experimental/dashboard.html

仪表板包含一些关于 Puppetserver 的实时指标,分为当前指标和平均指标:

  • 空闲 JRuby:可用于提供 Puppet 目录的可用 JRuby 实例数量。

  • 请求的 JRuby:代理请求的 JRuby 数量。

  • JRuby 借用时间:Puppetserver 为单个请求从代理方保留的时间(毫秒)。

  • JRuby 等待时间:代理平均需要等待 JRuby 的时间。

  • JVM 堆内存使用量:JVM 中包含 JRuby 的系统内存使用量。

  • CPU 使用率:Puppetserver 所使用的 CPU。

  • GC CPU 使用率:Puppetserver 上垃圾回收GC)所使用的 CPU 量。

我们可以检查这些数据,获取关于 Puppetserver 主要任务的许多信息,这些任务包括编译和提供目录。首先要查看的关键组件之一是JRuby 等待时间。我们的节点是否经常等待接收目录?如果我们发现等待时间增加,可能需要更多的 JRuby 来处理代理请求。这也可能表现为低平均空闲 JRuby 数量,或当前请求的 JRuby 状态较高。我们还可以检查JRuby 借用时间,以了解我们的目录有多大,以及每个节点需要多少时间与 Puppetserver 进行通信。最后,我们还可以查看一些指标,以了解是否已为 Puppetserver 分配足够的内存和 CPU。

我们还可以获取一些关于 API 使用的有用数据,例如前 10 个请求,让我们了解在我们的基础设施中最常用的 API。前 10 个功能有助于识别在主服务器上最常使用的 Puppet 功能,而我们的前 10 个资源则可以帮助我们了解在环境中最常用的代码。

PuppetDB 仪表盘

PuppetDB 拥有自己的仪表盘,旨在展示服务器的运行情况。它主要用于帮助理解 PuppetDB 存储的数据。它涵盖了一些性能指标,如 JVM 堆,还能快速展示活动和非活动节点的数量。以下是 PuppetDB 提供的信息:

  • JVM 堆:数据库的总内存堆大小

  • 活动和非活动节点:PuppetDB 中包含信息的节点

  • 资源:PuppetDB 中看到的总资源数

  • 资源重复:PuppetDB 可以提供的重复资源总数(数量越高越好)

  • 目录重复:PuppetDB 可以提供的重复目录总数(数量越高越好)

  • 命令队列:等待执行的命令数量

  • 命令处理:执行命令时与数据库交互的耗时

  • 已处理:自启动以来已处理的查询数量

  • 重试:需要多次执行的查询数量

  • 丢弃:没有返回值的查询数量

  • 拒绝:被拒绝的查询数量

  • 排队:等待写入数据库的平均时间

  • 命令持久化:将数据从内存移动到磁盘所需的时间

  • 集合查询:集合查询服务时间(秒)

  • 数据库压缩:数据库压缩的往返时间

  • 磁盘上的 DLO 大小:磁盘上动态大对象的大小

  • 丢弃的消息:未进入 PuppetDB 的消息

  • 同步时长:同步数据到数据库之间所需的时间

  • 最后同步:自上次数据库同步以来的秒数

默认情况下,PuppetDB 在端口 8080 上运行 PuppetDB 仪表盘,但将此限制为本地主机。我们可以通过将 Web 端口转发到工作站,在本地访问它。命令 ssh -L 8080:localhost:8080 <user>@<puppetdb-server> 将允许你在运行该命令的同一工作站上访问 PuppetDB 仪表盘,地址为 http://localhost:8080

我们可以使用这些信息检查 PuppetDB 服务器的状态。我们希望看到高资源重复和目录重复,这能加速使用 PuppetDB 运行 Puppet 的整体速度。我们的 JVM 堆内存可以告诉我们内存使用情况。活动和非活动节点帮助我们了解存储在 PuppetDB 中的内容,以及即将被淘汰的内容。其他大多数数据是围绕数据库本身的指标,帮助我们了解 PostgreSQL 服务器的健康状况。一旦我们了解一些简单的实时指标,就可以开始着手调整环境。

调优

在进行服务的横向扩展之前,我们应该先优化现有的工作负载。最好的横向扩展是你根本不需要做的扩展。不要在你还可以用单个大型单体实例支持工作负载时就构建更多的 Puppet 组件节点。为 Puppet 添加更多资源可以让它服务更多的代理。对于一个单体 Puppet 主节点能够服务多少代理没有硬性规定,即使有额外的编译主节点也是如此。每个组织的 Puppet 目录大小都不同,这也是大多数组织的主要不确定变量。

如果你只需要一些简单的设置来入门,Puppet 为小型单体主节点以及带有附加编译主节点的单体主节点提供了标准的推荐设置,详情请见:puppet.com/docs/pe/latest/tuning_monolithic.html

Puppetserver 调优

Puppetserver 为每个代理生成目录,使用存放在我们环境中的代码并通过 JRuby 提供服务。我们将配置 JVM 并在企业版和开源版本的 Puppet 中实施我们的更改。

Puppetserver 在我们基础设施中的主要工作是处理代理请求并返回目录。在 Puppet 的旧版本中,常用 RubyGem Passenger 来并发处理多个代理的请求。今天,Puppet 在 Puppetserver 上运行多个 JRuby 实例来处理并发请求。虽然 Ruby 本身是使用操作系统的本地编译器运行的,但 JRuby 在一个独立的 JVM 实例中运行 Ruby。这些 JRuby 实例可以提供更好的扩展性,支持 Puppet 的多个并发和线程安全的运行。每个 JRuby 每次只能服务一个代理,Puppet 会将代理排队,直到有 JRuby 可用。

每个 JVM(包含 JRuby 实例)都有最小和最大堆大小。最大堆大小确定 JVM 在垃圾收集开始前可以消耗多少内存。垃圾收集只是从最旧的数据到最新的数据开始清除内存的过程。最小堆大小确保新的 JVM 分配足够的内存来运行应用程序。如果 JRuby 无法为 Puppet 实例分配足够的内存,将触发 OutOfMemory 错误并关闭 Puppetserver。通常我们将我们的 Java 最大堆大小(有时称为 -Xmx)和最小堆大小(-Xms)设置为相同的值,以便新的 JRuby 以所需的内存启动。我们还可以使用 max-active-instances 设置最大 JRuby 实例数。Puppet 通常建议此数字接近 Puppetserver 可用的 CPU 数量。

Puppet Enterprise 实现

在 Puppet Enterprise 中,我们可以通过 Hiera 中的以下设置配置我们的 Java 设置:

puppet_enterprise::profile::master::java_args:
  Xmx: 512m
  Xms: 512m

开源实现

在开源中,我们需要使用自己的模块来管理我们的设置。幸运的是,camptocamp/puppetserver 提供了我们所需的一切!我们可以使用此模块创建适用于我们 Puppetserver 的配置文件:

class profile::puppetserver {

  class { 'puppetserver':
    config => {
      'java_args'     => {
        'xms'         => '4g',
        'xmx'         => '6g',
        'maxpermsize' => '512m',
      },
    }
  }

}

在开源安装中,较大安装中每个组件所需的 ulimits 可能不存在。如果您的主节点正在为大量节点提供服务且无法在 Linux 操作系统上打开更多文件,则可以按照 puppet.com/docs/pe/latest/config_ulimit.html 上的说明操作。

调整 PuppetDB

PuppetDB 安装在一个 PostgreSQL 实例上,通常可以像管理任何 PostgreSQL 服务器一样进行管理。我们有一些配置选项可以帮助调整您的 PostgreSQL PuppetDB 实例以适应您的环境:

  • 停用和清除节点

  • 调整最大堆大小

  • 调整线程数

停用和清除节点

PuppetDB 保存每个节点在您的 Puppet Enterprise 安装中检查的记录。在节点经常出现和消失的环境中,例如不可变基础设施中,可能会堆积大量有关影响数据库和基础设施性能的节点数据。默认情况下,Puppet 将过期未在七天内检查的节点,并将停止从目录导出对象。此设置可以在 puppet.conf[database] 部分下管理 node-ttl 设置。另一个设置 node-purge-ttl 让数据库知道何时删除节点的记录。默认情况下,Puppet Enterprise 的清除时间为 14 天。我们也可以使用 puppet node deactivatepuppet node purge 手动执行这些任务。

我们可以使用 puppetlabs/inifile 如下所示来管理默认设置:

# This profile will clean out nodes much more aggressively, deactivating nodes not seen for 2 days, and purging nodes not seen for 4.

class profile::puppetdb {

  ini_setting { 'Node TTL':
    ensure  => present,
    path    => '/etc/puppetlabs/puppet/puppet.conf',
    section => 'database',
    setting => 'node-ttl',
    value   => '2d',
  }

  ini_setting { 'Node Purge TTL':
    ensure  => present,
    path    => '/etc/puppetlabs/puppet/puppet.conf',
    section => 'database',
    setting => 'node-purge-ttl',
    value   => '4d',
  }

}

管理堆大小

我们的 PuppetDB 的最大堆大小将取决于节点总数,Puppet 运行的频率以及 Puppet 管理的资源量。确定堆大小需求的最简单方法是估计或使用默认值,并监视性能仪表板。如果您的数据库触发 OutOfMemory 异常,只需提供更大的内存分配并重新启动服务。如果 JVM 堆指标经常接近最大值,则需要使用 Java args 增加最大堆大小,由 PostgreSQL 的 init 脚本管理。PuppetDB 将从服务死亡时的队列相同点开始处理请求。在开源安装中,此文件将被命名为 puppetdb,在 Puppet Enterprise 安装中将被命名为 pe-puppetdb。在 Enterprise Linux 发行版(如 Red Hat)中,这些文件将位于 /etc/sysconfig。像 Ubuntu 这样的基于 Debian 的系统将把此文件放在 /etc/default 中。

在 Puppet Enterprise 安装中,我们可以使用以下 Hiera 值设置我们的堆大小:

puppet_enterprise::profile::puppetdb::java_args:
  Xms: 1024m
  Xmx: 1024m

在开源安装中,最好使用来自 Forge 的 puppet/puppetdb,我们可以简单地通过 puppetdb 类设置 Java args:

class profile::puppetdb {

  class {'puppetdb':
    java_args => {
      '-Xmx' => '1024m',
      '-Xms' => '1024m',
    },
  }

}

调整 CPU 线程

对 PuppetDB 进行 CPU 线程调整并不总是一个简单的情况,添加更多并且它将表现更好。PuppetDB 上的 CPU 正在使用 PostgreSQL 实例、消息队列MQ)和 PuppetDB 提供的 Web 服务器。如果您的服务器确实有可用的 CPU 资源,请考虑添加更多 CPU 线程以一次处理更多消息。如果增加 CPU 数量到 PuppetDB 实际上降低了吞吐量,那么确保更多的 CPU 资源可供 MQ 和 Web 服务器使用。CPU 线程的设置也可以在 puppet.conf[command-processing] 部分找到。

在 Puppet Enterprise 安装中,此设置将由 Hiera 管理:

puppet_enterprise::puppetdb::command_processing_threads: 2

在开源安装中,我们将再次使用 puppetlabs/puppetdb 来管理此设置:

class profile::puppetdb {

  class {'puppetdb':
    command_threads => '2',
  }

}

自动确定设置

现在我们已经看过一些设置,我们可以看一些工具,帮助我们利用我们的硬件提供一个体面的基线。首先,我们将自动调整我们的完整 Puppet Enterprise 安装,并使用 PGTune 调整我们的 PuppetDB 实例。

Puppet Enterprise

在检查和调整系统之前,我们将找到一组基于可用硬件的推荐设置。Puppet 的 Thomas Kishel 设计了一个 Puppet Face,用于查询 PuppetDB 以获取 Puppet Enterprise 基础设施。此命令检查系统上可用的资源,并为以下 Puppet Enterprise 安装提供合理的默认设置:

  • 单体基础架构

  • 带有编译主服务器的单体结构

  • 带有外部 PostgreSQL 的单体结构

  • 带有外部 PostgreSQL 的单体结构

  • 带有高可用性的单体结构

  • 带有编译主服务器和高可用性的单体结构

  • 分割基础架构

  • 带有编译主服务器的分割结构

  • 带有外部 PostgreSQL 的分割结构

  • 使用外部 PostgreSQL 拆分编译主服务器

要开始使用tkishel/pe_tune,我们需要将 Git 仓库克隆到主服务器上的 Puppet Enterprise 中,并使tune.rb脚本可执行:

git clone https://github.com/tkishel/pe_tune.git
chmod +x ./pe_tune/lib/puppet_x/puppetlabs/tune.rb 

当我们克隆并使二进制文件可执行后,我们将运行tune.rb来获取有关系统的信息,并在 Hiera 中返回合理的 Puppet Enterprise 设置:

[root@pe-puppet-master ~]# ./pe_tune/lib/puppet_x/puppetlabs/tune.rb
### Puppet Infrastructure Summary: Found a Monolithic Infrastructure

## Found: 4 CPU(s) / 9839 MB RAM for Primary Master pe-puppet-master
## Specify the following optimized settings in Hiera in nodes/pe-puppet-master.yaml

---
puppet_enterprise::profile::database::shared_buffers: 3072MB
puppet_enterprise::puppetdb::command_processing_threads: 2
puppet_enterprise::master::puppetserver::jruby_max_active_instances: 2
puppet_enterprise::master::puppetserver::reserved_code_cache: 1024m
puppet_enterprise::profile::master::java_args:
 Xms: 2048m
 Xmx: 2048m
puppet_enterprise::profile::puppetdb::java_args:
 Xms: 1024m
 Xmx: 1024m
puppet_enterprise::profile::console::java_args:
 Xms: 768m
 Xmx: 768m
puppet_enterprise::profile::orchestrator::java_args:
 Xms: 768m
 Xmx: 768m

## CPU Summary: Total/Used/Free: 4/4/0 for pe-puppet-master
## RAM Summary: Total/Used/Free: 9839/8704/1135 for pe-puppet-master
## JVM Summary: Using 768 MB per Puppet Server JRuby for pe-puppet-master

然后,我们可以将这些值放入 Hiera 中的任何地方,Puppet Enterprise 安装可以从那里获取。我建议使用common.yaml,除非你有专门为 Puppet 设置而保留的 Hiera 层。

默认情况下,该脚本将在少于 4 个 CPU 或 8GB 内存的基础设施主机上无法运行。即使在小于推荐的 4 个 CPU 和 8GB 内存的节点上,你也可以使用--force标志来运行命令,获取结果。

PuppetDB – 使用 PGTune 的 PostgreSQL

当不确定如何调整 PostgreSQL 服务器时,尝试使用 PGTune。该项目会读取当前的postgresql.conf并输出一个新的配置文件,包含为当前机器设计的调优设置。值得注意的是,这不会考虑消息队列或 Web 服务器所需的内存,因此通过稍微调低这些设置来留下少量额外资源,可以帮助提升性能。

请注意,PGTune 假设它运行的节点唯一的目的是提供 Postgres 服务器。这些设置在单一的整体主服务器上使用会比较困难,而tkishel/pe_tune会是配置这些服务器时更有用的工具。

我们将从克隆并进入当前的 PGTune 项目开始:

git clone https://github.com/gregs1104/pgtune.git
Cloning into 'pgtune'...
remote: Counting objects: 112, done.
remote: Total 112 (delta 0), reused 0 (delta 0), pack-reused 112
Receiving objects: 100% (112/112), 66.21 KiB | 0 bytes/s, done.
Resolving deltas: 100% (63/63), done.
cd pgtune

然后我们将对 Puppet Enterprise 的postgresql.conf运行 PGTune:

./pgtune -i /opt/puppetlabs/server/data/postgresql/9.6/data/postgresql.conf
#------------------------------------------------------------------------------
# pgtune for version 8.4 run on 2018-08-19
# Based on 3882384 KB RAM, platform Linux, 100 clients and mixed workload
#------------------------------------------------------------------------------

default_statistics_target = 100
maintenance_work_mem = 224MB
checkpoint_completion_target = 0.9
effective_cache_size = 2816MB
work_mem = 18MB
wal_buffers = 16MB
checkpoint_segments = 32
shared_buffers = 896MB
max_connections = 100

这些设置以手动管理postgresql.conf的形式返回。我们将这些值转换为 Puppet Enterprise Hiera 设置,并可以放入common.yaml中来驱动我们的 PuppetDB:

---
puppet_enterprise::profile::database::maintenance_work_mem: 224MB
puppet_enterprise::profile::database::checking_completion_target = 0.9
puppet_enterprise::profile::database::effective_cache_size: 2816MB
puppet_enterprise::profile::database::work_mem: 18MB
puppet_enterprise::profile::database::wal_buffers: 16MB
puppet_enterprise::profile::database::checkpoint_segments: 32
puppet_enterprise::profile::database::shared_buffers: 896MB

# PgTune recommends just 100 max_connections, but Puppet Enterprise 
# generally recommends a higher amount due to the number of nodes that 
# can connect to the system. I'll tune it for that purpose.
puppet_enterprise::profile::database::max_connections: 400

使用开源时,我们将依赖于puppetlabs/postgresql模块,它是puppetlabs/puppetdb的一个依赖。我们想要设置的每个值都是一个独立的资源,可以在 PuppetDB 级别的 Hiera 中表示。如果你在环境中有其他 PostgreSQL 服务器,建议不要将这些特定设置放在common.yaml中:

---
postgresql::server::config_entries:
  maintenance_work_mem: 224MB
  checkpoint_completion_target: 0.9
  effective_cache_size: 2816MB
  work_mem: 18MB
  wal_buffers: 16MB
  checkpoint_segments: 32
  shared_buffers: 896MB
  max_connections: 400

理解这些关键概念可以让我们配置各个节点以最大化性能。对于许多用户来说,这已经足够在他们的环境中运行 Puppet。如果是更为极端的情况,我们可以转向水平扩展,允许更多的 Puppetserver 和 PuppetDB 副本支持更多的代理。

水平扩展

当单一的整体主服务器无法再满足我们的环境需求时,我们将主服务器拆分为独立的组件:控制台、Puppetserver 和 PuppetDB。这使我们能够以更小的足迹服务更多的客户端。在不断增长的环境中,即便是这种设置,也可能无法满足所有代理的需求。

在本节中,我们将讨论如何扩展 Puppetserver、PuppetDB 和证书授权中心,以便为更多的代理提供服务。通过垂直调优和水平扩展的概念,我们可以为大量节点提供服务,最多可在单一设置中支持数万个单独的服务器。

Puppetserver

一般来说,在任何 Puppet 设置中,首个需要扩展的组件是 Puppetserver。Puppetserver 承担了 Puppet 中的大部分工作,负责将目录编译传输到代理。在本节中,我们将探讨一些关于 Puppetserver 能支持多少个代理的理论,如何创建新的 Puppetservers,以及围绕 Puppet 主节点的一些负载均衡策略。我们将从开源和企业版的角度来进行分析。

估算一个 Puppetserver 能支持的代理数量

Puppet 有一个数学公式用于估算一个 Puppetserver 能够支持多少个节点。这个公式只是一个估算值,不能替代实际的基准测试,因为如目录编译大小等因素通常会随着时间的推移发生变化。

Puppetserver 的估算公式表示为 j = ns/mr。在这个公式中,我们看到以下的值:

  • j:每个主节点的 JRuby 实例数量

  • m:编译主节点数量(Puppetservers)

  • n:主节点服务的节点数量

  • s:目录编译大小(秒)

  • r:运行间隔(秒)

使用这个公式,我们可以设定一个简单的指标来进行计算:一台拥有一个 JRuby 实例的 Puppetserver,平均目录编译时间为 10 秒,默认运行间隔为 30 分钟,最多能为多少个节点提供服务?我们的公式如下:1 = n10 / 11800*。我们可以简化为 1 = n10 / 1800。将公式两边相乘得到 1800 = n10。通过将两边同时除以 10,我们得到 n = 180

一台主节点,拥有一个 JRuby 实例,运行间隔为 30 分钟,目录编译时间为 10 秒,可以为 180 个代理提供服务。如果我们想要支持更多代理,可以选择以下方式:

  • 增加每个主节点的 JRuby 实例数量

  • 增加编译主节点数量

  • 减少运行间隔

  • 使用更高效的代码减少目录编译时间

只需将这个小型服务器升级为具有 8 个 CPU 的服务器,并将 jruby_max_active_instances 设置为 8,就可以在此服务器上支持 1,440 个代理。再增加两个具有相同数量 CPU 的编译主节点后,可以支持 4,320 个代理。我们可以不断增加更多的 Puppetservers,直到能够为我们基础设施中的所有节点提供服务。

添加新的编译主节点

在 Puppet Enterprise 安装中,增加新的编译主节点非常容易。只需将新节点添加到 PE Master 分类组下的 PE 基础设施中:

这些节点将接收与主节点相同的配置,包括代码管理器配置和与 PuppetDB 的必要连接。管理 Puppet Enterprise 中额外编译主节点没有任何隐藏的技巧。只需将它们分类并添加到负载均衡器中。

在开源版本中,我们需要确保每个 Puppet Master 都配置为使用 PuppetDB。幸运的是,puppetlabs/puppetdb为我们提供了这个连接:

class profile::puppetserver {
  class { 'puppetdb::master::config':
    puppetdb_server => <hostname of PuppetDB>,
  }
}

我们仍然需要确保这个开源安装能够检索代码。与 Code Manager 不同,r10k 不能跨服务器进行联邦管理,因此你需要确定将代码部署到这些主节点的策略。管理此问题的一种简便方法包含在puppet/r10k 模块中!puppet/r10k模块不仅可以在每个 Puppetserver 上以相同方式配置 r10k,还可以在该模块中使用新的 Puppet 任务来部署代码。可以从命令行运行此任务,或者最好是在 CI/CD 服务器上进行提交时运行:

$ puppet task run r10k::deploy environment=production -n puppet-master1,puppet-master2,puppet-master3

负载均衡

当我们有多个 Puppetserver 时,重要的是要决定代理如何确定连接到哪个服务器。我们将检查一种简单的策略,即将 Puppetserver 放置在最接近它们所服务节点的位置,以及适用于更大基础设施需求的负载均衡策略。如果存在隔离主节点的安全需求和更多 Puppetserver 进行目录编译的技术需求,这两种方法可以结合使用。

简单设置 – 直接连接

许多组织使用的最简单的设置之一是隔离数据中心并为每个数据中心提供一个 Puppetserver。一些组织在全球各地都有数据中心,无论是在云中不同的区域,还是在不同地点的本地数据中心。为这些单独的数据中心提供编译主节点是一个相对简单的任务,只需要做几件事:

  • 代理知道编译主节点的完全限定域名(FQDN)并具有与其的网络连接。

  • 编译主节点与主节点有连接,主节点有时被称为主节点的主节点

在这个设置中,在配置过程中,代理会联系本地的编译主节点进行代理安装。在 Puppet Enterprise 安装中,代理只需在配置过程中运行curl -k https://<compile_master>:8140/packages/current/install.bash命令,它将通过在 PE Master 节点组中找到的pe_repo分类来获取代理。该代理不需要与 PuppetDB、主节点或 PE 控制台的网络连接,因为信息将由中间的编译主节点处理。

以下是来自 Puppet 的图示,展示了在 Puppet Enterprise 的大型环境安装中,每个组件所需的防火墙连接:

在开源安装中,这些端口依然适用,尽管从 Puppet 控制台无法访问节点分类器 API 端点。

如果单个数据中心发展到需要多个编译主机,或者我们希望为每个数据中心集中管理编译主机,那么我们就需要关注负载均衡。在负载均衡的集群中,本节的所有内容仍然适用,但在负载均衡器后面有一些新要处理的内容。

负载均衡

在非常大的环境中,我们可能会担心是否有足够的资源为所有代理提供服务。我们开始构建更多的编译主机,代理需要连接到它们。当将编译主机放置在负载均衡器后时,只有几个关键的附加问题需要关注:证书管理和负载均衡策略。

Puppet 在编译时使用自签名证书在代理和主机之间建立受信任的 SSL 连接。默认情况下,主机和代理的 FQDN 会被记录在各自的证书中。在每次连接时,代理会检查证书以确保请求的域名包含在证书中。如果我们的代理通过 DNS 或负载均衡的 VIP 连接到puppet.example.com,但证书中没有明确包含该名称,代理将拒绝连接。我们需要为我们的编译主机池确定一个通用名称(通常只是一个短名称,例如puppet),并将其嵌入到证书中。我们可以在每个编译主机的puppet.conf文件的主配置部分中包含多个 DNS 备用名称:

[main]
dns_alt_names = puppet,puppet-portland
...

当我们第一次连接到 Puppet Master 时,这些dns_alt_names将被嵌入到我们的证书中。对于企业用户,这些证书不会出现在 Puppet Enterprise 控制台中,以防止有人意外地通过 GUI 批准 DNS 备用名称。您需要登录到 Puppet Master 并运行puppet cert sign <name> --allow-dns-alt-names来签署证书,并接受带有备用名称的证书。如果您已经构建了这个编译主机并需要重新生成证书,您可以在 Master of Masters 上运行puppet cert clean <name>,并在重新运行代理之前,在编译主机上使用sudo rm -r $(puppet master --configprint ssldir)删除 SSL 目录。

通常认为,在任何代理上,包括编译主机,删除 SSL 目录是安全的。然而,在作为集中式证书授权机构的主机上执行此操作,将会导致所有 SSL 连接和所有 Puppet 运行在环境中停止。如果这样做,您将需要在主机上重建证书授权。相关的操作说明可以参考:docs.puppet.com/puppet/4.4/ssl_regenerate_certificates.html

现在你的代理应该通过它们的公共 DNS 替代名称引用所有编译主机。你需要决定负载均衡策略:使用 DNS 轮询、DNS SRV 记录,或使用专用的负载均衡器。主要的 DNS 提供商都提供了 DNS 轮询和 SRV 记录机制,你应该参考他们的文档。我们将通过一个示例,设置 HAProxy 作为我们的编译主机的软件负载均衡器,假设它们都在一个单独的池中。我们将使用puppetlabs/haproxy及其使用示例来为多个编译主机构建一个 HAProxy 实例。我们也可以使用从第九章中导出的资源示例,导出的资源,但是由于我们通常不会将 Puppet 主机添加到负载均衡器中,所以我们将使用一个简单的示例。

class puppet::proxy {

  include ::haproxy

  haproxy::listen { 'puppetmaster':
    collect_exported => false,
    ipaddress        => $::ipaddress,
    ports            => '8140',
  }

  haproxy::balancermember { 'master00':
    listening_service => 'puppetmaster',
    server_names      => 'master00.packt.com',
    ipaddresses       => '10.10.10.100',
    ports             => '8140',
    options           => 'check',
  }
  haproxy::balancermember { 'master01':
    listening_service => 'puppetmaster',
    server_names      => 'master01.packt.com',
    ipaddresses       => '10.10.10.101',
    ports             => '8140',
    options           => 'check',
  }
}

使用此配置,我们的 HAProxy 将能够为所有请求连接到编译主机的代理提供服务。

证书颁发机构

在 Puppet Enterprise 安装中,编译主机的证书颁发机构部分相对容易解决。Puppet Enterprise 为 CA 和编译主机使用不同的节点组。通过将额外的编译主机添加到 PE 主分类组中,每个主机都配置为使用 Master of Masters 上的集中式证书颁发机构。

在 Puppet 开源版本中,我们需要在每个编译主机上禁用证书颁发机构(CA),可以通过 Trapperkeeper 来完成。你只需打开/etc/puppetlabs/puppetserver/services.d/ca.cfg文件,注释掉puppetlabs.services.ca.certificate-authority-service/certificate-authority-service这一行,并取消注释#puppetlabs.services.ca.certificate-authority-disabled-service/certificate-authority-disabled-service。最后,你需要在基础设施中的每个代理(包括编译主机)上,在puppet.conf[main]部分中添加ca_server设置,指向 Master of Masters。请注意,这要求通过 CA 端口与 Master of Masters 建立网络连接,默认端口是8140,但可以通过ca_port设置进行调整。

此配置的最终目标是每个编译主机都有一个 DNS 替代名称,并且每个代理都通过该 DNS 替代名称连接到主机,同时使用 Master of Masters 作为所有节点的证书颁发机构。

PuppetDB

扩展 PuppetDB 通常是扩展 PostgreSQL。一个单独的 PuppetDB 可以覆盖大量的节点和编译主机,但如果需要扩展 PuppetDB,请参考 PostgreSQL 文档和组织数据库指导。已知的扩展 PostgreSQL 方法,包括与 Puppet 配合使用的有:

  • 高可用性设置

  • 负载均衡

  • 数据库复制

  • 数据库集群

  • 连接池

概述

在本章中,我们讨论了如何扩展 Puppet。我们首先学习了如何监控 Puppet 内部组件,并如何调整单个 Puppet 组件的性能。接着,我们讨论了横向扩展,增加更多的编译主机以服务更多的代理。我们还讨论了如何通过 HAProxy 对 Puppet 服务器进行负载均衡,并提到 PuppetDB 可以像任何 PostgreSQL 数据库一样进行扩展。

在下一章中,我们将探讨 Puppet Enterprise 的故障排除。学习如何阅读和理解 Puppet 中可能出现的错误,将帮助你成为更好的实践者,并让你真正理解 Puppet 系统。

第十二章:故障排除与性能分析

有时,我们的 Puppet 基础设施和代码似乎并没有与我们协同工作。在本章中,我们将专注于排查一些常见问题。

本章将涵盖的主要主题如下:

  • Puppet 基础设施组件错误

  • 常见的目录编译错误

  • 日志记录

虽然这不是一个总是很令人兴奋的话题,但知道如何处理这些问题是成功管理任何系统和语言(包括 Puppet)的关键。在我们深入到代码之前,我们会确保我们的 Puppet 基础设施已经准备好。

常见组件错误

本节将讨论健康的 Puppet 安装。我们将主要关注我们在代理节点上看到的常见问题,以及这些问题可能对您的 Puppet 系统意味着什么。我们将在编写、测试和部署代码到服务器时常见的错误情况进行排查。我们将主要从 Puppet 代理的角度进行故障排除,因此您将看到团队成员在处理 Puppet 部署时最常遇到的问题。

Puppet 代理和 Puppetserver

Puppet 基础设施中的所有节点都包含一个 Puppet 代理。在分布式安装中,每个组件都与 Puppetserver 进行通信,就像基础设施中管理的其他节点一样。在单体安装中,Puppet 代理与自身进行通信。Puppet 管理的每个节点都必须使用代理来检索配置。由于代理无处不在,了解一些常见的代理错误将对故障排除具有普遍的帮助。以下是一些常见的导致代理故障的原因:

  • 证书重用

  • 连接到主节点时的错误用户上下文

  • 网络连接

  • DNS 替代名称

等待证书签署

运行代理第一次时,您会看到一个简单的错误信息,failed to retrieve certificate and waitforcert is disabled

Exiting; failed to retrieve certificate and waitforcert is disabled

这个特定的消息很容易解决。我们的代理正在通知我们,它没有收到来自主节点的签名证书。我们可以通过简单地以 root 用户登录到 Puppet Master 并签署我们的证书来解决这个问题。我们可以使用命令 puppet cert list 查看在 Puppet Master 上待处理的证书,如下所示:

[root@wordpress puppetlabs]# puppet agent -t
Exiting; no certificate found and waitforcert is disabled

在上面的代码中,我们可以看到我们的 wordpress 节点尚未签名,我们只需要使用 puppet cert sign 来批准该节点:

[root@pe-puppet-master ~]# puppet cert list
 "wordpress" (SHA256) F4:9E:56:9E:07:3F:66:B3:B4:CE:81:9E:1E:ED:FC:43:B9:A2:CC:88:78:8D:C5:30:CA:B0:B7:6D:0F:77:86:20

[root@pe-puppet-master ~]# puppet cert sign wordpress
Signing Certificate Request for:
 "wordpress" (SHA256) F4:9E:56:9E:07:3F:66:B3:B4:CE:81:9E:1E:ED:FC:43:B9:A2:CC:88:78:8D:C5:30:CA:B0:B7:6D:0F:77:86:20
Notice: Signed certificate request for wordpress
Notice: Removing file Puppet::SSL::CertificateRequest wordpress at '/etc/puppetlabs/puppet/ssl/ca/requests/wordpress.pem'

如果我们没有通过 autosign.conf 自动签署证书,或者没有使用提供自动签署功能的 ENC,我们将始终需要记得为新节点签署证书。

证书重用

有时,我们通过使用之前已知的 cert 名称来启动一个新节点,尤其是在不可变基础设施中。我们的 Puppet 基础设施在设计时考虑了证书安全,因此拥有一个 Puppet Master 已知名称的新节点时,会显示如下消息:

[root@wordpress puppet]# puppet agent -t
Error: Could not request certificate: The certificate retrieved from the master does not match the agent's private key. Did you forget to run as root?
Certificate fingerprint: 88:7F:B2:88:15:20:0A:55:3F:DE:2A:36:2C:B1:52:50:F1:77:96:EA:79:75:A1:00:B9:D6:3E:0B:93:45:D8:1C
To fix this, remove the certificate from both the master and the agent and then start a puppet run, which will automatically regenerate a certificate.
On the master:
 puppet cert clean wordpress
On the agent:
 1a. On most platforms: find /etc/puppetlabs/puppet/ssl -name wordpress.pem -delete
 1b. On Windows: del "\etc\puppetlabs\puppet\ssl\certs\wordpress.pem" /f
 2\. puppet agent -t

Exiting; failed to retrieve certificate and waitforcert is disabled

解决此错误的简单方法是,在重新运行代理之前,先清理 Puppet Master 上的证书,并重新签署证书,如下所示:

[root@pe-puppet-master manifests]# puppet cert clean wordpress
Notice: Revoked certificate with serial 18
Notice: Removing file Puppet::SSL::Certificate wordpress at '/etc/puppetlabs/puppet/ssl/ca/signed/wordpress.pem'
Notice: Removing file Puppet::SSL::Certificate wordpress at '/etc/puppetlabs/puppet/ssl/certs/wordpress.pem'

此外,Puppet 不允许我们重新运行代理,直到我们删除最近生成的证书。错误消息提供了删除证书的最佳命令,这样可以在代理上重新生成证书:find /etc/puppetlabs/puppet/ssl -name <fqdn>.pem -delete。在大多数代理上,实际上删除整个 SSL 目录是更安全的,使用命令 rm -rf /etc/puppetlabs/puppet/ssl

删除 Puppet Master 上的 SSL 目录会删除整个证书链,导致需要一整套新的证书。在 Puppet 的旧版本中,这个问题更难解决;现在,我们可以通过遵循以下指南来解决它:puppet.com/docs/puppet/latest/ssl_regenerate_certificates.html。确保不要不小心删除 Master 上的 SSL 证书,而是删除代理的证书。

防止此错误的方法很简单,只需在退役任何连接到 Puppet Master 的节点后,在 Puppet Master 上运行 puppet cert clean <nodename>

错误的 Puppet 用户

当我们编写代码时,我们通常会登录到测试机器上手动运行代理,并了解发生了什么情况。我们很少直接以 root 身份登录,而很容易忘记将用户切换为 root。这个问题尤其让人沮丧,因为它表现为证书错误。我们的个人用户生成了一个新的证书,无法通过 SSL 错误连接到 Master。你在错误日志中会注意到的关键区别是建议删除本地证书。

这种情况通常发生在测试时,将代理作为错误的用户身份运行。注意生成新密钥,以及第 1 行中的用户上下文和证书清理消息:在以下示例中,注意到一个新的 SSL 密钥正在生成,并且我正在以自己的用户身份而非 root 身份运行此命令:

[rary@wordpress ~]$ puppet agent -t
Info: Creating a new SSL key for wordpress
Info: Caching certificate for ca
Info: Caching certificate for wordpress
Error: Could not request certificate: The certificate retrieved from the master does not match the agent's private key. Did you forget to run as root?
Certificate fingerprint: 0C:10:48:BB:F9:F4:12:4A:66:52:FD:BB:33:DF:54:67:98:B4:D1:01:96:DE:6B:A4:D1:29:19:3C:C8:83:15:8C
To fix this, remove the certificate from both the master and the agent and then start a puppet run, which will automatically regenerate a certificate.
On the master:
 puppet cert clean wordpress
On the agent:
 1a. On most platforms: find /home/rary/.puppetlabs/etc/puppet/ssl -name wordpress.packt.com.pem -delete
 1b. On Windows: del "\home\rary\.puppetlabs\etc\puppet\ssl\certs\wordpress.packt.com.pem" /f
 2\. puppet agent -t

Exiting; failed to retrieve certificate and waitforcert is disabled

网络连接问题

在 Puppet 中,网络连接问题可能会非常嘈杂。以下代码示例中的代理无法与 Master 通信,原因可能是网络路由问题或防火墙阻止了与 Puppet Master 的流量。在以下示例中,防火墙阻止了代理与 Master 连接:

[root@wordpress ~]# puppet agent -t
Warning: Unable to fetch my node definition, but the agent run will continue:
Warning: Failed to open TCP connection to pe-puppet-master:8140 (No route to host - connect(2) for "pe-puppet-master" port 8140)
Info: Retrieving pluginfacts
Error: /File[/opt/puppetlabs/puppet/cache/facts.d]: Failed to generate additional resources using 'eval_generate': Failed to open TCP connection to pe-puppet-master:8140 (No route to host - connect(2) for "pe-puppet-master" port 8140)
Error: /File[/opt/puppetlabs/puppet/cache/facts.d]: Could not evaluate: Could not retrieve file metadata for puppet:///pluginfacts: Failed to open TCP connection to pe-puppet-master:8140 (No route to host - connect(2) for "pe-puppet-master" port 8140)
Info: Retrieving plugin
Error: /File[/opt/puppetlabs/puppet/cache/lib]: Failed to generate additional resources using 'eval_generate': Failed to open TCP connection to pe-puppet-master:8140 (No route to host - connect(2) for "pe-puppet-master" port 8140)
Error: /File[/opt/puppetlabs/puppet/cache/lib]: Could not evaluate: Could not retrieve file metadata for puppet:///plugins: Failed to open TCP connection to pe-puppet-master:8140 (No route to host - connect(2) for "pe-puppet-master" port 8140)
Info: Loading facts
Error: Could not retrieve catalog from remote server: Failed to open TCP connection to pe-puppet-master:8140 (No route to host - connect(2) for "pe-puppet-master" port 8140)
Warning: Not using cache on failed catalog
Error: Could not retrieve catalog; skipping run
Error: Could not send report: Failed to open TCP connection to pe-puppet-master:8140 (No route to host - connect(2) for "pe-puppet-master" port 8140)

你可能会注意到前面示例中的重复主题:No route to host(无法连接到主机)和Failed to open TCP Connection(无法打开 TCP 连接)。我们的目录编译的每个组件都会单独打印一条消息,提醒我们连接失败。当我们看到“无法连接到主机”时,说明在代理和主服务器之间有防火墙,或者没有通往主机的网络路径。这也可能是由于代理在尝试连接主服务器时,DNS 或/etc/hosts条目配置不正确导致的。

DNS 备用名称

DNS 备用名称在大型 Puppet 基础架构中非常方便。它们可以有效地为我们的服务器单独命名,或者作为一个整体命名。常见的 DNS 备用名称可能是puppet,这样你就可以使用负载均衡器来服务所有单独的 Puppetservers。

在以下示例中,我们尝试使用名称alt-name.puppet.net连接到我们的 Puppetserver,而这个名称在最初签署 Puppet 服务器时并没有包含在证书中:

[root@wordpress puppet]# puppet agent -t --server=alt-name.puppet.net
Warning: Unable to fetch my node definition, but the agent run will continue:
Warning: SSL_connect returned=1 errno=0 state=error: certificate verify failed: [ok for /CN=pe-puppet-master]
Info: Retrieving pluginfacts
Error: /File[/opt/puppetlabs/puppet/cache/facts.d]: Failed to generate additional resources using 'eval_generate': SSL_connect returned=1 errno=0 state=error: certificate verify failed: [ok for /CN=pe-puppet-master]
Error: /File[/opt/puppetlabs/puppet/cache/facts.d]: Could not evaluate: Could not retrieve file metadata for puppet:///pluginfacts: SSL_connect returned=1 errno=0 state=error: certificate verify failed: [ok for /CN=pe-puppet-master]
Info: Retrieving plugin
Error: /File[/opt/puppetlabs/puppet/cache/lib]: Failed to generate additional resources using 'eval_generate': SSL_connect returned=1 errno=0 state=error: certificate verify failed: [ok for /CN=pe-puppet-master]
Error: /File[/opt/puppetlabs/puppet/cache/lib]: Could not evaluate: Could not retrieve file metadata for puppet:///plugins: SSL_connect returned=1 errno=0 state=error: certificate verify failed: [ok for /CN=pe-puppet-master]
Info: Loading facts
Error: Could not retrieve catalog from remote server: SSL_connect returned=1 errno=0 state=error: certificate verify failed: [ok for /CN=pe-puppet-master]
Warning: Not using cache on failed catalog
Error: Could not retrieve catalog; skipping run
Error: Could not send report: SSL_connect returned=1 errno=0 state=error: certificate verify failed: [ok for /CN=pe-puppet-master]

解决此问题有两种可能的方法:要么设置代理通过已知的 DNS 名称调用主服务器,要么在 Puppetserver 上使用新的 DNS 备用名称重建证书。可以通过在受影响的主服务器上使用find /etc/puppetlabs/puppet/ssl -name <fqdn>.pem -delete删除 SSL 证书,然后在主服务器上运行puppet agent -t --dns-alt-names=<name1>,<name2>,<etc>,连接到主服务器的主节点,并生成一个新的证书。这个证书必须通过命令行在 CA(通常是主节点的主服务器)上签署,不能在 PE 控制台中签署,因为存在 DNS 备用名称的问题。

日期和时间

时间是保持 SSL 连接完整性的重要因素。puppetlabs/ntp通常是 Puppet 最常使用的模块,因为 Puppet 在每个节点的事务中都需要准确的日期和时间。如果你收到消息称证书吊销列表(CRL)在你的运行中尚未生效,请确保 NTP 在你的节点上正确配置:

[root@wordpress puppet]# puppet agent -t
Warning: Unable to fetch my node definition, but the agent run will continue:
Warning: SSL_connect returned=1 errno=0 state=error: certificate verify failed: [CRL is not yet valid for /CN=Puppet Enterprise CA generated on pe-puppet-master at +2018-06-15 02:28:12 +0000]
Info: Retrieving pluginfacts
Error: /File[/opt/puppetlabs/puppet/cache/facts.d]: Failed to generate additional resources using 'eval_generate': SSL_connect returned=1 errno=0 state=error: certificate verify failed: [CRL is not yet valid for /CN=Puppet Enterprise CA generated on pe-puppet-master at +2018-06-15 02:28:12 +0000]
Error: /File[/opt/puppetlabs/puppet/cache/facts.d]: Could not evaluate: Could not retrieve file metadata for puppet:///pluginfacts: SSL_connect returned=1 errno=0 state=error: certificate verify failed: [CRL is not yet valid for /CN=Puppet Enterprise CA generated on pe-puppet-master at +2018-06-15 02:28:12 +0000]
Info: Retrieving plugin
Error: /File[/opt/puppetlabs/puppet/cache/lib]: Failed to generate additional resources using 'eval_generate': SSL_connect returned=1 errno=0 state=error: certificate verify failed: [CRL is not yet valid for /CN=Puppet Enterprise CA generated on pe-puppet-master at +2018-06-15 02:28:12 +0000]
Error: /File[/opt/puppetlabs/puppet/cache/lib]: Could not evaluate: Could not retrieve file metadata for puppet:///plugins: SSL_connect returned=1 errno=0 state=error: certificate verify failed: [CRL is not yet valid for /CN=Puppet Enterprise CA generated on pe-puppet-master at +2018-06-15 02:28:12 +0000]
Info: Loading facts
Error: Could not retrieve catalog from remote server: SSL_connect returned=1 errno=0 state=error: certificate verify failed: [CRL is not yet valid for /CN=Puppet Enterprise CA generated on pe-puppet-master at +2018-06-15 02:28:12 +0000]
Warning: Not using cache on failed catalog
Error: Could not retrieve catalog; skipping run
Error: Could not send report: SSL_connect returned=1 errno=0 state=error: certificate verify failed: [CRL is not yet valid for /CN=Puppet Enterprise CA generated on pe-puppet-master at +2018-06-15 02:28:12 +0000]

PE 控制台服务宕机

如果 Puppet Enterprise 控制台超载,可能会触发OutOfMemory错误并崩溃。我通常在虚拟机或容器中启动小规模的 Puppet Enterprise 安装时看到这个问题,尤其是在我的本地笔记本电脑上。当控制台出现故障时,Puppet Enterprise 用户会收到错误信息,告知节点管理服务未运行。如果这个信息在代理运行中开始出现,用户应该检查 PE 控制台的状态和相关日志:

[root@wordpress ~]# puppet agent -t
Warning: Unable to fetch my node definition, but the agent run will continue:
Warning: Error 500 on SERVER: Server Error: Classification of wordpress failed due to a Node Manager service error. Please check /var/log/puppetlabs/console-services/console-services.log on the node(s) running the Node Manager service for more details.
Info: Retrieving pluginfacts
Info: Retrieving plugin
Info: Retrieving locales
Info: Loading facts
Error: Could not retrieve catalog from remote server: Error 500 on SERVER: Server Error: Failed when searching for node wordpress: Classification of wordpress failed due to a Node Manager service error. Please check /var/log/puppetlabs/console-services/console-services.log on the node(s) running the Node Manager service for more details. 
Warning: Not using cache on failed catalog
Error: Could not retrieve catalog; skipping run

本节仅适用于 Puppet Enterprise 用户。

目录错误

当触发目录编译错误时,Puppet 解析器会提醒我们,它无法从提供的代码中构建目录。Puppet 运行将失败,并且代理不会在无法编译目录的节点上进行配置。当 Puppet 无法读取代码,或者无法确定如何应用目录中提供的资源时,就会触发这些错误。在接下来的部分中,我们将涵盖以下常见的失败:

  • 语法错误

  • 重复资源声明

  • 缺失资源

  • 自动加载格式

  • 循环依赖

企业用户:分类组中的配置选项卡无法读取包含语法错误、缺失类或未按自动加载格式找到的类。

语法错误

语法错误是我们在开发代码时最常见的错误。当输入代码时,很容易忽视简单的语法错误,并将错误代码推送到测试环境中。在以下示例中,文件末尾缺少了类的闭括号:

[root@wordpress puppet]# puppet agent -t
Info: Using configured environment 'production'
Info: Retrieving pluginfacts
Info: Retrieving plugin
Info: Retrieving locales
Info: Loading facts
Error: Could not retrieve catalog from remote server: Error 500 on SERVER:
 Server Error: Syntax error at end of input (file: /etc/puppetlabs/code/environments/production/modules/profile/manifests/baseline.pp) on node wordpress

我们可以在部署到 Puppet Master 之前很久就测试这个错误。命令 puppet parser validate 如果我们在清单上运行它,将给出与代理相同的错误信息。PDK 的用户会发现,pdk validate 会在检查中运行这一项。以下代码演示了 Puppet 解析器验证的代理运行错误:

[root@pe-puppet-master manifests]# puppet parser validate baseline.pp
Error: Could not parse for environment production: Syntax error at end of input (file: /etc/puppetlabs/code/environments/production/modules/profile/manifests/baseline.pp)

这是将良好实践应用到 CI/CD 流水线中的最简单示例之一。你可以在第八章中找到更多有关添加此简单检查的好示例,通过任务和发现扩展 Puppet

像 Puppet 解析器验证这样的语法错误检查器会扫描代码,直到它们找到无法解析的行。通常,这些错误出现在报告的错误行上方!请始终检查报告行上方的代码。以下错误实际上是 example.pp 文件第 4 行缺少逗号:Error: Could not parse for environment production: Syntax error at 'source' (file: /Users/rary/workspace/packt/manifests/example.pp, line: 5, column: 5)

重复资源声明

Puppet 根据我们在清单中声明的每个资源构建目录。在良好的 Puppet 代码设计中,我们有包含或包含其他类的类。在开发过程中,有时我们会尝试声明一个已经在系统中应用的类中声明的资源,这是很常见的。按照设计,Puppet 会在遇到重复的资源声明时失败,原因很简单:目录如何决定应用哪个资源是正确的呢?在以下示例中,一个资源在两个不同的类中声明,并且这些类都应用到我的节点上:

[root@pe-puppet-master production]# puppet agent -t
Info: Using configured environment 'production'
Info: Retrieving pluginfacts
Info: Retrieving plugin
Info: Retrieving locales
Info: Loading facts
Error: Could not retrieve catalog from remote server: Error 500 on SERVER: Server Error: Evaluation Error: Error while evaluating a Resource Statement, Duplicate declaration: File[/var/log/custom] is already declared at (file: /etc/puppetlabs/code/environments/production/modules/profile/manifests/baseline.pp, line: 6); cannot redeclare (file: /etc/puppetlabs/code/environments/production/modules/profile/manifests/logging.pp, line: 3) (file: /etc/puppetlabs/code/environments/production/modules/profile/manifests/logging.pp, line: 3, column: 3) on node pe-puppet-master
Warning: Not using cache on failed catalog
Error: Could not retrieve catalog; skipping run

在前面的案例中,我在我的基准配置文件中设置了日志目录。我迭代并围绕日志设计了整个配置文件,并将我的目录包含在日志配置文件中。为了解决这个错误,我只需从基准配置文件中移除自定义的日志目录资源。

如果你需要声明一个资源,并可能在多个清单中使用它,你可能想使用虚拟资源。第九章,导出资源也涵盖了虚拟资源的内容。

缺失资源

当我们尝试使用 Puppet Master 或 Puppet 环境中不可用的资源时,可能会触发缺失资源错误,导致目录编译失败。虽然这些问题通常是由于拼写错误的资源类型引起的,但它们也可能是由于环境中缺失模块造成的。在以下示例中,我尝试使用 include ntp 引入 NTP 模块。记住,类也是资源:

[root@wordpress puppet]# puppet agent -t
Info: Using configured environment 'production'
Info: Retrieving pluginfacts
Info: Retrieving plugin
Info: Retrieving locales
Info: Loading facts
Error: Could not retrieve catalog from remote server: Error 500 on SERVER: Server Error: Evaluation Error: Error while evaluating a Function Call, Could not find class ::ntp for wordpress (file: /etc/puppetlabs/code/environments/production/modules/profile/manifests/baseline.pp, line: 3, column: 3) on node wordpress 
Warning: Not using cache on failed catalog
Error: Could not retrieve catalog; skipping run

我在环境中只是缺少了 NTP 类。我可以通过手动执行 puppet module install 来解决这个问题,但如果你使用的是 r10k 或 Code Manager,只需在环境的 Puppetfile 中添加模块条目及其所有依赖项:

mod 'puppetlabs/ntp'
mod 'puppetlabs/stdlib'

使用 Puppet 模块 install 方法确实能使模块对所有环境可用,但我只建议在用于测试代码的临时 Puppet Master 上使用它:

[root@pe-puppet-master manifests]# puppet module install puppetlabs/ntp
Notice: Preparing to install into /etc/puppetlabs/code/environments/production/modules ...
Notice: Downloading from https://forgeapi.puppet.com ...
Notice: Installing -- do not interrupt ...
/etc/puppetlabs/code/environments/production/modules
└─┬ puppetlabs-ntp (v7.2.0)
 └── puppetlabs-stdlib (v4.25.1)

Puppet 模块 install 默认会为我们获取所有的依赖项,而 R10k 和 Code Manager 不会,所以确保在 Puppetfile 中包含所有依赖项。

自动加载格式

如果我们包含类和定义类型的清单文件不在正确的目录中,主服务器将无法找到它们。在以下示例中,我尝试使用一个新的类:

[root@wordpress puppet]# puppet agent -t
Info: Using configured environment 'production'
Info: Retrieving pluginfacts
Info: Retrieving plugin
Info: Retrieving locales
Notice: /File[/opt/puppetlabs/puppet/cache/locales/ja/puppetlabs-ntp.po]/ensure: defined content as '{md5}7265ff57e178feb7a65835f7cf271e2c'
Info: Loading facts
Error: Could not retrieve catalog from remote server: Error 500 on SERVER: Server Error: Evaluation Error: Error while evaluating a Function Call, Could not find class ::profile::baseline::linux for wordpress
(file:/etc/puppetlabs/code/environments/production/modules/profile/manifests/baseline.pp, line: 4, column: 3) on node wordpress

我知道我已经编写了 linux.pp 清单文件,但主服务器找不到它。如果我在目录中运行 tree 命令,我会看到 profile::baseline::linux 实际上位于 profile::linux 的自动加载目录中。记住,目录为我们的命名空间提供了额外的层次:

profile/
└── manifests
    ├── baseline.pp # profile::baseline
    └── linux.pp # profile::baseline::linux <-- Can't find this

通过简单地将我的 Linux 基准清单移到 baseline 文件夹中,主服务器将能够找到这个清单:

profile/
└── manifests
    ├── baseline
    │   └── linux.pp # profile::baseline::linux <-- Found!
    └── baseline.pp # profile::baseline

循环依赖

循环依赖在 Puppet 开发中并不常见,但一旦出现,排查起来可能非常棘手。循环依赖发生在我们创建了一个依赖链(使用箭头指示符 ->)或排序元参数时。在以下示例中,我的三个通知语句相互依赖,形成了一个循环链 —— a -> b -> c -> a

class profile::baseline::linux {

# notify {'baseline': message => 'Applying the Linux Baseline!' }

  notify {'a':
    message => 'Resource A',
    require => Notify['b']
  }

  notify {'b':
    message => 'Resource B',
    require => Notify['c']
  }

  notify {'c':
    message => 'Resource C',
    require => Notify['a']
  }

}

当这个目录应用到节点时,我们将收到一条信息,告诉我们哪些资源在依赖链中:

[root@wordpress puppet]# puppet agent -t
Info: Using configured environment 'production'
Info: Retrieving pluginfacts
Info: Retrieving plugin
Info: Retrieving locales
Info: Loading facts
Info: Caching catalog for wordpress
Info: Applying configuration version '1535603400'
Error: Found 1 dependency cycle:
(Notify[a] => Notify[c] => Notify[b] => Notify[a])\nTry the '--graph' option and opening the resulting '.dot' file in OmniGraffle or GraphViz
Error: Failed to apply catalog: One or more resource dependency cycles detected in graph

注意代理中指示的 --graph 标志。如果我们再次运行代理,使用 puppet agent -t --graph,我们将得到一个 dot 文件,其中详细列出了我们的排序,并且能够突出显示我们的依赖循环。这个文件会写入 /opt/puppetlabs/puppet/cache/stage/graphs/cycles.dot。我可以在 GraphViz(开源软件)或 OmniGraffle 中打开这个文件,并以图形形式查看我的依赖链。下图显示了 OmniGraffle 中表示的这个通知循环:

调试模式 – 目录

有时,Puppet 会抛出一个并不立刻明显的错误。在下一个示例中,我尝试安装 apache httpd,但我拼写错了包名。如果你没有花很多时间在使用 Yum 的系统上工作,错误 Nothing to do 其实并不是一个非常清晰的错误:

[root@pe-puppet-master manifests]# puppet agent -t
Info: Using configured environment 'production'
Info: Retrieving pluginfacts
Info: Retrieving plugin
Info: Retrieving locales
Info: Loading facts
Info: Caching catalog for pe-puppet-master
Info: Applying configuration version '1535778801'
Notice: Applying the Linux Baseline!
Notice: /Stage[main]/Profile::Baseline::Linux/Notify[baseline]/message: defined 'message' as 'Applying the Linux Baseline!'
Error: Execution of '/usr/bin/yum -d 0 -e 0 -y install http' returned 1: Error: Nothing to do
Error: /Stage[main]/Profile::Baseline/Package[http]/ensure: change from 'purged' to 'present' failed: Execution of '/usr/bin/yum -d 0 -e 0 -y install http' returned 1: Error: Nothing to do
https://yum.puppet.com/puppet5/puppet5-release-el-7.noarch.rpm' returned 1: Error: Nothing to do
Info: Stage[main]: Unscheduling all events on Stage[main]

我可能想检查一下 Puppet 尝试让我系统做什么。我可以在代理上使用 --debug 标志,检查 Puppet 在系统底层执行的所有操作。我可以看到 Puppet 使用 rpm -q 检查软件包是否已安装在系统上。如果没有找到,它会执行特定的 Yum 命令:以不记录错误日志(-e 0)或调试(-d 0)的方式运行 Yum,并假设默认选择(-y)安装 http。最后,由于该资源失败,任何依赖它的资源都会失败安装:

Debug: Executing: '/usr/bin/rpm -q http --nosignature --nodigest --qf '%{NAME} %|EPOCH?{%{EPOCH}}:{0}| %{VERSION} %{RELEASE} %{ARCH}\n''
Debug: Executing: '/usr/bin/rpm -q http --nosignature --nodigest --qf '%{NAME} %|EPOCH?{%{EPOCH}}:{0}| %{VERSION} %{RELEASE} %{ARCH}\n' --whatprovides'
Debug: Packagehttp: Ensuring => present
Debug: Executing: '/usr/bin/yum -d 0 -e 0 -y install http'
Error: Execution of '/usr/bin/yum -d 0 -e 0 -y install http' returned 1: Error: Nothing to do
Error: /Stage[main]/Profile::Baseline/Package[http]/ensure: change from'purged' to 'present' failed: Execution of '/usr/bin/yum -d 0 -e 0 -y
 install http' returned 1: Error: Nothing to do
Debug: Class[Profile::Baseline]: Resource is being skipped, unscheduling all events

错误 Nothing to do 实际上没有解决。通过快速搜索你最喜欢的论坛可以找到一些可能的罪魁祸首,在这种情况下,http 不是 Yum 中的一个包。httpd,即 Apache Web 服务器,才是我想要安装的。

日志记录

日志记录是最有用的故障排除形式之一,前提是要主动监控。我们通常可以在问题成为用户报告的故障之前,识别出我们基础设施中的问题。通过了解 Puppet 可用的日志记录,你将知道在哪里查看系统退化的指示。在本节中,我们将探讨 Puppet 及其子组件可用的日志文件,并配置 Puppetserver 中的日志级别。

logback.xml 文件

除了 Puppet agent 之外,我们将记录的每个组件都将使用 Logback。虽然这不是一本关于 logback 的书,但我们会查看一些现有的 logback.xml 部分,以及我们可以修改的一些常见设置。

主要配置

主要配置包括以下 XML 文件的第一行和最后一行:

<configuration scan="true" scanPeriod="60 seconds">

scan 设置告诉 logback 重新扫描配置文件是否有更改,并在检测到更改时重新加载服务。scanPeriod 设置让配置知道多长时间扫描一次。我们使用这些设置,以便我们的日志配置能够随着文件动态更新;无需重启服务。

Appender

appender 配置部分管理日志文件。我已在 puppetserver.log 的 appender 中添加了注释,说明各行的作用:

<!-- Setting the name for future reference and making a Rolling Log File -->
    <appender name="F1" class="ch.qos.logback.core.rolling.RollingFileAppender">

<!-- Logging to /var/log/puppetlabs/puppetserver/puppetserver.log -->
        <file>/var/log/puppetlabs/puppetserver/puppetserver.log</file>

<!-- Appending to, not replacing the log -->
        <append>true</append>

<!-- Roll the file over based on Size and Time -->
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">

<!-- What to name the file as it's rolled over, with date variables -->
            <fileNamePattern>/var/log/puppetlabs/puppetserver/puppetserver-%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>

<!-- Maximum size of log file before rolling over -->
            <maxFileSize>200MB</maxFileSize>

<!-- Maximum Number of Files to keep - 90 logs -->
            <maxHistory>90</maxHistory>

<!-- Maximum Filesize of all files that will be kept. Up to 5 files with 200 MB -->
            <totalSizeCap>1GB</totalSizeCap>
        </rollingPolicy>
<!-- What to print for date and time with the message -->
        <encoder>
            <pattern>%d{yyyy-MM-dd'T'HH:mm:ss.SSSXXX} %-5p [%t] [%c{2}] %m%n</pattern>
        </encoder>
    </appender>

在前面的示例中,我们正在创建具有滚动策略的 puppetserver.log。我们将保留最多 90 个日志,但每当日志达到 200 MB 大小时就进行轮换,如果日志总大小超过 1 GB,则删除日志。我们将日期附加到我们滚动的日志中,并打印日志的时间戳。

你可能会看到一个指向 STDOUT 的 appender。这实际上是输出到 System.outSystem.error,本质上是附加到终端。

日志记录器

logback.xml 中的日志记录器充当应用程序生成的日志的指针:

    <logger name="puppetlabs.pcp" level="info" additivity="false">
      <appender-ref ref="PCP"/>
    </logger>

这个示例连接到 Puppetserver 应用程序中的puppetlabs.pcp日志,并收集信息级别的日志。additivity=false标志告诉日志替换文件,而不是附加到文件末尾。最后,appender-ref标签告诉日志记录器使用哪个附加器来进行日志记录配置。

根日志记录器

还有一种特殊类型的日志记录器,称为根日志记录器:

    <root level="info">
        <appender-ref ref="${logappender:-DUMMY}" />
        <appender-ref ref="F1" />
    </root>

根日志记录器充当默认值,允许你选择日志级别并提供 appender-refs 列表,以应用默认设置。可以把它看作是一个默认的组策略日志记录器,而不是应用于单个日志配置的设置。所有其他日志记录器会覆盖根日志记录器中的每个值。

Puppet 代理

Puppet 代理在每个节点上,并且 Puppet 代理的日志存储在该节点的本地。 这是我们使用的唯一不使用 logback 的日志文件,而是使用系统消息进行日志记录。Puppet 代理记录到它所运行的操作系统的syslog。每个操作系统使用不同的位置,如下所示:

  • Linux: /var/log/messages

  • macOS X: /var/log/system.log

  • Solaris: /var/adm/messages

  • Windows: 事件查看器

这里记录的信息与 Puppet 运行期间输出的信息相同。你可以在此日志文件中检查成功和失败的资源应用到节点的情况。

企业用户:你还可以在 Puppet Enterprise 控制台中查看代理日志,并可以使用过滤器来帮助缩小问题或状态的范围。你可以在每个节点页面的报告部分找到此日志。

PuppetDB

PuppetDB 的日志记录由位于 PuppetDB 服务器的配置文件/etc/puppetlabs/puppetdb/logback.xml管理。这个 logback 文件包含以下日志的条目,这些日志默认位于 /var/log/puppetlabs/puppetdb/

  • puppetdb.log:有关 PuppetDB 应用程序的信息

  • puppetdb-access.log:有关用户和机器访问 PuppetDB 的信息

  • puppetdb-status.log:PuppetDB 的当前状态

如果你在寻找 PostgreSQL 的日志,它们存储在/var/log/puppetlabs/postgresql中。这是标准的 PostgreSQL 日志记录。

Puppetserver

Puppetserver 的日志记录由位于 PuppetDB 服务器的配置文件/etc/puppetlabs/puppetserver/logback.xml管理。这个 logback 文件包含以下日志的条目,这些日志默认位于 /var/log/puppetlabs/puppetserver

  • puppetserver.log:包含编译错误的应用程序活动日志

  • pcp-broker.log:Puppet 上 PCP 经纪人活动的日志文件

  • pcp-broker-access.log:用户访问 Puppet 上 PCP 经纪人的日志文件

  • puppetserver-status.log:Puppetserver 状态指示器

Puppet Enterprise 控制台

控制台日志记录由位于 PuppetDB 服务器的配置文件/etc/puppetlabs/console-services/logback.xml管理。这个 logback 文件包含以下日志的条目,这些日志默认位于 /var/log/puppetlabs/console-services

  • console-services.log:Puppet Enterprise 控制台的日志

  • console-services-status.log:控制台的状态指示器

本节内容仅对 Puppet Enterprise 用户有用。

摘要

在本章中,我们讨论了故障排除 Puppet 的方法。我们回顾了 Puppetserver 和 Puppet 代理之间连接时常见的错误,分析了常见的清单编译失败以及如何调试它们。我们还介绍了 logback 以及主机上的日志文件。

posted @ 2025-07-08 12:24  绝不原创的飞龙  阅读(33)  评论(0)    收藏  举报