Puppet 配置管理
本文合并自:02-Puppet配置管理.md、3-4-Puppet自动化运维.md
本文介绍 Puppet 自动化配置管理工具,覆盖配置管理理念与幂等性、C/S 架构原理、安装部署、Master/Agent 证书认证、manifest 与 site.pp、核心资源类型(file/user/group/cron/service/exec/package/mount/host/yumrepo)、资源依赖与执行顺序、变量与条件、类与模块、ERB 模板、节点定义、Facter 差异化管理,以及推送 sudoers、部署 httpd/vsftpd 等完整实战,最后补充 Hiera、PuppetDB、导出资源、puppet-dashboard 报告平台与排错清单。
适用环境:CentOS 6.x / CentOS 7.3,Puppet 3.x(旧目录结构)与 Puppet 4.x/5.x/6.x(AIO 新目录结构)并列说明。
架构:Master/Agent(C/S),Master 监听 8140 端口。
一、配置管理概述
1.1 手工运维的困境
假设要管理 10000 台服务器,需要在所有机器上执行同一件事情(创建文件、修改文件、安装软件、启动服务、创建用户等),常见做法有:
- 逐个 ssh 连上去手动操作;
- 编写 shell 脚本批量执行。
但这两种方法都存在两个主要问题:
- 密码问题:可用
ssh-keygen生成空密钥(免密登录),或用expect自动应答密码解决; - 循环效率问题:10000 台机器,每台操作 10 秒,串行就需要 100000 秒(约 27.8 小时)。可通过 shell 并发执行解决。
shell 并发示例:
#!/bin/bash
for i in `seq 10`
do
echo $i
sleep 1
done & # 加后台符号与不加效果不一样,可自行测试
for j in `seq 11 20`
do
echo $j
sleep 1
done
即使解决了密码和并发,手工/脚本运维仍然有更深层的问题:
- 一致性无法保证:脚本执行到一半失败,机器处于"半配置"状态,同一批机器最终状态不一致(配置漂移 configuration drift);
- 不可重复:脚本跑第二遍往往会出错(重复添加同一行、重复创建用户报错),无法安全地反复执行;
- 不可审计:谁在什么时候改了哪台机器的什么配置,没有记录;
- 系统差异难处理:CentOS 6 用
service/chkconfig,CentOS 7 用systemctl;RHEL 用yum,SUSE 用zypper,脚本里全靠 if/else 堆叠。
差异化管理的典型 shell 逻辑:
if centos; then
xxxx
elif rhel; then
xxxx
elif suse; then
xxxx
else
xxxx
fi
1.2 配置管理工具的价值
集中化配置管理工具(Puppet、Ansible、SaltStack、Chef)带来的核心价值:
| 价值 | 含义 |
|---|---|
| 一致性(Consistency) | 所有节点向同一份"期望状态"收敛,消除配置漂移 |
| 可重复(Repeatable) | 同一份代码可反复执行,结果相同;新机器上线只需跑一遍代码 |
| 可审计(Auditable) | 配置即代码(Infrastructure as Code),进 Git 仓库,有提交记录、有 code review、可回滚 |
| 幂等(Idempotent) | 执行 1 次和执行 N 次的结果完全一样,不会重复叠加副作用 |
| 自文档化 | manifest 本身就是环境的文档,读代码即知系统配置 |
使用这类工具时,通常需要关注以下几点:
| 关注点 | 解决思路 |
|---|---|
| 大并发 | 使用 MQ 消息队列(如 ActiveMQ/RabbitMQ + MCollective) |
| 操作系统/版本差异 | 通过 facter 等工具获取客户端信息,按系统分支处理 |
| 异常处理 | 日志、报告(report) |
| 分组机制 | 按角色、正则、节点分组(node 定义) |
| Web 管理界面 | dashboard(puppet-dashboard、Foreman) |
| 操作语言 | 自有的 DSL 或声明式语言 |
1.3 主流配置管理工具对比
常见自动化运维工具:Cobbler、Shell 脚本、Puppet、Ansible、SaltStack、Chef、Python 自研。
四大主流配置管理工具对比:
| 对比项 | Puppet | Ansible | SaltStack | Chef |
|---|---|---|---|---|
| 开发语言 | Ruby(+ Clojure 写的 Puppet Server) | Python | Python | Ruby + Erlang |
| 是否需要 agent | 需要(也支持无 agent 的 puppet apply 单机模式) |
不需要(agentless) | 需要 minion(也有 salt-ssh 无 agent 模式) | 需要 chef-client |
| 通信方式 | HTTPS + SSL 双向证书,Master 监听 8140 | SSH(默认 22 端口) | ZeroMQ 消息队列,master 监听 4505/4506 | HTTPS REST API |
| 配置语言 | 自有声明式 DSL(.pp manifest) |
YAML(playbook) | YAML(state,SLS 文件) | Ruby DSL(recipe) |
| 学习曲线 | 较陡(需学 DSL、资源关系、Hiera) | 平缓(YAML + SSH,最易上手) | 中等 | 陡(需懂 Ruby) |
| 执行模型 | Pull(Agent 定期拉取,默认 30 分钟) | Push(控制机主动推) | Push 为主,也支持 Pull | Pull |
| 适用规模 | 大规模(数千至上万节点),强于"状态维持" | 中小规模及临时批量任务,强于"编排部署" | 大规模、要求实时性高(消息队列速度快) | 大规模,云环境较多 |
| 社区现状 | 成熟稳定,企业存量多,新项目增速放缓 | 最活跃,事实上的主流 | 活跃度中等 | 逐步收缩 |
| 典型场景 | 长期状态合规、基线管理 | 应用发布、临时运维任务 | 大规模实时命令下发 + 状态管理 | 云平台自动化 |
详见 14-02-Ansible自动化部署.md
详见 14-04-SaltStack配置管理.md
选型经验:
- 只是想"批量执行命令、快速上线应用" → Ansible;
- 需要"长期保证 1000+ 台机器的配置基线不漂移" → Puppet;
- 需要"秒级向上万台机器下发命令 + 状态管理" → SaltStack;
- 团队是 Ruby 技术栈、重云环境 → Chef。
1.4 声明式与命令式的区别(理解 Puppet 的关键)
这是初学 Puppet 最容易卡住的地方。Shell 脚本是命令式的,Puppet 是声明式的。
命令式(Imperative):描述"怎么做",一步一步下指令。
# 命令式:告诉机器每一步怎么做
useradd user3
echo "hello" > /tmp/test.txt
yum install -y httpd
systemctl start httpd
问题:第二次执行时 useradd user3 会报 "user already exists",脚本可能中断;echo > 会覆盖别人的修改也无从判断。
声明式(Declarative):描述"要什么结果"(期望状态 desired state),由工具自己判断当前状态、计算差异、只做必要的变更。
# 声明式:只描述我要的最终状态
user { 'user3':
ensure => present,
}
file { '/tmp/test.txt':
content => "hello\n",
}
package { 'httpd':
ensure => installed,
}
service { 'httpd':
ensure => running,
enable => true,
}
Puppet 每次运行时的逻辑是:
- 检查当前状态(用户 user3 存在吗?文件内容是这个吗?包装了吗?服务在跑吗?);
- 与声明的期望状态对比;
- 只对不一致的部分做修改,一致的部分什么都不做(输出中不会出现该资源的变更信息)。
1.5 幂等性(Idempotence)
幂等:同一操作执行一次与执行任意多次,系统最终状态相同,且不产生额外副作用。
mkdir /test→ 不幂等(第二次报 File exists)mkdir -p /test→ 幂等echo "x" >> /etc/hosts→ 不幂等(每次追加一行,跑 10 次多 10 行)file { '/etc/hosts': content => ... }→ 幂等(内容一致就不动)
Puppet 的绝大多数资源类型(file、package、service、user、group、cron、host、mount)天然幂等。
唯一容易破坏幂等性的是 exec 资源,因为它执行的是裸命令,Puppet 不知道这条命令做了什么。因此 exec 必须配合 onlyif / unless / creates / refreshonly 等"幂等守卫",否则每次 Puppet 运行(默认每 30 分钟一次)都会重复执行,造成灾难(比如反复重启服务、反复追加配置行)。
结论:能用原生资源类型表达的,绝不要用 exec。
二、Puppet 架构与运行流程
2.1 C/S 架构
Puppet 是典型的 C/S(Master/Agent)架构配置管理工具:
- master:管理端,集中编写配置清单(manifest),监听 8140 端口;
- agent:被管理端,周期性(默认 30 分钟,由
runinterval控制)从 master 拉取配置并在本机应用。
master
|
+----------+----------+
| | |
server1 server2 server3 ...... (agent)
Puppet Master
|
---------------------
| | |
Agent1 Agent2 Agent3
管理员在 master 上执行诸如 touch /tmp/123、rpm -ivh xxx、yum install xxx 等操作的"声明",由 agent 负责真正落地。
当被管理机器的操作系统或版本不同时,Puppet 提供一个叫 facter 的包,可以采集被管机器的系统、版本等信息,管理员据此写出差异化配置。
2.2 两种运行模式
(1)Master/Agent 模式(C/S,生产环境标准做法)
代码集中放在 Master 上,Agent 定期拉取。优点是集中管理、有报告、有证书鉴权。
(2)单机 apply 模式(Masterless)
不需要 Master,直接在本机执行 manifest:
puppet apply /etc/puppet/manifests/site.pp
puppet apply -e 'file { "/tmp/a.txt": content => "hi\n" }' # 直接执行一段代码
适用场景:学习测试、镜像打包(Packer/Docker 构建)、少量机器、用 Git + cron 自行分发代码的 masterless 架构。
2.3 核心组件
| 组件 | 作用 | 说明 |
|---|---|---|
| Puppet Master | 编译并下发 catalog,充当 CA | 3.x 默认用 Ruby 的 WEBrick(单线程,只能撑约 10 台 agent,仅供测试);生产需换 Passenger + Apache。4.x+ 改为 Puppet Server(Clojure/JRuby 编写,跑在 JVM 上,多线程,性能高得多,默认即生产可用) |
| Puppet Agent | 收集 facts、请求并应用 catalog、回报 report | 守护进程或 puppet agent --test 手工运行 |
| Facter | 采集节点系统信息(fact),如 OS 类型、版本、IP、内存、CPU | 结果作为顶层变量供 manifest 使用 |
| PuppetDB | 存储节点 facts、catalog、report 与导出资源 | 支持导出资源(exported resources)、查询 API;底层用 PostgreSQL |
| Hiera | 数据与代码分离的层级键值查找系统 | 把环境差异、敏感数据从 manifest 中抽出,放进 YAML |
| Puppet CA | 内置证书颁发机构,签发 Agent 证书 | 默认由 Master 自身承担 |
| Dashboard / Foreman | Web 报告与管理界面 | 见后文扩展章节 |
| MCollective | 基于消息队列的实时命令下发框架 | 配合 ActiveMQ/RabbitMQ 使用 |
2.4 Puppet 的运行流程(五步)
这是必须记住的核心流程:
Agent Master
| |
1. 收集 Facts (facter) |
|------- 2. 发送 Facts + 证书鉴权 ----------->|
| 3. 编译 Catalog
| (读 site.pp / node / class /
| 模块 / Hiera 数据,
| 把 manifest 编译成
| 针对该节点的资源清单)
|<------ 4. 下发 Catalog (纯资源列表) --------|
5. 应用 Catalog,比对当前状态并修改
|------- 6. 回报 Report -------------------->|
| (存入 PuppetDB / Dashboard)
- Agent 收集 Facts:Agent 启动后先运行 facter,采集本机 OS、版本、主机名、IP、内存等信息;
- 发送给 Master:通过 HTTPS(8140)把 facts 发给 Master,同时完成 SSL 双向认证;
- Master 编译 Catalog:Master 根据 facts 与节点名匹配 node 定义,读取对应的 class/模块/Hiera 数据,把声明式代码编译成一份只属于该节点的、已经解析完所有变量和条件判断的资源清单(Catalog)。注意:编译发生在 Master 端,Agent 拿到的是结果而不是代码;
- 下发 Catalog:Master 把 catalog 返回给 Agent;
- Agent 应用并回报:Agent 逐个资源比对当前状态与期望状态,只修改不一致的部分,然后生成 report 回报给 Master。
关键推论:修改
site.pp或模块代码后不需要重启 Master,因为 catalog 是每次请求时现场编译的。但修改puppet.conf必须重启服务才生效。
2.5 证书与 SSL 认证机制
为什么需要证书?
Master 会把整个环境的配置(可能包含密码、密钥、内部拓扑)下发给 Agent。如果任何机器都能冒充某个节点来请求 catalog,就能窃取配置;反之如果 Agent 不验证 Master,也可能被中间人下发恶意配置。因此 Puppet 使用 SSL 双向认证(mutual TLS):
- Agent 要验证 Master 的证书(防止连到假 Master);
- Master 要验证 Agent 的证书(确认它就是它声称的那个节点,节点名即证书 CN)。
CA 的角色
Puppet Master 默认自带一个 CA(证书颁发机构)。CA 的私钥/证书保存在 Master 的 ssldir/ca/ 下。所有 Agent 的证书都由这个 CA 签发,因此双方可以通过"都信任同一个 CA"来互相验证。
首次连接的证书签发流程
1. Master 首次启动 → 自动生成 CA 密钥对 + CA 证书 + Master 自身的服务器证书
2. Agent 首次运行 puppet agent --test
├─ 本地生成私钥
├─ 生成 CSR(证书签名请求,CN = certname,默认是 FQDN)
├─ 把 CSR 发送给 Master
└─ 下载 CA 证书(ca.pem),然后等待(阻塞或退出)
3. Master 端:puppet cert list → 看到待签名的请求
puppet cert sign <name> → 用 CA 私钥签发证书
4. Agent 再次运行 puppet agent --test
├─ 取回自己已签发的证书(<certname>.pem)
└─ 建立双向 SSL 连接,正式获取 catalog
证书与主机名、时间强绑定(后文排错章节详述):
- 证书的 CN 是节点名,主机名改了证书就失效;
- 证书有
notBefore/notAfter有效期,两端时间不同步会导致证书"尚未生效"或"已过期"。
2.6 版本演进与差异
Puppet 的版本跨度大,目录结构和命令差异明显,这是照抄旧教程最容易踩的坑。
(1)目录结构对照
| 项目 | Puppet 3.x(CentOS 6/7 早期,OSS 包) | Puppet 4.x / 5.x / 6.x(AIO 全包) |
|---|---|---|
| 配置目录 confdir | /etc/puppet/ |
/etc/puppetlabs/puppet/ |
| 主配置文件 | /etc/puppet/puppet.conf |
/etc/puppetlabs/puppet/puppet.conf |
| manifest 入口 | /etc/puppet/manifests/site.pp |
/etc/puppetlabs/code/environments/production/manifests/site.pp |
| 模块目录 | /etc/puppet/modules/ |
/etc/puppetlabs/code/environments/production/modules/(另有 /etc/puppetlabs/code/modules/) |
| 代码根目录 codedir | 无(混在 confdir) | /etc/puppetlabs/code/ |
| 数据目录 vardir | /var/lib/puppet/ |
/opt/puppetlabs/puppet/cache/ |
| SSL 目录 ssldir | /var/lib/puppet/ssl/ |
/etc/puppetlabs/puppet/ssl/ |
| 日志目录 logdir | /var/log/puppet/ |
/var/log/puppetlabs/puppet/(Server 日志在 /var/log/puppetlabs/puppetserver/) |
| 运行目录 rundir | /var/run/puppet/ |
/var/run/puppetlabs/ |
| Hiera 配置 | /etc/puppet/hiera.yaml |
/etc/puppetlabs/puppet/hiera.yaml(全局)+ 环境层 hiera.yaml(Hiera 5) |
| 可执行文件 | /usr/bin/puppet(系统 Ruby) |
/opt/puppetlabs/bin/puppet(自带 Ruby,需加 PATH 或做软链接) |
(2)包名与服务名对照
| 项目 | Puppet 3.x | Puppet 4.x+ (AIO) |
|---|---|---|
| Master 包名 | puppet-server |
puppetserver |
| Agent 包名 | puppet |
puppet-agent |
| Master 服务名 | puppetmaster |
puppetserver |
| Agent 服务名 | puppet(部分版本 puppetagent) |
puppet |
| Web 服务器 | WEBrick(测试)/ Passenger(生产) | 内置 Jetty(JVM),生产可用 |
| Ruby | 依赖系统 Ruby 1.8/1.9 | AIO 自带 Ruby,不污染系统 |
(3)命令对照
| 功能 | Puppet 3.x | Puppet 6.x+ |
|---|---|---|
| 查看待签证书 | puppet cert list |
puppetserver ca list |
| 查看全部证书 | puppet cert list --all |
puppetserver ca list --all |
| 签发证书 | puppet cert sign <name> |
puppetserver ca sign --certname <name> |
| 批量签发 | puppet cert --sign --all |
puppetserver ca sign --all |
| 吊销/清除证书 | puppet cert clean <name> |
puppetserver ca clean --certname <name> |
| 生成证书 | puppet cert generate <name> |
puppetserver ca generate --certname <name> |
说明:
puppet cert在 5.x 中被标记为废弃(deprecated),在 6.0 中被移除,改用puppetserver ca。Puppet 4.x/5.x 两者都可用。
(4)AIO(All-In-One)全包安装
从 4.x 开始,Puppet 官方改用 AIO 打包方式:puppet-agent 包里自带专属的 Ruby、Facter、Hiera、gem 依赖,全部装在 /opt/puppetlabs/ 下,与系统 Ruby 完全隔离。好处是不再有"系统 Ruby 版本冲突"问题,代价是路径变了、命令不在默认 PATH 里:
# AIO 安装后把 puppet 命令加入 PATH
echo 'export PATH=/opt/puppetlabs/bin:$PATH' >> /etc/profile.d/puppet.sh
source /etc/profile.d/puppet.sh
# 或建软链接
ln -s /opt/puppetlabs/bin/puppet /usr/bin/puppet
(5)语言层面的差异(3.x → 4.x)
- 4.x 引入数据类型系统(
String、Integer、Array[String]、Enum、Optional等),参数化类可做类型校验; - 4.x 移除了
import关键字(3.x 中import 'nodes.pp'的写法在 4.x 失效,改为把所有.pp放在环境的manifests/目录下自动加载); - 4.x 中裸词(bareword)不再自动当字符串,属性值建议一律加引号;
mode必须写成字符串'0644'; - 4.x 中
$::osfamily等 fact 建议用$facts['osfamily']访问(结构化 facts); - 4.x 默认启用 directory environments(目录环境),
/etc/puppetlabs/code/environments/<env>/即一个环境。
三、安装部署与证书管理
3.1 环境准备
实验环境规划(一个 master,两个 agent,全部为 CentOS 7.3):
| 角色 | 主机名 | IP | 说明 |
|---|---|---|---|
| Master | master.cluster.com | 10.1.1.2 | 管理端,监听 8140,兼 CA |
| Agent1 | agent1.cluster.com | 10.1.1.3 | 被管理端 |
| Agent2 | agent2.cluster.com | 10.1.1.4 | 被管理端 |
| (可选)yum 源 | server.cluster.com | 10.1.1.1 | 教学环境本地 yum 仓库 |
准备要求汇总:
- 主机名及互相绑定
/etc/hosts - 时间同步
- 静态 IP
- 关闭 firewalld、SELinux(或按下文放行 8140 端口)
- 配置 yum 源(本地 ISO、163、EPEL、Puppet 官方源)
3.1.1 主机名与 /etc/hosts(最常见故障源,务必正确)
这是 Puppet 部署中排第一位的故障原因,因为 Puppet 的证书 CN 就是主机名,Agent 必须能用名字(而不是 IP)连上 Master,Master 也要能反查 Agent 的名字。
CentOS 7 设置主机名:
hostnamectl set-hostname --static master.cluster.com
hostnamectl set-hostname --static agent1.cluster.com
hostnamectl set-hostname --static agent2.cluster.com
hostname -f # 验证 FQDN 是否正确
CentOS 6 设置主机名:
# 临时生效
hostname master.cluster.com
# 永久生效,编辑 /etc/sysconfig/network
# HOSTNAME=master.cluster.com
vi /etc/sysconfig/network
三台机器的 /etc/hosts 都要写全(Master 和所有 Agent 都要能互相解析):
# /etc/hosts
127.0.0.1 localhost localhost.localdomain
10.1.1.2 master.cluster.com master
10.1.1.3 agent1.cluster.com agent1
10.1.1.4 agent2.cluster.com agent2
要点强调:
- Agent 的
server =配置项必须写 Master 的主机名,不能写 IP,也不建议写别名(别名可能导致证书 CN 不匹配报错),且该名字必须能通过 hosts 或 DNS 解析; - Agent 自己的 FQDN 必须唯一且稳定,一旦签发证书后再改主机名,必须重新签发证书;
- 用
ping master.cluster.com、getent hosts master.cluster.com验证解析; - 克隆虚拟机做实验时,最容易出现两台 Agent 主机名相同 → 证书冲突。
3.1.2 时间同步(对证书有效性至关重要)
SSL 证书带有生效时间(notBefore)和过期时间(notAfter)。如果:
- Agent 时间比 Master 慢很多 → 证书看起来"尚未生效"(certificate is not yet valid);
- Agent 时间比 Master 快很多 → 证书看起来"已过期"(certificate has expired);
两种情况都会导致 SSL 握手失败,且报错信息很容易被误判为"证书没签"。
# CentOS 7:chrony(推荐)
yum install -y chrony
systemctl enable chronyd
systemctl start chronyd
chronyc sources -v
timedatectl set-timezone Asia/Shanghai
# CentOS 7/6:临时手工同步
ntpdate ntp1.aliyun.com
# CentOS 6:ntpd
yum install -y ntp
service ntpd start
chkconfig ntpd on
# 验证两端时间一致
date
详见 08-03-时间同步与Xinetd.md
3.1.3 防火墙放行 8140
Master 需要放行 TCP 8140(若搭 dashboard 还需 8141,PuppetDB 需 8081)。
# CentOS 7 firewalld
firewall-cmd --permanent --add-port=8140/tcp
firewall-cmd --permanent --add-port=8141/tcp # 可选:dashboard
firewall-cmd --reload
firewall-cmd --list-all
# 教学实验环境可直接关闭
systemctl stop firewalld
systemctl disable firewalld
# CentOS 6 iptables
iptables -I INPUT -p tcp --dport 8140 -j ACCEPT
service iptables save
service iptables restart
# 或关闭
service iptables stop
chkconfig iptables off
详见 07-03-防火墙与SELinux.md
3.1.4 SELinux 处理
SELinux 开启时,Puppet 用 file 资源下发的文件可能因缺少正确的安全上下文(context)而无法被服务读取(典型:下发 /var/www/html/index.html 后 httpd 403)。
# 查看状态
getenforce
sestatus
# 临时关闭(立即生效,重启失效)
setenforce 0
# 永久关闭:编辑 /etc/selinux/config
# SELINUX=disabled
sed -i 's/^SELINUX=enforcing/SELINUX=disabled/' /etc/selinux/config
# 若必须保持 enforcing,则在 file 资源中显式声明上下文
file { '/var/www/html/index.html':
ensure => file,
content => "hello\n",
seltype => 'httpd_sys_content_t', # 显式指定 SELinux 类型
selrole => 'object_r',
}
详见 07-03-防火墙与SELinux.md
3.1.5 静态 IP 与 NetworkManager
Puppet 节点必须是静态 IP,IP 漂移会导致 hosts 解析失效。CentOS 7 上 NetworkManager 可能覆盖 /etc/resolv.conf 与 hosts 相关行为,教学环境通常关闭它:
# CentOS 7:关闭 NetworkManager,使用 network 服务
systemctl stop NetworkManager
systemctl disable NetworkManager
systemctl restart network
systemctl enable network
# 或者保留 NetworkManager,但让它不改 DNS:
# 在 /etc/NetworkManager/NetworkManager.conf 的 [main] 段加
# dns=none
静态 IP 配置(CentOS 7,/etc/sysconfig/network-scripts/ifcfg-eth0):
TYPE=Ethernet
BOOTPROTO=static
NAME=eth0
DEVICE=eth0
ONBOOT=yes
IPADDR=10.1.1.2
NETMASK=255.255.255.0
GATEWAY=10.1.1.254
DNS1=114.114.114.114
NM_CONTROLLED=no
3.2 配置 yum 源
Puppet 官方源(Puppet 3.x,EL7):
# /etc/yum.repos.d/puppet.repo
[puppet]
name=puppet
baseurl=http://yum.puppetlabs.com/el/7Server/products/x86_64/
enabled=1
gpgcheck=0
[puppet-dependencies]
name=puppet-dependencies
baseurl=http://yum.puppetlabs.com/el/7Server/dependencies/x86_64/
enabled=1
gpgcheck=0
教学环境本地源(已将两个仓库合并):
# /etc/yum.repos.d/puppet.repo
[puppet]
name=puppet
baseurl=http://10.1.1.1/puppet
enabled=1
gpgcheck=0
Puppet 4.x+ 官方 release 包方式(PC1 / puppet5 / puppet6 仓库):
# Puppet 4.x(PC1 仓库)
rpm -Uvh https://yum.puppetlabs.com/puppetlabs-release-pc1-el-7.noarch.rpm
# Puppet 5.x
rpm -Uvh https://yum.puppetlabs.com/puppet5-release-el-7.noarch.rpm
# Puppet 6.x
rpm -Uvh https://yum.puppetlabs.com/puppet6-release-el-7.noarch.rpm
# CentOS 6 换成 el-6
rpm -Uvh https://yum.puppetlabs.com/puppetlabs-release-pc1-el-6.noarch.rpm
国内源加速(清华/阿里镜像,可选):
# /etc/yum.repos.d/puppet6.repo
[puppet6]
name=Puppet 6 Repository el 7 - x86_64
baseurl=https://mirrors.tuna.tsinghua.edu.cn/puppet/yum/puppet6/el/7/x86_64
enabled=1
gpgcheck=0
同时建议配置系统基础源与 EPEL:
# 阿里云 base 源
curl -o /etc/yum.repos.d/CentOS-Base.repo http://mirrors.aliyun.com/repo/Centos-7.repo
# EPEL
yum install -y epel-release
yum clean all && yum makecache
3.3 安装 Master
Puppet 3.x:
yum install puppet-server -y
# CentOS 7
systemctl start puppetmaster
systemctl enable puppetmaster
systemctl status puppetmaster
# CentOS 6
service puppetmaster start
chkconfig puppetmaster on
service puppetmaster status
# 验证端口
lsof -i:8140
netstat -tunlp | grep 8140
ss -tunlp | grep 8140
Puppet 4.x/5.x/6.x(puppetserver):
yum install puppetserver -y
# puppetserver 跑在 JVM 上,默认申请 2GB 内存,实验机内存小需调小
vi /etc/sysconfig/puppetserver
# JAVA_ARGS="-Xms512m -Xmx512m -Djruby.logger.class=com.puppetlabs.jruby_utils.jruby.Slf4jLogger"
systemctl start puppetserver
systemctl enable puppetserver
systemctl status puppetserver
# 首次启动较慢(JVM 预热 + 生成 CA),耐心等待
tail -f /var/log/puppetlabs/puppetserver/puppetserver.log
3.4 安装 Agent
在所有 agent 上执行。
Puppet 3.x:
yum install puppet -y
systemctl start puppet
systemctl enable puppet
systemctl status puppet
部分版本服务名为 puppetagent:
systemctl start puppetagent
systemctl enable puppetagent
systemctl status puppetagent
CentOS 6:
service puppet start
chkconfig puppet on
Puppet 4.x+:
yum install puppet-agent -y
export PATH=/opt/puppetlabs/bin:$PATH
puppet --version
systemctl start puppet
systemctl enable puppet
3.5 版本一致性要求
- Agent 的版本不应高于 Master 的版本。Master 负责编译 catalog,若 Agent 比 Master 新,Agent 可能请求 Master 不支持的 API 版本或语言特性,导致编译/应用失败;反之(Agent 略旧于 Master)通常是安全兼容的;
- 同一环境内的 Agent 尽量保持版本统一,避免同一份 manifest 在不同 Agent 上行为不一致(例如 3.x 与 4.x 对裸词、
mode类型的处理不同); - 3.x 的 Agent 不能连 4.x 的 Master(协议与 catalog 格式差异过大),跨大版本升级必须先升 Master 再升 Agent;
- 查看版本:
puppet --version
facter --version
ruby --version # 3.x 依赖系统 Ruby
/opt/puppetlabs/puppet/bin/ruby -v # 4.x+ AIO 自带 Ruby
rpm -qa | grep -E 'puppet|facter|ruby'
依赖说明:
- Puppet 3.x 依赖系统 Ruby 1.8.7/1.9.3+、
ruby-libs、ruby-shadow(管理用户密码需要)、facter、hiera; - Puppet 4.x+ 的 AIO 包自带 Ruby 2.x、Facter 3(用 C++ 重写,速度快很多)、Hiera,不依赖系统 Ruby;
facter是必装组件,Puppet 靠它采集 fact。
3.6 主配置文件 puppet.conf 逐项详解
路径对照:
| 版本 | 主配置文件路径 |
|---|---|
| Puppet 3.x | /etc/puppet/puppet.conf |
| Puppet 4.x+ | /etc/puppetlabs/puppet/puppet.conf |
配置文件是 INI 格式,分 [main]、[master](4.x+ 也叫 [server])、[agent] 三个段。[main] 是全局默认,其他段可覆盖它。
完整示例(Puppet 3.x,Master 端):
[main]
# 配置目录,所有相对路径的基准
confdir = /etc/puppet
# 变量数据目录:ssl、state、报告、缓存、客户端 catalog 都在这里
vardir = /var/lib/puppet
# 日志目录
logdir = /var/log/puppet
# PID 文件目录
rundir = /var/run/puppet
# SSL 证书目录(CA、私钥、证书、CRL)
ssldir = $vardir/ssl
# 模块搜索路径,多个路径用冒号分隔
modulepath = /etc/puppet/modules:/usr/share/puppet/modules
# Agent 端需要:指定 Master 主机名
server = master.cluster.com
[master]
# Master 自身的证书名(证书 CN),默认取 FQDN
certname = master.cluster.com
# 证书的备用名,让 Agent 可以用别名/IP 连接而不报 CN 不匹配
# 注意:修改此项后必须重新生成 Master 证书才生效
dns_alt_names = master.cluster.com,master,puppet,10.1.1.2
# 自动签名:true 表示无条件签发所有 CSR(危险);
# 也可指向 autosign.conf 文件按域名白名单自动签
autosign = /etc/puppet/autosign.conf
# 报告处理器:store 存本地 yaml,http 发给 dashboard,log 写日志
reports = store,http
reporturl = http://10.1.1.2:8141/reports
# manifest 入口文件(3.x)
manifest = $confdir/manifests/site.pp
# 环境目录(4.x+ 用 environmentpath)
environmentpath = $confdir/environments
[agent]
# Master 地址(必须是主机名,且可解析)
server = master.cluster.com
# 本机证书名,默认 FQDN;多网卡/多域名环境可显式指定
certname = agent1.cluster.com
# 守护模式下与 Master 同步的间隔,单位秒,默认 1800(30 分钟)
# 可写 30m / 1h / 3(秒)
runinterval = 1800
# 使用的环境
environment = production
# 是否把运行结果回报给 Master
report = true
# 随机延迟启动,避免上千 Agent 同时打爆 Master
splay = true
splaylimit = 600
# 是否以守护进程方式运行
daemonize = true
# 日志级别
log_level = notice
# 遇到 pluginsync(同步自定义 fact/函数)
pluginsync = true
Puppet 4.x+ 示例:
[main]
confdir = /etc/puppetlabs/puppet
codedir = /etc/puppetlabs/code
vardir = /opt/puppetlabs/puppet/cache
logdir = /var/log/puppetlabs/puppet
rundir = /var/run/puppetlabs
ssldir = /etc/puppetlabs/puppet/ssl
server = master.cluster.com
[server]
certname = master.cluster.com
dns_alt_names = master.cluster.com,puppet
autosign = /etc/puppetlabs/puppet/autosign.conf
reports = store,puppetdb
environmentpath = $codedir/environments
strict_variables = true
[agent]
server = master.cluster.com
certname = agent1.cluster.com
runinterval = 30m
environment = production
report = true
splay = true
关键参数逐项说明:
| 参数 | 所属段 | 说明 |
|---|---|---|
confdir |
main | 配置根目录,$confdir 可在其他参数中引用 |
vardir |
main | 可变数据目录(ssl、state、lock、报告、缓存) |
logdir |
main | 日志目录 |
rundir |
main | PID / lock 文件目录 |
ssldir |
main | 证书目录;重置证书就是删这个目录 |
modulepath |
main/master | 模块搜索路径(4.x 推荐用环境目录中的 modules) |
certname |
master/agent | 证书 CN,唯一标识节点,也是 node 匹配依据 |
dns_alt_names |
master | Master 证书的备用名,允许 Agent 用别名连接 |
autosign |
master | 自动签名开关或白名单文件 |
reports |
master | 报告处理方式:none/store/log/http/puppetdb |
manifest |
master | 3.x 的入口 manifest 文件 |
environmentpath |
master | 目录环境根路径 |
server |
agent | Master 主机名(必填) |
runinterval |
agent | 拉取间隔,默认 1800 秒 |
environment |
agent | 使用哪个环境(production/development) |
report |
agent | 是否回报 report |
splay / splaylimit |
agent | 随机化启动时间,削峰 |
noop |
agent | 全局空跑模式,只报告不修改 |
pluginsync |
agent | 同步 Master 上模块的自定义 fact/函数/类型 |
重要:
puppet.conf修改后必须重启对应服务才生效(Master 重启 puppetmaster/puppetserver,Agent 重启 puppet)。而 manifest/模块代码修改后不需要重启。查看某参数当前实际生效的值:
puppet config print all puppet config print modulepath puppet config print ssldir puppet config print confdir vardir logdir puppet config print server --section agent
3.7 证书签发的完整流程
3.7.1 第一步:Master 启动并生成 CA
首次启动 Master 时会自动生成 CA:
# Puppet 3.x
systemctl start puppetmaster
ls /var/lib/puppet/ssl/
# ca certs crl.pem private private_keys public_keys
ls /var/lib/puppet/ssl/ca/
# ca_crl.pem ca_crt.pem ca_key.pem ca_pub.pem inventory.txt
# requests serial signed
# Puppet 4.x+
systemctl start puppetserver
ls /etc/puppetlabs/puppet/ssl/ca/
验证 Master 端口已监听:
lsof -i:8140
ss -tunlp | grep 8140
3.7.2 第二步:Agent 配置并发起 CSR
在所有 agent 上编辑 /etc/puppet/puppet.conf(4.x 为 /etc/puppetlabs/puppet/puppet.conf),在 [main] 段(或 [agent] 段)加上:
server = master.cluster.com
这里必须写 puppet 服务器的主机名,别名可能有问题,并且必须能通过 hosts/DNS 解析。
发送验证请求(发起 CSR):
puppet agent --server=master.cluster.com --test
该命令会产生创建 SSL 证书的信息,典型输出:
Info: Creating a new SSL key for agent1.cluster.com
Info: Caching certificate for ca
Info: csr_attributes file loading from /etc/puppet/csr_attributes.yaml
Info: Creating a new SSL certificate request for agent1.cluster.com
Info: Certificate Request fingerprint (SHA256): 0D:FE:23:A0:...
Exiting; no certificate found and waitforcert is disabled
查看 agent 端证书目录,此时只有 CA 证书,还没有自己的证书:
ls /var/lib/puppet/ssl/certs/
# ca.pem
3.7.3 第三步:Master 签名
在 master 上列出待签请求:
puppet cert list
"agent1.cluster.com" (SHA256) 0D:FE:23:A0:BA:DC:59:74:95:A3:DD:0D:15:C3:68:1D:EC:9C:94:C5:49:9F:65:65:9A:1A:DB:EB:B4:C3:05:DD
"agent2.cluster.com" (SHA256) 36:A4:39:46:9D:09:BF:E7:96:D4:AE:A3:51:3B:C0:07:1A:E9:A1:A1:58:FB:DE:2F:28:09:1A:CD:9E:96:3B:1C
签发单个证书:
puppet cert sign agent1.cluster.com
对所有请求批量签名:
puppet cert --sign --all
# 等价写法
puppet cert sign --all
查看已签发的全部证书(+ 号表示已签发,无前缀表示待签):
puppet cert list --all
+ "master.cluster.com" (SHA256) AA:BB:... (alt names: "DNS:master.cluster.com", "DNS:puppet")
+ "agent1.cluster.com" (SHA256) 0D:FE:...
+ "agent2.cluster.com" (SHA256) 36:A4:...
吊销/清除某个 Agent 的证书:
puppet cert clean agent1.cluster.com
Puppet 4.x/5.x/6.x 的 puppetserver ca 对照命令:
puppetserver ca list # 待签
puppetserver ca list --all # 全部
puppetserver ca sign --certname agent1.cluster.com # 签发单个
puppetserver ca sign --all # 批量签发
puppetserver ca clean --certname agent1.cluster.com # 吊销清除
puppetserver ca revoke --certname agent1.cluster.com # 只吊销不删文件
puppetserver ca generate --certname agent3.cluster.com # 在 Master 端直接生成
3.7.4 第四步:Agent 再次拉取取回证书
回到所有 agent 再次执行:
puppet agent --server=master.cluster.com --test
再次查看 /var/lib/puppet/ssl/certs/,会多出一个以主机名命名的证书文件,例如 agent1 上为 agent1.cluster.com.pem:
ls /var/lib/puppet/ssl/certs/
# agent1.cluster.com.pem ca.pem
此时 SSL 双向认证建立完成,Agent 可以正常获取 catalog。
3.7.5 自动签名 autosign.conf
节点数量多(或云环境频繁扩缩容)时,手工签名不现实,可配置自动签名。
方式一:无条件自动签名(极不安全,仅限隔离的实验环境)
# puppet.conf 的 [master] 段
[master]
autosign = true
方式二:白名单文件(推荐)
# puppet.conf 的 [master] 段
[master]
autosign = /etc/puppet/autosign.conf
# /etc/puppet/autosign.conf
# 支持完整主机名
agent1.cluster.com
# 支持域名通配(*.开头)
*.cluster.com
*.test.example.com
# 4.x+ 路径
vi /etc/puppetlabs/puppet/autosign.conf
方式三:基于策略的自动签名(policy-based,最安全)
autosign 指向一个可执行脚本,Puppet 把 certname 作为参数、CSR 内容从标准输入传给它,脚本返回 0 则签发。可结合 csr_attributes.yaml 中的预共享密钥校验:
[master]
autosign = /usr/local/bin/check_csr.sh
#!/bin/bash
# /usr/local/bin/check_csr.sh
CERTNAME=$1
CSR=$(cat)
# 从 CSR 扩展属性中取出预共享 token 并校验
echo "$CSR" | openssl req -noout -text | grep -q "MySecretToken12345"
exit $?
自动签名的安全风险(必须知晓):
- 开启
autosign = true后,任何能连上 8140 的机器都能声称自己是任意节点名(例如冒充db-master.example.com)并拿到该节点的完整 catalog; - catalog 里往往包含数据库密码、SSH 密钥、API token 等敏感数据 → 直接导致配置泄露;
- 攻击者还可以抢先注册一个合法节点即将使用的名字,造成正常节点无法上线(拒绝服务);
- 因此:生产环境禁止
autosign = true,至少使用域名白名单,最好用带预共享密钥的策略脚本,并把 8140 端口限制在内网/安全组内。
3.7.6 证书问题的重置方法(重建连接)
如果建立连接有问题(证书冲突、主机名改过、克隆机重复、报 SSL 错误),需要两端都清理后重新签发。
# ===== 第一步:在 Master 上清除该 Agent 的证书 =====
puppet cert clean agent1.cluster.com
puppet cert clean agent2.cluster.com
# 4.x+
puppetserver ca clean --certname agent1.cluster.com
# 确认已清除
puppet cert list --all
# ===== 第二步:在 Agent 上删除本地 ssl 目录 =====
systemctl stop puppet # 先停掉守护进程,否则会占用
rm -rf /var/lib/puppet/ssl/* # Puppet 3.x
# rm -rf /etc/puppetlabs/puppet/ssl/* # Puppet 4.x+
# 原文档写法(只删 certs 子目录,多数情况够用,彻底重置建议删整个 ssl 目录)
rm /var/lib/puppet/ssl/certs/* -rf
# ===== 第三步:Agent 重新发起请求 =====
puppet agent --server=master.cluster.com --test
# ===== 第四步:Master 重新签名 =====
puppet cert list
puppet cert --sign --all
# ===== 第五步:Agent 再次运行取回证书 =====
puppet agent --server=master.cluster.com --test
# ===== 第六步:恢复守护进程 =====
systemctl start puppet
排错后重新执行上述 3.7.2 ~ 3.7.4 的步骤即可。
如果是 Master 自身的证书要重置(例如改了 dns_alt_names 或改了 Master 主机名),影响面更大,所有 Agent 都要重签:
# Master 上
systemctl stop puppetmaster # 或 puppetserver
rm -rf /var/lib/puppet/ssl/* # 3.x;4.x 为 /etc/puppetlabs/puppet/ssl/*
systemctl start puppetmaster # 重新生成 CA 与 Master 证书
# 所有 Agent 上都要清理并重新申请
systemctl stop puppet
rm -rf /var/lib/puppet/ssl/*
puppet agent --test --server=master.cluster.com
3.8 验证与 Agent 运行方式
服务启动与开机自启:
# Master(3.x / 4.x+)
systemctl start puppetmaster ; systemctl enable puppetmaster
systemctl start puppetserver ; systemctl enable puppetserver
# Agent
systemctl start puppet ; systemctl enable puppet
systemctl start puppetagent ; systemctl enable puppetagent # 部分版本服务名
# CentOS 6
service puppetmaster start ; chkconfig puppetmaster on
service puppet start ; chkconfig puppet on
Agent 手工运行的常用参数:
# 最常用:前台运行一次,输出详细过程(--test = --onetime --verbose --no-daemonize --show_diff --detailed-exitcodes 的组合)
puppet agent --test
# 指定 Master
puppet agent --test --server master.cluster.com
# 空跑演练:只报告"将会做什么",不真正修改系统(上线前必做)
puppet agent --test --noop
# 显示更详细的调试信息(排错必备,会打印 HTTP 请求、fact 加载、资源比对细节)
puppet agent --test --debug
# 只要详细输出,不要 debug 那么啰嗦
puppet agent --test --verbose
# 显示文件内容差异
puppet agent --test --show_diff
# 不以守护进程方式运行(原文档写法)
puppet agent --test --server=master.cluster.com --no-daemonize --verbose
# 指定环境
puppet agent --test --environment development
# 一次性运行后退出,用退出码判断结果(0=无变更, 2=有变更, 4=有失败, 6=有变更且有失败)
puppet agent --onetime --no-daemonize --detailed-exitcodes
守护模式与 runinterval:
Puppet agent 默认每 30 分钟与 master 同步一次。为快速验证,可在客户端 puppet.conf 的 [agent] 段加入:
runinterval = 3
表示 3 秒同步一次(仅用于实验,生产环境绝不可这样设,会打爆 Master)。改完重启服务:
systemctl restart puppet
systemctl restart puppetagent
日志位置:
| 内容 | Puppet 3.x | Puppet 4.x+ |
|---|---|---|
| Master 日志 | /var/log/puppet/masterhttp.log、/var/log/messages |
/var/log/puppetlabs/puppetserver/puppetserver.log |
| Agent 日志 | /var/log/messages(syslog)、/var/log/puppet/ |
/var/log/messages、journalctl -u puppet |
| 报告 | /var/lib/puppet/reports/<certname>/*.yaml |
/opt/puppetlabs/puppet/cache/reports/ |
tail -f /var/log/messages | grep -i puppet
journalctl -u puppet -f
journalctl -u puppetserver -f
四、Puppet DSL 与 Manifest 基础
4.1 manifest 文件
- Puppet 的配置清单文件称为 manifest,扩展名为
.pp; - 入口文件是
site.pp:- Puppet 3.x:
/etc/puppet/manifests/site.pp - Puppet 4.x+:
/etc/puppetlabs/code/environments/production/manifests/site.pp
- Puppet 3.x:
site.pp修改后立即生效,无需重启 master(因为每次 Agent 请求时都会重新编译 catalog)。
语法检查(写完必做,避免 Agent 拉到坏代码):
# 检查单个文件语法
puppet parser validate /etc/puppet/manifests/site.pp
puppet parser validate /etc/puppet/modules/httpd/manifests/init.pp
# 批量检查所有 pp 文件
find /etc/puppet -name '*.pp' -exec puppet parser validate {} \;
# 检查 ERB 模板语法
erb -P -x -T '-' /etc/puppet/modules/httpd/templates/httpd.conf.erb | ruby -c
# 代码风格检查(需安装 puppet-lint gem)
gem install puppet-lint
puppet-lint /etc/puppet/modules/httpd/manifests/init.pp
# 本机试跑一段代码
puppet apply --noop /etc/puppet/manifests/site.pp
代码风格约定(Puppet Style Guide):
- 缩进使用 2 个空格,不用 Tab;
- 属性的
=>尽量对齐; - 资源类型和属性名全部小写;
- 资源标题(title)用单引号;除需要变量插值时用双引号;
- 每个资源块的最后一个属性后也加逗号(trailing comma),便于后续增删;
ensure永远写在属性列表的第一行;- 注释使用
#; - 模块名、类名只用小写字母、数字和下划线,不能有横线。
# 这是单行注释
/*
Puppet 也支持 C 风格多行注释(不推荐,官方建议用 #)
*/
# 推荐风格
file { '/etc/motd':
ensure => file,
owner => 'root',
group => 'root',
mode => '0644',
content => "Welcome to ${::fqdn}\n",
}
4.2 第一个 manifest 测试
服务端编写 site.pp(/etc/puppet/manifests/site.pp):
node default { # default 节点代表所有节点
file { "/tmp/test.txt": # 资源 title,未使用 path 参数时即文件路径
content => "hello,world\n", # 文件内容,文件不存在会自动创建
}
}
或写作(更规范的风格):
node default {
file { '/tmp/test.txt':
ensure => file,
content => "hello, world\n",
}
}
客户端验证:
puppet agent --test --server master.cluster.com
cat /tmp/test.txt
# hello,world
手动同步与排错:
同步异常时,在 agent 端手动执行查看报错:
puppet agent --test --server master.cluster.com
若提示:
Notice: Run of Puppet configuration client already in progress; skipping (/var/lib/puppet/state/agent_catalog_run.lock exists)
说明有另一个 Puppet 运行实例(通常是守护进程)正在跑,处理方式:
rm -rf /var/lib/puppet/state/agent_catalog_run.lock
systemctl stop puppet
systemctl stop puppetagent
puppet agent --test --server master.cluster.com
根据红色错误提示排错,完成后再启动服务:
systemctl start puppet
systemctl start puppetagent
更多资源语法参考:http://docs.puppetlabs.com/references/latest/type.html
4.3 资源声明的通用语法
资源类型 { '资源标题(title)':
属性1 => 值1,
属性2 => 值2,
}
- 资源类型(type):
file、package、service、exec等,小写; - 标题(title / namevar):资源的唯一标识。同一类型下 title 不能重复,否则报
Duplicate declaration。多数资源类型的 title 默认就是其name/path属性的值; - 属性(attribute):
=>左边属性名,右边值; - 引用资源:类型首字母大写 + 方括号,如
Package['httpd']、File['/etc/httpd/conf/httpd.conf']、Service['httpd']。
4.4 元参数(Metaparameters,所有资源类型通用)
| 元参数 | 作用 |
|---|---|
ensure |
期望状态(严格说是多数类型的普通属性,但用法通用) |
require |
本资源依赖某资源,被依赖者先执行 |
before |
本资源必须在某资源之前执行 |
notify |
本资源变更后,通知某资源刷新(refresh) |
subscribe |
本资源订阅某资源,被订阅者变更时自己刷新 |
tag |
给资源打标签,可用 puppet agent --tags xxx 只应用部分资源 |
alias |
给资源起别名,便于引用 |
schedule |
限制资源只在某时间窗口内应用 |
noop |
单个资源级别的空跑 |
audit |
审计某属性的变化而不修改它 |
loglevel |
该资源产生日志的级别 |
stage |
指定运行阶段(run stage) |
# tag 与 audit 示例
file { '/etc/sudoers':
ensure => file,
audit => [owner, group, mode], # 只审计不修改
tag => ['security', 'baseline'],
}
# 只应用带 security 标签的资源
puppet agent --test --tags security
五、核心资源类型详解
5.1 file 资源
常用属性:
| 属性 | 说明 |
|---|---|
path |
文件路径(不写则用 title) |
ensure |
present(存在即可)/ absent(删除)/ file(普通文件)/ directory(目录)/ link(软链接) |
content |
文件内容(字符串,支持 \n、变量插值、template()) |
source |
源文件路径或 puppet:/// URL |
target |
软链接指向的目标(ensure => link 时必填) |
mode |
权限,如 '0755'、'4755'(4.x 必须写成字符串) |
owner / group |
属主 / 属组 |
recurse |
true/false/remote/inf,递归处理目录 |
purge |
配合 recurse,删除目标目录中不由 Puppet 管理的多余文件 |
force |
删除目录、替换目录为文件等强制操作时需设为 true/yes |
backup |
备份策略:false 不备份,或写 filebucket 名字 |
replace |
false 时只在文件不存在时创建,已存在则不覆盖内容 |
links |
follow/manage,遇到符号链接的处理方式 |
checksum |
比对方式:md5(默认)/mtime/ctime |
show_diff |
是否在日志中显示内容差异 |
seltype/selrole/seluser/selrange |
SELinux 上下文 |
validate_cmd |
应用前用命令校验文件(如 visudo -c -f %),校验失败则不写入 |
5.1.1 创建目录、文件并指定权限
node /\.cluster\.com$/ {
file { "/test/":
ensure => directory,
}
file { "/test/abc":
content => "haha\n",
mode => 4755,
owner => bin,
group => daemon,
}
}
4.x 规范写法(属性值加引号):
node /\.cluster\.com$/ {
file { '/test/':
ensure => directory,
mode => '0755',
}
file { '/test/abc':
ensure => file,
content => "haha\n",
mode => '4755',
owner => 'bin',
group => 'daemon',
}
}
5.1.2 用 source 推送文件内容
把客户端 /etc/fstab 的内容复制为 /test/abc(相当于 cp /etc/fstab /test/abc,会覆盖原内容):
node /^agent\d+\.cluster\.com/ {
file { "/test/":
ensure => directory,
}
file { "/test/abc":
source => "/etc/fstab",
mode => 4755,
owner => bin,
group => daemon,
}
}
也可简写为:
file { "/test/abc":
source => "/etc/fstab",
mode => "4755",
owner => "bin",
group => "daemon",
}
注意:
source => "/etc/fstab"这种本地绝对路径是指 Agent 本机的路径。要从 Master 分发文件必须用puppet:///协议,见后文"文件分发"章节。
5.1.3 创建软链接
# 匹配 agentX.cluster.com,X 为任意数字
node /^agent\d+\.cluster\.com/ {
file { "/tmp/fstab":
ensure => link, # 创建软链接
target => "/etc/fstab", # 源文件
}
}
5.1.4 删除文件与目录
node 'agent1.cluster.com' {
file { "/tmp/test.txt":
ensure => absent, # 保证文件不存在,存在则删除
}
}
node 'agent2.cluster.com' {
file { "/tmp/test.txt":
ensure => absent,
}
}
删除目录需要加
force => yes(否则 Puppet 拒绝删除非空目录)。
file { '/tmp/olddir':
ensure => absent,
force => true, # 递归删除目录及其内容
purge => true,
}
5.1.5 递归分发目录、purge 清理与备份
# 把 Master 上的整个目录递归同步到 Agent,并清理 Agent 上多余的文件
file { '/etc/nginx/conf.d':
ensure => directory,
owner => 'root',
group => 'root',
mode => '0644',
recurse => true, # 递归
purge => true, # 删除目录中不由 Puppet 管理的多余文件
force => true, # purge 遇到子目录时需要
source => 'puppet:///modules/nginx/conf.d',
backup => false, # 不备份被覆盖的旧文件
}
# 只递归修改权限,不下发内容
file { '/opt/app':
ensure => directory,
owner => 'appuser',
group => 'appuser',
recurse => true,
}
# 只在文件不存在时创建,已存在不覆盖(适合初始化模板)
file { '/etc/app/local.conf':
ensure => file,
content => "# 本地覆盖配置,Puppet 只创建一次\n",
replace => false,
}
# 写入前校验语法(避免下发坏的 sudoers 把系统弄坏)
file { '/etc/sudoers':
ensure => file,
owner => 'root',
group => 'root',
mode => '0440',
source => 'puppet:///modules/sudo/etc/sudoers',
validate_cmd => '/usr/sbin/visudo -c -f %',
}
5.2 package 资源
常用属性:
| 属性 | 说明 |
|---|---|
name |
包名(不写则用 title) |
ensure |
installed / present(已装即可)、latest(每次运行都升级到最新)、absent(卸载)、purged(卸载并删配置)、具体版本号如 '2.4.6-45.el7'、held(锁定版本) |
provider |
包管理器:yum、rpm、apt、dpkg、gem、pip、npm 等 |
source |
本地或远程包文件路径(provider => rpm 时必填) |
install_options |
传给包管理器的额外参数,如 ['--enablerepo=epel'] |
responsefile |
应答文件(Debian 系) |
推送 yum 配置并安装 vsftpd:
node /^agent\d+\.cluster\.com/ {
file { "/etc/yum.repos.d/rhel-source.repo":
content => "[server]\nname=server\nbaseurl=file:///yum\nenabled=1\ngpgcheck=0\n",
mode => 644,
owner => root,
group => root,
}
package { 'install vsftpd':
provider => yum,
name => vsftpd,
ensure => installed,
}
}
注意上例中资源 title 是
'install vsftpd'(一个描述性名字),真正的包名由name => vsftpd指定。若省略name,则 title 必须就是包名。
更多用法:
# 最简写法:title 即包名
package { 'httpd':
ensure => installed,
}
# 一次装多个包(数组作为 title)
package { ['httpd', 'httpd-devel', 'mod_ssl', 'php']:
ensure => installed,
}
# 每次运行都保证是最新版(慎用,可能造成意外升级)
package { 'nginx':
ensure => latest,
}
# 锁定精确版本(生产推荐)
package { 'httpd':
ensure => '2.4.6-45.el7.centos',
}
# 卸载
package { 'telnet':
ensure => absent,
}
# 从 EPEL 仓库安装
package { 'htop':
ensure => installed,
install_options => ['--enablerepo=epel'],
}
# 用 rpm provider 装本地包
package { 'custom-agent':
ensure => installed,
provider => rpm,
source => '/tmp/custom-agent-1.0-1.el7.x86_64.rpm',
}
# 装 gem
package { 'bundler':
ensure => installed,
provider => gem,
}
5.3 service 资源
常用属性:
| 属性 | 说明 |
|---|---|
name |
服务名(不写则用 title) |
ensure |
running / true(确保运行)、stopped / false(确保停止) |
enable |
true 开机自启(相当于 systemctl enable / chkconfig on)、false 禁止自启 |
hasrestart |
该服务的脚本是否支持 restart 参数。true 时 Puppet 用 service X restart;false 时用 stop + start |
hasstatus |
该服务脚本是否支持 status 参数。true(默认)时用 service X status 判断状态;false 时 Puppet 用 ps 查进程名 |
status |
自定义状态检查命令(脚本不标准时用) |
start / stop / restart |
自定义启动/停止/重启命令 |
pattern |
hasstatus => false 时用于 ps 匹配的进程名模式 |
provider |
systemd、redhat(SysV init)、init、upstart 等 |
path |
init 脚本搜索路径 |
控制客户端服务状态:
node /^agent\d+\.cluster\.com/ {
service { sshd:
ensure => "stopped", # 确保停止,启动了会被关闭
enable => false, # 相当于 systemctl disable sshd
}
service { httpd:
ensure => "running", # 确保运行,关闭了会被启动
enable => true, # 相当于 systemctl enable httpd
}
}
规范写法:
node /^agent\d+\.cluster\.com/ {
service { 'sshd':
ensure => 'stopped',
enable => false,
}
service { 'httpd':
ensure => 'running',
enable => true,
}
}
自定义启停命令与非标准脚本的处理:
# 服务脚本不支持 restart,让 Puppet 用 stop+start
service { 'myapp':
ensure => running,
enable => true,
hasrestart => false,
hasstatus => false,
pattern => 'myapp_daemon', # 用 ps 匹配进程判断状态
}
# 完全自定义命令
service { 'legacy-app':
ensure => running,
start => '/opt/legacy/bin/start.sh',
stop => '/opt/legacy/bin/stop.sh',
restart => '/opt/legacy/bin/restart.sh',
status => '/opt/legacy/bin/status.sh',
}
# 强制指定 provider(CentOS 6 用 redhat,CentOS 7 用 systemd)
service { 'httpd':
ensure => running,
enable => true,
provider => $::operatingsystemmajrelease ? {
'6' => 'redhat',
default => 'systemd',
},
}
5.4 exec 资源
exec 是唯一命令式的资源类型,也是最容易破坏幂等性的资源,应尽量少用。
常用属性:
| 属性 | 说明 |
|---|---|
command |
要执行的命令(不写则用 title) |
path |
命令搜索路径。若命令未写绝对路径则必须设置 path,否则报 "Could not find command" |
cwd |
命令执行的工作目录 |
user / group |
以哪个用户/组身份执行 |
onlyif |
幂等守卫1:只有当该命令退出码为 0 时才执行 command |
unless |
幂等守卫2:只有当该命令退出码非 0 时才执行 command |
creates |
幂等守卫3:指定一个文件路径,该文件不存在时才执行 command(最常用、最直观) |
refreshonly |
幂等守卫4:true 时只有被其他资源 notify 时才执行(平时不执行) |
timeout |
超时秒数,默认 300 秒;0 表示不限制 |
tries / try_sleep |
失败重试次数与间隔 |
returns |
认为是"成功"的退出码,默认 0,可写数组 |
logoutput |
是否记录命令输出:true/false/on_failure(默认) |
environment |
设置环境变量,如 ['HOME=/root', 'LANG=C'] |
provider |
posix(默认)或 shell(支持管道、重定向等 shell 语法) |
创建脚本并用 exec 执行:
node /^agent\d+\.cluster\.com/ {
file { "/shell/":
ensure => directory,
}
file { "/shell/1.sh":
content => "mkdir -p /test\ntouch /test/{1..100}\n",
mode => 755,
owner => root,
group => root,
}
exec { "exec-shell":
cwd => "/shell",
command => "sh 1.sh",
user => "root",
path => "/bin:/sbin:/usr/local/bin:/usr/local/sbin:/usr/sbin:/usr/bin",
}
}
简化写法:
node /^agent\d+\.cluster\.com/ {
file { "/shell/":
ensure => directory,
}
file { "/shell/1.sh":
content => "mkdir -p /test\ntouch /test/{1..100}\n",
mode => "755",
}
exec { "exec-shell":
cwd => "/shell",
command => "sh 1.sh",
user => "root",
path => "/bin:/sbin:/usr/local/bin:/usr/local/sbin:/usr/sbin:/usr/bin",
}
}
上面这个 exec 是不幂等的:每次 Puppet 运行(默认每 30 分钟)都会重新执行
sh 1.sh。下面给出三种幂等守卫的正确写法。
三种幂等守卫的对比与用法:
# ① creates —— 最直观:产物文件存在就不再执行(适合解压、编译、初始化)
exec { 'extract-tomcat':
command => 'tar zxf /tmp/apache-tomcat-9.tar.gz -C /opt/',
path => '/bin:/usr/bin',
creates => '/opt/apache-tomcat-9', # 该目录已存在则跳过
}
# ② unless —— 检查命令返回非 0(即"条件不满足")时才执行
exec { 'add-iptables-rule':
command => 'iptables -I INPUT -p tcp --dport 8080 -j ACCEPT',
path => '/sbin:/usr/sbin:/bin',
unless => 'iptables -C INPUT -p tcp --dport 8080 -j ACCEPT', # 规则已存在则不加
}
exec { 'set-mysql-root-password':
command => "mysqladmin -u root password 'Secret123'",
path => '/bin:/usr/bin',
unless => "mysql -u root -pSecret123 -e 'select 1'", # 已能用新密码登录则跳过
}
# ③ onlyif —— 检查命令返回 0(即"条件满足")时才执行
exec { 'restart-if-degraded':
command => 'systemctl restart myapp',
path => '/bin:/usr/bin',
onlyif => '/usr/local/bin/check_degraded.sh', # 脚本返回 0 表示确实降级了
}
# ④ refreshonly —— 平时不跑,只在被 notify 时跑(配合配置文件变更)
exec { 'reload-sysctl':
command => 'sysctl -p /etc/sysctl.conf',
path => '/sbin:/usr/sbin',
refreshonly => true,
subscribe => File['/etc/sysctl.conf'],
}
# 需要管道/重定向时用 shell provider
exec { 'count-users':
command => 'wc -l /etc/passwd > /tmp/usercount',
provider => shell,
creates => '/tmp/usercount',
}
# 超时与重试
exec { 'long-build':
command => '/opt/build.sh',
timeout => 1800, # 30 分钟
tries => 3,
try_sleep => 60,
logoutput => true,
}
为什么 exec 应尽量少用?
- 破坏幂等性:稍不注意就每次运行都执行,反复追加配置行、反复重启服务;
- 不可预知:Puppet 不知道命令做了什么,
--noop空跑时无法准确预告变更; - 报告无意义:报告里只显示"执行了命令",不体现真正的系统状态变化;
- 跨平台差异:命令路径、参数在不同发行版上不同,失去了 Puppet 抽象层的价值;
- 调试困难:失败原因藏在命令输出里;
- 依赖顺序难表达:无法像原生资源那样自动推导依赖。
能用原生资源替代的场景(务必替代):
| 不要这样写 | 应该这样写 |
|---|---|
exec { 'useradd u1': } |
user { 'u1': ensure => present } |
exec { 'yum install -y httpd': } |
package { 'httpd': ensure => installed } |
exec { 'systemctl start httpd': } |
service { 'httpd': ensure => running } |
exec { 'sed -i ... /etc/xxx.conf': } |
file { '/etc/xxx.conf': content => template(...) } 或 file_line(stdlib 的 augeas/社区 file_line) |
exec { 'echo "1.1.1.1 h1" >> /etc/hosts': } |
host { 'h1': ip => '1.1.1.1' } |
exec { 'mount /dev/sdb1 /data': } |
mount { '/data': ensure => mounted, device => ... } |
exec { 'crontab -l ...': } |
cron { 'job1': ... } |
5.5 user 与 group 资源
user 常用属性:
| 属性 | 说明 |
|---|---|
name |
用户名(不写则用 title) |
ensure |
present / absent / role |
uid |
用户 UID |
gid |
主组(组名或 GID,该组必须已存在) |
groups |
附加组(数组) |
shell |
登录 shell,如 /bin/bash、/sbin/nologin |
home |
家目录路径 |
managehome |
true 时创建家目录(等价 useradd -m);删用户时同时删家目录 |
password |
加密后的密码串(不能写明文,需用 openssl passwd -1 或 grub-crypt 生成)。CentOS 6 上需装 ruby-shadow |
comment |
备注(GECOS 字段) |
expiry |
账号过期日期,YYYY-MM-DD 或 absent |
password_max_age / password_min_age |
密码最长/最短使用天数 |
system |
true 创建系统账号(UID < 1000) |
membership |
minimum(只保证包含指定组,默认)/ inclusive(精确等于指定组列表,多余的组会被移除) |
forcelocal |
强制使用本地 provider(有 LDAP 时需要) |
purge_ssh_keys |
true 时清理 authorized_keys 中不由 Puppet 管理的密钥 |
group 常用属性:
| 属性 | 说明 |
|---|---|
name |
组名 |
ensure |
present / absent |
gid |
组 GID |
members |
组成员(数组,部分 provider 支持) |
system |
系统组 |
auth_membership |
true 时精确控制成员列表 |
创建 user2(相当于在 agent 上 useradd user2):
node /^agent\d+\.cluster\.com/ {
user { "user2":
ensure => present,
managehome => true, # 创建家目录
}
}
删除 user2、新增 user3 和组 group1:
node /^agent\d+\.cluster\.com/ {
user { "user2":
ensure => absent,
managehome => true, # 删用户时连同家目录一起删除
}
user { "user3":
ensure => present,
uid => 505,
shell => "/sbin/nologin",
}
group { "group1":
ensure => present,
gid => 520,
}
}
指定 uid 时不要随意写
gid,若该 gid 的组不存在会出错。 正确做法是先声明group资源,并用require建立依赖。
完整、规范的用户管理示例:
group { 'developers':
ensure => present,
gid => 3000,
}
user { 'zhangsan':
ensure => present,
uid => 3001,
gid => 'developers',
groups => ['wheel', 'docker'],
shell => '/bin/bash',
home => '/home/zhangsan',
managehome => true,
comment => 'Zhang San - Dev Team',
# openssl passwd -1 'MyPassword' 生成
password => '$1$xyz$abcdefghijklmnopqrstuv',
password_max_age => 90,
password_min_age => 1,
require => Group['developers'], # 关键:先建组
}
# 批量创建用户(数组 title)
user { ['op1', 'op2', 'op3']:
ensure => present,
gid => 'developers',
shell => '/bin/bash',
managehome => true,
require => Group['developers'],
}
# 精确控制附加组(多余的组会被移除)
user { 'appuser':
ensure => present,
groups => ['appgroup'],
membership => 'inclusive',
}
# 下发 SSH 公钥(ssh_authorized_key 资源)
ssh_authorized_key { 'zhangsan@laptop':
ensure => present,
user => 'zhangsan',
type => 'ssh-rsa',
key => 'AAAAB3NzaC1yc2EAAAADAQABAAABAQ...',
require => User['zhangsan'],
}
5.6 cron 资源
常用属性:
| 属性 | 说明 |
|---|---|
name |
任务标识(会作为注释写入 crontab) |
ensure |
present / absent |
command |
要执行的命令 |
user |
属于哪个用户的 crontab,默认 root |
minute |
分,0-59,可写数组或 '*/5' |
hour |
时,0-23 |
monthday |
日,1-31 |
month |
月,1-12 |
weekday |
周,0-7 或 'Mon' |
special |
reboot/hourly/daily/weekly/monthly/yearly |
environment |
crontab 中的环境变量,如 ['PATH=/bin:/usr/bin', 'MAILTO=""'] |
target |
目标 crontab 文件 |
在客户端创建定时任务(用对应用户 crontab -l 验证):
node /^agent\d+\.cluster\.com/ {
cron { "cron1": # 注释名称
command => "/sbin/init 0",
user => "root",
minute => "01",
hour => "21", # 21:01 执行;未写字段默认为 *
}
}
更多写法:
# 每 5 分钟同步一次时间
cron { 'ntp-sync':
ensure => present,
command => '/usr/sbin/ntpdate ntp1.aliyun.com > /dev/null 2>&1',
user => 'root',
minute => '*/5',
environment => ['PATH=/sbin:/bin:/usr/sbin:/usr/bin', 'MAILTO=""'],
}
# 每天凌晨 2:30 备份
cron { 'daily-backup':
ensure => present,
command => '/usr/local/bin/backup.sh',
user => 'root',
hour => 2,
minute => 30,
}
# 每周一、三、五的 3 点清理日志
cron { 'clean-logs':
ensure => present,
command => '/usr/local/bin/cleanlog.sh',
user => 'root',
hour => 3,
minute => 0,
weekday => [1, 3, 5],
}
# 开机时执行
cron { 'on-reboot':
ensure => present,
command => '/usr/local/bin/init_env.sh',
user => 'root',
special => 'reboot',
}
# 删除某个定时任务
cron { 'old-job':
ensure => absent,
}
5.7 host 资源
修改 /etc/hosts 中的记录(主机名不变,修改 IP 或别名):
node default {
host { "agent1.cluster.com":
ip => "10.1.1.4",
host_aliases => "agent2haha",
}
}
常用属性: name(主机名)、ensure(present/absent)、ip、host_aliases(别名,字符串或数组)、comment、target(默认 /etc/hosts)。
host { 'master.cluster.com':
ensure => present,
ip => '10.1.1.2',
host_aliases => ['master', 'puppet'],
comment => 'managed by puppet',
}
host { 'obsolete.example.com':
ensure => absent,
}
5.8 mount 资源
将块设备挂载到目录:
node default {
mount { "/mnt": # title 即挂载点
ensure => "mounted", # 写入 /etc/fstab 并立即挂载
device => "UUID=3f1de712-7864-47ea-b0b4-9fa5781b2e88",
}
}
ensure 支持的取值:
| 值 | 含义 |
|---|---|
defined / present |
只写入 /etc/fstab,不挂载 |
mounted |
写入 /etc/fstab 并立即挂载 |
unmounted |
保留 /etc/fstab 条目,但确保未挂载 |
absent |
卸载并从 /etc/fstab 中删除 |
常用属性: name(挂载点)、device、fstype、options、dump、pass、atboot、remounts。
# 挂载本地分区
mount { '/data':
ensure => mounted,
device => 'UUID=3f1de712-7864-47ea-b0b4-9fa5781b2e88',
fstype => 'xfs',
options => 'defaults,noatime',
dump => '0',
pass => '2',
atboot => true,
require => File['/data'],
}
file { '/data':
ensure => directory,
}
# 挂载 NFS
mount { '/nfsdata':
ensure => mounted,
device => '10.1.1.1:/export/data',
fstype => 'nfs',
options => 'defaults,_netdev,vers=3',
}
# 卸载并移除条目
mount { '/olddata':
ensure => absent,
}
5.9 yumrepo 资源
用原生资源管理 yum 源,比 file 下发 .repo 文件更规范(幂等且能逐项修改)。
常用属性: name(repo id)、ensure、descr(对应 name=)、baseurl、mirrorlist、enabled、gpgcheck、gpgkey、priority、exclude、includepkgs、proxy、cost、sslverify、target。
# Puppet 官方源
yumrepo { 'puppet':
ensure => present,
descr => 'Puppet Repository',
baseurl => 'http://yum.puppetlabs.com/el/7Server/products/x86_64/',
enabled => 1,
gpgcheck => 0,
}
yumrepo { 'puppet-dependencies':
ensure => present,
descr => 'Puppet Dependencies',
baseurl => 'http://yum.puppetlabs.com/el/7Server/dependencies/x86_64/',
enabled => 1,
gpgcheck => 0,
}
# 本地教学源
yumrepo { 'server':
ensure => present,
descr => 'local server repo',
baseurl => 'file:///yum',
enabled => 1,
gpgcheck => 0,
}
# EPEL(带 GPG 校验)
yumrepo { 'epel':
ensure => present,
descr => 'Extra Packages for Enterprise Linux 7 - $basearch',
baseurl => 'http://mirrors.aliyun.com/epel/7/$basearch',
enabled => 1,
gpgcheck => 1,
gpgkey => 'http://mirrors.aliyun.com/epel/RPM-GPG-KEY-EPEL-7',
priority => 10,
}
# 禁用某个源
yumrepo { 'extras':
enabled => 0,
}
# 删除某个源
yumrepo { 'obsolete-repo':
ensure => absent,
}
5.10 其他常用资源类型速览
# augeas —— 精确编辑结构化配置文件(sshd_config、fstab、sysctl 等)
augeas { 'sshd-no-root-login':
context => '/files/etc/ssh/sshd_config',
changes => [
'set PermitRootLogin no',
'set MaxAuthTries 3',
],
notify => Service['sshd'],
}
# selboolean —— SELinux 布尔值
selboolean { 'httpd_can_network_connect':
value => on,
persistent => true,
}
# notify —— 打印一条消息(调试用)
notify { 'debug-osfamily':
message => "This node osfamily is ${::osfamily}, release ${::operatingsystemmajrelease}",
}
# tidy —— 清理旧文件(按年龄/大小)
tidy { '/var/log/myapp':
age => '4w',
recurse => true,
matches => ['*.log'],
}
# filebucket —— 备份被覆盖的文件到服务端
filebucket { 'main':
server => 'master.cluster.com',
path => false,
}
# schedule —— 时间窗口
schedule { 'maintenance-window':
range => '2 - 4',
period => daily,
repeat => 1,
}
六、资源依赖与执行顺序
6.1 关键点:Puppet 默认不保证执行顺序
这是初学者最容易犯的错误。 manifest 中资源的书写顺序 不等于 执行顺序。Puppet 3.x 默认按内部哈希顺序(近似随机)应用资源;即使 4.x 默认改为 ordering = manifest(按 manifest 书写顺序),跨 class、跨模块之间的顺序依然不保证,并且依赖显式声明才可靠。
因此下面这段代码是错误的(可能先起服务再装包,导致失败):
# 错误示范:没有声明依赖
package { 'httpd': ensure => installed }
file { '/etc/httpd/conf/httpd.conf': source => 'puppet:///modules/httpd/httpd.conf' }
service { 'httpd': ensure => running }
正确做法:显式声明依赖关系,形成 包 → 配置 → 服务(Package → File → Service) 这一经典模式。
6.2 四种元参数的对应关系
| 元参数 | 写在哪个资源上 | 含义 | 等价箭头写法 |
|---|---|---|---|
require => X |
后执行的资源 | 我依赖 X,X 先执行 | X -> Me |
before => X |
先执行的资源 | 我必须在 X 之前执行 | Me -> X |
notify => X |
先执行的资源 | 我变了就通知 X 刷新 | Me ~> X |
subscribe => X |
后执行的资源 | 我订阅 X,X 变了我刷新 | X ~> Me |
互为反向的两对:require ↔ before,notify ↔ subscribe。表达同一关系用哪个都行,取决于你想把关系写在哪个资源上(写在"当事人"上更内聚)。
# 写法一:用 require(关系写在后执行的资源上)
package { 'httpd':
ensure => installed,
}
file { '/etc/httpd/conf/httpd.conf':
ensure => file,
owner => 'root',
group => 'root',
mode => '0644',
source => 'puppet:///modules/httpd/httpd.conf',
require => Package['httpd'], # 先装包
notify => Service['httpd'], # 配置变了就重启服务
}
service { 'httpd':
ensure => running,
enable => true,
require => File['/etc/httpd/conf/httpd.conf'],
}
# 写法二:用 before / subscribe(等价效果)
package { 'httpd':
ensure => installed,
before => File['/etc/httpd/conf/httpd.conf'],
}
file { '/etc/httpd/conf/httpd.conf':
ensure => file,
source => 'puppet:///modules/httpd/httpd.conf',
}
service { 'httpd':
ensure => running,
enable => true,
subscribe => File['/etc/httpd/conf/httpd.conf'], # 订阅配置文件变化
}
6.3 箭头符号(链式关系)
| 符号 | 名称 | 含义 |
|---|---|---|
-> |
顺序箭头(ordering arrow) | 左边先执行,右边后执行 |
~> |
通知箭头(notification arrow) | 左边先执行,且左边有变更时通知右边刷新 |
<- |
反向顺序箭头 | 右边先执行(等价于把 -> 反过来写,不推荐) |
<~ |
反向通知箭头 | 右边先执行并通知左边(不推荐) |
# 最简洁的链式声明:装包 -> 配置 ~> 重启服务
Package['httpd'] -> File['/etc/httpd/conf/httpd.conf'] ~> Service['httpd']
# 也可以对整个类做链式
Class['yumrepo_setup'] -> Class['httpd'] -> Class['monitoring']
# 也可以直接把资源声明写进链
package { 'nginx': ensure => installed }
-> file { '/etc/nginx/nginx.conf': content => template('nginx/nginx.conf.erb') }
~> service { 'nginx': ensure => running, enable => true }
# 反向箭头(可读性差,官方不推荐)
Service['httpd'] <- File['/etc/httpd/conf/httpd.conf'] <- Package['httpd']
6.4 notify / subscribe 与 refresh 机制
notify / subscribe 除了表达"顺序",还额外表达"刷新(refresh)":当被通知方所监听的资源实际发生了变更(而不是每次运行)时,被通知方会执行它的 refresh 动作。
不同资源类型的 refresh 动作:
| 资源类型 | refresh 动作 |
|---|---|
service |
重启服务(hasrestart => true 用 restart,否则 stop+start) |
exec |
执行 command(配合 refreshonly => true 时只在被通知时执行) |
mount |
重新挂载(remounts => true 用 remount) |
经典模式:改配置文件自动重启服务
class nginx {
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,
}
}
运行效果:
- 第 1 次运行:装包 → 下发配置 → 启动服务(并因为配置变更触发一次 restart);
- 之后每次运行(配置未改):什么都不做,输出中没有任何变更;
- 你在 Master 上改了模板 → Agent 下次运行时检测到配置内容不同 → 更新文件 → 自动 restart nginx。
多个资源通知同一个服务(数组形式):
file { '/etc/nginx/nginx.conf':
content => template('nginx/nginx.conf.erb'),
notify => Service['nginx'],
}
file { '/etc/nginx/conf.d/vhost.conf':
content => template('nginx/vhost.conf.erb'),
notify => Service['nginx'],
}
service { 'nginx':
ensure => running,
enable => true,
subscribe => [ File['/etc/nginx/nginx.conf'], File['/etc/nginx/conf.d/vhost.conf'] ],
}
即使多个资源同时变更,Puppet 在一次运行中只会 refresh 一次(不会重启 N 次)。
用 reload 代替 restart(避免中断连接):
# 定义一个 refreshonly 的 exec 来做 reload
exec { 'nginx-reload':
command => '/usr/sbin/nginx -t && /bin/systemctl reload nginx',
provider => shell,
refreshonly => true,
subscribe => File['/etc/nginx/nginx.conf'],
require => Service['nginx'],
}
6.5 依赖环(dependency cycle)
如果依赖关系形成闭环,Puppet 无法确定顺序,直接编译失败:
# 错误:A 依赖 B,B 又依赖 A
file { '/tmp/a':
content => "a\n",
require => File['/tmp/b'],
}
file { '/tmp/b':
content => "b\n",
require => File['/tmp/a'],
}
报错信息:
Error: Could not apply complete catalog: Found 1 dependency cycle:
(File[/tmp/a] => File[/tmp/b] => File[/tmp/a])
Try the '--graph' option and opening the resulting '.dot' file in OmniGraffle or GraphViz
定位方法:
# 生成依赖关系图
puppet agent --test --graph
ls /var/lib/puppet/state/graphs/
# cycles.dot expanded_relationships.dot relationships.dot resources.dot
# 转成图片查看(需安装 graphviz)
yum install -y graphviz
dot -Tpng /var/lib/puppet/state/graphs/cycles.dot -o /tmp/cycles.png
# 4.x+ 路径
ls /opt/puppetlabs/puppet/cache/state/graphs/
常见环的来源:
- 同时写了
require和before指向对方; - Class 之间用
Class['a'] -> Class['b']而 b 内部又require => Class['a']之外还反向依赖; - 用
anchor模式不当(老版本contain/anchor问题)。
七、变量、数据类型与条件判断
7.1 变量定义与作用域
# 变量以 $ 开头,赋值用 =,一旦赋值不可修改(immutable)
$webroot = '/var/www/html'
$http_port = 80
$enabled = true
$packages = ['httpd', 'httpd-devel', 'mod_ssl']
$settings = { 'port' => 80, 'root' => '/web' }
# 引用变量
file { "${webroot}/index.html":
content => "port is ${http_port}\n",
}
作用域(scope):
| 作用域 | 说明 |
|---|---|
| 顶层作用域(top scope) | 在 site.pp 中直接定义的变量、以及所有 facts。用 $::varname 显式引用 |
| 节点作用域(node scope) | node {} 块内定义的变量 |
| 类作用域(class scope) | class {} 内定义的变量,外部用 $classname::varname 引用 |
# site.pp(顶层)
$puppetserver = 'master.cluster.com'
$company = 'MyCorp'
class apache {
$docroot = '/var/www/html' # 类作用域
notify { 'show':
# $::company 显式引用顶层作用域,避免被同名局部变量遮蔽
message => "company=${::company}, server=${::puppetserver}, docroot=${docroot}",
}
}
class other {
notify { 'cross-class':
# 跨类引用:$类名::变量名
message => "apache docroot is ${apache::docroot}",
}
}
强烈建议:引用 fact 和顶层变量时一律加
$::前缀(如$::osfamily、$::fqdn),可以避免被局部同名变量意外遮蔽,这是 Puppet 代码规范的重要一条。Puppet 4.x+ 推荐用结构化 facts:
$facts['os']['family']、$facts['networking']['ip'],并开启strict_variables = true让引用未定义变量直接报错。
7.2 字符串插值与引号区别
| 引号 | 是否插值 | 是否解释转义符 | 用途 |
|---|---|---|---|
'单引号' |
否 | 只解释 \\ 和 \' |
普通字面量、资源 title(推荐默认使用) |
"双引号" |
是(${} 或 $var) |
解释 \n、\t 等 |
需要变量插值或换行符时 |
$name = 'world'
notify { 'a': message => 'hello $name' } # 输出:hello $name(不插值)
notify { 'b': message => "hello $name" } # 输出:hello world
notify { 'c': message => "hello ${name}" } # 输出:hello world(推荐写法,边界清晰)
notify { 'd': message => "line1\nline2" } # 输出两行
# 复杂表达式必须用 ${}
$ip_last = '5'
file { "/web/index.html":
content => "web${ip_last}\n",
}
# 引用数组/哈希元素、方法调用
$arr = ['a', 'b', 'c']
$h = { 'k1' => 'v1' }
notify { 'e': message => "first=${arr[0]}, k1=${h['k1']}, upper=${name.upcase}" }
规范建议:没有变量插值需求就用单引号;
${}优先于裸$var。
7.3 数据类型:数组与哈希
# 数组
$packages = ['httpd', 'httpd-devel', 'mod_ssl']
$ports = [80, 443, 8080]
# 数组可直接作为资源 title,一次声明多个资源
package { $packages:
ensure => installed,
}
# 访问元素(下标从 0 开始,支持负数)
notify { 'arr': message => "first=${packages[0]}, last=${packages[-1]}" }
# 哈希(键值对)
$vhosts = {
'www.a.com' => { 'port' => 80, 'root' => '/web/a' },
'www.b.com' => { 'port' => 443, 'root' => '/web/b' },
}
notify { 'hash': message => "a.com root = ${vhosts['www.a.com']['root']}" }
# 常用内置函数(部分需 stdlib 模块)
notify { 'funcs':
message => "size=${size($packages)} join=${join($packages, ',')} upcase=${upcase('abc')}",
}
# 4.x 迭代(each / map / filter / reduce)
$vhosts.each |$name, $cfg| {
file { "/etc/httpd/conf.d/${name}.conf":
ensure => file,
content => "ServerName ${name}\nDocumentRoot ${cfg['root']}\nListen ${cfg['port']}\n",
}
}
$enabled_pkgs = $packages.filter |$p| { $p =~ /^httpd/ }
$upper_pkgs = $packages.map |$p| { upcase($p) }
# 4.x 类型系统(参数化类中做校验)
# String, Integer, Boolean, Array[String], Hash[String, Any],
# Enum['present','absent'], Optional[String], Variant[String, Integer], Pattern[/^\d+$/]
7.4 Facts 变量与 facter
Facts 是 Facter 在 Agent 端采集的系统信息,编译 catalog 时作为顶层变量可直接使用。
查看所有 fact:
# 列出全部 fact(3.x 是扁平的,4.x+ 是结构化 JSON/YAML)
facter
# 查看单个 fact
facter osfamily
facter operatingsystem
facter operatingsystemrelease
facter operatingsystemmajrelease
facter ipaddress
facter fqdn
facter memorysize
facter processorcount
# 结构化输出
facter -p # 包含 puppet 自定义 fact(pluginsync 同步来的)
facter -j # JSON 格式
facter -y # YAML 格式
facter os # 4.x:整个 os 结构体
facter os.family
facter networking.ip
# 通过 puppet 命令查看(含自定义 fact)
puppet facts
puppet facts find $(hostname -f)
常用 fact 一览:
| Fact 名 | 示例值 | 说明 |
|---|---|---|
$::osfamily |
RedHat / Debian / Suse |
操作系统家族(做分支的首选) |
$::operatingsystem |
CentOS / RedHat / Fedora / Ubuntu |
具体发行版 |
$::operatingsystemrelease |
7.3.1611 |
完整版本号 |
$::operatingsystemmajrelease |
7 |
主版本号(区分 CentOS 6/7 的关键) |
$::hostname |
agent1 |
短主机名 |
$::fqdn |
agent1.cluster.com |
完整域名 |
$::domain |
cluster.com |
域名 |
$::ipaddress |
10.1.1.3 |
主 IP |
$::ipaddress_eth0 |
10.1.1.3 |
指定网卡 IP |
$::netmask / $::network_eth0 |
掩码/网段 | |
$::macaddress |
MAC 地址 | |
$::architecture |
x86_64 |
架构 |
$::kernel |
Linux |
内核类型 |
$::kernelrelease |
3.10.0-514.el7.x86_64 |
内核版本 |
$::processorcount |
4 |
CPU 核数 |
$::memorysize / $::memorytotal |
3.86 GB |
内存 |
$::memorysize_mb |
3952.00 |
内存(MB,数值可比较) |
$::virtual |
vmware / kvm / physical / docker |
虚拟化类型 |
$::is_virtual |
true / false |
是否虚拟机 |
$::selinux |
true / false |
SELinux 是否启用 |
$::uptime_days |
12 |
运行天数 |
$::timezone |
CST |
时区 |
$::path |
Agent 的 PATH | |
$::puppetversion |
3.6.2 |
Puppet 版本 |
$::clientcert |
agent1.cluster.com |
Agent 的证书名(= node 名) |
$::environment |
production |
当前环境 |
$::blockdevices / $::partitions |
块设备/分区信息 |
内置变量(非 fact,由 Puppet 提供):
| 变量 | 说明 |
|---|---|
$::clientcert |
Agent 证书名 |
$::clientversion |
Agent 的 Puppet 版本 |
$::environment |
当前环境名 |
$::servername / $::serverip / $::serverversion |
Master 的主机名/IP/版本 |
$module_name |
当前所在模块名 |
$caller_module_name |
调用方模块名 |
$title / $name |
在 define 定义类型中,代表实例的标题 |
自定义 fact:
方式一:模块中的 Ruby 自定义 fact(放在 模块/lib/facter/xxx.rb,靠 pluginsync 同步到 Agent):
# /etc/puppet/modules/myapp/lib/facter/app_version.rb
Facter.add(:app_version) do
setcode do
if File.exist?('/opt/myapp/VERSION')
File.read('/opt/myapp/VERSION').strip
else
'none'
end
end
end
# 带条件限制的 fact
Facter.add(:server_role) do
confine :osfamily => 'RedHat'
setcode do
Facter::Core::Execution.exec('cat /etc/server_role 2>/dev/null') || 'unknown'
end
end
# manifest 中直接使用
notify { 'ver': message => "app version = ${::app_version}" }
方式二:外部 fact(更简单,无需写 Ruby),放在 Agent 的 /etc/facter/facts.d/ 或 /opt/puppetlabs/facter/facts.d/:
# /etc/facter/facts.d/custom.txt
server_role=web
datacenter=bj-idc1
app_env=production
# /etc/facter/facts.d/custom.yaml
---
server_role: web
datacenter: bj-idc1
tags:
- web
- frontend
#!/bin/bash
# /etc/facter/facts.d/disk_info.sh (必须可执行,输出 key=value)
echo "root_disk_free=$(df -h / | tail -1 | awk '{print $4}')"
方式三:环境变量(临时):
FACTER_server_role=web puppet agent --test
7.5 条件判断
7.5.1 if / elsif / else
if $::osfamily == 'RedHat' {
$apache_pkg = 'httpd'
$apache_svc = 'httpd'
$apache_dir = '/etc/httpd/conf'
} elsif $::osfamily == 'Debian' {
$apache_pkg = 'apache2'
$apache_svc = 'apache2'
$apache_dir = '/etc/apache2'
} else {
fail("Unsupported osfamily: ${::osfamily}")
}
# 数值比较
if $::memorysize_mb > 4096 {
$worker_processes = 4
} else {
$worker_processes = 2
}
# in 操作符(判断是否在数组/字符串/哈希键中)
if $::operatingsystem in ['RedHat', 'CentOS', 'Fedora'] {
notify { 'redhat-like': message => 'This is a RedHat-like system' }
}
# 布尔与逻辑运算
if ($::osfamily == 'RedHat') and ($::operatingsystemmajrelease == '7') {
notify { 'el7': message => 'CentOS/RHEL 7' }
}
if !$::is_virtual {
notify { 'phys': message => 'Physical machine' }
}
7.5.2 unless(相当于 if not)
unless $::osfamily == 'RedHat' {
fail("This module only supports RedHat family, got ${::osfamily}")
}
unless defined(Package['httpd']) {
package { 'httpd': ensure => installed }
}
7.5.3 case
case $::osfamily {
'RedHat': {
$pkg = 'httpd'
$conf = '/etc/httpd/conf/httpd.conf'
$confdir = '/etc/httpd/conf.d'
}
'Debian': {
$pkg = 'apache2'
$conf = '/etc/apache2/apache2.conf'
$confdir = '/etc/apache2/conf-enabled'
}
/^(Suse|SLES)$/: { # case 支持正则
$pkg = 'apache2'
$conf = '/etc/apache2/httpd.conf'
$confdir = '/etc/apache2/conf.d'
}
default: {
fail("Unsupported osfamily: ${::osfamily}")
}
}
# 多值匹配(逗号分隔)
case $::operatingsystem {
'RedHat', 'CentOS', 'OracleLinux', 'Scientific': {
$repo_type = 'yum'
}
'Ubuntu', 'Debian': {
$repo_type = 'apt'
}
default: { $repo_type = 'unknown' }
}
7.5.4 selector 选择器(? {})
selector 是一个表达式,用来给变量赋值或直接作为属性值,不能包含资源声明。
# 给变量赋值
$apache_pkg = $::osfamily ? {
'RedHat' => 'httpd',
'Debian' => 'apache2',
default => 'httpd',
}
# 直接作为属性值
service { 'apache':
name => $::osfamily ? {
'RedHat' => 'httpd',
'Debian' => 'apache2',
},
ensure => running,
enable => true,
}
# 正则匹配
$env_short = $::fqdn ? {
/^prod/ => 'production',
/^test/ => 'testing',
/^dev/ => 'development',
default => 'unknown',
}
7.5.5 比较与正则匹配运算符
| 运算符 | 说明 |
|---|---|
== / != |
相等 / 不等(字符串比较不区分大小写) |
< > <= >= |
数值/版本比较 |
=~ |
匹配正则(成功返回 true,并把捕获组放入 $1、$2) |
!~ |
不匹配正则 |
in |
包含(数组元素 / 字符串子串 / 哈希键) |
and / or / ! |
逻辑与 / 或 / 非 |
+ - * / % |
算术运算 |
<< / >> |
位移 |
if $::fqdn =~ /^web(\d+)\.cluster\.com$/ {
$web_index = $1 # 捕获组
notify { 'idx': message => "This is web node number ${web_index}" }
}
if $::kernelrelease !~ /^3\.10/ {
notify { 'kernel-warn': message => 'Unexpected kernel version' }
}
if 'httpd' in $packages { notify { 'has-httpd': message => 'httpd in list' } }
7.5.6 按 osfamily / operatingsystemmajrelease 做版本分支(实用写法)
这是处理 CentOS 6 与 CentOS 7 差异的标准做法:
class baseline {
# 服务管理方式差异
case $::operatingsystemmajrelease {
'6': {
# CentOS 6:SysV init + iptables + ntpd
$service_provider = 'redhat'
$firewall_pkg = 'iptables'
$firewall_svc = 'iptables'
$time_pkg = 'ntp'
$time_svc = 'ntpd'
$network_svc = 'network'
$syslog_conf = '/etc/rsyslog.conf'
}
'7', '8': {
# CentOS 7/8:systemd + firewalld + chrony
$service_provider = 'systemd'
$firewall_pkg = 'firewalld'
$firewall_svc = 'firewalld'
$time_pkg = 'chrony'
$time_svc = 'chronyd'
$network_svc = 'network'
$syslog_conf = '/etc/rsyslog.conf'
}
default: {
fail("Unsupported EL release: ${::operatingsystemmajrelease}")
}
}
package { $time_pkg:
ensure => installed,
}
service { $time_svc:
ensure => running,
enable => true,
provider => $service_provider,
require => Package[$time_pkg],
}
}
八、Class 类与模块
8.1 class 的定义与声明
定义 class(只是定义,不会执行):
class sudo {
package { 'sudo':
ensure => present,
}
file { '/etc/sudoers':
ensure => file,
mode => '0440',
owner => 'root',
group => 'root',
source => 'puppet:///modules/sudo/etc/sudoers',
require => Package['sudo'],
}
}
声明(调用)class 的两种方式:
# 方式一:include(幂等,可重复 include 同一个类,只生效一次)
node 'agent1.cluster.com' {
include sudo
include httpd
include ntp
}
# include 也可一次包含多个
include sudo, httpd, ntp
include ['sudo', 'httpd', 'ntp']
# 方式二:resource-like 声明(可传参数,但同一个类只能声明一次,重复会报 Duplicate declaration)
node 'agent1.cluster.com' {
class { 'httpd':
port => 8080,
docroot => '/web',
}
}
# require 声明(include + 建立依赖)
node 'agent1.cluster.com' {
require yumrepo_setup # 保证 yumrepo_setup 先执行
include httpd
}
# contain(把类包含进当前类的容器,保证依赖关系正确传递)
class webstack {
contain nginx
contain php
Class['php'] -> Class['nginx']
}
| 声明方式 | 能否传参 | 能否重复 | 说明 |
|---|---|---|---|
include x |
否(参数从 Hiera/默认值取) | 能(幂等) | 推荐,尤其配合 Hiera 自动参数查找 |
class { 'x': p => v } |
能 | 不能 | 只能声明一次,多处引用易冲突 |
require x |
否 | 能 | include + 依赖 |
contain x |
否 | 能 | 保证类的资源被"包含"在父类的依赖边界内 |
8.2 参数化类与默认值
# 定义带参数的类(参数有默认值,可被覆盖)
class httpd (
$package_name = 'httpd',
$service_name = 'httpd',
$port = 80,
$docroot = '/var/www/html',
$server_admin = 'root@localhost',
$ensure = 'running',
$enable = true,
) {
package { $package_name:
ensure => installed,
}
file { '/etc/httpd/conf/httpd.conf':
ensure => file,
owner => 'root',
group => 'root',
mode => '0644',
content => template('httpd/httpd.conf.erb'), # 模板中可用上面的参数
require => Package[$package_name],
notify => Service[$service_name],
}
file { $docroot:
ensure => directory,
owner => 'apache',
group => 'apache',
}
service { $service_name:
ensure => $ensure,
enable => $enable,
}
}
Puppet 4.x+ 带数据类型校验的参数化类(推荐,能提前发现错误):
class httpd (
String $package_name = 'httpd',
String $service_name = 'httpd',
Integer[1, 65535] $port = 80,
Stdlib::Absolutepath $docroot = '/var/www/html',
Enum['running','stopped'] $ensure = 'running',
Boolean $enable = true,
Optional[String] $server_admin = undef,
Array[String] $extra_pkgs = [],
Hash $vhosts = {},
) {
# ...
}
调用并传参:
# 用 resource-like 声明传参
class { 'httpd':
port => 8080,
docroot => '/web',
}
# 或者用 include + Hiera 自动参数查找(推荐)
# 在 Hiera 数据文件中写:
# httpd::port: 8080
# httpd::docroot: /web
include httpd
# 也可用 class 默认参数(resource default)
Class['httpd'] { port => 8080 } # 不推荐
8.3 类继承(inherits)与为什么不推荐
class base {
package { 'wget': ensure => installed }
file { '/etc/motd':
ensure => file,
content => "Base MOTD\n",
}
}
# 子类继承父类,可用 +> 追加、用 资源引用 {} 覆盖父类属性
class base::web inherits base {
# 覆盖父类的 File['/etc/motd'] 的 content
File['/etc/motd'] {
content => "Web Server MOTD\n",
}
package { 'httpd': ensure => installed }
}
为什么不推荐使用 inherits:
- 只有继承能覆盖属性这个特性容易被滥用,导致资源的最终属性值散落在多个类中,难以追踪;
- 继承链一长,谁覆盖了谁完全看不清,出问题极难排查;
- 继承会自动 include 父类,隐式引入大量资源,容易和别处的声明冲突(Duplicate declaration);
- Puppet 官方风格指南明确建议:用参数化类 + Hiera 数据分离来代替继承;
- 唯一被官方认可的用途是"
xxx::params模式"(把平台相关的变量集中在一个 params 类里,然后inherits xxx::params),但在 Puppet 4.x+ 中这也已被 Hiera 数据 in module 取代。
替代方案(推荐):
# 推荐:参数化 + 组合,不用继承
class motd (
String $content = "Default MOTD\n",
) {
file { '/etc/motd':
ensure => file,
content => $content,
}
}
class profile::webserver {
class { 'motd':
content => "Web Server MOTD\n",
}
include httpd
}
8.4 模块的标准目录结构
模块是 Puppet 组织可复用配置的方式。一个模块典型目录结构如下:
/etc/puppet/modules/<module_name>/ # Puppet 3.x
/etc/puppetlabs/code/environments/production/modules/<module_name>/ # Puppet 4.x+
├── manifests/ # 【必需】所有 .pp 清单文件
│ ├── init.pp # 模块入口,定义与模块同名的 class
│ ├── install.pp # 子类 <module>::install
│ ├── config.pp # 子类 <module>::config
│ ├── service.pp # 子类 <module>::service
│ ├── params.pp # 子类 <module>::params(平台变量)
│ └── vhost.pp # 定义类型 <module>::vhost
├── files/ # 静态文件,通过 puppet:///modules/<module>/<file> 分发
│ └── etc/
│ └── sudoers
├── templates/ # ERB / EPP 模板,通过 template('<module>/x.erb') 引用
│ └── httpd.conf.erb
├── lib/ # Ruby 扩展(靠 pluginsync 同步到 Agent)
│ ├── facter/ # 自定义 fact
│ │ └── app_version.rb
│ └── puppet/
│ ├── parser/functions/ # 自定义函数(3.x API)
│ ├── functions/ # 自定义函数(4.x API)
│ ├── type/ # 自定义资源类型
│ └── provider/ # 自定义 provider
├── examples/ # 使用示例,可用 puppet apply 直接测试
│ └── init.pp
├── tests/ # 旧版的示例目录(等同 examples)
├── spec/ # rspec-puppet 单元测试
├── facts.d/ # 外部 fact(静态)
├── data/ # 模块内 Hiera 数据(Hiera 5)
│ └── common.yaml
├── hiera.yaml # 模块内 Hiera 层级配置(Hiera 5)
├── metadata.json # 【重要】模块元数据:名称、版本、依赖、支持的 OS
├── README.md # 文档
├── CHANGELOG.md
└── LICENSE
简化版(教学环境最常用的三个目录):
/etc/puppet/modules/sudo/
├── files/
├── templates/
└── manifests/
└── init.pp
各目录/文件的作用:
| 路径 | 作用 |
|---|---|
manifests/ |
存放 .pp 清单,模块的逻辑代码都在这里 |
manifests/init.pp |
模块入口,必须定义与模块同名的 class |
files/ |
静态文件仓库,用 source => 'puppet:///modules/<模块名>/<相对路径>' 分发 |
templates/ |
ERB/EPP 模板,用 template('<模块名>/文件名.erb') 或 epp() 渲染 |
lib/ |
Ruby 代码:自定义 fact、函数、资源类型、provider;Agent 端靠 pluginsync = true 自动同步 |
examples/ |
示例 manifest,用于 puppet apply --noop examples/init.pp 快速测试 |
metadata.json |
模块元数据,puppet module 命令与 Forge 依赖此文件 |
data/ + hiera.yaml |
模块内置默认数据(Hiera 5 的 "data in modules"),替代旧的 params.pp |
spec/ |
rspec-puppet 单元测试 |
metadata.json 示例:
{
"name": "mycorp-httpd",
"version": "1.2.0",
"author": "MyCorp Ops",
"summary": "Manage Apache httpd",
"license": "Apache-2.0",
"source": "https://git.mycorp.com/puppet/httpd",
"project_page": "https://git.mycorp.com/puppet/httpd",
"issues_url": "https://git.mycorp.com/puppet/httpd/issues",
"dependencies": [
{ "name": "puppetlabs/stdlib", "version_requirement": ">= 4.13.1 < 9.0.0" },
{ "name": "puppetlabs/concat", "version_requirement": ">= 2.0.0 < 8.0.0" }
],
"operatingsystem_support": [
{ "operatingsystem": "CentOS", "operatingsystemrelease": ["6", "7", "8"] },
{ "operatingsystem": "RedHat", "operatingsystemrelease": ["6", "7", "8"] }
],
"requirements": [
{ "name": "puppet", "version_requirement": ">= 4.10.0 < 8.0.0" }
]
}
8.5 模块的自动加载规则与命名约定
Puppet 会根据类名自动推导它所在的文件路径,因此命名必须严格遵守约定,否则会报 Could not find class。
| 类名 | 对应文件路径(相对模块根) |
|---|---|
httpd |
httpd/manifests/init.pp |
httpd::install |
httpd/manifests/install.pp |
httpd::config |
httpd/manifests/config.pp |
httpd::params |
httpd/manifests/params.pp |
httpd::vhost::ssl |
httpd/manifests/vhost/ssl.pp |
httpd::vhost(定义类型) |
httpd/manifests/vhost.pp |
规则总结:
- 类名用
::分隔命名空间,第一段必须等于模块名; ::对应目录层级,最后一段对应文件名(去掉.pp);- 与模块同名的类放在
init.pp中; - 模块名和类名只能包含小写字母、数字、下划线,不能有横线(
my-module非法,应写my_module); - 一个
.pp文件中应只定义一个类(或一个定义类型); - 模块目录必须位于
modulepath之内,否则找不到。
# 查看当前生效的模块路径
puppet config print modulepath
# 列出已安装模块及其依赖状态
puppet module list
puppet module list --tree
推荐的模块内部分层写法(install / config / service 三段式):
# httpd/manifests/init.pp
class httpd (
$port = 80,
$docroot = '/var/www/html',
) {
# 用 contain 保证依赖边界正确
contain httpd::install
contain httpd::config
contain httpd::service
Class['httpd::install']
-> Class['httpd::config']
~> Class['httpd::service']
}
# httpd/manifests/install.pp
class httpd::install {
package { ['httpd', 'httpd-devel']:
ensure => installed,
}
}
# httpd/manifests/config.pp
class httpd::config {
file { '/etc/httpd/conf/httpd.conf':
ensure => file,
owner => 'root',
group => 'root',
mode => '0644',
content => template('httpd/httpd.conf.erb'),
}
}
# httpd/manifests/service.pp
class httpd::service {
service { 'httpd':
ensure => running,
enable => true,
}
}
8.6 定义类型 define 与 class 的区别
class 在一个节点上只能存在一份(单例,singleton);define 定义的类型可以在同一节点上实例化多次(像自定义资源类型一样使用)。
# 定义类型:httpd/manifests/vhost.pp → 类型名 httpd::vhost
define httpd::vhost (
$port = 80,
$docroot = "/var/www/${name}", # $name 就是实例的 title
$server_name = $title, # $title 同 $name
$ssl = false,
) {
file { $docroot:
ensure => directory,
owner => 'apache',
group => 'apache',
}
file { "/etc/httpd/conf.d/${name}.conf":
ensure => file,
content => template('httpd/vhost.conf.erb'),
notify => Service['httpd'],
}
file { "${docroot}/index.html":
ensure => file,
content => "<h1>${server_name}</h1>\n",
require => File[$docroot],
}
}
# 使用:可以实例化任意多次
node 'web1.cluster.com' {
include httpd
httpd::vhost { 'www.a.com':
port => 80,
docroot => '/web/a',
}
httpd::vhost { 'www.b.com':
port => 8080,
docroot => '/web/b',
}
httpd::vhost { 'www.c.com':
port => 443,
ssl => true,
}
}
对比表:
| 项目 | class | define(定义类型) |
|---|---|---|
| 关键字 | class |
define |
| 一个节点上可存在几份 | 1 份(单例) | 任意多份 |
| 声明方式 | include x / class { 'x': } |
x { 'title': }(像资源一样) |
| 内置变量 | 无 $title/$name |
有 $title 和 $name(= 实例标题) |
可否被 include |
可 | 不可 |
| 可否用 Hiera 自动参数查找 | 可 | 不可(需显式传参或在类中查) |
| 适用场景 | 一台机器上"唯一"的东西:安装某软件、全局配置、启动服务 | 一台机器上"多份"的东西:多个虚拟主机、多个用户、多个 Nginx server 块、多个日志轮转规则 |
何时用哪个(判断标准):
- 问自己:"这台机器上会有几个这样的东西?"
- 只会有一个(httpd 装一次、sshd 配一份)→ class;
- 会有多个(3 个 vhost、5 个数据库、10 个用户)→ define。
- 定义类型内部的资源标题必须包含
$name或$title,否则实例化第二次时资源标题重复,报Duplicate declaration。
# 错误:资源标题不含 $name,实例化两次就冲突
define bad_example ($content = 'x') {
file { '/tmp/fixed.txt': # ← 固定标题,第二个实例必定冲突
content => $content,
}
}
# 正确
define good_example ($content = 'x') {
file { "/tmp/${name}.txt": # ← 标题含 $name
content => $content,
}
}
good_example { 'a': content => "A\n" }
good_example { 'b': content => "B\n" }
8.7 模块复用与 Puppet Forge
Puppet Forge(https://forge.puppet.com)是官方模块仓库,大量常用软件已有成熟模块,不必自己重复写。
# 搜索模块
puppet module search apache
puppet module search ntp
# 安装模块(默认装到 modulepath 的第一个目录)
puppet module install puppetlabs-stdlib
puppet module install puppetlabs-apache
puppet module install puppetlabs-ntp --version 8.5.0
puppet module install puppetlabs-mysql
# 指定安装目录 / 指定环境
puppet module install puppetlabs-apache --modulepath /etc/puppet/modules
puppet module install puppetlabs-apache --environment production
# 离线安装(内网环境:先下载 tar.gz,再本地安装)
puppet module install /tmp/puppetlabs-stdlib-9.0.0.tar.gz --ignore-dependencies
# 列出、升级、卸载
puppet module list
puppet module list --tree
puppet module upgrade puppetlabs-apache
puppet module uninstall puppetlabs-apache
# 生成一个新模块的骨架(自动创建标准目录结构与 metadata.json)
puppet module generate mycorp-myapp
必装基础模块:puppetlabs-stdlib(Puppet 标准库),提供大量实用函数,几乎所有 Forge 模块都依赖它:
| 常用 stdlib 函数 | 作用 |
|---|---|
validate_string / validate_bool / validate_array / validate_hash |
参数类型校验(3.x 时代) |
validate_absolute_path |
校验是否绝对路径 |
str2bool / bool2str / str2saltedsha512 |
类型转换 |
join / split / flatten / unique / sort |
数组操作 |
merge / keys / values / has_key |
哈希操作 |
file_line |
修改文件中的单行(避免用 exec + sed) |
pick / pick_default |
取第一个非空值 |
dirname / basename |
路径处理 |
getvar |
动态取变量 |
ensure_packages / ensure_resource |
幂等地确保资源存在(避免 Duplicate declaration) |
# file_line:只改一行,不覆盖整个文件(非常实用)
include stdlib
file_line { 'sshd-disable-root':
path => '/etc/ssh/sshd_config',
line => 'PermitRootLogin no',
match => '^#?PermitRootLogin',
notify => Service['sshd'],
}
file_line { 'set-max-open-files':
path => '/etc/security/limits.conf',
line => '* - nofile 65535',
}
# ensure_resource:多个模块可能都想声明同一资源时,避免重复声明报错
ensure_resource('package', 'wget', { 'ensure' => 'installed' })
ensure_packages(['vim', 'wget', 'curl'], { 'ensure' => 'installed' })
8.8 site.pp 中的 node 定义与节点匹配规则
Puppet 通过 node 关键字为不同节点定义不同配置,支持精确节点名、default 以及正则表达式(*当前版本不支持通配符 ``**)。
节点名的来源:Agent 的 certname(默认是 FQDN),可用 facter fqdn 或 puppet config print certname 查看。
8.8.1 精确匹配
为不同节点写不同内容:
node 'agent1.cluster.com' { # 只针对 agent1,节点名用单引号
file { "/tmp/test.txt":
content => "hahahaha\n",
}
}
node 'agent2.cluster.com' { # 只针对 agent2
file { "/tmp/test.txt":
content => "hehehehe\n",
}
}
为不同节点删除文件:
node 'agent1.cluster.com' {
file { "/tmp/test.txt":
ensure => absent, # 保证文件不存在,存在则删除;删目录需加 force => yes
}
}
node 'agent2.cluster.com' {
file { "/tmp/test.txt":
ensure => absent,
}
}
一个 node 块可匹配多个节点名(逗号分隔):
node 'web1.cluster.com', 'web2.cluster.com', 'web3.cluster.com' {
include httpd
}
8.8.2 正则匹配一批节点
# 匹配 agentX.cluster.com,X 为任意数字
# 也可写为 /^agent/ 或 /cluster\.com$/
node /^agent\d+\.cluster\.com/ {
file { "/tmp/fstab":
ensure => link, # 创建软链接
target => "/etc/fstab", # 源文件
}
}
# 匹配所有 .cluster.com 结尾的节点
node /\.cluster\.com$/ {
include sudo
}
# 匹配 web 开头的节点
node /^web/ {
include httpd
}
# 匹配 db 开头且在 bj 机房
node /^db\d+\.bj\.cluster\.com$/ {
include mysql
}
8.8.3 default 节点
node default { # default 节点代表所有"未被其他 node 块匹配到"的节点
file { '/etc/motd':
ensure => file,
content => "Managed by Puppet\n",
}
}
8.8.4 节点匹配规则(重要)
Puppet 的节点匹配是排他的:只有一个 node 块会生效,按以下优先级选择:
- 精确节点名匹配(最高优先级);
- 若无精确匹配,尝试正则匹配(多个正则都匹配时,行为不确定,应避免重叠);
- 若都不匹配,使用
default节点; - 若连
default都没有 → 报错Could not find default node or by name with 'xxx'。
关键陷阱:如果某节点同时符合 'agent1.cluster.com' 和 /\.cluster\.com$/,只会执行精确匹配的那个块,正则块里的 include sudo 不会生效。这是"Agent 运行没有任何变更"的常见原因。
解决方案:用节点继承(不推荐)或把公共配置抽成基础类:
# 推荐:把公共配置抽成 base 类,每个 node 块都 include 它
class base {
include sudo
include ntp
include motd
}
node 'agent1.cluster.com' {
include base
include httpd
}
node 'agent2.cluster.com' {
include base
include mysql
}
node default {
include base
}
# 或用 node 继承(Puppet 4 中已废弃,不推荐)
node basenode {
include sudo
include ntp
}
node 'agent1.cluster.com' inherits basenode {
include httpd
}
8.8.5 3.x 的 import 与 4.x 的自动加载
Puppet 3.x:可以把节点定义拆到单独文件,用 import 引入。
# /etc/puppet/manifests/site.pp
import 'nodes.pp'
import 'nodes/*.pp'
$puppetserver = "master.cluster.com"
# /etc/puppet/manifests/nodes.pp
node /\.cluster\.com$/ {
include sudo
}
注意:
import在 Puppet 3.x 中已被标记废弃,在 Puppet 4.x 中被彻底移除。
Puppet 4.x+:把所有 .pp 文件直接放在环境的 manifests/ 目录下即可,Puppet 会按字母顺序自动加载全部文件,无需 import。
/etc/puppetlabs/code/environments/production/manifests/
├── site.pp # 通常放全局配置和 default 节点
├── nodes_web.pp # web 节点定义
├── nodes_db.pp # db 节点定义
└── nodes_misc.pp
8.8.6 ENC(External Node Classifier,外部节点分类器)
节点数量大或分组逻辑复杂(来自 CMDB、Foreman、数据库)时,用 node {} 硬编码不现实。ENC 是一个由 Master 调用的外部脚本,Puppet 把节点名作为参数传给它,脚本返回 YAML 描述该节点应该应用哪些类、参数与环境。
# puppet.conf 的 [master] / [server] 段
[master]
node_terminus = exec
external_nodes = /usr/local/bin/enc.rb
#!/bin/bash
# /usr/local/bin/enc.sh —— 最简 ENC 示例,接收节点名,输出 YAML
CERTNAME=$1
case "$CERTNAME" in
web*)
cat <<EOF
---
classes:
base:
httpd:
port: 80
docroot: /web
parameters:
role: webserver
datacenter: bj-idc1
environment: production
EOF
;;
db*)
cat <<EOF
---
classes:
base:
mysql:
parameters:
role: database
environment: production
EOF
;;
*)
cat <<EOF
---
classes:
base:
environment: production
EOF
;;
esac
exit 0
要点:
- ENC 脚本必须可执行、以 puppet 用户能运行、退出码 0;
- 输出的 YAML 中
classes下的类会被自动 include,parameters会成为该节点的顶层变量; - ENC 与
node {}定义可以共存(默认 ENC 优先,node块中额外的类会合并,具体行为取决于版本); - 常见的 ENC 实现:Foreman、Puppet Enterprise Console、自研脚本查 CMDB/数据库。
九、ERB 模板
9.1 模板的用途
content => "..." 只能写死内容,source => 只能分发完全一样的文件。但现实中每台机器的配置文件往往大部分相同、少数几行不同(监听 IP、worker 数、主机名、内存相关参数)。模板就是解决这个问题的:一份带占位符的文件,渲染时把变量替换成该节点的实际值。
- ERB(Embedded Ruby):Puppet 传统模板语言,扩展名
.erb,在 Master 端渲染; - EPP(Embedded Puppet):Puppet 4.x+ 引入的新模板语言,扩展名
.epp,用 Puppet 语法而非 Ruby。
模板文件放在模块的 templates/ 目录下,用 template() / epp() 函数引用。
9.2 ERB 标签语法
| 标签 | 名称 | 作用 |
|---|---|---|
<%= 表达式 %> |
输出标签 | 求值并输出结果到文件 |
<% 语句 %> |
执行标签 | 只执行 Ruby 代码,不输出(用于 if / each / 赋值) |
<%# 注释 %> |
注释标签 | 模板注释,不输出到生成的文件 |
<%- 语句 %> |
去左空白 | 执行代码并去掉标签前的空白 |
<% 语句 -%> |
去右换行 | 执行代码并去掉标签后的换行(避免生成空行) |
<%% / %%> |
转义 | 输出字面的 <% / %> |
9.3 在模板中引用变量与 Facts
<%# 三种引用方式,本质是 Puppet 把变量注入为 Ruby 实例变量 %>
<%# 方式一:@变量名 —— 最常用、最推荐 %>
ServerName <%= @fqdn %>
Listen <%= @port %>
DocumentRoot <%= @docroot %>
ServerAdmin <%= @server_admin %>
<%# 方式二:scope 方法 —— 可访问其他作用域的变量,或变量名带命名空间时 %>
Server IP: <%= scope['::ipaddress'] %>
Top var: <%= scope['::puppetserver'] %>
Class var: <%= scope['httpd::docroot'] %>
<%# 旧写法(3.x) %>
Old style: <%= scope.lookupvar('::ipaddress') %>
<%# 方式三:判断变量是否定义 %>
<% if scope['::app_version'] %>
AppVersion <%= scope['::app_version'] %>
<% end %>
要点:
@变量名只能访问当前作用域(当前类的参数与局部变量)和顶层 facts;- 变量名带
::或需要跨作用域取值时,必须用scope['...'];@variable未定义时值为nil(不会报错),可用if判断。
9.4 模板中的条件与循环
<%# ===== 条件判断 ===== %>
<% if @osfamily == 'RedHat' -%>
User apache
Group apache
<% else -%>
User www-data
Group www-data
<% end -%>
<%# 多分支 %>
<% case @operatingsystemmajrelease
when '6' -%>
# CentOS 6 configuration
PidFile /var/run/httpd/httpd.pid
<% when '7' -%>
# CentOS 7 configuration
PidFile /run/httpd/httpd.pid
<% else -%>
# Unknown release
<% end -%>
<%# ===== 数值计算 ===== %>
<%# 按 CPU 核数设置 worker 进程 %>
worker_processes <%= @processorcount %>;
<%# 内存的一半作为缓存(Ruby 表达式) %>
cache_size <%= (@memorysize_mb.to_f / 2).round %>M;
max_clients <%= @processorcount.to_i * 256 %>
<%# ===== 遍历数组 ===== %>
<% @allowed_ips.each do |ip| -%>
allow <%= ip %>;
<% end -%>
<%# 带索引 %>
<% @upstreams.each_with_index do |server, i| -%>
server <%= server %> weight=<%= 10 - i %>;
<% end -%>
<%# ===== 遍历哈希 ===== %>
<% @vhosts.each do |name, cfg| -%>
<VirtualHost *:<%= cfg['port'] %>>
ServerName <%= name %>
DocumentRoot <%= cfg['root'] %>
</VirtualHost>
<% end -%>
<%# 排序遍历(保证输出稳定,避免每次运行文件内容顺序不同导致误判变更) %>
<% @settings.sort.each do |k, v| -%>
<%= k %> = <%= v %>
<% end -%>
<%# ===== 数组 join ===== %>
Listen <%= @ports.join(' ') %>
重要细节:模板中遍历哈希时一定要排序(
.sort.each),否则不同次渲染出的行顺序可能不同,Puppet 会认为文件"变了",每次运行都重写文件并触发服务重启,破坏幂等性。
9.5 template 函数
# 单个模板
file { '/etc/httpd/conf/httpd.conf':
ensure => file,
owner => 'root',
group => 'root',
mode => '0644',
content => template('httpd/httpd.conf.erb'), # 对应 httpd/templates/httpd.conf.erb
notify => Service['httpd'],
}
# 多个模板拼接(按顺序连接成一个文件)
file { '/etc/nginx/nginx.conf':
content => template('nginx/header.erb', 'nginx/http.erb', 'nginx/footer.erb'),
}
# inline_template:直接在 manifest 中写一小段 ERB
$motd_text = inline_template("Host: <%= @fqdn %> IP: <%= @ipaddress %>\n")
file { '/etc/motd':
content => $motd_text,
}
template() 的路径规则:template('<模块名>/<templates 目录下的相对路径>')
| 写法 | 实际文件(Puppet 3.x) |
|---|---|
template('httpd/httpd.conf.erb') |
/etc/puppet/modules/httpd/templates/httpd.conf.erb |
template('httpd/vhost/ssl.conf.erb') |
/etc/puppet/modules/httpd/templates/vhost/ssl.conf.erb |
9.6 EPP 模板(Puppet 4.x+)与 ERB 的差异
EPP 用 Puppet 语言而不是 Ruby,语法更一致,还支持显式声明参数。
<%- | String $servername,
Integer $port = 80,
Array[String] $allowed_ips = [],
| -%>
<%# 上面是 EPP 的参数声明块,可做类型校验 %>
ServerName <%= $servername %>
Listen <%= $port %>
<% $allowed_ips.each |$ip| { -%>
Allow from <%= $ip %>
<% } -%>
<% if $facts['os']['family'] == 'RedHat' { -%>
User apache
<% } -%>
file { '/etc/httpd/conf.d/vhost.conf':
content => epp('httpd/vhost.conf.epp', {
'servername' => 'www.example.com',
'port' => 8080,
'allowed_ips' => ['10.1.1.0/24', '192.168.0.0/16'],
}),
}
# inline_epp
$text = inline_epp('Host is <%= $facts["fqdn"] %>')
ERB 与 EPP 对比:
| 项目 | ERB | EPP |
|---|---|---|
| 语言 | Ruby | Puppet |
| 扩展名 | .erb |
.epp |
| 变量引用 | <%= @var %> / scope['x'] |
<%= $var %> / $facts['x'] |
| 循环 | <% arr.each do \|x\| %> ... <% end %> |
<% $arr.each \|$x\| { %> ... <% } %> |
| 显式参数 | 不支持 | 支持(带类型校验) |
| 可用能力 | 完整 Ruby(强大但危险) | 限制在 Puppet 语言内(更安全) |
| 兼容性 | 所有版本 | Puppet 4.0+ |
| 调用函数 | template() / inline_template() |
epp() / inline_epp() |
| 建议 | 存量代码维护 | 新代码首选 |
9.7 完整示例:用模板生成 httpd 配置
模块结构:
/etc/puppet/modules/httpd/
├── manifests/
│ └── init.pp
└── templates/
└── httpd.conf.erb
httpd/templates/httpd.conf.erb:
<%# 由 Puppet 管理,请勿手工修改 —— 本行为 ERB 注释,不会出现在生成的文件中 %>
#
# WARNING: This file is managed by Puppet.
# Any manual changes will be overwritten on the next Puppet run.
# Generated for: <%= @fqdn %> (<%= @ipaddress %>) at template render time
#
ServerRoot "/etc/httpd"
Listen <%= @port %>
<% if @ssl_enabled -%>
Listen <%= @ssl_port %>
<% end -%>
<%# CentOS 6 与 7 的 PidFile 路径不同 %>
<% if @operatingsystemmajrelease == '6' -%>
PidFile /var/run/httpd/httpd.pid
<% else -%>
PidFile /run/httpd/httpd.pid
<% end -%>
User <%= @httpd_user %>
Group <%= @httpd_group %>
ServerAdmin <%= @server_admin %>
ServerName <%= @fqdn %>:<%= @port %>
<%# 按 CPU 核数与内存动态计算性能参数 %>
StartServers <%= @processorcount %>
MinSpareServers <%= @processorcount %>
MaxSpareServers <%= @processorcount.to_i * 2 %>
MaxRequestWorkers <%= @processorcount.to_i * 64 %>
MaxConnectionsPerChild 10000
DocumentRoot "<%= @docroot %>"
<Directory "<%= @docroot %>">
AllowOverride None
Options <%= @dir_options %>
<% if @operatingsystemmajrelease == '6' -%>
Order allow,deny
Allow from all
<% else -%>
Require all granted
<% end -%>
</Directory>
DirectoryIndex <%= @dir_index.join(' ') %>
<%# 遍历数组生成多个 Listen/Alias %>
<% @aliases.sort.each do |url, path| -%>
Alias <%= url %> "<%= path %>"
<% end -%>
<%# 遍历数组生成允许访问的网段 %>
<Location /server-status>
SetHandler server-status
<% @admin_networks.each do |net| -%>
Require ip <%= net %>
<% end -%>
</Location>
ErrorLog "logs/error_log"
LogLevel <%= @log_level %>
CustomLog "logs/access_log" combined
IncludeOptional conf.d/*.conf
httpd/manifests/init.pp:
class httpd (
$port = 80,
$ssl_enabled = false,
$ssl_port = 443,
$docroot = '/var/www/html',
$server_admin = 'root@localhost',
$log_level = 'warn',
$dir_options = 'Indexes FollowSymLinks',
$dir_index = ['index.html', 'index.php'],
$aliases = {},
$admin_networks = ['127.0.0.1'],
) {
# 按发行版决定运行用户
case $::osfamily {
'RedHat': {
$httpd_pkg = 'httpd'
$httpd_svc = 'httpd'
$httpd_conf = '/etc/httpd/conf/httpd.conf'
$httpd_user = 'apache'
$httpd_group = 'apache'
}
'Debian': {
$httpd_pkg = 'apache2'
$httpd_svc = 'apache2'
$httpd_conf = '/etc/apache2/apache2.conf'
$httpd_user = 'www-data'
$httpd_group = 'www-data'
}
default: { fail("Unsupported osfamily: ${::osfamily}") }
}
package { $httpd_pkg:
ensure => installed,
}
file { $docroot:
ensure => directory,
owner => $httpd_user,
group => $httpd_group,
mode => '0755',
require => Package[$httpd_pkg],
}
file { $httpd_conf:
ensure => file,
owner => 'root',
group => 'root',
mode => '0644',
content => template('httpd/httpd.conf.erb'),
require => Package[$httpd_pkg],
notify => Service[$httpd_svc],
validate_cmd => '/usr/sbin/httpd -t -f %', # 校验配置语法后才写入
}
service { $httpd_svc:
ensure => running,
enable => true,
}
}
在 site.pp 中使用:
node /^web\d+\.cluster\.com$/ {
class { 'httpd':
port => 80,
docroot => '/web',
server_admin => 'ops@cluster.com',
log_level => 'warn',
dir_index => ['index.html', 'index.htm'],
aliases => { '/static' => '/data/static', '/img' => '/data/img' },
admin_networks => ['10.1.1.0/24'],
}
}
9.8 完整示例:用模板生成 Nginx 配置
nginx/templates/nginx.conf.erb:
# Managed by Puppet. DO NOT EDIT.
user <%= @nginx_user %>;
worker_processes <%= @worker_processes %>;
worker_rlimit_nofile <%= @worker_rlimit_nofile %>;
error_log /var/log/nginx/error.log <%= @log_level %>;
pid /var/run/nginx.pid;
events {
use epoll;
worker_connections <%= @worker_connections %>;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
tcp_nopush on;
keepalive_timeout <%= @keepalive_timeout %>;
client_max_body_size <%= @client_max_body_size %>;
<% if @gzip_enabled -%>
gzip on;
gzip_min_length 1k;
gzip_comp_level 4;
gzip_types text/plain text/css application/json application/javascript;
<% end -%>
<%# 生成 upstream 后端池 %>
<% @upstreams.sort.each do |pool, servers| -%>
upstream <%= pool %> {
<% servers.each do |s| -%>
server <%= s %> max_fails=3 fail_timeout=30s;
<% end -%>
keepalive 32;
}
<% end -%>
server {
listen <%= @listen_port %>;
server_name <%= @server_name %>;
root <%= @docroot %>;
index index.html index.htm;
<% @locations.sort.each do |path, backend| -%>
location <%= path %> {
proxy_pass http://<%= backend %>;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
<% end -%>
location /nginx_status {
stub_status on;
access_log off;
<% @admin_networks.each do |net| -%>
allow <%= net %>;
<% end -%>
deny all;
}
}
include /etc/nginx/conf.d/*.conf;
}
nginx/manifests/init.pp:
class nginx (
$nginx_user = 'nginx',
$listen_port = 80,
$server_name = $::fqdn,
$docroot = '/usr/share/nginx/html',
$log_level = 'warn',
$keepalive_timeout = 65,
$client_max_body_size = '50m',
$gzip_enabled = true,
$worker_connections = 10240,
$worker_rlimit_nofile = 65535,
$upstreams = {},
$locations = {},
$admin_networks = ['127.0.0.1'],
) {
# worker_processes 按 CPU 核数自动设定
$worker_processes = $::processorcount
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'],
validate_cmd => '/usr/sbin/nginx -t -c %',
}
service { 'nginx':
ensure => running,
enable => true,
}
}
# site.pp 中调用
node 'lb1.cluster.com' {
class { 'nginx':
listen_port => 80,
server_name => 'www.cluster.com',
upstreams => {
'app_pool' => ['10.1.1.11:8080', '10.1.1.12:8080'],
'api_pool' => ['10.1.1.21:9000'],
},
locations => {
'/' => 'app_pool',
'/api' => 'api_pool',
},
admin_networks => ['10.1.1.0/24'],
}
}
十、文件分发与 puppet:/// 协议
10.1 files 目录与 puppet:/// 三段式路径规则
file 资源的 source 属性若指向 Master 上的文件,必须用 puppet:/// URL。
标准三段式(Puppet 3.x 后推荐,模块方式):
puppet:///modules/<模块名>/<文件在模块 files 目录下的相对路径>
↑ ↑ ↑ ↑
协议 固定关键字 模块名 相对 files/ 的路径(不含 "files/" 这一层)
| URL | 实际文件位置(Puppet 3.x) |
|---|---|
puppet:///modules/sudo/etc/sudoers |
/etc/puppet/modules/sudo/files/etc/sudoers |
puppet:///modules/httpd/httpd.conf |
/etc/puppet/modules/httpd/files/httpd.conf |
puppet:///modules/nginx/conf.d/vhost.conf |
/etc/puppet/modules/nginx/files/conf.d/vhost.conf |
注意:URL 中不写
files/,Puppet 自动在模块的files/目录下查找。这是最容易写错的地方。
四段式(旧式 fileserver mount 方式,Puppet 3.x 及更早):
puppet://<master 主机名>/<fileserver.conf 中定义的挂载点名>/<挂载点下的相对路径>
需要在 /etc/puppet/fileserver.conf 中定义挂载点:
# /etc/puppet/fileserver.conf
[sudo]
path /etc/puppet/modules/sudo
allow *
[httpd]
path /etc/puppet/modules/httpd
allow *
[extra_files]
path /data/puppet_files
allow 10.1.1.0/24
deny 10.1.1.99
对应的 URL:
$puppetserver = 'master.cluster.com'
file { '/etc/sudoers':
# 四段式:协议 + 服务器名 + 挂载点名 + 相对路径
source => "puppet://$puppetserver/sudo/files/etc/sudoers",
}
两种写法对比:
| 方式 | URL 形式 | 是否需要 fileserver.conf | 推荐度 |
|---|---|---|---|
| 模块方式(三段式) | puppet:///modules/<模块>/<路径> |
不需要(自动可用) | 推荐 |
| 挂载点方式(四段式) | puppet://<server>/<mount>/<路径> |
需要 | 兼容老代码 |
| 省略主机名 | puppet:///...(三个斜杠) |
- | Agent 自动用 server 配置的地址 |
使用三个斜杠
puppet:///时,Agent 会自动使用puppet.conf中server =配置的 Master;写成puppet://主机名/...则是硬编码 Master 地址(不利于多环境/多 Master)。推荐用三斜杠。
文件权限要求:模块目录及文件需要 puppet 用户可读:
chown -R puppet /etc/puppet/modules/
find /etc/puppet/modules -type d -exec chmod 755 {} \;
find /etc/puppet/modules -type f -exec chmod 644 {} \;
10.2 source 的多路径回退写法
source 可以是数组,Puppet 按顺序尝试,用第一个存在的文件。这是实现"按节点/按环境定制、否则用默认"的经典技巧:
file { '/etc/motd':
ensure => file,
source => [
"puppet:///modules/motd/${::fqdn}/motd", # ① 该主机专属版本
"puppet:///modules/motd/${::domain}/motd", # ② 该域专属版本
"puppet:///modules/motd/${::osfamily}/motd", # ③ 该 OS 家族版本
'puppet:///modules/motd/motd', # ④ 默认版本(兜底)
],
}
对应的目录结构:
/etc/puppet/modules/motd/files/
├── agent1.cluster.com/
│ └── motd # agent1 专属
├── cluster.com/
│ └── motd # cluster.com 域通用
├── RedHat/
│ └── motd # RedHat 系通用
└── motd # 全局默认
# 另一个常见用法:按环境回退
file { '/etc/app/app.conf':
ensure => file,
source => [
"puppet:///modules/app/conf/${::environment}/app.conf",
'puppet:///modules/app/conf/default/app.conf',
],
notify => Service['app'],
}
# 也支持 http/https/file 源(4.x+)
file { '/tmp/tool.tar.gz':
ensure => file,
source => 'https://mirror.internal.com/tools/tool.tar.gz',
}
若数组中所有路径都不存在,Puppet 报错
Could not retrieve information from environment production source(s)。
10.3 递归目录分发与 purge 清理
# 完整分发一个目录树,并保持 Agent 端与 Master 端完全一致
file { '/etc/nginx/conf.d':
ensure => directory,
owner => 'root',
group => 'root',
mode => '0644',
recurse => true, # 递归分发子目录与文件
purge => true, # 删除 Agent 上不由 Puppet 管理的多余文件
force => true, # purge 遇到子目录时必须加
source => 'puppet:///modules/nginx/conf.d',
backup => false, # 不备份被删的文件
notify => Service['nginx'],
}
recurse 的取值:
| 值 | 说明 |
|---|---|
false(默认) |
不递归,只处理目录本身(如属主属组) |
true / inf |
无限深度递归 |
remote |
只递归远端 source的内容,不管理本地多出来的文件(purge 无效) |
recurselimit => N |
限制递归层数 |
# 只递归 2 层
file { '/opt/app':
ensure => directory,
recurse => true,
recurselimit => 2,
owner => 'appuser',
}
# 清空一个目录(只保留目录本身)
file { '/var/cache/myapp':
ensure => directory,
recurse => true,
purge => true,
force => true,
}
# 清理 authorized_keys 中不由 Puppet 管理的密钥
user { 'deploy':
ensure => present,
managehome => true,
purge_ssh_keys => true,
}
purge 的注意事项(危险操作):
purge => true会删除目标目录中 Puppet 不认识的所有文件,用错会造成数据丢失;- 千万不要对
/etc、/var、/home这类目录开 purge; - 上线前必须先用
--noop演练确认要删的文件清单:
puppet agent --test --noop
# 输出中会显示 File[/etc/nginx/conf.d/old.conf]/ensure: current_value file, should be absent (noop)
十一、完整实战案例
11.1 案例一:用模块统一推送 /etc/sudoers
需求:所有 .cluster.com 节点安装 sudo 包,并统一使用 Master 上维护的 sudoers 文件。
第一步:在 master 上建立模块目录结构
mkdir -p /etc/puppet/modules/sudo/{files,templates,manifests}
第二步:在 master 上指定模块路径
编辑 /etc/puppet/puppet.conf,在 [main] 段增加:
modulepath = /etc/puppet/modules
第三步:配置 fileserver.conf,允许客户端访问模块目录
# /etc/puppet/fileserver.conf
[sudo]
path /etc/puppet/modules/sudo
allow *
第四步:配置 site.pp
/etc/puppet/manifests/site.pp:
import 'nodes.pp'
$puppetserver = "master.cluster.com"
第五步:配置 nodes.pp
/etc/puppet/manifests/nodes.pp,大量节点时可用正则集中配置:
node /\.cluster\.com$/ {
include sudo # 所有 .cluster.com 结尾的节点调用 sudo 类
}
第六步:编写模块核心 init.pp
/etc/puppet/modules/sudo/manifests/init.pp。其中 $operatingsystem 的值由 facter 返回:
class sudo {
package { sudo:
provider => yum,
name => sudo,
ensure => present,
}
if $operatingsystem in [ "RedHat", "CentOS", "Fedora" ] {
file { "/etc/sudoers":
owner => "root",
group => "root",
mode => 0440,
source => "puppet://$puppetserver/sudo/files/etc/sudoers",
require => Package["sudo"],
}
} else {
fail("Doesn't support this OS: $operatingsystem")
}
}
第七步:准备要下发的真实文件
mkdir -p /etc/puppet/modules/sudo/files/etc
cp /etc/sudoers /etc/puppet/modules/sudo/files/etc/sudoers
# 按需修改拷贝后的 sudoers,修改内容即要推送到 agent 的内容
vi /etc/puppet/modules/sudo/files/etc/sudoers
chown -R puppet /etc/puppet/modules/
要下发的 sudoers 内容示例(在文件末尾追加):
## 由 Puppet 统一管理,请勿手工修改本机文件
%wheel ALL=(ALL) ALL
ops ALL=(ALL) NOPASSWD: ALL
dev ALL=(ALL) NOPASSWD: /bin/systemctl restart myapp, /usr/bin/tail
Defaults logfile=/var/log/sudo.log
第八步:客户端验证
puppet agent --test --server master.cluster.com --no-daemonize --verbose
或直接在 agent 上:
cat /etc/sudoers
ls -l /etc/sudoers # 验证权限为 0440,属主 root:root
visudo -c # 验证语法正确
sudo -l -U ops # 验证权限生效
改进版(Puppet 4.x 风格,用三段式 URL + validate_cmd + 参数化 + sudoers.d 分片):
# modules/sudo/manifests/init.pp
class sudo (
String $sudoers_source = 'puppet:///modules/sudo/etc/sudoers',
Boolean $manage_dropin = true,
) {
unless $::osfamily == 'RedHat' {
fail("sudo module doesn't support this OS: ${::operatingsystem}")
}
package { 'sudo':
ensure => present,
}
file { '/etc/sudoers':
ensure => file,
owner => 'root',
group => 'root',
mode => '0440',
source => $sudoers_source,
require => Package['sudo'],
validate_cmd => '/usr/sbin/visudo -c -f %', # 语法错误则不写入,防止把系统弄坏
}
# 用 sudoers.d 分片管理更安全(不动主文件)
if $manage_dropin {
file { '/etc/sudoers.d':
ensure => directory,
owner => 'root',
group => 'root',
mode => '0750',
recurse => true,
purge => true,
source => 'puppet:///modules/sudo/sudoers.d',
require => Package['sudo'],
}
}
}
11.2 案例二:统一管理 /etc/hosts
需求:所有节点的 /etc/hosts 保持一致,包含集群内所有主机的解析记录。
模块目录结构:
/etc/puppet/modules/hosts/
├── manifests/
│ └── init.pp
└── templates/
└── hosts.erb
方式一:用 host 资源逐条声明(推荐,最幂等,不影响文件中其他手工内容)
# modules/hosts/manifests/init.pp
class hosts (
Hash $entries = {},
) {
# 集群固定成员
host { 'master.cluster.com':
ensure => present,
ip => '10.1.1.2',
host_aliases => ['master', 'puppet'],
comment => 'managed by puppet',
}
host { 'agent1.cluster.com':
ensure => present,
ip => '10.1.1.3',
host_aliases => ['agent1'],
}
host { 'agent2.cluster.com':
ensure => present,
ip => '10.1.1.4',
host_aliases => ['agent2'],
}
host { 'server.cluster.com':
ensure => present,
ip => '10.1.1.1',
host_aliases => ['server', 'yumsrv'],
}
# 通过参数(可由 Hiera 提供)批量追加
$entries.each |String $name, Hash $cfg| {
host { $name:
ensure => 'present',
ip => $cfg['ip'],
host_aliases => $cfg['aliases'],
}
}
# 清理已下线的主机记录
host { 'oldnode.cluster.com':
ensure => absent,
}
}
方式二:用模板整体覆盖 /etc/hosts(强一致,但会抹掉本地手工修改)
<%# modules/hosts/templates/hosts.erb %>
# This file is managed by Puppet. DO NOT EDIT MANUALLY.
# Generated for <%= @fqdn %>
127.0.0.1 localhost localhost.localdomain localhost4 localhost4.localdomain4
::1 localhost localhost.localdomain localhost6 localhost6.localdomain6
<%# 排序遍历,保证输出稳定,避免每次运行都认为文件变了 %>
<% @cluster_hosts.sort.each do |name, cfg| -%>
<%= cfg['ip'] %> <%= name %> <%= (cfg['aliases'] || []).join(' ') %>
<% end -%>
# modules/hosts/manifests/init.pp(模板方式)
class hosts (
Hash $cluster_hosts = {
'master.cluster.com' => { 'ip' => '10.1.1.2', 'aliases' => ['master', 'puppet'] },
'agent1.cluster.com' => { 'ip' => '10.1.1.3', 'aliases' => ['agent1'] },
'agent2.cluster.com' => { 'ip' => '10.1.1.4', 'aliases' => ['agent2'] },
'server.cluster.com' => { 'ip' => '10.1.1.1', 'aliases' => ['server'] },
},
) {
file { '/etc/hosts':
ensure => file,
owner => 'root',
group => 'root',
mode => '0644',
content => template('hosts/hosts.erb'),
backup => 'main', # 覆盖前备份到 filebucket
}
}
在 site.pp 中引用:
node /\.cluster\.com$/ {
include hosts
}
验证:
puppet agent --test --noop # 先看差异
puppet agent --test
cat /etc/hosts
ping -c1 agent2.cluster.com
getent hosts master.cluster.com
11.3 案例三:批量创建用户与用户组
需求:为运维组、开发组统一创建用户和组,分发 SSH 公钥,并清理离职人员账号。
模块目录结构:
/etc/puppet/modules/accounts/
├── manifests/
│ ├── init.pp
│ └── user.pp # define accounts::user
└── files/
└── ssh_keys/
├── zhangsan.pub
└── lisi.pub
modules/accounts/manifests/user.pp(用 define 支持多实例):
define accounts::user (
String $group,
Integer $uid,
Enum['present','absent'] $ensure = 'present',
Array[String] $groups = [],
String $shell = '/bin/bash',
String $comment = '',
Optional[String] $password = undef,
Optional[String] $ssh_key = undef,
) {
user { $title:
ensure => $ensure,
uid => $uid,
gid => $group,
groups => $groups,
shell => $shell,
home => "/home/${title}",
managehome => true,
comment => $comment,
password => $password,
require => Group[$group],
}
if $ensure == 'present' {
file { "/home/${title}/.ssh":
ensure => directory,
owner => $title,
group => $group,
mode => '0700',
require => User[$title],
}
if $ssh_key {
file { "/home/${title}/.ssh/authorized_keys":
ensure => file,
owner => $title,
group => $group,
mode => '0600',
content => "${ssh_key}\n",
require => File["/home/${title}/.ssh"],
}
}
}
}
modules/accounts/manifests/init.pp:
class accounts (
Hash $users = {},
Hash $groups = {},
) {
# ===== 用户组 =====
group { 'ops':
ensure => present,
gid => 3000,
}
group { 'dev':
ensure => present,
gid => 3001,
}
group { 'dba':
ensure => present,
gid => 3002,
}
# ===== 运维组用户 =====
accounts::user { 'zhangsan':
uid => 3101,
group => 'ops',
groups => ['wheel'],
comment => 'Zhang San - Ops',
# openssl passwd -1 'Passw0rd!' 生成
password => '$1$Vs3xQ2mZ$aBcDeFgHiJkLmNoPqRsTu0',
ssh_key => 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCzhang... zhangsan@laptop',
}
accounts::user { 'lisi':
uid => 3102,
group => 'ops',
groups => ['wheel'],
comment => 'Li Si - Ops',
ssh_key => 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQClisi... lisi@laptop',
}
# ===== 开发组用户(不给 wheel,不允许登录部分机器) =====
accounts::user { 'wangwu':
uid => 3201,
group => 'dev',
comment => 'Wang Wu - Dev',
}
# ===== 应用运行账号(系统账号,禁止登录) =====
user { 'appuser':
ensure => present,
uid => 3900,
gid => 'dev',
shell => '/sbin/nologin',
home => '/opt/app',
managehome => false,
system => false,
comment => 'application runtime account',
require => Group['dev'],
}
# ===== 清理离职人员 =====
user { ['zhaoliu', 'sunqi']:
ensure => absent,
managehome => true, # 连家目录一起删
}
# ===== 由 Hiera 提供的额外用户 =====
$users.each |String $name, Hash $cfg| {
accounts::user { $name: * => $cfg }
}
}
site.pp 引用:
node /\.cluster\.com$/ {
include accounts
include sudo # 配合 sudo 模块给 wheel 组授权
}
验证:
puppet agent --test
id zhangsan
getent group ops dev dba
ls -ld /home/zhangsan /home/zhangsan/.ssh
cat /home/zhangsan/.ssh/authorized_keys
id zhaoliu # 应该报 no such user
11.4 案例四:完整部署 vsftpd(site.pp 直写版)
配置 yum、安装包、修改配置(拒绝匿名登录、所有普通用户支持 chroot)、启动服务并开机自启:
node /^agent\d+\.cluster\.com/ {
file { "/etc/yum.repos.d/abc.repo":
content => "[server]\nname=server\nbaseurl=file:///yum\nenabled=1\ngpgcheck=0\n",
mode => 644,
owner => root,
group => root,
}
package { 'install vsftpd':
provider => yum,
name => vsftpd,
ensure => installed,
}
file { "/tmp/1.sh":
content => "sed -i '/^anonymous_enable/s/YES/NO/' /etc/vsftpd/vsftpd.conf\nsed -i '/chroot_local_user=YES/s/#//' /etc/vsftpd/vsftpd.conf\n",
}
exec { "exec-shell":
cwd => "/tmp",
command => "sh 1.sh; rm -rf 1.sh",
user => "root",
path => "/bin:/sbin:/usr/local/bin:/usr/local/sbin:/usr/sbin:/usr/bin",
}
service { vsftpd:
ensure => true,
enable => true,
}
}
改进版(消除 exec,改用配置文件模板,幂等且可维护):
/etc/puppet/modules/vsftpd/
├── manifests/
│ └── init.pp
└── templates/
└── vsftpd.conf.erb
<%# modules/vsftpd/templates/vsftpd.conf.erb %>
# Managed by Puppet. DO NOT EDIT.
anonymous_enable=<%= @anonymous_enable ? 'YES' : 'NO' %>
local_enable=YES
write_enable=YES
local_umask=022
dirmessage_enable=YES
xferlog_enable=YES
connect_from_port_20=YES
xferlog_std_format=YES
listen=<%= @listen ? 'YES' : 'NO' %>
listen_ipv6=NO
pam_service_name=vsftpd
userlist_enable=YES
tcp_wrappers=YES
<%# 所有普通用户 chroot 到家目录 %>
<% if @chroot_local_user -%>
chroot_local_user=YES
allow_writeable_chroot=YES
<% end -%>
max_clients=<%= @max_clients %>
max_per_ip=<%= @max_per_ip %>
idle_session_timeout=<%= @idle_timeout %>
# modules/vsftpd/manifests/init.pp
class vsftpd (
Boolean $anonymous_enable = false,
Boolean $chroot_local_user = true,
Boolean $listen = true,
Integer $max_clients = 200,
Integer $max_per_ip = 10,
Integer $idle_timeout = 600,
) {
# 用 yumrepo 资源替代 file 下发 repo 文件
yumrepo { 'server':
ensure => present,
descr => 'local server repo',
baseurl => 'file:///yum',
enabled => 1,
gpgcheck => 0,
}
package { 'vsftpd':
ensure => installed,
require => Yumrepo['server'],
}
file { '/etc/vsftpd/vsftpd.conf':
ensure => file,
owner => 'root',
group => 'root',
mode => '0600',
content => template('vsftpd/vsftpd.conf.erb'),
require => Package['vsftpd'],
notify => Service['vsftpd'], # 配置变了自动重启
}
service { 'vsftpd':
ensure => running,
enable => true,
}
}
# site.pp
node /^agent\d+\.cluster\.com/ {
class { 'vsftpd':
anonymous_enable => false,
chroot_local_user => true,
max_clients => 100,
}
}
验证:
puppet agent --test
rpm -q vsftpd
systemctl is-active vsftpd ; systemctl is-enabled vsftpd
grep -E 'anonymous_enable|chroot_local_user' /etc/vsftpd/vsftpd.conf
ftp localhost # 匿名登录应被拒绝
11.5 案例五:用模块部署 httpd(原练习完整保留 + 改进版)
需求:
- 配置 yum;
- 安装 httpd、httpd-devel;
- 修改 httpd 家目录为
/web,并在每个家目录生成主页index.html,内容为web$IP主页($IP为 agent 节点 IP 的最后一个数字); - httpd 配置文件跟随 puppet 服务器变更;
- 服务启动并开机自启。
11.5.1 原始参考答案(Puppet 3.x 风格)
1. 建立模块目录
mkdir -p /etc/puppet/modules/httpd/{files,templates,manifests}
2. site.pp
import 'nodes.pp'
$puppetserver = "puppetmaster.cluster.com"
3. nodes.pp
node /\.cluster\.com$/ {
include sudo
include httpd
}
4. init.pp
/etc/puppet/modules/httpd/manifests/init.pp:
class httpd {
file { "/etc/yum.repos.d/rhel-source.repo":
content => "[server]\nname=server\nbaseurl=file:///yum\nenabled=1\ngpgcheck=0\n",
mode => 644,
owner => root,
group => root,
}
package { 'install httpd':
provider => yum,
name => httpd,
ensure => installed,
}
package { 'install httpd-devel':
provider => yum,
name => httpd-devel,
ensure => installed,
}
file { "/web":
ensure => directory,
}
file { "/tmp/1.sh":
content => "#!/bin/bash\nip=`ifconfig eth0 | grep broadcast |awk '{print \$2}' |awk -F. '{print \$4}'`\necho web\$ip > /web/index.html\nsed -i '119,131s/\/var\/www\/html/\/web/' /etc/httpd/conf/httpd.conf\n",
mode => 755,
owner => root,
group => root,
}
exec { "exec-shell":
cwd => "/tmp",
command => "sh 1.sh",
user => "root",
path => "/bin:/sbin:/usr/local/bin:/usr/local/sbin:/usr/sbin:/usr/bin",
}
if $operatingsystem in [ "RedHat", "CentOS", "Fedora" ] {
file { "/etc/httpd/conf/httpd.conf":
owner => "root",
group => "root",
mode => 0440,
source => "puppet://$puppetserver/httpd/files/etc/httpd.conf",
require => Package["httpd"],
}
} else {
fail("Doesn't support this OS: $operatingsystem")
}
service { httpd:
ensure => "running",
enable => true,
}
}
5. puppet.conf 指定 modulepath
modulepath = /etc/puppet/modules
6. fileserver.conf 增加 httpd 段
[sudo]
path /etc/puppet/modules/sudo
allow *
[httpd]
path /etc/puppet/modules/httpd
allow *
7. 准备配置文件并重启 master
mkdir -p /etc/puppet/modules/httpd/files/etc
cp /etc/httpd/conf/httpd.conf /etc/puppet/modules/httpd/files/etc/httpd.conf
# 拷过来后将家目录改为 /web
vi /etc/puppet/modules/httpd/files/etc/httpd.conf
chown -R puppet /etc/puppet/modules/
systemctl restart puppetmaster
8. 客户端验证
puppet agent --test --server master.cluster.com
rpm -qa | grep httpd
ls -ld /web
cat /web/index.html # 应显示 web3 / web4(IP 最后一段)
grep -n 'DocumentRoot' /etc/httpd/conf/httpd.conf
systemctl is-active httpd
curl http://localhost/
11.5.2 改进版:完整可跑的 httpd 模块(消除 exec,用模板 + notify 联动)
上面的原始答案用 exec + sed 修改配置文件,不幂等(每次运行都会跑 1.sh),并且 sed '119,131s/...' 依赖行号,配置文件版本一变就失效。下面是生产可用的写法。
模块目录结构:
/etc/puppet/modules/httpd/
├── manifests/
│ ├── init.pp # class httpd(入口,串联三段)
│ ├── params.pp # class httpd::params(平台差异变量)
│ ├── install.pp # class httpd::install
│ ├── config.pp # class httpd::config
│ ├── service.pp # class httpd::service
│ └── vhost.pp # define httpd::vhost(多虚拟主机)
├── templates/
│ ├── httpd.conf.erb
│ ├── index.html.erb
│ └── vhost.conf.erb
├── files/
│ └── etc/
│ └── httpd.conf # 可选:静态整体覆盖用
├── examples/
│ └── init.pp
└── metadata.json
manifests/params.pp:
class httpd::params {
case $::osfamily {
'RedHat': {
$package_names = ['httpd', 'httpd-devel']
$service_name = 'httpd'
$conf_file = '/etc/httpd/conf/httpd.conf'
$conf_dir = '/etc/httpd/conf.d'
$run_user = 'apache'
$run_group = 'apache'
# CentOS 6 与 7 的 PID 文件路径不同
$pid_file = $::operatingsystemmajrelease ? {
'6' => '/var/run/httpd/httpd.pid',
default => '/run/httpd/httpd.pid',
}
$svc_provider = $::operatingsystemmajrelease ? {
'6' => 'redhat',
default => 'systemd',
}
}
'Debian': {
$package_names = ['apache2', 'apache2-dev']
$service_name = 'apache2'
$conf_file = '/etc/apache2/apache2.conf'
$conf_dir = '/etc/apache2/conf-enabled'
$run_user = 'www-data'
$run_group = 'www-data'
$pid_file = '/var/run/apache2/apache2.pid'
$svc_provider = 'systemd'
}
default: {
fail("Doesn't support this OS: ${::operatingsystem} (${::osfamily})")
}
}
}
manifests/install.pp:
class httpd::install inherits httpd::params {
# 用 yumrepo 资源管理 yum 源(替代下发 .repo 文件)
if $::osfamily == 'RedHat' {
yumrepo { 'server':
ensure => present,
descr => 'local server repo',
baseurl => 'file:///yum',
enabled => 1,
gpgcheck => 0,
}
package { $httpd::params::package_names:
ensure => installed,
require => Yumrepo['server'],
}
} else {
package { $httpd::params::package_names:
ensure => installed,
}
}
}
manifests/config.pp:
class httpd::config inherits httpd::params {
# 家目录(DocumentRoot)
file { $httpd::docroot:
ensure => directory,
owner => $httpd::params::run_user,
group => $httpd::params::run_group,
mode => '0755',
}
# 主页:内容为 web + IP 最后一段(用 Puppet 内置函数取,不用 exec/awk)
file { "${httpd::docroot}/index.html":
ensure => file,
owner => $httpd::params::run_user,
group => $httpd::params::run_group,
mode => '0644',
content => template('httpd/index.html.erb'),
require => File[$httpd::docroot],
}
# 主配置文件:用模板生成,DocumentRoot 由变量注入,不再依赖 sed 行号
file { $httpd::params::conf_file:
ensure => file,
owner => 'root',
group => 'root',
mode => '0644', # 注意:原答案写 0440,httpd 以 root 读取尚可,但 0644 更常规
content => template('httpd/httpd.conf.erb'),
require => Package[$httpd::params::package_names],
validate_cmd => $::osfamily ? {
'RedHat' => '/usr/sbin/httpd -t -f %',
default => undef,
},
}
# 虚拟主机 drop-in 目录,purge 掉手工添加的多余配置
file { $httpd::params::conf_dir:
ensure => directory,
owner => 'root',
group => 'root',
mode => '0755',
recurse => true,
purge => false, # 生产上确认无手工文件后再开 true
}
}
templates/index.html.erb:
<%# 取 IP 最后一段:10.1.1.3 -> 3 %>
<% ip_last = @ipaddress.split('.').last -%>
web<%= ip_last %>
生成的 /web/index.html 内容即 web3(agent1)、web4(agent2),满足需求 3。
若想输出更完整的 HTML 页面:
<% ip_last = @ipaddress.split('.').last -%>
<!DOCTYPE html>
<html>
<head><meta charset="utf-8"><title>web<%= ip_last %></title></head>
<body>
<h1>web<%= ip_last %> 主页</h1>
<p>Hostname: <%= @fqdn %></p>
<p>IP: <%= @ipaddress %></p>
<p>OS: <%= @operatingsystem %> <%= @operatingsystemrelease %></p>
<p>Managed by Puppet (<%= @puppetversion %>)</p>
</body>
</html>
templates/httpd.conf.erb(关键片段,完整版见第九章):
# Managed by Puppet. DO NOT EDIT MANUALLY.
ServerRoot "/etc/httpd"
Listen <%= @port %>
PidFile <%= scope['httpd::params::pid_file'] %>
User <%= scope['httpd::params::run_user'] %>
Group <%= scope['httpd::params::run_group'] %>
ServerAdmin <%= @server_admin %>
ServerName <%= @fqdn %>:<%= @port %>
<%# 需求 3 的核心:DocumentRoot 改为 /web %>
DocumentRoot "<%= @docroot %>"
<Directory />
AllowOverride none
Require all denied
</Directory>
<Directory "<%= @docroot %>">
Options Indexes FollowSymLinks
AllowOverride None
<% if @operatingsystemmajrelease == '6' -%>
Order allow,deny
Allow from all
<% else -%>
Require all granted
<% end -%>
</Directory>
<IfModule dir_module>
DirectoryIndex index.html index.htm
</IfModule>
<Files ".ht*">
Require all denied
</Files>
ErrorLog "logs/error_log"
LogLevel <%= @log_level %>
<IfModule log_config_module>
LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combined
CustomLog "logs/access_log" combined
</IfModule>
<IfModule mime_module>
TypesConfig /etc/mime.types
AddType application/x-compress .Z
AddType application/x-gzip .gz .tgz
</IfModule>
AddDefaultCharset UTF-8
EnableSendfile on
IncludeOptional conf.d/*.conf
manifests/service.pp:
class httpd::service inherits httpd::params {
service { $httpd::params::service_name:
ensure => $httpd::ensure,
enable => $httpd::enable,
provider => $httpd::params::svc_provider,
hasrestart => true,
hasstatus => true,
}
}
manifests/init.pp(入口,串联三段并建立 notify 联动):
# 部署并管理 httpd:安装 -> 配置 -> 服务,配置变更自动重启
class httpd (
Integer[1,65535] $port = 80,
String $docroot = '/web',
String $server_admin = 'root@localhost',
String $log_level = 'warn',
Enum['running','stopped'] $ensure = 'running',
Boolean $enable = true,
) inherits httpd::params {
contain httpd::install
contain httpd::config
contain httpd::service
# 核心:安装 -> 配置 ~> 服务(~> 表示配置变更时刷新/重启服务)
Class['httpd::install']
-> Class['httpd::config']
~> Class['httpd::service']
}
manifests/vhost.pp(可选:支持多虚拟主机):
define httpd::vhost (
Integer $port = 80,
String $docroot = "/web/${name}",
String $server_name = $title,
) {
include httpd
file { $docroot:
ensure => directory,
owner => $httpd::params::run_user,
group => $httpd::params::run_group,
mode => '0755',
}
file { "${docroot}/index.html":
ensure => file,
content => "<h1>${server_name}</h1>\n",
require => File[$docroot],
}
file { "${httpd::params::conf_dir}/${name}.conf":
ensure => file,
owner => 'root',
group => 'root',
mode => '0644',
content => template('httpd/vhost.conf.erb'),
notify => Service[$httpd::params::service_name],
}
}
templates/vhost.conf.erb:
# Managed by Puppet - vhost <%= @name %>
Listen <%= @port %>
<VirtualHost *:<%= @port %>>
ServerName <%= @server_name %>
DocumentRoot "<%= @docroot %>"
ErrorLog logs/<%= @name %>-error_log
CustomLog logs/<%= @name %>-access_log combined
<Directory "<%= @docroot %>">
Options Indexes FollowSymLinks
AllowOverride None
<% if @operatingsystemmajrelease == '6' -%>
Order allow,deny
Allow from all
<% else -%>
Require all granted
<% end -%>
</Directory>
</VirtualHost>
examples/init.pp(本机快速测试用):
class { 'httpd':
port => 80,
docroot => '/web',
}
# 在 Master 上先本机试跑,不需要 Agent
cd /etc/puppet/modules/httpd
puppet apply --modulepath /etc/puppet/modules --noop examples/init.pp
puppet apply --modulepath /etc/puppet/modules examples/init.pp
在 site.pp 中的引用方式:
# Puppet 3.x:/etc/puppet/manifests/site.pp
import 'nodes.pp'
$puppetserver = "master.cluster.com"
# Puppet 3.x:/etc/puppet/manifests/nodes.pp
node /\.cluster\.com$/ {
include sudo
include hosts
include accounts
class { 'httpd':
port => 80,
docroot => '/web',
server_admin => 'ops@cluster.com',
}
}
# 只在 web 节点上开多虚拟主机
node /^web\d+\.cluster\.com$/ {
include sudo
httpd::vhost { 'www.a.com': port => 8081, docroot => '/web/a' }
httpd::vhost { 'www.b.com': port => 8082, docroot => '/web/b' }
}
# Puppet 4.x+:/etc/puppetlabs/code/environments/production/manifests/site.pp
# 无需 import,同目录的 .pp 自动加载
node default {
include base
}
node /\.cluster\.com$/ {
include sudo
class { 'httpd':
port => 80,
docroot => '/web',
}
}
验证方法:
# ① Master 端先做语法检查
puppet parser validate /etc/puppet/manifests/site.pp
puppet parser validate /etc/puppet/modules/httpd/manifests/*.pp
erb -P -x -T '-' /etc/puppet/modules/httpd/templates/httpd.conf.erb | ruby -c
# ② Agent 端空跑演练,确认将要发生的变更
puppet agent --test --noop --server master.cluster.com
# ③ 正式执行
puppet agent --test --server master.cluster.com
# ④ 检查结果
rpm -q httpd httpd-devel
ls -ld /web
cat /web/index.html # 应为 web3 / web4
grep -n 'DocumentRoot' /etc/httpd/conf/httpd.conf # 应为 /web
httpd -t # 配置语法检查
systemctl is-active httpd ; systemctl is-enabled httpd
curl http://localhost/
curl http://10.1.1.3/ # 从 Master 上访问 agent1
# ⑤ 验证幂等性:再跑一次,应该没有任何变更输出
puppet agent --test --server master.cluster.com
# 期望输出:Notice: Applied catalog in x.xx seconds(没有 Notice: /Stage[main]/... changed 行)
# ⑥ 验证 notify 联动:在 Master 上改模板中的 log_level,再跑 Agent
# 应看到 httpd.conf 内容更新 + Service[httpd] 被 refresh(重启)
11.6 案例六:部署 NTP 客户端配置
需求:所有节点统一使用内网 NTP 服务器,CentOS 6 用 ntpd,CentOS 7 用 chronyd。
详见 08-03-时间同步与Xinetd.md
模块目录结构:
/etc/puppet/modules/ntp/
├── manifests/
│ └── init.pp
└── templates/
├── ntp.conf.erb # CentOS 6
└── chrony.conf.erb # CentOS 7
templates/ntp.conf.erb(CentOS 6 / ntpd):
# Managed by Puppet. DO NOT EDIT.
driftfile /var/lib/ntp/drift
restrict default kod nomodify notrap nopeer noquery
restrict -6 default kod nomodify notrap nopeer noquery
restrict 127.0.0.1
restrict -6 ::1
<% @allowed_networks.each do |net| -%>
restrict <%= net %> mask 255.255.255.0 nomodify notrap
<% end -%>
<% @servers.each do |srv| -%>
server <%= srv %> iburst
<% end -%>
includefile /etc/ntp/crypto/pw
keys /etc/ntp/keys
templates/chrony.conf.erb(CentOS 7 / chronyd):
# Managed by Puppet. DO NOT EDIT.
<% @servers.each do |srv| -%>
server <%= srv %> iburst
<% end -%>
driftfile /var/lib/chrony/drift
makestep 1.0 3
rtcsync
<% @allowed_networks.each do |net| -%>
allow <%= net %>
<% end -%>
logdir /var/log/chrony
manifests/init.pp:
class ntp (
Array[String] $servers = ['10.1.1.1', 'ntp1.aliyun.com', 'ntp2.aliyun.com'],
Array[String] $allowed_networks = ['10.1.1.0/24'],
String $timezone = 'Asia/Shanghai',
) {
# 按发行版选择实现
case $::operatingsystemmajrelease {
'6': {
$pkg = 'ntp'
$svc = 'ntpd'
$conf = '/etc/ntp.conf'
$template = 'ntp/ntp.conf.erb'
$provider = 'redhat'
}
'7', '8': {
$pkg = 'chrony'
$svc = 'chronyd'
$conf = '/etc/chrony.conf'
$template = 'ntp/chrony.conf.erb'
$provider = 'systemd'
}
default: {
fail("Unsupported EL release: ${::operatingsystemmajrelease}")
}
}
package { $pkg:
ensure => installed,
}
file { $conf:
ensure => file,
owner => 'root',
group => 'root',
mode => '0644',
content => template($template),
require => Package[$pkg],
notify => Service[$svc],
}
service { $svc:
ensure => running,
enable => true,
provider => $provider,
}
# 统一时区(Puppet 时间同步的前提,也影响证书有效性判断)
file { '/etc/localtime':
ensure => link,
target => "/usr/share/zoneinfo/${timezone}",
force => true,
}
# CentOS 6 的时区配置文件
if $::operatingsystemmajrelease == '6' {
file { '/etc/sysconfig/clock':
ensure => file,
content => "ZONE=\"${timezone}\"\nUTC=false\nARC=false\n",
}
}
# 兜底:每小时强制校时一次(应对长期漂移)
cron { 'force-time-sync':
ensure => present,
command => "/usr/sbin/ntpdate ${servers[0]} > /dev/null 2>&1",
user => 'root',
minute => 15,
environment => ['PATH=/sbin:/bin:/usr/sbin:/usr/bin', 'MAILTO=""'],
}
}
site.pp 引用与验证:
node /\.cluster\.com$/ {
class { 'ntp':
servers => ['10.1.1.1'],
allowed_networks => ['10.1.1.0/24'],
timezone => 'Asia/Shanghai',
}
}
puppet agent --test
# CentOS 7
systemctl is-active chronyd ; chronyc sources -v ; timedatectl
# CentOS 6
service ntpd status ; ntpq -p ; date
11.7 案例七:SSH 加固配置
需求:禁止 root 直接登录、禁用密码登录(只允许密钥)、限制来源网段、缩短空闲超时。
详见 07-03-防火墙与SELinux.md
模块目录结构:
/etc/puppet/modules/ssh/
├── manifests/
│ └── init.pp
└── templates/
└── sshd_config.erb
templates/sshd_config.erb:
# Managed by Puppet. DO NOT EDIT MANUALLY.
# Generated for <%= @fqdn %>
Port <%= @port %>
<% @listen_addresses.each do |addr| -%>
ListenAddress <%= addr %>
<% end -%>
Protocol 2
HostKey /etc/ssh/ssh_host_rsa_key
HostKey /etc/ssh/ssh_host_ecdsa_key
HostKey /etc/ssh/ssh_host_ed25519_key
SyslogFacility AUTHPRIV
LogLevel <%= @log_level %>
# ===== 认证策略 =====
PermitRootLogin <%= @permit_root_login %>
PubkeyAuthentication yes
AuthorizedKeysFile .ssh/authorized_keys
PasswordAuthentication <%= @password_authentication ? 'yes' : 'no' %>
PermitEmptyPasswords no
ChallengeResponseAuthentication no
KerberosAuthentication no
GSSAPIAuthentication no
UsePAM yes
MaxAuthTries <%= @max_auth_tries %>
MaxSessions <%= @max_sessions %>
LoginGraceTime <%= @login_grace_time %>
# ===== 会话超时 =====
ClientAliveInterval <%= @client_alive_interval %>
ClientAliveCountMax <%= @client_alive_count_max %>
TCPKeepAlive yes
# ===== 访问控制 =====
<% unless @allow_users.empty? -%>
AllowUsers <%= @allow_users.join(' ') %>
<% end -%>
<% unless @allow_groups.empty? -%>
AllowGroups <%= @allow_groups.join(' ') %>
<% end -%>
<% unless @deny_users.empty? -%>
DenyUsers <%= @deny_users.join(' ') %>
<% end -%>
# ===== 其他加固项 =====
X11Forwarding <%= @x11_forwarding ? 'yes' : 'no' %>
AllowTcpForwarding <%= @allow_tcp_forwarding ? 'yes' : 'no' %>
PermitUserEnvironment no
IgnoreRhosts yes
HostbasedAuthentication no
StrictModes yes
Compression delayed
UseDNS no
PrintMotd yes
PrintLastLog yes
Banner /etc/ssh/banner
# ===== 加密算法白名单(禁用弱算法) =====
<% if @operatingsystemmajrelease.to_i >= 7 -%>
Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-ctr
MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,hmac-sha2-512,hmac-sha2-256
KexAlgorithms curve25519-sha256@libssh.org,diffie-hellman-group-exchange-sha256
<% end -%>
Subsystem sftp /usr/libexec/openssh/sftp-server
manifests/init.pp:
class ssh (
Integer[1,65535] $port = 22,
Array[String] $listen_addresses = ['0.0.0.0'],
Enum['yes','no','without-password','prohibit-password','forced-commands-only']
$permit_root_login = 'no',
Boolean $password_authentication = false,
Integer $max_auth_tries = 3,
Integer $max_sessions = 10,
Integer $login_grace_time = 60,
Integer $client_alive_interval = 300,
Integer $client_alive_count_max = 2,
Array[String] $allow_users = [],
Array[String] $allow_groups = ['ops', 'wheel'],
Array[String] $deny_users = [],
Boolean $x11_forwarding = false,
Boolean $allow_tcp_forwarding = true,
String $log_level = 'INFO',
String $banner_text = "Authorized access only. All activity is monitored.\n",
) {
package { 'openssh-server':
ensure => installed,
}
file { '/etc/ssh/banner':
ensure => file,
owner => 'root',
group => 'root',
mode => '0644',
content => $banner_text,
}
file { '/etc/ssh/sshd_config':
ensure => file,
owner => 'root',
group => 'root',
mode => '0600',
content => template('ssh/sshd_config.erb'),
require => [ Package['openssh-server'], File['/etc/ssh/banner'] ],
notify => Service['sshd'],
# 关键:配置语法错误时不写入,避免把自己锁在门外
validate_cmd => '/usr/sbin/sshd -t -f %',
}
service { 'sshd':
ensure => running,
enable => true,
hasrestart => true,
provider => $::operatingsystemmajrelease ? {
'6' => 'redhat',
default => 'systemd',
},
}
}
site.pp 引用:
node /\.cluster\.com$/ {
class { 'ssh':
permit_root_login => 'no',
password_authentication => false, # 只允许密钥登录
allow_groups => ['ops', 'wheel'],
client_alive_interval => 300,
x11_forwarding => false,
}
include accounts # 确保 ops 组和用户密钥已下发
Class['accounts'] -> Class['ssh'] # 先建用户再改 sshd,避免锁死
}
验证(务必先在测试机验证,并保留一个已登录的会话):
puppet agent --test --noop # 先空跑看差异!
puppet agent --test
sshd -t # 语法检查
grep -E '^(PermitRootLogin|PasswordAuthentication|AllowGroups)' /etc/ssh/sshd_config
systemctl is-active sshd
# 在【另一个终端】测试新连接能否登录,确认没被锁死
ssh -o PreferredAuthentications=password root@10.1.1.3 # 应被拒绝
ssh zhangsan@10.1.1.3 # 应用密钥登录成功
安全警告:修改 sshd 配置有把自己锁在门外的风险。务必:① 先
--noop;② 保留一个已建立的 SSH 会话不要断开;③validate_cmd必须配置;④ 先在一台测试机验证再全量推送。
十二、进阶特性
12.1 环境(Environment)
环境让同一个 Master 同时提供多套代码,用于隔离开发、测试、生产,实现代码上线流程。
Puppet 3.x 的目录环境(directory environments):
# /etc/puppet/puppet.conf
[main]
environmentpath = $confdir/environments
# 旧式配置(config-file environments,已废弃)
# [production]
# modulepath = /etc/puppet/environments/production/modules
# manifest = /etc/puppet/environments/production/manifests/site.pp
Puppet 4.x+ 的标准目录布局:
/etc/puppetlabs/code/environments/
├── production/ # 生产环境(默认)
│ ├── environment.conf # 该环境的配置(可覆盖 modulepath 等)
│ ├── hiera.yaml # 该环境的 Hiera 层级(Hiera 5)
│ ├── manifests/
│ │ ├── site.pp
│ │ └── nodes.pp
│ ├── modules/ # 该环境私有的模块
│ │ ├── httpd/
│ │ ├── sudo/
│ │ └── ntp/
│ └── data/ # Hiera 数据
│ ├── common.yaml
│ ├── nodes/
│ │ └── agent1.cluster.com.yaml
│ └── os/
│ └── RedHat.yaml
├── development/ # 开发环境
│ ├── environment.conf
│ ├── manifests/
│ │ └── site.pp
│ └── modules/
└── testing/ # 测试环境
├── manifests/
└── modules/
environment.conf 示例:
# /etc/puppetlabs/code/environments/production/environment.conf
# 模块搜索路径($basemodulepath 是全局公共模块目录)
modulepath = ./modules:./site:$basemodulepath
# manifest 入口(默认就是 ./manifests)
manifest = ./manifests
# 环境代码缓存时间,0 表示每次都重新加载(开发环境用 0,生产用 30~180 秒)
environment_timeout = 0
# 该环境使用的 ENC
# config_version = ./scripts/config_version.sh
指定环境的方式:
# ① Agent 命令行临时指定
puppet agent --test --environment development
# ② Agent 配置文件固定指定
# puppet.conf 的 [agent] 段
# environment = development
# ③ puppet apply 指定
puppet apply --environment development /etc/puppetlabs/code/environments/development/manifests/site.pp
# ④ ENC 返回的 environment 字段(优先级最高)
# 查看当前环境
puppet config print environment
facter environment
在 manifest 中使用环境变量:
# 按环境区分配置
class app {
case $::environment {
'production': {
$db_host = 'db-prod.cluster.com'
$log_level = 'warn'
$replicas = 6
}
'testing': {
$db_host = 'db-test.cluster.com'
$log_level = 'info'
$replicas = 2
}
'development': {
$db_host = 'localhost'
$log_level = 'debug'
$replicas = 1
}
default: { fail("Unknown environment: ${::environment}") }
}
file { '/etc/app/app.conf':
content => template('app/app.conf.erb'),
}
}
注意:环境名只能包含小写字母、数字和下划线;环境目录必须在
environmentpath下;新增环境目录后无需重启(受environment_timeout影响)。
12.2 Hiera:数据与代码分离
为什么需要 Hiera?
不用 Hiera 时,环境差异和敏感数据只能硬编码在 manifest 里:
# 反面示例:数据写死在代码里
class mysql {
$root_password = 'Secret123!' # 密码进了 Git,泄露风险
$bind_address = '10.1.1.10' # 换机房就要改代码
$buffer_pool = '8G' # 不同机型不一样
}
问题:
- 敏感数据进代码库(密码、密钥、token 提交到 Git 后不可撤销);
- 环境差异污染代码(每换一个机房/环境就要改 if/case);
- 无法复用模块(模块里塞满了本公司专有的值)。
Hiera 把"做什么(代码)"和"用什么值(数据)"分开:模块代码保持通用,数据放在按层级组织的 YAML 里,Puppet 按层级顺序查找,越具体的层覆盖越通用的层。
Hiera 3(配合 Puppet 3.x/4.x)全局配置 /etc/puppet/hiera.yaml:
---
:backends:
- yaml
- eyaml # 加密数据后端(hiera-eyaml)
:yaml:
:datadir: /etc/puppet/hieradata
:eyaml:
:datadir: /etc/puppet/hieradata
:pkcs7_private_key: /etc/puppet/keys/private_key.pkcs7.pem
:pkcs7_public_key: /etc/puppet/keys/public_key.pkcs7.pem
# 层级:从上到下依次查找,上面的优先级高
:hierarchy:
- "nodes/%{::clientcert}" # ① 单节点专属
- "roles/%{::role}" # ② 按角色(自定义 fact)
- "env/%{::environment}" # ③ 按环境
- "os/%{::osfamily}-%{::operatingsystemmajrelease}" # ④ 按 OS 版本
- "os/%{::osfamily}" # ⑤ 按 OS 家族
- "datacenter/%{::datacenter}" # ⑥ 按机房
- common # ⑦ 全局默认(兜底)
:logger: console
:merge_behavior: deeper # 哈希深合并(需 deep_merge gem)
Hiera 5(Puppet 4.9+ 推荐)环境层配置 environments/production/hiera.yaml:
---
version: 5
defaults:
datadir: data
data_hash: yaml_data
hierarchy:
- name: "Per-node data"
path: "nodes/%{trusted.certname}.yaml"
- name: "Per-role data"
path: "roles/%{facts.role}.yaml"
- name: "Per-environment data"
path: "env/%{server_facts.environment}.yaml"
- name: "Per-OS major release"
path: "os/%{facts.os.family}-%{facts.os.release.major}.yaml"
- name: "Per-OS family"
path: "os/%{facts.os.family}.yaml"
- name: "Per-datacenter"
path: "datacenter/%{facts.datacenter}.yaml"
- name: "Encrypted secrets"
lookup_key: eyaml_lookup_key
path: "secrets/%{server_facts.environment}.eyaml"
options:
pkcs7_private_key: /etc/puppetlabs/puppet/eyaml/private_key.pkcs7.pem
pkcs7_public_key: /etc/puppetlabs/puppet/eyaml/public_key.pkcs7.pem
- name: "Common data"
path: "common.yaml"
数据文件示例:
# hieradata/common.yaml —— 全局默认
---
# 类的自动参数查找:<类名>::<参数名>
httpd::port: 80
httpd::docroot: /var/www/html
httpd::log_level: warn
httpd::server_admin: ops@cluster.com
ssh::permit_root_login: 'no'
ssh::password_authentication: false
ssh::allow_groups:
- ops
- wheel
ntp::servers:
- 10.1.1.1
- ntp1.aliyun.com
ntp::timezone: Asia/Shanghai
# 普通键值(用 hiera()/lookup() 取)
dns_servers:
- 114.114.114.114
- 8.8.8.8
base_packages:
- vim-enhanced
- wget
- curl
- net-tools
- lsof
- tree
# 该节点应包含哪些类(配合 hiera_include)
classes:
- base
- sudo
- ntp
- ssh
# hieradata/os/RedHat-6.yaml —— CentOS 6 专属
---
httpd::pid_file: /var/run/httpd/httpd.pid
ntp::package_name: ntp
ntp::service_name: ntpd
service_provider: redhat
base_packages:
- vim-enhanced
- wget
- sysstat
# hieradata/os/RedHat-7.yaml —— CentOS 7 专属
---
httpd::pid_file: /run/httpd/httpd.pid
ntp::package_name: chrony
ntp::service_name: chronyd
service_provider: systemd
# hieradata/env/production.yaml —— 生产环境
---
httpd::log_level: warn
app::db_host: db-prod.cluster.com
app::replicas: 6
app::debug: false
# hieradata/env/development.yaml —— 开发环境
---
httpd::log_level: debug
app::db_host: localhost
app::replicas: 1
app::debug: true
# hieradata/nodes/agent1.cluster.com.yaml —— 单节点覆盖(优先级最高)
---
httpd::port: 8080
httpd::docroot: /web
classes:
- base
- sudo
- ntp
- ssh
- httpd
# hieradata/roles/webserver.yaml —— 按角色
---
classes:
- base
- httpd
- monitoring::web
httpd::docroot: /web
在代码中查找数据:
# ===== 方式一:自动参数查找(Automatic Parameter Lookup,最推荐)=====
# 只要 include 一个参数化类,Puppet 自动去 Hiera 找 <类名>::<参数名>
class httpd (
$port = 80, # 若 Hiera 中有 httpd::port,则用 Hiera 的值
$docroot = '/var/www/html',
) { ... }
include httpd # 参数自动从 Hiera 注入,无需显式传参
# ===== 方式二:hiera 函数系列(Hiera 3 时代)=====
$dns = hiera('dns_servers') # 优先级查找,返回第一个命中
$dns = hiera('dns_servers', ['8.8.8.8']) # 带默认值
$all = hiera_array('base_packages') # 合并所有层的数组
$merged = hiera_hash('app_settings') # 合并所有层的哈希
hiera_include('classes') # 把 classes 键中列的类全部 include
# ===== 方式三:lookup 函数(Puppet 4.x+ 统一入口,推荐)=====
$dns = lookup('dns_servers')
$dns = lookup('dns_servers', Array[String], 'unique', ['8.8.8.8'])
# ↑键 ↑类型断言 ↑合并策略 ↑默认值
# 合并策略:
# 'first'(默认,取最高优先级层的值)
# 'unique'(数组合并去重,等价 hiera_array)
# 'hash'(哈希浅合并,等价 hiera_hash)
# 'deep'(哈希深合并)
$settings = lookup('app_settings', Hash, 'deep', {})
# 带完整选项的写法
$pkgs = lookup({
'name' => 'base_packages',
'value_type' => Array[String],
'merge' => 'unique',
'default_value' => [],
})
# 用 lookup_options 在 Hiera 数据中声明每个键的合并策略
# hieradata/common.yaml 中声明合并策略
---
lookup_options:
base_packages:
merge: unique
app_settings:
merge: deep
classes:
merge: unique
用 hiera_include 实现"数据驱动的节点分类"(可完全取代 node 块):
# site.pp 只需一行
node default {
hiera_include('classes') # 每个节点应用哪些类完全由 Hiera 数据决定
}
# Puppet 4.x+ 写法
node default {
lookup('classes', Array[String], 'unique', []).include
}
命令行调试 Hiera:
# Hiera 3:模拟某节点的查找(需提供 fact)
hiera httpd::port ::clientcert=agent1.cluster.com ::osfamily=RedHat ::environment=production
hiera -a base_packages ::osfamily=RedHat # 数组合并
hiera -h app_settings ::osfamily=RedHat # 哈希合并
hiera -d httpd::port ::clientcert=agent1.cluster.com # debug 模式,显示查找了哪些文件
# Hiera 5 / Puppet 4.x+:用 puppet lookup(更准确,会加载真实 facts)
puppet lookup httpd::port --node agent1.cluster.com
puppet lookup httpd::port --node agent1.cluster.com --explain # 显示完整查找过程
puppet lookup classes --node agent1.cluster.com --merge unique
puppet lookup --environment development app::db_host --node agent1.cluster.com
用 eyaml 加密敏感数据:
# 安装
gem install hiera-eyaml
# 4.x+ 用自带 gem
/opt/puppetlabs/puppet/bin/gem install hiera-eyaml
# 生成密钥对
eyaml createkeys
mkdir -p /etc/puppetlabs/puppet/eyaml
mv keys/*.pem /etc/puppetlabs/puppet/eyaml/
chown -R puppet:puppet /etc/puppetlabs/puppet/eyaml
chmod 500 /etc/puppetlabs/puppet/eyaml
chmod 400 /etc/puppetlabs/puppet/eyaml/*.pem
# 加密一个值
eyaml encrypt -s 'MySecretPassword' \
--pkcs7-public-key=/etc/puppetlabs/puppet/eyaml/public_key.pkcs7.pem
# 编辑加密文件(自动解密-编辑-重新加密)
eyaml edit hieradata/secrets/production.eyaml
# hieradata/secrets/production.eyaml
---
mysql::root_password: >
ENC[PKCS7,MIIBiQYJKoZIhvcNAQcDoIIBejCCAXYCAQAxggEhMIIBHQIBADAFMAACAQEw
DQYJKoZIhvcNAQEBBQAEggEAflcCq7cSDaPpXPP5MDLQmYnVE0ZKC3fL...]
app::api_token: >
ENC[PKCS7,MIIBiQYJKoZIhvcNAQcDoIIBejCCAXYCAQAxggEhMIIBHQIBADAFMAAC...]
# 代码中透明使用,无需关心加密
class mysql (
Sensitive[String] $root_password, # Sensitive 类型可避免密码出现在日志中
) {
file { '/root/.my.cnf':
ensure => file,
mode => '0600',
content => Sensitive("[client]\npassword=${root_password.unwrap}\n"),
show_diff => false, # 日志中不显示差异内容
}
}
12.3 PuppetDB 与导出资源(Exported Resources)
PuppetDB 是 Puppet 的数据存储服务,保存所有节点的 facts、catalog、report 和导出资源,底层用 PostgreSQL,提供查询 API。
安装 PuppetDB:
# Master 端
yum install -y puppetdb puppetdb-termini
# PostgreSQL
yum install -y postgresql-server
postgresql-setup initdb
systemctl enable --now postgresql
su - postgres -c "createuser -DRSP puppetdb"
su - postgres -c "createdb -E UTF8 -O puppetdb puppetdb"
# 配置数据库连接
vi /etc/puppetlabs/puppetdb/conf.d/database.ini
# /etc/puppetlabs/puppetdb/conf.d/database.ini
[database]
subname = //localhost:5432/puppetdb
username = puppetdb
password = puppetdb_password
# 报告保留期限
report-ttl = 14d
node-ttl = 7d
node-purge-ttl = 14d
# /etc/puppetlabs/puppetdb/conf.d/jetty.ini
[jetty]
host = 0.0.0.0
port = 8080
ssl-host = 0.0.0.0
ssl-port = 8081
ssl-key = /etc/puppetlabs/puppetdb/ssl/private.pem
ssl-cert = /etc/puppetlabs/puppetdb/ssl/public.pem
ssl-ca-cert = /etc/puppetlabs/puppetdb/ssl/ca.pem
puppetdb ssl-setup
systemctl enable --now puppetdb
让 Master 使用 PuppetDB:
# /etc/puppetlabs/puppet/puppet.conf
[master]
storeconfigs = true # 【关键】开启导出资源存储
storeconfigs_backend = puppetdb
reports = store,puppetdb
# /etc/puppetlabs/puppet/routes.yaml
---
master:
facts:
terminus: puppetdb
cache: yaml
# /etc/puppetlabs/puppet/puppetdb.conf
[main]
server_urls = https://master.cluster.com:8081
systemctl restart puppetserver
导出资源的语法:
| 语法 | 名称 | 含义 |
|---|---|---|
@@resource { ... } |
导出资源(exported resource) | 把资源声明导出到 PuppetDB,不在本节点应用 |
Resource <<| 查询条件 |>> |
收集器(collector) | 从 PuppetDB 收集符合条件的导出资源并在本节点应用 |
@resource { ... } |
虚拟资源(virtual resource) | 只在本节点内声明但不应用,需 Resource <| |> 实现 |
Resource <| 条件 |> |
虚拟资源收集器 | 实现(realize)本节点内的虚拟资源 |
realize(Resource['x']) |
realize 函数 | 实现虚拟资源的另一种写法 |
12.3.1 典型场景一:自动生成 /etc/hosts
每个节点导出自己的 host 记录,所有节点收集全部记录 → 自动维护完整的 hosts 文件,新增节点无需改代码。
# modules/hosts/manifests/init.pp
class hosts {
# ① 每个节点导出自己的 host 记录(@@ 表示导出)
@@host { $::fqdn:
ensure => present,
ip => $::ipaddress,
host_aliases => [$::hostname],
tag => 'cluster_hosts', # 打标签便于收集
}
# ② 收集所有节点导出的 host 记录并在本机应用
Host <<| tag == 'cluster_hosts' |>>
# 也可以按条件筛选,例如只收集同机房的
# Host <<| tag == 'cluster_hosts' and datacenter == $::datacenter |>>
}
node /\.cluster\.com$/ {
include hosts
}
运行效果:第一轮所有 Agent 跑完后,PuppetDB 里就有了全部节点的 host 记录;第二轮每个节点都会得到完整的 /etc/hosts。
12.3.2 典型场景二:自动生成 Nagios/Zabbix 监控配置
被监控节点导出自己的监控定义,监控服务器收集全部定义 → 新机器上线自动纳入监控。
# modules/monitoring/manifests/client.pp —— 所有被监控节点
class monitoring::client {
package { 'nrpe': ensure => installed }
# 导出该节点的 Nagios 主机定义
@@nagios_host { $::fqdn:
ensure => present,
alias => $::hostname,
address => $::ipaddress,
use => 'linux-server',
target => '/etc/nagios/conf.d/hosts.cfg',
tag => 'nagios_managed',
}
# 导出该节点的服务检查定义
@@nagios_service { "check_ping_${::fqdn}":
ensure => present,
host_name => $::fqdn,
check_command => 'check_ping!100.0,20%!500.0,60%',
service_description => 'PING',
use => 'generic-service',
target => '/etc/nagios/conf.d/services.cfg',
tag => 'nagios_managed',
}
@@nagios_service { "check_disk_${::fqdn}":
ensure => present,
host_name => $::fqdn,
check_command => 'check_nrpe!check_disk',
service_description => 'Disk Usage',
use => 'generic-service',
target => '/etc/nagios/conf.d/services.cfg',
tag => 'nagios_managed',
}
}
# modules/monitoring/manifests/server.pp —— 监控服务器
class monitoring::server {
package { ['nagios', 'nagios-plugins-all', 'nrpe']:
ensure => installed,
}
file { '/etc/nagios/conf.d':
ensure => directory,
owner => 'nagios',
group => 'nagios',
mode => '0755',
}
# 收集所有被监控节点导出的定义
Nagios_host <<| tag == 'nagios_managed' |>> {
notify => Service['nagios'],
}
Nagios_service <<| tag == 'nagios_managed' |>> {
notify => Service['nagios'],
}
service { 'nagios':
ensure => running,
enable => true,
require => Package['nagios'],
}
}
12.3.3 典型场景三:自动生成负载均衡后端池、SSH known_hosts
# 后端应用节点导出自己
class app::backend {
@@concat::fragment { "upstream_${::fqdn}":
target => '/etc/nginx/conf.d/upstream.conf',
content => " server ${::ipaddress}:8080 max_fails=3 fail_timeout=30s;\n",
order => '10',
tag => 'app_upstream',
}
}
# 负载均衡节点收集
class app::loadbalancer {
concat { '/etc/nginx/conf.d/upstream.conf':
ensure => present,
notify => Service['nginx'],
}
concat::fragment { 'upstream_header':
target => '/etc/nginx/conf.d/upstream.conf',
content => "upstream app_pool {\n",
order => '01',
}
Concat::Fragment <<| tag == 'app_upstream' |>>
concat::fragment { 'upstream_footer':
target => '/etc/nginx/conf.d/upstream.conf',
content => " keepalive 32;\n}\n",
order => '99',
}
}
# 自动互信:每个节点导出自己的 SSH host key,所有节点收集
class ssh::knownhosts {
@@sshkey { $::fqdn:
ensure => present,
type => 'ssh-rsa',
key => $::sshrsakey,
host_aliases => [$::hostname, $::ipaddress],
tag => 'cluster_sshkeys',
}
Sshkey <<| tag == 'cluster_sshkeys' |>>
}
查询 PuppetDB:
# 查所有节点
curl -k https://master.cluster.com:8081/pdb/query/v4/nodes | python -m json.tool
# 查某节点的 facts
curl -k 'https://master.cluster.com:8081/pdb/query/v4/facts?query=["=","certname","agent1.cluster.com"]'
# 查所有 CentOS 7 节点
curl -k -G https://master.cluster.com:8081/pdb/query/v4/facts \
--data-urlencode 'query=["and",["=","name","operatingsystemmajrelease"],["=","value","7"]]'
# 查所有导出资源
curl -k -G https://master.cluster.com:8081/pdb/query/v4/resources \
--data-urlencode 'query=["and",["=","exported",true],["=","type","Host"]]'
# 查最近的报告
curl -k https://master.cluster.com:8081/pdb/query/v4/reports | python -m json.tool
# 用 puppet query 命令(需 puppetdb-cli)
puppet query 'nodes {}'
puppet query 'inventory { facts.os.family = "RedHat" }'
导出资源的注意事项:
- 必须先开启
storeconfigs = true且配好 PuppetDB,否则@@静默失效或报错; - 需要两轮 Puppet 运行才能生效(第一轮导出,第二轮才能被收集到);
- 导出资源的标题必须全局唯一(含
$::fqdn或$::hostname),否则不同节点导出同名资源会冲突; - 节点下线后要用
puppet node deactivate <certname>从 PuppetDB 中清理,否则残留的导出资源会一直被收集; - 大规模环境下收集器会显著增加 catalog 编译时间,需注意性能。
# 清理已下线节点
puppet node deactivate agent2.cluster.com
puppet node clean agent2.cluster.com # 同时清证书
12.4 report 与 Dashboard
report 处理器(reports 参数):
| 值 | 作用 |
|---|---|
none |
不处理报告 |
store |
存为 YAML 文件($vardir/reports/<certname>/*.yaml) |
log |
写入 Master 的日志 |
http |
POST 到指定的 reporturl(puppet-dashboard 用这个) |
puppetdb |
存入 PuppetDB |
tagmail |
按 tag 发邮件 |
# Master 端 puppet.conf
[master]
reports = store,http,puppetdb
reporturl = http://10.1.1.2:8141/reports
# Agent 端
[agent]
report = true
报告中包含的信息:本次运行的耗时、每个资源的状态(unchanged / changed / failed / skipped / out_of_sync)、变更前后的值、失败原因、节点的 facts、catalog 版本。
Web 界面选择:
| 工具 | 说明 |
|---|---|
| puppet-dashboard | 老牌轻量报告平台(Rails 应用 + MySQL),见下文完整搭建步骤 |
| Foreman | 功能最全:报告 + ENC + 主机生命周期管理 + PXE 装机,可完全替代 dashboard |
| PuppetBoard | 基于 PuppetDB API 的轻量 Web 界面(Python/Flask),只读,部署最简单 |
| Puppet Enterprise Console | 商业版自带的控制台 |
12.5 puppet apply 单机模式
# 应用一个 manifest 文件
puppet apply /etc/puppet/manifests/site.pp
# 直接执行一段代码
puppet apply -e 'file { "/tmp/a.txt": ensure => file, content => "hi\n" }'
puppet apply -e 'package { "vim-enhanced": ensure => installed }'
puppet apply -e 'include httpd'
# 空跑演练
puppet apply --noop /etc/puppet/manifests/site.pp
# 指定模块路径与 Hiera 配置
puppet apply --modulepath /etc/puppet/modules \
--hiera_config /etc/puppet/hiera.yaml \
/etc/puppet/manifests/site.pp
# 详细输出 / 调试
puppet apply --verbose --debug --show_diff site.pp
# 用退出码判断结果(CI 中很有用)
puppet apply --detailed-exitcodes site.pp
# 0 = 无变更无失败;2 = 有变更;4 = 有失败;6 = 有变更且有失败
# 测试单个模块(模块自带 examples)
cd /etc/puppet/modules/httpd
puppet apply --modulepath /etc/puppet/modules examples/init.pp
适用场景:
- 学习与调试:不用搭 Master 即可验证 manifest 语法与逻辑;
- 镜像打包:Packer 构建 AMI/OVA、Docker 镜像构建时执行一次 Puppet 代码固化基线;
- Masterless 架构:不部署 Master,把代码用 Git 拉到每台机器,用 cron 定期
puppet apply(适合节点少、不想维护 Master 的场景); - Bootstrap:新机器初装时先跑一次 apply 完成 Puppet Agent 自身的配置,再交给 Master 管理;
- 紧急修复:Master 故障时在单机上应急执行。
Masterless 架构示例:
#!/bin/bash
# /usr/local/bin/puppet-masterless.sh —— 由 cron 每 30 分钟调用
set -e
CODE_DIR=/opt/puppet-code
cd "$CODE_DIR"
git fetch --all --quiet
git reset --hard origin/master --quiet
/opt/puppetlabs/bin/puppet apply \
--modulepath "${CODE_DIR}/modules:${CODE_DIR}/site" \
--hiera_config "${CODE_DIR}/hiera.yaml" \
--detailed-exitcodes \
"${CODE_DIR}/manifests/site.pp" \
--logdest /var/log/puppet-apply.log
# 用 Puppet 管理这个 cron(自举)
cron { 'puppet-masterless':
ensure => present,
command => '/usr/local/bin/puppet-masterless.sh',
user => 'root',
minute => ['3', '33'],
environment => ['PATH=/opt/puppetlabs/bin:/sbin:/bin:/usr/sbin:/usr/bin', 'MAILTO=ops@cluster.com'],
}
12.6 与 Git 结合做代码版本管理 + r10k
为什么必须把 Puppet 代码放进 Git?
Puppet 代码就是基础设施的定义(Infrastructure as Code),必须享有和应用代码同等的工程实践:版本历史、分支、Code Review、回滚、CI 检查。
详见 14-01-Git版本控制.md
基本工作流:
# 1. 把 Puppet 代码目录初始化为 Git 仓库
cd /etc/puppetlabs/code/environments/production
git init
cat > .gitignore <<'EOF'
*.swp
*.bak
.vagrant/
spec/fixtures/modules/
secrets/*.plain.yaml
EOF
git add .
git commit -m "Initial Puppet code"
git remote add origin git@git.cluster.com:ops/puppet-control.git
git push -u origin master
# 2. 开发新功能用分支
git checkout -b feature/add-redis-module
# ... 编写模块 ...
puppet parser validate manifests/*.pp modules/redis/manifests/*.pp
puppet-lint modules/redis/manifests/
git add . && git commit -m "Add redis module"
git push origin feature/add-redis-module
# 3. 提 Merge Request → Code Review → 合并到 master
Git 分支与 Puppet 环境的映射(关键理念):
Git 分支 → Puppet 环境
──────────────────────────────────────────────
master / production → production 环境
testing → testing 环境
development → development 环境
feature/add-redis → feature_add_redis 环境(临时测试)
这样开发者可以在自己的 feature 分支上测试(puppet agent --test --environment feature_add_redis),验证通过后合并到 production 分支,代码自动进入生产环境。
post-receive 钩子实现自动部署(简易方案):
#!/bin/bash
# Git 服务器上的 hooks/post-receive
while read oldrev newrev refname; do
BRANCH=$(echo "$refname" | sed 's|refs/heads/||')
TARGET="/etc/puppetlabs/code/environments/${BRANCH}"
if [ ! -d "$TARGET" ]; then
mkdir -p "$TARGET"
git clone /srv/git/puppet-control.git -b "$BRANCH" "$TARGET"
fi
cd "$TARGET"
git fetch --all
git reset --hard "$newrev"
chown -R puppet:puppet "$TARGET"
echo "Deployed branch $BRANCH to $TARGET"
done
r10k 简述
r10k 是 Puppet 官方推荐的环境与模块部署工具,核心能力是"Git 分支 = Puppet 环境"的自动化。
# 安装
gem install r10k
/opt/puppetlabs/puppet/bin/gem install r10k
# /etc/puppetlabs/r10k/r10k.yaml
---
# 缓存目录
cachedir: '/var/cache/r10k'
# control repo(存放 Puppetfile 与 manifests 的仓库)
sources:
main:
remote: 'git@git.cluster.com:ops/puppet-control.git'
basedir: '/etc/puppetlabs/code/environments'
# Forge 代理(内网环境)
forge:
proxy: 'http://proxy.cluster.com:3128'
baseurl: 'https://forgeapi.puppet.com'
# control repo 中的 Puppetfile —— 声明依赖哪些外部模块及版本
forge 'https://forgeapi.puppet.com'
# 从 Forge 安装,锁定版本
mod 'puppetlabs/stdlib', '9.0.0'
mod 'puppetlabs/concat', '7.4.0'
mod 'puppetlabs/apache', '10.1.1'
mod 'puppetlabs/mysql', '14.0.0'
mod 'puppetlabs/ntp', '10.1.0'
# 从内部 Git 仓库安装(指定分支/tag/commit)
mod 'mycorp_base',
:git => 'git@git.cluster.com:puppet/mycorp_base.git',
:branch => 'master'
mod 'mycorp_app',
:git => 'git@git.cluster.com:puppet/mycorp_app.git',
:tag => 'v2.3.1'
mod 'mycorp_monitoring',
:git => 'git@git.cluster.com:puppet/mycorp_monitoring.git',
:ref => 'a1b2c3d4'
# 部署所有环境(每个 Git 分支变成一个环境目录)
r10k deploy environment -p
# -p / --puppetfile 表示同时按 Puppetfile 安装模块
# 只部署某个环境
r10k deploy environment production -p
r10k deploy environment development -p
# 只更新模块
r10k deploy module -e production stdlib apache
# 查看将要部署的环境
r10k deploy display -p
# 常见配合:Git post-receive 钩子中调用 r10k
完整的 CI/CD 流水线(GitLab CI 示例):
# .gitlab-ci.yml
stages:
- lint
- unit
- deploy
syntax:
stage: lint
script:
- find . -name '*.pp' -exec puppet parser validate {} \;
- find . -name '*.epp' -exec puppet epp validate {} \;
- puppet-lint --relative manifests/ site/
- find . -name '*.erb' -exec sh -c 'erb -P -x -T "-" {} | ruby -c' \;
- find . -name '*.yaml' -exec ruby -e 'require "yaml"; YAML.load_file(ARGV[0])' {} \;
rspec:
stage: unit
script:
- bundle install
- bundle exec rake spec
deploy_production:
stage: deploy
only:
- master
script:
- ssh puppet@master.cluster.com 'r10k deploy environment production -p'
十三、扩展:puppet-dashboard 报告平台
Puppet 还有一些生态插件可课后自行研究:
- Web 报告查看;
- ActiveMQ/RabbitMQ + mcollective 实现队列高效推送;
- Web 管理工具 puppet-dashboard 和 foreman。
参考网站:http://www.kisspuppet.com/
下面是 puppet-dashboard 的简易配置(在 RHEL 6.5 上测试成功,未在 CentOS 7.3 上测试)。
说明:puppet-dashboard 依赖 Ruby 1.8 + Rails 2.x,属于 Puppet 3.x 时代的产物,在 CentOS 7 / Puppet 4.x+ 上已不适用。新环境请改用 Foreman 或 PuppetBoard。
13.1 安装软件包
在 puppetmaster 上:
yum install puppet-dashboard ruby-mysql mysql-server
13.2 启动 MySQL 并建库授权
/etc/init.d/mysqld restart
mysql
mysql> create database dashboard character set utf8;
mysql> grant all on dashboard.* to 'dashboard'@'localhost' identified by '123';
mysql> flush privileges;
mysql> quit
13.3 配置 database.yml
/usr/share/puppet-dashboard/config/database.yml:
production:
database: dashboard
username: dashboard
password: 123
encoding: utf8
adapter: mysql
13.4 导入数据
cd /usr/share/puppet-dashboard/
rake gems:refresh_specs
rake RAILS_ENV=production db:migrate
验证:
mysql> use dashboard;
mysql> show tables; -- 验证导入了 18 张表
13.5 使用 Passenger 运行 dashboard
Passenger 方式效率较高,但安装麻烦,需要用 gem 安装 ruby 相关包。
yum install ruby-devel ruby-libs rubygems libcurl-devel httpd* mod_ssl -y
gem 包位于笔记目录的 arch/puppet_gem_soft 下:
daemon_controller-1.1.7.gem
passenger-4.0.30.gem
rack-1.5.2.gem
rake-10.0.1.gem
将这四个包拷到 puppetmaster,进入拷贝目录执行:
gem list --local # 查看已安装 gem
*** LOCAL GEMS ***
json (1.5.5)
gem install --local passenger-4.0.30.gem # 自动解决依赖
gem list --local
*** LOCAL GEMS ***
daemon_controller (1.1.7)
json (1.5.5)
passenger (4.0.30)
rack (1.5.2)
rake (10.0.1)
passenger-install-apache2-module
确认模块生成:
ls /usr/lib/ruby/gems/1.8/gems/passenger-4.0.30/buildout/apache2/mod_passenger.so
13.6 配置虚拟主机
新建 /etc/httpd/conf.d/passenger.conf:
LoadModule passenger_module /usr/lib/ruby/gems/1.8/gems/passenger-4.0.30/buildout/apache2/mod_passenger.so
<IfModule mod_passenger.c>
PassengerRoot /usr/lib/ruby/gems/1.8/gems/passenger-4.0.30
PassengerRuby /usr/bin/ruby
PassengerHighPerformance on
PassengerMaxPoolSize 12
PassengerPoolIdleTime 1500
PassengerStatThrottleRate 120
# RailsAutoDetect On
</IfModule>
Listen 8141
<VirtualHost *:8141>
DocumentRoot "/usr/share/puppet-dashboard/public/"
<Directory "/usr/share/puppet-dashboard/public/">
Options None
AllowOverride AuthConfig
Order allow,deny
allow from all
</Directory>
ErrorLog /var/log/httpd/dashboard.error.log
LogLevel warn
CustomLog /var/log/httpd/dashboard.access.log combined
</VirtualHost>
13.7 启动服务
/etc/rc.d/init.d/puppetmaster restart
/etc/rc.d/init.d/httpd restart
通过 firefox 访问 http://172.16.21.12:8141/,此时还没有信息。
13.8 配置 agent 自动上报
在 puppetmaster 的 /etc/puppet/puppet.conf 的 [main] 段加:
reports = http
reporturl = http://172.16.21.12:8141/reports
13.9 开启后台报告处理进程
cd /usr/share/puppet-dashboard/
rake RAILS_ENV=production jobs:work &
再访问 http://172.16.21.12:8141/ 即可看到报告,但时间默认为 UTC,需改为 CST。
13.10 修改时区
/usr/share/puppet-dashboard/config/settings.yml:
time_zone: 'Asia/Shanghai'
/etc/init.d/httpd restart
再次访问,时间显示正常。
十四、排错清单
14.1 Agent 连不上 Master
典型报错:
Error: Could not request certificate: Connection refused - connect(2) for "master.cluster.com" port 8140
Error: Could not retrieve catalog from remote server: Connection refused
Error: getaddrinfo: Name or service not known
排查流程(按顺序):
# ① 名字能否解析(最常见)
ping -c1 master.cluster.com
getent hosts master.cluster.com
cat /etc/hosts # 检查是否有 Master 的记录
# 修复:在 /etc/hosts 中补上 10.1.1.2 master.cluster.com
# ② 端口能否连通
telnet master.cluster.com 8140
nc -zv master.cluster.com 8140
curl -k https://master.cluster.com:8140 # 应返回 400/404 而非 refused
# ③ Master 端服务是否在跑、端口是否监听
systemctl status puppetmaster # 或 puppetserver
lsof -i:8140
ss -tunlp | grep 8140
# 若没监听:查看日志
tail -50 /var/log/messages | grep -i puppet
tail -50 /var/log/puppetlabs/puppetserver/puppetserver.log
# ④ 防火墙是否放行
# Master 端
firewall-cmd --list-all | grep 8140
iptables -L -n | grep 8140
# 临时验证:systemctl stop firewalld
# ⑤ SELinux 是否拦截
getenforce
ausearch -m avc -ts recent | grep -i puppet
setenforce 0 # 临时验证
# ⑥ Agent 的 server 配置是否正确
grep -n 'server' /etc/puppet/puppet.conf
puppet config print server --section agent
# 必须是主机名,不能是 IP,且必须能解析
# ⑦ 是否有代理/路由问题
env | grep -i proxy
ip route get 10.1.1.2
traceroute master.cluster.com
14.2 证书报错
14.2.1 SSL_connect / certificate verify failed
Error: Could not retrieve catalog from remote server: SSL_connect returned=1 errno=0
state=error: certificate verify failed: [self signed certificate in certificate chain]
Warning: Unable to fetch my node definition, but the agent run will continue
原因:Agent 本地缓存的 CA 证书与 Master 当前的 CA 不匹配(Master 重装过、Master 的 ssl 目录被重建过)。
解决步骤:
# ① Master 上清除该节点旧证书
puppet cert clean agent1.cluster.com
# 4.x+
puppetserver ca clean --certname agent1.cluster.com
# ② Agent 上彻底删除本地 ssl 目录
systemctl stop puppet
rm -rf /var/lib/puppet/ssl/* # 3.x
rm -rf /etc/puppetlabs/puppet/ssl/* # 4.x+
# ③ Agent 重新请求
puppet agent --test --server master.cluster.com
# ④ Master 签名
puppet cert sign agent1.cluster.com
# ⑤ Agent 再次运行
puppet agent --test --server master.cluster.com
systemctl start puppet
14.2.2 hostname 不匹配(CN 与请求名不一致)
Error: Could not retrieve catalog from remote server: hostname "master" does not match
the server certificate (master.cluster.com)
Warning: Server hostname 'puppet' did not match server certificate; expected master.cluster.com
原因:Agent 用别名(如 puppet、master、IP)连接,但 Master 证书的 CN / SAN 中没有这个名字。
解决方案 A(推荐):Agent 改用 Master 证书的 CN 连接
# 改 puppet.conf 中的 server 为完整的 CN
sed -i 's/^\s*server\s*=.*/ server = master.cluster.com/' /etc/puppet/puppet.conf
puppet agent --test
解决方案 B:给 Master 证书加备用名(dns_alt_names),需重新生成 Master 证书
# Master 上
vi /etc/puppet/puppet.conf
# [master]
# dns_alt_names = master.cluster.com,master,puppet,10.1.1.2
systemctl stop puppetmaster
# 只删 Master 自己的证书,保留 CA(否则所有 Agent 都要重签)
rm -f /var/lib/puppet/ssl/certs/master.cluster.com.pem
rm -f /var/lib/puppet/ssl/private_keys/master.cluster.com.pem
rm -f /var/lib/puppet/ssl/public_keys/master.cluster.com.pem
rm -f /var/lib/puppet/ssl/ca/signed/master.cluster.com.pem
# 重新生成带 alt names 的 Master 证书
puppet cert generate master.cluster.com --dns_alt_names=master.cluster.com,master,puppet,10.1.1.2
# 4.x+
puppetserver ca generate --certname master.cluster.com --subject-alt-names master,puppet,10.1.1.2
systemctl start puppetmaster
# 验证证书中的 SAN
openssl x509 -in /var/lib/puppet/ssl/certs/master.cluster.com.pem -noout -text | grep -A1 'Alternative'
14.2.3 Agent 主机名改过导致证书失效
Error: Could not request certificate: The certificate retrieved from the master does not
match the agent's private key.
按 14.2.1 的完整重置流程处理(两端都清)。
14.2.4 证书未生效 / 已过期(时间不同步)
Error: SSL_connect returned=1 errno=0 state=error: certificate verify failed:
[certificate is not yet valid for /CN=agent1.cluster.com]
Error: ... [certificate has expired for /CN=master.cluster.com]
排查与解决:
# ① 对比两端时间(这是根因)
date # Master 和 Agent 上都执行
timedatectl # CentOS 7
# ② 查看证书的有效期
openssl x509 -in /var/lib/puppet/ssl/certs/agent1.cluster.com.pem -noout -dates
# notBefore=Aug 22 03:00:00 2026 GMT
# notAfter=Aug 21 03:00:00 2031 GMT
# ③ 强制同步时间
systemctl stop puppet
ntpdate ntp1.aliyun.com # 或 chronyc makestep
timedatectl set-timezone Asia/Shanghai
hwclock -w # 写入硬件时钟
# ④ 启用持续同步
systemctl enable --now chronyd # CentOS 7
chkconfig ntpd on; service ntpd start # CentOS 6
# ⑤ 若证书确实已过期(默认 5 年),需重新签发
puppet cert clean agent1.cluster.com # Master
rm -rf /var/lib/puppet/ssl/* # Agent
puppet agent --test # Agent 重新申请
puppet cert sign agent1.cluster.com # Master 签名
# ⑥ 检查所有证书的过期时间(提前预警)
for f in /var/lib/puppet/ssl/ca/signed/*.pem; do
echo -n "$(basename $f .pem): "
openssl x509 -in "$f" -noout -enddate
done
详见 08-03-时间同步与Xinetd.md
14.2.5 CSR 已存在冲突
Error: Could not request certificate: Error 400 on SERVER:
CSR for host "agent1.cluster.com" has already been signed / already exists
# Master 上清掉旧的 CSR 与证书
puppet cert clean agent1.cluster.com
ls /var/lib/puppet/ssl/ca/requests/
rm -f /var/lib/puppet/ssl/ca/requests/agent1.cluster.com.pem
# Agent 上清本地
rm -rf /var/lib/puppet/ssl/*
puppet agent --test
14.3 Could not find class(模块路径与命名问题)
Error: Could not find class httpd for agent1.cluster.com on node agent1.cluster.com
Error: Evaluation Error: Error while evaluating a Function Call, Could not find class ::httpd
排查清单:
# ① modulepath 配置是否正确、模块是否在该路径下
puppet config print modulepath
puppet config print modulepath --section master
ls /etc/puppet/modules/
# ② 类名与文件路径是否符合自动加载规则
# class httpd → httpd/manifests/init.pp
# class httpd::config → httpd/manifests/config.pp
ls -l /etc/puppet/modules/httpd/manifests/init.pp
grep -n '^class' /etc/puppet/modules/httpd/manifests/init.pp
# init.pp 中的类名必须严格等于模块名
# ③ 模块名/类名是否含非法字符(横线、大写字母)
# 错误:my-module、MyModule 正确:my_module
ls /etc/puppet/modules/ | grep -- '-'
# ④ 目录/文件权限,puppet 用户能否读
ls -ld /etc/puppet/modules /etc/puppet/modules/httpd /etc/puppet/modules/httpd/manifests
chown -R puppet /etc/puppet/modules/
find /etc/puppet/modules -type d -exec chmod 755 {} \;
find /etc/puppet/modules -type f -exec chmod 644 {} \;
# ⑤ 语法错误导致类未被解析
puppet parser validate /etc/puppet/modules/httpd/manifests/init.pp
# ⑥ 环境不对:Agent 用的环境里没有这个模块
puppet config print environment --section agent
ls /etc/puppetlabs/code/environments/production/modules/
# ⑦ 4.x:environment.conf 中 modulepath 是否包含模块所在目录
cat /etc/puppetlabs/code/environments/production/environment.conf
# ⑧ 列出 Puppet 实际能看到的模块
puppet module list
puppet module list --environment production
常见根因总结:
| 现象 | 根因 |
|---|---|
| 类名与模块名不一致 | init.pp 里写 class apache 但目录叫 httpd |
| 文件放错位置 | 把 config.pp 放在 manifests/ 之外 |
| 模块名带横线 | my-app 应改为 my_app |
| modulepath 没配 | puppet.conf 的 [main] 缺 modulepath |
| 权限问题 | 模块目录属主不是 puppet |
| 环境放错 | 模块放在 development,Agent 用 production |
14.4 Duplicate declaration(重复声明资源)
Error: Duplicate declaration: File[/etc/httpd/conf/httpd.conf] is already declared
in file /etc/puppet/modules/httpd/manifests/init.pp:15;
cannot redeclare at /etc/puppet/modules/webserver/manifests/init.pp:8
Error: Duplicate declaration: Package[httpd] is already declared ...
原因:同一个资源(类型 + 标题)在同一个 catalog 中被声明了两次。Puppet 要求每个资源全局唯一。
常见场景与解决:
| 场景 | 解决方案 |
|---|---|
两个模块都声明了 package { 'httpd': } |
把公共资源抽到一个独立类(如 base::packages),两边都 include 它 |
两个模块都声明了 package { 'wget': } |
用 ensure_packages(['wget'])(stdlib)或 ensure_resource('package', 'wget', {...}) |
define 中的资源标题没含 $name |
资源标题必须写成 "/tmp/${name}.txt" |
同一个类被 class {} 声明了两次 |
改用 include(幂等),或只在一处声明 |
node 块和 class 里都声明了同一资源 |
统一放到 class 里 |
用 file 和 file_line 同时管一个文件 |
只用一种方式 |
# 解决方案①:抽公共类
class base::packages {
package { ['httpd', 'wget', 'vim-enhanced']:
ensure => installed,
}
}
class httpd {
include base::packages # 不再自己声明 package
file { '/etc/httpd/conf/httpd.conf': ... }
}
class webserver {
include base::packages
# ...
}
# 解决方案②:用 ensure_resource / ensure_packages(stdlib)
class module_a {
ensure_packages(['wget', 'curl'], { 'ensure' => 'installed' })
}
class module_b {
ensure_packages(['wget', 'unzip'], { 'ensure' => 'installed' }) # wget 不会冲突
}
# ensure_resource 也可用于 file/user 等
ensure_resource('file', '/opt/data', { 'ensure' => 'directory' })
# 解决方案③:用虚拟资源(virtual resource),声明多次实现一次
class allusers {
@user { 'zhangsan': ensure => present, uid => 3101 }
@user { 'lisi': ensure => present, uid => 3102 }
@user { 'wangwu': ensure => present, uid => 3201 }
}
class webserver {
include allusers
realize(User['zhangsan'], User['lisi']) # 只实现需要的
}
class dbserver {
include allusers
User <| uid > 3200 |> # 用收集器按条件实现
}
定位重复声明的位置:
# 报错信息本身会给出两个文件和行号,直接去看
# 也可以搜索
grep -rn "package { 'httpd'" /etc/puppet/modules/
grep -rn 'httpd.conf' /etc/puppet/modules/ /etc/puppet/manifests/
# 在 Master 上编译某节点的 catalog 查看完整报错
puppet catalog find agent1.cluster.com --terminus compiler
14.5 依赖环(dependency cycle)
Error: Could not apply complete catalog: Found 1 dependency cycle:
(File[/etc/app.conf] => Service[app] => File[/etc/app.conf])
Try the '--graph' option and opening the resulting '.dot' file in OmniGraffle or GraphViz
定位方法:
# ① 直接看报错括号里的环,报错已给出完整链路
# ② 生成关系图
puppet agent --test --graph
ls -l /var/lib/puppet/state/graphs/
# cycles.dot expanded_relationships.dot relationships.dot resources.dot
# 4.x+ 路径
ls -l /opt/puppetlabs/puppet/cache/state/graphs/
# ③ 转成图片
yum install -y graphviz
dot -Tpng /var/lib/puppet/state/graphs/cycles.dot -o /tmp/cycles.png
dot -Tsvg /var/lib/puppet/state/graphs/relationships.dot -o /tmp/rel.svg
# ④ 直接看 dot 文件内容(小规模够用)
cat /var/lib/puppet/state/graphs/cycles.dot
常见根因:
# ① 同时写了 require 和 before 指向对方
file { '/etc/app.conf':
require => Service['app'], # 错:配置依赖服务
notify => Service['app'], # 又通知服务 → 成环
}
# 修复:删掉 require,只保留 notify
# ② 类之间双向依赖
class a { include b; Class['b'] -> Class['a'] } # 错
class b { include a }
# ③ exec 的 require 指向了会 notify 自己的资源
exec { 'reload':
refreshonly => true,
subscribe => File['/etc/app.conf'],
require => File['/etc/app.conf'], # 冗余但不成环
before => File['/etc/app.conf'], # 错:成环
}
# ④ 定义类型内部引用了外部会依赖自己的资源
14.6 exec 不幂等(每次运行都执行)
现象:每次 puppet agent --test 都看到同样的 exec 被执行,服务被反复重启,配置行被反复追加。
Notice: /Stage[main]/Httpd/Exec[exec-shell]/returns: executed successfully
每一次运行都出现上面这行,就说明不幂等。
解决:给 exec 加幂等守卫
# ❌ 不幂等
exec { 'exec-shell':
cwd => '/tmp',
command => 'sh 1.sh',
path => '/bin:/sbin:/usr/bin:/usr/sbin',
}
# ✅ 用 creates:产物存在就跳过
exec { 'exec-shell':
cwd => '/tmp',
command => 'sh 1.sh',
path => '/bin:/sbin:/usr/bin:/usr/sbin',
creates => '/web/index.html',
}
# ✅ 用 unless:条件检查
exec { 'add-config-line':
command => 'echo "max_conn=100" >> /etc/app.conf',
provider => shell,
unless => 'grep -q "^max_conn=" /etc/app.conf',
}
# ✅ 用 onlyif
exec { 'fix-perm':
command => 'chmod 600 /etc/app/secret',
path => '/bin:/usr/bin',
onlyif => 'test $(stat -c %a /etc/app/secret) != "600"',
}
# ✅ 用 refreshonly:只在被通知时执行
exec { 'reload-config':
command => 'systemctl reload app',
path => '/bin:/usr/bin',
refreshonly => true,
subscribe => File['/etc/app.conf'],
}
# ✅ 最佳方案:根本不用 exec,改用原生资源
file { '/web/index.html':
ensure => file,
content => template('httpd/index.html.erb'),
}
验证幂等性的方法:连续跑两次 puppet agent --test,第二次的输出中不应有任何 changed / executed 行。
puppet agent --test
puppet agent --test | grep -E 'changed|executed|Notice: /Stage'
# 第二次应该没有任何输出(除了 "Applied catalog in x.xx seconds")
14.7 文件权限与 SELinux 导致 file 资源失败
Error: /Stage[main]/Httpd/File[/etc/httpd/conf/httpd.conf]: Could not evaluate:
Could not retrieve information from environment production source(s)
puppet:///modules/httpd/httpd.conf
Error: Failed to generate additional resources using 'eval_generate': Permission denied
Notice: /File[/var/www/html/index.html]/seluser: seluser changed 'unconfined_u' to 'system_u'
排查:
# ① Master 端:模块文件是否存在、路径是否正确
# puppet:///modules/httpd/httpd.conf → /etc/puppet/modules/httpd/files/httpd.conf
ls -l /etc/puppet/modules/httpd/files/httpd.conf
# 注意 URL 中不写 files/ 这一层,这是最常见的写错点
# ② Master 端:puppet 用户能否读
sudo -u puppet cat /etc/puppet/modules/httpd/files/httpd.conf
chown -R puppet /etc/puppet/modules/
find /etc/puppet/modules -type d -exec chmod 755 {} \;
find /etc/puppet/modules -type f -exec chmod 644 {} \;
# ③ Agent 端:目标目录是否存在、是否可写
ls -ld /etc/httpd/conf
# 若父目录不存在,需先声明 file { '/etc/httpd/conf': ensure => directory }
# ④ SELinux 拦截
getenforce
ls -Z /var/www/html/index.html
ausearch -m avc -ts recent
grep -i denied /var/log/audit/audit.log | tail -20
# 临时验证
setenforce 0
puppet agent --test
setenforce 1
# 正确修复:恢复默认上下文
restorecon -Rv /var/www/html
# 或在 file 资源中显式指定
file { '/var/www/html/index.html':
ensure => file,
content => "hello\n",
owner => 'apache',
group => 'apache',
mode => '0644',
seltype => 'httpd_sys_content_t',
seluser => 'system_u',
selrole => 'object_r',
}
# 非标准目录做 DocumentRoot 时,还要放开 SELinux
selboolean { 'httpd_read_user_content':
value => on,
persistent => true,
}
exec { 'semanage-web':
command => 'semanage fcontext -a -t httpd_sys_content_t "/web(/.*)?" && restorecon -Rv /web',
path => '/usr/sbin:/usr/bin:/sbin:/bin',
provider => shell,
unless => 'semanage fcontext -l | grep -q "^/web"',
require => File['/web'],
}
详见 07-03-防火墙与SELinux.md
14.8 --noop 与实际执行结果不一致
现象:--noop 说会做 3 处变更,实际执行时做了 5 处,或者反过来。
原因分析:
| 原因 | 说明 |
|---|---|
| exec 的不可预测性 | --noop 不会真的执行 exec 的 command,因此后续依赖 exec 产物的资源状态无法预测 |
| 级联效应 | 例如 package 在 noop 下没真装,安装后才会出现的配置文件目录在 noop 时不存在,相关 file 资源的判断就不准 |
| notify/refresh 链 | noop 下服务不会真的重启,refresh 的连带效果看不到 |
| 动态生成的资源 | eval_generate(如 recurse => true 的目录)在 noop 下枚举结果可能不同 |
| 外部状态变化 | 两次运行之间系统被别人改了 |
noop => false 覆盖 |
某个资源显式写了 noop => false,在全局 noop 时仍会真执行 |
正确的使用方式:
# ① noop 只用于"评估影响范围",不要指望 100% 精确
puppet agent --test --noop --show_diff
# ② 分阶段验证:先 noop,再在一台机器上真跑,确认无误后全量
puppet agent --test --tags httpd --noop # 只看 httpd 相关
puppet agent --test --tags httpd # 只执行 httpd 相关
# ③ 用退出码判断
puppet agent --test --noop --detailed-exitcodes
echo $? # 2 = 有待变更;0 = 无变更
# ④ 对 exec 尽量写清 unless/onlyif/creates,让 noop 能正确判断
# ⑤ 单个资源强制真执行(谨慎)
exec { 'critical-fix':
command => '/usr/local/bin/fix.sh',
noop => false, # 即使全局 --noop 也会真执行
creates => '/var/lib/fixed',
}
14.9 Agent 运行成功但没有任何变更(节点没匹配到 class)
现象:puppet agent --test 输出正常、无报错,但什么都没做:
Info: Retrieving pluginfacts
Info: Retrieving plugin
Info: Caching catalog for agent1.cluster.com
Info: Applying configuration version '1755840000'
Notice: Applied catalog in 0.03 seconds
排查清单(这是最容易困惑的问题):
# ① 【最常见】节点匹配规则的排他性
# node 匹配只生效一个块:精确名 > 正则 > default
# 若同时存在 node 'agent1.cluster.com' {} 和 node /\.cluster\.com$/ {}
# 则 agent1 只执行精确匹配的那个块,正则块里的 include 完全不生效
grep -n '^node' /etc/puppet/manifests/site.pp /etc/puppet/manifests/nodes.pp
# 修复:把公共配置抽成 base 类,每个 node 块都 include base
# ② 节点名(certname)与 node 块里写的名字是否一致
puppet config print certname # Agent 上
facter fqdn
puppet cert list --all # Master 上看实际签发的名字
# 若 certname 是 agent1(短名)而 node 写的是 agent1.cluster.com,就匹配不到
# ③ site.pp 是否被 Master 真正读取(路径是否对)
puppet config print manifest --section master
ls -l /etc/puppet/manifests/site.pp
# 4.x+:确认在正确的环境目录下
ls -l /etc/puppetlabs/code/environments/production/manifests/
# ④ Puppet 3.x:import 'nodes.pp' 是否写了、文件是否存在
grep -n 'import' /etc/puppet/manifests/site.pp
ls -l /etc/puppet/manifests/nodes.pp
# ⑤ 4.x:是否还在用已被移除的 import(会静默不生效或报错)
# 修复:删掉 import,把 nodes.pp 直接放在 manifests/ 目录下自动加载
# ⑥ Agent 用的环境里有没有代码
puppet config print environment --section agent
ls /etc/puppetlabs/code/environments/$(puppet config print environment)/manifests/
# ⑦ 在 Master 上直接编译该节点的 catalog,看里面到底有什么资源
puppet catalog find agent1.cluster.com --terminus compiler | python -m json.tool | grep '"title"'
# 若 catalog 里只有 Stage/Class[main]/Class[Settings],说明确实没匹配到任何 class
# ⑧ 用 --debug 看节点匹配过程
puppet agent --test --debug 2>&1 | grep -iE 'node|class|catalog'
# ⑨ ENC 返回空
/usr/local/bin/enc.sh agent1.cluster.com # 手工执行 ENC 脚本看输出
# ⑩ 是否被 --tags 限制了
puppet agent --test # 不加 --tags 再试一次
典型修复示例:
# ❌ 错误:agent1 只会执行第一个块,include sudo 不生效
node 'agent1.cluster.com' {
file { '/tmp/a': content => "a\n" }
}
node /\.cluster\.com$/ {
include sudo
include httpd
}
# ✅ 正确:把公共配置抽成类
class base {
include sudo
include ntp
include hosts
}
node 'agent1.cluster.com' {
include base
include httpd
file { '/tmp/a': content => "a\n" }
}
node 'agent2.cluster.com' {
include base
include mysql
}
node default {
include base
}
14.10 Run of Puppet configuration client already in progress
Notice: Run of Puppet configuration client already in progress; skipping
(/var/lib/puppet/state/agent_catalog_run.lock exists)
原因:Agent 守护进程正在运行(或上次运行异常退出留下了 lock 文件),同一时刻只允许一个 Puppet 运行实例。
处理方式:
# ① 确认是否真有 puppet 进程在跑
ps -ef | grep -v grep | grep puppet
# ② 若确实没有进程,删除残留的 lock 文件
rm -rf /var/lib/puppet/state/agent_catalog_run.lock # 3.x
rm -rf /opt/puppetlabs/puppet/cache/state/agent_catalog_run.lock # 4.x+
# ③ 停掉守护进程再手工运行
systemctl stop puppet
systemctl stop puppetagent
puppet agent --test --server master.cluster.com
# ④ 根据红色错误提示排错,完成后再启动服务
systemctl start puppet
systemctl start puppetagent
# ⑤ 若有 disabled 锁(被 puppet agent --disable 禁用过)
cat /var/lib/puppet/state/agent_disabled.lock
puppet agent --enable # 解除禁用
puppet agent --disable "maintenance window" # 主动禁用(维护窗口用)
14.11 puppet.conf 改了不生效
现象:改了 puppet.conf 中的 runinterval、server、modulepath、autosign,但行为没变。
原因与解决:
# ① 【核心】puppet.conf 修改后必须重启对应服务
systemctl restart puppetmaster # Master(3.x)
systemctl restart puppetserver # Master(4.x+),JVM 重启较慢
systemctl restart puppet # Agent
# 对比:manifest / 模块代码修改【不需要】重启,因为 catalog 是实时编译的
# ② 改错了段(section)
# server 要写在 [main] 或 [agent],写在 [master] 无效
# autosign / reports 要写在 [master],写在 [agent] 无效
# manifest / modulepath 要写在 [main] 或 [master]
# ③ 改错了文件(3.x 与 4.x 路径不同)
puppet config print config # 查看实际使用的配置文件路径
# 3.x: /etc/puppet/puppet.conf
# 4.x: /etc/puppetlabs/puppet/puppet.conf
# ④ 用 puppet config print 验证参数是否真的生效
puppet config print runinterval --section agent
puppet config print server --section agent
puppet config print modulepath --section master
puppet config print autosign --section master
puppet config print all | grep -i interval
# ⑤ 用 puppet config set 修改(自动写对位置)
puppet config set server master.cluster.com --section agent
puppet config set runinterval 1800 --section agent
puppet config set autosign /etc/puppet/autosign.conf --section master
# ⑥ 语法错误(INI 格式,段名要有方括号,等号两边空格无所谓)
# 检查是否有 Tab 混用、中文全角符号、多余引号
# ⑦ environment_timeout 缓存导致代码变更不生效(4.x+)
# environment.conf 中设 environment_timeout = 0(开发)
# 或手工清缓存
curl -k -X DELETE --cert /etc/puppetlabs/puppet/ssl/certs/master.cluster.com.pem \
--key /etc/puppetlabs/puppet/ssl/private_keys/master.cluster.com.pem \
--cacert /etc/puppetlabs/puppet/ssl/certs/ca.pem \
https://master.cluster.com:8140/puppet-admin-api/v1/environment-cache
14.12 日志在哪看与 --debug 的使用
日志位置汇总:
| 内容 | Puppet 3.x | Puppet 4.x+ |
|---|---|---|
| Master 应用日志 | /var/log/puppet/masterhttp.log |
/var/log/puppetlabs/puppetserver/puppetserver.log |
| Master 访问日志 | - | /var/log/puppetlabs/puppetserver/puppetserver-access.log |
| Master GC/JVM | - | /var/log/puppetlabs/puppetserver/puppetserver_gc.log |
| Agent 日志 | /var/log/messages(syslog) |
/var/log/messages、journalctl -u puppet |
| 报告 YAML | /var/lib/puppet/reports/<certname>/ |
/opt/puppetlabs/puppet/cache/reports/<certname>/ |
| 关系图 | /var/lib/puppet/state/graphs/ |
/opt/puppetlabs/puppet/cache/state/graphs/ |
| 最后一次运行摘要 | /var/lib/puppet/state/last_run_summary.yaml |
/opt/puppetlabs/puppet/cache/state/last_run_summary.yaml |
| 最后一次运行报告 | /var/lib/puppet/state/last_run_report.yaml |
/opt/puppetlabs/puppet/cache/state/last_run_report.yaml |
| PuppetDB | - | /var/log/puppetlabs/puppetdb/puppetdb.log |
常用查看命令:
# Agent 日志(syslog)
tail -f /var/log/messages | grep -i puppet
journalctl -u puppet -f
journalctl -u puppet --since '10 min ago'
# Master 日志
tail -f /var/log/puppetlabs/puppetserver/puppetserver.log
journalctl -u puppetserver -f
tail -f /var/log/messages | grep -i puppet-master
# 最后一次运行摘要(快速判断有多少变更/失败)
cat /var/lib/puppet/state/last_run_summary.yaml
--debug 的使用(排错最强武器):
# 完整调试输出:显示 HTTP 请求、fact 加载、每个资源的比对细节、provider 选择
puppet agent --test --debug
# 输出量很大,建议重定向后过滤
puppet agent --test --debug > /tmp/puppet-debug.log 2>&1
grep -iE 'error|warn|fail' /tmp/puppet-debug.log
grep -i 'Executing' /tmp/puppet-debug.log # 看实际执行的命令
grep -i 'Facter' /tmp/puppet-debug.log # 看 fact 加载
grep -i 'https://' /tmp/puppet-debug.log # 看请求了哪些 URL
grep -i 'file_metadata' /tmp/puppet-debug.log # 看文件分发查找过程
# 组合参数
puppet agent --test --debug --show_diff --trace --evaltrace
# --trace 出错时打印 Ruby 调用栈
# --evaltrace 显示每个资源的求值耗时(找慢资源)
# 只调试特定标签的资源
puppet agent --test --debug --tags httpd
# Master 端调试:直接编译某节点的 catalog
puppet master --debug --no-daemonize # 3.x,前台跑 master
puppet catalog find agent1.cluster.com --terminus compiler --debug
# 单独测试一个资源
puppet resource file /etc/httpd/conf/httpd.conf
puppet resource service httpd
puppet resource package httpd
puppet resource user zhangsan
# 甚至可以直接修改
puppet resource service httpd ensure=running enable=true
# 查看某资源类型支持的所有属性
puppet describe file
puppet describe exec
puppet describe service --list # 列出所有资源类型
14.13 其他常见问题
| 报错 / 现象 | 原因与解决 |
|---|---|
Error 400 on SERVER: Not authorized to call find on /file_metadata/... |
fileserver.conf 或 auth.conf 未放行;改用 puppet:///modules/ 三段式路径 |
Could not find data item xxx in any Hiera data file |
Hiera 键不存在且没给默认值;用 puppet lookup xxx --explain 排查层级 |
Failed to apply catalog: Parameter mode failed: mode must be a valid octal number |
Puppet 4.x 中 mode 必须写成字符串 '0644',不能写 0644 数字 |
Error: Could not find command 'xxx' |
exec 没设 path,或命令未安装;写绝对路径或补 path |
Error: Systemd start for xxx failed! |
服务本身启动失败;到 Agent 上 systemctl status xxx -l 和 journalctl -xe 看真实原因 |
Package[xxx]: Could not evaluate: Execution of '/usr/bin/yum -d 0 -e 0 -y install xxx' returned 1 |
yum 源问题;先在 Agent 上手工 yum install xxx 验证 |
| Master 内存耗尽 / OOM(4.x+) | puppetserver 是 JVM 应用,调 /etc/sysconfig/puppetserver 的 JAVA_ARGS |
| Agent 运行很慢(几十秒到几分钟) | 用 --evaltrace 找慢资源;常见是 recurse => true 的大目录、ensure => latest 的包、exec 超时 |
| WEBrick 撑不住(3.x) | WEBrick 单线程只能撑约 10 个 agent,生产必须换 Passenger + Apache 或升级到 puppetserver |
| 多个 Agent 同时打爆 Master | 开启 splay = true 与 splaylimit,或错开 runinterval |
Warning: Local environment: "production" doesn't match server specified node environment "development" |
Agent 与 ENC/Master 指定的环境不一致;统一 environment 配置 |
| 改了 fact 但 manifest 里取到旧值 | Agent 端 fact 缓存或 pluginsync 未同步;rm -rf $vardir/facts.d 后重跑 |
练习
一、基础概念题
1. 为什么需要配置管理工具?手工 SSH 或 shell 脚本批量运维存在哪两个主要问题,如何解决?
答:手工/脚本运维的两个主要问题是:
- 密码问题:需要在每台机器上输入密码。解决办法:用
ssh-keygen生成空密钥做免密登录,或用expect自动应答密码。 - 循环效率问题:10000 台机器每台操作 10 秒,串行需要 100000 秒。解决办法:shell 并发执行(把循环体放后台)。
除此之外,脚本方式还有一致性无法保证、不可重复执行、不可审计、系统差异难处理等更深层问题,这才是配置管理工具的真正价值所在。
2. 写一段 shell 并发执行的示例代码。
答:
#!/bin/bash
for i in `seq 10`
do
echo $i
sleep 1
done & # 加后台符号与不加效果不一样,可自行测试
for j in `seq 11 20`
do
echo $j
sleep 1
done
3. 使用集中化配置管理工具通常需要关注哪些点?分别如何解决?
答:
| 关注点 | 解决思路 |
|---|---|
| 大并发 | 使用 MQ 消息队列 |
| 操作系统/版本差异 | 通过 facter 等工具获取客户端信息,按系统分支处理 |
| 异常处理 | 日志、报告 |
| 分组机制 | 按角色、正则、节点分组 |
| Web 管理界面 | dashboard |
| 操作语言 | 自有的 DSL 或声明式语言 |
4. 列举常见的自动化运维工具。
答:Cobbler、Shell 脚本、Puppet、Ansible、SaltStack、Chef、Python 自研。
5. 什么是声明式?什么是幂等性?为什么说 exec 资源是 Puppet 中破坏幂等性的主要来源?
答:
- 声明式:只描述"期望的最终状态"(desired state),由工具自己检查当前状态、计算差异、只做必要的修改。与之相对的命令式(如 shell 脚本)描述"一步一步怎么做"。
- 幂等性:同一操作执行 1 次和执行 N 次的系统最终状态相同,且不产生额外副作用。例如
mkdir /test不幂等(第二次报错),mkdir -p /test幂等。 - exec 破坏幂等性的原因:exec 执行的是裸命令,Puppet 无法知道这条命令做了什么、当前状态是否已经满足,因此默认每次运行(默认每 30 分钟一次)都会重新执行。必须用
onlyif、unless、creates、refreshonly四种幂等守卫之一来约束。
6. Puppet 是什么架构?master 和 agent 各自的作用是什么?默认端口和同步间隔是多少?
答:Puppet 是 C/S(Master/Agent)架构。
- master:管理端,集中编写配置清单(manifest),监听 8140 端口,兼任 CA;
- agent:被管理端,周期性(默认 30 分钟,由
runinterval控制)从 master 拉取配置并在本机应用。
管理员在 master 上写声明,由 agent 负责真正落地。
7. 简述 Puppet 的运行流程五步。
答:
- Agent 收集 Facts:运行 facter 采集本机 OS、版本、主机名、IP、内存等信息;
- 发送给 Master:通过 HTTPS(8140)把 facts 发给 Master,同时完成 SSL 双向认证;
- Master 编译 Catalog:根据 facts 匹配 node 定义,读取对应的 class/模块/Hiera 数据,把声明式代码编译成只属于该节点的资源清单(Catalog);
- 下发 Catalog:Master 把 catalog 返回给 Agent;
- Agent 应用并回报 Report:Agent 逐个资源比对当前状态与期望状态,只修改不一致的部分,然后生成 report 回报给 Master。
8. 为什么 Puppet 需要 SSL 证书?CA 的角色是什么?首次连接的证书签发流程是怎样的?
答:
- 为什么需要:Master 会把整个环境的配置(可能含密码、密钥)下发给 Agent。若任何机器都能冒充某节点请求 catalog,就能窃取配置;若 Agent 不验证 Master,也可能被中间人下发恶意配置。因此需要 SSL 双向认证(mutual TLS)。
- CA 的角色:Puppet Master 默认自带一个 CA,所有 Agent 的证书都由它签发,双方通过"都信任同一个 CA"实现互相验证。
- 签发流程:① Master 首次启动生成 CA 与自身服务器证书;② Agent 首次运行
puppet agent --test,生成私钥和 CSR 发给 Master,同时下载 CA 证书;③ Master 上puppet cert list查看待签,puppet cert sign <name>签发;④ Agent 再次运行,取回自己的证书,建立双向 SSL 连接。
9. facter 的作用是什么?
答:facter 是 Puppet 提供的一个包/命令,用于采集被管理机器的操作系统、版本、IP、内存、CPU 等信息(称为 fact)。这些 fact 作为顶层变量供 manifest 使用,管理员据此写出差异化配置(如按 $::osfamily 区分 RedHat 与 Debian,按 $::operatingsystemmajrelease 区分 CentOS 6 与 7)。
10. Puppet 3.x 与 4.x+ 的目录结构、包名、命令有哪些差异?
答:
| 项目 | Puppet 3.x | Puppet 4.x+(AIO) |
|---|---|---|
| 配置目录 | /etc/puppet/ |
/etc/puppetlabs/puppet/ |
| manifest 入口 | /etc/puppet/manifests/site.pp |
/etc/puppetlabs/code/environments/production/manifests/site.pp |
| 模块目录 | /etc/puppet/modules/ |
.../environments/production/modules/ |
| SSL 目录 | /var/lib/puppet/ssl/ |
/etc/puppetlabs/puppet/ssl/ |
| 数据目录 | /var/lib/puppet/ |
/opt/puppetlabs/puppet/cache/ |
| 可执行文件 | /usr/bin/puppet |
/opt/puppetlabs/bin/puppet |
| Master 包名/服务名 | puppet-server / puppetmaster |
puppetserver / puppetserver |
| Agent 包名/服务名 | puppet / puppet(或 puppetagent) |
puppet-agent / puppet |
| Web 服务器 | WEBrick(测试)/ Passenger(生产) | 内置 Jetty(JVM,生产可用) |
| 证书命令 | puppet cert list/sign/clean |
puppetserver ca list/sign/clean |
import 关键字 |
支持(已废弃) | 已移除 |
二、环境与安装题
11. 部署 Puppet 实验环境需要做哪些准备工作?画出实验环境规划表。
答:
| 角色 | 主机名 | IP |
|---|---|---|
| Master | master.cluster.com | 10.1.1.2 |
| Agent1 | agent1.cluster.com | 10.1.1.3 |
| Agent2 | agent2.cluster.com | 10.1.1.4 |
准备要求:
- 主机名及互相绑定
/etc/hosts - 时间同步
- 静态 IP
- 关闭 firewalld、SELinux
- 配置 yum 源(本地 ISO、163、EPEL、Puppet 官方源)
12. 为什么主机名和 /etc/hosts 是最常见的故障源?
答:因为 Puppet 的证书 CN 就是主机名(certname,默认 FQDN),Agent 必须用名字(不能用 IP,不建议用别名)连接 Master,Master 也要能反查 Agent 的名字。若解析不正确会导致:连接失败(getaddrinfo: Name or service not known)、证书 CN 不匹配(hostname "master" does not match the server certificate)、克隆虚拟机主机名重复导致证书冲突。所有节点的 /etc/hosts 都要写全集群的解析记录。
13. 时间同步为什么对 Puppet 至关重要?
答:SSL 证书带有生效时间(notBefore)和过期时间(notAfter):
- Agent 时间比 Master 慢很多 → 证书"尚未生效"(
certificate is not yet valid); - Agent 时间比 Master 快很多 → 证书"已过期"(
certificate has expired)。
两种情况都导致 SSL 握手失败,且报错很容易被误判为"证书没签"。因此必须用 chrony(CentOS 7)或 ntpd(CentOS 6)保持两端时间同步,并统一时区。
14. 写出 Puppet 官方 yum 源的配置。
答:
[puppet]
name=puppet
baseurl=http://yum.puppetlabs.com/el/7Server/products/x86_64/
enabled=1
gpgcheck=0
[puppet-dependencies]
name=puppet-dependencies
baseurl=http://yum.puppetlabs.com/el/7Server/dependencies/x86_64/
enabled=1
gpgcheck=0
教学环境本地源(已将两个仓库合并):
[puppet]
name=puppet
baseurl=http://10.1.1.1/puppet
enabled=1
gpgcheck=0
15. 写出 Master 端和 Agent 端的安装与启动命令,以及如何验证端口。
答:
# Master 端
yum install puppet-server -y
systemctl start puppetmaster
systemctl enable puppetmaster
systemctl status puppetmaster
lsof -i:8140
# Agent 端
yum install puppet -y
systemctl start puppet
systemctl enable puppet
systemctl status puppet
# 部分版本 Agent 服务名为 puppetagent
systemctl start puppetagent
systemctl enable puppetagent
systemctl status puppetagent
16. Agent 与 Master 的版本有什么一致性要求?
答:Agent 的版本不应高于 Master 的版本。Master 负责编译 catalog,若 Agent 比 Master 新,可能请求 Master 不支持的 API 版本或语言特性导致失败;反之(Agent 略旧)通常兼容。同一环境内 Agent 版本应尽量统一。3.x 的 Agent 不能连 4.x 的 Master,跨大版本升级必须先升 Master 再升 Agent。
三、证书管理题
17. 完整写出 Master 与 Agent 建立连接的四步操作。
答:
# 第一步:Agent 上配置 /etc/puppet/puppet.conf 的 [main] 段
# server = master.cluster.com
# 第二步:Agent 发起证书请求
puppet agent --server=master.cluster.com --test
# 此时 /var/lib/puppet/ssl/certs/ 下只有 ca.pem
# 第三步:Master 上查看并签名
puppet cert list
puppet cert --sign --all
# 或签单个:puppet cert sign agent1.cluster.com
# 第四步:Agent 再次运行,取回自己的证书
puppet agent --server=master.cluster.com --test
# 此时 /var/lib/puppet/ssl/certs/ 下多出 agent1.cluster.com.pem
18. 如果证书出问题需要重建连接,完整步骤是什么?
答:必须两端都清理后重新签发:
# ① Master 上清除该 Agent 的证书
puppet cert clean agent1.cluster.com
puppet cert clean agent2.cluster.com
# ② Agent 上删除本地证书
systemctl stop puppet
rm /var/lib/puppet/ssl/certs/* -rf
# 彻底重置建议删整个 ssl 目录:rm -rf /var/lib/puppet/ssl/*
# ③ Agent 重新发起请求
puppet agent --server=master.cluster.com --test
# ④ Master 重新签名
puppet cert list
puppet cert --sign --all
# ⑤ Agent 再次运行取回证书
puppet agent --server=master.cluster.com --test
# ⑥ 恢复守护进程
systemctl start puppet
19. 写出 Puppet 3.x 与 6.x 的证书命令对照表。
答:
| 功能 | Puppet 3.x | Puppet 6.x+ |
|---|---|---|
| 查看待签证书 | puppet cert list |
puppetserver ca list |
| 查看全部证书 | puppet cert list --all |
puppetserver ca list --all |
| 签发单个 | puppet cert sign <name> |
puppetserver ca sign --certname <name> |
| 批量签发 | puppet cert --sign --all |
puppetserver ca sign --all |
| 吊销清除 | puppet cert clean <name> |
puppetserver ca clean --certname <name> |
20. autosign 自动签名怎么配置?有什么安全风险?
答:
配置方式:
# 无条件自动签名(极不安全,仅限隔离实验环境)
[master]
autosign = true
# 白名单文件(推荐)
[master]
autosign = /etc/puppet/autosign.conf
# /etc/puppet/autosign.conf
agent1.cluster.com
*.cluster.com
安全风险:
- 开启
autosign = true后,任何能连上 8140 的机器都能声称自己是任意节点名(如冒充db-master)并拿到该节点的完整 catalog; - catalog 里往往包含数据库密码、SSH 密钥、API token → 直接导致配置泄露;
- 攻击者可抢先注册合法节点即将使用的名字,造成正常节点无法上线(拒绝服务)。
结论:生产环境禁止 autosign = true,至少用域名白名单,最好用带预共享密钥的策略脚本,并把 8140 限制在内网。
四、Manifest 与资源题
21. 写一个 manifest,让所有节点在 /tmp/test.txt 中生成内容 hello,world。site.pp 修改后需要重启 master 吗?
答:
# /etc/puppet/manifests/site.pp
node default { # default 节点代表所有节点
file { "/tmp/test.txt": # 资源 title,未使用 path 参数时即文件路径
content => "hello,world\n", # 文件内容,文件不存在会自动创建
}
}
site.pp 修改后立即生效,无需重启 master(catalog 是每次请求时现场编译的)。客户端验证:
puppet agent --test --server master.cluster.com
cat /tmp/test.txt
# hello,world
22. Agent 默认 30 分钟同步一次,如何改成 3 秒?
答:在客户端 /etc/puppet/puppet.conf 的 [agent] 段加入:
runinterval = 3
然后重启服务:
systemctl restart puppet
systemctl restart puppetagent
注意:这仅用于实验,生产环境绝不可这样设,会打爆 Master。
23. 分别为 agent1 和 agent2 在 /tmp/test.txt 中写入不同内容。
答:
node 'agent1.cluster.com' { # 只针对 agent1,节点名用单引号
file { "/tmp/test.txt":
content => "hahahaha\n",
}
}
node 'agent2.cluster.com' { # 只针对 agent2
file { "/tmp/test.txt":
content => "hehehehe\n",
}
}
24. 删除 agent1 和 agent2 上的 /tmp/test.txt。删除目录需要注意什么?
答:
node 'agent1.cluster.com' {
file { "/tmp/test.txt":
ensure => absent, # 保证文件不存在,存在则删除
}
}
node 'agent2.cluster.com' {
file { "/tmp/test.txt":
ensure => absent,
}
}
删除目录需要加 force => yes,否则 Puppet 拒绝删除非空目录。
25. 用正则匹配一批节点,在它们上创建一个指向 /etc/fstab 的软链接 /tmp/fstab。
答:
# 匹配 agentX.cluster.com,X 为任意数字
# 也可写为 /^agent/ 或 /cluster\.com$/
node /^agent\d+\.cluster\.com/ {
file { "/tmp/fstab":
ensure => link, # 创建软链接
target => "/etc/fstab", # 源文件
}
}
26. 在所有 .cluster.com 节点上创建目录 /test/ 和文件 /test/abc(内容 haha,权限 4755,属主 bin,属组 daemon)。
答:
node /\.cluster\.com$/ {
file { "/test/":
ensure => directory,
}
file { "/test/abc":
content => "haha\n",
mode => 4755,
owner => bin,
group => daemon,
}
}
27. 把客户端本机的 /etc/fstab 复制为 /test/abc。
答:
node /^agent\d+\.cluster\.com/ {
file { "/test/":
ensure => directory,
}
file { "/test/abc":
source => "/etc/fstab",
mode => 4755,
owner => bin,
group => daemon,
}
}
注意:source 写本地绝对路径指的是 Agent 本机的路径。要从 Master 分发文件必须用 puppet:/// 协议。
28. 列出 file 资源的常用属性及说明。
答:
| 属性 | 说明 |
|---|---|
ensure |
present/absent/file/directory/link |
content |
文件内容(字符串) |
source |
源文件或 puppet:/// URL |
target |
软链接目标 |
mode |
权限,如 0755、4755 |
owner/group |
属主/属组 |
force |
删除目录等场景需设为 yes |
recurse |
递归处理目录 |
purge |
配合 recurse,删除不由 Puppet 管理的多余文件 |
backup |
备份策略 |
replace |
false 时只在文件不存在时创建 |
validate_cmd |
写入前用命令校验 |
29. 在 agent 上创建用户 user2 并生成家目录。
答:
node /^agent\d+\.cluster\.com/ {
user { "user2":
ensure => present,
managehome => true, # 创建家目录
}
}
30. 删除 user2(连家目录)、新增 user3(uid 505,shell 为 nologin)和组 group1(gid 520)。指定 uid 时要注意什么?
答:
node /^agent\d+\.cluster\.com/ {
user { "user2":
ensure => absent,
managehome => true, # 删用户时连同家目录一起删除
}
user { "user3":
ensure => present,
uid => 505,
shell => "/sbin/nologin",
}
group { "group1":
ensure => present,
gid => 520,
}
}
指定 uid 时不要随意写
gid,若该 gid 的组不存在会出错。 正确做法是先声明group资源,并用require => Group['xxx']建立依赖。
31. 在客户端创建一个 21:01 执行 /sbin/init 0 的定时任务。
答:
node /^agent\d+\.cluster\.com/ {
cron { "cron1": # 注释名称
command => "/sbin/init 0",
user => "root",
minute => "01",
hour => "21", # 21:01 执行;未写字段默认为 *
}
}
32. 让客户端的 sshd 停止且禁止自启,httpd 运行且开机自启。
答:
node /^agent\d+\.cluster\.com/ {
service { sshd:
ensure => "stopped", # 确保停止,启动了会被关闭
enable => false, # 相当于 systemctl disable sshd
}
service { httpd:
ensure => "running", # 确保运行,关闭了会被启动
enable => true, # 相当于 systemctl enable httpd
}
}
33. 在客户端创建 /shell/1.sh 脚本(内容为创建 /test 并生成 100 个文件)并用 exec 执行。这段代码幂等吗?如何改进?
答:
node /^agent\d+\.cluster\.com/ {
file { "/shell/":
ensure => directory,
}
file { "/shell/1.sh":
content => "mkdir -p /test\ntouch /test/{1..100}\n",
mode => 755,
owner => root,
group => root,
}
exec { "exec-shell":
cwd => "/shell",
command => "sh 1.sh",
user => "root",
path => "/bin:/sbin:/usr/local/bin:/usr/local/sbin:/usr/sbin:/usr/bin",
}
}
这段代码不幂等:每次 Puppet 运行都会重新执行 sh 1.sh。改进方式是加幂等守卫:
exec { "exec-shell":
cwd => "/shell",
command => "sh 1.sh",
user => "root",
path => "/bin:/sbin:/usr/local/bin:/usr/local/sbin:/usr/sbin:/usr/bin",
creates => "/test/100", # 产物存在就不再执行
# 或 unless => 'test -f /test/100'
}
34. 推送 yum 配置并安装 vsftpd。
答:
node /^agent\d+\.cluster\.com/ {
file { "/etc/yum.repos.d/rhel-source.repo":
content => "[server]\nname=server\nbaseurl=file:///yum\nenabled=1\ngpgcheck=0\n",
mode => 644,
owner => root,
group => root,
}
package { 'install vsftpd':
provider => yum,
name => vsftpd,
ensure => installed,
}
}
更规范的写法是用 yumrepo 资源替代 file 下发 repo 文件。
35. 用 mount 资源把一个 UUID 设备挂载到 /mnt。ensure 支持哪些值?
答:
node default {
mount { "/mnt": # title 即挂载点
ensure => "mounted", # 写入 /etc/fstab 并立即挂载
device => "UUID=3f1de712-7864-47ea-b0b4-9fa5781b2e88",
}
}
ensure 支持:
| 值 | 含义 |
|---|---|
defined / present |
只写入 /etc/fstab,不挂载 |
mounted |
写入 /etc/fstab 并立即挂载 |
unmounted |
保留 fstab 条目但确保未挂载 |
absent |
卸载并从 fstab 删除 |
36. 用 host 资源修改 /etc/hosts 中 agent1 的 IP 和别名。
答:
node default {
host { "agent1.cluster.com":
ip => "10.1.1.4",
host_aliases => "agent2haha",
}
}
37. Puppet 默认保证资源执行顺序吗?如何显式声明依赖?四种元参数的对应关系是什么?
答:Puppet 默认不保证执行顺序,manifest 中的书写顺序不等于执行顺序。必须显式声明依赖。
| 元参数 | 写在哪个资源上 | 含义 | 等价箭头 |
|---|---|---|---|
require => X |
后执行的资源 | 我依赖 X,X 先执行 | X -> Me |
before => X |
先执行的资源 | 我必须在 X 之前 | Me -> X |
notify => X |
先执行的资源 | 我变了就通知 X 刷新 | Me ~> X |
subscribe => X |
后执行的资源 | 我订阅 X,X 变了我刷新 | X ~> Me |
require ↔ before 互为反向,notify ↔ subscribe 互为反向。箭头符号:->(顺序)、~>(顺序 + 刷新)、<-、<~(反向,不推荐)。
38. 写出"改配置文件自动重启服务"的经典模式代码。
答:
class nginx {
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,
}
}
# 或用链式箭头
Package['nginx'] -> File['/etc/nginx/nginx.conf'] ~> Service['nginx']
39. 用 case 和 selector 两种方式,按 osfamily 决定 apache 的包名。
答:
# case
case $::osfamily {
'RedHat': { $apache_pkg = 'httpd' }
'Debian': { $apache_pkg = 'apache2' }
default: { fail("Unsupported osfamily: ${::osfamily}") }
}
# selector
$apache_pkg = $::osfamily ? {
'RedHat' => 'httpd',
'Debian' => 'apache2',
default => 'httpd',
}
40. 写出区分 CentOS 6 与 CentOS 7 的实用分支代码(服务管理方式、防火墙、时间同步)。
答:
case $::operatingsystemmajrelease {
'6': {
# CentOS 6:SysV init + iptables + ntpd
$service_provider = 'redhat'
$firewall_svc = 'iptables'
$time_pkg = 'ntp'
$time_svc = 'ntpd'
}
'7', '8': {
# CentOS 7/8:systemd + firewalld + chrony
$service_provider = 'systemd'
$firewall_svc = 'firewalld'
$time_pkg = 'chrony'
$time_svc = 'chronyd'
}
default: {
fail("Unsupported EL release: ${::operatingsystemmajrelease}")
}
}
package { $time_pkg: ensure => installed }
service { $time_svc:
ensure => running,
enable => true,
provider => $service_provider,
require => Package[$time_pkg],
}
五、类、模块与模板题
41. 什么是类?什么是模块?为什么要用模块?
答:
- 类(class):一组资源的命名集合,可复用、可参数化。类只是"定义",必须被
include或class {}声明后才会进入 catalog。 - 模块(module):按约定目录结构组织的一个功能单元,包含 manifests(代码)、files(静态文件)、templates(模板)、lib(自定义 fact/函数)等。
- 为什么用模块:把
site.pp里堆积的资源按功能拆分,实现复用、独立版本管理、可从 Forge 分享下载;Puppet 的自动加载机制也依赖模块目录结构。
42. 写出一个标准模块的目录结构,并说明各目录用途和自动加载命名规则。
答:
/etc/puppet/modules/mymodule/ # 3.x
/etc/puppetlabs/code/environments/production/modules/mymodule/ # 4.x+
├── manifests/
│ ├── init.pp # class mymodule (模块同名类,必须在 init.pp)
│ ├── install.pp # class mymodule::install
│ ├── config.pp # class mymodule::config
│ ├── service.pp # class mymodule::service
│ ├── params.pp # class mymodule::params
│ └── vhost.pp # define mymodule::vhost
├── files/ # 静态文件,用 puppet:///modules/mymodule/xxx 引用
├── templates/ # ERB/EPP 模板,用 template('mymodule/xxx.erb') 引用
├── lib/
│ ├── facter/ # 自定义 fact(Ruby)
│ └── puppet/ # 自定义函数/资源类型
├── examples/ # 或 tests/,使用示例
├── spec/ # rspec-puppet 单元测试
├── data/ # 模块内 Hiera 数据(Hiera 5)
├── hiera.yaml # 模块内 Hiera 配置(Hiera 5)
├── metadata.json # 模块元数据:名称、版本、依赖、支持的 OS
└── README.md
自动加载规则:类名 a::b::c → 文件 a/manifests/b/c.pp;类名 a(与模块同名)→ 文件 a/manifests/init.pp。文件名与类名必须严格对应,否则报 Could not find class。
43. 完成实验:编写 sudo 模块,把 master 上的 sudoers 推送到所有客户端。写出全部 8 个步骤。
答:
1)创建模块目录:
mkdir -p /etc/puppet/modules/sudo/{manifests,files/etc,templates,lib,tests,spec}
2)在 /etc/puppet/puppet.conf 的 [main] 段指定模块路径:
modulepath = /etc/puppet/modules
3)配置 /etc/puppet/fileserver.conf,允许客户端拉取文件:
[modules]
allow *
4)编写 /etc/puppet/manifests/site.pp:
import 'nodes.pp' # 导入节点定义文件(3.x 语法)
$puppetserver = 'master.cluster.com' # 定义全局变量,供模块引用
5)编写 /etc/puppet/manifests/nodes.pp:
node /^agent\d+\.cluster\.com/ {
include sudo # 声明 sudo 类
}
6)编写 /etc/puppet/modules/sudo/manifests/init.pp:
class sudo {
package { sudo:
ensure => present,
}
if $operatingsystem in [ "RedHat", "CentOS", "Fedora" ] {
file { "/etc/sudoers":
owner => "root",
group => "root",
mode => 0440,
source => "puppet://$puppetserver/sudo/files/etc/sudoers",
require => Package["sudo"],
}
} else {
fail("this module is not support $operatingsystem")
}
}
7)把要分发的 sudoers 放进模块并授权:
cp /etc/sudoers /etc/puppet/modules/sudo/files/etc/
chown -R puppet /etc/puppet/modules/sudo/
systemctl restart puppetmaster
8)客户端验证:
puppet agent --test --server master.cluster.com
44. puppet:///modules/xxx 这种三段式路径为什么不写 files?四段式又是什么?
答:三段式 puppet:///modules/<模块名>/<相对路径> 是模块文件挂载点的简写,Puppet 会自动在模块的 files/ 目录下找,所以路径里不写 files:
source => "puppet:///modules/sudo/etc/sudoers"
# 实际文件:/etc/puppet/modules/sudo/files/etc/sudoers
四段式 puppet://<服务器>/<挂载点>/<路径> 显式指定服务器和 fileserver.conf 中定义的挂载点,早期文档中常见(如 puppet://$puppetserver/sudo/files/etc/sudoers 就是把 sudo 当挂载点用)。推荐统一用三段式,服务器由 Agent 的 server 参数决定,迁移时不用改代码。
45. source 可以写多个吗?有什么用?
答:可以写数组,Puppet 按顺序回退到第一个存在的文件,非常适合"节点专属配置优先,找不到就用通用配置":
file { '/etc/myapp.conf':
source => [
"puppet:///modules/myapp/conf/${::fqdn}.conf", # 本机专属
"puppet:///modules/myapp/conf/${::osfamily}.conf", # 按系统
'puppet:///modules/myapp/conf/default.conf', # 兜底
],
}
46. 完成实验:编写 httpd 模块,安装 httpd、修改 DocumentRoot 为 /web、生成首页并启动服务。写出完整步骤。
答(原始实验版,7 步):
1)创建目录:
mkdir -p /etc/puppet/modules/httpd/{manifests,files/etc/httpd/conf,templates,lib,tests,spec}
2)modulepath、fileserver.conf、site.pp 同 sudo 模块实验。
3)/etc/puppet/manifests/nodes.pp:
node /^agent\d+\.cluster\.com/ {
include httpd
}
4)/etc/puppet/modules/httpd/manifests/init.pp:
class httpd {
package { httpd:
ensure => present,
}
file { "/etc/httpd/conf/httpd.conf":
owner => "root",
group => "root",
mode => 0644,
source => "puppet://$puppetserver/httpd/files/etc/httpd/conf/httpd.conf",
require => Package["httpd"],
}
file { "/web":
ensure => directory,
}
file { "/shell/":
ensure => directory,
}
file { "/shell/1.sh":
content => "ip=`ifconfig eth0 | grep 'inet ' | awk '{print \$2}' | awk -F. '{print \$4}'`\necho \"web\$ip\" > /web/index.html\n",
mode => 755,
}
exec { "make-index":
cwd => "/shell",
command => "sh 1.sh",
path => "/bin:/sbin:/usr/local/bin:/usr/local/sbin:/usr/sbin:/usr/bin",
require => File["/web"],
}
service { httpd:
ensure => running,
enable => true,
require => File["/etc/httpd/conf/httpd.conf"],
}
}
5)准备配置文件,把 DocumentRoot 与目录段改为 /web:
cp /etc/httpd/conf/httpd.conf /etc/puppet/modules/httpd/files/etc/httpd/conf/
sed -i '119,131s/\/var\/www\/html/\/web/' /etc/puppet/modules/httpd/files/etc/httpd/conf/httpd.conf
6)授权并重启:
chown -R puppet /etc/puppet/modules/httpd/
systemctl restart puppetmaster
7)客户端验证:
puppet agent --test --server master.cluster.com
curl http://10.1.1.3 # web3
curl http://10.1.1.4 # web4
生产改进版(用 ERB 模板替代 exec + awk、用
contain拆分三段式、加validate_cmd)见正文 11.5.2 小节。
47. ERB 模板中三种标签的区别?如何引用变量和 Facts?
答:
| 标签 | 作用 |
|---|---|
<%= expr %> |
求值并输出结果 |
<% code %> |
执行 Ruby 代码但不输出(用于 if/each) |
<%# 注释 %> |
模板注释,不出现在结果中 |
<%- -%> |
去掉标签前后的空白/换行 |
引用变量:
<%= @port %> <%# 当前作用域变量、类参数 %>
<%= @fqdn %> <%= @ipaddress %> <%# 顶层 Facts 直接用 @ %>
<%= scope['::nginx::params::user'] %> <%# 跨作用域取值 %>
<%= scope.lookupvar('::osfamily') %> <%# 老写法 %>
48. 用 ERB 写一个模板,为不同节点生成含 worker 数、监听端口和 upstream 列表的 Nginx 配置。
答:
# 由 Puppet 管理,请勿手工修改
user <%= @nginx_user %>;
worker_processes <%= @processorcount %>; <%# 直接用 CPU 核数 Fact %>
events {
worker_connections <%= @worker_connections %>;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
<% @upstreams.each_pair do |name, members| -%>
upstream <%= name %> {
<% members.each do |m| -%>
server <%= m %>;
<% end -%>
}
<% end -%>
server {
listen <%= @port %>;
server_name <%= @fqdn %>;
<% if @enable_ssl -%>
listen 443 ssl;
ssl_certificate <%= @ssl_cert %>;
ssl_certificate_key <%= @ssl_key %>;
<% end -%>
location / {
root <%= @docroot %>;
index index.html index.htm;
}
}
}
对应的类:
class nginx (
Integer $port = 80,
Integer $worker_connections = 1024,
String $nginx_user = 'nginx',
String $docroot = '/web',
Boolean $enable_ssl = false,
Optional[String] $ssl_cert = undef,
Optional[String] $ssl_key = undef,
Hash $upstreams = {},
) {
file { '/etc/nginx/nginx.conf':
ensure => file,
content => template('nginx/nginx.conf.erb'),
validate_cmd => '/usr/sbin/nginx -t -c %',
notify => Service['nginx'],
}
}
49. define 与 class 有什么区别?什么时候用 define?
答:
class |
define |
|
|---|---|---|
| 实例个数 | 每节点只能声明一次 | 可用不同 title 声明多次 |
| 声明方式 | include x / class { 'x': } |
x { 'title': }(像资源) |
| 内置变量 | 无 | $title、$name |
| 典型用途 | 一个服务/组件的整体配置 | 需要重复创建的东西 |
用 define 的场景:多个 Nginx/Apache 虚拟主机、批量用户、批量 sudo 规则、批量 cron 任务。例如:
define httpd::vhost (
String $docroot,
Integer $port = 80,
String $servername = $title,
) {
file { "/etc/httpd/conf.d/${title}.conf":
ensure => file,
content => template('httpd/vhost.conf.erb'),
notify => Service['httpd'],
}
file { $docroot: ensure => directory }
}
# 声明多次
httpd::vhost { 'www.example.com': docroot => '/web/www', port => 80 }
httpd::vhost { 'api.example.com': docroot => '/web/api', port => 8080 }
50. 节点匹配的优先级规则是什么?如果一个节点同时能匹配精确名、正则和 default 会怎样?
答:Puppet 的节点匹配是排他的,只会命中一个 node 块,优先级:
- 精确的节点名(FQDN 完全相同)
- 正则表达式节点(多个正则都匹配时行为不确定,应避免重叠)
node default
也就是说:只要有精确匹配,正则和 default 都不生效;只要有正则匹配,default 不生效。这也是"Agent 运行成功但没有任何变更"的常见原因——节点被一个空的 node 块或错误的 default 抢走了。
生产环境推荐用 Hiera + roles/profiles 或 ENC 代替大量 node 块。
六、进阶与排错题
51. Puppet 的 environment 是什么?production 和 development 目录怎么布局?
答:environment 是一套独立的代码与数据(modules + manifests + hiera),Agent 可以指定运行在哪个环境里,实现"测试环境验证后再上生产"。
/etc/puppetlabs/code/environments/
├── production/
│ ├── environment.conf
│ ├── manifests/site.pp
│ ├── modules/
│ ├── hiera.yaml
│ └── data/
└── development/
├── environment.conf
├── manifests/site.pp
└── modules/
指定环境运行:
puppet agent --test --environment development
# 或在 puppet.conf 的 [agent] 段写 environment = development
52. Hiera 解决什么问题?写出 Hiera 5 的分层配置示例。
答:Hiera 把数据从代码中分离出来,让同一份模块代码在不同节点/环境使用不同参数,避免为每个节点复制一份 manifest。
# /etc/puppetlabs/code/environments/production/hiera.yaml
---
version: 5
defaults:
datadir: data
data_hash: yaml_data
hierarchy:
- name: "节点专属数据"
path: "nodes/%{trusted.certname}.yaml"
- name: "按角色"
path: "roles/%{facts.role}.yaml"
- name: "按操作系统"
path: "os/%{facts.os.family}-%{facts.os.release.major}.yaml"
- name: "公共默认值"
path: "common.yaml"
# data/common.yaml
nginx::port: 80
nginx::worker_connections: 1024
# data/nodes/agent1.cluster.com.yaml
nginx::port: 8080
Puppet 4+ 的自动参数查找(APL):类 nginx 的参数 $port 会自动去 Hiera 找 key nginx::port,无需显式调用。手工查找用 lookup('nginx::port'),调试用 puppet lookup nginx::port --explain --node agent1.cluster.com。
53. 什么是导出资源?举一个典型使用场景并写出代码。
答:导出资源(exported resources)借助 PuppetDB,让一个节点声明的资源被其他节点收集,实现节点间自动发现。需要在 Master 上开启 storeconfigs = true 和 PuppetDB。
- 声明:
@@前缀 - 收集:
<<| 条件 |>>
典型场景:所有 Web 节点把自己的 host 记录导出,每个节点收集全部记录,自动维护 /etc/hosts:
class base::hosts {
# 每个节点导出自己的 host 条目
@@host { $::fqdn:
ip => $::ipaddress,
host_aliases => [$::hostname],
}
# 收集所有节点导出的 host 条目
Host <<| |>>
}
其他场景:Nginx 负载均衡自动收集后端节点、Nagios 自动注册被监控主机、SSH 主机密钥自动互信(@@sshkey)、备份客户端自动注册到备份服务器。
54. Agent 报 Error: Could not request certificate: ... SSL_connect ... certificate verify failed,排查思路?
答:
- 时间:
date对比两端,时间偏差过大会导致证书"尚未生效"或"已过期"。用ntpdate/chronyd同步后重试。 - 主机名:Agent 连接用的名字必须在 Master 证书的 CN 或
dns_alt_names里。若用 IP 或别名连接,需在 Master 的puppet.conf设dns_alt_names = puppet,master,master.cluster.com,然后重新生成 Master 证书。 - 残留证书:改过主机名或重装过,两端 ssl 目录有旧证书。彻底重置:
# Master
puppet cert clean agent1.cluster.com
# Agent
rm -rf /var/lib/puppet/ssl/* # 3.x
rm -rf /etc/puppetlabs/puppet/ssl/* # 4.x+
puppet agent --test --server master.cluster.com
# Master 再次签发
puppet cert sign agent1.cluster.com
- CA 不一致:Agent 的
certs/ca.pem与 Master CA 不匹配,同样按第 3 步清理。
55. Error: Could not find class xxx 有哪些常见原因?
答:
modulepath没包含模块所在目录(puppet config print modulepath确认)。- 类名与文件路径不匹配(
class a::b必须在a/manifests/b.pp;class a必须在a/manifests/init.pp)。 - 模块目录名带了大写、横线或版本号后缀(如
nginx-1.0),模块名只能是小写字母、数字、下划线。 - 文件权限问题,
puppet用户读不到(chown -R puppet /etc/puppet/modules)。 - 环境放错了(代码放在 production 但 Agent 跑在 development)。
- 3.x 里用了
import但没导入对应文件。
56. 报 Duplicate declaration: File[/etc/xxx] is already declared 怎么处理?
答:同一资源(相同类型 + 相同 title/path)在 catalog 中只允许出现一次。处理办法:
- 找到重复声明的位置(报错信息会给出两处文件行号)。
- 把公共资源抽到一个基础类里,其他类通过
include/require复用。 - 用
ensure_resource('file', '/etc/xxx', {...})(stdlib)做幂等声明。 - 只需要改文件里的一行时,用
file_line(stdlib)而不是整体管理该文件。 - 需要"允许重复但只生效一次"的语义时用虚拟资源:
@user { 'tom': ... }+realize(User['tom'])。
57. 报依赖环 Found 1 dependency cycle 如何定位?
答:
puppet agent --test --graph
# 生成 /var/lib/puppet/state/graphs/*.dot
dot -Tpng /var/lib/puppet/state/graphs/cycles.dot -o cycles.png
看图找出互相依赖的资源,去掉其中一个方向的 require/notify。常见错误是同时写了 A require B 和 B notify A。
58. --noop 显示会变更,但实际运行后没变化(或反之),可能是什么原因?
答:
--noop只做模拟,exec资源不会真的执行,所以依赖 exec 产物的后续资源判断可能失真。- 资源被
noop => true元参数固定为模拟模式。 - 权限或 SELinux 导致实际写入失败(
--debug看详细报错,getenforce/ausearch -m avc排查)。 - 文件被
replace => false保护,只在不存在时创建。 - 目标节点匹配到了别的 node 块(见第 50 题)。
59. 改了 puppet.conf 但不生效,怎么办?
答:
- 确认改的是正确的文件:3.x 是
/etc/puppet/puppet.conf,4.x+ 是/etc/puppetlabs/puppet/puppet.conf。用puppet config print config确认实际加载路径。 - 确认改在正确的段里:
[agent]的参数写到[master]里不会生效。 - 用
puppet config print <参数名> --section agent验证实际取值。 - Master 端的多数参数需要重启服务:
systemctl restart puppetmaster/systemctl restart puppetserver。 - 注意命令行参数优先级最高,会覆盖配置文件。
60. Agent 运行提示 Could not run: Could not create PID file 或有 lock 文件,如何处理?
答:报错通常形如:
notice: Run of Puppet configuration client already in progress;
skipping (/var/lib/puppet/state/agent_catalog_run.lock exists)
原因是已有一个 Agent 进程在运行(守护模式与手工 --test 冲突),或上次运行被强杀留下了锁文件。处理:
ps -ef | grep puppet # 确认没有正在运行的进程
systemctl stop puppet # 先停守护进程再手工测试
rm -f /var/lib/puppet/state/agent_catalog_run.lock
puppet agent --test --server master.cluster.com
七、动手练习
练习 A:搭建完整的 Master/Agent 环境
要求:1 台 Master(master.cluster.com,10.1.1.2)、2 台 Agent(agent1/agent2,10.1.1.3/10.1.1.4)。完成主机名、/etc/hosts、时间同步、防火墙放行 8140、安装、证书签发全流程,最后用 puppet cert list --all 证明两个 Agent 都已签发。
练习 B:编写 base 基础模块
要求实现一个 base 模块,对所有节点统一做以下事情,并能在 CentOS 6/7 上通用:
- 用
host资源统一/etc/hosts(含 master 与所有 agent); - 安装并启动时间同步服务(6 用 ntpd,7 用 chronyd),配置文件用 ERB 模板生成,指向
master.cluster.com; - 创建运维组
ops(gid 2000)与用户deploy(uid 2000,属组 ops,带家目录); - 下发
/etc/sudoers.d/ops,允许 ops 组免密 sudo,必须带validate_cmd校验; - 下发
/etc/motd,内容包含主机名、IP、CPU 核数(用 Facts); - 所有文件资源必须正确声明依赖,配置变更能自动重启对应服务。
练习 C:把 httpd 模块改造为生产级
以正文 11.5.1 的原始实验为起点,完成:
- 拆分为
httpd::params、httpd::install、httpd::config、httpd::service四个类,用contain+ 链式箭头组织; - 用 ERB 模板替代
exec+awk生成首页(首页内容web<IP最后一段>); - 主配置文件用模板生成,
DocumentRoot可通过类参数配置,默认/web; - 定义
httpd::vhost自定义资源类型,支持创建多个虚拟主机; - 端口、DocumentRoot、虚拟主机列表全部通过 Hiera 提供,节点专属数据覆盖公共默认值;
- 用
puppet parser validate和puppet agent --test --noop验证,确认第二次运行为零变更(幂等)。
练习 D:自定义 fact 与条件逻辑
编写一个自定义 fact app_role,规则为:主机名以 web 开头返回 web,以 db 开头返回 db,否则返回 other。然后在 manifest 中根据 $::app_role 分别安装 nginx / mariadb / 仅安装基础包。要求同时给出 Ruby 版(lib/facter/app_role.rb)和外部 fact 版(/etc/facter/facts.d/app_role.txt)两种实现。
练习 E:幂等性改造
下面这段代码存在三个问题,请指出并改写为幂等、可维护的版本:
node default {
exec { 'add-user':
command => 'useradd tom',
path => '/usr/sbin:/usr/bin',
}
exec { 'install-nginx':
command => 'yum -y install nginx',
path => '/usr/bin',
}
exec { 'start-nginx':
command => 'service nginx start',
path => '/sbin:/usr/sbin',
}
}
参考答案要点:三处都该用原生资源(user/package/service)而不是 exec;exec 无守卫条件(unless/creates/onlyif)每次都会执行且第二次直接报错;资源间没有依赖声明,执行顺序不确定。改写:
node default {
user { 'tom': ensure => present, managehome => true }
package { 'nginx': ensure => installed }
service { 'nginx':
ensure => running,
enable => true,
require => Package['nginx'],
}
}
练习 F:Git 管理 Puppet 代码
把 /etc/puppet/(或 /etc/puppetlabs/code/)纳入 Git 管理,创建 production 与 development 两个分支,编写 Puppetfile,用 r10k 把两个分支自动同步为两个 Puppet 环境,并验证 puppet agent --test --environment development 拉取的是 development 分支的代码。
详见 14-01-Git版本控制.md
本章小结
一、核心概念回顾
- 配置管理的价值:一致性、可重复、可审计、幂等。把基础设施变成代码(IaC),用版本控制管理,消除配置漂移。
- 声明式 vs 命令式:Puppet 是声明式的——描述"最终状态"而不是"执行步骤",由 Puppet 自己判断当前状态并决定是否需要动作。这是幂等性的基础。
- 幂等性:同一份代码执行 N 次结果一致,第二次运行应为零变更。
exec是幂等性最大的破坏者,必须加unless/onlyif/creates/refreshonly守卫,能用原生资源就不要用 exec。 - 架构:Master/Agent 的 C/S 模式,走 8140 端口的双向 SSL;Agent 默认每 30 分钟(
runinterval = 1800)拉取一次。单机可用puppet apply无 Master 运行。 - 运行流程五步:Facter 采集 Facts → 发送给 Master → Master 编译 Catalog → 下发 Catalog → Agent 应用并回报 Report。
- 证书是第一道门槛:主机名解析 + 时间同步 + 证书签发,三者任一出错都连不上 Master。
二、常用命令速查表
Agent 端
| 命令 | 说明 |
|---|---|
puppet agent --test |
前台运行一次,输出详细日志(最常用) |
puppet agent --test --server master.cluster.com |
指定 Master 运行一次 |
puppet agent --test --noop |
模拟运行,只显示会做什么,不真的改 |
puppet agent --test --debug |
输出调试级日志,排错必备 |
puppet agent --test --verbose |
输出较详细信息 |
puppet agent --test --show_diff |
显示文件变更的 diff |
puppet agent --test --evaltrace |
显示每个资源的求值过程与耗时 |
puppet agent --test --trace |
出错时打印 Ruby 堆栈 |
puppet agent --test --tags nginx |
只应用带指定 tag 的资源 |
puppet agent --test --environment development |
指定环境运行 |
puppet agent --test --detailed-exitcodes |
返回细分退出码(2=有变更,4=有失败,6=两者都有) |
puppet agent --test --graph |
生成依赖关系图(排查依赖环) |
puppet agent --enable / --disable "原因" |
启用/禁用 Agent 运行 |
puppet apply site.pp |
单机模式直接应用 manifest |
puppet apply -e 'notice("hi")' |
直接执行一段代码 |
Master 端
| 命令 | 说明 |
|---|---|
puppet cert list |
查看待签发的证书请求(3.x) |
puppet cert list --all |
查看所有证书,+ 表示已签发 |
puppet cert sign agent1.cluster.com |
签发指定证书 |
puppet cert --sign --all / puppet cert sign --all |
签发全部待签请求 |
puppet cert clean agent1.cluster.com |
吊销并删除指定节点证书 |
puppetserver ca list --all |
4.x+/6.x 查看所有证书 |
puppetserver ca sign --certname agent1 |
4.x+/6.x 签发 |
puppetserver ca sign --all |
4.x+/6.x 全部签发 |
puppetserver ca clean --certname agent1 |
4.x+/6.x 清理证书 |
systemctl restart puppetmaster |
重启 Master(3.x) |
systemctl restart puppetserver |
重启 Master(4.x+) |
lsof -i:8140 / netstat -tnlp \| grep 8140 |
确认 Master 已监听 |
通用/调试
| 命令 | 说明 |
|---|---|
puppet parser validate site.pp |
语法检查(改完必做) |
puppet-lint site.pp |
代码风格检查 |
puppet config print |
打印全部配置项 |
puppet config print modulepath |
打印模块路径 |
puppet config print config --section agent |
打印实际加载的配置文件路径 |
puppet describe file |
查看资源类型的全部属性与说明 |
puppet describe --list |
列出所有可用资源类型 |
puppet resource user root |
把当前系统状态反查为 Puppet 代码 |
puppet resource service httpd ensure=running |
直接用命令行改状态 |
puppet module install puppetlabs-stdlib |
从 Forge 安装模块 |
puppet module list |
列出已安装模块 |
puppet module search nginx |
搜索模块 |
puppet lookup nginx::port --explain |
调试 Hiera 查找过程 |
facter / facter -p |
查看全部 Facts(-p 含自定义 fact) |
facter osfamily ipaddress |
查看指定 Fact |
puppet catalog find agent1.cluster.com |
查看为某节点编译的 catalog |
三、核心资源类型与属性速查表
| 资源 | 作用 | 关键属性 |
|---|---|---|
file |
文件/目录/软链接 | ensure(present/absent/file/directory/link)、content、source、target、mode、owner、group、recurse、purge、force、replace、backup、validate_cmd、show_diff |
package |
软件包 | ensure(installed/present/latest/absent/purged/版本号)、name、provider(yum/rpm/gem/pip)、source、install_options |
service |
服务 | ensure(running/stopped)、enable(true/false)、name、provider(redhat/systemd)、hasstatus、hasrestart、restart、status、pattern |
exec |
执行命令(慎用) | command、path、cwd、user、unless、onlyif、creates、refreshonly、timeout、tries、environment、logoutput |
user |
用户 | ensure、uid、gid、groups、shell、home、managehome、password、comment、expiry |
group |
用户组 | ensure、gid、members |
cron |
定时任务 | command、user、minute、hour、monthday、month、weekday、ensure、environment |
host |
/etc/hosts 条目 |
ip、host_aliases、ensure、comment、target |
mount |
挂载点 | ensure(mounted/unmounted/present/absent)、device、fstype、options、dump、pass、atboot |
yumrepo |
yum 仓库 | baseurl、mirrorlist、enabled、gpgcheck、gpgkey、descr、priority |
notify |
输出日志消息 | message、withpath |
tidy |
清理旧文件 | path、age、size、recurse、matches、rmdirs |
ssh_authorized_key |
公钥免密 | user、type、key、ensure |
sshkey |
known_hosts | type、key、host_aliases |
augeas |
结构化改配置 | context、changes、onlyif、lens |
selboolean |
SELinux 布尔值 | value、persistent |
file_line(stdlib) |
只改文件中的一行 | path、line、match、ensure |
元参数(所有资源可用)
| 元参数 | 作用 |
|---|---|
require |
依赖:目标资源先执行 |
before |
我在目标资源之前执行 |
notify |
我变更后通知目标资源刷新 |
subscribe |
目标资源变更后我刷新 |
tag |
打标签,配合 --tags 选择性运行 |
alias |
资源别名 |
schedule |
只在指定时间窗口内执行 |
noop |
该资源只模拟不执行 |
audit |
审计属性变化但不修改 |
loglevel |
该资源日志级别 |
stage |
指定运行阶段(配合 stage 资源) |
四、版本差异要点
| 项目 | Puppet 3.x | Puppet 4.x/5.x/6.x(AIO) |
|---|---|---|
| 配置目录 | /etc/puppet/ |
/etc/puppetlabs/puppet/ |
| 代码目录 | /etc/puppet/manifests、/etc/puppet/modules |
/etc/puppetlabs/code/environments/production/ |
| 数据目录 | /var/lib/puppet/ |
/opt/puppetlabs/puppet/cache/ |
| SSL 目录 | /var/lib/puppet/ssl/ |
/etc/puppetlabs/puppet/ssl/ |
| 日志 | /var/log/puppet/ |
/var/log/puppetlabs/ |
| 二进制 | 系统 Ruby + /usr/bin/puppet |
/opt/puppetlabs/bin/puppet(自带 Ruby) |
| Master 包名 | puppet-server |
puppetserver |
| Agent 包名 | puppet |
puppet-agent |
| Master 服务 | puppetmaster(WEBrick,需 Passenger 上生产) |
puppetserver(Jetty + JRuby,生产可用) |
| Agent 服务 | puppet / puppetagent |
puppet |
| 证书命令 | puppet cert list/sign/clean |
puppetserver ca list/sign/clean |
import |
支持 | 已移除,改用目录环境自动加载 |
| 数据类型 | 无强类型 | 支持 String/Integer/Enum/Optional/Sensitive 等 |
| Hiera | Hiera 3(:backends/:hierarchy) |
Hiera 5(version: 5/hierarchy),支持模块层 |
| 模板 | ERB | ERB + EPP |
| 项目 | CentOS 6 | CentOS 7 |
|---|---|---|
| 初始化系统 | SysV init(service/chkconfig) |
systemd(systemctl) |
| Puppet provider | redhat |
systemd |
| 主机名 | /etc/sysconfig/network + hostname |
hostnamectl set-hostname --static |
| 防火墙 | iptables(-A INPUT -p tcp --dport 8140 -j ACCEPT) |
firewalld(firewall-cmd --add-port=8140/tcp --permanent) |
| 时间同步 | ntp / ntpd / ntpdate |
chrony / chronyd / chronyc sources |
| 网络服务 | network |
NetworkManager + network |
五、排错口诀
"名、时、证、火、类、序、幂"七字诀
- 名——主机名与解析先过关。Master/Agent 的
hostname -f必须是规划好的 FQDN,/etc/hosts双向可解析,Agent 必须能 ping 通 Master 的名字(不是只有 IP)。这是最高频的故障源。 - 时——时间必须同步。证书的有效期校验对时间极其敏感,偏差过大直接
certificate verify failed。 - 证——证书要签发且要干净。
puppet cert list --all看+;改过主机名/重装过就两端清 ssl 目录 +puppet cert clean后重来。 - 火——防火墙与 SELinux。Master 放行 8140,必要时先
setenforce 0定位是否 SELinux 拦截,确认后用semanage/restorecon正规解决而不是长期关闭。 - 类——找不到类看路径。
Could not find class先查modulepath、类名与文件名的对应关系、模块目录名是否合法、puppet用户能否读取。 - 序——顺序靠声明不靠书写。默认无序,必须用
require/before/notify/subscribe或->/~>;出现依赖环用--graph+ graphviz 出图定位。 - 幂——第二次运行必须零变更。跑两遍
puppet agent --test,第二遍还有notice: ... changed就说明有资源不幂等,重点查exec。
通用排错三板斧
# 1. 语法先过
puppet parser validate /etc/puppet/manifests/site.pp
# 2. 模拟看差异
puppet agent --test --noop --show_diff
# 3. 调试看细节
puppet agent --test --debug --evaltrace --trace
日志位置
| 内容 | 3.x 路径 | 4.x+ 路径 |
|---|---|---|
| Master 日志 | /var/log/puppet/masterhttp.log、/var/log/messages |
/var/log/puppetlabs/puppetserver/puppetserver.log |
| Agent 日志 | /var/log/messages(syslog) |
/var/log/messages、journalctl -u puppet |
| 运行锁文件 | /var/lib/puppet/state/agent_catalog_run.lock |
/opt/puppetlabs/puppet/cache/state/agent_catalog_run.lock |
| 上次运行摘要 | /var/lib/puppet/state/last_run_summary.yaml |
/opt/puppetlabs/puppet/cache/state/last_run_summary.yaml |
六、生产实践建议
- 代码进 Git,用分支对应环境,配合 r10k 自动部署,任何变更走 MR/PR 评审。
- 数据与代码分离,参数一律走 Hiera,敏感数据用 hiera-eyaml 加密。
- 禁用
autosign = true,用域名白名单或带预共享密钥的策略脚本。 - 少用
exec,优先原生资源和augeas/file_line;必须用时一定加守卫条件。 - 上线前必跑
--noop,并在测试环境先验证;关键变更用--tags缩小影响范围。 - 模块化 + roles/profiles 分层,避免在
site.pp里堆节点定义。 - 配置文件模板化,配合
validate_cmd校验语法,防止推错配置导致服务起不来。 - 接入报告系统(PuppetDB + Foreman/PuppetBoard),做到变更可审计、节点状态可视化。
runinterval保持默认或适当拉长,大规模环境务必开启splay打散请求,避免瞬时压垮 Master。- 写 rspec-puppet 测试 + puppet-lint 检查,纳入 CI 流水线。
详见 14-01-Git版本控制.md 详见 14-02-Ansible自动化部署.md 详见 14-04-SaltStack配置管理.md
此处评论已关闭