Chef Tutorial: Learn Configuration Management from Scratch (2026)
When I inherited a 200-server infrastructure with no documentation, Chef was the tool that brought order to chaos. Unlike ad-hoc shell scripts, Chef defines your infrastructure as code using Ruby domain-specific language. Cookbooks and recipes describe the desired state, and Chef applies that state consistently across all nodes. Chef follows a pull model: nodes run the Chef Infra Client on a schedule, fetch their policy from a Chef Infra Server, and converge toward the defined state. This tutorial covers Chef from workstation setup to cookbook development to node management, drawing from real production experiences managing fleets of both Linux and Windows servers.
Chef Architecture and Workstation Setup
Chef has three main components: the workstation where you write cookbooks, the Chef Infra Server that stores policies and cookbooks, and the Chef Infra Client that runs on each managed node. The client checks in with the server periodically — typically every 30 minutes — downloads its run list, and converges the node. Chef Development Kit (ChefDK) or Chef Workstation includes everything you need: knife for interacting with the server, chef-client for local testing, and Test Kitchen for infrastructure testing. I set up a dedicated workstation VM to keep cookbook development separate from my daily machine. The knife tool is your primary interface — upload cookbooks, bootstrap nodes, search for nodes by attribute, and manage roles and environments. Authentication uses a private key generated when you create your Chef user.
chef generate cookbook my_cookbook
cd my_cookbook
kitchen list
# Server connection
knife ssl check
knife upload cookbooks my_cookbook
Cookbooks, Recipes, and Resources
A cookbook is the fundamental unit of configuration in Chef. It contains recipes, templates, files, attributes, and metadata. Recipes are Ruby files that declare resources — package, service, file, template, user, directory — each with a state and action. Resources are idempotent: Chef checks the current state before taking action. If nginx is already installed, the package resource does nothing. If the service is already running, the service resource reports OK. This convergence model ensures nodes stay in the desired state even if someone manually changes a configuration file. I write small, focused recipes that each manage one concern — one for Nginx, one for the application deployment, one for monitoring. Metadata.rb declares cookbook dependencies, supported platforms, and version constraints.
# recipes/default.rb
package 'nginx' do
action :install
end
service 'nginx' do
action [:enable, :start]
supports restart: true, reload: true
end
template '/etc/nginx/nginx.conf' do
source 'nginx.conf.erb'
owner 'root'
group 'root'
mode '0644'
notifies :reload, 'service[nginx]'
end
Attributes, Templates, and Data Bags
Attributes parameterize cookbooks. They can be set at the cookbook level (default), by the environment, by the role, or by the node itself, with increasing precedence. I use default attributes in the cookbook's attributes/default.rb for sensible defaults, then override them in roles or environments for specific configurations. Templates use the Embedded Ruby (ERB) template system to generate configuration files dynamically based on attributes. Data bags store JSON data on the Chef Server — useful for application configuration, user accounts, or SSL certificates. Encrypted data bags add a layer of security for sensitive data like database passwords. I combine data bags with search: the client queries the Chef Server to find related nodes and dynamically configure connections.
# attributes/default.rb
default['myapp']['port'] = 3000
default['myapp']['user'] = 'appuser'
default['myapp']['log_level'] = 'info'
# templates/default/myapp.conf.erb
port <%= node['myapp']['port'] %>
user <%= node['myapp']['user'] %>
log_level <%= node['myapp']['log_level'] %>
# Searching for database nodes
search(:node, 'role:database')
Roles, Environments, and Run Lists
Roles and environments organize configuration at scale. A role describes a server's function — webserver, database, loadbalancer — and includes a run list of recipes and role-specific attributes. An environment defines the deployment stage — development, staging, production — with environment-specific attributes and cookbook version constraints. The run list is an ordered list of recipes and roles that the node applies during convergence. I assign roles to nodes during bootstrapping with knife bootstrap. The policyfile feature, introduced in Chef 12, replaces the roles-and-environments pattern with a single policy document that pins cookbook versions and declares the run list. For teams managing hundreds of nodes, policyfiles simplify the promotion workflow from development through production.
# roles/webserver.rb
name 'webserver'
description 'Web server role'
run_list 'recipe[nginx]', 'recipe[myapp::web]', 'recipe[monitoring]'
default_attributes 'nginx' => { 'worker_processes' => 4 }
# knife bootstrap
knife bootstrap 10.0.1.50 --ssh-user admin --sudo \
--node-name web-01 --run-list 'role[webserver]' \
--environment production
Chef Infra Client Run and Convergence
When the Chef Infra Client runs, it follows a sequence: authenticates with the server, fetches the node object, builds the node's run list from roles and recipes, compiles the resource collection by evaluating Ruby code, then converges each resource — comparing the current state to the desired state and making changes if needed. The Ohai tool runs at the start of every client run to gather system data — platform, filesystem, network interfaces, memory — which becomes node attributes available to recipes. If a resource fails, Chef reports the error and stops, sending the exception to the Chef Server for reporting. I use the chef-client --local-mode flag for testing cookbooks without a server. The --why-run flag shows what would change without making modifications.
sudo chef-client
sudo chef-client --local-mode --runlist 'recipe[my_cookbook]' --why-run
grep -A5 "FATAL" /var/log/chef/client.log
knife node show web-01 --attribute 'kernel'
Test Kitchen and ChefSpec for Testing
Untested cookbooks are tech debt. Test Kitchen provides a testing framework that spins up isolated environments — Docker containers, Vagrant VMs, or cloud instances — runs your cookbook, and verifies the state. I write integration tests using InSpec, a compliance testing framework that describes the expected state of a system. A test checks that a package is installed, a service is running, a port is listening, or a file has the correct permissions. ChefSpec allows unit testing of Chef resources in isolation by compiling the resource collection and asserting that specific resources and actions are called. Combined with style testing via Cookstyle (RuboCop for Chef), this gives a robust testing pyramid: unit tests for logic, integration tests for behavior, and linting for style.
# kitchen.yml
---
provisioner:
name: chef_zero
platforms:
- name: ubuntu-22.04
suites:
- name: default
run_list:
- recipe[my_cookbook::default]
verifier:
inspec_tests:
- test/integration/default
# test/integration/default/default_test.rb
control 'nginx-installed' do
impact 1.0
describe package('nginx') do
it { should be_installed }
end
describe service('nginx') do
it { should be_enabled }
it { should be_running }
end
end
Frequently Asked Questions
What is the difference between Chef and Ansible?
Chef uses a pull model — nodes run the Chef client on a schedule to fetch and apply policies. Ansible uses a push model — a control node connects via SSH and executes playbooks. Chef uses Ruby DSL and requires a server component. Ansible is agentless and uses YAML. Choose Chef for large, complex environments that need continuous enforcement, and Ansible for simpler setups or ad-hoc automation.
Do I need a Chef Server for production?
Yes. The Chef Server stores cookbooks, policies, and node state. It provides search capabilities, role-based access control, and API access. You can run Chef Solo or Chef Zero for local testing, but they lack the server's features. For production, deploy the Chef Server on dedicated hardware or use Hosted Chef by Progress.
How do I handle secrets like database passwords in Chef?
Use encrypted data bags. Create a data bag item with the secret, encrypt it with a shared secret key, and store it on the Chef Server. In the recipe, decrypt it with Chef::EncryptedDataBagItem.load. The secret key must be distributed to nodes that need to decrypt — typically via a secure channel during bootstrap.
What is Ohai and why does Chef use it?
Ohai collects system configuration data — platform, hostname, network interfaces, filesystem, memory — and exposes it as node attributes. Chef uses Ohai at the start of every client run to populate automatic attributes. These attributes can be used in recipes for conditional logic like platform-specific package names or dynamic template values.
Originally published on Ayodhyyya. Last updated June 1, 2026.