DebOps 工程师的 Puppet8 指南(三)

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

译者:飞龙

协议:CC BY-NC-SA 4.0

第九章:使用 Puppet 处理数据

本章将重点介绍如何使用 Puppet 处理数据。我们将讨论 Hiera,Puppet 的键值数据查找工具,以及它如何确保 Puppet 的可重用代码在不增加过多逻辑和变量的情况下更加可配置。将回顾 Hiera 的基本结构,展示它如何以层级方式存储数据,提供基于规则的键查找,且无需繁琐操作,以及如何使用不同的后端查找数据中的键以返回值,这些后端实现可能是 YAML 数据文件或应用程序的 API 调用。将讨论自动参数查找的使用,展示它如何让参数化配置文件自动接收数据,以及如何在 Puppet 代码中直接使用查找功能来调用数据。我们将简要讨论 Hiera 3 和 Hiera 5 在传统 Puppet 中的变化。接下来,将详细回顾三个 Hiera 层级(全局层、环境层和模块层),讨论在这些不同层级中如何管理层级和数据。将展示查找合并和优先级行为的选项,突出如何通过第一次匹配或合并不同的值来查找数据。然后,我们将根据使用案例和最佳实践讨论数据应该在何时何地使用,以及代码应该直接保存在控制仓库中还是保存在单独的 Hiera 数据仓库中。接着,我们将讨论数据的安全性,展示如何通过不同的方法在存储、传输和在 Puppet 代码中使用时保持数据安全,重点介绍使用 Sensitive 类型、node_encrypt 模块以及通过 eyaml 加密文件的效果和局限性。最后,将回顾一些常见问题及故障排除方法/工具,展示如何最佳地使用 lookup 命令调试和解释值,以及为什么我们永远不应该在层级中使用全局变量,如何避免使用默认值,使用 Hiera 进行分类的风险,以及如何通过 Hiera 数据管理器 (HDM) 工具使数据更易于访问。

本章将涵盖以下主要主题:

  • 什么是 Hiera?

  • Hiera 层级

  • 决定何时使用静态代码或动态数据

  • 保持数据安全

  • 陷阱、难点和问题

技术要求

github.com/puppetlabs/control-repo 克隆控制仓库到你的 GitHub 账户,并将其命名为 controlrepo-chapter9,然后在生产分支中更新以下内容:

)

)

)

+   `hiera.yaml` 与 [`github.com/PacktPublishing/Puppet-8-for-DevOps-Engineers/blob/main/ch09/hiera.yaml`](https://github.com/PacktPublishing/Puppet-8-for-DevOps-Engineers/blob/main/ch09/hiera.yaml)

通过从github.com/PacktPublishing/Puppet-8-for-DevOps-Engineers/blob/main/ch09/params.json 下载 params.json 文件,并更新控制仓库位置和控制仓库中的 SSH 密钥,来构建一个包含两个 Linux 客户端和两个 Windows 客户端的标准集群。然后,在 pecdm 目录下运行以下命令:

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

首先,让我们了解一下 Hiera 是什么以及它为何被使用。

什么是 Hiera?

到目前为止,我们讨论了如何使用 Puppet 创建有状态且可重用的代码,以及如何通过使用角色和配置文件方法使得参数可用,从而使模块可配置。我们还展示了如何在代码中使用这些参数,但为了创建一个可扩展、可读且特定于站点的数据源,Puppet 使用了一个名为 Hiera 的工具。如果不在 Puppet 代码中使用 Hiera 数据,将需要无休止的逻辑和变量来表示节点例外、位置差异、操作系统版本差异、组织差异以及许多其他情况所需的数据变化。

Hiera 是一个数据查找工具,可以在 JSON、HOCON、YAML 和 EYAML 文件中查找值,支持内置后端,或使用自定义后端调用外部数据源,例如网站或数据库。它以键值对的形式存储数据,可以通过代码中的函数调用显式查找,也可以通过自动参数查找自动查找,后者通过将类中的参数名称与 Hiera 数据值进行匹配来实现。正如这个名字所暗示的那样,Hiera 专注于使用层级来查找数据,查找过程遵循一个常见的默认值,数据源的层级越具体,匹配的节点数据越精确。这些层级在 hiera.yaml 文件中配置;该 YAML 文件按优先级列出各个层级。此 hiera.yaml 文件设置了要使用的 Hiera 版本,这是必需的,虽然 5 是唯一的活跃版本。

使用内置后端

对于层级映射中的内置后端,将会有一个层级列表,每个层级将包含以下内容:

  • name – 描述层级的可读标签

  • datadir – 相对于 hiera.yaml 的基础路径,所有数据都存储在此路径下

  • data_hash – 要使用的 Hiera 后端/文件类型

  • pathpathsglobglobsmapped_paths——文件路径或相对于datadir的数据路径。

还可以使用这些键创建默认映射,这样就不需要在每个层次结构中重复值。

data_hash查找函数键接受yaml_datajson_datahocon_data作为值,但大多数 Puppet 实现仅使用 YAML 数据,因此本书将默认使用yaml_data后端。

文件路径允许层次结构级别使用与节点相关的代码中插值的变量,声明数据文件的特定位置,例如与%{<variable_name}相关的全局变量,并通过点(.)访问facts数组来调用事实。因此,%{facts.application_owner}将访问application_owner事实。进一步的点可以用来访问结构化事实,例如%{facts.os.family}可以访问os事实中的family值。类似地,受信任的事实可以从trusted数组中访问,例如%{trusted.certname},并且可以使用%{trusted.external.pds.data}访问受信任的外部事实。

因此,可以在hiera.yaml文件中使用以下代码创建一个简单的层次结构:

---
version: 5
defaults:
  datadir: data
  data_hash: yaml_data
hierarchy:
  - name: "Node data"
    path: "nodes/%{trusted.certname}.yaml"
  - name: "Location"
    path: "location/%{fact.data_center}.yaml"
  - name: "Common data"
    path: "common.yaml"

这个层次结构意味着,具有certname可信事实为examplehostdata_center事实为enterprisedc1的主机,首先会在data/nodes/examplehost.yaml中查找,然后在data/location/enterprisedc1.yaml中查找,最后在/data/common.yaml公共文件中查找。

也可以将多个变量插值组合在路径中,例如更新位置层以根据另一个事实进行区分——例如,假设存在brand事实,并且组织中的不同品牌将对数据中心有所不同,那么路径可以写成path: "location/%{facts.brand}-%{fact.data_center}.yaml"

所以,如果examplehostbrand事实设置为retail,它将会在data/location/retail-enterprisedc1.yaml中查找。

在这些查找中,如果在当前层次找不到匹配的文件,它将返回空值并进入下一层。改用paths路径文件变量可以简化这一过程。由于层次结构之间唯一的实际差异是路径,因此可以通过一个单一的层次结构声明和带有paths数组的路径来简化。例如,前一个示例中的层次结构可以简化为一个层次结构,使用paths

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

如果需要为不同的后端添加额外的 Hiera 层次结构,那么必须理解,任何层次结构都会按照顺序检查所有路径,然后才会进入下一个层次,这可能会防止简化并保持正确的层次顺序。

在本节中,我们将讨论 glob,因为它们在代码库中可能会出现,但它们不应被使用,因为它们会使数据结构比任何环境实际需要的更复杂。

文件路径可以使用globglobs来传递 Ruby 风格的Dir.glob方法。此方法的完整文档可以在www.puppet.com/docs/puppet/latest/hiera_config_yaml_5.html#specifying_file_paths查看。这允许使用以下功能:

  • 星号(*)作为通配符

  • 两个星号(**)用于递归匹配目录

  • 问号(?)用于匹配任意一个字符

  • 用逗号分隔的列表({this,that,or,not})用于与列表中的任何选项进行字面匹配

  • 方括号内的字符集([xyz])用于匹配给定集合中的任意一个字符

  • 反斜杠(\)用于转义特殊字符

例如,取facts.os.windows事实,然后从display_id(在 Windows 2019 的后续版本中引入)或release_id(在 Windows 2016 中引入并在 Windows 2019 中弃用)中进行匹配。这个组合允许为一个反复变化的来源创建一个一致的 Hiera 层,并且需要组合事实来查找不同的版本:

- name: "Windows Release"
  glob: "windows_release/{%{facts.windows.display_id},%{facts.windows.release_id}}"

要创建一个包含主接口或网络域的网络信息层,可以创建以下代码,它将搜索网络文件夹中的任何目录结构以进行匹配:

- name: "Domain or Network"
    glob: "network/**/{%{facts.networking.domain},%{facts.networking.interfaces.ethernet.bindings.0.network}}.yaml"

如果找到多个匹配项,文件将按字母数字顺序进行搜索。此外,多个字符串可以使用globs:进行搜索,并以类似路径的方式传递字符串数组。

最终的文件路径选项是mapped_paths。此选项通过提供一个包含字符串集合的变量、一个变量名(该变量映射字符串集合中的每个元素)和一个模板来工作。例如,如果一个名为$oracle_sids的事实包含['ora1','ora2','ora3']的数组,则以下层次结构将在/oracle_dbs/ora1.yaml/oracle_dbs/ora2.yaml/oracle_dbs/ora3.yaml文件中执行查找:

- name: "Oracle sids"
    mapped_paths: [oracle_sid, sid, "oracle_dbs/%{sid}.yaml"]

虽然我们已经花了一些时间来讲解通配符(globs),但需要重申的是,这应该仅用于理解代码中预先存在的复杂数据结构,并帮助你进行重构和简化。这不应在新的 代码库 中使用。

在详细讨论了层次结构之后,现在是时候转向使用的数据以及如何调用该层次结构的查找了。正如在使用内置后端部分中提到的,YAML 是最常用的内置数据类型,并将在所有示例中使用,但差异仅体现在语言的表示方式,而非实际使用的结构。

在 YAML 数据文件中,我们创建键值对和带有值列表的键。键可以是单一值,但更常见的是使用格式<module_name>::<paramater_name>来构造,其中module_name可以包含多个段,反映模块内的某个类命名空间。

举个例子,对于exampleapp配置文件模块,一个数据文件可能包含将enable_service参数设置为true的设置,它可能包含[opt1,opt2,opt3]的选项数组,对于user的参数,它可能包含一个每个用户设置的哈希,用于创建exampleuseranotheruser。这将如下所示:

---
profile::exampleapp::enable_service: true
profile::exampleapp::options:
  - opt1
  - opt2
  - opt3
profile::exampleapp::users:
  exampleuser:
    uid: 101
    home: /app/exampleapp
    gig: 102
  anotheruser:
    uid: 201

访问数据

下一个要点是如何在 Puppet 代码中访问这个层级和数据,正如本章开头所提到的,Puppet 有两种方式在代码中查找数据:通过自动类参数查找或通过 Puppet 查找函数。推荐的模型是通过自动参数查找将几乎所有需要的数据传递给配置文件类,使用角色和配置文件模型(在第八章中讨论)和 Forge。

自动类参数查找通过获取任何已被包含/声明为资源的类的参数来工作,首先检查参数是否已通过声明设置,如果没有,则对每个<module_name>::<parameter_name>形式的参数执行 Hiera 查找。需要注意的是,这本身不是 Hiera 中的命名空间键;它只是一个字符串名称,值不能插入到数据结构中。在使用配置文件并具有已设置的配置文件模块和 Oracle 配置文件的情况下,这可能看起来像是profile::oracle::version。为了设置此数据,我们可能会在data/nodes/server1.example.com.yaml文件中为server1.example.com节点设置特定的版本,如以下行所示,将profile::oracle的版本参数设置为 Oracle 21c:

profile::oracle::version: 21c

如果此查找失败,它将查看类清单中是否为参数设置了任何默认值,然后将其赋值为undef

默认情况下,通过 Hiera 找到的数据将以字符串或字符串数组的形式返回;稍后我们将展示如何将其转换。

注意

自动类参数查找不适用于定义的资源类型,仅适用于类。为了模仿这一功能,您可以在代码中使用显式的lookup()调用。

Puppet 代码中的另一个机制是lookup函数。它更直接,可以在 Puppet 代码中使用;它通过一个键调用,这个键可以是多个段,每个段由两个冒号(::)分隔,或者它可以是简单的全局值。这里使用冒号只是约定,并不深入到数据结构中。为了查找相同的 Oracle 参数,以下示例将其赋值给oracle_version变量:

$``oracle_version=lookup(profile::oracle::version)

如果数据是一个数组,可以使用点表示法访问特定的键:

$``exampleuser_id=lookup(profile::exampleapp::users.exampleuser.id)

如果查找返回的值为空,可以通过函数中的参数或选项哈希提供默认值(完整选项可以在文档中查看:www.puppet.com/docs/puppet/latest/hiera_automatic.html#puppet_lookup-arguments),并提供一个返回的值——例如,如果上一个示例的查找未返回任何值,可以使用以下内容来返回 no id found 字符串:

exampleuser_id=lookup(profile::exampleapp::users.exampleuser.id, {default_value => 'no id found'})

这将在 Pitfalls, gotchas, and issues 部分中详细讨论,但提供默认值被认为是一个不好的实践,因为它隐藏了失败,并可能让人误以为值已经找到且一切正常运行。还可以注意到第二个和第三个参数被标记为 undef;这些是数据类型和合并策略,将在接下来的部分讨论。

注意

lookup 函数替代了 Hiera 3 中的传统 hiera_<data_type>hiera 函数。由于这些函数已被弃用,不应使用它们,因为它们可能产生不一致的结果。

到目前为止讨论的内容是最简单的情况,我们只期望查找一个值并找到第一个匹配项。这是 Hiera 的默认行为,允许你根据不同的具体程度重写值。不过,有时候,你可能希望返回所有层级中所有值的某种组合。可以在数据文件中设置查找选项,来描述应该如何进行这种操作。

lookup_options 保留键允许为查找操作设置不同的合并行为,查找操作可以是针对特定键或遵循以下格式的正则表达式:

lookup_options:
  <key name or regular expression>:
  merge: <MERGE OPTION>

最常见的方法是将这种行为放在common.yaml文件中,但如果例如,节点重写或某些优先级重写可能更重要,那么将其放到层次结构的不同层级中也是有意义的。

可以在数据文件中设置四种合并行为:

  • first – 返回层次结构顺序中的第一个匹配项

  • unique – 返回层次结构中所有匹配的唯一值的数组

  • hash – 返回一个哈希值,浅度合并哈希键,使用最高层次的键匹配

  • deep – 返回一个哈希值,深度合并哈希键,使用最高层次的键匹配

Hiera 的默认行为 first 会按照层次结构顺序查找第一个匹配的值。假设没有为该键声明其他的 lookup_option 值,那么就不需要隐式声明它。但是,如果例如,common.yaml 被设置为 unique,而对于我们的节点异常,我们只希望设置在 profile:oracle::limits 中声明的值,我们可以在节点的 YAML 数据文件中设置以下内容:

lookup_options:
  profile::oracle::limits:
    merge: first

unique关键字将查找所有匹配的键,并返回合并后的扁平化数组。因此,例如,如果我们想在一个配置文件中安装所有请求的 Oracle 版本,我们可以设置如下:

lookup_options:
  profile::oracle::versions
    merge: unique

如果在节点级别找到了11值,在组织级别找到了12,并且在公共层级找到了11,13,那么返回的值将是一个数组[11,12,13]

hash关键字将通过合并哈希的顶层键来合并所有匹配级别的哈希。这本质上执行的是浅层哈希合并,意味着顶层键会被合并,但合并不会递归地下降并合并嵌套在其下的数据结构。这将保持键的书写顺序,从最低优先级的数据源中匹配,但会从最高优先级的源中获取值。可以将其理解为在从最高到最低级别的过程中,将键逐步添加到哈希中。它会覆盖并附加值,但不会递归地合并键中的值。例如,假设在profile::oracle::limits上执行查找,在最低级别,common.yaml存在并包含以下内容:

lookup_options:
  profile::oracle::limits
    merge: hash
profile::oracle::limits:
  '*/nofile':
    soft: 2048
    hard: 8192
  'oracle/nofile':
    soft: 65536
    hard: 65536
  'oracle/nproc':
    soft: 2048
    hard: 16384
  'oracle/stack':
    soft: 10240
    hard: 32768

然后假设/node/examplenode.server.com.yaml由于以下hiera.yaml部分而具有更高优先级:

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

/node/examplenode.server.com.yaml包含以下内容:

profile::oracle::limits:
  'oracle/nproc':
    soft: 4096
    hard: 16384
  'oracle/memlock':
    soft:  3145728
    hard:  4194304
  'oracle/stack':
    hard: 65536

profile::oracle::limits的哈希查找将返回以下内容:

profile::oracle::limits:
  '*/nofile':
    soft: 2048
    hard: 8192
  'oracle/nofile':
    soft: 65536
    hard: 65536
  'oracle/nproc':
    soft: 4096
    hard: 16384
  'oracle/stack':
    hard: 65536
  'oracle/memlock':
    soft:  3145728
    hard:  4194304

请注意,在这种情况下,profile::oracle::limits.oracle/stack键是从最高优先级获取的,因此只看到了硬值,没有执行递归合并。使用带点(.)的简化语法可以访问哈希或数组中的元素,在数组的情况下,会使用索引号。

deep合并结合了任意数量的哈希或数组,并且能够递归地合并哈希或数组中的值。这意味着hash值与另一个deep合并一起合并,且数组不会被扁平化,可以包含嵌套的数组。如果之前的查找选项被配置为deep_merge,则该查找将返回oracle/stack键的硬性和软性限制。

注意

在哈希中合并超过三个嵌套层级会对 Hiera 的性能产生严重影响,因此应避免这种做法。

还有一些选项可以分配以影响数组的合并。例如,sort_merged_arrays将导致合并后的数组按键排序,而不是默认行为,即数组按从最低优先级到最高优先级的顺序排序,merge_hash_arrays则表示如果设置为true,数组中的哈希将进行深度合并。另一个选项允许deep合并具有knockout_prefix键,其中包含一个值的键,通常以双破折号(--)表示,作为值前缀使用,将导致移除而不是添加该值。

例如,如果在第八章中给出的灵活类模型得到了实现,使用 deep 合并和 knockout 前缀将允许在每个层级添加或移除类:

lookup_options:
 profile::base::extra_classes:
   merge:
     strategy: deep
     knockout_prefix: --
     sort_merged_arrays: true

一些示例数据可能是 node/example.server.com.yaml,其中,层级的最高级别 node 包含以下代码:

profile::base::extra_classes:
  - pci::dss
  - email

相比之下,datacenter/europe.dc.1.yaml 这个较低层级包含了以下内容:

profile::base::extra_classes:
  - email
  - gdpr

这将导致 profile::base::extra_classes 查找包含 gdprpci::dss,按此顺序排列,但不包含 email

到目前为止,示例使用了在 common.yaml 中设置 lookup_options 的最常见位置。但 lookup_options 执行哈希合并,这将获取每个键找到的最高顺序。所以举个例子,假设 /data/common.yaml,这个最低层级,包含以下代码:

lookup_options:
  profile::base::extra_classes:
    merge:
      strategy: deep
      knockout_prefix: --
      sort_merged_arrays: true

/data/example.server.com.yaml,在更高的层级,包含了以下内容:

lookup_options:
  profile::base::extra_classes:
    merge: first:

然后,在 /data/example.server.com.yaml 中匹配 profile::base::extra_classes 键的查找将使用第一个匹配查找,而不是 deep 合并。

另一种查找选项是使用正则表达式和 convert_to 选项,将值转换为其他类型,而非字符串。一个特别有用的例子是,当使用我们希望保持敏感的值时,我们可以简单地在层级的公共级别添加一个正则表达式字符串,这将匹配所有以 profile 开头,且最终键名以 password 结尾的键,并确保该参数被转换为 Sensitive

---
lookup_options:
  '^profile::.+::\w+_password$':

数据安全保持部分中,将会有更多关于保护数据的讨论。

虽然基本上可以在 lookup 函数中覆盖数据文件中设置的查找设置,但我们强烈建议避免这样做,因为数据中可能会说明一种情况,而 lookup 函数却表现不同。这可能导致数据的变化对 lookup 函数产生意外的后果。如果确实需要,语法可以在文档中找到:www.puppet.com/docs/puppet/8/hiera_automatic.html#puppet_lookup

插值也可以通过变量和函数在 Hiera 数据中使用。虽然这可以避免数据的重复,但它也可能使数据变得比我们希望的更加复杂,因此一般建议避免这样做。

与使用事实的层级相同,trustedserver_facts 可以提供一致的变量,且这些变量以相同的方式进行插值,因此,一个简单的例子是设置一个使用主机名的 config 文件,如下所示:

tivoli_config_file: '/opt/app/tivoli/client/%{trusted.hostname}.conf'

Hiera 提供了有限数量的特殊插值函数。它们不同于 Puppet 函数。以下函数可用于插入 Hiera 数据:

  • lookup(或 hiera

  • alias

  • literal

  • scope

使用与变量相同的格式,可以声明一个函数,如 ${<function>(<arguments>)}

lookup 函数允许从数据中查找 Hiera 值。这可以有效地防止数据重复输入并减少维护工作,因为如果数据发生更改,只需在一个地方进行更改。例如,类似于仓库服务器这样的内容可能根据客户端的位置有所不同,或者可能反复使用以提供包的完整位置。以下示例展示了如何使用查找功能来提供两个二进制文件的完整路径,从而减少重复:

profile::base::artifactoryserver: artifactory.example.com
profile::exampleapp1::binary:  %{lookup (profile::base::artifactoryserver)}/exampleapp1.rpm
profile::anotherapp::binary:  %{lookup (profile::base::artifactoryserver)}/anotherapp.rpm

这也会使维护变得更加简单;如果 artifactory 服务器发生更改,只需更新一行即可。

alias 函数允许在 Hiera 数据中返回数据结构,因为 lookup 仅返回字符串。因此,如果 base 配置文件有一个 extensions 参数,它接受一个字符串数组并且我们希望将相同的扩展名列表传递给另一个配置文件 exampleapp,则可以像这样编写:

profile::base::extensions:
  -  'option1'
  -  'option2'
  -  'option3'
profile::exampleapp::extensions: "%{alias(profile::base::extensions)}"

literal 函数允许转义百分号符号(%),以避免它被解释为变量或函数进行插值。为了做到这一点,我们可以使用 %{literal('%')} 函数,其中 % 符号需要被使用。这在某些场景下非常有用,比如 Apache 配置文件或 Windows 环境变量;例如,如果我们想在 profile::nuget:: 中使用 %PACKAGEHOME%/External 字符串,则可以使用以下代码:

profile::nuget::
: %{literal('%')}{PACKAGEHOME} %{literal('%')}

scope 函数可能仅在遗留代码中使用。它实际上只是进行变量插值,只有在 Puppet 变量动态作用域时才有用。在本节中的 Tivoli 示例将写成 tivoli_config_file: '/opt/app/tivoli/client/%{scope(facts.hostname)}.conf'

使用自定义后端

除了到目前为止描述的内置后端外,还可以编写自定义后端或从 Forge 下载并配置到 Hiera 中。编写自定义后端超出了本书的范围,但 Puppet 的文档涵盖了如何编写它们,详见 www.puppet.com/docs/puppet/8/hiera_custom_backends.html#custom_backends_overview

自定义后端使用三种数据类型之一,根据数据访问的性能需求选择。

data_hash 后端类型,如同内置后端所示,用于读取成本较低的数据源,如磁盘上的文件。该配置文件用于数据小、静态、可以一次性读取且大部分数据都被使用的场景。它返回键值对的哈希值。

lookup_key 类型用于读取成本较高的数据源,例如安全的 HTTP API 连接。此配置用于数据量大,且仅部分数据使用,并且在编译过程中可能发生变化的情况。它返回一个键值对。最常用的自定义后端是 hiera-eyaml,用于加密 Hiera,这将在 保持数据 安全 部分详细讲解。

data_dig 后端类型用于访问集合中任意元素的数据源,例如数据库。与 lookup_key 的配置相似,但它访问元素的子键来返回一个键值对,该函数将深入到一个点分隔的键。

另一个需要提及的数据类型是 hiera3_backend,它仅在从旧版 Puppet 配置中迁移时相关;本书不会覆盖此配置,但详细信息可以在 Puppet 文档中找到,网址为 www.puppet.com/docs/puppet/8/hiera_config_yaml_5.html。Puppet 文档提供了如何从 Hiera 3 后端迁移的指导,如果你在遗留代码中遇到它们,可以访问 www.puppet.com/docs/puppet/8/hiera_migrate.html

注意

从用户的角度来看,Hiera 版本 5 是 Hiera 3 的演进,Hiera 4 是实验性版本,但 Hiera 5 在 Puppet 本身中得到了完全实现,而 Hiera 3 则是独立的实现。Puppet 7 及以下版本依赖于 Ruby gem 来支持 Hiera 版本 3,支持任何扩展了 Hiera:Backend 的遗留 Hiera 3 后端。这个依赖在 Puppet 8 中被移除。

这些数据类型可以与文件路径结合使用,正如之前与内置后端讨论的那样,另外还可以使用 uriuris 路径来指向如 Web 来源等 URI。

options 参数允许传入一个哈希,包含自定义后端所需的任何内容,例如凭证或密钥信息,具体内容将依赖于实现。

大多数模块会在其 README 文件中解释如何使用 options 参数。例如,forge.puppet.com/modules/petems/hiera_vault/ 是 HashiCorp Vault 的 Hiera 后端;根据他们的示例,以下代码展示了一个假设的示例,其中密钥都以 secret_ 开头,来自 vault.example.com 服务器,并为两个团队(digitaltrade)设置了挂载点,这些团队使用节点名称、位置和 common 作为他们的密钥层次结构:

hierarchy:
  - name: "Vault secrets"
    lookup_key: hiera_vault
    options:
      confine_to_keys:
        - "^secret_.*"
      ssl_verify: false
      address: https://vault.example.com:8200
      token: notreallyatoken>
      default_field: value
      mounts:
        digital:
          - %{::trusted.certname}
          - %{::trusted.extensions.pp_region}
          - common
        trade:
          - %{::trusted.certname}
          - %{::trusted.extensions.pp_region}
          - common

另一个示例是 forge.puppet.com/modules/tragiccode/azure_key_vault/,它允许访问 Azure 中的秘密。如果我们创建一个基于服务器部门分配的查找,查找以 secret 开头的密钥,结果将如下所示:

- name: 'Department Azure secrets'
    lookup_key: azure_key_vault::lookup
    options:
      vault_name: "%{trusted.extensions.pp_department}"
      vault_api_version: '2023-02-04'
      metadata_api_version: '2023-02-11'
      key_replacement_token: '-'
      confine_to_keys:
        - '^secret_.*'

第十三章中,将讨论Puppet 数据服务PDS),以及一系列用于扩展 Puppet 数据访问的后端。

现在我们已经了解了 Hiera 的工作原理,让我们来看看它如何在 Puppet 的不同层次中工作。

Hiera 层次

Hiera 仅在单一层级的上下文中进行过讨论,但实际上有三个层次的层级,每个层级都有其自身的层次配置。当 Puppet 在执行时进行查找时,它将遍历这些层级,检查每个层次中的层级结构。

全局层是第一层,并默认配置在$confdir/hiera.yaml中,通常路径为/etc/puppetlabs/puppet/hiera.yaml。Hiera 3 仅在此层工作,它的存在更多是为了兼容性考虑。Puppet 的文档建议,它的唯一目的应该是为了 Hiera 3 兼容性,并作为全局覆盖,但我们建议你完全不要使用它,因为它存在于代码部署和控制流程之外,这些将在第十一章中详细讲解。这将使文件的控制局限于 Puppet 服务器,只有在你希望绕过代码部署过程时,才可能需要这种情况。

环境层是下一个也是主要的数据层,它通常配置在每个环境中,路径通常为/etc/puppetlabs/code/production/hiera.yaml。环境和控制库将在第十一章中详细讨论,但为了理解这里的背景,环境是为特定组的 Puppet 节点设置的、具有固定版本的 Puppet 模块和清单,而控制库是用于管理环境的模块结构,包含一个名为 Puppetfile 的文件,详细说明了模块的源、应部署的版本以及部署位置。

需要决定hiera.yaml文件和数据是与控制库一起包含,还是将其与包含 Hiera 数据的模块分开。这是通过控制库将模块部署到环境中的数据目录,并确保 Hiera 在其hiera.yaml文件中使用该数据路径来配置的。当一组数据的控制需要由特定团队或组进行管理,而将其包含在控制库中可能会导致过多的访问/可见性时,这种分离是有意义的。例如,如果我们的hiera.yaml文件配置为使用数据作为源路径,我们可以通过在 Puppetfile 中添加条目,将模块中的 Hiera 数据添加到该路径中:

mod 'exampleorg_hieradata',
  :git    => 'https://<your_git_server>/exampleorg/hieradata.git',
  :install_path => 'data'

最后一层是模块层,这一层通过每个模块内的 hiera.yaml 文件进行配置,通常模块内还会有一个数据文件夹。因此,当在服务器的环境中部署时,hiera.yaml 文件可能位于类似 /etc/puppetlabs/code/environments/production/modules/example_module/hiera.yaml 的位置。模块层的最佳用途是为模块中所有类的参数设置默认值,同时要小心保持它们与模块的关注点相关,而不是外部的组织数据,这些数据更适合放在环境层中。可以在 puppetlabs/ntp 模块中看到设置默认值的示例,访问地址为 github.com/puppetlabs/puppetlabs-ntp,该模块根据操作系统版本设置默认值。hiera.yaml 文件还可以配置为支持对特定操作系统版本的逐渐细化,从默认值和一般的操作系统系列(如 Windows)到特定的完整操作系统版本,如 AlmaLinux-8.5,如以下代码所示:

hierarchy:
  - name: 'Full Version' 
    path: '%{facts.os.name}-%{facts.os.release.full}.yaml' 
  - name: 'Major Version' 
    path: '%{facts.os.name}-%{facts.os.release.major}.yaml' 
  - name: 'Distribution Name' 
    path: '%{facts.os.name}.yaml' 
  - name: 'Operating System Family' 
    path: '%{facts.os.family}-family.yaml' 
  - name: 'common' 
    path: 'common.yaml

注意

模块层通常被视为 params.pp 类的替代方法,后者曾是模块模式的一部分,包含默认值和 Hiera 查找调用。在现代 Hiera 层和自动参数查找机制出现之前,params.pp 类曾被广泛使用。

你只能在模块的命名空间中绑定数据键,因此在 exampleapp 模块中,只能设置 exampleapp::key 的值,不能设置像 key1 这样的全局键或其他模块如 anotherapp::key。这可能会导致另一种模式选项,特别适用于内部编写的模块,其中利用这个限制可以让应用团队完全控制模块的环境数据,而不影响其他模块。这对于由特定团队拥有的配置文件模块尤其重要,该团队希望管理期望。

default_hierarchy 有时被称为第四层,仅在模块层可用;它本质上是在模块层次结构中声明一个 default_hierarchy 键。与这一层的主要区别在于,只有当其他三层中没有匹配时,才会调用这一层,因此没有合并行为:

default_hierarchy:
  - name: 'defaults'
    path: 'defaults.yaml'
    data_hash: yaml_data

注意

default_hierarchy 产生的行为与 params.pp 方法相同,因为在三个 Hiera 层中只要有任何匹配项,它都会忽略并且不会合并任何匹配的值。

在回顾了这些层次之后,接下来会提出一个问题:应该如何构建这些层次结构。层次结构可以迅速变得复杂,但我们应当记住,基本的方法是应从节点的最具体数据开始,到通用数据为止。它们应尽可能简短,因为数据文件更容易处理,创建的层次结构越多,对 Puppet 基础设施性能的影响越大。过多的后端(特别是定制的后端)会带来复杂性和外部依赖,可能会破坏 Puppet 的编译。使用角色和配置文件方法应当减少在 Hiera 中管理的数据量,如果内建事实不够用,可以创建自定义事实,并且可以在路径中一起使用多个事实。

全局层适合仅基于节点名称和所有节点共享的数据来构建,因为它仅在 Puppet 代码环境控制之外进行覆盖时使用。

对于环境层,常见的节点数据结构如下所示:

  • 节点的名称

  • 节点所有者

  • 节点的目的

  • 节点的位置

  • 所有节点共享的数据

这可能导致一个简单的层次结构,如下所示:

- name: "Node data"
  path: "node/%{trusted.certname}.yaml"
- name: "Org data"
  path: "node/%{facts.org}.yaml"
- name: "Application-Tier"
  path: "app_tier/%{facts.app_tier}.yaml"
- name: "Datacenter"
  path: "datacenter/%{facts.datacenter}.yaml"
- name: "Common data"
  path: "common.yaml"

如前所述,模块层则成为以操作系统版本和平台等事实为基础的默认值的集中地。

注意

不要在任何层次结构中直接使用 environment 事实。应当使用环境层来处理基于环境的数据。

实验 – 向模块添加数据

在这个实验中,从 第八章 下载并更新 Grafana 模块,将默认值存储在 Hiera 数据中,而不是在参数中。

为此,假设 common.yaml 文件将包含 init.pp 中的所有当前默认值。

对于 Red Hat,我们将有如下内容:download_source = 'dl.grafana.com/enterprise/release/grafana-enterprise-8.4.3-1.x86_64.rpm'package_provider ='yum'

对于 Windows,我们将有如下内容:

download_source = 'https://dl.grafana.com/enterprise/release/grafana-enterprise-9.4.1.windows-amd64.msi'
package_provider = 'windows'

你可以参考github.com/PacktPublishing/Puppet-8-for-DevOps-Engineers/tree/main/ch08/grafana

一个示例答案可以参考 github.com/PacktPublishing/Puppet-8-for-DevOps-Engineers/tree/main/ch09/grafana

)

在后续的保持数据安全部分,将展示如何正确地保护密码,而不仅仅是将其作为明文保存在 YAML 文件中。

在这一部分,我们已经看到了如何使用 Hiera 的三层结构,以及如何在这些层中构建层次结构。接下来,我们将探讨何时应当在 Hiera 中使用数据,何时应直接在代码中使用数据。

决定何时使用静态代码或动态数据

在浏览了所有管理数据结构的可能性并查看了本书中介绍的代码示例后,可能会提出一个问题:何时编写代码,何时使用数据。图 9.1 展示了一个决策树:

https://github.com/OpenDocCN/freelearn-devops-pt3-zh/raw/master/docs/ppt8-dop-engi/img/B18492_09_01.jpg

图 9.1 – 数据或代码决策树

第一个关键点是,如果数据在节点之间没有变化并且仅使用一次,最简单的方法是将数据硬编码在 Puppet 代码中——例如,直接将文件资源属性中的文件所有者设置为exampleuser

如果某个值被多次使用,那么显然将该值分配给一个变量并在需要的地方使用这个变量是有价值的。如果值需要更改,这简化了维护,但这也意味着在阅读代码时需要跟踪变量。

另一方面,如果在不同节点之间存在变化,并且需要在某些条件下覆盖该值,首先应该考虑逻辑的复杂性。如果只是一个简单的检查,那么将其抽象到 Hiera 中的收益并不大;将值抽象到 Hiera 中的问题在于它们在查看代码时不再明显,需要进行翻译和思考。因此,如果可以使用简单的条件逻辑,通常更好的做法是将值保留在代码中。

一旦逻辑变得更加复杂,并且可能会根据条件的组合而变化,我们可以使用 Hiera 数据和自动参数查找,或者如果有必要的话,使用lookup函数。

在整个过程中,最好使用当时可用的最简单方法,并随着代码的变化和增长逐步增加复杂性。为了将来创建复杂的数据结构和进行抽象仅仅会增加复杂性,需要更多的工作而无法带来实际的好处。

保持数据安全

管理数据的一个关键要素是确保机密数据的安全,使用 Puppet 时,必须将数据存储、传输到客户端并在 Puppet 代码中设置状态,这可能会带来挑战。在本节中,我们将讨论保护数据的可用方法、数据可以在哪些层级上进行保护,以及在每个层级使用的方法的局限性。

最常见的第一步是保护存储中的数据。这可以通过使用hiera-eyaml来实现,hiera-eyaml是一个可用的自定义 Hiera 后端,地址为github.com/voxpupuli/hiera-eyaml。该模块创建pkcs7密钥,然后用于加密和解密数据。在按照模块中的指示创建并分发密钥后,可以创建一个层次结构,例如以下示例:

hierarchy:
  - name: "Hiera data in yaml and eyaml files committed to the control-repo"
    lookup_key: eyaml_lookup_key
    options:
      pkcs7_private_key: /etc/puppetlabs/puppet/eyaml/private_key.pkcs7.pem
      pkcs7_public_key:  /etc/puppetlabs/puppet/eyaml/public_key.pkcs7.pem
    paths:
      - "nodes/%{trusted.certname}.yaml"
      - "location/%{facts.whereami}/%{facts.group}.yaml"
      - "groups/%{facts.group}.yaml"
      - "secrets/nodes/%{trusted.certname}.eyaml"
      - "os/%{facts.os.family}.yaml"
      - "common.yaml"

可以简化层级结构,注意 eyaml 后端也可以读取 YAML 文件,且没有理由将 yamleyaml 文件分离到不同的层级中,只要它们的路径和选项相同,如前面的示例所示。

hiera-eyaml 对于简单的加密和涉及有限用户加密秘密的情况是可行的,但对于更大的设置,使用 gpg 密钥与 github.com/voxpupuli/hiera-eyaml-gpg 比在多个团队之间共享签名密钥更为实用。配置和密钥管理完成后,这只需通过使用 gpg_gnugpghome 选项而不是 pkcs7 密钥选项来变化,例如如下所示:

    options:
      gpg_gnupghome: /opt/puppetlabs/server/data/puppetserver/.gnupg

这些加密数据文件方法的替代方案是,如果存在合适的安全密钥存储,例如 HashiCorp Vault,或云原生密钥存储,如 Azure Key Vault,那么使用能够访问这些服务的后端将确保数据安全存储。

无论选择哪个后端,这仅能确保数据在存储中是安全的。正如在访问数据部分所讨论的那样,默认情况下,当通过 Puppet 代码访问时,Hiera 将返回一个字符串。在 Puppet 5.5 及以上版本中,可以使用lookup_options将参数类型转换为Sensitive,并应谨慎确保所有安全参数都通过通配符或显式命名来覆盖。

必须小心使用 Sensitive 数据类型;容易错误地将其保密,使得无法在需要的位置使用它,或在使用 unwrap 函数时不小心暴露它。

当使用 filecontent 时,例如,以下尝试将 secret_value 放入 /etc/secure 文件中的做法会暴露在文件差异中,正如在 第三章 中讨论的那样,这是在报告日志中记录文件变化的比较时:

file {'/etc/secure':
  ensure => present,
  content => ${secret_value},
}

可以通过将file_diff参数设置为false或设置服务器不使用文件差异来防止此问题。

类似地,对于模板,也需要小心。如果使用 Puppet 6.2 或更高版本,模板将直接与 Sensitive 值一起工作,你可以在模板中直接使用 Sensitive 值:

file {'/tmp/test1':
  ensure => present,
  content => (epp('example.epp', { 'password' => $secure_password })),
}

对于低于 Puppet 6.2 的版本,你需要在模板中解包变量,然后将内容标记为 Sensitive,如以下示例所示:

content => Sensitive(epp('example.epp', { 'password' => unwrap($secure_password)})),

正确使用Sensitive选项可以避免将数据记录到日志中,但不幸的是,它不会阻止数据出现在目录文件本身中,如果你正在使用 PuppetDB,目录也会在那里存储。在这种情况下,使用forge.puppet.com/modules/binford2k/node_encrypt中提供的node_encrypt模块,可以使用客户端的密钥加密目录中的任何机密数据,并通过使用Deferred函数在应用目录时解密这些数据。这可以将机密数据从目录和应用目录后生成的报告中排除。

假设在基础设施上已经按照配置node_encrypt的说明进行设置,那么在之前代码中为content参数赋值的行可以更新为调用node_encrypt::secret函数,如下所示:

content => (epp('example.epp', { 'password' => $secure_password })). node_encrypt::secret,

注意

当前版本的node_encrypt依赖于 Puppet 6 中引入的Deferred函数,因此在旧版本上工作时需要使用版本 0.4.1,并且应使用node_encrypt::file类型,而不是file类型来加密文件资源。

本节展示了如何确保数据在存储、传输到目录和报告处理中的安全性,以及可能遇到的一些问题。在下一节中,我们将讨论在 Hiera 中处理数据时的常见问题。

实验 - 使用 eyaml 存储秘密数据

在本实验中,使用了puppet-hiera_eyaml模块来配置默认的pkcs密钥,设置了一个全局的 Hiera 配置,以查看节点名称、操作系统和通用值。在site.pp中,执行了一个 Hiera 查找,用来查找secret::examplefiles的值,该值作为内容创建了/var/tmp/secret_example文件并将其放置在 Puppet 主服务器上。查找的默认值未设置。在本实验中,你将加密一个秘密并将其添加到操作系统级别,使得文件的内容发生变化。

SSH 到主服务器并提升为 root 用户:

ssh centos@<primary_host>
sudo su -

/etc/puppetlabs/puppet目录中运行eyaml encrypt –p命令,并在提示符下输入你选择的秘密数据:

cd /etc/puppetlabs/puppet
eyaml encrypt -p

将以ENC[开头的字符串后面的输出复制,并粘贴到/etc/puppetlabs/puppet/data/os/RedHat.eyaml的 data 部分,使其包含如下内容:

---
secret::example: ENC[PKCS7,<long string of chars>]

运行puppet agent –t,观察/var/tmp/secret_example的内容变化为你设置的内容。

这是一个非常简单的示例,需要注意的是,正如Hiera 层部分所强调的,你更可能使用环境层次结构并保持数据安全,正如保持数据安全部分所示,通过在查找的options参数中使用 Sensitive 选项来实现。此外,eyaml使用的公钥可以复制到桌面上以加密秘密数据,前提是这对你所在组织的安全政策足够安全。

现在我们已经全面回顾了 Hiera 配置,接下来我们将展示如何理解查找和数据的问题。

陷阱、注意事项和问题

在处理包含多个层级和层次的大数据集时,可能会变得很难理解为什么某些答案被生成或错误是如何插入的。本节将专注于理解和调试数据查找的方式,以及可以使数据更清晰的工具。

Hiera 的问题通常可以归纳为几个类别:语法、格式、后端通信和性能问题、层级顺序错误等。

puppet lookup命令是测试 Hiera 数据的最佳方式,实际上就像在 Puppet 代码中使用的lookup函数一样。在主服务器上使用此命令时,其基本语法是puppet lookup <key> --node <node_name> --environment <environment_name>

此命令将返回值(如果找到),否则返回空。了解此命令的各种标志的效果非常重要,这些标志可以返回更详细的信息。一个常见的错误是同时使用--debug--explain标志;它们不应该一起使用,因为前者侧重于高水平的日志记录,帮助你理解为什么会生成如语法、格式或后端之类的错误,而后者侧重于展示如何得到一个值,Hiera 查找了哪里,以及它找到了什么。

例如,motd::contentexplain查找可能如下所示:

puppet lookup --explain motd::content --node node-name --environment production
 Searching for "lookup_options"
  Global Data Provider (hiera configuration version 5)
    Using configuration "/etc/puppetlabs/puppet/hiera.yaml"
    Hierarchy entry "Classifier Configuration Data"
      No such key: "lookup_options"
  Environment Data Provider (hiera configuration version 5)
    Using configuration "/etc/puppetlabs/code/environments/production/hiera.yaml"
    Merge strategy hash
      Hierarchy entry "Yaml backend"
        Merge strategy hash
          Path "/etc/puppetlabs/code/environments/production/data/nodes/pe-server-0-540983.05eqwrwxv1ourfszstaygpgbth.zx.internal.cloudapp.net.yaml"
            Original path: "nodes/%{trusted.certname}.yaml"
            Path not found
          Path "/etc/puppetlabs/code/environments/production/data/common.yaml"
            Original path: "common.yaml"
           Found key: "motd::content" value: "test"

从调试输出中,我们可以看到更多关于 Facter 和其他系统操作的信息,如下所示的命令和示例输出:

puppet lookup motd::content –-node node-name –-environment production -–debug
Debug: Facter: Managed to read hostname: pe-server-0-d6a9f5 and domain: vhcpsckl41fedgadugqovud0sa.cwx.internal.cloudapp.net
Debug: Facter: Loading external facts
Debug: Facter: fact "domain" has resolved to: vhcpsckl41fedgadugqovud0sa.cwx.internal.cloudapp.net
Debug: Lookup of 'motd::content'
  Searching for "lookup_options"
    Global Data Provider (hiera configuration version 5)
      Using configuration "/etc/puppetlabs/puppet/hiera.yaml"
      Hierarchy entry "Example yaml"
        Merge strategy hash
          Path "/etc/puppetlabs/puppet/data/nodes/pe-server-0-d6a9f5.vhcpsckl41fedgadugqovud0sa.cwx.internal.cloudapp.net.eyaml"

如果没有提供节点,查找操作将默认假设查找的是你运行命令的服务器,并且环境将默认设置为production

在语法和格式问题方面,最常见的错误之一是 YAML 文件的开头---格式错误。这可能会以几种方式发生:

  • 行的开头不小心添加了空格或发生了 Unicode 字符转换,导致它变成。在这种情况下,debug中的错误将如下所示:

错误:无法运行:(<unknown>):在第 2 行 第 8 列的上下文中不允许映射值

  • 如果在破折号中间插入了空格,例如-- -,那么在debug中将看到如下错误:

错误:无法运行:(<unknown>):在解析块集合时没有找到预期的指示符,在第 1 行 第 1 列

另一个常见的语法错误是使用键值对时,冒号符号(:)和值之间没有空格;因此key: valuekey : value是有效的,而key:value不是,它在调试时会像下面这样报错:

错误:无法运行:(<unknown>):在第 3 行 第 10 列的上下文中不允许映射值

如果使用制表符而不是空格进行缩进,那么在调试时可能会导致类似以下的错误:

Error: Could not run: (<unknown>): found character that cannot start any token while scanning for the next token at line 4 column 1

在格式化时,使用单引号包围包含变量的数据会导致返回变量名的字面字符串,而不是变量插值。

文件权限也可能是一个问题,因此,确保以相同用户身份运行查找命令是值得的,因为 Puppet 通常在pe-puppetpuppet用户下运行。

使用--debug,可以帮助查看是否是自定义后端出现问题、错误或性能下降。通常,我们建议检查像 PDS 和外部数据提供者这样的模式。

请注意,这不会调试实际数据,只会调试hiera.yaml文件,数据文件如果不是有效的 YAML 格式会被忽略,可以通过--explain查看。

在层次结构问题方面,--explain标志会非常有用,因为它会逐步解释所使用的配置文件、找到的层次结构、合并策略和详细检查的路径,从而清楚地展示它如何逐步遍历层次结构,以及为什么它可能没有按预期工作。

根据在层次结构中使用的变量,可能需要使用--compile标志,因为默认情况下,在使用 Puppet lookup时,它不会执行目录编译,因此只有$facts$trusted$server_facts变量可用。我们强烈建议避免使用清单中的任意值,因为这些值可能会极大地增加查找的复杂度并产生不可预测的结果。

从中可以看出,你总是应该使用Facter数组,以避免模块变量和顶层Facter变量冲突的风险。

一些其他选项可以用来测试更改配置后会发生什么,比如使用--merge标志更改合并策略,或者通过--facts提供更新的事实数据等。

查找命令选项的完整命令参考可以在www.puppet.com/docs/puppet/latest/man/lookup.html查看。

如果更新全局 Hiera 文件,请小心重启Puppet 服务器服务以确保重新读取该文件。

在前面访问数据部分中提到过,我们不建议在lookup函数中使用默认值。模块或配置文件中的数据默认值应该是有意义的。因此,提供默认的配置文件位置对一个模块来说是有意义的,尤其是如果你期望大多数用户只会使用它。但如果仅仅为了避免查找失败而添加默认值,那可能是一个严重的错误,这会掩盖 Hiera 数据或代码中的问题,这些问题不会被注意到,因为代码会用默认值成功应用。需要避免的关键问题是,传递一个默认值,然后需要在 Puppet 代码中使用大量逻辑来处理该值。

在 Hiera 中进行分类是可能的,因为一些用户选择查找 Hiera 数据并将类包含在site.pp文件中。像github.com/ripienaar/puppet-classifier这样的模块专注于这种方法。需要考虑代码结构的平衡,正如我们灵活的角色和配置文件方法中所展示的那样。通过将过多数据放入 Hiera,它可能会使代码不再直观,因为数据在代码中不直接可见。因此,最好考虑提高复杂度是否值得。

Hiera 的一个问题是其结构,这使得不太参与的用户无法访问。为了让 Hiera 数据更可见,Betadots Hiera Data Managerforge.puppet.com/modules/betadots/hdm)是一个很好的选择,因为它允许图形化搜索、更新和删除 Hiera 数据。然而,在生产环境中,这应仅限于查看数据。

https://github.com/OpenDocCN/freelearn-devops-pt3-zh/raw/master/docs/ppt8-dop-engi/img/B18492_09_02.jpg

图 9.2 – Hiera 数据管理器示例查找

另一个让 Hiera 数据更易于自助服务的选项是 PDS,详细内容将在第十三章中讨论。

实验室 – 排查 Hiera 问题

在生产环境中排查 Hiera 数据问题:

  1. 通过 SSH 连接到主服务器,提升为 root 用户,并部署lab_error环境:

    ssh centos@<primary_host>
    sudo su -
    puppet code deploy environment lab_error --wait
    
  2. lab_error环境中的主服务器上,以debug标志执行对profile::error::example键的查找,并解决发现的错误,纠正control仓库中的问题,并运行前一步的code deploy命令:

    puppet lookup profile::error::example --debug --environment lab_error
    
  3. 解决controlrepo-chapter9/datahiera.yaml中的数据错误。

  4. 运行相同的命令并使用explain,以了解它如何到达当前解决方案,并找出为什么它没有基于os.family事实找到值:

    puppet lookup profile::error::example --debug --environment lab_error
    
  5. 更新control仓库分支中的 Hiera 数据,lab_error,并重新部署,使得查找现在能找到主节点的os.family事实值:

puppet code deploy environment lab_error --wait

**puppet lookup profile::error::example --debug --**environment lab_error

查看github.com/PacktPublishing/Puppet-8-for-DevOps-Engineers/tree/main/ch09/data_solutions中的评论解决方案。

作为在技术要求部分创建实验室环境的一部分,已通过puppet-HDM模块安装了 HDM。尝试使用 HDM 按照以下步骤查看数据:

  1. http://<public IP of puppetserver>:3000处打开一个网页浏览器。

  2. 完成注册详细信息以创建管理员用户(详细信息不重要)。

  3. 点击non-admin user(详细信息同样不重要)。

  4. 点击右上角的管理员用户名,注销,然后以您创建的非管理员用户身份重新登录。

  5. 依次选择 environment productionlab_error

  6. 探索在每个环境中 HDM 可见的 Hiera 键值。

总结

在这一章中,我们探讨了 Puppet 如何通过 Hiera 工具处理数据,从而减少在代码中表示节点、数据中心、组织、操作系统及其他配置差异所需的复杂性。Hiera 被证明是一个基于数据层级结构的工具,它允许我们根据事实访问不同的文件。它具有内置的后端,可以将数据存储在 YAML、JSON、HOCON 和 EYAML 文件中。展示了数据结构,我们研究了如何将值放入数据文件以及如何执行查找;这里还考察了合并类型,以及如何在数组中使用如 knockout 前缀等特殊设置。

接着,我们展示了一些可以使用的自定义后端,这些后端具有不同配置文件上的数据类型;通常,这些是特定的集成工具,如 Vault 或来自 Forge 的 EYAML,或者是公司内部开发的集成工具,用于访问数据。

接下来,我们讲解了 Hiera 如何在三个层级上工作——全局、环境和模块——展示了全局层在现代 Puppet 设置中的作用较小,但可以作为覆盖系统使用,环境作为数据的主要来源,模块则允许为模块设置默认值。然后讨论了一些常见的层级结构设计方法,包括一种通过节点名称、节点所有者、节点目的、节点位置和所有节点共同的数据来逐步构建的方式。

关于如何决定是在代码中使用数据还是在 Hiera 中使用数据的回顾显示,这取决于数据需要的灵活性,而这从硬编码在 Puppet 代码中的静态数据到需要精确描述完整层级结构的更复杂灵活的数据都有不同的需求。建议不要提前构建,而是根据需要重构,以避免使数据变得比实际需求更复杂。

接着,我们讨论了如何确保数据在存储和传输中的安全性,以及在 Puppet catalogs、报告和 PuppetDB 中使用时的安全性。我们展示了如何使用 eyaml 通过更灵活的 PGP 方法加密值来确保存储中的数据安全,这允许多个密钥和团队的使用。然后,展示了 Sensitive 值,以确保值不会暴露在日志或代码中。这不会防止在 catalogs 和报告中暴露值,node_encrypt 模块被展示用于在配置时加密资源和值,并通过 Deferred 函数应用。

然后回顾了调试和故障排除的方法,重点介绍了--explain--debug之间的区别。前者可以帮助理解如何审查层级结构,后者则返回如语法错误和后端失败等错误。建议小心使用 Hiera 作为分类器,因为这会将分类信息从代码中抽象出来,但也强调在后续章节中 PDS 确实采用了这种方法。

在下一章,在详细回顾了 Puppet 语言之后,焦点将转向 Puppet 基础设施。我们将审视构成 Puppet 平台的开源组件,它们如何通过 API 向系统提供服务,以及它们如何进行通信和日志记录。将详细探讨 Puppet 代理的完整生命周期,包括代理注册过程及其与平台的通信。PuppetDB 和 PostgreSQL 将被用来存储诸如事实、报告和清单等数据,并允许通过Puppet 查询语言PQL)进行发现和审查。然后,我们将讨论编译服务器作为 Puppet 水平扩展的方法。

第三部分 – Puppet 平台和 Bolt 编排

在这一部分,你将了解 Puppet 是如何作为一个平台构建的,各个组件如何协作和通信,以及用于实现规模的常见架构方法。接下来我们将展示可以用来分类哪些代码应用到服务器上的各种方法,以及如何对代码进行版本控制和部署到基础设施。Bolt 将作为 Puppet 执行过程脚本和代码的方式被介绍,它可以是传统脚本,也可以是基于 Puppet 语言的计划。然后我们将回顾如何通过各种工具和第三方产品来监控、调整和集成 Puppet 基础设施。

这一部分包含以下章节:

  • 第十章Puppet 平台的组成部分和功能

  • 第十一章分类和发布管理

  • 第十二章用于编排的 Bolt

  • 第十三章进一步使用 Puppet 服务器

第十章:Puppet 平台部分和功能

到目前为止,我们讨论了 Puppet 作为一种语言,但在本章及后续章节中,我们将开始关注 Puppet 作为一个平台,以及平台的基础设施和组件。

图 10.1 中,展示了本章将讨论的 Puppet Server 和 Puppet 客户端服务的完整架构。这些服务专注于如何在服务器上强制执行 Puppet 代码:

https://github.com/OpenDocCN/freelearn-devops-pt3-zh/raw/master/docs/ppt8-dop-engi/img/B18492_10_01.jpg

图 10.1 – Puppet 服务器和客户端组件

我们将首先强调,在本书中不会详细介绍安装方法。对于开源 Puppet 和 Puppet Enterprise,有几个开源项目可以作为自动化基础;在本书中,我们使用了 peadpecd 模块作为最自动化的 Puppet 编辑器PE)安装机制。随着各组件的讨论,我们还将提到 Puppet 包的版本如何不同,并查看一些相关的安装版本、关键用户、目录、配置文件和已安装的服务。

首先,我们将检查 Puppet Server 提供的核心服务。这些服务包括接收客户端请求的清单编译、处理它们的当前状态,并根据 Puppet 代码确定如何配置它们。证书授权中心CA)允许代理安全地注册并与 Puppet 服务器通信。它还包括一些相关的 API 服务,以便访问、请求和控制这些服务。

在了解了服务器的功能后,我们将展示 Puppet agent 如何与服务器通信,请求由 CA 签署密钥,清单编译的通信过程,以及 agent 如何处理并存储返回的清单。

接下来我们将查看 PuppetDB 如何用于存储事实、清单和事件,以及如何通过 Puppet 查询语言PQL)和 API 访问这些信息。我们还将研究 PuppetDB 和 PostgreSQL 之间的关系,作为前端应用程序与后端数据库架构的连接,并讨论 Puppet 服务如何直接将其他数据存储在 PostgreSQL 中。

接着将展示如何使用编译服务器水平扩展,以编译数十万台服务器的清单。

在这些主题中,我们将突出 PE 和开源 Puppet 配置之间的细微差异。

本章不涉及与 PE 相关的编排器特性、PE 控制台或支持的架构(这些可以使服务拆分到更具可扩展性的基础设施中);这些将在 第十四章 中讨论。

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

  • Puppet 平台安装和版本控制

  • Puppet 服务器

  • Puppet agent 到 server 的生命周期

  • PuppetDB 和 PostgreSQL

  • 使用编译器进行扩展

注意

作为努力从其产品中移除有害术语的一部分,Puppet 放弃了使用master servercompile master的术语,现在使用primary servercompile server。由于这些名称已深植人心,某些地方的类或配置设置仍然会提到master

技术要求

github.com/puppetlabs/control-repo克隆控制库到你的 GitHub 账户,创建一个名为controlrepo-chapter10的库。

通过下载github.com/PacktPublishing/Puppet-8-for-DevOps-Engineers/blob/main/ch10/params.json中的params.json文件,并用你的控制库位置和控制库的 SSH 密钥更新它,来构建一个包含三个编译器和三个客户端的大型集群。然后,从你的pecdm目录运行以下命令:

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

Puppet 平台的安装与版本管理

本书选择不深入探讨 Puppet 的安装方法;对于开源版本的安装说明,几乎没有需要补充的内容,详细内容请见puppet.com/docs/puppet/latest/server/install_from_packages.html,任何进一步的自动化选择都将高度依赖于你组织的使用场景,以及你希望集成的工具和产品集。

对于开源 Puppet,有多个项目可以自动化 Puppet 的部署、配置和集成,比如 example42 的psick(github.com/example42/psick)或 Foreman 项目(github.com/theforeman/foreman-installer),后者有一个专门用于安装 Puppet Server 的模块(forge.puppet.com/modules/theforeman/puppet),即使不使用 Foreman,也可以用来安装 Puppet。类似 PE 设置提供的仪表板也可以在诸如 Puppetboard(forge.puppet.com/modules/puppet/puppetboard)或 Puppet Summary(github.com/skx/puppet-summary)等项目中找到。

对于 PE,尽管可以在puppet.com/docs/pe/2021.7/installing_pe.html找到手动安装说明,但自动化的选择是显而易见的,即使用 Puppet 支持的peadm模块;在第十二章中,我们将回顾如何在实验中使用该模块,并将pecdm作为 Bolt 项目使用。

安装的包中需要注意的关键点是,Puppet 仓库提供了不同版本的 Ruby、OpenSSL、Hiera 和 Facter,以供不同版本的 Puppet 使用,且像 puppetserver 这样的包可能与正在安装的 Puppet 版本不匹配——例如,Puppet 7.17 会安装 Puppet Server 版本 7.8;这些关联版本可以在发布说明中找到。对于 PE,你可以在文档中查看所有底层开源包版本,网址为 puppet.com/docs/pe/2021.7/component_versions_in_recent_pe_releases.html#component_versions_in_recent_pe_releases

Puppet Server

在 Puppet 的历史版本中,基于 Ruby 的解决方案如 WEBrick 或 Passenger 被用来运行 Puppet 服务,但在所有现代版本的 Puppet 中,为了提高扩展性和性能,Puppet Server 作为一个 Clojure 和 Ruby 应用程序运行在 Java 虚拟机 (JVM) 上。Puppet Server 具有多个相关的服务,这些服务共享状态并在它们之间路由请求。这些服务运行在单一的 JVM 进程中,使用 Trapperkeeper 服务框架,Trapperkeeper 是一个用于托管长时间运行应用程序的 Clojure 框架。

Puppet Server 通过 open source Puppet 中的 puppetserver 包和 PE 中的 pe-puppetserver 包进行安装。这样会创建一个同名的系统服务,并生成配置文件,默认情况下,这些文件会放置在 /etc/puppetlabs/puppetserver/conf.d 目录下,采用 人类优化配置对象表示法 (HOCON) 格式。

注意

Puppet 的 hocon 模块是自动化管理 HOCON 文件的最佳方式 (forge.puppet.com/modules/puppetlabs/hocon)。

接下来,我们将查看构成 Puppet Server 的服务。

内嵌 Web 服务器

Puppet 在 JVM 中包含一个基于 Jetty 的 Web 服务器,用于设置挂载点和通信,以便在组件之间进行 Web 请求并访问 API。

webserver.conf 文件设置了 Web 服务器的主要配置,如 web-routes.conf 文件的位置,后者通过挂载处理程序来设置 Web API 访问的挂载点,如以下示例文件所示:

# Configure the mount points for the web apps.
web-router-service: {
    # These two should not be modified because the Puppet 4 agent expects them to
    # be mounted at these specific paths.
    "puppetlabs.services.ca.certificate-authority-service/certificate-authority-service": "/puppet-ca"
    "puppetlabs.services.master.master-service/master-service": "/puppet"
    # This controls the mount point for the Puppet administration API.
    "puppetlabs.services.puppet-admin.puppet-admin-service/puppet-admin-service": "/puppet-admin-api"
}

在此文件中列出了客户端与服务器之间通信所需的核心挂载点:

puppet-ca 挂载点供客户端与 CA 服务进行通信,并检查或发出 证书签名请求 (CSR)。

  • master-service 提供一个挂载点,供客户端通过 JRuby 解释器编译的目录请求。

  • 默认情况下,webserver.conf 中设置的请求日志记录配置位于 /etc/puppetlabs/puppetserver/request-logging.xml,它决定了 HTTP 访问请求的记录方式。默认情况下,消息将被记录到 /var/log/puppetlabs/puppetserver/puppetserver-access.log

本节内容应帮助你了解嵌入式 Web 服务如何在 JVM 中设置 Web 服务器,及其为不同组件的 Puppet Server 请求提供必要的挂载点,并记录这些请求。接下来,我们将查看通过挂载点提供的两个核心 API,分别是通过 /puppet/puppet_ca 访问的 Puppet API,以及通过 /puppet_admin_api 访问的 Admin API。

Puppet API 服务

Puppet API 服务由嵌入式 Web 服务器创建的两个端点组成——/puppet 用于配置相关服务,/puppet-ca 用于 CA。

两者都通过如 /v3 这样的字符串进行版本控制,授权通过 auth.conf 文件控制,该文件是 HOCON 格式的文件。除非需要更高级的访问权限来集成服务,否则你不太可能需要编辑该文件,但为了展示示例内容,以下代码允许 Puppet 节点从 API 请求自己的目录:

        {
            # Allow nodes to retrieve their own catalog
            match-request: {
                path: "^/puppet/v3/catalog/([^/]+)$"
                type: regex
                method: [get, post]
            }
            allow: "$1"
            sort-order: 500
            name: "puppetlabs v3 catalog from agents"
        },

注意

有关自定义授权的更详细说明,请参见 github.com/puppetlabs/trapperkeeper-authorization/blob/main/doc/authorization-config.md

所有 Puppet 5 到 8 的现代版本中的 Puppet 代理使用 /puppet/v3 端点服务来管理客户端。v3 API 具有两种类型的端点——间接指令环境端点。

间接指令的格式为 /puppet/v3/<indirection>/<key>?environment=<environment>

在这里,间接值是请求的间接指令,键是与调用间接指令相关的键,环境是该请求应该使用的环境。例如,若要请求编译目录,客户端将构建以下内容:

/puppet/v3/catalog/pe.example.com?environment=production

服务器下/puppet/v3/路径下存在以下间接指令:

  • 事实facts 端点允许为指定的节点名称设置事实

  • 目录:返回指定节点的目录

  • 节点:返回节点信息,例如分类

  • 文件桶 文件:管理文件桶的内容

  • 文件内容:返回文件内容,例如模块中的文件

  • 文件元数据:返回文件的元数据,例如模块中文件的权限

  • 报告:允许存储节点的 Puppet 报告

服务器下/puppet/v3/路径下存在以下间接指令:

  • 环境类:返回请求环境中可以解析的所有类

  • 环境模块:返回环境中所有模块的信息,例如它们的名称和版本

  • 静态文件内容:返回特定版本的文件资源在某个环境中的文件内容

未作为间接指令的独立环境端点允许简单调用 /puppet/v3/environments,该调用返回服务器已知的所有环境。在下一章中,我们将更详细地讨论环境。

工具和服务也可以访问这些相同的端点来检查数据,并且存在一个v4 API,具有一个目录端点,可以更广泛地使用 PuppetDB 来操作事实和目录。它被如 octocatalog-diffgithub.com/github/octocatalog-diff)等工具使用,这些工具可以生成、比较和操作目录。

/puppet-ca 端点采用类似的格式,使用 v1 和指令,如下所示:

  • 证书:返回指定名称的证书

  • 证书清理:吊销并删除证书

  • 证书状态:请求证书或 CSR 的状态

  • 证书吊销列表:请求 证书吊销列表 (CRL) 文件

例如,要请求server.example.com的证书,可以访问以下端点:/puppet-ca/v1/certificate/server.example.com

这些操作将在本章的 CA 部分进行更详细的讨论。

在本节中,我们没有详细讨论每个端点及其 API 调用,但在本章后面,我们将查看客户端与服务器的生命周期,跟踪调用日志,并强调它们的用途,以展示 Puppet 如何使用这些 API。端点的完整详细信息可以在puppet.com/docs/puppet/latest/http_api/http_report.html查看。

Admin API

Admin API 只有两个端点在 /puppet_admin/v1/,如下所示:

  • 环境缓存:用于清除环境数据的缓存

  • JRuby 池:用于清除 JRuby 池或获取正在运行的 JRuby 实例的 Ruby 线程转储

这两个端点用于更深入的开发工作,因此超出了本书的范围,但有助于完整地展示 Puppet 服务器组件。可以在puppet.com/docs/puppet/latest/server/admin-api/v1/jruby-pool.htmlpuppet.com/docs/puppet/latest/server/admin-api/v1/environment-cache.html查看这些端点的详细信息。

CA

默认情况下,Puppet 使用其内置的 CA 和 公钥基础设施 (PKI) 来保护所有 SSL 通信。

有两个命令用于与 Puppet CA 设置进行交互——puppetserver ca 用于服务器端操作,如签署或吊销证书,puppet ssl 用于代理端任务,如请求和下载证书。这些命令通过 CLI 调用 puppet-ca 端点。

注意

尽管引入了 puppet-ca 端点,之前 ruby ca 实现的五个命令在 Puppet 6 之前仍然可用:puppet certificatepuppet certpuppet certificate_requestpuppet capuppet certificate_revocation_list。这些命令已被 puppetserver capuppet ssl 命令取代。即使你使用的是 Puppet 5,强烈建议不要使用这些 Ruby 命令,因为同时使用 API 和 Ruby 实现可能会破坏 CA。

尽管在介绍中讨论的安装自动化应该涵盖初始设置,但通过运行 puppetserver ca setup 检查 CA 设置是否已执行也是值得的。在 puppetserver/pe-puppetserver 服务启动之前,它将创建一个单独的根 CA 和一个中间签名 CA。如果在此步骤之前启动了 puppetserver/pe-puppetserver 服务,它将创建一个单一的根 CA 和签名 CA,这是 Puppet 以前的操作方式。除非有特定需求使用单一证书,否则应避免此情况。从 PE 2019.x 和 Puppet 6.x 开始,这些证书的有效期为 15 年;之前为 5 年,而且需要理解的是,升级 Puppet 版本并不会延长 CA 的有效期。

注意

通过 ca_extend 模块可以扩展过期的 CA(forge.puppet.com/modules/puppetlabs/ca_extend)。

在此步骤中创建的密钥和证书将保存在一个名为 /etc/puppetlabs/puppetserver/ca 的目录中(适用于 Puppet 7 及以上版本),或者保存在 /etc/puppetlabs/puppet/ca 目录中(适用于 Puppet 6 及以下版本)。为了避免混淆,新的目录位置下会有一个指向 /etc/puppetlabs/puppet/ca 的路径。该目录将包含以下内容:

  • ca_crl.pem:CRL 文件

  • ca_crt.pem:CA 签名的证书公钥

  • ca_key.pem:CA 私钥

  • ca_pub.pem:CA 公钥

  • inventory.txt:CA 签名的证书列表,包括其序列号和到期日期

  • requests:未签名的 CSR 文件

  • root_key.pem:如果使用单独的根 CA 和中间 CA,这是用于签署 CA 证书的根密钥

  • serial:此文件包含证书新序列号的递增计数器

  • signed:此文件夹包含所有已签名的 CSR 文件

除了这些文件外,还可以维护基础设施 CRL,默认情况下,开源 Puppet 不使用该 CRL,但 PE 使用该 CRL。为了保持较小的 CRL,infra_inventory.txt 文件用于管理 Puppet 基础设施服务器;当被吊销时,这些系统会被添加到infra_crl.pem中。通过在puppet.conf文件中将infra certificate-authority.enable-infra-crl设置为true,可以启用此功能。我们将在本章后续部分详细讨论puppet.conf文件。此方法意味着 Puppet 客户端只需要接收较小的基础设施 CRL,这对于有大量服务器更替的环境非常重要。将维护以下文件:

  • Infra_inventory.txt:CA 为基础设施服务器签名的证书列表

  • Infra_serials:此文件包含基础设施服务器新序列号的递增计数器

  • Infra_crl.pem:基础设施服务器的 CRL

如果您的组织需要使用外部 CA,可以使用组织自己的根 CA,并通过puppetserver ca import命令导入它(完整过程请参考puppet.com/docs/puppet/latest/server/intermediate_ca.html),让 Puppet 充当中间 CA。或者,可以通过部署一个单独的外部生成的根 CA 和签名 CA 来禁用 CA 服务,详细说明请参见puppet.com/docs/puppet/latest/config_ssl_external_ca.html。本书不推荐使用此方法,因为它需要自动化证书分发,而 Puppet 服务不再执行此操作。

当代理向 CA 发出请求时,CSR 会被发送,默认情况下,签名策略需要等待手动签名,CSR 存储在requests文件夹中。待签名的请求可以通过运行puppetserver ca list进行查看,然后通过运行puppetserver ca sign --certname < certname to sign >进行签名。所有已签名的证书可以通过运行puppetserver ca list --all来查看。

如果您使用 PE,可以在 PE Web 控制台上执行和查看证书签名,如图 10.2所示:

https://github.com/OpenDocCN/freelearn-devops-pt3-zh/raw/master/docs/ppt8-dop-engi/img/B18492_10_02.jpg

图 10.2 – PE 控制台证书签名

可以使用puppetserver ca revoke --certname < certname to revoke >命令吊销证书,并且可以运行puppetserver ca clean --certname < revoked certname >来清理并从 CA 中删除被吊销的证书。

在使用手动自动签名的工作流中,像 VMware 的vRealize OrchestratorVRO)这样的工具通常会在部署和退役服务器时调用 CA API。

为了自动化此过程,可以通过三种方式配置自动签名。将 autosign = true 添加到 puppet.confmaster 部分时,该更改会导致 CA 签署任何请求,但绝不应在生产环境中使用。

第二种方法是在 /etc/puppetlabs/puppet/autosign.conf 创建一个 autosign.conf 文件。在此文件中,可以包含服务器名称或域名通配符,每一行代表一个可以自动签名的节点名称或域名。例如,假设文件内容如下:

server1.puppet.com
*.example.com

这意味着 server1.puppet.comexample.com 域中的任何服务器都会被自动签名。

第三种方法是将 autosign 值设置为 puppet.conf 文件中的一个脚本。该脚本可以是任何语言编写的,并且将接收证书名称作为第一个参数,然后将 CSR 内容作为标准输入。脚本应以零返回码结束以进行签名,或者以非零返回码结束以不进行签名。这导致了一个常见的方法,即在 CSR 中包含一个用于检查的秘密,或者在公共云中使用标签。讨论编写这些脚本超出了本书的范围,尽管 Puppet 只提供了如何构建这些脚本的说明,地址为 puppet.com/docs/puppet/latest/ssl_autosign.html#ssl_policy_based_autosigning,而亚马逊在 aws.amazon.com/blogs/mt/aws-opsworks-puppet-enterprise-and-an-alternate-implementation-for-policy-based-auto-signing/ 提供了一个很好的示例。

本节已阐述了如何配置 CA 并将其作为 Puppet 服务器运行。本章稍后将回顾代理的完整生命周期,展示客户端如何创建 CSR 并使用 CA 完成 Puppet Server 提供的服务,并查看 JRuby 解释器。

JRuby 解释器

JRuby 是 Ruby 的 Java 实现,允许在 JVM 上使用 Ruby;这比传统的 Ruby 部署(如 Ruby on Rails)具有更好的可扩展性,因为大多数 Ruby 解释器不支持线程安全,且使用锁来一次运行一个线程。Puppet Server 拥有一个 JRuby 解释器/实例池,这些实例可以执行各种应用程序工作,如编译目录和处理报告。池中的解释器数量反映了可以同时运行的 Ruby 应用程序操作的数量,可以通过 puppetserver.conf 文件中的 max-active-instances 参数配置,或通过控制台中的 Hiera 在 PE 中配置,或通过 puppet_enterprise::master::puppetserver::jruby_max_active_instances 在代码中配置。我们将在 第十三章 中更详细地讨论这一点,届时我们将讨论用于审查和设置此大小的度量标准和工具。

在讨论完 Puppet Server 的组件后,我们将查看诸如用户、日志记录和文件系统等配置,以了解这些服务可以如何定制以及它们的要求。

Puppet Server 的配置和日志

我们在讨论每个组件时简要提到了某些配置文件和可用设置,但我们将在此总结。对于大多数配置文件,通常不需要进行自定义,大多数默认设置就能满足您的要求。

对于 PE,pe-puppetserver Puppet Server 服务将在 pe-puppet 账户下运行,而在开源 Puppet 上,puppetserver 服务将在 puppet 账户下运行。在这两个账户中,它们将设置 nologin shell,以便用户仅提供一个账户来运行服务并拥有服务相关的文件。

以下配置文件和应用目录将被创建并使用:

  • /etc/puppetlabs/puppetserver/bootstrap.cfg:此文件包含 Trapperkeeper 应启动的服务列表;这些是由嵌入式 Web 服务器挂载的处理程序。

  • /etc/puppetlabs/puppetserver/request-logging.xml:定义 HTTP 访问请求如何被记录的文件。

  • /etc/puppetlabs/puppetserver/conf.d:此目录包含以下主要的 HOCON 格式配置文件:

    • global.conf:此文件为 Puppet 设置全局配置,默认仅包含日志配置文件的位置。

    • webserver.conf:此文件配置嵌入式 Web 服务器的细节,如端口和日志记录。

    • web-routes.conf:此文件为 Puppet 的 Web 服务设置挂载点。

    • puppetserver.conf:此文件设置核心 Puppet Server 应用程序的配置,例如正在运行的 jruby 实例数量。

    • auth.conf:此文件设置由 web-routes.conf 挂载的端点的访问权限。

    • ca.conf:此文件配置 CA 的设置。

    • products.conf:一个可选文件,可以设置产品设置,如分析数据和更新检查。

  • /etc/puppetlabs/puppetserver/ssl/ca:与 Puppet CA 相关的证书和密钥(在 Puppet 6 及以下版本中为 /etc/puppetlabs/puppet/ssl/ca)。

  • /opt/puppetlabs/puppet/lib/ruby/vendor_gems:Puppet Server 将与 CA 操作相关的 Ruby gems 放置在此目录中。

  • /opt/puppetlabs/server:此目录包含用于运行 Puppet Server 的 JRuby-gems 和二进制文件。

  • /var/run/puppetlabs/puppetserver/puppetserver.pid:此文件包含正在运行的 Puppet 进程的 PID。

  • /etc/puppetlabs/puppet.conf:此文件包含主机上 Puppet 客户端和 Puppet Server 的配置。可以通过运行 puppet config print 查看这些设置。

文件中的绝大多数设置将使用默认值,除非需要外部集成,如外部根 CA 等,这些设置只作为参考来帮助理解 Puppet 的配置。有关设置的完整参考和选项,可以在puppet.com/docs/puppet/latest/server/configuration.html查看/etc/puppetlab/puppetserver基础设置。

注意

如果您选择了引言中提到的某个开源 Puppet 自动化工具/模块,它可能在安装时允许设置配置值。

PE 用户应注意,由于配置的自动化程度较高,许多设置(例如puppetserver.conf中的设置)是通过 Hiera 配置的,应遵循puppet.com/docs/pe/2021.7/config_puppetserver.html中的文档进行配置。

调整这些设置的配置将在第十三章中详细讨论。

/etc/puppetlabs/puppet.conf的完整设置选项可以在www.puppet.com/docs/puppet/latest/config_file_main.html查看;该文件本身提供了配置 Puppet 服务器、Puppet 代理,以及puppet apply运行方式的各个部分。各部分包括main(提供默认值)、agent(为 Puppet 客户端提供设置)、user(提供使用 Puppet apply时的设置),以及master/server(用于将设置应用于 Puppet 服务器)。

自 Puppet 6 版本以来,已可以使用server部分代替master部分,但许多自动化工具尚未跟进这一变化,由于它们不是可互换的术语,且可能会引起混淆,因此请小心,仅使用与您的实现相关的术语。

Puppet 首先应用来自master/serverapplyagent部分的设置,然后回退到main部分,如果找不到设置,则会使用默认值。

让我们看一下在peadm构建的 Puppet 实验室服务器上某个文件的示例内容:

[master]
node_terminus = classifier
storeconfigs = true
storeconfigs_backend = puppetdb
reports = puppetdb
certname = pe-server-davidsand-0-cffe02.tq2kpafq5bsehkpub4ur5a35ya.xx.internal.cloudapp.net

方括号表示一个部分的名称,后面跟着一组键值对。这里的设置展示了我们 Puppet 服务器的证书名称(certname),还表明它通过reports设置将报告发送到 PuppetDB,设置为storeconfigs=true时,它会存储目录、节点和事实信息,这些信息将存储在 PuppetDB 中,并且storeconfigs_backend设置为 PuppetDB。最后,node_terminus设置为classifier,这反映了主服务器应如何分类客户端。这个内容将在下一章中详细讨论。

查看和操作设置(包括puppet.conf中未设置的默认值)最好的方法是使用puppet config命令,它可以显示所有设置。通过运行puppet config print all known,设置将被打印出来,或者可以通过详细说明部分和要打印的值来打印单个设置,命令为puppet config print --section master certnamepuppet config命令还可以使用setdelete选项添加或删除值,并选择一个部分键和值来执行操作。例如,以下命令将从master部分删除storeconfigs并将证书名称更改为newname.example.com

puppet config delete --section master storeconfigs
puppet config add --section master certname newname.example.com

这些命令将在文件中没有相应部分时自动添加该部分,但 Puppet 服务需要重启以使任何更改生效。

在下一部分我们将通过更多示例操作puppet.conf文件,查看代理生命周期,但puppet.conf文件的完整选项和语法可以查看puppet.com/docs/puppet/latest/config_file_main.html

默认情况下,Puppet 服务器会将日志保存在/var/log/puppetlabs/puppetserver下的以下文件中:

  • Puppetserver.log:这是记录主要服务器活动(如编译错误和警告)日志的地方。

  • Puppetserver-access.log:这是记录对 HTTP 端点的请求的地方。

  • Puppetserver_gc.log:这是收集垃圾回收日志的地方。

现在我们已经全面回顾了 Puppet 服务器组件,接下来我们将查看 Puppet 代理的配置和生命周期,了解这些服务如何被客户端使用,以及如何监控和查看一个周期的日志。

Puppet 代理到服务器的生命周期。

本节将讨论 Puppet 代理如何向我们运行的 Puppet 服务器组件发出请求,以及它在请求配置以强制执行时如何确保其通信安全。需要注意的是,Puppet 服务器本身也包含 Puppet 代理。

Puppet 代理的安装详细信息请参见puppet.com/docs/puppet/latest/install_agents.html#install_agents(开源)和puppet.com/docs/pe/2021.7/installing_agents.html#installing_agents(PE)。将此安装与服务器部署工作流集成,并确保将必要的配置放置在/etc/puppetlab/puppet.conf中,对于自动化至关重要。

注意

puppet_conf模块提供了管理 Puppet 配置文件的任务(forge.puppet.com/modules/puppetlabs/puppet_conf)。

大多数设置将取决于你的环境配置,但对于大多数环境,默认设置将会被采用,关键设置是确保在agent部分的服务器设置已正确配置,以便代理知道应联系哪个 Puppet 服务器——开源 Puppet 或 PE-Puppet。然后可以在 root 用户下启动 PE 服务。默认情况下,这将每 30 分钟联系一次 Puppet 服务器,或者可以通过运行puppet agent -t命令手动触发。

图 10.3 显示了该 Puppet 证书过程的工作流,客户端确保其拥有签名的 SSL 证书以确保与 Puppet 服务器的安全通信:

https://github.com/OpenDocCN/freelearn-devops-pt3-zh/raw/master/docs/ppt8-dop-engi/img/B18492_10_03.jpg

图 10.3 – Puppet 客户端证书工作流

第一步是验证证书。在ssl目录/etc/puppetlabs/puppet/ssl中,以下文件将已经存在或在此过程中创建:

  • private_keys/<certificate_name>.pem:用于创建 CSR 的私钥

  • certs/<certificate_name>.pem:返回的为该客户端签署的证书

  • certs/ca.pem:从 Puppet 服务器发送的 CA 证书副本

  • crl.pem:来自 Puppet 服务器的 CRL

  • certificate_requests/<certificate_name>.pem:将发送到 Puppet 服务器的 CSR,在收到签署的证书后将被删除

除了此目录,还可以创建一个/etc/puppetlabs/puppet/csr_attributes.yaml文件,并在其中包含将被包含在 CSR 中的受信事实。这将导致在 Puppet 服务器签署 CSR 时,受信事实被包含在客户端的证书中。

使用受信事实可以确保硬性分类信息不会被更改,例如将生产服务器重新分类为开发环境,或更改角色,因为这两者都可能导致安全性降低。组织 IDOID)号码转换为名称,可以在puppet.com/docs/puppet/latest/ssl_attributes_extensions.html查看。此文件必须在创建 CSR 之前存在;否则,唯一的方法是重新开始来更改 CSR 或证书。

图 10.3所示,如果私钥不存在,客户端会在检查本地的ca.pemCRL.pem副本之前,先生成一个新的密钥,并向服务器发起请求,若这两者中的任何一个不存在,则进行下载。接下来,客户端会检查是否存在已签名的证书,如果没有,则向客户端请求该证书。如果存在已签名的客户端证书,客户端可以继续请求节点数据;否则,客户端会创建一个 CSR 文件并将其发送到主服务器。如果在puppet.conf中启用了waitcert设置,客户端将等待 CSR 由服务器签名,并每 2 分钟检查一次主服务器的状态。在未来的运行中,客户端会向服务器提供其已签名的证书,以证明其身份。

在确保了安全通信之后,第一步是从服务器到客户端执行插件同步,确保所有的事实、功能、资源类型、资源提供者和 Augeas 镜头都通过file_metadata端点下载到客户端。

一旦完成此步骤,客户端运行facter,将输出发送到 Puppet 客户端,并通过\catalog端点向 Puppet 服务器请求目录。该目录的副本将存储在客户端的cache目录中(通过在puppet.conf中配置vardir参数)。默认情况下,路径为%PROGRAMDATA%\PuppetLabs\puppet\cache\client_data\catalog\<certname>.json。(PROGRAMDATA通常是C:\Program Data\,在 Linux 和 Unix 系统上为/opt/puppetlabs/puppet/cache/client_data/catalog/<certname>.json。)客户端接收此目录并执行步骤,强制执行 Puppet 代码中描述的状态,或者如果客户端设置为以无操作模式运行,则模拟该目录。客户端生成报告,并默认通过report端点将其发送回 Puppet 服务器。此操作可以配置为将报告发送到其他报告处理器,例如 Splunk,这将在第十三章中讨论。

除了client_data文件夹中的目录外,还会在cache目录中生成其他几个用于调查的有用文件:

  • lib:这是由插件同步从主服务器同步的各种插件的缓存。

  • facter:此文件将包含自定义事实。

  • facts.d:在这里,外部事实由插件同步从主服务器缓存。

  • reports:包含最后生成的报告文件。

  • state:此目录包含与先前 Puppet 运行状态相关的文件和目录:

    • classes.txt:列出上次应用的目录中包含的类。

    • graphs:如果在 Puppet 运行期间使用了graph选项,生成的资源和依赖关系的.dot 图形文件将保存在这里。

    • last_run_report.yaml:这是一个完整的报告,列出所有资源以及它们在目录强制执行期间如何被检查或更改。

    • resources.txt:上次应用的目录中包含的资源列表

    • state.yaml:所有资源的列表及其上次检查或同步的时间,用于诸如audit等功能

一些目录和文件已被忽略,因为它们要么是为了遗留目的,要么是为了本书不推荐的做法,例如filebucket。完整列表请参见puppet.com/docs/puppet/latest/dirs_vardir.html

注意

如果客户端与 Puppet 基础设施失去连接,缓存的目录将用于确保继续执行其最后已知的状态。

代理到服务器的最后一步是将事件报告发送到 Puppet Server。这些报告将反映目录中每个资源的事件。这些事件可能具有以下状态之一:

  • 失败 – 这是一个应用目录时出错的事件,或是如依赖关系问题或该特定资源的问题

  • 修正 – 资源在上次运行时是正确的,但必须修正

  • 有意的 – 资源必须创建或修正,但在上次运行时状态不正确

  • 未更改 – 资源处于正确状态,无需更改

默认情况下,未更改的事件不会在 Puppet 8 中报告。此更改是为了减少存储报告所需的存储空间。可以通过在每个代理的puppet.conf文件中设置exclude_unchanged_resources=false来更改此设置。

报告事件还将反映客户端代理运行的模式,或资源是否设置为与客户端的应用方式不同。尽管相同的事件状态仍然适用,但每个事件将报告该事件是在执行模式下发生,还是在无操作模式下发生。如在第三章中所讨论的,无操作模式意味着资源只是有效地测试,看资源是否需要更改以符合声明的状态。在第十五章中,我们将讨论如何在遗留环境中使用此方法,查看配置漂移的大小,并选择逐步的方法,以避免在生产系统中造成问题。

关于访问这些报告,我们将在第十三章中看到如何使用报告处理器将其发送到第三方工具,并在第十四章中看到 Puppet Enterprise 如何作为其图形控制台的一部分提供事件查看器界面。

实验室 – 监控证书签名日志

为了更好地理解这个过程,我们将描述如何通过删除节点的证书并重新注册来监控 Puppet 运行的过程。在注册过程中,我们将监控日志以查看此过程中的 API 请求,并记录过程步骤。以下是步骤:

  1. 打开 SSH 终端会话到 Linux 客户端,并为主 Puppet 服务器打开两个独立的 SSH 终端会话。

  2. 在 Linux 客户端上,运行以下命令:

    puppet ssl clean
    
  3. 在其中一个服务器会话中,运行puppetserver ca clean --certname <实例名称>(请注意,这应该是证书名称,可以通过在节点上运行puppet config print certname来检查)。

  4. 在 Linux 客户端上,使用以下命令将ssl目录移动到备份位置:

    mv /etc/puppetlabs/puppet/ssl /etc/puppetlabs/puppet/ssl.old
    
  5. 在其中一个 Puppet 服务器会话中,运行tail -f /var/log/puppetlabs/puppetserver/puppetserver-access.log,而在另一个会话中,运行tail -f /var/log/puppetlabs/puppetserver/puppetserver.log

  6. 在节点上运行puppet agent -t并查看 Puppet 服务器会话中的调用。

  7. 在 Web 控制台中,使用客户端上的puppet agent –t。注意服务器中的access.logpuppetserver.log文件中的新调用,并了解这些如何与本节中讨论的步骤相关。

  8. 查看为客户端接收到的目录,并检查缓存中的其他文件。

提示

使用像jq这样的工具可以使查看 JSON 更加轻松 (stedolan.github.io/jq/download/)。

要查看此实验的日志输出示例,请参见以下文件:

github.com/PacktPublishing/Puppet-8-for-DevOps-Engineers/blob/main/ch10/puppet_access_log_extract 显示了带有注释的访问日志,解释了输出内容

github.com/PacktPublishing/Puppet-8-for-DevOps-Engineers/blob/main/ch10/puppet_server_log_extract 显示了 Puppet 服务器日志,带有注释,解释了输出内容

github.com/PacktPublishing/Puppet-8-for-DevOps-Engineers/blob/main/ch10/puppet_client_terminal.txt 显示了客户端终端和输入的命令

github.com/PacktPublishing/Puppet-8-for-DevOps-Engineers/blob/main/ch10/puppet_server_terminal.txt 显示了服务器终端和输入的命令

PuppetDB 和 PostgreSQL

PuppetDB 允许收集 Puppet 数据和一些高级功能,如导出的资源。在开源 Puppet 中,它是完全可选的,而 PE 默认安装 PuppetDB。以下是 PuppetDB 保存的内容:

  • 来自节点的最后事实

  • 为每个节点编译的最后一个目录

  • 每个节点的事件报告默认为 14 天

  • 导出的资源

PuppetDB 是一个运行在 JVM 上的 Clojure 前端应用程序,使用 PostgreSQL 作为后端数据库。这种常见架构是后端数据库只提供表格,而前端数据库包含应用程序对象,相较于单一数据库,这具有一些关键优势。它简化了 PuppetDB 的更新过程,因为实际数据可以保留在后端表中,并且它还允许良好的可扩展性——正如我们将在本章的最后一节中看到的,通过编译器扩展——PuppetDB 可以通过在多个编译服务器上运行 PuppetDB 进行水平扩展,从而减轻主服务器 PuppetDB 服务的负载。

有关 PuppetDB 安装和配置的信息,请参阅forge.puppet.com/modules/puppetlabs/puppetdb。PuppetDB 可能会包含在您选择的任何自动化工具中,并且是 PE 的一部分。

PostgreSQL 为 PE 创建一个pe-postgres用户,或为开源 Puppet 创建一个postgres用户,该用户用于运行 PostgreSQL 数据库。此用户将使用nologinshell 并拥有运行 Postgres 所需的相关文件。PostgreSQL 使用以下目录:

  • /opt/puppetlabs/server/apps/postgresql/{version}:用于安装数据库应用程序

  • /opt/puppetlabs/server/data/postgresql/{version}:用于存储数据库的数据文件

  • /var/log/puppetlabs/postgresql/{version}:用于存储数据库的日志

PuppetDB 为 PE 创建一个pe-puppetdb用户,或为开源 Puppet 创建一个puppetdb用户,该用户用于在nologinshell 下运行 PuppetDB 数据库,并拥有运行 PuppetDB 所需的相关文件。由于 PuppetDB 是一个运行在 JVM 上的 Clojure 应用程序;它在结构上与 Puppet Web 服务器非常相似,具有挂载在/pdb端点的处理程序,并且通过auth.conf文件定义谁可以访问此端点。PuppetDB 使用以下目录,并突出显示了一些关键文件:

  • /etc/puppetlabs/puppetdb:此目录包含 PuppetDB 的配置文件,包括以下内容:

    • bootstrap.confbootstrap.conf文件列出了应在 Trapperkeeper 框架中启动的服务
  • /etc/puppetlabs/puppetdb/conf.d:此目录包含ini格式的配置文件:

    • auth.conf:配置谁可以访问已公开的端点

    • routing.ini:配置哪些处理程序应在端点处公开

  • /opt/puppetlabs/server/apps/puppetdb:此目录包含 PuppetDB 的应用程序二进制文件

  • /opt/puppetlabs/server/data/puppetdb:此目录包含 PuppetDB 的数据

本书的范围不包括深入探讨PuppetDB的配置,但你可以参考puppet.com/docs/puppetdb/latest/configure.html获取更多信息。然而,在第十三章中,我们将更深入地探讨如何监控、审查和优化 PuppetDB 和 PostgreSQL 性能,以及如何使用像forge.puppet.com/modules/puppetlabs/pe_databases这样的模块来帮助维护。

目前,我们将回顾如何通过 PQL 和 HTTP 调用访问数据,使用/pdb端点,或者通过puppet query命令行调用端点。

注意

抽象语法树AST)查询语言也可以作为查询格式使用。然而,随着 PQL 的使用,它现在几乎没有用处,但可以通过www.puppet.com/docs/puppetdb/8/api/query/v4/ast.html查看。

PuppetDB 被结构化为多个实体,以便访问不同类型的数据。以下是每个实体的列表以及它所包含的端点简要描述:

  • aggregate_event_countsevent_counts实体的汇总计数

  • catalogs:每个节点存储的目录

  • edges:边是目录中关系信息,如包含依赖

  • environments:PuppetDB 已知的环境

  • event_counts:报告中关于各个资源的事件计数

  • events:事件反映了报告中执行的资源操作

  • facts:每个节点返回的事实

  • fact_contents:此实体结构化为更方便地访问事实内容

  • fact_names:所有已知的事实名称

  • fact_paths:类似于fact_names实体,但为结构化事实提供了进一步的粒度

  • nodes:节点信息

  • producers:生成器是编译目录并发送报告的服务器

  • reports:报告包含应用目录的结果

  • resources:目录中的资源信息

要开始查看 PQL 查询,最简单的方式是返回实体中的所有数据。这可以通过简单列出实体名称和空的大括号来完成。例如,要返回所有节点数据,可以使用nodes {};要在大括号中查找具有特定参数的节点,使用属性名称及其应等于(=)、包含(~)、小于(<)或大于(>)的值。例如,要返回最后报告状态未改变的节点,查询为nodes { latest_report_status = "``unchanged"}

我们不会列出这些查询的输出,因为它们可能非常冗长,但你将在本节末尝试在实验中制作一些示例。

这些属性语句可以通过 ! 进一步否定,使用 and/or 链接,并用括号 () 括起来以包含不同的语句。例如,要进行更复杂的查询,查找某个文件是否以错误的权限声明,我们可以运行此 PQL 查询:

resources { (type = "File" and title = "/etc/motd") and ! ( parameters.mode = "0644" and parameters.owner ="root") }

在命令行中,这也可以通过 puppet query resource {'latest_report_status = "``unchanged"}' 来运行。

PuppetDB 查询还可以在 Puppet 代码中使用 PuppetDB 函数。以下是一个示例:

$changed_nodes = puppetdb_query(node[certname]{ resource {'latest_report_status = "unchanged"}}) .map |$value| { $value["certname"] }
notify {"Nodes changed":
    message => "The following nodes changed on their last run ${join($changed_nodes, ', ')}",
}

在所有这些示例中,假设证书已经设置,以便通过 Puppet 基础设施或运行查询的客户端进行安全的 SSL 通信。如果使用默认位置,puppet query 命令会自动拾取证书,但也可以像这样进行设置:

puppet query '<PQL query>' \
  --urls https://puppetdb.example.com:8081 \
  --cacert /etc/puppetlabs/puppet/ssl/certs/ca.pem \
  --cert /etc/puppetlabs/puppet/ssl/certs/<certname_of_local_host>..pem \
  --key /etc/puppetlabs/puppet/ssl/private_keys/<certname_of_local_host>..pem

Web 点也可以通过 curl 或等效的命令访问,如下所示:

curl -X GET <fqdn_of_puppetDB_host>https://<fqdn_of_puppetDB_host>:8081/pdb/query/v4\
  --tlsv1 \
  --cacert /etc/puppetlabs/puppet/ssl/certs/ca.pem \
  --cert /etc/puppetlabs/puppet/ssl/certs/<certname_of_local_host>.pem \
  --key /etc/puppetlabs/puppet/ssl/private_keys/<cert_name_of_local_host.pem \
  --data-urlencode 'query=<PQL query>'

为了允许从桌面或其他节点直接进行查询,可以使用 Puppet 客户端工具。关于在 Open Source Puppet 上安装的设置说明详见puppet.com/docs/puppetdb/latest/pdb_client_tools.html,而 Puppet Enterprise 的安装说明可参考www.puppet.com/docs/pe/2021.7/installing_pe_client_tools.html

另外,可以通过按照puppet.com/docs/puppetdb/latest/configure.html#jetty-http-settings中的说明,禁用 SSL 身份验证,以允许未经身份验证的查询。本书强烈建议不要这样做,因为这会使网络上的任何人都能访问数据。

在本节中,我们展示了一些可以与 PQL 一起使用的实体和查询。虽然逐一列举这些实体的所有可能选项以及 PQL 可用的选项范围是不现实的,但完整的详细信息可以在文档中查看:puppet.com/docs/puppetdb/latest/api/query/v4/entities.html。此外,更多的 PQL 查询示例可以在文档中查看:puppet.com/docs/puppetdb/latest/api/query/examples-pql.html,而 Vox Pupuli 社区正在其网页上建立有用的示例,网址为voxpupuli.org/docs/pql_queries/

实验 – 查询 PuppetDB

SSH 连接到主服务器并查询 PuppetDB 获取以下信息:

  • 列出所有编译器服务器的内存大小(提示:编译器服务器都有一个受信任的事实,Facter 也有内存事实)。

  • 列出在 Puppet 服务器上强制执行的所有服务。

  • 列出每个服务器最新报告的开始和结束时间。

示例答案可以在github.com/PacktPublishing/Puppet-8-for-DevOps-Engineers/blob/main/ch10/PQL_samples_answers.txt找到。

注意

在大规模生产系统中使用这些查询时要小心;某些端点(如报告)可能包含大量数据,查询可能会给系统带来很大压力和负载。

使用编译器进行扩展

  1. 到目前为止,Puppet 平台组件的回顾假设所有组件都位于单一的主服务器上。然而,随着托管节点数量的增加,单一服务器处理这些节点变得不切实际。根据 Puppet 的文档,默认设置下,主服务器最多可以管理 2,500 个客户端。为了处理日益增长的节点数量,Puppet 采用了水平扩展,使用了 Puppet 编译服务器。在图 10.4中,显示了一部分主服务被移到编译服务器上。这些服务器可以在客户端的配置文件中配置为轮询选择,或放置在负载均衡器后面。这使得多个节点可以协同工作来编译目录,同时仍然允许某些服务在主服务器上运行。根据 Puppet 的文档,在默认的编译器设置下,每个编译器最多可以服务 3,000 个客户端:

https://github.com/OpenDocCN/freelearn-devops-pt3-zh/raw/master/docs/ppt8-dop-engi/img/B18492_10_04.jpg

图 10.4 – Puppet 编译器服务

编译服务器托管着主服务器上存在的一部分服务,例如 Puppet Server 和 PuppetDB。这使得能够远程完成和同步目录编译请求,从而增加了编译目录所需的 JRuby 实例数量。

  1. 将客户端请求指向编译服务器的最常用方法是利用硬件或基于云的负载均衡器。由于有多种负载均衡器可供选择,Puppet 并未提供明确的配置指导。然而,它建议使用/status/v1/simple端点来检查编译服务器的健康状况。如果负载均衡器不支持 HTTP 健康检查,可以检查主机是否在端口8140上侦听 TCP 连接,这可以提供有限的检查。

  2. 还有一些替代负载均衡器的方法,例如使用 DNS SRV 记录,详细信息可以参考puppet.com/docs/puppet/latest/server/scaling_puppet_server.html#using-dns-srv-records,或者使用具有轮询设置的 DNS 条目,详细信息可以参考puppet.com/docs/puppet/latest/server/scaling_puppet_server.html#using-round-robin-dns,但由于这些方法较少使用,本书不会详细讲解。

注意

puppet.conf文件中,可以将多个服务器列表添加到客户端服务器值中,以便联系,但该列表仅在发生故障时有效,并不会尝试平衡连接。

  1. 对于编译服务器,CA(证书颁发机构)仍然保留在单一的 Puppet 主服务器上,并在客户端发送其 CSR 或证书进行检查时进行回溯。

  2. 正如本章开头所述,我们将避免详细讨论安装过程,因为这对 Puppet 自带的说明并没有太大帮助,相关说明可以在puppet.com/docs/puppet/latest/server/scaling_puppet_server.html(开源版本)和puppet.com/docs/pe/2021.7/installing_compilers.html(PE 版本)中找到。然而,必须注意,如果在 TCP 代理模式或 DNS 轮询方法中使用负载均衡器,则编译服务器可能需要在其puppet.conf文件中添加dns_alt_names。这是为了启用所有可能在负载均衡器请求中使用的服务器名称。

  3. 即使启用了负载均衡器,也可以通过运行puppet agent -t server=<server to send request>直接定向到编译服务器。

  4. 第十三章中,我们将提供有关如何监控和管理服务器设置以实现可扩展性的更详细信息,而在第十四章中,我们将讨论 Puppet 的参考架构以实现可扩展性。然而,重要的是要注意,如果编译服务器距离主服务器过远,可能会出现延迟问题。因此,建议根据最佳实践将它们保持在同一区域内(云计算术语中)。

实验 – 查看编译器和负载均衡器配置

部署的实验环境由三个编译服务器组成。你可以查看它们正在编译的报告以及 pecdm 如何配置负载均衡器,具体如下:

  1. 登录到网页控制台并查看 Puppet 实例服务器的报告运行。在报告的Metrics部分,查找Report submitted by部分,并注意这可能在不同报告中有所不同。如果报告数量较少,请进入Jobs部分,并多次运行 Puppet,以生成更多报告。

  2. 查看 PECDM 如何在 Terraform 模块中创建 Azure 负载均衡器,网址为github.com/puppetlabs/terraform-azure-pe_arch/blob/main/modules/loadbalancer/main.tf

总结

在本章中,我们了解了 Puppet 服务器提供的服务以及嵌入式 Web 服务器如何将处理程序附加到挂载点,这些挂载点可以通过 HTTP 请求访问。

展示了/puppet端点为配置请求提供服务,演示了 indirectors 或环境如何请求特定的组件,例如从服务器请求目录。/puppet-ca端点同样通过 indirectors 允许向 CA 发出请求。接着展示了/puppet-admin-api端点,允许清除环境缓存和 JRuby 实例,这是更高级的管理操作。

接着展示了 Puppet 如何创建一个包含根 CA 和中间 CA 的 CA 服务器来签名证书,或者可以在传统模式下运行,使用单一合并的 CA。然后讨论了使用外部提供的证书的选项。展示了证书请求签名的过程,使用puppetserver certificate命令管理证书和请求,使用puppet ssl命令管理代理证书管理。接着展示了如何通过自动签名来自动化这个过程,可以根据命名规则或通过运行脚本来自动签署所有请求。

讨论了 JRuby 解释器,展示了 JRuby 是 Ruby 在 Java 上的实现,能够以可扩展和并发的方式运行 Puppet 的 Ruby 组件,比如编译 Puppet 代码。

展示了用户、服务、配置文件和日志的概述,检查了puppet.conf的服务器端配置,并展示了如何配置和查看文件中的设置,以及如何使用puppet config命令查看默认值。

在回顾了 Puppet Server 的组件之后,接下来查看了 Puppet 客户端生命周期,了解了代理如何向 CA 发出 CSR 请求,并发送事实信息和目录请求。查看了日志,展示了请求的位置以及如何通过请求进行追踪。展示了如何通过puppet.conf配置客户端,以及如何向 CSR 添加额外的信息。

接着探讨了 PuppetDB 和 PostgreSQL,作为前端/后端数据库架构,能够存储通过应用 Puppet 目录生成的报告,以及来自节点的最新事实和事件。我们回顾了文件目录和日志位置,接着查看了如何使用 PQL 在 API、命令行和 Puppet 代码中查询 PuppetDB。

接着展示了编译器如何允许 Puppet Server 水平扩展,支持将 Puppet Server 和 PuppetDB 服务部署到多个服务器上,并为客户端进行负载均衡。

在下一章中,我们将展示 Puppet 如何对请求目录编译的客户端进行分类,以便它知道应用哪个版本的代码以及哪些类。我们将展示如何通过环境使多个版本的代码共存于主服务器,并展示如何使用控制库来管理应该包含的模块和版本。

第十一章:分类与发布管理

本章的重点将是 Puppet 如何部署代码,并将这些代码分类到服务器上。首先将讨论环境,展示如何创建具有特定模块版本的服务器隔离组。我们将讨论如何提供静态和临时环境。我们将展示现代 Puppet 如何使用基于目录的环境,将环境代码放在特定位置,如 site.pp 主清单文件或一组清单文件,并通过这些节点定义中的 Hiera 查找,或者通过主服务器运行的 外部节点分类器 (ENC) 脚本来实现。还将讨论 Puppet Enterprise 中 分类服务 的实现,展示如何在这些解决方案的基础上构建,使用其自身的 ENC 脚本,并增加了在 Web 控制台中使用节点组的额外功能。

将详细查看 Puppet 代理的运行,展示其中的步骤,以及在编译目录时,数据是如何加载、缓存和刷新。

接下来,将展示如何使用控制库结构和 Puppetfiles 管理模块,以便使用 r10kg10k 将代码部署到环境中,并讨论根据本地基础设施的配置使用不同方法来同步代码。然后将讨论特定于 PE 的实现 r10k

在审视了分类与发布管理的技术结构后,将重点放在使用这些技术与受监管流程和多个团队合作时面临的挑战与局限性。

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

  • Puppet 环境

  • 理解节点分类

  • Puppet 运行

  • 管理和部署 Puppet 代码

  • 实验—分类与部署代码

技术要求

github.com/puppetlabs/control-repo 克隆控制库到你的 controlrepo-chapter11 GitHub 账户,并更新此库中的以下文件:

Puppet 环境

Puppet 环境是一种定义用于服务器组的特定版本模块、清单和数据的方法。不幸的是,环境是一个在组织中用于其他目的的通用技术术语,很容易造成混淆。最好的建议是在讨论 Puppet 之外的内容时,始终使用Puppet 代码环境,以防止 Puppet 环境与其他任何东西直接关联。

现代 Puppet 环境是基于目录的动态环境,这意味着 Puppet 服务器——或者在puppet apply的情况下,客户端——将查找分配的环境是否存在于一个目录中。多个变量设置了相关目录的位置,包括environments目录本身,我们强烈建议将所有这些设置保持为默认值,以避免混淆和问题。接下来我们将了解环境中的代码目录和路径的层级。

环境目录和路径

第一层是由puppet.conf中的codedir变量设置的代码和数据目录,默认值为 Unix 上的/etc/puppetlabs/code,Windows 上的%PROGRAMDATA%\PuppetLabs\code(通常为C:\ProgramData\PuppetLabs\code)。Puppet Server 不使用puppet.conf中的codedir设置,而是使用puppetserver.conf中的jruby-puppet.master-code-dir,因此如果更改了这两个设置,都需要进行配置。

注意

Puppet 3.3之前,环境是通过puppet.conf文件声明的,每个环境都必须在一个包含modulepathmanifests变量的节中声明。今天的 Puppet 仍然可以技术性地实现这一点,如果没有设置codedir,但没有理由采用这种方式。

代码和数据目录包含两个目录。首先,有一个模块目录,用于提供在puppet.conf中默认的basemodulepath变量中包含的全局用户模块。默认情况下,basemodulepath变量在 Unix 上包含$codedir/modules:/opt/puppetlabs/puppet/modules,在 Windows 上包含$codedir\modules。Unix 上的额外目录由 PE Server 安装使用,用于放置用于配置 PE 的模块。这些模块以pe为前缀,以避免与环境中已经使用的任何模块混淆。

第二个目录是环境目录;根据puppet.confenvironmentpath的默认设置,它是$codedir/environments,并且是查看环境的地方。

注意

codedir目录用于包含全局 Hiera 数据和配置,并且默认使用hiera_config设置。如果找到$codedir/hiera.yaml文件,它将覆盖默认的$confdir/hiera.yaml文件,这个文件现在是标准的,正如在第九章中讨论的那样。

environments目录中,要创建的每个环境都会有一个包含小写字母、数字和下划线的名称的目录。每个环境目录可以包含以下内容:

  • $modulepath指定的目录中的 Puppet 模块

  • 目录中hiera.yaml文件中配置的 Hiera 数据

  • $manifest指定的目录中清单或一组清单中的分类数据

  • 目录中的environment.conf文件中的环境配置数据

在回顾了环境的目录和路径之后,我们将更详细地查看环境配置文件。

环境配置文件

可以在environment目录中的environment.conf文件中设置环境配置数据;该文件具有类似于puppet.conf的 INI 格式,但没有节(sections)。

默认情况下,如果modulepath环境变量没有在environment.conf中设置,它将被设置为$environmentpath/$environment/modules:$basemodulepath

因此,在基于 Unix 的系统中,默认情况下将是以下内容:

/etc/puppetlabs/code/environments/$environment/modules: /opt/puppetlabs/puppet/modules

在 Windows 系统中,它将是这样的:

C:/ProgramData/PuppetLabs/code/environments/production/modules;C:/ProgramData/PuppetLabs/code/modules

请记得使用分号(;)分隔 Windows 系统中的目录列表,使用冒号(:)分隔 Unix 系统中的目录列表。

管理和部署 Puppet 代码部分,我们将讨论如何将模块部署到该目录中,并如何列出modulepath中每个目录的内容。

注意

永远不要将modulepath变量设置为从另一个环境目录读取。在Puppet 运行部分,我们将讨论环境数据被缓存和刷新时可能带来的不一致效果。

manifest变量可以是单个清单文件,也可以是包含多个清单的目录,这些清单将按字母顺序读取。如果路径以正斜杠(/)或句点(.)结尾,Puppet 会将此变量视为包含目录,并能识别它是一个目录。如果environment.conf中没有设置,默认值将是$environmentpath/$environment/manifests目录,对于基于 Unix 的系统,这个路径为/etc/puppetlabs/code/environments/$environment/manifests,对于基于 Windows 的系统,这个路径为C:/ProgramData/PuppetLabs/code/environments/$environment/manifests。目录环境将永远不会使用puppet.conf中的全局manifest设置。在下一节中,我们将更详细地讨论如何使用节点定义和 Hiera 查找来对这些清单进行服务器分类。

environment_timeout变量表示 Puppet Server 将缓存特定环境的时间,并覆盖设置的值。Puppet 建议不要在environment.conf中设置此项,只使用puppet.conf中的全局版本,并且只使用0unlimited。缓存的作用将在本章的Puppet 运行部分进一步讨论。

config_version变量可以设置一个脚本,在目录编译后运行,并将输出作为日志的一部分返回。如果默认没有设置,脚本将返回目录编译时的时间,格式为 Unix 纪元(自 1970 年 1 月 1 日午夜 UTC/GMT 以来经过的秒数)。对于默认的纪元脚本,输出将如下所示:

Info: Applying configuration version '1663239677'

管理和部署 Puppet 代码部分中将展示一个更有用的示例,当使用基于 Git 的部署解决方案时。

注意

environment.confconfig_version脚本可以使用basemodulepathenvironmentcodedir全局变量。

现在我们已经审查了环境配置,了解如何验证配置以及部署的环境类型是非常有用的。

环境验证和部署

可以使用puppet config print命令检查在puppet.confenvironment.conf中讨论的设置,通过部署--environment标志查看特定环境,使用--section查看puppet.conf中的特定部分。例如,要检查puppet.conf中的codedir变量和生产环境中的modulepath变量,可以运行以下命令:

puppet config print codedir
puppet config print --environment production modulepath

默认情况下,Puppet Server 会创建一个生产环境,但运行apply的 Puppet 客户端不会。对于这两种情况,生产环境是 Puppet 默认运行的环境。在本章的下一部分,我们将展示服务器如何被分类到其他环境中。

有三种环境策略:永久性、临时性和组织隔离。永久性环境通常是长期存在的,环境命名通常与服务器的用途相匹配,例如服务器是产品服务器还是开发服务器。临时性环境是在进行变更测试后推广之前可以使用的环境,而组织隔离环境则反映了分割的基础设施,其中不同团队(如 Windows 和 Linux 团队)拥有不同的服务器并且有不同的环境。这些策略可以根据需要结合使用,以满足组织的需求。

既然我们已经了解了 Puppet 代码环境,接下来我们将学习如何根据环境中的使用情况以及该环境中的模块集合来分类客户端。

理解节点分类

节点的分类涉及确定一个节点应该使用哪个环境,应该应用哪些类,以及应该应用哪些参数。理想的情况是为一个主机应用单一的角色类,但业务逻辑可能更复杂。这适用于 Puppet Server 上的代理运行和puppet apply运行。

在定义了什么是节点分类之后,我们将看看可以用于分类的方法,首先介绍节点定义作为最简单的方法。

节点定义

节点分类的最基本方法是使用puppet.conf,其中节点名称与puppet.conf中的certname设置相同,默认为节点的完全限定域名 (FQDN)。

节点定义的语法在这里设置:

  • node关键字

  • 一个作为字符串的节点名称,default

  • 以下 Puppet 代码项的混合,位于花括号({})内:

    • 类别声明

    • 变量

    • 资源声明

    • 收集器

    • 条件语句

    • 链接关系

    • 函数

建议将节点定义控制在最低限度,并仅使用类声明和变量。如果任何清单包含节点定义,则节点定义必须匹配所有节点,否则与不匹配的节点的编译将失败。通常通过确保存在默认定义,即使默认定义不包含任何代码,也能确保安全。

一个节点将只匹配一个节点定义,并按以下优先级进行排序:

  • 完全匹配的名称

  • 正则表达式匹配(多个正则表达式匹配是不可预测的,只有一个会被使用)

  • default(如果节点未能匹配任何其他定义,节点将匹配此关键字)

注意

default之前的优先级步骤会查找主机名的部分匹配,如果puppet.conf主服务器中的strict_hostname_checking设置为false。为了避免这种不安全的匹配,Puppet 5.5.19+ 和 6.13.0+ 默认设置为true,在 Puppet 7 及之后的版本中,已删除该选项。

例如,以下代码将把server1.exampleapp.com分类到role::oracle类别,将server2.exampleapp.comserver3.exampleapp.com分类到role::apache类别。其他以exampleapp.com结尾的服务器将根据操作系统系列分类为role::example_common_windowsrole::example_common_linux,例如server5.exampleapp.com,其他节点将被分类为role::common,例如server1.anotherapp.com

node /.exampleapp.com$ {
  if $facts['os']['family'] {
    include role::example_common_windows
  else
    include role::example_common_linux
  }
}
node 'server1.exampleapp.com' {
  include role::oracle
}
node 'server2.exampleapp.com','server3.exampleapp.com' {
  include role::apache
}
node default {
  include role::common
}

默认情况下,manifest目录中会有一个site.pp文件,以保持简单,但该目录中的多个清单可以包含节点定义,这些定义可以根据组织、用例或所有权来组织文件。显然,拥有大量节点定义并不适用;保持节点定义简洁的推荐方法是使用一个默认定义,该定义查看节点的证书以具有一个pp_role扩展名,包含角色名称,如此代码示例所示:

node default {
  $role = getvar('trusted.extensions.pp_role')
  if ($role == undef) {
    fail("${trusted['certname']} does not have a pp_role trusted fact")
  }
  elsif (!defined($role)) {
    fail("${role} is not a valid role class")
  }
  else {
    include($role)
  }
}

使用getvar函数来避免没有证书的主机出现问题,并使用defined函数确认声明的角色在环境中可见,它将包括证书中声明的角色。

任何在节点定义外应用的代码将适用于所有节点,但像这样设置不受控的全局默认值并不是推荐的做法。在之前的代码块中,使用了角色类,但也可以为例外包含任何类。

本地的 puppet apply 调用将不会查找 puppet.conf 中的 manifest 变量设置,而是会根据命令行传递的内容进行操作,可以通过 –e 标志或传递特定的清单文件来实现。

在查看了基于代码的节点分类方法后,我们现在将查看如何使用 Hiera 数据来对节点进行分类。

使用 Hiera 进行节点分类

可以使用 Hiera 数组和 lookup 函数在默认节点定义中采取更具数据驱动的方法。虽然 lookup 函数可以在节点定义外使用,但我们建议避免这样做,以确保如果为节点特别添加了其他节点定义,它只会应用该节点定义,而不是更难预测的混合结果。

第一步是,如我们在 第九章 中看到的,确保每个环境中有适当的 Hiera 层次结构,假设在 hiera.yaml 环境中有一个简单的层次结构,包括节点、操作系统和默认设置,如此处所示:

datadir: data 
data_hash: yaml_data 
  - name: "Node data" 
    path: "nodes/%{trusted.certname}.yaml"
  - name: "OS defaults" 
    path: "os/%{facts.os.family}.yaml" 
  - name: "Common data" 
    path: "common.yaml

然后,我们可以在 default 节点定义中添加查找:

node default {
lookup( {
  'name'          => 'classes',
  'value_type'    => Array,
  'default_value' => [],
  'merge'         => {
    'strategy' => 'unique',
  },
} ).each | $classification | {
  include $classification
}

虽然将变量命名为 class 看起来更合适,但由于 class 是一个保留字,因此无法这样做。

环境级别的 Hiera 数据可以添加到 common.yaml 文件中,以确保默认情况下服务器获得 core 角色:

---
classes:
  - role::core

然后,在数据文件中创建一个 os/RedHat.yaml 文件,包含以下代码:

---
classes:
  - role::core::redhat

这将确保所有来自红帽家族的服务器,如 CentOS,将被分配到 role::core::redhat 类。要将特定角色分配给服务器,我们创建一个 node/exampleapp.example.com.yaml 文件,包含以下代码:

---
classes:
  - role::docker

这将把 role::docker 类分配给 exampleapp.example.com 节点。

为了允许例外和更复杂的组合设置,可以使用哈希而不是数组,将 site.pp 中的查找策略从唯一查找改为深度合并策略,并将数据从数组改为哈希:

node default {
lookup( {
  'name'          => 'classes',
  'value_type'    => Hash,
  'default_value' => []
  'merge' =>
    'strategy' =>  'deep',
}).each | $classification | {
  include $classification
}

在这种情况下,我们可以使用仅在 Hiera 中可见的键,接管角色构建并直接使用配置文件,设置一个 common.yaml 文件以确保默认分类获得核心配置和安全配置文件:

---
classes:
base profile: profile::core
security profile: profile::security

然后,对于特定的服务器 exampleapp.example.com,可以在 node/exampleapp.example.com.yaml 中设置 security_profile 变量:

---
classes:
security_profile: profile::security::legacy

这将覆盖安全配置文件键,并导致 exampleapp.example.com 被分类为 profile::security::legacyprofile::core

可以构建更复杂的基于 Hiera 的键查找来基于 Facter 值查找,但由于这在本书中不推荐使用,因此已展示足够的细节来理解 Hiera 的使用方法。值得一提的是,example42 的 psick 模块 forge.puppet.com/modules/example42/psick 使用了 Hiera 方法,并且可以用于在 Linux 环境中以预设和分阶段的方式包含模块。只需包含 psick 类并简单地通过哈希设置 Hiera 键,就足以对主机进行分类:

psick::firstrun::linux_classes
psick::pre::linux_classes
psick::base::linux_classes
psick::profiles::linux_classes

在详细审查了分类的代码和数据方法后,我们将介绍使用 ENC 脚本的更高级方法。

ENC 脚本

ENC 是一个脚本,Puppet Server 或 puppet apply 调用可以运行该脚本。该脚本的要求是接收客户端的 certname 参数,并返回一个非零的返回码(表示未知节点)或包含类、参数和环境的 YAML 输出,用于目录编译。在这个 ENC 内部,可以访问各种外部数据源引用,例如 PuppetDB 或你组织的内部数据源。重要的是 ENC 使用的编程语言,而不是它用什么语言编写。

一个示例输出如下所示:

---
classes:
  role::core::windows
  sqlserver_instance:
    features:
      - SQL
    source: E:/
    sql_sysadmin_accounts:
      - myuser
parameters:
  dns_servers:
    - 2001:4860:4860::8888
     - 2001:4860:4860::8844
  mail_server: mail.example.com
  vault_enabled: true
environment: uat

在这个示例中,可以看到服务器将应用 role::core::windows 类,并且会使用 sqlserver_instance 类及其相关参数,这些参数将作为目录中的全局变量,并且环境为用户验收 测试UAT)。

通常最好通过 Hiera 数据传递类参数,但这只是为了演示在 ENC 输出中可以实现的内容。

要配置 ENC 脚本的使用,必须在 puppet.conf 中设置两个变量:首先是 node_terminus,默认值为 plain,只使用清单来定义分类。将 node_terminus 设置为 exec 后,第二个变量 external_nodes 将被检查,这应该设置为脚本的位置。例如,Foreman 项目使用一个在其配置模块中定义的 ENC,如下所示:

node_terminus = exec
external_nodes = /etc/puppetlabs/puppet/node.rb

脚本的内容可以在这里查看:github.com/theforeman/puppet-puppetserver_foreman/blob/master/files/enc.rb

用于放置此脚本的配置模块可以在 forge.puppet.com/modules/theforeman/puppetserver_foreman 找到。

开发 ENC 脚本超出了本书的范围,建议避免通过这种方式访问外部数据,因为访问可能会很昂贵。

我们已经介绍了 ENC 脚本的工作原理,但 PE 使用自己类型的 ENC 脚本,并具有额外的功能。

PE 分类器

PE 提供了自己的 ENC 分类器,访问分类服务 API,这是一个 Clojure 应用程序,并将节点组信息存储在 PostgreSQL 分类数据库中。

通过在 puppet.conf 中设置 node_terminus = classifier 来进行配置,安装程序已设置该项,并且不应更改,因为更改后将不受支持。

注意

node_terminus 在 PE 中曾在 PE 4 及之前版本中被称为 console

节点组有两种类型:环境组和分类组。环境组用于将环境分配给节点,而分类节点用于分配类并添加参数和变量。可以在 PE Web 控制台的 节点 部分查看和配置节点组。

所有节点组都可以包含规则,这些规则可以基于事实或通过直接命名要包含在节点组中的服务器来定义。它们可以包含任何带有任何定义的类参数的类,这些参数会被分类到这些匹配的节点中,这些参数被称为配置数据,像 Hiera 数据一样充当覆盖项,并优先于 Hiera,以及作为全局变量为组声明的变量。

注意

较旧版本的 PE 默认不启用配置数据,必须在 /etc/puppetlabs/puppet/hiera.yaml 中添加一个部分:

hierarchy: - name: "分类器配置数据" data_hash: classifier_data

默认情况下,如图 11.1所示,PE 将拥有一个 所有节点 节点组,作为所有配置的父节点组,并在其下分为 所有环境,一个作为所有声明的环境组的父组的环境组,以及 PE 基础架构,一个用于配置 PE 架构的分类组:

https://github.com/OpenDocCN/freelearn-devops-pt3-zh/raw/master/docs/ppt8-dop-engi/img/B18492_11_01.jpg

图 11.1 – PE 默认节点组

环境组在图 11.1中被标记为 trusted.extensions.pp_environment 事实,在规则中将生产或开发环境匹配到同名的组,并确保分配相应的环境。如果没有设置 trusted.extensions.pp_environmentpp_enviroment 受信事实将防止服务器被移动到另一个环境,而无需重新生成服务器证书,这将需要访问客户端和主服务器。命令为 puppet agent –``t --environment=myfeaturebranch

环境的开发和部署方法将在管理和部署 Puppet 代码部分进一步讨论,但可能需要在生产和开发之间增加更多环境层级,在这种情况下,推荐的做法是在所有环境下创建一个该环境名称的节点组,并创建一个规则,匹配 trusted.extensions.pp_environment 和您设置的环境名称。

环境组应保持简单,因此避免分配任何类参数或变量。

当分类组嵌套时,它们会继承父组的定义。在创建组结构时,从一般的配置层开始,然后将其细化到更具体的分类组是有意义的。例如,可以看到puppet_master_host被设置,这适用于所有 Puppet 基础设施主机,然后设置特定的服务和功能,如编译器或 PuppetDB,这将仅在部分节点上配置。

这可能会引起混淆,因为这种继承也适用于规则,因此,如果父规则已经设置了限制节点的规则,子节点组的规则将与父节点组的规则结合使用。这同样适用于节点的固定;您不能忽略规则并将任何可见于主服务器的服务器固定住。还需要注意的是,如果子节点组没有规则,它将不会应用分类,即使是从父组继承的分类。

关于分类节点组中环境变量的目的,可能会引起进一步的混淆;这并不是定义分配的类将从哪里运行,而是告诉节点组在哪个环境中查找可用的类名。如果节点组在开发和生产节点之间共享,并且新的类最初在开发环境中引入,然后再推广到生产环境,那么这可能会导致问题,因此通常情况下,应用节点组使用最低级别的环境来全面查看类名是最有意义的。

为了简化操作,建议使用直接的分类角色,这些角色作为所有节点的子角色,并仅通过将trusted.extensions.pp_role匹配到特定的类角色名称来设置规则,然后将该角色类分配给分类角色组。

为了自动化节点组的创建,可以使用node_manager模块(forge.puppet.com/modules/WhatsARanjit/node_manager)通过 Puppet 代码管理它们,这也是peadm模块本身配置 Puppet 节点组信息的方式。例如,peadm确保带有puppet/puppetdb-database可信扩展的节点被分配到PE 数据库节点组,代码如下:

node_group { 'PE Database':,
  rule => ['or',
    ['and', ['=', ['trusted', 'extensions', peadm::oid('peadm_role')], 'puppet/puppetdb-database']],
    ['=', 'name', $primary_host],
  ]
}

注意

node manager模块有一个purge_behavior设置,如果将资源的该设置为none,则确保仅应用您希望对节点组进行的特定更改。默认情况下,这个设置为all,会移除您未声明的任何设置。

另外,可以使用 API 执行节点组数据的备份和恢复,使用/classifier-api/v1/groups保存到文件,并使用/classifier-api/v1/import-hierarchy恢复。Peadm使用这些 API 实现备份和恢复分类任务:github.com/puppetlabs/puppetlabs-peadm/tree/main/tasks

注意

从 PE 版本 2019.2 开始,提供了一个$pe_node_groups顶级作用域变量,返回所有节点组。

使用Puppet 数据服务PDS)通过外部数据添加类的进一步方法将在第十三章中展示。但在回顾了各种分类方法后,我们将讨论最佳实践方法来对节点进行分类。

推荐方法

可以使用 ENCS 和节点定义方法的混合方式,因为它会合并信息,但这可能会使理解分类发生的位置变得更加困难。如果可能的话,最佳做法是选择一种方法,或者至少明确每种机制的目的,例如基于证书匹配角色的节点定义和匹配节点异常的 Hiera。

假设分类尚未由您的组织选择,或者在您的配置模型中是特定的,比如使用 Foreman 或psick,我们建议使用基于开源 Puppet 证书中pp_role扩展的默认节点定义的简单模式:使用与节点组角色匹配的pp_role扩展和与 PE 使用的环境匹配的pp_environment。这是 Puppet 支持所期望的,也是构建模型,但它限制了在 Hiera 数据设置中使用任何变量或配置数据。

节点定义使用 Hiera 对节点进行分类部分讨论了其他机制,因为在许多组织中,分类已经存在,并且不容易更改,因此必须理解它。如果必须生成复杂的分类,重要的是要知道这是否意味着数据没有放在正确的位置,或者——更糟糕的是——Puppet 没有得到有效使用,生产了过多的服务器变种。当我们维持严格的标准并尽量减少例外时,服务器可以轻松处置并重建,从而减少运营复杂性和支持团队的认知负担。

现在您已经理解了服务器如何被分类到环境和类中,我们将展示在 Puppet 运行期间如何加载和缓存不同的数据。

Puppet 运行

本节将详细介绍 Puppet 运行和分类的步骤。对于 Puppet 运行的情况,puppet apply命令应被视为 Puppet 服务器和客户端在同一节点上的等价物。

当客户端发出目录请求时,四项内容会被发送到服务器:

  • 节点名称

  • 节点的证书(未发送apply

  • Facts

  • 请求的环境

节点名称是 certname,并与请求的环境一起嵌入到 API 请求中——例如,/puppet/v3/catalog/exampleserver.example.com?environment=uat

证书可以包含扩展,这些扩展将被转化为可信的事实。

服务器接收到代理数据后,向配置的节点终端请求节点对象。在 plain 的情况下,这将是空白的;对于 execclassifier,将返回包含类、参数和环境的 YAML 输出。

默认情况下,puppet.confstrict-environment-mode 设置为 false,并且返回的环境将覆盖代理请求;如果设置为 true,则目录编译将失败。如果代理在 Puppet 执行过程中指定了环境,agent_specified_environment 事实将会出现。

变量将根据事实设置,既作为顶级作用域变量,也作为 $facts 哈希中的变量,将证书中的扩展作为 $trusted 哈希中的可信事实,以及从节点终端返回的参数作为顶级作用域变量。

主清单将被评估,首先查看它是否由环境配置定义,如果未设置,则由客户端的 puppet.conf 文件定义。如果存在任何节点定义,Puppet 将尝试匹配 certname,如果匹配失败,则编译将失败。

任何在节点定义之外的资源都会被评估并添加到目录中以及任何类中。如 节点定义 部分所述,不建议在节点定义之外声明任何内容。匹配的节点定义将评估代码,覆盖节点定义中声明的任何顶级作用域变量,将资源添加到目录,并加载并声明节点定义中的类。

Puppet 然后将加载包含在主清单中声明的类,使用为该环境配置的 modulepath 变量。每当加载一个类时,代码会被评估,资源会被添加到目录中,任何在其中声明的类也会被加载并评估。

Puppet 然后加载并评估从节点对象返回的类。

在了解了 Puppet 如何分类节点以及代理如何处理这些分类方法后,现在是时候查看如何管理和部署环境到主服务器,以便将正确版本的代码提供给节点。

管理和部署 Puppet 代码

默认情况下,只需创建文件夹并将模块内容放置到适当位置,再结合 puppet module install 命令从 Forge API 自动拉取,就足以使模块在环境中可见,并允许它们被打包到包管理中以创建版本。但我们并不推荐这种方法,因为它将模块和环境的部署集中化,很可能使得单个团队成为 gatekeeper。我们将看到控制库提供了更灵活的控制。

最常见的方法是使用一个名为控制仓库的 Git 仓库。Puppet 提供了这个仓库的模板,地址为 github.com/puppetlabs/control-repo

注意

Puppet Forge 作者 example42 提供了其自己的模板化控制仓库,用于与其集成和预设计的实现方法:github.com/example42/psick

Puppet 的控制仓库模板包含了本章第一部分中讨论的许多目录和文件,以及 Hiera 数据和一些特定于模块部署的附加文件。图 11.2 显示了 Puppet 控制仓库的内容:

https://github.com/OpenDocCN/freelearn-devops-pt3-zh/raw/master/docs/ppt8-dop-engi/img/B18492_11_02.jpg

图 11.2 – Puppet 控制仓库模板的文件结构

在本章的第一部分,Puppet 环境,我们讨论了许多文件和目录,其中包括 environment.conf、配置版本脚本以及用于分类的 manifests 目录。还可以看到 hiera.yaml 中的 Hiera 配置以及数据目录,显示了一个简单的初始节点的两层结构,用于匹配特定节点名称和公共数据,作为不匹配节点的默认设置。site-modules 目录旨在展示临时计划和任务如何作为该控制仓库的一部分部署,并可能为角色和配置文件提供存放位置。scripts 目录也值得查看,以了解在 github.com/puppetlabs/control-repo/blob/production/scripts/config_version.sh 中的配置版本脚本如何将有关环境的 Git 修订控制信息添加到执行中。我们尚未审查的部分是 Puppetfile 文件。

Puppetfile 文件是基于 Ruby 的 moduledir 作为变量,或某个模块的 installpath 参数。我们不推荐这样做,因为这可能会让不熟悉你环境的用户感到困惑,并且如果设置在环境目录之外,可能会影响缓存,导致环境不一致。本节稍后会讨论这一点。

Puppetfile 模块声明的最简单形式包含以下内容:

  • mod 关键字

  • 单引号中的名称

  • 可选地跟一个逗号,再加上版本号或 : latest 关键字

例如,以下代码块假设 Puppet Forge 作为源,并在模块不存在时安装 dsc-octopusdsc 的最新版本,但不会导致模块被更新:

mod 'dsc-octopusdsc'
mod 'puppetlabs-chocolatey', '6.2.0'
mod 'puppetlabs-stdlib' , :latest

这段代码将安装 puppetlabs-chocolatey 到固定版本 6.2.0,并将安装 puppetlabs-stdlib 并保持更新到最新版本。需要注意的是,这不会导致 Puppet Forge 依赖项被安装——这些必须在 Puppetfile 中手动管理。在 Puppet Forge 查看模块文档时,你会看到如何将模块添加到 Puppetfile 的示例代码。

要访问其他 Git 仓库中的模块,应提供 git 选项和仓库的 HTTP 地址。然后,可以将其与以下选项之一配对,以克隆 Git 仓库的特定版本:

  • ref,指向标签、提交或分支的引用

  • tag,使用特定标签

  • commit,具有特定的提交引用

  • branch,具有分支名称或 :control_branch 关键字(它将自动查找控制仓库的分支名称)

  • default_branch,如果所有前述选项失败时使用的分支

以下代码演示了如何混合和匹配前述列表中的 git 选项:

mod 'exampleorg-examplemodule1',
  :git => 'https://internalgitservice.com/exampleorg/examplemodule1',
  :tag =>  'v.0.1'
mod 'exampleorg-examplemodule2',
  :git => 'https://internalgitservice.com/exampleorg/examplemodule2',
  :commit => '68a140bd096a55019b3d5c8c347436b318779161'
mod 'anotherorg-anothermodule',
  :git => 'https://internalgitservice.com/anotherorg/anothermodule',
  :branch => :control_branch,
  :default_branch => 'main'

这段代码块从同一个 Git 组织中获取 examplemodule1tag 版本 v.0.1examplemodule2commit 版本 68a140bd096a55019b3d5c8c347436b318779161。对于 anothermodule,如果存在与我们要部署的环境同名的分支,它将使用该分支;否则,它将克隆 main 分支。

在访问 Puppet Forge API 受限的隔离网络环境中,或在受到监管的环境中,若要求公司存储所有代码的副本以便审计,可能需要从 Forge 下载代码副本,并从公司自己的 Git 系统使用。在这种情况下,强烈建议你按照模块页面上的项目 URL,执行 Git 克隆 Puppet Forge 模块的源代码,然后将远程目录切换到你自己 Git 仓库的副本。这样可以确保提交历史得以保留,并且你可以定期克隆代码并将新提交添加到本地仓库。

无论 Forge 模块是如何下载的,如果它们不是直接从 Forge 最新版本下载的,那么频繁检查版本并将其作为定期测试和更新的流程非常重要。这可以确保你获取到最新的功能和修复,并避免执行大版本升级,因为大版本升级更难测试。关注 Content and Tooling (CAT) 团队的博客 puppetlabs.github.io/content-and-tooling-team/blog/ 可以帮助你跟踪模块发布。

注意

JFrog Artifactory 用户可以使用 Puppet Forge 插件在内部同步和托管模块,具体操作请参考 www.jfrog.com/confluence/display/JFROG/Puppet+Repositories

使用这种结构来管理多个环境时,只需在 Git 仓库中创建分支,每个分支代表一个环境,且每个环境可以有其独立的内容进行部署。

管理部署的标准系统是r10k,它还为 PE 提供了更多的集成功能。

r10k的安装说明简单明了,并且可以直接从forge.puppet.com/modules/puppet/r10k的仓库中获取。配置 PE 中 Code Manager 的说明可以通过节点组或通过 Hiera 提供,详细信息请参见puppet.com/docs/pe/2021.7/code_mgr_config.html

在这两种情况下,作为这些说明的一部分,将生成一个 SSH 密钥,用于r10k与任何你已声明的 Git 仓库之间的通信。

Puppet 开源的另一个替代选项是使用g10k ([forge.puppet.com/modules/landcareresearch/g10k](https://forge.puppet.com/modules/landcareresearch/g10k)),它是r10k`在Go语言中的重写,并且在性能上有显著的提升。

注意

你仍然可以在 PE 中直接使用r10k,但这是 Puppet 不提供支持的做法。

对于开源 Puppet,在配置并部署r10k之后,可以运行sudo -H -u puppet r10k deploy production命令来部署特定的分支,或者省略环境名称以部署所有可用的环境。还可以使用 Sinatra 服务器配置 Webhook,详细信息请参见r10k的说明,forge.puppet.com/modules/puppet/r10k/readme#webhook-support

对于 PE,Puppet Code Manager 是一个使用在 PE puppet code deploy命令中生成的令牌的/code-manager API。例如,下面的代码将为当前登录用户生成一个令牌,该令牌在接下来的 2 小时内有效,然后在生产环境中进行部署:

puppet-access login --lifetime 2h
puppet code deploy production --wait

在这两种版本中,要查看已部署的模块,可以使用puppet module --list,该命令还会显示任何依赖问题。

注意

Puppet Code Manager 在底层使用r10k。为了获得更详细的调试信息,可以运行以下命令,该命令用于在生产环境中进行部署:

runuser -u pe-puppet -- /opt/puppetlabs/puppet/bin/r10k -c /opt/puppetlabs/server/data/code-manager/r10k.yaml deploy environment production --puppetfile --``verbose debug2

对于这些部署,理解可能发生的缓存非常重要。所有 Puppet 代码在加载环境时都会被读取和解析——hiera.yaml文件也是如此——直到环境缓存过期或 JRuby 实例被刷新,才会重新读取。environment.conf文件默认将此设置为unlimited。虽然 Puppet 模板和 Hiera 数据在每次函数调用时都会从磁盘重新读取,但它们不会被缓存。这意味着,如果对r10k之外的 Hiera 数据或 Puppet 模板进行任何本地编辑,它们将被视为有效。也意味着如果环境具有查看其他环境的模块路径,部署只会看到 Hiera 和模板的更新。因此,强烈建议避免使用这种方法。

使用编译器同步代码时,开源 Puppet 根据你的环境提供了不同的部署方式:在每个编译器节点上安装并运行r10k、从主服务器到编译器执行rsync操作,或者使用从主服务器到所有编译器的只读网络文件共享NFS)。这一选择完全取决于你的组织在网络配置和安全标准方面的最佳方案。

在 PE 中,代码管理器使用文件同步客户端和服务器进行特定实现,如图 11.3所示:

https://github.com/OpenDocCN/freelearn-devops-pt3-zh/raw/master/docs/ppt8-dop-engi/img/B18492_11_03.jpg

图 11.3 – Puppet 代码管理器架构

代码部署请求将通过命令行或工具以带有 RBAC 令牌的请求形式传入。这将把代码拉取到主服务器的提交暂存目录。所有基础设施节点的文件同步客户端都有一个轮询监视器,能够看到部署并提醒文件同步过程。根据是否启用了无锁代码部署(此功能在 PE 2021.2 中引入),文件同步过程会做出两种响应中的一种。如果相关服务器未启用无锁代码部署,则需要保留所有 JRuby 实例,以防止任何目录运行使用不一致的环境。记住在Puppet 运行部分中不同环境数据如何被缓存,一旦保留,文件将同步到环境目录,并释放 JRuby 实例。这意味着代码部署可能会对性能产生影响。

如果启用了无锁代码部署,则会使用符号链接或 symlink 来管理环境目录,这意味着文件同步会同步到一个以版本提交命名的文件夹,并且在同步完成后,会将环境的符号链接重定向到这个新文件夹。这需要更多的磁盘空间,因为多个环境将同时部署,但它确保目录能继续运行,因为它们会使用符号链接开始时的目录。要启用无锁代码部署,请按照puppet.com/docs/pe/2021.7/lockless-code-deploys.html上的说明进行操作。

现在我们了解了 Puppet 如何将代码部署到环境中,我们将看一下可用于管理模块代码在这些环境中推广的工作流。

创建工作流

创建工作流来部署代码有两种常见的方法。第一种方法是将控制仓库作为版本的中央守门人。这意味着在 Puppetfile 中的每个模块声明都有特定版本,并且通常会使用如tagcommitbranch等特定引用更新最低级别的环境。这些更改会在功能分支中进行测试,然后通过将更改从一个分支合并到另一个分支,运行服务器上的代码,并确认预期结果,从而推动这些更改通过环境。例如,这样的过程可能包括以下步骤:

  • 创建控制仓库的功能分支并将module1标签版本从 1.1 更新为 1.2

  • 将功能分支与开发分支合并并部署开发环境

  • 将开发分支与 UAT 分支合并并部署 UAT 环境

  • 将 UAT 分支与生产分支合并并部署生产环境

这不是一种自然的 Git 流,并且不使用主分支。它非常专注于部署,要求更多地管理环境。这种方法对于多个团队尤其困难,因为它要求像 Puppet 平台团队这样的守门人来管理对 Puppetfile 控制仓库的更改,并管理何时进行代码部署的时间表。

如果采用这种方法,建议使用多个控制仓库并使用前缀配置设置——这对于希望使用不同模块集的团队(如 Windows 和 Linux)或希望在控制仓库周围进行隔离和保护,并且希望拥有代码和服务器的独立所有权但又想共享基础设施的团队非常有用。

第二种方法是将控制仓库中的所有模块都设置为使用control_branch分支,默认分支为main。维护 Puppetfile 时,只需要添加和移除模块。版本管理将由模块本身负责,代码更改会从临时功能分支推送到主分支,然后再合并到每个静态环境分支。以下是一个示例:

  • module1和控制仓库上创建一个功能分支,并测试代码更改

  • module1的功能分支与main分支合并

  • 将模块分支的更改从main合并到开发环境,然后进行部署和测试

  • 将模块分支的更改从开发环境合并到 UAT,然后进行部署和测试

  • 将模块分支的更改从 UAT 合并到生产环境,然后进行部署和测试

强烈建议将管道工具作为拉取请求PR)和部署过程的一部分。PE 的持续交付CD4PE)(在第十四章中讨论)配有预构建的检查,帮助简化这一过程,但也存在各种工具,如 Jenkins 或 GitHub,可以确保在完成 PR 之前执行我们在第八章中讨论的 pre-commit 钩子检查和测试。

注意

一些现成的优秀 pre-commit 钩子的来源可以在以下网址找到:pre-commit.com/hooks.htmlgithub.com/pre-commit/pre-commit-hooks,以及github.com/mattiasgeniar/puppet-pre-commit-hook

实验 – 分类和部署代码

在本实验中,完成以下任务:

总结

本章中,我们讨论了如何使用 Puppet 环境来管理模块的特定版本、分类和要应用于 Puppet 客户端组的数据。回顾了用于配置这些内容的目录结构和变量。

对将服务器分类到不同环境、分配类和参数的选项进行了审查,查看了清单文件中的节点定义,使用 Hiera 在节点定义中创建更复杂的基于数据的计算,然后是 ENC 脚本,这些脚本可以访问如 PuppetDB 等源并返回类、环境和参数的 YAML 输出,以便进行分类。随后,展示了 PE 如何在 ENC 方法的基础上进行扩展,并使用自己的 ENC 脚本与节点组结合使用,存储如何将服务器分类到环境并分配类的数据。

强调了可以将各种方法结合使用,但推荐的做法是保持简洁;对于开源 Puppet,只需使用默认节点定义来查找pp_role受信事实进行分类,并将环境设置放入puppet.conf,而对于 PE,建议使用节点组与pp_rolepp_environment受信事实的一一匹配。

随后展示了 Puppet 目录请求如何将数据发送到 Puppet 服务器,以及如何使用分类文件和脚本来生成目录,重点介绍了如何缓存不同类型的 Puppet 资源。

随后展示了如何部署环境,使用基于 Git 的 Puppet 控制库来包含环境的文件和目录,每个 Git 分支代表一个特定的环境。Puppetfile 被展示为列出应部署到环境中的模块,并指定模块的版本和位置。

接着讨论了如何通过r10k及其在r10k基础上的 PE 代码管理器实现来部署代码到服务器。对于使用编译器的服务器,我们回顾了各种方法来保持所有基础设施上的代码部署,这将取决于本地基础设施和标准。对于 PE,展示了代码管理器包含文件同步功能,以保持代码的同步。

随后介绍了工作流方法,展示了使用带有 Puppetfile 的控制库,设置版本并在推送模块版本更改时更新最低级别环境(如开发环境)的传统方法。第二种推荐的方法显示,控制库将依赖于模块本身,控制库会查找按环境命名的分支,允许团队独立工作和部署。无论哪种系统,重点都是使用合适的流水线工具并配合 Webhooks 来自动化部署。

本章重点介绍了用于有状态配置管理的 Puppet 基础设施和语言,下一章将介绍 Bolt 和 Orchestrator,展示如何使用 Bolt 作为独立工具或通过 PE 基础设施中的 PE Orchestrator 来执行过程任务。

第十二章:用于编排的 Bolt

在本章中,我们将介绍Bolt和 Puppet Enterprise 的orchestrator。我们将展示 Bolt 是 Puppet 用于临时编排的工具,能够处理不适合 Puppet 基于状态强制执行模型的工作。我们将讨论如何配置它以连接具有不同传输机制和凭证的客户端,并执行简单命令和上传文件。此外,我们将展示如何通过 Bolt 运行tasks,这些任务可以是多种语言的单一操作脚本,而plans则允许通过逻辑和变量在 Puppet 或 YAML 语言中编写任务组合。我们还将探讨项目目录结构,允许存储和共享 Bolt 内容。这将与如何使用Puppet Enterprise Cloud Deployment ModulePECDMBolt 项目作为示例,将计划和任务存储在 Puppet 模块中进行比较。接着,我们将展示如何通过插件扩展 Bolt,从其他来源动态加载信息。我们还将展示如何将 Bolt 与 Puppet 直接结合使用,应用清单块,连接到 PuppetDB,并使用 Hiera。

在本章中,我们将介绍以下主要内容:

  • 探索和配置 Bolt

  • 理解项目的结构

  • 任务与计划介绍

  • 插件

技术要求

将控制仓库controlrepo-chapter12github.com/puppetlabs/control-repo克隆到您的 GitHub 账户,并用github.com/PacktPublishing/Puppet-8-for-DevOps-Engineers/blob/main/ch12/Puppetfile的内容更新 Puppetfile。

通过从github.com/PacktPublishing/Puppet-8-for-DevOps-Engineers/blob/main/ch12/params.json下载params.json文件,并更新其中的控制仓库位置和控制仓库的 SSH 密钥,构建一个标准集群,包含两个 Unix 客户端和两个 Windows 客户端。然后,在pecdm目录中运行以下命令:

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

探索和配置 Bolt

到目前为止,本书主要集中在 Puppet 作为基于状态和幂等的配置管理工具的优势。但也有一些场景,其中这种方法并不适用,例如作为故障排除一部分的服务重启,或者使用供应商提供的安装脚本进行应用部署排序。许多任务属于更广泛自动化努力的一部分,属于临时和一次性的任务;因此,Puppet 推出了 Bolt,作为一个无代理的编排工具。自 2017 年发布以来,Bolt 已经进入 3.x 版本,并且经历了快速的发展。到了 2022 年,Bolt 趋于稳定,发布和功能更新大大减少,但我们强烈建议您尽可能保持 Bolt 的最新版本,以避免任何混淆。

在审阅了 Bolt 作为临时任务运行器的通用目的之后,第一步是理解 Bolt 如何通过传输和目标连接到客户端。

通过传输和目标连接到客户端

Bolt 是一个完全开放源项目,位于github.com/puppetlabs/bolt,使用bolt编写。它通过提供的各种传输连接到设备,这是一种机制/协议,允许它在不需要代理的情况下建立到多个平台(如虚拟机、网络设备或容器)的连接。可用的传输如下。

系统传输:

  • 本地,顾名思义,仅在本地机器上运行命令。

  • net-ssh Ruby 库或native ssh,如果选择使用的话。通常用于 Linux 和 Unix 机器。

  • Windows 远程管理WinRM),用于连接基于 Microsoft Windows 的机器。

远程,用于 API 或基于 Web 的设备,例如网络设备如交换机。

Puppet Enterprise 传输:

  • Puppet 通信协议PCP),与 Puppet Enterprise 编排服务一起使用,在第十四章中讨论。

容器传输:

  • Docker,由 Docker Inc 开发的应用容器技术。

  • Pod 管理器Podman),由 Red Hat 开发的应用容器引擎。

  • Linux 容器超级监视程序LXD),是一个使用Linux 容器LXC)的系统容器引擎,由linuxcontainers.org开发并由 Canonical 赞助。

注意

除非在传输设置中将native-ssh设置为true,否则 Bolt 无法使用 SSH 连接到 Windows 目标,具体参见puppet.com/docs/bolt/latest/bolt_known_issues.html#unable-to-authenticate-with-ed25519-keys-over-ssh-transport-on-windows

默认情况下,Bolt 将使用本地 SSH 配置,在其最简单的级别上可以直接在被称为目标的设备上运行命令。一个简单的例子命令如下:

bolt command run 'uname' --targets examplehost.example.com

这里,命令在单引号内,提供的目标是可解析的主机名或 IP 地址。Bolt 还具有PowerShell 命令,为 PowerShell 用户提供更集成的体验,具有更灵活的命令链接和使用结构化数据作为参数的能力。与之前相同的命令作为 PowerShell 命令看起来如下:

Invoke-BoltCommand -Command 'uname' -Targets examplehost.example.com

这会使用默认的 SSH 传输设置,使用当前用户和任何已保存的凭证。如果想在命令行中做出选择,可以在传输名称前加上传输选择 <transport_name>://,多个目标使用逗号(,)分隔,并设置其他选项来配置传输。例如,如果 WinRM 没有配置 SSL 连接,则需要设置用户名、密码和 no-ssl 选项。以下是一个示例命令:

bolt command run 'systeminfo' --targets winrm:// host1.example.com,winrm://host2.example.com --user windows --password Pupp3tL@b5P0rtl@nd! --no-ssl

此命令将使用 winrm 连接在 host1.example.comhost2.example.com 目标上运行 systeminfo,并使用 windowsPupp3tL@b5P0rtl@nd! 凭证,同时不进行 SSL 检查。Bolt 默认并行运行请求,最多同时运行 50 个请求。可以通过 --concurrent 参数调整并发数。

可以在文档中查看每种传输方式的完整选项列表:puppet.com/docs/bolt/latest/bolt_transports_reference.html

注意

Bolt 1.3.6 弃用了 nodes 标志,改为使用 targets,并在 Bolt 2.0.0 中移除了该标志。

使用 Bolt 运行临时命令

本节将展示如何使用 Bolt 运行临时命令,包括 Windows PowerShell 和 Linux Shell 命令示例。下表展示了这些命令在不同实现中的对比:

https://github.com/OpenDocCN/freelearn-devops-pt3-zh/raw/master/docs/ppt8-dop-engi/img/B18492_12_01.jpg

图 12.1 – PowerShell 和 Linux Bolt 命令

要运行带引号的命令,请使用双引号或反斜杠(\)进行转义。例如,我们可以在 /etc/locale 中搜索 lang,命令为 grep -I 'lang'。为了做到这一点,可以在 PowerShell 中运行以下命令:

Invoke-BoltCommand -Command "grep -i 'lang' /etc/locale" -Targets ssh://examplehost.example.com –User centos -PasswordPrompt -RunAs root

在这个例子中,password-prompt 选项将在命令行上安全地提示输入密码,而不是直接将其输入到执行的命令中。

要运行文件中列出的多个命令,我们不是建议运行脚本,而是逐步执行一组命令;对于文件中的多个目标,可以使用 @ 符号和引号包围的文件名('')。例如,要从名为 commandlist 的文件中运行一组命令,并在 targetfile 中列出的目标上执行,可以运行以下命令:

bolt command run '@commandlist' --targets '@targetfile'

对于基于 Unix 的系统,要从 stdin 读取输入以供目标或命令使用,可以用减号(-)替代目标或命令字符串。因此,若想使用相同的 targetfile 并将 cat 命令的输出传递给 bolt 命令,可以运行以下命令:

cat targetfile | bolt command run '@commandlist' --targets -

要在 hosts1.example.comhost2.example.com 目标上运行 unamedate 命令,可以使用以下命令:

echo -e "uname \\ndate" | bolt command run - --targets host1.example.com, host2.example.com

注意

同时使用文件和 stdin 列出命令将导致与目标建立单一连接,执行所有命令。

要运行文件中的脚本,可以使用bolt script run命令或Invoke-BoltScript -ScriptPowerShell cmdlet,并在命令末尾传递任何参数。例如,在 Unix 主机上,可以使用以下命令在application_clients文件中的目标上运行带有10.6 no-gui参数的install.sh脚本:

bolt script run ./scripts/install.sh --targets @application_clients 10.6 no-gui

可以使用arguments标志来明确每个传递值的参数名称。任何带空格的参数可以用引号('')括起来。例如,在 Windows 系统上运行带有-Channel LTS参数的dotnet-install.ps1脚本,命令如下:

Invoke-BoltScript -Script dotnet-install.ps1 -Targets @targetsfile '-Channel LTS'

在 Unix 中,任何脚本都可以通过在文件顶部包含一个 shebang(#!)行来指定解释器,从而在目标上执行。对于 Windows 目标,.ps1.rb.pp文件默认启用,但可以在配置文件中启用其他扩展,这将在下一部分讨论。脚本可以从modulepath中找到,形式为<modulename>/scripts/install.sh,也可以是bolt文件夹根目录的相对路径,或者是绝对路径。

在 Unix 系统中,Puppet 清单文件和 Puppet 代码的部分可以通过以下方式应用到一组目标:

bolt apply manifests/exampleapp.pp --targets @targetsfile

在 PowerShell 中,可以使用以下命令实现:

Invoke-BoltApply -Manifest manifests/exampleapp.pp -Targets @targetsfile

要应用 Puppet 代码,以下命令会确保 Unix 系统上的/etc/exampleapp目录存在:

bolt apply --execute "file { '/etc/exampleapp: ensure => present }" --targets servers

对于 PowerShell cmdlets,使用的命令如下:

Invoke-BoltApply -Execute "file { '/etc/exampleapp': ensure => present }" -Targets servers

这种格式应该与puppet applypuppet apply -e '<code>'相似。类似地,对于通过 Bolt 应用的代码,我们必须确保代码被声明为包含在目录中,而不仅仅是定义了。当一个类或类型被定义时,它可以在目录中使用,但不会被添加到目录中。在前面的示例中,如果exampleapp.pp包含一个带有资源的类定义,那么会出现警告:Manifest only contains definitions and will result in no changes on the targets。类本身需要被包含才能将其添加到目录中,并通过 Bolt 应用。

还有一些命令可以将文件从本地机器上传到目标,或从目标下载到本地机器。以下命令展示了在 Unix 和 Windows 版本中的一些简单示例。第一个列出的文件是源文件,第二个是目标文件,无论是上传还是下载:

bolt file upload /rpms/cowsay.rpm /tmp/ --targets @targets
Send-BoltFile -Source /installer/installer.exe -Destination /users/exampleuser/installer.exe -Targets @targets
bolt file download /etc/exampleapp//logfile.log /var/tmp/logfile.log --targets @targets
Receive-BoltFile -Source /ProgramData/exampleapp/logfile.log\puppet.log -Destination /user/exampleuser/puppet.log -Targets @targets

现在,让我们来看一下输出。

输出和调试

到目前为止,重点一直是如何运行命令,而不是输出。默认情况下,Bolt 会将这些命令记录到从中运行 Bolt 命令的目录中的bolt-debug.log文件中,并显示在控制台上。共有六个日志级别:

  • trace:最详细的日志级别,显示 Bolt 的内部工作过程。

  • debug:关于特定目标步骤的信息。

  • info:这是高级日志,显示 Bolt 中发生的步骤。

  • warn:关于弃用功能和其他有害场景的警告。这是默认的控制台级别。

  • error:在执行 Bolt 命令时遇到的错误消息。

  • fatal:来自 Puppet 代码的错误消息,这些代码与 Bolt 一起使用。

可以使用 --log-level 标志选择特定的日志级别,并使用 format 标志选择输出格式,支持 humanjsonrainbow。在三台主机上运行 uname 的 Bolt 命令输出,JSON 格式如下所示:

{ "items": [
{"target":"host1.example.com","action":"command","object":"uname","status":"success","value":{"stdout":"Linux\n","stderr":"","merged_output":"Linux\n","exit_code":0}}
,
{"target":"host1.example.com","action":"command","object":"uname","status":"success","value":{"stdout":"Linux\n","stderr":"","merged_output":"Linux\n","exit_code":0}}
,
{"target":"host1.example.com","action":"command","object":"uname","status":"success","value":{"stdout":"Linux\n","stderr":"","merged_output":"Linux\n","exit_code":0}}
],
"target_count": 3, "elapsed_time": 2 }

相比之下,以人类可读格式显示时,内容将如下所示:

Started on host1.example.com...
Started on host2.example.com...
Started on host3.example.com...
Finished on host1.example.com:
  Linux
Finished on host2.example.com:
  Linux
Finished on host3.example.com:
  Linux
Successful on 3 targets: host1.example.com, host2.example.com, host3.example.com
Ran on 3 targets in 2.89 sec

rainbow 输出与人类可读格式类似,但正如其名字所示,它使得每一行都呈现为多种颜色。

作为输出的一部分,将生成一个 .rerun.json 文件。此文件列出了在运行过程中处理的目标,指示哪些目标失败,哪些目标成功。对于下一个 Bolt 命令,我们可以使用 --rerun 标志,值可以为 successfailureall。此命令将读取 .rerun.json 中的相关目标部分,并使用上一次运行的目标。例如,以下命令可能是在 install 任务失败并选择对所有失败进行清理任务时运行:

Invoke-BoltTask -Name install_failure_cleanup -Targets @targets.file -Rerun failure

命令有更多选项;完整的命令参考文档可以通过以下链接查看:puppet.com/docs/bolt/latest/bolt_command_reference.html 针对 Unix 系统的命令和 puppet.com/docs/bolt/latest/bolt_cmdlet_reference.html 针对 PowerShell 系统的命令。

注意

Bolt 具有内置的 CLI 指南,可以通过在 Unix 或 PowerShell 命令行中运行 bolt guide 来访问。

到目前为止,我们使用 Bolt 讨论的内容在非常小的规模下有用,但显然也适用于大规模的服务器和更复杂的配置。因此,接下来要讨论的是项目结构和配置文件。

理解项目结构

在其中存在一个 bolt-project.yaml 文件,该文件包含一个 name 键。要创建该文件,请在希望添加 Bolt 项目文件的目录中,分别针对 Unix 系统运行 bolt project init 或 PowerShell 中运行 New-BoltProject。此命令将使用目录名作为项目名称,但你可以通过运行带有名称的命令来覆盖这一点,分别为 Unix 系统的 bolt project init customname 或 PowerShell 中的 New-BoltProject -Name customname 命令。

项目名称必须以小写字母开头,并且只能使用小写字母、数字和下划线。这是因为 Bolt 项目类似于模块,并会加载到模块路径中。需要注意的是,如果 Bolt 项目与模块具有相同名称,则 Bolt 项目中的模块将被覆盖在模块路径中。

在该目录中,init 命令将创建 bolt-project.yamlinventory.yaml.git-ignore 文件。

现在,让我们看看如何配置一个 Bolt 项目。

配置项目

bolt-project.yaml 包含用于覆盖默认 Bolt 行为的设置,许多内容在前一节中已经讨论过。可以在此处设置用于 Bolt 命令的设置,以及项目配置,例如配置文件和数据的路径。通常情况下,这些设置的默认值不需要更改,核心设置将包括 modules 属性,该属性定义了在 Bolt 项目中管理的模块,以及 planspoliciestasks 属性,这些属性通过提供可见的列表来限制每个项目项的可见性,用户可以看到该列表。一个包含一些模块,并选择要公开可见的计划、策略和任务的 bolt-project.yaml 文件示例如下:

name: packtproject
modules:
- name: puppetlabs-stdlib
- name: puppetlabs-peadm
  version_requirement: 3.9.0
- name: puppetlabs/bolt_shim
- git: https://github.com/binford2k/binford2k-rockstar
  ref: 0.1.0
plans:
- packproject
- peadm::provision
policies:
- packproject::lab
tasks:
- bolt_shim::command

设置的完整列表可以在 puppet.com/docs/bolt/latest/bolt_project_reference.html 中找到。

module 属性有多种更新方式。当从 Forge 添加项目时,可以通过 bolt module add Unix 命令或 Add-BoltModule PowerShell cmdlet 更新。例如,在 Unix 系统中,bolt module add puppetlabs/apt 将更新 bolt-project.yaml 中的 modules 参数,包含 - name:puppetlabs-apt

然后,可以使用 bolt module install Unix 命令或 Install-BoltModule PowerShell cmdlet,这将自动完成几项操作:

  • 查找所有 Forge 模块的依赖项

  • 查找兼容版本

  • 更新 Puppetfile

  • 将模块安装到 Bolt 项目中

模块还可以在项目创建时通过以下 Unix 系统命令添加:

bolt project init example_project --modules puppetlabs-apache,puppetlabs-mysql

在 PowerShell 中,可以使用以下命令来完成此操作:

New-BoltProject -Name example_project -Modules puppetlabs-apache,puppetlabs-mysql

如果需要将模块固定在特定版本或添加 Git 模块,则需要手动将这些模块添加到 Bolt 项目文件中,并使用以下 Bolt 模块安装命令运行 Force 标志:在 Windows 上使用 Install-BoltModule -Force 或在 Unix 系统上使用 bolt module install --force

这些模块允许我们在计划中使用 Puppet 代码,并从模块中引入计划和任务,详细内容将在 介绍任务和 计划 部分中展示。

配置传输

inventory.yaml 文件包含有关目标的配置信息,创建目标组并提供有关 Bolt 如何与它们连接的详细信息。清单包含一个顶层,其中包括作为所有目标默认设置的设置,允许基于共同设置(如所有 Windows 节点使用某些 WinRM 设置)对目标进行分组的组对象,以及目标对象(即单独的设置)。对于每个设置,都可以使用一些通用字段:

  • 别名:用来代替统一资源标识符URI)的别名,它可以更简短且更易于人类阅读

  • 配置:目标的传输配置选项的映射

  • 事实:目标(们)的事实映射

  • 特性:要启用的特性数组(特性将在本章后续部分讨论)

  • 名称:与组一起使用,以提供易于阅读的名称

  • 插件钩子:插件配置的映射(插件将在本章的 插件 部分讨论)

  • URI:目标的 URI

  • 变量:变量的映射

一个示例清单文件可能如下所示:

config:
  transport: ssh
  ssh:
    host-key-check: false
    run-as: root
    native-ssh: true
    ssh-command: 'ssh'
groups:
  - name: agents
 groups:
  - name: linux_agents
    targets:
      - 20.117.165.119
  -name: windows_agents
    targets:
     - 20.117.165.218
     config:
      winrm:
        user: windowsuser
        password: Pupp3tL@b5P0rtl@nd!
        ssl: false
targets:
  - name: primary:
  - 20.117.166.6

这将提供 SSH 传输的默认设置。需要注意的是,在此示例中,展示了如何在任何清单中创建组内的组,以便简化管理和组的设置。在此案例中,我们有一个名为 agents 的组,其中包含 linux_agents 组和 windows_agents 组。windows_agents 组包含 WinRM 传输配置。这使得我们可以对所有代理运行 Bolt,但为每个代理设置不同的传输方式。然后,在这些组外有一个名为 Primary 的单一目标。

完整的 inventory.yaml 配置文档可以在 puppet.com/docs/bolt/latest/bolt_inventory_reference.html 上找到,而传输配置文档可以在 puppet.com/docs/bolt/latest/bolt_transports_reference.html 上查看。

要返回 inventory.yaml 文件的内容,可以使用 bolt inventory show Unix 命令或 Get-BoltInventory PowerShell cmdlet。可以使用 targets 标志查看特定目标。

如前一节所述,对于 Windows 脚本,可以使用清单文件允许附加扩展,因此在 config 部分,可以添加以下内容以允许运行 .py.pl 脚本:

config:
  winrm:
    extensions:
      - .py
      - .pl

在回顾如何在 Bolt 中配置项目级别的设置之后,现在需要了解如何在 Bolt 中设置系统级别的设置,以及如何将以前的遗留版本的 Bolt 项目配置为不同的方式。

系统级别和遗留版本

除了项目设置外,系统级别的设置可以在基于 Unix 的系统中的/etc/puppetlabs/bolt/bolt-defaults.yaml文件以及 Windows 系统中的%PROGRAMDATA%\PuppetLabs\bolt\etc\bolt-defaults.yaml文件中进行设置。用户级别的设置可以在用户主目录下的.puppetlabs/etc/bolt/bolt-defaults.yaml文件中进行设置。

Bolt 会根据以下优先级顺序选择使用哪个项目:

  1. BOLT_PROJECT环境变量中设置的项目位置

  2. 在设置了项目位置的 Bolt 命令中的project标志(--project /tmp/myproject

  3. 通过从当前目录向上遍历,直到找到bolt-project.yamlboltdir目录

  4. 用户主目录下的.puppetlabs/bolt/文件夹

注意

在 Unix 环境中,Bolt 不会加载一个世界可写的 Bolt 项目目录。

如果你希望将 Bolt 嵌入到一个应用项目中,但基本的 Bolt 项目文件会使应用变得杂乱,你可以通过在应用目录中创建一个boltdir目录来嵌入一个 Bolt 项目。即使是这样,Bolt 仍然可以从父目录运行,因为它会识别boltdir作为包含项目的目录。

如果你之前使用过 2.36 版本之前的旧版 Bolt,你会注意到项目曾经只创建一个bolt.yaml文件,而不是bolt-project.yamlinventory.yaml。对 v1 bolt.yaml 项目的支持在 Bolt 的 v3.0.0 版本中被移除。此外,随着 v2.42 版本中手动编辑 Puppetfile 的弃用和 v3.0.0 版本中手动编辑的移除,Bolt 管理的模块也发生了变化。这也改变了模块路径,从包含site-modulessite模块变更为现代版本的modules.modules。之前,托管的模块存在于modules中,非托管的模块则位于sitesite-modules中。现在已经更改为托管的模块位于.modules中,非托管的模块位于modules中。为了将旧版的 Bolt 项目迁移到新版本,可以运行bolt project migrate Unix 命令或Update-BoltProject PowerShell 命令。像所有自动化转换一样,请确保在迁移之前备份好配置并进行版本控制。有关迁移过程中的更改详细信息,请参阅puppet.com/docs/bolt/latest/projects.html#migrate-a-bolt-project

在回顾了为 Bolt 配置和目标传输创建的结构之后,现在是时候通过任务和计划来查看更结构化的运行 Bolt 方式了。

引入任务和计划

任务计划更像是脚本,允许用户管理参数、逻辑和动作之间的流程。与普通的 Puppet 代码不同,计划和任务按顺序运行脚本,即使是那些编译目录清单的 Puppet 计划也是如此。

创建任务

任务是单一操作的脚本,可以使用任何在目标机器上运行的语言。与我们之前使用 Bolt 执行的普通脚本相比,任务的主要区别如下:

  • 任务与 JSON 文件配对,以提供元数据,例如参数,使它们更易于共享和重用

  • 任务可以处理结构化/类型化的输入和输出

  • 任务可以处理多种实现,使其跨平台

它们可以存储在 Bolt 项目的任务目录中,或者在 Puppet 模块的任务目录中。任务实现应在名称中包含其扩展名。名称可以包含数字、下划线和大小写字母

在调用这些任务时,会创建一个命名空间,由包含任务的 Bolt 项目或模块的名称以及任务名称组成,除非任务被命名为 init,在这种情况下它只会通过 Bolt 项目或模块的名称来引用。

例如,安装代理的任务是 peadm::agent_install

注意

.json.md 扩展名是保留的,不能用于任务。

对于 Unix Shell 系统,脚本部分必须在文件顶部包含一个 shebang (#!) 行,指定解释器。

任务实现的一个例子是当使用 PEADM 模块配置实验室时,在 Unix 系统下的 agent_install.sh 任务中使用以下代码:

#!/bin/bash,
set -e
if [ -x "/opt/puppetlabs/bin/puppet" ]; then
echo "ERROR: Puppet agent is already installed. Re-install, re-configuration, or upgrade not supported. Please uninstall the agent before running this task."
exit 1
fi
flags=$(echo $PT_install_flags | sed -e 's/^\["*//' -e 's/"*\]$//' -e 's/", *"/ /g')
curl -k "https://${PT_server}:8140/packages/current/install.bash" | bash -s -- $flags

参数是基于以 $PT_ 开头的变量传递的。

使用 PowerShell,它具有内置的参数处理器,可以通过在名为 agent_install.ps1 的任务中使用 param 函数来完成,而无需使用 $PT_

param(
  $install_flags
  $server
)
if (Test-Path "C:\Program Files\Puppet Labs\Puppet\puppet\bin\puppet"){
Write-Host "ERROR: Puppet agent is already installed. Re-install, re-configuration, or upgrade not supported. Please uninstall the agent before running this task."
Exit 1
}
$flags=$install_flags -replace '^\["*','' -replace 's/"*\]$','' -replace '/", *"',' '
[Net.ServicePointManager]::ServerCertificateValidationCallback = {$true}; $webClient = New-Object System.Net.WebClient; $webClient.DownloadFile("https://${server}:8140/packages/current/install.ps1", 'install.ps1'); .\install.ps1 $flags

为了使这些文件对 Bolt 命令可见并允许调用者传递参数,会写一个与任务同名的 JSON 文件。对于 agent_install 示例,它看起来是这样的:

{
  "description": "Install the Puppet agent from a master",
  "parameters": {
    "server": {
      "type": "String",
      "description": "The resolvable name of the Puppet server to install from"
    },
    "install_flags": {
      "type": "Array[String]",
      "description": "Positional arguments to pass to the shell installer",
      "default": []
    }
  },
  "implementations": [
    {"name": "agent_install.sh", "requirements": ["shell"]},
    {"name": "agent_install.ps1", "requirements": ["powershell"]}
  ]
}

元数据提供了任务的描述,列出任务时会显示。此外,元数据包括参数列表,参数名称必须以小写字母开头,并且仅包含小写字母、下划线和数字。还可以指定参数类型,该类型可以匹配任何可以在 JSON 格式中表示的 Puppet 类型,以及参数的默认值。

确保类型是枚举类型或更具体的类型,例如在指定大小范围内的整数,可以使任务更加安全,限制输入,从而减少攻击面。此外,在任务中,您应确保正在处理的实现的参数得到正确分隔,并且不允许调用字符串。具体示例可以参考 puppet.com/docs/bolt/latest/writing_tasks.html#secure-coding-practices-for-tasks

implementations 参数允许我们定义在什么环境中使用哪些脚本。在此情况下,确保 .sh 实现运行在 Unix shell 上,而 .ps1 实现运行在 PowerShell 上。

有了这个文件,bolt task show Unix 命令或Get-BoltTask PowerShell cmdlet 将显示模块路径中所有可用的模块,并且可以通过bolt task show <taskname>Get-BoltTask –Name <taskname>查看特定任务。

private参数设置为true可以防止任务出现在任务列表中,这对于隐藏正在开发中的任务非常有用,尽管如我们在配置项目部分所示,这也可以在 Bolt 项目级别实现。

通过将参数值设置为true,可以将参数标记为sensitive,并且在代码中将变量设置为sensitive可以确保它们在日志和输出中被屏蔽。

元数据中的supports_noop参数允许用户向任务传递noop参数,这将使得_noop参数的值为truefalse。然后,你可以在任务代码中使用此参数来逻辑检查是否应进行更改或仅进行测试。

如果将remote参数设置为true,则任务只能在远程传输上运行,以防止任务在不兼容的传输上运行。

对于一个选项较多或返回信息较多的任务,使用结构化输入输出可能会比仅使用简单参数更好。

默认情况下,Bolt 将任务参数作为单个 JSON 对象传递给STDIN,并将环境变量一并传递。然后,Ruby 脚本可以使用以下代码行读取这些参数:params = JSON.parse(STDIN.read)

对于复杂的输出,应该确保任务在任务中打印单个 JSON 对象到stdout。这在你希望在另一个任务中使用结果时非常有用。例如,在 Python 中,以下代码片段将把两个值集的 JSON 转储到 stdout,使用json.dump将结果字符串转换为 JSON 并传递给 Python 用于打印到 stdout 的sys.stdout方法:

result = { "example1": "value1 , "example2": "value2" }
json.dump(result, sys.stdout)

要从任务中返回错误消息,可以返回一个Error对象。在结构化输出中,预期会有_error键,并且msg键作为 UI 中的人类可读消息,kind作为脚本处理的字符串,details包含有关任务失败的结构化数据,例如退出码尾部。以下是一个例子:

{ "_error": { "msg": "Task exit code 1", "kind": "puppetlabs.tasks/task-error", "details": { "exitcode": 1 } } }

如果没有_error键,Bolt 将生成一个通用错误。

注意

在一个模块中,可以运行pdk new task <taskname>来生成<taskname>.json文件和<taskname>.sh文件,文件会保存在任务文件夹中。

要运行这些任务,可以使用bolt task run Unix 命令或Invoke-BoltTask PowerShell cmdlet,并通过传递参数作为命令行参数或使用@符号来传递一个 JSON 字符串或带有.json扩展名的文件。例如,第一个任务会在 agents 组中的目标上安装 Puppet 代理,并设置 server 和install_flags参数:

bolt task run peadm::install_agent --targets agents server=primary.example.com install_flags= ["--puppet-service-ensure","stopped","agent:certname=node.example.com"]

第二个任务将运行package任务,并通过params标志传入 JSON 字符串,以检查apache2软件包的状态:

Invoke-BoltTask -Name package -Targets @targetservers -Params '{action="status";name="apache2"}'

在学习了如何创建和运行任务后,现在是时候回顾一下计划,计划允许在管理任务时应用更强的结构、逻辑和流程控制,并且能够使用 Puppet 代码。

创建 Puppet 计划

计划是用 Puppet 代码或 YAML 编写的,它允许将多个任务和命令结合起来,并在它们之间应用逻辑和数据流控制。

Puppet 计划是以清单形式编写的,格式类似于 Puppet 类。它以plan关键字开始,接着是计划的名称、括号()内的属性,以及大括号{}中的代码。例如,位于计划目录中的一个示例项目的计划如下所示:

plan exampleproject::exampleplan(
  TargetSpec $nodes,
  Enum ['true', 'false'] $manage_user,
) {
  <code>
}

计划的命名方式与任务类似,第一部分是模块或项目的名称,第二部分及其后续部分以小写字母、数字和下划线命名。

它们不能使用保留字,也不能与 Puppet 数据类型相同。

init.pp类与任务和模块不同。它会跳过任务直接命名的需求。然而,它只能在基础层级使用,而不能在任何子目录中使用。

要创建一个新计划,可以使用以下命令,分别适用于 Unix 系统和 PowerShell:

bolt plan new <PLAN NAME> --pp
New-BoltPlan -Name <PLAN NAME> -Pp

在回顾了如何创建计划后,我们将看到计划如何通过TargetSpec类型接收目标和传输信息。

构建目标

除了普通的属性数据类型外,计划还使用TargetSpec类型,这使得可以使用与在通过传输和目标连接客户端部分中用于 Bolt 命令目标相同的字符串,例如ssh://examplehost.comTarget类型的数组,以及递归的TargetSpec类型数组。

Target类型表示一个目标及其特定连接方式,以便它们可以被添加到清单文件中。

在计划中,可以使用get_targets函数从TargetSpec中返回目标。以下是一个简单的使用示例:

plan restart_apache_servers(
TargetSpec $apache_servers,
){
 get_targets($apache_servers).each |Target $apache_server | {
 run_task('apache', $target_node, 'action' => 'reload')
 }
}

该计划接受一个TargetSpec对象apache_servers,该对象被传递给get_targets函数。然后,Apache 的reload任务在每个目标服务器上运行,action参数设置为reload

目标对象还可以在计划清单中构建并通过以set_add_开头的函数进行更改,这些函数用于配置文件中各个部分的操作,如set_configset_varadd_factsadd_to_group函数。例如,可以像这样组装一个新目标:

$example_server = Target.new('name'; => 'exampleserver')
$example_server.set_config('transport', 'ssh')
$example_server.set_config(['ssh', 'password', 's3cur3!')
$example_server.add_facts({'application' => 'example'})

可以访问目标的部分内容,例如$example_server.config['ssh'],但是这些目标只会在计划运行时存在于内存中。

现在我们已经理解了如何使用计划连接到客户端,我们将展示如何在计划的 Puppet 代码块中使用函数,利用 Bolt 和 Puppet 核心语言的特性。

使用计划函数

正如构造目标部分所示,使用run_task,Bolt 计划函数可以在 Puppet 代码块中本身使用,其中许多与直接在 Bolt 中运行的命令类型相同,例如run_commandrun_scriptrun_task。完整的命令列表可以在puppet.com/docs/bolt/latest/plan_functions.html中找到。

也可以使用run_plan函数在一个计划中运行另一个计划。这对于确保没有计划变得过大,并且可以更容易地重复使用它们是非常有用的。在 PEADM 模块中可以观察到的一种模式是使用subplan文件夹存放我们只期望在计划中使用的计划,从而减少目录的大小和复杂性。

需要注意的是,大多数 Puppet 语言特性,如函数、sensitive 类型和 lambdas,可以在此代码中使用,但其他特性,如延迟函数,不能使用,因为目录并未发送到节点以供应用。这些差异已在puppet.com/docs/bolt/latest/writing_plans.html#puppet-and-ruby-functions-in-plans中详细记录。

例如,在 PEADM 中,以下run_command函数停止所有存储在$all_targets变量中的目标上的 Puppet,然后在covert_target变量中的目标上运行modify_certificate计划,传入一个主要的add参数和要添加的扩展:

run_command('systemctl stop puppet', $all_targets)
run_plan('peadm::modify_certificate', $convert_targets,
  primary_host => $primary_target,
  add_extensions => {
    'pp_auth_role' => 'pe_compiler',
  },
)

Puppet 代码也可以通过apply函数应用,类似于运行puppet apply命令。例如,PEADM 使用以下代码创建节点组:

apply($primary_target) {
class { 'peadm::setup::node_manager_yaml':
  primary_host => $primary_target.peadm::certname(),
}

这应用了node_manager_yaml类,传入了primary_host参数。需要注意的是,如果在应用 Puppet 代码之前需要 Puppet 库,可以使用apply_prep函数,确保在使用apply函数之前这些库已经可用。

日志记录和结果

要在计划中添加日志记录,使用out::messageout::verbose函数,在每次运行时记录消息,只有当 Bolt 以verbose模式运行时才会输出详细消息。以下是一个示例:

out::message('Error')
out::verbose("Heres the error: $detailed_output")

Error将在每次 Bolt 运行时打印,但只有在使用–verbose标志时,第二条消息才会显示。

每个函数返回一个ResultSet类型的对象,每个目标包含自己的Result对象类型,除了apply函数,其ResultSet包含ApplyResult对象。计划返回一个PlanResult类型的输出,可以包含所有这些数据类型以及几乎所有 Puppet 数据类型。

这些对象可以分配给变量,然后使用函数来公开数据。所有这些对象类型中常用的两个函数是ok,它返回一个简单的布尔值来确认是否有任何错误;和value函数返回运行的输出。

更多类型特定函数可以在puppet.com/docs/bolt/latest/bolt_types_reference.html文档中查看。

要从计划返回输出,应使用返回函数与任何适当的数据类型;这可以是来自任务的直接输出或仅仅是一个字符串。如果没有使用返回函数,则输出将为undef。例如,以下代码将运行任务error_check_task,仅当成功时才会从任务output_task返回ResultSet类型的输出;否则,将返回字符串OH NO

plan return_result( $targets )
$did_this_work = run_task('error_check_task', $targets)
If $did_this_work.ok {
out::message('It worked')
return run_task('output_task', $targets)
}else{
Return "OH NO"
}

现在,让我们看看如何处理错误。

处理错误

要执行简单检查并因此失败计划,可以使用fail_plan函数。例如,以下代码将检查$targets变量是否仅包含单个目标:

unless get_targets($targets).size == 1 {
    fail_plan('This plan only accepts one target.')
  }

如果 Bolt 函数失败并且未将_catch_errors设置为true,则计划将失败。如果使用了_catch_errors,则可以允许计划继续执行并处理错误:

$install_agent_results = run_task('agent_install', $agents , '_catch_errors' => true)
$ install_agent_results.each |$agent_result| {
$target = $agent_result.target.name
if $result.ok
 { notice("${target} installed correctly ${result.value}")
} else {
 notice("${target} failed install with error: ${result.error.message}")
 }
}

或者,可以使用catch_result函数来捕获特定类型的错误,如下所示:

$install_agent_results = catch_error(agent_install/connection_error) || { run_task('agent_install', $agents , '_catch_errors' => true)
}

通过了解计划中的日志记录和错误处理,我们现在可以看看如何在计划中使用外部数据。由于 Bolt 使用 Puppet 作为库,因此可以使用 Hiera 访问外部数据。正如在第九章中所述,这可以确保我们将代码和数据分离,就像我们在 Puppet 代码中所做的那样。

管理数据源

可以使用内置的 facts 计划从主机收集事实,或者使用puppetdb_facts从 PuppetDB 收集,假设已在 Bolt 配置中设置了 PuppetDB。使用任何计划都会导致目标自动查询 PuppetDB 并更新其内存中的库存。以下示例将在targets上运行facts,并且将os.name事实等于Windows的目标分配给windows_targets变量:

run_plan('facts', 'targets' => $targets)
$windows_targets = get_targets($targets).filter |$target| { $target.facts['os']['name'] == 'Windows' }

PuppetDB 还可以使用puppetdb_query函数执行通用查询。要返回 PuppetDB 中列出的windows主机的所有certnames事实值,请使用以下代码:

$windows_targets = get_targets (puppetdb_query('inventory[certname] { facts.os.name = "windows" }'))

可以通过使用模块或 Bolt 项目级别的 Hiera,并具有适当的hiera.yaml,来在计划中使用 Hiera。然后可以在apply函数内或直接在计划中使用lookup函数。如果在apply函数内使用lookup,并假设已运行apply_prep函数,我们可以收集所有事实,并且 Hiera 将按预期工作。在计划中使用时,需要注意的重要区别是:Bolt 不像正常的 Puppet 代码(通过类)那样具有自动参数查找功能,并且 Bolt 层次结构不能使用顶层变量或事实。在 Bolt 中使用 Hiera 时,它使用两个层次的层级,项目和模块层级,其中项目层级具有更高的优先级。

Bolt 层次结构的一个示例是一个包含带有节点数据的层级的hiera.yaml项目,以及没有节点数据的plan_hierarchy键:

Hierarchy: -
- name: "Nodes" path: "targets/%{trusted.certname}.yaml"
- name: "Org" path: "%{org}.yaml"
plan_hierarchy:
- name: "Org" path: "%{org}.yaml"

计划中的lookup函数可以执行以下操作,并通过application变量能够在计划层次结构的组织级别查找dns_server_name变量:

plan exampleproject::exampleplan(
TargetSpec $nodes,
String $application
){
$dns_server_name = lookup('dns_server_name)
}

在接下来的部分,我们将探讨如何使用注释来记录元数据。

记录计划元数据

与任务不同,由于计划没有metadata.json文件,因此需要通过注释来记录,以便在运行puppet plans show <plan name>时提供描述。第一行注释将作为描述,或者可以使用@summary标签。使用@param <param name>的注释表示它是参数的描述,使用@api private将计划标记为私有。使用所有这些字段的示例如下:

# @summary This plan is just for example
# @api private
# @param example_servers The targets to run this plan on
# @param manage_user Whether the user account should be managed
plan exampleproject::exampleplan(
TargetSpec $example_servers,
Enum ['true', 'false'] $manage_user
){

数据类型的详细信息可以通过bolt plan show命令自动获取。

注意

将计划和任务添加到控制仓库可能是有用的,但需要注意的是,在使用 PDK 验证时,PDK 无法验证计划,并且只会忽略默认的最低级计划目录中的计划。如果你的结构将计划放在较低级别,你需要运行pdk来忽略这些低级目录中的计划,例如pdk set config project.validate.ignore subdir1/subdir2/plan

计划测试

测试 Puppet 计划超出了本书的范围。这是因为计划测试目前尚未完全实现,并且相比于我们在第八章中看到的常规 RSpec 测试,计划测试更具挑战性。某些功能尚未实现,例如模拟上传文件或自定义函数,这使得与模块测试相比,进行有意义且完整的测试变得困难。目前可以使用的测试功能可参见puppet.com/docs/bolt/latest/testing_plans.html

介绍 YAML 计划

由于 YAML 计划的使用频率远低于 Puppet 计划,这里将简要概述 YAML 计划。它们的命名方式与基于 Puppet 的计划类似,但以.yaml扩展名(而非.yml)结尾。然而,没有创建它们的命令。YAML 计划包含以下内容:

  • 描述:将在show命令中显示的内容

  • 参数:可以传递给计划的参数哈希

  • 私有:一个布尔值,表示计划是否对show命令可见

  • 返回值:计划返回的数组、布尔值、哈希值、数字或字符串

  • 步骤:将要运行的步骤数组

步骤本质上表示将在该步骤中执行的操作和步骤所需的变量。Bolt 中可用的选项与 Puppet 计划中的操作类似,例如命令、任务、脚本、文件下载和文件上传。与 Puppet 计划一样,YAML 计划可以通过计划步骤调用其他计划。

以下示例任务计划使用来自 Forge 的 Docker puppetlabs模块forge.puppet.com/modules/puppetlabs/docker来创建并加入一个额外的管理节点到 Docker swarm,展示了一些这些功能的使用:

description: configure docker swarm
paramters:
  firstnode
    type: TargetSpec
  Othernodes
    Type: Targetspec
- name: init
    task: docker::swarm_init
    targets: $firstnode
  - name: token
    task: docker::swarm_token
    targets: $firstnode
  - name:facts
    Fact:
    targets: $firstnode
  - name: managersjoin
    task: docker::join_swarm
    targets: $othernodes
    parameters:
      token: $token.map |$token_result| { $token_result['stdout'] }
       manager_ip: $facts.map |$facts_result| { $facts_result['stdout']['networking']['interfaces']['ip'] }
return $managersjoin.map | $managersjoin_result| {$managersjoin_result['stdout']}

该任务使用TargetSpec类型的firstnodeothernodes变量将服务器提供给目标。它使用swarm_init任务在第一个节点上初始化,并在此节点上运行swarm_token任务。接下来,Fact任务将在firstnode上运行,最后一步,join_swarm任务将在othernodes上运行。可以看到,调用具有前一步名称的变量可以让我们访问该步骤创建的输出。因此,我们可以获取令牌步骤的输出,并映射返回的 taskspec 类型,将stdout作为令牌使用。对于manager_ip参数,我们执行类似的操作,但这次,由于stdout中有更多内容,我们必须找到希望传递的networking.interface.ip地址事实。计划接着设置返回键,使用join步骤的stdout输出来确认计划的结果。

还可以使用eval步骤来计算值,并且可以使用 Puppet 和 Bolt 函数。messageverbose步骤用于输出,就像在 Puppet 计划中一样,而字符串插值遵循正常的 Puppet 原则,单引号('')没有插值,仅打印文本,双引号("")进行插值,同时使用管道符(|)和换行符可以将一块 Puppet 代码的表达式显示到下一行。

为了展示其中的一些内容,以下计划将安装一组字符串作为包:

parameters:
  packages:
    type: Array[String]
  servers:
    type: Targetspec
Steps:
  -name: unique_packages
  eval: $packages.unique
  -name: numer_of_packages
  eval: $unique_packages.size
  - verbose: 'Installing ${number_of_packages} packages'
  - name: install
    task: example::install_packages
    parameters:
      packages:  $unique_packages
      Targets: $servers
Return: $install.map | $install_result| {$install_result['stdout']}

我们可以看到,unique_packages eval 步骤使用 unique 函数来查找数组中的唯一值,而 numer_of_packages eval 步骤使用 size 函数,其结果传递给 verbose 输出并插入到一个字符串中,显示包的数量。example::install_packages 任务在 unique_packages 评估步骤的输出之后运行,然后将其输出用于返回值。

这只是使用 YAML 计划的总结。每个步骤的完整选项和更多内容可以在文档中找到:puppet.com/docs/bolt/latest/writing_yaml_plans.html

在接下来的部分,我们将查看一些常用的 Bolt 插件示例。

插件

插件允许 Bolt 在执行期间动态加载数据。插件本质上只是包含任务的模块,bolt_plugin.json 文件标识了哪些任务是插件,以及它们是何种类型的插件。有些插件内建于 Bolt 中,而其他插件可以被添加以扩展功能。

Bolt 插件有三种类型:

  • 引用:用于从外部源获取数据,例如将信息加载到库存文件中

  • 秘密:用于创建密钥来加密文本和解密密文

  • 在目标上调用 apply_prep 函数

我们将在以下小节中详细查看这些内容。

引用插件

inventory.yamlbolt-project.yaml 文件使用 _plugin 键,其值为插件名称,并随后列出与插件相关的参数。例如,要使用 puppetdb 插件并查询和选择 PuppetDB 中所有窗口节点,我们可以在 inventory.yaml 中添加以下组:

groups:
  - name: windows
    targets:
      - _plugin: puppetdb
        query: 'inventory[certname] { facts.kernel = "Windows" }'

这是假设 PuppetDB 连接配置详情已设置在其中一个配置文件中。

注意

配置了 PuppetDB 插件后,可以使用类似以下的单次查询来查询 PuppetDB:bolt task run 'inventory[certname] { facts.kernel = "``Windows" }'

另一种引用插件的方法是使用密码,其中 prompt 插件会提示用户从命令行输入密码。例如,以下内容将确保在对 target1.example.com 进行操作时,Bolt 会使用 winrm 连接,用户为 bill,密码则通过提示 Enter your password 来输入:

targets:
  - target1.example.com
  config:
  winrm:
    user: bill
    password:
      _plugin: prompt
      message: Enter your password

插件还可以通过 resolve_references 函数在计划中使用。以下示例展示了 pecdm 模块通过 resolve_references 函数使用插件的一个小节:

$inventory = ['server', 'psql', 'compiler', 'node', 'windows_node' ].reduce({}) |Hash $memo, String $i| {,
$memo + { $i => resolve_references( {
'_plugin' => 'terraform',
'dir' => $tf_dir,

在前面的代码块中,它本质上是通过每个组名进行迭代,并构建一个从 tf_dir 变量设置的 terraform 目录中读取的目标条目的数组。要查看进一步的示例,请查看你的实验室设置中的 inventory.yaml 文件内容,该文件使用了 terraform 插件。

秘密插件

pckcs7是 Bolt 的默认且唯一的密钥插件。要创建加密密钥,请运行bolt secret createkeys -–force Unix 命令或New-BoltSecretKey -Force PowerShell cmdlet。这将在项目的keys文件夹中创建密钥。可以通过bolt secret encrypt 'N33dt0kn0wba515!' --plugin pckcs7 Unix 命令或Protect-BoltSecret -Text 'N33dt0kn0wba515! ' -Plugin pckcs7 PowerShell cmdlet 生成密文。

此命令生成的密文可以在如inventory.yaml这样的地方使用,举个例子,使用pkcs7引用插件:

targets:
  - uri: target1.example.com
    config:
      ssh:
        password:
          _plugin: pkcs7
          encrypted_value: |
            ENC[PKCS7,MIIBiQYJK]

请注意,之前的加密字符串已经缩短,且其默认密钥大小为2048。可以通过在bolt-project.yaml文件中配置插件或默认配置和用户配置来更改这一点。

Puppet 库

apply_prep函数在计划中被调用。插件运行的每个目标必须能够使用插件所用的脚本语言。目前,只有puppet-agent作为 Puppet 库插件存在,并且默认配置为可用。但任何未来的库或自定义编写的库,都将以类似本例的方式添加到 Bolt、用户或默认配置中:

plugin-hooks:
  puppet_library:
    plugin: task
    task: package
    parameters:
      name: puppet-agent
      action: install

支持的内置插件完整列表可以在puppet.com/docs/bolt/latest/supported_plugins.html查看。编写插件不在本书的范围内,但puppet.com/docs/bolt/latest/writing_plugins.html中的文档提供了进一步的建议。

在详细讲解了 Bolt 后,我们将在以下实验中实践创建和使用 Bolt 项目。

实验 - 创建和使用 Bolt 项目

在本实验中,我们将创建一个 Bolt 项目。我们将创建一个任务,在 Windows 和 Linux 节点上运行facter命令。

步骤如下:

  • 使用以下代码行创建一个 Bolt 项目:

    bolt project init packtlab
    
  • 通过在 PECDM Bolt 项目中查找 Windows 和 Linux 客户端并复制输出,创建一个inventory.yaml文件:

    bolt inventory show --targets agent_nodes --detail
    bolt inventory show --targets windows_agent_nodes --detail
    
  • 编写一个任务,覆盖 Windows 和 Linux,运行facter命令,如果只需要返回单一事实,则传递一个参数。

  • 编写一个计划,使用run_command来运行facter并返回计划的结果。

  • 在 Windows 和 Linux 客户端上运行任务和计划。

  • 你可以在github.com/PacktPublishing/Puppet-8-for-DevOps-Engineers/tree/main/ch12找到示例解决方案。

总结

在这一章中,我们展示了 Bolt 如何通过提供执行临时操作的能力来补充 Puppet 的状态管理方法,处理那些不符合 Puppet 声明式强制方法的任务。我们还展示了传输方式如何使 Bolt 能够连接到目标。我们查看了如何通过 Unix 或 PowerShell 使用 Bolt 命令来执行命令、脚本、Puppet 代码和清单,此外还可以上传和下载文件。我们回顾了 Bolt 如何记录到 bolt-debug.log 文件,并且如何配置日志记录以获取更多的日志,以便解决不同的问题。

然后我们展示了 Bolt 项目如何提供目录结构来存储配置和数据。Bolt 项目提供 inventory.yaml 文件来存储目标和传输配置,提供 bolt-project.yaml 文件来存储项目级别的配置设置,并允许将模块依赖项下载到项目中。我们讨论了如何将 Bolt 项目加载到模块路径中,并与其下载的任何模块一起使用。随后,我们强调了项目格式如何随着不同版本的 Bolt 发生变化,以及如何使用 bolt migrate 命令将旧版本项目转换为新格式。

接着我们讨论了任务是单一操作脚本,可以使用任何在目标机器上运行的语言,配合 JSON 文件提供元数据,如参数。我们还展示了如何根据目标列出多个实现。我们查看了如何使用敏感参数使得任务能够使用密码和其他机密信息,而不会在 API 中记录。我们介绍了 noop 选项,它作为标准方式传递参数给任务并以不执行模式运行。我们还展示了如何远程任务包含 remote 参数,设置为 true,并使用远程传输方式,使得 Web 访问服务能够使用任务,即便不能通过传统方式登录。

接着,我们讨论了任务如何能够共享实现中的脚本,并引用其他模块。我们还讨论了一些安全实践,以确保参数能安全地传递给任务。

接着讨论了计划(Plans),它们是将多个任务一起运行并提供逻辑和控制流的方式。我们看到计划可以使用 Puppet 语言或 YAML 编写,目标可以通过 targetspec 数据类型和函数创建。我们还看到了如何在运行计划后返回结构化的结果。

我们接着讨论了 Bolt 插件如何通过引用插件来动态加载数据到 Bolt 运行中,使用引用插件来获取和存储数据,例如从 Terraform 填充数据到清单中。我们还可以使用机密插件提供加密和解密值所需的密钥,以便在 Bolt 运行中使用。我们查看的第三种插件是 Puppet 库插件,目前仅实现了通过 Bolt 安装 Puppet 代理。

在本章中,我们看到如何将 Bolt 与 Puppet 配合使用,结合声明式和有状态的语言方法,发挥两者的优势,从而使 Puppet 配置更加灵活。

在回顾了如何使用 Bolt 和 Puppet Enterprise 之后,在下一章中,我们将探讨如何监控和扩展 Puppet 基础设施、审查性能问题,并使用Puppet 数据服务来实现外部数据模式,允许用户通过自助服务 API 将数据输入到 Puppet 设置中。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值