1. 这不是写代码,是给服务器“开处方”:Chef Recipes 的真实定位
很多人第一次看到 Chef 的文档,第一反应是:“哦,又一个用 Ruby 写的自动化工具”,然后顺手点开一个
recipe
文件,看到
package 'nginx'
、
service 'nginx' do action [:enable, :start] end
这类语法,下意识就把它当成“带点 DSL 的 Ruby 脚本”来读。我刚接触 Chef 那会儿也这么干过——结果在生产环境里连续三天没跑通一个最基础的 Nginx 部署,反复重装、重启、查日志,最后发现根本不是语法错了,而是我把
recipe
当成了“执行清单”,却完全忽略了它背后那套
声明式配置管理哲学
。
Chef 的核心从来不是“让机器执行什么命令”,而是“告诉 Chef 你希望系统最终处于什么状态”。这个“状态”,才是
recipe
真正要描述的对象。比如
package 'nginx'
不是说“现在去 apt-get install nginx”,而是声明“这台机器上必须存在 nginx 这个包,且版本符合约束”;
file '/etc/nginx/nginx.conf'
不是“把文件拷过去”,而是声明“该路径下的文件内容、权限、属主必须与 recipe 中定义的完全一致”。一旦理解了这个前提,你就不会在
recipe
里写
execute 'curl -sL https://get.docker.com | sh'
这种命令式操作——因为 Chef 无法验证它的执行结果是否可逆、是否幂等、是否真正达成了你想要的状态。
这也是为什么标题里强调的是 “Configuration Management 101”,而不是 “Chef 速成班”。Chef 是配置管理(CM)领域里一个成熟、稳定、企业级落地案例极多的工具,它背后承载的是整个运维工程化演进的逻辑:从人肉 SSH 执行命令 → Shell 脚本批量执行 → 到今天以“状态声明+收敛引擎+资源抽象”为内核的现代 CM 实践。而
recipe
,就是你向这套系统提交的“状态处方单”。它不负责执行细节,只负责精准描述目标;真正的执行、校验、修复,全部由 Chef Client 在后台自动完成。所以,写好一个 recipe,本质是训练自己用“终态思维”去建模基础设施——这比学会几个 Ruby 语法重要十倍。
你不需要是 Ruby 专家,但必须能读懂 Ruby 的基本结构;你不需要精通 Linux 内核,但得清楚
/etc/
下哪些文件改了会影响服务行为;你不需要会写 Web 框架,但得明白
template
资源和 ERB 模板之间是怎么联动的。这门课的门槛不在语言,而在思维方式的切换。如果你正在被“自动化 license manager 启动失败”、“Homebrew Portable Ruby 升级卡住”这类问题困扰,恰恰说明你已经站在了从“脚本运维”迈向“配置即代码”的临界点上——而 Chef Recipes,就是帮你跨过这道坎的第一块踏脚石。
2. Recipe 的骨架与血肉:从空壳到可运行的完整解剖
一个 Chef recipe 看似简单,往往就几十行代码,但它内部有非常清晰的分层结构。我习惯把它拆成三个层次: 声明层(Declarative Layer)、上下文层(Context Layer)、执行层(Execution Layer) 。绝大多数初学者写的 recipe 出问题,都是混淆了这三层的职责边界。
2.1 声明层:只说“要什么”,不说“怎么要”
这是 recipe 的绝对核心,也是 Chef 最具辨识度的部分。所有以
resource_name 'name' do ... end
形式出现的代码块,都属于这一层。例如:
package 'git' do
version '2.39.2-1'
action :install
end
user 'deploy' do
comment 'Application deployment user'
uid 1001
gid 'users'
home '/home/deploy'
shell '/bin/bash'
manage_home true
action :create
end
注意这里的关键点:
-
package和user是 Chef 内置的 资源(Resource) ,不是 Ruby 方法; -
'git'和'deploy'是该资源的 名称(Name) ,Chef 会用它做唯一标识和日志输出; -
version、uid、home等是该资源的 属性(Property) ,用于精确描述终态; -
action :install和action :create是该资源的 动作(Action) ,表示你希望 Chef 对该资源执行的操作。
提示:
:install和:create是默认动作,可以省略。但像service 'nginx'的默认动作是:nothing,必须显式写action [:enable, :start],否则 Chef 根本不会去碰这个服务。这是新手踩坑最多的地方之一——不是 recipe 写错了,而是忘了写动作。
2.2 上下文层:为声明提供“决策依据”
光有声明不够,现实世界充满条件分支。Chef 提供了
if
、
unless
、
only_if
、
not_if
四种主流控制方式,但它们的语义和触发时机完全不同,不能混用。
-
if/unless是 Ruby 层面的条件判断,在 recipe 解析阶段(compile phase)就执行,决定某段资源声明是否被加载进 Chef 的资源集合(Resource Collection)。它适合判断平台类型、Chef 客户端版本、节点属性等静态信息。
if node['platform_family'] == 'debian'
package 'nginx' do
# Debian 系统安装 nginx
end
elsif node['platform_family'] == 'rhel'
package 'nginx' do
# RHEL 系统安装 nginx
end
end
-
only_if/not_if是资源层面的守卫条件,在 converge phase(执行阶段)才检查,用于判断“当前系统状态是否满足执行该资源的前提”。它调用的是系统命令或 Ruby 表达式,返回true/false决定是否跳过该资源。
execute 'install_docker' do
command 'curl -sSL https://get.docker.com | sh'
only_if { ::File.exist?('/usr/bin/curl') }
end
注意:
only_if { ::File.exist?('/usr/bin/curl') }中的::File是 Ruby 全局命名空间写法,避免与 Chef 自己的file资源冲突。这是实操中容易忽略的细节,不加::可能导致语法错误。
2.3 执行层:当声明无法覆盖时的“最后一公里”
Chef 的设计哲学是“尽量用声明式资源”,但总有例外。比如你需要运行一段临时脚本、调用某个未封装成资源的 CLI 工具、或者做一次性的数据迁移。这时就要用
execute
或
bash
资源。但请注意:它们不是“万能胶水”,而是“高风险补丁”。
bash 'setup_ruby_env' do
code <<-EOH
echo 'export PATH="/opt/rubies/ruby-3.1.4/bin:$PATH"' >> /etc/profile.d/ruby.sh
chmod +x /etc/profile.d/ruby.sh
EOH
environment({ 'HOME' => '/root' })
not_if { ::File.exist?('/etc/profile.d/ruby.sh') }
end
这段代码的问题在于:它用
bash
资源强行修改了
/etc/profile.d/
,但 Chef 并不知道这个文件的内容应该是什么,也无法校验它是否被正确写入。如果后续有人手动改了这个文件,Chef 下次 converge 时不会修复它——因为它没有对应的
file
资源来声明终态。更稳妥的做法是:
template '/etc/profile.d/ruby.sh' do
source 'ruby.sh.erb'
mode '0755'
variables(
ruby_path: '/opt/rubies/ruby-3.1.4/bin'
)
end
再配合一个
ruby.sh.erb
模板文件:
#!/bin/sh
export PATH="<%= @ruby_path %>:${PATH}"
这样,Chef 就能持续保证
/etc/profile.d/ruby.sh
的内容、权限、可执行性完全符合预期。这才是配置管理该有的样子。
3. 从零开始写一个真实可用的 Chef Recipe:部署轻量级监控 Agent
我们来写一个完整的、可直接上手的 recipe,目标是在 Ubuntu 22.04 节点上部署
telegraf
(InfluxData 开源的指标采集 Agent),并确保它:
- 使用官方 APT 仓库安装最新稳定版;
-
配置文件
/etc/telegraf/telegraf.conf由模板生成,包含自定义采集项; - 服务自动启用并启动;
- 如果配置文件被手动修改,下次 converge 时自动恢复。
这个场景很典型:它涉及包管理、文件模板、服务管理、条件判断,几乎覆盖了 80% 的日常运维需求。
3.1 第一步:创建 cookbook 和 recipe 结构
Chef 的最小可部署单元是
cookbook
,recipe 是其中的“菜谱”。我们用
chef generate cookbook monitoring
创建骨架,然后进入
recipes/default.rb
开始编写。
注意:不要用
chef generate recipe单独生成 recipe 文件。Chef 的最佳实践是每个功能模块一个 cookbook,recipe 是它的组成部分。把所有东西塞进一个 cookbook 会导致后期维护爆炸。
3.2 第二步:声明 Telegraf 包安装(声明层)
# Add InfluxData repository key
apt_package 'influxdb-keyring' do
package_name 'influxdb-keyring'
action :install
end
# Add InfluxData APT repository
apt_repository 'influxdata' do
uri 'https://repos.influxdata.com/debian'
components ['stable']
arch 'amd64'
deb_src false
key 'https://repos.influxdata.com/influxdb.key'
action :add
end
# Install telegraf package
apt_package 'telegraf' do
action :install
end
这里用了三个资源:
apt_package
(安装 key 包)、
apt_repository
(添加源)、
apt_package
(安装主程序)。关键点:
-
apt_repository的key属性支持 URL,Chef 会自动下载并导入 GPG 密钥; -
components ['stable']明确指定只启用stable组件,避免意外拉取testing或unstable版本; -
没有写
version属性,意味着接受仓库里的最新稳定版——这符合监控 agent 的更新策略(我们希望它及时获得安全补丁)。
3.3 第三步:用模板生成配置文件(上下文层 + 声明层)
先创建模板文件
templates/default/telegraf.conf.erb
:
[agent]
interval = "10s"
round_interval = true
metric_batch_size = 1000
metric_buffer_limit = 10000
collection_jitter = "0s"
flush_interval = "10s"
flush_jitter = "0s"
precision = ""
hostname = "<%= @node_hostname %>"
omit_hostname = false
[[outputs.influxdb_v2]]
urls = ["https://<%= @influxdb_url %>"]
token = "<%= @influxdb_token %>"
organization = "<%= @influxdb_org %>"
bucket = "<%= @influxdb_bucket %>"
[[inputs.cpu]]
percpu = true
totalcpu = true
collect_cpu_time = false
report_active = false
[[inputs.disk]]
ignore_fs = ["tmpfs", "devtmpfs", "devfs", "iso9660", "overlay", "aufs", "squashfs"]
再在
default.rb
中引用它:
# Generate telegraf config from template
template '/etc/telegraf/telegraf.conf' do
source 'telegraf.conf.erb'
owner 'root'
group 'root'
mode '0644'
variables(
node_hostname: node['fqdn'],
influxdb_url: node['monitoring']['influxdb']['url'],
influxdb_token: node['monitoring']['influxdb']['token'],
influxdb_org: node['monitoring']['influxdb']['organization'],
influxdb_bucket: node['monitoring']['influxdb']['bucket']
)
notifies :restart, 'service[telegraf]', :delayed
end
这里的关键技巧:
-
variables传入的值全部来自node属性,而不是硬编码。这意味着配置是可复用的:同一份 recipe,通过不同节点的node['monitoring']属性,就能部署到测试、预发、生产环境; -
notifies :restart, 'service[telegraf]', :delayed表示:当这个模板文件内容发生变更时,延迟通知service[telegraf]资源执行:restart动作。:delayed是重点——它把所有 notify 收集起来,等所有资源 converge 完成后再统一执行,避免频繁重启服务。
3.4 第四步:管理 Telegraf 服务(声明层 + 执行层)
# Ensure telegraf service is enabled and running
service 'telegraf' do
action [:enable, :start]
supports status: true, restart: true, reload: true
end
# Optional: verify service is actually running (extra safety)
ruby_block 'verify_telegraf_running' do
block do
require 'open3'
stdout, stderr, status = Open3.capture3('systemctl is-active telegraf')
raise "Telegraf service is not active: #{stdout.strip}" unless status.success? && stdout.strip == 'active'
end
not_if { ::File.exist?('/proc/1') && ::File.read('/proc/1/cmdline').include?('systemd') }
end
最后一段
ruby_block
是典型的“执行层”补充。它用 Ruby 调用
systemctl
验证服务状态,并在失败时抛出异常中断 converge。
not_if
条件确保只在 systemd 系统上执行(避免在 SysVinit 机器上报错)。这种“声明为主、执行兜底”的组合,是写出健壮 recipe 的关键心法。
4. 避坑指南:那些只有亲手试过才知道的“暗礁”
写 Chef recipe 最痛苦的不是学不会语法,而是被一些极其隐蔽的细节卡住数小时,查日志、翻文档、问社区,最后发现只是少了一个冒号、多了一个空格、或者搞错了执行阶段。我把这些年踩过的、最常被问到的坑,按严重程度排序列出来。
4.1 “Failed to install Homebrew Portable Ruby” 类错误的根源
这个错误在 macOS 上高频出现,尤其当你用
chef-client
命令本地运行时。根本原因不是 Chef 本身,而是 Chef Desktop(旧称 ChefDK)自带的 Ruby 运行时与 macOS 系统 Ruby、Homebrew Ruby 之间的版本冲突。
具体链路是:
- Chef Desktop 3.x+ 默认捆绑 Ruby 3.1.x;
-
但某些老版本的 Chef cookbook(尤其是依赖
build-essential的)会尝试调用brew install ruby; - Homebrew 检测到系统已有 Ruby 3.1.x,拒绝降级安装,报错 “your system version is too old”(实际是太新了);
- Chef 流程中断。
解决方案不是升级 Homebrew,而是绕过它 :
# 在 metadata.rb 中明确指定不使用 brew
depends 'build-essential', '~> 9.0'
# 在 attributes/default.rb 中覆盖 build-essential 的行为
default['build-essential']['compile_time'] = false
default['build-essential']['install_compiler'] = false
更彻底的办法是:在 macOS 节点上,直接用
chef-client --local-mode --runlist 'recipe[monitoring]'
,并确保你的 cookbook 不依赖任何需要编译的 gem(如
mysql2
)。如果真需要,改用
chef exec gem install xxx
在 Chef Ruby 环境下安装,而不是依赖
build-essential
。
4.2 “The automation license manager service has not been started!” 的映射逻辑
这个错误常见于工业自动化软件(如 Siemens TIA Portal、Automation Studio),但它在 Chef 场景下有镜像问题:你写了
service 'automationlm' do action :start end
,但 converge 后服务还是 inactive。
原因通常是:
-
服务名不匹配:
systemctl list-unit-files | grep auto查到的真实 unit 名是automation-license-manager.service,而你在 recipe 里写了service 'automationlm'; -
依赖服务未启动:
automationlm依赖postgresql或redis,但你的 recipe 没声明这些前置依赖; - SELinux/AppArmor 限制:在 CentOS/RHEL 上,安全模块阻止了服务访问必要路径。
排查三步法 :
-
sudo chef-client --log-level debug运行,看日志里service[xxx]资源的converge输出是否显示start成功; -
sudo systemctl status automation-license-manager.service查看详细错误(常含Failed to start后的reason); -
sudo journalctl -u automation-license-manager.service -n 50 --no-pager看服务自身日志。
终极 fix
:永远用
systemctl cat xxx.service
查看服务 unit 文件,复制 exact name 到 recipe 中,并用
service
资源的
supports
属性声明能力:
service 'automation-license-manager' do
action [:enable, :start]
supports status: true, restart: true, reload: false
# 如果 unit 文件里写了 Wants=postgresql.service,则必须先确保 postgresql 已启动
end
4.3 模板变量失效:ERB 中的
@
符号陷阱
这是 Ruby 和 Chef 混合编程里最经典的“幽灵 Bug”。你写了:
# templates/default/app.conf.erb
log_level = "<%= @log_level %>"
并在 recipe 中传入
variables(log_level: 'info')
,但生成的文件里
log_level = ""
。
原因:
@log_level
是实例变量(instance variable),而 Chef 的 ERB 模板上下文是
Chef::Provider::Template::Renderer
的实例,它
不自动代理
variables
哈希中的键为实例变量
。正确的写法是:
log_level = "<%= @new_resource.variables[:log_level] %>"
# 或更简洁(Chef 推荐):
log_level = "<%= @log_level %>" # ✅ 但前提是 recipe 中用 variables(log_level: 'info') 传入
等等,这不矛盾吗?不矛盾。Chef 的模板渲染器做了特殊处理:它会把
variables
哈希的每个 key,自动挂载为
@key
形式的实例变量。所以
variables(log_level: 'info')
→
@log_level
可用;但
variables({ log_level: 'info' })
(哈希字面量)→
@log_level
不可用,必须用
@new_resource.variables[:log_level]
。
实操心得:永远用
variables(key: value)形式传参,不要用variables({ key: value })。前者是 Chef DSL 语法糖,后者是纯 Ruby 哈希,语义完全不同。
4.4 “Totally Integrated Automation” 式的过度设计反模式
这个词出自西门子,形容其产品线深度耦合。在 Chef 世界里,它对应一种危险倾向:试图用一个 mega-recipe 涵盖从 OS 初始化、Docker 安装、K8s 集群部署、到应用发布的全部流程。
后果是灾难性的:
- 单个 recipe 超过 500 行,无法做原子性测试;
- 一处修改引发连锁失败,回滚成本极高;
- 新人无法理解依赖关系,不敢动任何一行;
-
chef-clientconverge 时间从 30 秒飙升到 15 分钟。
我的解耦原则 :
-
每个 cookbook 只解决一个明确问题:
os-hardening、docker-ce、kubernetes-node、myapp-web; -
recipe 只做“本层”事:
docker-cecookbook 的 default.rb 只管 Docker 引擎安装和基础配置,不碰容器镜像; -
用
include_recipe组合,不用 copy-paste 复制逻辑; -
用
depends声明强依赖,用suggests声明弱依赖。
例如,部署一个 Web 应用的完整 run-list 应该是:
recipe[os-hardening]
,
recipe[docker-ce]
,
recipe[kubernetes-node]
,
recipe[myapp-web]
而不是一个
recipe[full-stack-deployment]
。前者可独立测试、灰度发布、快速迭代;后者就是一颗定时炸弹。
5. 实战调试:从 “Converge Failed” 到 “Converge Complete” 的完整推演
理论讲完,我们模拟一次真实的故障排查。假设你刚写完上面的
telegraf
recipe,上传到 Chef Server,然后在一台 Ubuntu 22.04 节点上运行
sudo chef-client
,结果报错:
* apt_package[telegraf] action install
================================================================================
Error executing action `install` on resource 'apt_package[telegraf]'
================================================================================
Mixlib::ShellOut::ShellCommandFailed
------------------------------------
Expected process to exit with [0], but received '100'
---- Begin output of apt-get -q -y install telegraf=1.28.1-1 ----
STDOUT: Reading package lists...
Building dependency tree...
Reading state information...
E: Version '1.28.1-1' for 'telegraf' was not found
---- End output of apt-get -q -y install telegraf=1.28.1-1 ----
5.1 第一层:看错误关键词,定位资源
错误信息开头就指明了:
apt_package[telegraf] action install
失败。说明问题出在包安装环节,不是模板、不是服务。立刻聚焦到 recipe 中
apt_package 'telegraf'
这一段。
5.2 第二层:分析错误输出,抓住核心线索
关键句:
E: Version '1.28.1-1' for 'telegraf' was not found
。这说明 Chef 尝试安装一个
精确版本
,但 APT 仓库里没有这个版本。为什么 Chef 会指定精确版本?因为你可能在
apt_package
资源里写了
version '1.28.1-1'
,或者你的
metadata.rb
里锁定了 cookbook 版本,而那个版本的
attributes/default.rb
设了
default['telegraf']['version'] = '1.28.1-1'
。
5.3 第三层:验证假设,动手确认
登录目标节点,手动执行 Chef 试图执行的命令:
# 查看 telegraf 在仓库里的可用版本
apt-cache policy telegraf
# 输出类似:
# telegraf:
# Installed: (none)
# Candidate: 1.29.2-1
# Version table:
# 1.29.2-1 500
# 500 https://repos.influxdata.com/debian jammy/stable amd64 Packages
# 1.28.3-1 500
# 500 https://repos.influxdata.com/debian jammy/stable amd64 Packages
果然,
1.28.1-1
不在列表里。最新的是
1.29.2-1
。
5.4 第四层:选择修复路径,评估影响
有两个选择:
-
方案A(推荐)
:删掉
version属性,让 Chef 安装Candidate版本(即最新稳定版); -
方案B
:把
version改成1.29.2-1,并加allow_downgrade true(以防未来仓库里删了这个版本)。
选 A 的理由:监控 agent 需要保持更新,锁定小版本反而增加维护负担;Chef 的设计哲学就是“声明终态”,而“最新稳定版”就是一个合理的终态描述。
5.5 第五层:修改、测试、验证闭环
修改 recipe:
# BEFORE
apt_package 'telegraf' do
version '1.28.1-1'
action :install
end
# AFTER
apt_package 'telegraf' do
action :install
end
然后本地测试(不走 Chef Server):
# 在节点上,用本地模式运行
sudo chef-client --local-mode --runlist 'recipe[monitoring]'
观察输出:
-
apt_package[telegraf]行应显示install成功; -
template[/etc/telegraf/telegraf.conf]应显示up to date(首次运行是create); -
service[telegraf]应显示enable和start成功; -
最后一行是
Chef Client finished, 3/3 resources updated。
再验证终态:
dpkg -l | grep telegraf # 确认已安装 1.29.2-1
sudo systemctl status telegraf # 确认 active (running)
sudo telegraf --test | head -20 # 确认配置语法正确,能采集指标
全部通过,converge 成功。整个过程从看到报错到修复上线,不超过 10 分钟。这就是掌握 Chef 调试逻辑后的效率。
6. 进阶思考:Recipes 如何融入现代 DevOps 流水线
写好单个 recipe 只是起点。真正的价值在于让它成为 CI/CD 流水线的一环。我目前在团队里推行的 Chef 流水线是四阶模型:
6.1 阶段一:本地开发与单元测试(Dev)
-
用
chef exec rspec运行 recipe 的 RSpec 单元测试,验证资源声明逻辑; -
用
foodcritic检查 DSL 最佳实践(如是否用了not_if替代if); -
用
cookstyle(基于 RuboCop)检查 Ruby 代码风格。
6.2 阶段二:集成测试(Test)
-
用
kitchen(Test Kitchen)在 Docker/VirtualBox 中启动真实 Ubuntu/CentOS 虚拟机; -
kitchen converge运行 recipe; -
kitchen verify用 InSpec 断言终态(如describe package('telegraf') do it { should be_installed } end); - 这一步确保 recipe 在目标平台上 100% 可运行。
6.3 阶段三:自动化发布(CI)
- Git Push 触发 GitHub Actions;
-
自动运行
kitchen test; -
全部通过后,自动打 tag(如
v1.2.0),并knife cookbook upload monitoring到 Chef Server; -
更新
environments/production.json的 cookbook 版本约束。
6.4 阶段四:灰度发布与监控(CD)
- 生产环境分批 rollout:先 5% 节点 → 观察 1 小时 → 50% → 全量;
- 所有节点上报 converge 日志到 ELK;
-
设置告警:
converge failed超过 2 次/小时,或telegraf服务 down 超过 5 分钟; -
每次发布后,自动触发
influxdb查询,对比发布前后 CPU、内存、磁盘 I/O 基线,确认无性能 regress。
这个流水线把 Chef Recipes 从“手工运维脚本”升级为“可测试、可发布、可监控”的软件制品。它不再是一个人的经验沉淀,而是一套组织级的基础设施交付标准。
最后分享一个小技巧:我在每个 cookbook 的
README.md
里,都放一个
Quick Start
区块,里面是三行命令:
# 1. 安装依赖
chef exec bundle install
# 2. 本地测试
kitchen test
# 3. 发布到 Chef Server(需配置 knife.rb)
knife cookbook upload monitoring
新人 clone 代码后,复制粘贴这三行,5 分钟内就能跑通整个流程。技术文档的价值,不在于写得多全,而在于让第一个读者,能在最短时间内,亲手做出第一个成功。
我在实际使用中发现,最难的从来不是写对一行代码,而是让团队所有人建立起对“终态一致性”的敬畏感。当大家不再问“这个脚本跑完没?”,而是问“这个节点的 converge 状态是 up-to-date 还是 out-of-sync?”,你就知道,配置管理的种子,真的发芽了。

4197

被折叠的 条评论
为什么被折叠?



