
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 ServerAgent 端跑着 Puppet Agent。Agent 每隔一段时间默认 30 分钟就会向 Master 发起一次请求说“给我最新的配置”。Master 经过一系列处理把编译好的 Catalog可以理解成一份配置清单发给 AgentAgent 拿到之后在本地执行把机器调整到 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 Serveryum 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-agentrpm -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.ppclass 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 支持 EPPEmbedded Puppet和 ERBEmbedded Ruby两种模板格式。EPP 是 Puppet 自己的模板语言语法更贴近 Puppet DSLERB 是 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 只是其中一种实现方式理解它背后的声明式思维和幂等性原则比学会某个具体工具的命令更重要。我在实际使用中最大的体会是不要为了自动化而自动化先想清楚要解决什么问题再选合适的工具和方案。