Puppet 是一个配置管理系统。 它用于使主机进入所需状态并维持该状态。
我与 Puppet 合作已有五年多了。 本文本质上是对官方文档中的要点进行翻译和重新排序的汇编,这将让初学者快速理解 Puppet 的本质。
基本信息
Puppet 的操作系统是客户端-服务器,尽管它也支持功能有限的无服务器操作。
使用拉操作模型:默认情况下,客户端每半小时联系服务器一次以获取配置并应用它。 如果您使用过 Ansible,那么他们会使用不同的推送模型:管理员启动应用配置的过程,客户端本身不会应用任何内容。
网络通信时,采用双向TLS加密:服务器和客户端都有自己的私钥和相应的证书。 通常,服务器为客户端颁发证书,但原则上可以使用外部 CA。
宣言简介
在 Puppet 术语中 到傀儡服务器 已连接 节点 (节点)。 节点的配置已写入 在宣言中 使用特殊的编程语言 - Puppet DSL。
Puppet DSL 是一种声明性语言。 它以各个资源声明的形式描述节点的所需状态,例如:
- 该文件存在并且具有特定内容。
- 该软件包已安装。
- 服务已开始。
资源可以互联:
- 存在依赖关系,它们影响资源的使用顺序。
例如,“首先安装包,然后编辑配置文件,然后启动服务。” - 有通知 - 如果资源发生更改,它会向订阅它的资源发送通知。
例如,如果配置文件发生更改,您可以自动重新启动服务。
此外,Puppet DSL 还具有函数和变量,以及条件语句和选择器。 还支持各种模板机制 - EPP 和 ERB。
Puppet 是用 Ruby 编写的,因此许多结构和术语都取自 Ruby。 Ruby 允许您扩展 Puppet - 添加复杂的逻辑、新类型的资源、功能。
当 Puppet 运行时,服务器上每个特定节点的清单都会编译到一个目录中。 目录 是计算函数、变量和条件语句扩展的值后的资源及其关系的列表。
语法和代码风格
如果提供的示例还不够,以下是官方文档的部分内容,可帮助您理解语法:
以下是清单的示例:
# Комментарии пишутся, как и много где, после решётки.
#
# Описание конфигурации ноды начинается с ключевого слова node,
# за которым следует селектор ноды — хостнейм (с доменом или без)
# или регулярное выражение для хостнеймов, или ключевое слово default.
#
# После этого в фигурных скобках описывается собственно конфигурация ноды.
#
# Одна и та же нода может попасть под несколько селекторов. Про приоритет
# селекторов написано в статье про синтаксис описания нод.
node 'hostname', 'f.q.d.n', /regexp/ {
# Конфигурация по сути является перечислением ресурсов и их параметров.
#
# У каждого ресурса есть тип и название.
#
# Внимание: не может быть двух ресурсов одного типа с одинаковыми названиями!
#
# Описание ресурса начинается с его типа. Тип пишется в нижнем регистре.
# Про разные типы ресурсов написано ниже.
#
# После типа в фигурных скобках пишется название ресурса, потом двоеточие,
# дальше идёт опциональное перечисление параметров ресурса и их значений.
# Значения параметров указываются через т.н. hash rocket (=>).
resource { 'title':
param1 => value1,
param2 => value2,
param3 => value3,
}
}
缩进和换行不是清单的必需部分,但建议这样做
- 不使用两个空格缩进、制表符。
- 大括号由空格分隔;冒号不由空格分隔。
- 每个参数后面都加逗号,包括最后一个。 每个参数都位于单独的行上。 没有参数和一个参数的情况是例外:您可以写在一行上且不带逗号(即:
resource { 'title': }
иresource { 'title': param => value }
). - 参数上的箭头应处于同一水平。
- 前面写着资源关系箭头。
pappetserver 上文件的位置
为了进一步解释,我将引入“根目录”的概念。 根目录是包含特定节点的 Puppet 配置的目录。
根目录根据 Puppet 版本和使用的环境而有所不同。 环境是存储在单独目录中的独立配置集。 通常与 git 结合使用,在这种情况下,环境是从 git 分支创建的。 因此,每个节点位于一种环境或另一种环境中。 这可以在节点本身上配置,也可以在 ENC 中配置,我将在下一篇文章中讨论。
- 在第三个版本(“旧 Puppet”)中,基本目录是
/etc/puppet
。 环境的使用是可选的 - 例如,我们不将它们与旧的 Puppet 一起使用。 如果使用环境,它们通常存储在/etc/puppet/environments
,根目录将是环境目录。 如果不使用环境,根目录将是基目录。 - 从第四个版本(“新 Puppet”)开始,环境的使用成为强制性的,并且基本目录被移动到
/etc/puppetlabs/code
。 因此,环境存储在/etc/puppetlabs/code/environments
,根目录是环境目录。
根目录下必须有子目录 manifests
,其中包含一个或多个描述节点的清单。 另外,还应该有一个子目录 modules
,其中包含模块。 稍后我会告诉你哪些模块。 另外,旧的Puppet可能还有一个子目录 files
,其中包含我们复制到节点的各种文件。 在新的 Puppet 中,所有文件都放置在模块中。
清单文件的扩展名 .pp
.
几个战斗例子
节点及其资源的描述
在节点上 server1.testdomain
必须创建一个文件 /etc/issue
有内容 Debian GNU/Linux n l
。 该文件必须由用户和组拥有 root
,访问权限必须是 644
.
我们写下宣言:
node 'server1.testdomain' { # блок конфигурации, относящийся к ноде server1.testdomain
file { '/etc/issue': # описываем файл /etc/issue
ensure => present, # этот файл должен существовать
content => 'Debian GNU/Linux n l', # у него должно быть такое содержимое
owner => root, # пользователь-владелец
group => root, # группа-владелец
mode => '0644', # права на файл. Они заданы в виде строки (в кавычках), потому что иначе число с 0 в начале будет воспринято как записанное в восьмеричной системе, и всё пойдёт не так, как задумано
}
}
节点上资源之间的关系
在节点上 server2.testdomain
nginx 必须正在运行,并使用之前准备好的配置。
我们来分解一下问题:
- 需要安装该包
nginx
. - 需要从服务器复制配置文件。
- 该服务需要运行
nginx
. - 如果更新配置,则必须重新启动服务。
我们写下宣言:
node 'server2.testdomain' { # блок конфигурации, относящийся к ноде server2.testdomain
package { 'nginx': # описываем пакет nginx
ensure => installed, # он должен быть установлен
}
# Прямая стрелка (->) говорит о том, что ресурс ниже должен
# создаваться после ресурса, описанного выше.
# Такие зависимости транзитивны.
-> file { '/etc/nginx': # описываем файл /etc/nginx
ensure => directory, # это должна быть директория
source => 'puppet:///modules/example/nginx-conf', # её содержимое нужно брать с паппет-сервера по указанному адресу
recurse => true, # копировать файлы рекурсивно
purge => true, # нужно удалять лишние файлы (те, которых нет в источнике)
force => true, # удалять лишние директории
}
# Волнистая стрелка (~>) говорит о том, что ресурс ниже должен
# подписаться на изменения ресурса, описанного выше.
# Волнистая стрелка включает в себя прямую (->).
~> service { 'nginx': # описываем сервис nginx
ensure => running, # он должен быть запущен
enable => true, # его нужно запускать автоматически при старте системы
}
# Когда ресурс типа service получает уведомление,
# соответствующий сервис перезапускается.
}
为此,您大约需要 puppet 服务器上的以下文件位置:
/etc/puppetlabs/code/environments/production/ # (это для нового Паппета, для старого корневой директорией будет /etc/puppet)
├── manifests/
│ └── site.pp
└── modules/
└── example/
└── files/
└── nginx-conf/
├── nginx.conf
├── mime.types
└── conf.d/
└── some.conf
资源类型
可以在此处找到支持的资源类型的完整列表
文件
管理文件、目录、符号链接、其内容和访问权限。
选项:
- 资源名称 — 文件路径(可选)
- 径 — 文件路径(如果名称中未指定)
- 确保 - 文件类型:
absent
- 删除一个文件present
— 必须有任何类型的文件(如果没有文件,将创建一个常规文件)file
- 常规文件directory
- 目录link
- 符号链接
- 内容 — 文件内容(仅适用于常规文件,不能与 资源 или 目标)
- 资源 — 指向要从中复制文件内容的路径的链接(不能与 内容 или 目标)。 可以指定为带有方案的 URI
puppet:
(然后将使用来自 puppet 服务器的文件),并使用该方案http:
(我希望清楚在这种情况下会发生什么),即使有图表file:
或者作为没有模式的绝对路径(然后将使用节点上本地 FS 中的文件) - 目标 — 符号链接应指向的位置(不能与 内容 или 资源)
- 业主 — 应该拥有该文件的用户
- 组 — 文件应属于的组
- 模式 — 文件权限(作为字符串)
- 递归 - 启用递归目录处理
- 清除 - 允许删除 Puppet 中未描述的文件
- 力 - 允许删除 Puppet 中未描述的目录
包
安装和删除软件包。 能够处理通知 - 如果指定了参数,则重新安装包 刷新时重新安装.
选项:
- 资源名称 — 包名称(可选)
- 姓名 — 包名称(如果名称中未指定)
- 提供者 — 使用的包管理器
- 确保 — 所需的包装状态:
present
,installed
- 安装的任何版本latest
- 安装最新版本absent
- 已删除(apt-get remove
)purged
— 与配置文件一起删除(apt-get purge
)held
- 软件包版本已锁定(apt-mark hold
)любая другая строка
— 指定版本已安装
- 刷新时重新安装 -如果
true
,然后在收到通知后将重新安装该软件包。 对于基于源代码的发行版非常有用,在这种发行版中,更改构建参数时可能需要重新构建包。 默认false
.
服务
管理服务。 能够处理通知 - 重新启动服务。
选项:
- 资源名称 — 要管理的服务(可选)
- 姓名 — 需要管理的服务(如果名称中没有指定)
- 确保 — 期望的服务状态:
running
- 推出stopped
- 停止了
- enable — 控制启动服务的能力:
true
— 自动运行已启用(systemctl enable
)mask
- 伪装(systemctl mask
)false
— 自动运行被禁用(systemctl disable
)
- 重新开始 - 重新启动服务的命令
- 状态 — 检查服务状态的命令
- 已重新启动 — 指示服务 initscript 是否支持重新启动。 如果
false
并且指定了参数 重新开始 — 使用此参数的值。 如果false
和参数 重新开始 未指定 - 服务已停止并开始重新启动(但 systemd 使用命令systemctl restart
). - 状态 — 指示服务初始化脚本是否支持该命令
status
。 如果false
,然后使用参数值 状态。 默认true
.
EXEC
运行外部命令。 如果不指定参数 创建, 除非, 除非 или 仅刷新,每次运行 Puppet 时都会运行该命令。 能够处理通知 - 运行命令。
选项:
- 资源名称 — 要执行的命令(可选)
- 命令 — 要执行的命令(如果名称中未指定)
- 径 — 查找可执行文件的路径
- 除非 — 如果此参数中指定的命令以零返回代码完成,则将执行主命令
- 除非 — 如果此参数中指定的命令以非零返回码完成,则将执行主命令
- 创建 — 如果该参数指定的文件不存在,则执行主命令
- 仅刷新 -如果
true
,那么只有当此 exec 收到来自其他资源的通知时才会运行该命令 - 电脑 — 运行命令的目录
- 用户 — 运行命令的用户
- 提供者 - 如何运行命令:
- POSIX — 简单地创建了一个子进程,请务必指定 径
- 壳 - 该命令在 shell 中启动
/bin/sh
, 可以不指定 径,您可以使用通配符、管道和其他 shell 功能。 通常会自动检测是否有任何特殊字符(|
,;
,&&
,||
等等)。
cron的
控制 cronjobs。
选项:
- 资源名称 - 只是某种标识符
- 确保 — 皇冠工作状态:
present
- 如果不存在则创建absent
- 如果存在则删除
- 命令 - 要运行什么命令
- 环境 — 在哪个环境中运行命令(环境变量及其值的列表,通过
=
) - 用户 — 从哪个用户运行命令
- 分钟, 小时, 平日, 月, 月日 — 何时运行 cron。 如果未指定任何这些属性,则其在 crontab 中的值将为
*
.
在木偶 6.0 中 cron的 好像
关于一般资源
资源唯一性要求
我们最常遇到的错误是 重复申报。 当目录中出现两个或多个同类型同名资源时,会出现此错误。
因此,我再写一次: 同一节点的清单不应包含具有相同标题的相同类型的资源!
有时需要安装具有相同名称但使用不同包管理器的包。 在这种情况下,您需要使用参数 name
避免错误:
package { 'ruby-mysql':
ensure => installed,
name => 'mysql',
provider => 'gem',
}
package { 'python-mysql':
ensure => installed,
name => 'mysql',
provider => 'pip',
}
其他资源类型有类似的选项来帮助避免重复 - name
у 服务, command
у EXEC, 等等。
元参数
每种资源类型都有一些特殊参数,无论其性质如何。
元参数的完整列表
短名单:
- 要求 — 该参数表示该资源依赖于哪些资源。
- before - 该参数指定哪些资源依赖于该资源。
- 订阅 — 该参数指定该资源从哪些资源接收通知。
- 通知 — 该参数指定哪些资源接收来自该资源的通知。
所有列出的元参数都接受单个资源链接或方括号中的链接数组。
资源链接
资源链接只是对资源的提及。 它们主要用于指示依赖关系。 引用不存在的资源将导致编译错误。
链接的语法如下:资源类型用大写字母(如果类型名称包含双冒号,则冒号之间的名称各部分均大写),然后方括号中的资源名称(名称的大小写不会改变!)。 不应有空格;方括号紧接在类型名称之后。
示例:
file { '/file1': ensure => present }
file { '/file2':
ensure => directory,
before => File['/file1'],
}
file { '/file3': ensure => absent }
File['/file1'] -> File['/file3']
依赖项和通知
如前所述,资源之间的简单依赖关系是可传递的。 顺便说一句,添加依赖项时要小心 - 您可以创建循环依赖项,这将导致编译错误。
与依赖关系不同,通知是不可传递的。 以下规则适用于通知:
- 如果资源收到通知,则会更新。 更新操作取决于资源类型 - EXEC 运行命令, 服务 重新启动服务, 包 重新安装该软件包。 如果资源没有定义更新操作,则不会发生任何事情。
- 在 Puppet 的一次运行期间,资源更新不会超过一次。 这是可能的,因为通知包含依赖关系并且依赖关系图不包含循环。
- 如果 Puppet 更改资源的状态,该资源会向订阅它的所有资源发送通知。
- 如果资源更新,它会向订阅该资源的所有资源发送通知。
处理未指定的参数
通常,如果某些资源参数没有默认值,并且清单中未指定该参数,则 Puppet 不会更改节点上相应资源的该属性。 例如,如果类型的资源 文件 未指定参数 owner
,那么Puppet不会改变相应文件的所有者。
类、变量和定义简介
假设我们有几个节点,它们具有相同的配置部分,但也存在差异 - 否则我们可以在一个块中描述所有内容 node {}
。 当然,您可以简单地复制配置的相同部分,但总的来说,这是一个糟糕的解决方案 - 配置会增长,如果您更改配置的一般部分,则必须在许多地方编辑相同的内容。 同时,很容易犯错误,一般来说,DRY(不要重复自己)原则的发明是有原因的。
为了解决这个问题有这样的设计 类.
类
首先需要描述类。 描述本身不会在任何地方添加任何资源。 该类在清单中进行了描述:
# Описание класса начинается с ключевого слова class и его названия.
# Дальше идёт тело класса в фигурных скобках.
class example_class {
...
}
之后可以使用该类:
# первый вариант использования — в стиле ресурса с типом class
class { 'example_class': }
# второй вариант использования — с помощью функции include
include example_class
# про отличие этих двух вариантов будет рассказано дальше
上一个任务的示例 - 让我们将 nginx 的安装和配置移到一个类中:
class nginx_example {
package { 'nginx':
ensure => installed,
}
-> file { '/etc/nginx':
ensure => directory,
source => 'puppet:///modules/example/nginx-conf',
recure => true,
purge => true,
force => true,
}
~> service { 'nginx':
ensure => running,
enable => true,
}
}
node 'server2.testdomain' {
include nginx_example
}
变量
上一个示例中的类根本不灵活,因为它总是带来相同的 nginx 配置。 让我们将路径设置为配置变量,然后该类可用于以任何配置安装 nginx。
可以办到
注意:Puppet 中的变量是不可变的!
另外,变量只有在声明后才能访问,否则变量的值将被 undef
.
使用变量的示例:
# создание переменных
$variable = 'value'
$var2 = 1
$var3 = true
$var4 = undef
# использование переменных
$var5 = $var6
file { '/tmp/text': content => $variable }
# интерполяция переменных — раскрытие значения переменных в строках. Работает только в двойных кавычках!
$var6 = "Variable with name variable has value ${variable}"
傀儡有 命名空间,相应地,变量有 视野范围:同名变量可以定义在不同的命名空间中。 解析变量的值时,将在当前命名空间中搜索该变量,然后在封闭的命名空间中搜索,依此类推。
命名空间示例:
- 全局 - 类或节点描述之外的变量位于此处;
- 节点描述中的节点命名空间;
- 类描述中的类命名空间。
为了避免访问变量时出现歧义,可以在变量名中指定命名空间:
# переменная без пространства имён
$var
# переменная в глобальном пространстве имён
$::var
# переменная в пространстве имён класса
$classname::var
$::classname::var
我们同意 nginx 配置的路径位于变量中 $nginx_conf_source
。 然后类将如下所示:
class nginx_example {
package { 'nginx':
ensure => installed,
}
-> file { '/etc/nginx':
ensure => directory,
source => $nginx_conf_source, # здесь используем переменную вместо фиксированной строки
recure => true,
purge => true,
force => true,
}
~> service { 'nginx':
ensure => running,
enable => true,
}
}
node 'server2.testdomain' {
$nginx_conf_source = 'puppet:///modules/example/nginx-conf'
include nginx_example
}
然而,给出的例子很糟糕,因为有一些“秘密知识”,即类中的某个地方使用了具有这样那样名称的变量。 让这些知识变得普遍化是更正确的——类可以有参数。
类参数 是类命名空间中的变量,它们在类头中指定,并且可以像类主体中的常规变量一样使用。 参数值是在使用清单中的类时指定的。
该参数可以设置为默认值。 如果一个参数没有默认值并且使用时没有设置该值,就会导致编译错误。
让我们对上面示例中的类进行参数化,并添加两个参数:第一个参数是必需的,是配置的路径,第二个参数是可选的,是 nginx 的包的名称(例如,在 Debian 中,有包 nginx
, nginx-light
, nginx-full
).
# переменные описываются сразу после имени класса в круглых скобках
class nginx_example (
$conf_source,
$package_name = 'nginx-light', # параметр со значением по умолчанию
) {
package { $package_name:
ensure => installed,
}
-> file { '/etc/nginx':
ensure => directory,
source => $conf_source,
recurse => true,
purge => true,
force => true,
}
~> service { 'nginx':
ensure => running,
enable => true,
}
}
node 'server2.testdomain' {
# если мы хотим задать параметры класса, функция include не подойдёт* — нужно использовать resource-style declaration
# *на самом деле подойдёт, но про это расскажу в следующей серии. Ключевое слово "Hiera".
class { 'nginx_example':
conf_source => 'puppet:///modules/example/nginx-conf', # задаём параметры класса точно так же, как параметры для других ресурсов
}
}
在 Puppet 中,变量是类型化的。 吃
类型写在参数名称之前:
class example (
String $param1,
Integer $param2,
Array $param3,
Hash $param4,
Hash[String, String] $param5,
) {
...
}
类:包括类名 vs 类{'classname':}
每个类都是一个类型的资源 程。 与任何其他类型的资源一样,同一节点上不能有同一类的两个实例。
如果您尝试使用以下命令将类添加到同一节点两次 class { 'classname':}
(没有区别,参数不同或相同),会出现编译错误。 但如果您使用资源样式的类,则可以立即在清单中显式设置其所有参数。
但是,如果您使用 include
,那么可以根据需要多次添加该类。 事实是 include
是一个幂等函数,用于检查类是否已添加到目录中。 如果该类不在目录中,则会将其添加,如果已经存在,则不执行任何操作。 但如果使用 include
您无法在类声明期间设置类参数 - 所有必需的参数必须在外部数据源 - Hiera 或 ENC 中设置。 我们将在下一篇文章中讨论它们。
定义
正如上一个块中所述,同一个类不能在一个节点上出现多次。 但是,在某些情况下,您需要能够在同一节点上使用具有不同参数的同一代码块。 换句话说,需要一种自己的资源类型。
例如,为了安装 PHP 模块,我们在 Avito 中执行以下操作:
- 安装包含此模块的软件包。
- 让我们为此模块创建一个配置文件。
- 我们创建 php-fpm 配置的符号链接。
- 我们为 php cli 创建一个指向配置的符号链接。
在这种情况下,诸如 $title
,资源名称在声明时所在的位置。 就像类的情况一样,必须首先描述定义,然后才能使用它。
带有 PHP 模块的简化示例:
define php74::module (
$php_module_name = $title,
$php_package_name = "php7.4-${title}",
$version = 'installed',
$priority = '20',
$data = "extension=${title}.son",
$php_module_path = '/etc/php/7.4/mods-available',
) {
package { $php_package_name:
ensure => $version,
install_options => ['-o', 'DPkg::NoTriggers=true'], # триггеры дебиановских php-пакетов сами создают симлинки и перезапускают сервис php-fpm - нам это не нужно, так как и симлинками, и сервисом мы управляем с помощью Puppet
}
-> file { "${php_module_path}/${php_module_name}.ini":
ensure => $ensure,
content => $data,
}
file { "/etc/php/7.4/cli/conf.d/${priority}-${php_module_name}.ini":
ensure => link,
target => "${php_module_path}/${php_module_name}.ini",
}
file { "/etc/php/7.4/fpm/conf.d/${priority}-${php_module_name}.ini":
ensure => link,
target => "${php_module_path}/${php_module_name}.ini",
}
}
node server3.testdomain {
php74::module { 'sqlite3': }
php74::module { 'amqp': php_package_name => 'php-amqp' }
php74::module { 'msgpack': priority => '10' }
}
捕获重复声明错误的最简单方法是在 Define 中。 如果定义具有带有常量名称的资源,并且某个节点上有该定义的两个或多个实例,则会发生这种情况。
保护自己免受这种情况的影响很容易:定义中的所有资源都必须有一个名称,具体取决于 $title
。 另一种方法是幂等添加资源;在最简单的情况下,将定义的所有实例共有的资源移动到一个单独的类中并将该类包含在定义中就足够了 - 函数 include
幂等的。
添加资源时还有其他方法可以实现幂等性,即使用函数 defined
и ensure_resources
,但我会在下一集中告诉你。
类和定义的依赖关系和通知
类和定义添加以下规则来处理依赖项和通知:
- 对类/定义的依赖会增加对该类/定义的所有资源的依赖;
- 类/定义依赖项将依赖项添加到所有类/定义资源;
- class/define通知通知该class/define的所有资源;
- class/define 订阅订阅该 class/define 的所有资源。
条件语句和选择器
if
这里的一切都很简单:
if ВЫРАЖЕНИЕ1 {
...
} elsif ВЫРАЖЕНИЕ2 {
...
} else {
...
}
除非
except 是 if 的倒转:如果表达式为 false,则将执行代码块。
unless ВЫРАЖЕНИЕ {
...
}
案件
这里也没有什么复杂的。 您可以使用正则值(字符串、数字等)、正则表达式和数据类型作为值。
case ВЫРАЖЕНИЕ {
ЗНАЧЕНИЕ1: { ... }
ЗНАЧЕНИЕ2, ЗНАЧЕНИЕ3: { ... }
default: { ... }
}
选择器
选择器是一种类似于 case
,但它不是执行代码块,而是返回一个值。
$var = $othervar ? { 'val1' => 1, 'val2' => 2, default => 3 }
模块
当配置较小时,可以轻松地将其保存在一个清单中。 但是我们描述的配置越多,清单中的类和节点就越多,它就会增长,并且使用起来变得不方便。
此外,还存在代码重用的问题——当所有代码都在一个清单中时,很难与其他人共享此代码。 为了解决这两个问题,Puppet 有一个称为模块的实体。
模块 - 这些是放置在单独目录中的类、定义和其他 Puppet 实体的集合。 换句话说,模块是 Puppet 逻辑的一个独立部分。 例如,可能有一个用于与 nginx 一起工作的模块,并且它将包含并且仅包含与 nginx 一起工作所需的内容,或者可能有一个用于与 PHP 一起工作的模块,等等。
模块是有版本的,并且还支持模块之间的依赖关系。 有一个开放的模块存储库 -
在puppet服务器上,模块位于根目录的modules子目录中。 每个模块内部都有一个标准的目录方案 - 清单、文件、模板、lib 等等。
模块中的文件结构
模块的根目录可能包含以下具有描述性名称的目录:
manifests
- 它包含宣言files
- 它包含文件templates
- 它包含模板lib
— 它包含 Ruby 代码
这不是目录和文件的完整列表,但目前对于本文来说已经足够了。
模块中的资源名称和文件名称
模块中的资源(类、定义)不能随意命名。 此外,资源名称与 Puppet 将在其中查找该资源描述的文件名称之间存在直接对应关系。 如果违反命名规则,Puppet 根本找不到资源描述,并且会出现编译错误。
规则很简单:
- 模块中的所有资源都必须位于模块命名空间中。 如果模块被调用
foo
,那么其中的所有资源都应该被命名foo::<anything>
要不就foo
. - 具有模块名称的资源必须位于文件中
init.pp
. - 对于其他资源,文件命名方案如下:
- 模块名称的前缀被丢弃
- 所有双冒号(如果有)都替换为斜杠
- 添加了扩展名
.pp
我将用一个例子来演示。 假设我正在编写一个模块 nginx
。 它包含以下资源:
- 类
nginx
清单中描述的init.pp
; - 类
nginx::service
清单中描述的service.pp
; - 定义
nginx::server
清单中描述的server.pp
; - 定义
nginx::server::location
清单中描述的server/location.pp
.
模板
想必大家都知道什么是模板,这里就不详细介绍了。 但我会留下它以防万一
如何使用模板:可以使用函数扩展模板的含义 template
,它传递到模板的路径。 对于类型资源 文件 与参数一起使用 content
。 例如,像这样:
file { '/tmp/example': content => template('modulename/templatename.erb')
查看路径 <modulename>/<filename>
暗示文件 <rootdir>/modules/<modulename>/templates/<filename>
.
此外,还有一个功能 inline_template
— 它接收模板文本作为输入,而不是文件名。
在模板中,您可以使用当前范围内的所有 Puppet 变量。
Puppet 支持 ERB 和 EPP 格式的模板:
简单介绍一下ERB
控制结构:
<%= ВЫРАЖЕНИЕ %>
— 插入表达式的值<% ВЫРАЖЕНИЕ %>
— 计算表达式的值(不插入它)。 条件语句 (if) 和循环 (each) 通常放在这里。<%# КОММЕНТАРИЙ %>
ERB 中的表达式是用 Ruby 编写的(ERB 实际上是嵌入式 Ruby)。
要从清单访问变量,您需要添加 @
到变量名。 要删除出现在控件结构之后的换行符,您需要使用结束标记 -%>
.
使用模板的示例
假设我正在编写一个模块来控制 ZooKeeper。 负责创建配置的类看起来像这样:
class zookeeper::configure (
Array[String] $nodes,
Integer $port_client,
Integer $port_quorum,
Integer $port_leader,
Hash[String, Any] $properties,
String $datadir,
) {
file { '/etc/zookeeper/conf/zoo.cfg':
ensure => present,
content => template('zookeeper/zoo.cfg.erb'),
}
}
以及对应的模板 zoo.cfg.erb
- 所以:
<% if @nodes.length > 0 -%>
<% @nodes.each do |node, id| -%>
server.<%= id %>=<%= node %>:<%= @port_leader %>:<%= @port_quorum %>;<%= @port_client %>
<% end -%>
<% end -%>
dataDir=<%= @datadir %>
<% @properties.each do |k, v| -%>
<%= k %>=<%= v %>
<% end -%>
事实和内置变量
配置的具体部分通常取决于节点上当前发生的情况。 例如,根据 Debian 版本,您需要安装一个或另一个版本的软件包。 您可以手动监控所有这些,如果节点发生变化则重写清单。 但这不是一个严肃的方法;自动化要好得多。
为了获取有关节点的信息,Puppet 有一种称为事实的机制。 事实 - 这是有关节点的信息,在清单中以全局命名空间中的普通变量的形式提供。 例如,主机名、操作系统版本、处理器体系结构、用户列表、网络接口及其地址列表等等。 事实可作为常规变量在清单和模板中使用。
使用事实的示例:
notify { "Running OS ${facts['os']['name']} version ${facts['os']['release']['full']}": }
# ресурс типа notify просто выводит сообщение в лог
从形式上来说,事实有一个名称(字符串)和一个值(可以使用多种类型:字符串、数组、字典)。 吃
在操作过程中,puppet 代理首先将所有可用的事实收集器从 pappetserver 复制到节点,然后启动它们并将收集到的事实发送到服务器; 此后,服务器开始编译目录。
可执行文件形式的事实
这些事实被放置在目录中的模块中 facts.d
。 当然,这些文件必须是可执行的。 运行时,它们必须以 YAML 或 key=value 格式将信息输出到标准输出。
不要忘记,这些事实适用于由部署模块的 poppet 服务器控制的所有节点。 因此,在脚本中,请注意检查系统是否具有事实运行所需的所有程序和文件。
#!/bin/sh
echo "testfact=success"
#!/bin/sh
echo '{"testyamlfact":"success"}'
红宝石的事实
这些事实被放置在目录中的模块中 lib/facter
.
# всё начинается с вызова функции Facter.add с именем факта и блоком кода
Facter.add('ladvd') do
# в блоках confine описываются условия применимости факта — код внутри блока должен вернуть true, иначе значение факта не вычисляется и не возвращается
confine do
Facter::Core::Execution.which('ladvdc') # проверим, что в PATH есть такой исполняемый файл
end
confine do
File.socket?('/var/run/ladvd.sock') # проверим, что есть такой UNIX-domain socket
end
# в блоке setcode происходит собственно вычисление значения факта
setcode do
hash = {}
if (out = Facter::Core::Execution.execute('ladvdc -b'))
out.split.each do |l|
line = l.split('=')
next if line.length != 2
name, value = line
hash[name.strip.downcase.tr(' ', '_')] = value.strip.chomp(''').reverse.chomp(''').reverse
end
end
hash # значение последнего выражения в блоке setcode является значением факта
end
end
文字事实
这些事实被放置在目录中的节点上 /etc/facter/facts.d
在旧木偶或 /etc/puppetlabs/facts.d
在新的木偶中。
examplefact=examplevalue
---
examplefact2: examplevalue2
anotherfact: anothervalue
了解事实
有两种方法可以了解事实:
- 通过字典
$facts
:$facts['fqdn']
; - 使用事实名称作为变量名称:
$fqdn
.
最好用字典 $facts
,或者更好的是,指示全局名称空间($::facts
).
内置变量
除了事实之外,还有
- 可信的事实 — 从客户端证书中获取的变量(由于证书通常是在 poppet 服务器上颁发的,因此代理不能只获取并更改其证书,因此这些变量是“受信任的”):证书的名称、证书的名称主机和域,证书的扩展名。
- 服务器事实 —与服务器信息相关的变量—版本、名称、服务器IP地址、环境。
- 代理事实 — 由 puppet-agent 直接添加的变量,而不是事实 — 证书名称、代理版本、puppet 版本。
- 主变量 - Pappetmaster 变量(原文如此!)。 与以下内容大致相同 服务器事实,加上配置参数值可用。
- 编译器变量 — 每个范围内不同的编译器变量:当前模块的名称和访问当前对象的模块的名称。 例如,它们可用于检查您的私有类是否未被其他模块直接使用。
添加1:如何运行和调试这一切?
文章中包含了很多puppet代码的例子,但根本没有告诉我们如何运行这段代码。 嗯,我正在纠正自己。
一个代理足以运行 Puppet,但在大多数情况下,您还需要一台服务器。
代理人
至少从版本 XNUMX 开始,puppet-agent 软件包来自
在最简单的情况下,要使用 puppet 配置,以无服务器模式启动代理就足够了:前提是将 puppet 代码复制到节点,启动 puppet apply <путь к манифесту>
:
atikhonov@atikhonov ~/puppet-test $ cat helloworld.pp
node default {
notify { 'Hello world!': }
}
atikhonov@atikhonov ~/puppet-test $ puppet apply helloworld.pp
Notice: Compiled catalog for atikhonov.localdomain in environment production in 0.01 seconds
Notice: Hello world!
Notice: /Stage[main]/Main/Node[default]/Notify[Hello world!]/message: defined 'message' as 'Hello world!'
Notice: Applied catalog in 0.01 seconds
当然,最好设置服务器并以守护进程模式在节点上运行代理 - 然后每半小时一次,它们将应用从服务器下载的配置。
您可以模仿推送工作模式——转到您感兴趣的节点并开始 sudo puppet agent -t
. 钥匙 -t
(--test
)实际上包括几个可以单独启用的选项。 这些选项包括以下内容:
- 不要以守护进程模式运行(默认情况下,代理以守护进程模式启动);
- 应用目录后关闭(默认情况下,代理将继续工作并每半小时应用一次配置);
- 写详细的工作日志;
- 显示文件中的更改。
代理具有无需更改的操作模式 - 当您不确定是否编写了正确的配置并想要检查代理在操作期间究竟会更改什么时,可以使用它。 该模式由参数启用 --noop
在命令行上: sudo puppet agent -t --noop
.
此外,您可以启用工作的调试日志 - 在其中,puppet 会记录它执行的所有操作:关于它当前正在处理的资源、关于该资源的参数、关于它启动的程序。 当然这是一个参数 --debug
.
服务器
在本文中,我不会考虑 pappetserver 的完整设置以及向其部署代码;我只会说,有一个开箱即用的功能齐全的服务器版本,不需要额外的配置即可与少量服务器一起使用节点(例如,最多一百个)。 大量的节点将需要调整 - 默认情况下,puppetserver 启动不超过 XNUMX 个工作进程,为了获得更好的性能,您需要增加它们的数量,并且不要忘记增加内存限制,否则服务器大部分时间都会进行垃圾收集。
代码部署 - 如果您快速轻松地需要它,请查看(r10k)[
附录 2:编码指南
- 将所有逻辑放在类和定义中。
- 将类和定义保留在模块中,而不是保留在描述节点的清单中。
- 使用事实。
- 不要根据主机名创建 if。
- 随意为类和定义添加参数 - 这比隐藏在类/定义主体中的隐式逻辑更好。
我将在下一篇文章中解释为什么我建议这样做。
结论
让我们结束介绍吧。 在下一篇文章中我将向您介绍 Hiera、ENC 和 PuppetDB。
只有注册用户才能参与调查。
事实上,还有更多的材料 - 我可以就以下主题撰写文章,对您有兴趣阅读的内容进行投票:
- 59,1%高级 puppet 构造 - 一些下一级的东西:循环、映射和其他 lambda 表达式、资源收集器、导出资源以及通过 Puppet、标签、提供程序、抽象数据类型进行的主机间通信。13
- 31,8%“我是我母亲的管理员”,或者我们 Avito 如何与多个不同版本的 poppet 服务器交朋友,原则上是有关管理 poppet 服务器的部分。 7
- 81,8%我们如何编写傀儡代码:检测、文档、测试、CI/CD.18
22 位用户投票。 9 名用户弃权。
来源: habr.com