Puppet 资源声明语言是 Puppet 配置管理的核心表达方式。它没有采用传统脚本里“先执行 A、再判断 B、最后调用 C”的过程式思路,而是让管理员直接描述每个资源应该处于的最终状态。例如某个软件包必须存在、某个服务必须运行、某个配置文件的内容必须匹配指定字符串。Puppet 在每次运行时计算当前状态与目标状态的差异,只对不一致的部分执行操作,因此天然带有幂等性。理解这套语言的关键,是抓住资源类型、资源标题、属性列表以及资源之间的依赖关系这几个基础概念。

下面从资源声明的基本语法开始,逐步拆解文件、软件包、服务等常用资源的声明方式,并讨论元参数和变量条件在复杂场景下的运用。
资源声明的基本结构与名称规则
Puppet 资源声明由类型、标题和一组属性组成。一条典型声明写作 type { 'title': attribute => value, }。类型可以是 file、package、service、user 等内置类型,也可以是自定义类型。标题是一个字符串,用来标识资源实例。属性部分由若干键值对组成,键是资源类型定义的参数名,值可以是字符串、数组、哈希、布尔值等。整个声明以分号或换行结束,Puppet 语法接受灵活的空白和缩进。
每个资源类型都有一个特殊的 namevar 属性,它决定资源的唯一标识。对于 file 类型,namevar 是 path,所以可以直接用文件路径作为标题:file { '/etc/motd': ensure => present }。也可以把标题写成便于阅读的别名,再单独指定 path:file { 'motd': path => '/etc/motd', ensure => present }。此时资源在 Puppet 内部仍以路径 /etc/motd 作为唯一标识,如果后续要引用该资源,就必须写成 File['/etc/motd'],而不是 File['motd']。这是很多新手容易混淆的地方,因为资源标题和实际唯一标识并不总是相同。
资源声明的标题字符串可以使用单引号或双引号。单引号表示完全字面量,双引号会触发变量插值。例如 $path = '/etc/motd' 之后,file { "$path": } 中的双引号会让变量生效,而单引号则不会。实际使用中建议优先采用单引号,除非确实需要插值,这样可以减少意外解析。同一类型下不允许出现两个相同的 namevar 值,否则 Puppet 会报重复声明错误。
file { '/etc/motd':
ensure => present,
content => "Welcome to Puppet-managed host\n",
owner => 'root',
group => 'root',
mode => '0644',
}
常用资源类型与属性声明实践
package、service 和 file 是自动化配置中最基础的三类资源。package 负责软件包的安装、升级和移除,核心属性是 ensure,取值可以是 present、absent、installed 或 latest。其中 present 和 installed 都表示软件包应当存在,latest 表示必须升级到最新版本,absent 则要求软件包被卸载。不同操作系统可能对这些值的支持略有差异,但绝大多数平台都能识别这些枚举。
service 资源用于控制服务的运行状态和开机自启。常用的 ensure 取值有 running 和 stopped,分别表示服务必须运行或必须停止。enable 属性接受布尔值,决定服务是否加入系统启动项。在一个完整的 Web 服务器配置中,通常会先声明 package 安装 nginx,再声明 file 管理配置文件,最后声明 service 启动服务。
package { 'nginx':
ensure => installed,
}
file { '/etc/nginx/nginx.conf':
ensure => file,
content => template('nginx/nginx.conf.erb'),
require => Package['nginx'],
notify => Service['nginx'],
}
service { 'nginx':
ensure => running,
enable => true,
}
属性值的数据类型并不固定。例如 file 资源的 source 属性可以是一个字符串,也可以是一个字符串数组,Puppet 会依次尝试每个源直到找到文件。数组写法为 source => [ 'puppet:///modules/nginx/nginx.conf', 'puppet:///modules/base/nginx.conf' ]。哈希类型也很常见,比如 file 的 replace、validate_cmd 等属性可以根据场景传递更复杂的配置。Puppet 4 以后可以对属性指定数据类型约束,帮助在编译期发现错误,但普通声明中不写类型也完全可以工作。
元参数与资源依赖编排
元参数是可以应用于任何资源类型的特殊属性,用于控制资源之间的执行顺序、刷新行为、审计方式等。四种最常见的元参数是 require、before、subscribe 和 notify。require 表示当前资源必须在被引用资源之后执行;before 表示当前资源必须在被引用资源之前执行。两者方向相反,选择哪个取决于哪种表达更清晰。notify 和 subscribe 除了排序之外还带有刷新语义:当被引用资源发生实际变更时,会触发当前资源刷新,典型场景是配置文件变化后重启服务。
除了在属性中直接写元参数,Puppet 还提供链式箭头语法。排序箭头 -> 表示前一个资源先于后一个资源执行,订阅箭头 ~> 表示前一个资源变更时通知后一个资源刷新。例如下面的声明把软件包、配置文件和服务按顺序串联起来:
Package['nginx'] -> File['/etc/nginx/nginx.conf'] ~> Service['nginx']
这种写法更加紧凑,适合依赖关系明确的线性场景。需要注意的是,依赖关系不能形成环,否则 Puppet 会在编译阶段报循环依赖错误。资源引用方括号内的字符串必须与资源的 namevar 值一致,而不是与标题字面量一致。当资源的 namevar 和标题不同时,引用处必须使用 namevar 值。依赖编排让 Puppet 不需要每次手动排序,配置变更可以自动按正确顺序执行。
变量、条件与迭代在资源声明中的运用
Puppet 变量以 $ 开头,赋值后不可再次修改,这是声明式语言的重要约束。变量让资源声明可以参数化,例如把软件包名、路径、端口等抽到变量中,同一份声明能够在不同节点上使用不同值。变量名区分大小写,并且有作用域规则:顶层变量对所有类可见,而类内部变量需要通过完整路径访问。
$web_package = 'nginx'
$web_config = '/etc/nginx/nginx.conf'
package { $web_package:
ensure => installed,
}
file { $web_config:
ensure => file,
content => "worker_processes auto;\n",
require => Package[$web_package],
}
条件语句 if、unless 和 case 可以根据节点信息、操作系统或外部数据决定声明哪些资源。Puppet 还提供 selector 表达式,它类似三元表达式,但可以根据多个条件返回不同值。迭代方面,Puppet 4 以后支持 each 函数,可以遍历数组或哈希并执行代码块。不过资源声明本身不适合在声明内部直接使用循环,通常做法是将可重复部分封装为定义资源类型,再通过 each 生成多个实例。
写好资源声明语言的关键是保持声明简短、明确,把复杂逻辑放到类、定义资源类型和 Hiera 数据中,而不是堆叠在单条资源上。掌握了类型、属性、元参数和变量组合之后,就能用 Puppet 管理大规模服务器集群,让配置变更可预测、可重复、可审计。