如果你做过服务器配置,肯定遇到过这种场景:一台机器在测试环境里好好的,一到生产环境就各种不对劲。其实很多时候不是代码问题,而是环境的差异没有在配置阶段处理好。Chef 这个工具,就是把配置本身变成代码,然后通过 Ruby DSL 在代码里加上判断,让不同环境、不同角色的机器各自生成自己的配置。今天我就从最基础的条件逻辑开始,聊聊怎么用 Chef 写出真正会看情况的配置。

一、Chef DSL 是啥玩意儿?

Chef 的核心思路是“基础设施即代码”。你把服务器上要装的软件、要改的配置、要启动的服务,都用代码描述出来,Chef 来负责执行。它底层用的是 Ruby,但你别怕,平时写配方的时候基本不用写类、继承那一套,而是用 Ruby DSL 的语法,像写配置清单一样。

可以这样理解:DSL 就像菜单,你不需要知道厨师怎么炒菜,只需要选择菜品。Chef 的 DSL 就是把安装软件、启动服务这些操作变成菜单上的选项,你负责组合选项,Chef 负责炒菜。因为菜单本身是 Ruby 写的,所以点菜时你还可以说“如果今天有牛肉,就来牛肉面”,这就是条件逻辑。

比如下面这个最简单的配方,就是安装 Nginx 并让它开机自启:

# 技术栈:Chef Ruby DSL
# 安装 Nginx 软件包
package 'nginx' do
  action :install
end

# 启动并设置 Nginx 开机自启
service 'nginx' do
  action [:enable, :start]
end

看,代码本身很直白。正因为它的本质是 Ruby,你可以在里面用判断、分支、循环这些编程逻辑。这就是 Chef 比普通脚本灵活的地方。

二、条件逻辑的基本写法

2.1 用 if 和 elsif 判断平台

不同操作系统,软件包名字可能完全不一样。CentOS 上叫 httpd,Ubuntu 上叫 apache2,你要是写死一个名字,换台机器就挂了。用 if 判断一下平台就行:

# 技术栈:Chef Ruby DSL
# 根据当前系统的 platform 属性选择要安装的包
if node['platform'] == 'centos'
  package 'httpd' do
    action :install
  end
elsif node['platform'] == 'ubuntu'
  package 'apache2' do
    action :install
  end
else
  log '不知名的系统,先不装'
end

在这个例子里,node 对象是 Chef 提供的节点信息入口,里面记录了机器的操作系统、内存、磁盘、IP 等很多内容。这些信息是自动采集的,你不需要手动填。

2.2 反向条件怎么写

有时候你想说的是“除非……否则……”。比如“除非 wget 已经装好了,否则就装上它”,用反向条件很顺手:

# 技术栈:Chef Ruby DSL
# 如果系统的包列表里没有 wget,就执行安装
unless node['packages'].key?('wget')
  package 'wget' do
    action :install
  end
end

这种写法就是“如果不满足条件就执行”。如果你更喜欢用感叹号表示否定,也能达到一样的效果,但反向条件读起来更亲切,也更适合非程序员理解。

2.3 把分支逻辑写清楚

如果你有一堆平台要判断,if 写多了会显得乱。按平台族分类处理的分支语句更适合这种场景:

# 技术栈:Chef Ruby DSL
# 按平台族分类处理
case node['platform_family']
when 'rhel', 'fedora'
  package 'httpd' do
    action :install
  end
when 'debian'
  package 'apache2' do
    action :install
  end
else
  log "这个平台族 #{node['platform_family']} 我没见过"
end

这里有个小知识点:Ruby 的字符串插值需要用井号加花括号,也就是把变量嵌进字符串。这种写法在动态配置里很常见。

2.4 资源自带的条件判断

除了在配方里写判断,Chef 的资源本身也支持条件参数。比如“只有当前没有 Nginx 进程时才启动 Nginx”,就可以写成:

# 技术栈:Chef Ruby DSL
# 执行 Nginx 启动命令,但已经有进程在跑就跳过
execute 'start_nginx' do
  command 'nginx'
  not_if 'pgrep nginx'
end

这种条件参数的执行时机和普通条件不一样。普通条件在编译配置阶段就已经决定好了,而资源条件会在真正执行动作之前临时检查状态。这个差异在复杂场景里很重要。

2.5 循环加条件,批量处理更简单

Chef 的 DSL 允许直接用 Ruby 的循环。比如你要创建一批用户,但又不想覆盖已经存在的用户,可以这样写:

# 技术栈:Chef Ruby DSL
# 要创建的用户名单
users = %w(alice bob carol)

users.each do |user|
  # 如果系统中已经有这个用户,就跳过
  next if node['etc']['passwd'].key?(user)

  user user do
    action :create
  end
end

这里的跳过逻辑和普通 Ruby 循环一样,意思是“跳过本次循环”。这种循环加条件组合起来,能减少很多重复的配方代码。

三、动态配置的典型场景

3.1 用 Ruby 拼接出配置内容

动态配置最直接的做法,就是把配置文件的内容放在变量里,等到运行时再拼出来。比如配置一个 Nginx 虚拟主机:

# 技术栈:Chef Ruby DSL
# 从节点属性读取端口和根目录,拼成配置内容
nginx_conf = "server {\n"
nginx_conf << "  listen #{node['nginx']['port']};\n"
nginx_conf << "  root #{node['nginx']['root']};\n"
nginx_conf << "}\n"

file '/etc/nginx/conf.d/my_site.conf' do
  content nginx_conf
  owner 'root'
  group 'root'
  mode '0644'
end

这里 file 资源负责把变量内容写到指定路径。文件内容不是写死的,而是根据属性动态生成。以后改端口,只需要改节点属性,不需要动配方本身。

3.2 结合环境动态调整配置

生产环境和测试环境的差异,通常是运维大头。你可以用当前环境名称做判断,生成不同的配置:

# 技术栈:Chef Ruby DSL
# 根据环境决定日志路径
log_root = case node['chef_environment']
           when 'production'
             '/var/log/myapp/prod'
           when 'staging'
             '/var/log/myapp/staging'
           else
             '/var/log/myapp/dev'
           end

# 根据环境决定是否启用 HTTPS
ssl_enabled = node['chef_environment'] == 'production'

# 把上面这些变量拼进 Nginx 配置
nginx_conf = <<~EOF
  server {
    listen #{ssl_enabled ? 443 : 80};
    root #{node['application']['root']};
    access_log #{log_root}/access.log;
  }
EOF

file '/etc/nginx/conf.d/myapp.conf' do
  content nginx_conf
  mode '0644'
end

这个示例里既有分支判断,也有三元表达式。只要 Ruby 语法过关,你就能写出非常灵活的配置生成逻辑。

3.3 用数据包做“配置字典”

Chef 的数据包可以看作一个 JSON 字典。你可以在数据包里存数据库地址、用户名、端口等,然后按需读取:

# 技术栈:Chef Ruby DSL
# 从名为 secrets 的数据包里读取 database 项
db_config = data_bag_item('secrets', 'database')

# 根据数据库类型安装对应客户端
case db_config['engine']
when 'mysql'
  package 'mysql-client' do
    action :install
  end
when 'postgresql'
  package 'postgresql-client' do
    action :install
  end
end

# 动态生成应用配置文件
file "/etc/myapp/#{node['app']['name']}.yml" do
  content "db_host: #{db_config['host']}\ndb_user: #{db_config['user']}\n"
end

数据包让配置和代码分离,敏感信息还可以用加密数据包来保护,是生产环境里很实用的手段。

四、关联技术:Ohai 和 Search 是个好帮手

4.1 Ohai 自动采集节点信息

前面反复用到平台属性,这些数据从哪来?就是 Ohai。Chef 每次运行时,Ohai 会收集当前系统的各种信息,填充到节点对象里。比如判断 CPU 核数来决定要不要开启多线程配置:

# 技术栈:Chef Ruby DSL
# CPU 核数不足 4 时就跳过某些优化
if node['cpu']['total'].to_i >= 4
  include_recipe 'myapp::tune_for_multi_core'
else
  log 'CPU 核数少,不优化'
end

类似的信息还有很多,内存、磁盘、网络、内核参数,都是条件判断的素材。用好这些,你的配置才能真正“看人下菜碟”。

4.2 Search 动态发现伙伴节点

集群环境下,某台机器需要知道所有 Web 节点的 IP。Chef 的搜索功能可以直接查询节点列表:

# 技术栈:Chef Ruby DSL
# 找出所有角色为 web 的节点
web_nodes = search(:node, 'role:web')

# 把 IP 拼成一个 upstream 配置
upstream_block = web_nodes.map do |n|
  "server #{n['ipaddress']}:#{node['app']['port']};"
end.join("\n")

file '/etc/nginx/conf.d/upstream.conf' do
  content "upstream backend {\n#{upstream_block}\n}\n"
end

这个场景在负载均衡配置里特别常见。新节点加入集群后,Chef 会自动把它加进负载均衡列表,不用再手动维护 IP。

五、应用场景与优缺点分析

5.1 适合应用在哪

第一,多环境交付。开发、测试、生产环境差异大,用属性加条件逻辑就能统一管理。第二,灰度发布。先让少数节点开启新配置,验证没问题再把条件放开,扩大到整个集群。第三,混合云管理。不同云厂商的网络、存储、命名规则差异很大,用条件判断来适配最能发挥作用。第四,大规模集群自动化。新服务器一加入,Chef 就能根据角色自动安装软件、生成配置、加入负载均衡。

5.2 优点

代码复用性很高,一个配方可以在多个平台、多个环境复用。幂等性也做得好,重复执行不会把机器搞坏。再就是可审计,任何改动都在代码里留痕,符合基础设施即代码的理念。更重要的是,条件逻辑让静态的配置变成了动态的决策,机器越复杂越能体现价值。

5.3 缺点

学习曲线还是有点陡。Ruby 没接触过的人,刚开始写 DSL 容易懵。而且 Chef 里属性、数据包、搜索这些概念有点多,理解成本高。另外,条件逻辑写多了,配方会变得很乱,维护起来有难度。大规模使用搜索也会给 Chef Server 带来压力,需要控制查询频率。

六、注意事项

条件判断不要只认一个属性,尽量用平台族和具体平台搭配,避免给新系统留坑。不要把太复杂的 Ruby 逻辑直接怼在资源定义里,推荐先写成变量再使用。用数据包时,密码这类敏感信息一定要用加密数据包,别明文存。在配方里使用搜索要谨慎,尽量缩小查询范围,比如按环境过滤,避免每次都全表扫描。

还要注意资源条件参数和普通条件判断的差别。如果条件依赖某个文件在本次运行中刚生成,普通条件在编译阶段判断时可能还看不到那个文件,这时用资源条件参数会更稳。最后,不要为了炫技写出多层嵌套的条件,保持配方可读性比什么都重要。

七、总结

Chef 的 Ruby DSL 不复杂,但真正写好需要理解条件逻辑的动态性。配置文件不是写死文本,而是根据节点属性、环境、数据包、集群状态去推演。搭配 Ohai 和 Search,你才能把配置管理从“人肉维护”提升到“自动适配”。遇到新需求时,先想清楚哪些东西是固定的,哪些东西随环境变化,然后把这些变化点变成条件和属性,Chef 就能替你搞定大部分麻烦。