1. 从手动到自动:为什么我最终选择了Puppet
干了七八年运维,前几年我一直觉得“自动化配置管理”是个伪命题。服务器数量少的时候,手动SSH上去改改配置文件、装装包,几分钟搞定,何必折腾什么自动化工具?直到有一次,一个线上环境因为某台机器的Nginx配置被人手动改过,导致负载均衡策略失效,排查了整整一个下午才定位到问题。那次事故之后,我开始认真审视配置管理这件事。
Puppet是我接触的第一款配置管理工具,也是目前团队里跑得最稳的一套方案。它的核心逻辑其实不复杂:你用一种叫Manifest的描述性语言,把“服务器应该长什么样”写清楚,Puppet负责把机器拉到那个状态。跟Shell脚本最大的区别在于,Shell脚本是“过程式”的——你先执行A再执行B,中间任何一步出错都可能留下烂摊子;而Puppet是“声明式”的——你只描述最终状态,它自己决定怎么达到,而且反复执行结果一致,这就是所谓的幂等性。
这篇文章适合谁看?如果你手里管着十几台以上的服务器,经常因为配置不一致、环境漂移、手动操作失误而头疼,那Puppet值得你花时间研究。如果你只是管一两台机器,说实话,Ansible可能更轻量,但Puppet的思想依然值得了解。我会从架构设计、核心概念、实操步骤、常见坑几个维度展开,尽量把我在实际项目中踩过的坑和总结的经验都倒出来。
2. Puppet的核心架构与关键概念拆解
2.1 Agent/Master架构到底怎么选
Puppet有两种运行模式:一种是Agent/Master模式,一种是单机模式(也叫Masterless)。很多人一开始纠结选哪个,我的建议是先搞清楚你的规模。
Agent/Master模式下,所有被管理的机器上跑一个Puppet Agent进程,定期(默认30分钟)向Puppet Master发起请求,拉取自己的Catalog(可以理解为该机器的配置清单),然后在本地执行。Master端集中存放所有Manifest和模块。这种模式适合机器数量多、需要集中管控的场景,缺点是Master可能成为单点,需要做高可用。
单机模式则是把Manifest直接放在被管理机器上,用puppet apply命令本地执行。没有Master,没有证书认证,部署简单,适合机器数量少或者临时环境。我早期做测试环境就是用单机模式,后来机器上了50台之后才切到Agent/Master。
注意:Agent/Master模式下,证书认证是第一个拦路虎。Agent第一次连接Master时会请求证书,Master需要手动或自动签发。如果证书没签,Agent会一直卡在等待状态,日志里能看到“Waiting for certificate”之类的提示。
2.2 Manifest、Resource、Class、Module的关系
这四个概念是Puppet的骨架,必须搞清楚。
Resource是最小配置单元。比如一个文件、一个用户、一个服务、一个软件包,都是Resource。每个Resource有类型(type)、标题(title)和一堆属性(attribute)。举个例子:
file { '/etc/nginx/nginx.conf': ensure => file, owner => 'root', group => 'root', mode => '0644', content => template('nginx/nginx.conf.erb'), }这段代码声明了一个文件资源,Puppet会确保这个文件存在、属主属组正确、权限正确、内容来自指定模板。如果文件已经存在且内容一致,Puppet什么都不做——这就是幂等性。
Manifest就是包含一个或多个Resource的文件,后缀是.pp。Class是把相关Resource打包在一起的容器,可以理解成编程里的函数。Module是Class、文件、模板、事实(Facts)的集合,有固定的目录结构。实际项目中,我们几乎不会把所有东西写在一个Manifest里,而是按功能拆成Module,比如nginx模块、mysql模块、users模块。
2.3 目录结构长什么样
一个标准的Module目录结构是这样的:
nginx/ ├── manifests/ │ ├── init.pp │ ├── config.pp │ └── service.pp ├── templates/ │ └── nginx.conf.erb ├── files/ │ └── nginx.repo ├── tests/ │ └── init.pp └── metadata.jsonmanifests/init.pp是入口文件,里面定义的Class名字必须和模块名一致。templates放ERB模板,files放静态文件。这个结构不是随便定的,Puppet的自动加载机制依赖它。你如果把文件放错位置,Puppet会报“Could not find template”之类的错误。
3. 从零搭建一套可用的Puppet环境
3.1 安装与基础配置
假设我们用两台机器做演示:一台Master(主机名puppet-master),一台Agent(主机名puppet-agent)。操作系统以常见的Linux发行版为例。
Master端安装:
# 添加官方仓库(以某主流发行版为例) rpm -Uvh https://yum.puppet.com/puppet7-release-el-7.noarch.rpm yum install -y puppetserverAgent端安装:
rpm -Uvh https://yum.puppet.com/puppet7-release-el-7.noarch.rpm yum install -y puppet-agent安装完成后,Master端需要调整一下JVM内存,默认2G对小型环境够用,但如果模块多了会吃紧。编辑/etc/sysconfig/puppetserver,把JAVA_ARGS里的-Xms和-Xmx调到合适值,我一般设成2G和4G。
启动Master:
systemctl enable puppetserver systemctl start puppetserverAgent端编辑/etc/puppetlabs/puppet/puppet.conf,在[agent]段加上server = puppet-master,然后启动Agent:
systemctl enable puppet systemctl start puppet3.2 证书签发:第一道坎
Agent启动后会自动向Master请求证书。在Master上执行:
puppetserver ca list --all你会看到Agent的证书请求,状态是requested。签发:
puppetserver ca sign --certname puppet-agent签发成功后,Agent端再执行一次puppet agent -t,就能正常拉取Catalog了。如果Agent一直没反应,检查Agent端的/var/log/puppetlabs/puppet/puppet.log,常见问题是时间不同步导致证书验证失败,或者防火墙挡了8140端口。
实操心得:生产环境建议开启自动签发,但一定要配合
autosign策略,比如只允许特定域名后缀的机器自动签。手动签发在机器多的时候会疯掉。
3.3 写第一个Manifest并应用
在Master的/etc/puppetlabs/code/environments/production/manifests/目录下创建site.pp:
node 'puppet-agent' { file { '/tmp/hello-puppet.txt': ensure => file, content => "This file is managed by Puppet.\n", mode => '0644', } }Agent端执行puppet agent -t,然后去/tmp下看,应该多了个hello-puppet.txt。再执行一次puppet agent -t,Puppet会报告“Applied catalog in X seconds”,但不会重复创建文件,因为状态已经一致了。
这个简单的例子展示了Puppet的核心工作流:定义状态、应用、验证幂等性。实际项目中,我们会把site.pp作为入口,里面用include引入各个模块的Class。
4. 实战:用Puppet管理Nginx和用户账户
4.1 编写Nginx模块
先创建模块目录:
cd /etc/puppetlabs/code/environments/production/modules mkdir -p nginx/{manifests,templates,files}manifests/init.pp:
class nginx { package { 'nginx': ensure => installed, } file { '/etc/nginx/nginx.conf': ensure => file, owner => 'root', group => 'root', mode => '0644', content => template('nginx/nginx.conf.erb'), require => Package['nginx'], notify => Service['nginx'], } service { 'nginx': ensure => running, enable => true, subscribe => File['/etc/nginx/nginx.conf'], } }这里有几个关键点:require确保包先装再改配置,notify和subscribe确保配置变更后服务重启。这两个元参数是Puppet里控制执行顺序和触发动作的核心,用好了能避免很多“配置改了但服务没重载”的问题。
templates/nginx.conf.erb里可以用ERB语法动态生成配置,比如根据Facts里的CPU核数设置worker_processes:
worker_processes <%= @processors['count'] %>;@processors是Puppet自动收集的Fact,不需要额外配置。
4.2 批量管理用户账户
用户管理是配置管理里最常见的需求之一。假设我们要确保一批运维账户存在,且SSH公钥正确。
class users { $ops_users = ['alice', 'bob', 'carol'] $ops_users.each |String $user| { user { $user: ensure => present, shell => '/bin/bash', managehome => true, } file { "/home/${user}/.ssh": ensure => directory, owner => $user, group => $user, mode => '0700', require => User[$user], } file { "/home/${user}/.ssh/authorized_keys": ensure => file, owner => $user, group => $user, mode => '0600', content => file("users/${user}_authorized_keys"), require => File["/home/${user}/.ssh"], } } }然后在files/目录下放对应的公钥文件。这样新增一个用户只需要改$ops_users数组,加一个公钥文件,跑一次Puppet就搞定。
注意:
managehome => true会创建家目录,但如果家目录已经存在且里面有数据,Puppet不会删除。不过如果你不小心把ensure设成absent,Puppet会连家目录一起删掉,这个操作不可逆,生产环境一定要小心。
4.3 用Hiera做数据分离
把数据硬编码在Manifest里不是好习惯。Puppet提供了Hiera来做数据与代码分离。配置hiera.yaml:
version: 5 defaults: datadir: data data_hash: yaml_data hierarchy: - name: "Per-node data" path: "nodes/%{trusted.certname}.yaml" - name: "Common data" path: "common.yaml"然后在data/common.yaml里定义:
nginx::worker_processes: 4 users::ops_users: - alice - bob - carolManifest里用lookup函数读取:
$ops_users = lookup('users::ops_users', Array[String], 'first', [])这样做的好处是,不同环境、不同节点的数据可以分层覆盖,代码逻辑不用动。比如测试环境的Nginx worker数设小一点,生产环境设大一点,只改数据文件就行。
5. 常见问题与排查技巧实录
5.1 排查思路速查表
| 现象 | 可能原因 | 排查命令 |
|---|---|---|
| Agent卡在等待证书 | 证书未签发 | puppetserver ca list --all |
| Catalog编译失败 | Manifest语法错误 | puppet parser validate xxx.pp |
| 配置未生效 | 依赖关系缺失 | puppet agent -t --debug |
| 服务未重启 | notify/subscribe未配 | 检查Resource的元参数 |
| 模板渲染报错 | ERB变量不存在 | puppet apply --test本地测试 |
| 执行超时 | Master内存不足 | 检查/var/log/puppetlabs/puppetserver/puppetserver.log |
5.2 几个我踩过的坑
坑一:Resource标题重复。同一个Manifest里不能有两个相同类型且相同标题的Resource。比如你写了两个file { '/etc/hosts': },Puppet会直接报错。解决办法是用不同的标题,或者用ensure => present的数组形式。
坑二:执行顺序不一定是书写顺序。Puppet默认不保证Resource按你写的顺序执行,除非你用require、before、notify、subscribe这些元参数显式指定。我早期写了一个装包、改配置、起服务的Manifest,结果服务先起来了,配置还没改,导致服务启动失败。后来全部加上依赖关系才稳定。
坑三:Facts收集不全。有些自定义Fact需要额外装facter插件,或者用external facts。如果Manifest里引用了不存在的Fact,编译会报错。建议在模板里用@fact之前,先用facter -p确认一下。
坑四:环境隔离没做好。Puppet默认有production环境,但你可以创建testing、staging等环境。不同环境的代码目录是独立的。我见过有人直接在production里改代码,结果所有机器立刻生效,出了事故回滚都来不及。正确做法是在测试环境验证通过后再合并到生产。
5.3 性能优化的小技巧
Master端如果管理几百台机器,Catalog编译会成为瓶颈。几个优化方向:一是开启environment_timeout缓存,避免每次请求都重新编译;二是用puppetdb做事实存储和查询,减少Agent端的Facts传输量;三是把不常变的模块拆成静态Catalog,减少编译开销。
Agent端可以调整runinterval,默认30分钟。如果机器多,可以错开执行时间,避免同时向Master发起请求造成尖峰。在puppet.conf里加splay = true和splaylimit = 300,让Agent在30分钟间隔内随机延迟最多5分钟再执行。
6. 从Puppet出发:配置管理的进阶思路
Puppet解决了“机器状态一致性”的问题,但它不是银弹。实际项目中,我通常会把Puppet和CI/CD流水线结合起来:代码合并到主分支后,自动触发Puppet环境更新,再通过puppet job或者bolt做批量任务编排。这样从代码提交到机器生效,全程不需要人工登录服务器。
另外,Puppet的测试也很重要。puppet-lint做语法检查,rspec-puppet做单元测试,beaker做集成测试。我一般要求团队里每个模块至少要有puppet-lint通过,核心模块要有rspec覆盖。这些工具在CI里跑一遍,能挡掉大部分低级错误。
如果你刚开始接触配置管理,我的建议是不要一上来就追求大而全。先从一个简单的模块开始,比如统一/etc/hosts或者管理NTP服务,跑通整个流程,理解Agent/Master的交互、证书签发、Catalog编译这些基础环节。等这套跑顺了,再逐步把更多服务纳入管理。一口吃不成胖子,配置管理这件事,稳比快重要。
最后分享一个我个人的习惯:每次改完Manifest,先在本地用puppet apply --noop跑一遍,看看Puppet打算做什么变更。--noop是“干跑”模式,不会真正修改系统,但会告诉你哪些Resource会发生变化。这个习惯帮我避免了好几次误操作,尤其是涉及删除文件或停止服务的场景。