1. 从手动到自动:为什么我最终选择了 Puppet
干了七八年运维,前前后后管过几百台服务器,从最早的手工 SSH 一台台敲命令,到后来写 Shell 脚本批量跑,再到用上配置管理工具,这条路走得不算轻松。今天想聊聊 Puppet 自动化配置这个事儿,不是那种官方文档式的科普,而是我自己在真实环境里摸爬滚打之后,觉得值得分享的一些东西。
Puppet 是什么?简单说,它是一款开源的配置管理工具,用声明式的 DSL 来描述服务器的目标状态。你告诉它“这台机器上要装 Nginx、配置文件长这样、服务必须处于运行状态”,它就会自动帮你搞定,而且会持续保证这个状态不被破坏。它解决的核心问题是:当你的服务器从几台变成几十台、几百台的时候,手动登录每台机器去改配置这件事,不仅效率低,而且极易出错。Puppet 能帮你把配置变成代码,纳入版本管理,实现可重复、可审计、可回滚的自动化运维。
这篇文章适合谁看?如果你是个运维工程师,手里管着十几台以上的服务器,还在用脚本或者手工方式维护配置,那 Puppet 值得你花时间了解一下。如果你已经用过 Ansible 或者 SaltStack,想对比一下 Puppet 的差异,我也会在文中穿插一些我的看法。即使你完全没接触过配置管理工具,只要对 Linux 基本操作有了解,跟着思路走也能理解 Puppet 的核心逻辑。
我写这篇东西的出发点很简单:网上关于 Puppet 的中文资料,要么是翻译官方文档,读起来干巴巴的;要么是几年前的旧文,版本都对不上。我想用从业者的视角,把 Puppet 的核心概念、实操步骤、踩过的坑,用说人话的方式讲清楚。文章会比较长,因为我会把每个关键环节的原理和操作都展开说,建议你找个安静的时间慢慢看,或者收藏起来当参考手册用。
2. Puppet 核心架构与工作原理拆解
2.1 Agent/Master 架构到底怎么运转的
Puppet 最经典的部署模式是 Agent/Master 架构。Master 端跑着 Puppet Server,Agent 端跑着 Puppet Agent。Agent 每隔一段时间(默认 30 分钟)就会向 Master 发起一次请求,说“给我最新的配置”。Master 经过一系列处理,把编译好的 Catalog(可以理解成一份配置清单)发给 Agent,Agent 拿到之后在本地执行,把机器调整到 Catalog 描述的状态。
这个流程听起来简单,但里面有几个关键环节值得展开说。Agent 发起请求的时候,会先把自己的 Facts 信息发给 Master。Facts 是什么?就是 Agent 机器的各种属性,比如操作系统版本、IP 地址、主机名、内存大小、已挂载的磁盘等等。Master 拿到这些 Facts 之后,结合你在代码里写的条件判断,才能生成针对这台机器的个性化 Catalog。举个例子,你写了一句“如果是 CentOS 就装 httpd,如果是 Ubuntu 就装 apache2”,Master 就是靠 Facts 里的 osfamily 字段来判断该走哪个分支的。
Catalog 编译完成之后,Master 会把它存起来,然后 Agent 通过 HTTPS 拉取。Agent 在本地应用 Catalog 的时候,会调用各种 Provider 来实际执行操作。Provider 是 Puppet 里很核心的一个概念,它是资源类型在不同平台上的具体实现。比如 package 这个资源类型,在 CentOS 上对应的 Provider 是 yum,在 Ubuntu 上对应的是 apt。你写代码的时候不用关心底层用什么命令,Puppet 会根据 Facts 自动选择合适的 Provider。
注意:Agent 和 Master 之间的通信是基于 HTTPS 的,需要证书认证。第一次连接的时候,Agent 会向 Master 申请证书,Master 需要手动或者自动签发。这个环节如果没处理好,Agent 会一直卡在等待证书签发的状态。
2.2 声明式语言和幂等性:Puppet 的灵魂
Puppet 用的是声明式 DSL,这跟 Shell 脚本那种命令式的方式有本质区别。命令式是你告诉机器“先做这个,再做那个”,而声明式是你告诉机器“我要最终变成这个样子”。这个区别听起来抽象,但实际用起来感受很深。
举个例子,你要确保一个文件存在并且内容正确。用 Shell 脚本你可能这么写:先判断文件是否存在,不存在就创建;再判断内容是否匹配,不匹配就覆盖。但用 Puppet,你只需要写一句 file 资源,声明这个文件应该存在、内容应该是什么、权限应该是什么。Puppet 自己会去判断当前状态和目标状态的差异,只做必要的变更。
这就是幂等性——同一个配置应用一次和应用一百次,结果是一样的,不会因为重复执行而出问题。幂等性是配置管理工具的基石,没有它,自动化就无从谈起。我见过太多人写的脚本,跑第一遍没问题,跑第二遍就报错,或者把配置改乱了,根本原因就是没有做到幂等。
Puppet 的幂等性是怎么实现的?每个资源类型都有自己的“当前状态”和“目标状态”对比逻辑。比如 service 资源,它会先检查服务当前是运行还是停止,如果跟目标状态一致,就什么都不做;不一致才执行启动或停止操作。这个检查过程对用户是透明的,你不需要自己写判断逻辑。
2.3 资源、类、模块:代码组织的三层结构
Puppet 的代码组织有一套清晰的层级关系,理解这个对后续写代码至关重要。
最底层是资源(Resource),这是 Puppet 的最小配置单元。一个资源描述一个具体的东西,比如一个软件包、一个文件、一个服务、一个用户。每个资源都有类型(type)、标题(title)和一组属性(attribute)。比如:
package { 'nginx': ensure => installed, }这行代码声明了一个 package 资源,标题是 nginx,目标是确保它被安装。
往上一层是类(Class),类是一组资源的集合,用来实现某个完整的功能。比如一个 nginx 类,里面可能包含安装 nginx 包、管理 nginx.conf 配置文件、启动 nginx 服务这几个资源。类可以被其他类包含或引用,实现代码复用。
再往上是模块(Module),模块是类的集合,通常按照功能或角色来组织。一个模块有固定的目录结构,包含 manifests、templates、files、tests 等子目录。比如你可以有一个 nginx 模块、一个 mysql 模块、一个用户管理模块。模块是 Puppet 代码分发和复用的基本单位,你可以把自己写的模块打包分享给别人,也可以用别人写好的现成模块。
这三层结构的好处是职责分明。资源关注“做什么”,类关注“怎么组合”,模块关注“怎么打包和复用”。刚开始写 Puppet 代码的时候,很容易把所有东西都堆在一个文件里,写到后面自己都看不懂。按照这个三层结构来组织,代码的可维护性会好很多。
3. 环境搭建与基础配置实操
3.1 部署前的规划:机器角色和网络要求
在动手装 Puppet 之前,有几件事得先想清楚。第一是机器角色划分,哪台机器做 Master,哪些做 Agent。Master 对性能有一定要求,因为要处理所有 Agent 的请求和 Catalog 编译,建议至少 2 核 4G 起步。Agent 就是被管理的业务机器,对 Puppet 本身没有特殊资源要求。
第二是网络规划。Agent 需要能访问 Master 的 8140 端口,这是 Puppet Server 的默认监听端口。如果你的机器分布在不同的网段或安全域,得提前把防火墙策略开好。另外,所有机器的时间要同步,因为证书认证对时间敏感,时间偏差太大会导致证书校验失败。
第三是主机名规划。Puppet 的证书认证是基于主机名的,Agent 用哪个主机名去申请证书,证书里就会记录哪个名字。如果主机名不规范或者会变动,后面管理起来会很麻烦。我的建议是提前规划好一套命名规则,比如用“角色-序号.域名”的格式,并且在 /etc/hosts 或者 DNS 里做好解析。
提示:如果只是测试环境,可以用 Vagrant 或者 Docker 快速起几台虚拟机来练手。生产环境的话,建议先在测试环境把流程跑通,再上生产。
3.2 Master 端安装与初始化配置
我以 CentOS 7 为例来说安装过程,其他发行版的思路类似,只是包管理命令不同。
首先配置 Puppet 的官方软件源。Puppet 提供了各个平台的仓库包,安装之后会自动配好 yum 源。
rpm -Uvh https://yum.puppet.com/puppet6-release-el-7.noarch.rpm然后安装 Puppet Server:
yum install -y puppetserver安装完成之后,需要调整一下 JVM 内存配置。Puppet Server 是基于 JVM 的,默认配置可能给的内存比较大,如果机器内存有限,需要改小一点。编辑 /etc/sysconfig/puppetserver 文件,找到 JAVA_ARGS 那一行:
JAVA_ARGS="-Xms512m -Xmx512m"测试环境给 512M 就够了,生产环境根据 Agent 数量适当调大,一般 2G 到 4G 比较常见。
接下来启动服务并设置开机自启:
systemctl start puppetserver systemctl enable puppetserver启动之后检查一下服务状态和端口监听:
systemctl status puppetserver ss -tlnp | grep 8140如果 8140 端口正常监听,说明 Master 端基本就绪了。
3.3 Agent 端安装与证书签发流程
Agent 端的安装更简单,同样先配好软件源,然后安装 puppet-agent:
rpm -Uvh https://yum.puppet.com/puppet6-release-el-7.noarch.rpm yum install -y puppet-agent安装完成后,需要告诉 Agent 它的 Master 是谁。编辑 /etc/puppetlabs/puppet/puppet.conf,在 [main] 段落下添加:
[main] server = master.example.com把 master.example.com 替换成你实际的 Master 主机名。
然后启动 Agent 服务,它会自动向 Master 发起证书申请:
systemctl start puppet systemctl enable puppet这时候 Agent 会处于等待状态。回到 Master 上,查看待签发的证书请求:
puppetserver ca list你会看到 Agent 的主机名出现在列表里。签发证书:
puppetserver ca sign --certname agent01.example.com签发成功后,Agent 那边通常会在下一个运行周期自动拉取配置。你也可以手动触发一次:
puppet agent -t-t 是 test 的意思,会立即执行一次配置应用,并且输出详细的日志。第一次跑的时候,Agent 会从 Master 拉取 Catalog 并应用。如果 Master 上还没有给这个 Agent 分配任何类,那它什么都不会做,但证书认证流程算是走通了。
注意:如果 Agent 数量多,手动签发证书会很累。可以在 Master 的 puppet.conf 里配置 autosign,让符合特定规则的 Agent 自动签发。但 autosign 有安全风险,生产环境慎用,或者配合严格的域名匹配规则来限制范围。
4. 编写可维护的 Puppet 代码:从入门到进阶
4.1 第一个模块:用 Nginx 举例走通全流程
光说不练假把式,咱们用一个实际的例子把 Puppet 代码的编写流程走一遍。假设我们要写一个 nginx 模块,功能是安装 nginx、管理配置文件、保证服务运行。
首先在 Master 的模块目录下创建模块结构。/etc/puppetlabs/code/environments/production/modules/ 是默认的模块存放路径。用 Puppet 自带的命令生成模块骨架:
puppet module generate myorg-nginx这个命令会交互式地问你一些问题,回答完之后会在当前目录生成一个标准的模块目录结构。不过这个命令生成的骨架比较啰嗦,我一般习惯手动创建需要的目录:
mkdir -p /etc/puppetlabs/code/environments/production/modules/nginx/{manifests,files,templates}然后创建主类文件 manifests/init.pp:
class nginx { package { 'nginx': ensure => installed, } file { '/etc/nginx/nginx.conf': ensure => file, owner => 'root', group => 'root', mode => '0644', source => 'puppet:///modules/nginx/nginx.conf', require => Package['nginx'], notify => Service['nginx'], } service { 'nginx': ensure => running, enable => true, require => Package['nginx'], subscribe => File['/etc/nginx/nginx.conf'], } }这段代码的逻辑很清晰:先装包,再管配置文件,最后保证服务运行。require 表示依赖关系,确保包先装好再管文件。notify 和 subscribe 表示通知关系,当配置文件发生变化时,触发服务重启。
把一份 nginx.conf 模板放到 files 目录下,然后在 Agent 上应用这个类。怎么应用?有两种方式:一种是在 Master 的 site.pp 里直接声明,另一种是通过 ENC(外部节点分类器)或者 Hiera 来分配。简单起见,先在 site.pp 里写:
node 'agent01.example.com' { include nginx }然后在 Agent 上跑 puppet agent -t,你就能看到 Puppet 自动完成了安装、配置、启动的全过程。
4.2 用 Hiera 分离数据与代码
上面那个例子有个问题:配置数据(比如 nginx 的 worker 进程数、监听端口)硬编码在代码里了。如果不同机器需要不同的配置,就得写一堆条件判断,代码会变得很难维护。Hiera 就是来解决这个问题的。
Hiera 是 Puppet 的键值查找系统,它把数据从代码里抽出来,按照层级结构组织。默认的层级通常包括:全局层、环境层、角色层、节点层。查找一个键的时候,Hiera 会从最具体的层级往上找,找到就返回。
配置 Hiera 需要修改 Master 上的 hiera.yaml 文件。Puppet 6 的路径是 /etc/puppetlabs/puppet/hiera.yaml。一个典型的配置长这样:
version: 5 hierarchy: - name: "Per-node data" path: "nodes/%{trusted.certname}.yaml" - name: "Per-role data" path: "roles/%{role}.yaml" - name: "Common data" path: "common.yaml"然后在 data 目录下创建对应的 yaml 文件。比如 common.yaml 里写:
nginx::worker_processes: 4 nginx::listen_port: 80代码里就可以用 lookup 函数来读取这些值:
class nginx ( Integer $worker_processes = lookup('nginx::worker_processes'), Integer $listen_port = lookup('nginx::listen_port'), ) { # ... }这样不同机器只要在对应的 yaml 文件里覆盖一下值,就能得到不同的配置,代码本身完全不用改。这个思路跟编程里的“配置与代码分离”是一脉相承的,用好了能极大提升代码的可维护性。
4.3 模板机制:动态生成配置文件的利器
有时候配置文件不是静态的,需要根据 Facts 或者变量动态生成。比如 nginx 的 worker_processes 最好设置成 CPU 核数,这个值每台机器可能不一样。这时候就要用到模板了。
Puppet 支持 EPP(Embedded Puppet)和 ERB(Embedded Ruby)两种模板格式。EPP 是 Puppet 自己的模板语言,语法更贴近 Puppet DSL;ERB 是 Ruby 的模板系统,功能更强大但语法稍显啰嗦。我一般优先用 EPP,除非有复杂的逻辑处理需求。
在 templates 目录下创建 nginx.conf.epp:
<%= $nginx::worker_processes %>; worker_processes auto;等等,上面写错了,EPP 的语法是<%= $variable %>这种形式。正确的例子:
worker_processes <%= $worker_processes %>; listen <%= $listen_port %>;然后在 file 资源里引用模板:
file { '/etc/nginx/nginx.conf': ensure => file, content => epp('nginx/nginx.conf.epp', { 'worker_processes' => $worker_processes, 'listen_port' => $listen_port, }), # ... }模板的好处是灵活,你可以根据 Facts 里的 CPU 核数、内存大小等动态计算配置值。但也要注意别把太多逻辑塞进模板里,模板越复杂越难调试。我的经验是,模板里只做简单的变量替换和条件判断,复杂的逻辑放在 Puppet 代码里处理。
5. 常见问题排查与避坑经验实录
5.1 证书问题:Agent 连不上 Master 怎么办
证书问题是 Puppet 新手最容易遇到的坑,没有之一。典型症状是 Agent 跑 puppet agent -t 的时候报错,提示 SSL 证书验证失败,或者一直卡在“Requesting certificate”状态。
排查思路分几步走。先在 Master 上用 puppetserver ca list --all 看看 Agent 的证书请求在不在列表里。如果不在,说明 Agent 根本没连上 Master,检查网络连通性和 Master 的 8140 端口是否可达。如果在列表里但状态是 requested,说明证书还没签发,用 puppetserver ca sign 签一下就行。
如果证书已经签发了但 Agent 还是报错,可能是证书不匹配。常见原因是 Agent 的主机名变了,或者之前用不同的主机名申请过证书。解决办法是在 Master 上清理掉旧证书,在 Agent 上删掉本地的 SSL 目录,然后重新申请:
# Master 上 puppetserver ca clean --certname agent01.example.com # Agent 上 rm -rf /etc/puppetlabs/puppet/ssl puppet agent -t提示:Agent 的证书默认有效期是 5 年,到期前需要续签。可以配置 puppetserver 的 CA 自动续签,或者提前手动处理。我遇到过因为证书过期导致整个集群失联的情况,排查了半天才想起来是证书问题,这个坑希望大家别再踩。
5.2 Catalog 编译失败:代码报错怎么定位
Catalog 编译失败是另一类高频问题。Agent 跑 puppet agent -t 的时候,如果 Master 端编译 Catalog 出错,Agent 会收到一个错误信息,但信息往往不够详细。这时候需要去 Master 上看日志。
Puppet Server 的日志在 /var/log/puppetlabs/puppetserver/puppetserver.log。编译错误通常会记录在这里,包括具体的文件、行号和错误原因。常见的编译错误包括:语法错误(少了个逗号、括号不匹配)、资源重复定义(同一个资源在多个地方声明了)、变量未定义、类引用错误等。
我自己的习惯是,写完代码先在 Master 上用 puppet parser validate 检查语法:
puppet parser validate /etc/puppetlabs/code/environments/production/modules/nginx/manifests/init.pp这个命令能快速发现语法层面的问题。语法没问题的话,再用 puppet apply --noop 做一次模拟执行,看看逻辑上有没有问题。--noop 是 dry-run 模式,不会真正改变系统状态,只告诉你“如果执行会做什么”。
5.3 资源依赖顺序:为什么我的服务启动失败
Puppet 的资源默认是乱序执行的,它只保证你显式声明的依赖关系。如果你没写依赖,Puppet 可能先启动服务再装包,那服务肯定起不来。这是新手写 Puppet 代码时非常容易忽略的一点。
解决方法是显式声明依赖关系。Puppet 提供了几种方式:
- require:当前资源依赖某个资源,被依赖的资源先执行
- before:当前资源先于某个资源执行
- notify:跟 require 类似,但被依赖的资源如果发生变化,会触发当前资源刷新
- subscribe:跟 before 类似,但当前资源会订阅被依赖资源的变化
用箭头语法可以更简洁地表达依赖:
Package['nginx'] -> File['/etc/nginx/nginx.conf'] ~> Service['nginx']-> 表示左边的资源先于右边执行,~> 表示左边资源变化时通知右边资源刷新。这个链式写法很直观,我一般优先用这种。
注意:依赖关系不要写得太复杂,否则会形成环,Puppet 会报错。如果依赖链很长,考虑拆分成多个类,用 include 来组织。
5.4 性能优化:Agent 数量多了怎么调
当 Agent 数量从几十台涨到几百台的时候,Master 可能会扛不住。常见的性能瓶颈有几个:JVM 内存不足、Catalog 编译慢、证书签发请求堆积。
JVM 内存的调整前面说过了,根据 Agent 数量适当调大。Catalog 编译慢通常是因为代码写得太复杂,或者 Facts 太多。可以优化代码结构,减少不必要的条件判断和函数调用。另外,Puppet Server 支持 JRuby 多实例,可以通过调整 jruby-puppet 的 max-active-instances 参数来提升并发处理能力。
还有一个容易被忽略的点是报告存储。Puppet 默认会把每次 Agent 运行的报告存到 Master 上,Agent 多了之后报告会占满磁盘。可以在 puppet.conf 里关掉报告存储,或者配置定期清理:
[master] reports = none如果确实需要报告,可以存到外部数据库,而不是本地文件系统。
6. 从 Puppet 到自动化运维体系的思考
6.1 Puppet 和 Ansible 到底怎么选
经常有人问我,Puppet 和 Ansible 选哪个。我的回答通常是:看场景。Puppet 是 C/S 架构,Agent 会持续运行并定期拉取配置,适合需要长期保持状态一致性的场景,比如大规模服务器的基线管理。Ansible 是 SSH 推送模式,不需要在目标机器上装 Agent,适合临时性的任务执行和配置下发。
如果你的环境里服务器数量多、配置复杂、需要持续合规检查,Puppet 的模型更合适。如果你只是想批量执行一些命令,或者管理网络设备这类不方便装 Agent 的场景,Ansible 更轻便。当然,两者并不是互斥的,很多团队是 Puppet 管基线,Ansible 管发布和临时任务,各取所长。
6.2 配置管理只是起点,不是终点
Puppet 解决了配置一致性的问题,但它不是银弹。在实际运维体系里,配置管理只是其中一环。你还需要考虑:代码怎么版本管理(Git)、变更怎么审核(Code Review)、发布怎么灰度(分批应用)、状态怎么监控(Prometheus 等)、日志怎么收集(ELK 等)。
我见过一些团队上了 Puppet 之后,觉得万事大吉了,结果代码随便改、没有审核、没有测试,最后配置管理本身成了故障源。工具是死的,流程和规范是活的。把 Puppet 代码当成正式代码来管理,走 Git 流程,做 Code Review,在测试环境验证后再上生产,这些工程实践比工具本身更重要。
6.3 我踩过的那些坑和总结的经验
最后分享几个我实际踩过的坑,希望能帮你少走弯路。
第一个坑是 Facts 信任问题。Puppet 的 trusted facts 是从证书里提取的,不可篡改,适合用来做安全相关的判断。但普通 facts 是 Agent 上报的,理论上可以被篡改。如果你的代码逻辑依赖 facts 做安全决策,一定要用 trusted facts。
第二个坑是环境隔离。Puppet 支持多环境(production、testing、development),但默认配置下所有环境共享同一个模块路径。如果不同环境需要不同版本的模块,得配置 environmentpath 和 modulepath,让每个环境有独立的模块目录。
第三个坑是代码测试。Puppet 代码也是代码,也需要测试。可以用 rspec-puppet 写单元测试,用 beaker 写集成测试。虽然写测试要花时间,但比起在生产环境出故障再回滚,这个投入是值得的。
第四个坑是文档。Puppet 代码写多了之后,模块之间的依赖关系会变得复杂。一定要写 README,说明每个模块的功能、依赖、参数、示例。我吃过这个亏,半年后回头看自己写的模块,完全想不起来当时为什么那么设计。
自动化运维这条路,工具在变,但核心逻辑不变:把重复的事情交给机器,把人的精力留给真正需要判断力的事情。Puppet 只是其中一种实现方式,理解它背后的声明式思维和幂等性原则,比学会某个具体工具的命令更重要。我在实际使用中最大的体会是,不要为了自动化而自动化,先想清楚要解决什么问题,再选合适的工具和方案。