运维知识
悠悠
2026年4月17日

从零开始搞定Chef自动化部署,让服务器管理不再头疼

最近公司的服务器数量又增加了不少,每次部署新应用都要重复执行一堆命令,配置文件改来改去的,实在是太麻烦了。我琢磨着得找个自动化工具来解决这个问题,经过一番调研,最终选择了Chef。

说实话,刚开始接触Chef的时候,被它的概念搞得有点晕。什么Cookbook、Recipe、Node、Role的,感觉像是在学做菜一样。不过用了一段时间后发现,这玩意儿还真挺好使的,今天就把我踩过的坑和实践经验分享给大家。

Chef到底是个啥

简单来说,Chef就是一个自动化配置管理工具。你可以把它理解成一个"厨师",按照你写好的"菜谱"(Cookbook),自动在服务器上"做菜"(配置软件和服务)。

我觉得Chef最大的优势就是它用Ruby写配置,代码即配置的理念让运维工作变得更加程序化。不像以前写shell脚本那样,到处都是if-else判断,Chef的DSL(领域特定语言)让配置文件看起来更清晰。

搭建Chef环境

部署Chef主要有三个组件:Chef Server、Chef Workstation和Chef Client。我当时是在Ubuntu 20.04上搭建的,过程还算顺利。

安装Chef其实挺简单的,在工作站上执行:

curl -L https://omnitruck.chef.io/install.sh | sudo bash

这个脚本会自动检测你的系统,下载对应版本的Chef。安装完成后,可以用chef --version检查一下版本。

配置Chef Server稍微麻烦点。我是直接用的Chef官方提供的包,大概有200多MB:

wget https://packages.chef.io/files/stable/chef-server/14.0.0/ubuntu/20.04/chef-server-core_14.0.0-1_amd64.deb
sudo dpkg -i chef-server-core_14.0.0-1_amd64.deb
sudo chef-server-ctl reconfigure

配置过程需要等一会儿,它会自动安装PostgreSQL、RabbitMQ等依赖服务。配置完成后,创建管理员用户和组织:

sudo chef-server-ctl user-create admin Admin User admin@example.com 'password' --filename admin.pem
sudo chef-server-ctl org-create myorg 'My Organization' --association_user admin --filename myorg-validator.pem

这里生成的.pem文件很重要,是后续连接Chef Server的凭证,一定要保存好。

写第一个Cookbook

Cookbook是Chef的核心概念,可以理解为一个配置包,里面包含了Recipe(具体的配置步骤)、Attributes(变量)、Templates(模板文件)等。

创建Cookbook很简单:

chef generate cookbook nginx_setup

这会生成一个标准的Cookbook目录结构。我第一次写的是一个安装配置Nginx的Cookbook,在recipes/default.rb里写入:

# 安装nginx包
package 'nginx' do
  action :install
end

# 创建网站目录
directory '/var/www/mysite' do
  owner 'www-data'
  group 'www-data'
  mode '0755'
  action :create
end

# 配置nginx
template '/etc/nginx/sites-available/mysite' do
  source 'mysite.conf.erb'
  owner 'root'
  group 'root'
  mode '0644'
  variables(
    server_name: node['nginx_setup']['server_name'],
    port: node['nginx_setup']['port']
  )
  notifies :restart, 'service[nginx]', :delayed
end

# 启用站点
link '/etc/nginx/sites-enabled/mysite' do
  to '/etc/nginx/sites-available/mysite'
  notifies :restart, 'service[nginx]', :delayed
end

# 确保nginx服务运行
service 'nginx' do
  action [:enable, :start]
end

这个Recipe做了几件事:安装Nginx包、创建网站目录、使用模板生成配置文件、启用站点、确保服务运行。看起来是不是比写shell脚本清晰多了?

模板的妙用

Chef的模板功能特别实用。在templates/default/mysite.conf.erb中:

server {
    listen <%= @port %>;
    server_name <%= @server_name %>;
  
    root /var/www/mysite;
    index index.html index.htm;
  
    location / {
        try_files $uri $uri/ =404;
    }
  
    access_log /var/log/nginx/<%= @server_name %>_access.log;
    error_log /var/log/nginx/<%= @server_name %>_error.log;
}

模板中的变量会在运行时被替换,这样同一个模板可以用于不同的环境,只需要改变attributes就行了。

Attributes的灵活运用

Attributes是Chef中用来存储配置数据的地方。在attributes/default.rb中定义默认值:

default['nginx_setup']['server_name'] = 'example.com'
default['nginx_setup']['port'] = 80
default['nginx_setup']['worker_processes'] = 'auto'
default['nginx_setup']['worker_connections'] = 1024

这些属性可以在不同层级被覆盖,比如在Role、Environment或者Node级别。这种机制让配置管理变得非常灵活。

我在实际使用中,通常会为不同环境创建不同的Environment文件:

# environments/production.rb
name 'production'
description 'Production environment'

override_attributes(
  'nginx_setup' => {
    'server_name' => 'prod.example.com',
    'worker_processes' => 4,
    'worker_connections' => 2048
  }
)

Data Bags存储敏感信息

处理敏感信息时,Data Bags是个好选择。比如数据库密码、API密钥这些,不适合直接写在代码里。

创建一个加密的Data Bag:

knife data bag create credentials mysql --secret-file /path/to/secret_key

然后在Recipe中使用:

mysql_creds = Chef::EncryptedDataBagItem.load('credentials', 'mysql')

mysql_service 'default' do
  port '3306'
  version '5.7'
  initial_root_password mysql_creds['root_password']
  action [:create, :start]
end

这样密码就不会明文出现在代码中了,安全性大大提高。

实战案例:部署一个完整的Web应用栈

说了这么多理论,来个实际的例子。我曾经用Chef部署过一个典型的LAMP栈应用,包括Nginx作为反向代理、多个PHP-FPM实例、MySQL数据库。

整个Cookbook的结构是这样的:

webapp_stack/
├── recipes/
│   ├── default.rb
│   ├── nginx.rb
│   ├── php.rb
│   ├── mysql.rb
│   └── app_deploy.rb
├── templates/
│   ├── nginx_vhost.conf.erb
│   ├── php_pool.conf.erb
│   └── app_config.php.erb
├── attributes/
│   └── default.rb
└── metadata.rb

recipes/default.rb中,我按顺序调用其他Recipe:

include_recipe 'webapp_stack::mysql'
include_recipe 'webapp_stack::php'
include_recipe 'webapp_stack::nginx'
include_recipe 'webapp_stack::app_deploy'

PHP-FPM的配置比较有意思,我用了一个循环来创建多个池:

# recipes/php.rb
node['webapp_stack']['php_pools'].each do |pool_name, pool_config|
  template "/etc/php/7.4/fpm/pool.d/#{pool_name}.conf" do
    source 'php_pool.conf.erb'
    variables(
      pool_name: pool_name,
      user: pool_config['user'],
      group: pool_config['group'],
      listen: pool_config['listen'],
      pm_max_children: pool_config['pm_max_children']
    )
    notifies :restart, 'service[php7.4-fpm]', :delayed
  end
end

这样可以很方便地根据需要创建多个PHP-FPM池,实现应用隔离。

使用Role组织节点

当服务器数量增多后,一个个管理Node会很麻烦。这时候Role就派上用场了。我通常会创建几个标准角色:

# roles/web_server.rb
name 'web_server'
description 'Web server role'
run_list(
  'recipe[webapp_stack::nginx]',
  'recipe[webapp_stack::php]'
)

default_attributes(
  'nginx_setup' => {
    'worker_processes' => 2
  }
)
# roles/db_server.rb
name 'db_server'
description 'Database server role'
run_list(
  'recipe[webapp_stack::mysql]'
)

default_attributes(
  'mysql' => {
    'bind_address' => '0.0.0.0'
  }
)

然后给节点分配角色:

knife node run_list add web01.example.com 'role[web_server]'
knife node run_list add db01.example.com 'role[db_server]'

调试技巧

写Cookbook的过程中难免会遇到问题,分享几个调试技巧。

使用chef-client--why-run模式可以预览会执行哪些操作,不会真正执行:

sudo chef-client --why-run

在Recipe中可以用log资源输出调试信息:

log "Current value of server_name: #{node['nginx_setup']['server_name']}" do
  level :info
end

如果某个资源执行失败,可以用ignore_failure让Chef继续执行:

package 'some-optional-package' do
  ignore_failure true
end

但这个要谨慎使用,可能会掩盖真正的问题。

版本管理和测试

Cookbook也是代码,当然要做版本管理。我的做法是每个Cookbook都是一个Git仓库,在metadata.rb中管理版本:

name 'webapp_stack'
maintainer 'Your Name'
maintainer_email 'your@email.com'
license 'All Rights Reserved'
description 'Installs/Configures webapp stack'
version '1.2.3'

depends 'mysql', '~> 8.0'
depends 'php', '~> 4.0'

版本号遵循语义化版本规范,这样在升级时能清楚知道变更的影响范围。

测试方面,可以用Test Kitchen进行集成测试:

# .kitchen.yml
driver:
  name: vagrant

provisioner:
  name: chef_zero

platforms:
  - name: ubuntu-20.04

suites:
  - name: default
    run_list:
      - recipe[webapp_stack::default]
    attributes:
      webapp_stack:
        server_name: test.example.com

运行kitchen test就能在虚拟机中测试Cookbook了。

一些坑和经验

使用Chef这段时间,也踩了不少坑。比如有一次,我在Recipe中直接修改了配置文件,结果每次运行chef-client都会触发服务重启,即使配置没有变化。后来才知道要用only_ifnot_if来添加条件判断:

template '/etc/nginx/nginx.conf' do
  source 'nginx.conf.erb'
  notifies :restart, 'service[nginx]', :delayed
  not_if { ::File.exist?('/etc/nginx/nginx.conf') && 
           ::File.read('/etc/nginx/nginx.conf').include?('# Managed by Chef') }
end

还有一个经验是,不要在Recipe中使用太多Ruby的高级特性。虽然Chef支持完整的Ruby语法,但过于复杂的代码会让其他人难以理解和维护。保持简单直观是最好的。

资源的执行顺序也很重要。Chef默认按照Recipe中的顺序执行,但有时需要用notifiessubscribes来控制资源间的依赖关系:

template '/etc/app/config.yml' do
  source 'config.yml.erb'
  notifies :restart, 'service[app]', :immediately
end

service 'app' do
  action :nothing
  subscribes :restart, 'template[/etc/app/config.yml]', :immediately
end

性能优化

当管理的节点数量增多后,性能问题就显现出来了。几个优化建议:

  1. 合理使用lazy属性评估,避免不必要的计算:
template '/etc/app/config.yml' do
  variables lazy {
    {
      db_host: search(:node, 'role:database').first['ipaddress']
    }
  }
end
  1. 减少search的使用,search操作比较耗时,可以考虑用Data Bags或Attributes代替。
  2. 使用chef-client的定时运行而不是持续运行模式,减少资源消耗。

文章目录

博主介绍

热爱技术的云计算运维工程师,Python全栈工程师,分享开发经验与生活感悟。
欢迎关注我的微信公众号@运维躬行录,领取海量学习资料

微信二维码