Chef and Puppet, the titans of infrastructure configuration management, are fundamentally about automating the declarative state of your systems.

Let’s watch Chef in action. Imagine you have a web server that needs Nginx installed and configured to serve a specific site.

# cookbooks/my_webserver/recipes/default.rb
package 'nginx' do
  action :install
end

template '/etc/nginx/sites-available/my_site' do
  source 'my_site.erb'
  owner 'root'
  group 'root'
  mode '0644'
  notifies :reload, 'service[nginx]'
end

link '/etc/nginx/sites-enabled/my_site' do
  to '/etc/nginx/sites-available/my_site'
  notifies :reload, 'service[nginx]'
end

service 'nginx' do
  action [:enable, :start]
end

This Chef recipe declares that nginx should be installed, a configuration file should exist at /etc/nginx/sites-available/my_site (rendered from a template file cookbooks/my_webserver/templates/default/my_site.erb), a symbolic link should be created to enable the site, and the nginx service should be enabled and running. When you run chef-client on a node, Chef compares this desired state to the actual state of the system and makes changes only if necessary.

Puppet offers a very similar capability, expressed in its own DSL.

# modules/my_webserver/manifests/init.pp
package { 'nginx':
  ensure => installed,
}

file { '/etc/nginx/sites-available/my_site':
  ensure  => file,
  content => template('my_webserver/my_site.erb'),
  owner   => 'root',
  group   => 'root',
  mode    => '0644',
  notify  => Service['nginx'],
}

file { '/etc/nginx/sites-enabled/my_site':
  ensure => link,
  target => '/etc/nginx/sites-available/my_site',
  notify => Service['nginx'],
}

service { 'nginx':
  ensure => running,
  enable => true,
}

Here, Puppet declares that the nginx package must be installed, a file at /etc/nginx/sites-available/my_site must exist with specific content (from a template modules/my_webserver/templates/my_site.erb), a link must exist, and the nginx service must be running and enabled. Like Chef, Puppet’s agent will reconcile the system’s state against this manifest.

The core problem these tools solve is the "configuration drift" or "snowflake server" problem. Without them, servers become unique, unrepeatable snowflakes. You log in, make a change, forget about it, and a week later, nobody knows why that server is configured differently. Chef and Puppet introduce idempotency: applying the same configuration multiple times has the same effect as applying it once. This ensures that your infrastructure is consistently defined and reproducible, making it easier to scale, troubleshoot, and maintain. They manage resources like packages, files, services, users, and more, treating them as distinct entities with defined states.

The magic lies in the declarative nature. You don’t tell Chef or Puppet how to install Nginx; you tell them that Nginx should be installed. The tool figures out the underlying commands (apt-get install nginx, yum install nginx, etc.) based on the operating system it’s running on. This abstraction is key to managing diverse environments.

When you define a service like nginx in Chef or Puppet and set action [:enable, :start] or ensure => running, enable => true, you’re not just starting a process. You’re telling the system that this service should be managed by the OS’s init system (like systemd or SysVinit). If the service crashes, the OS is configured to restart it, and the configuration management tool ensures this desired state is maintained. This goes beyond a simple systemctl start nginx command.

A common point of confusion is the difference between Chef’s "resources" and Puppet’s "resources." While both use the term, they are essentially the same concept: a managed entity within your infrastructure, such as a package, a file, or a service. The specific syntax and available properties for each resource type differ between Chef and Puppet, but the underlying principle of declaring their desired state remains consistent. For example, in Chef, you’d use package 'nginx' do action :install end, while in Puppet, it’s package { 'nginx': ensure => installed }.

The most surprising thing to many is how deeply these tools integrate with the operating system’s native management capabilities. When you declare a file in Chef or Puppet, it’s not just copying a file; it’s often managing ownership, permissions, SELinux contexts, and ensuring it’s deployed atomically. For services, it means integrating with systemd unit files or init.d scripts, ensuring that the service is managed by the OS’s lifecycle. This level of integration is what allows for robust, self-healing infrastructure.

The next hurdle is understanding how to manage secrets securely within these systems, as hardcoding sensitive information directly into cookbooks or manifests is a major security anti-pattern.

Want structured learning?

Take the full DevOps & Platform Engineering course →