The most surprising thing about configuration management tools like Ansible, Puppet, and Chef is that they’re all fundamentally trying to solve the same problem: making sure your servers are in a desired, repeatable state, but they achieve this with dramatically different philosophies and architectures.

Let’s see how this plays out with a simple example: ensuring nginx is installed and running on a set of servers.

Imagine you have a file named nginx.yml for Ansible:

---
- hosts: webservers
  become: yes
  tasks:
    - name: Ensure nginx is installed
      ansible.builtin.package:
        name: nginx
        state: present

    - name: Ensure nginx service is running and enabled
      ansible.builtin.service:
        name: nginx
        state: started
        enabled: yes

When you run ansible-playbook nginx.yml, Ansible connects to your webservers (defined in its inventory) via SSH (or WinRM) and executes these tasks directly on each machine. It’s imperative: "do this, then do that." It’s also agentless, meaning no special software needs to be pre-installed on the target machines beyond Python (for most modules).

Now, consider Puppet. A Puppet manifest for the same goal might look like this in nginx.pp:

package { 'nginx':
  ensure => installed,
}

service { 'nginx':
  ensure     => running,
  enable     => true,
  require    => Package['nginx'],
}

This manifest describes a desired state. Puppet works with an agent installed on each target machine. This agent periodically communicates with a central Puppet master, retrieves its configuration catalog, and then enforces that catalog locally. It’s declarative: "this is what I want the system to look like." The agent figures out how to get there.

Chef, similarly, uses a declarative approach, but it’s structured around "recipes" and "cookbooks." A Chef recipe for nginx might look like this:

package 'nginx' do
  action :install
end

service 'nginx' do
  supports status: true, restart: true, reload: true
  action [:enable, :start]
  subscribes :restart, 'package[nginx]', :immediately
end

Like Puppet, Chef typically relies on an agent (the chef-client) running on the target nodes. This agent checks in with a Chef server (or a standalone Chef Solo setup) to fetch its configuration and apply it. The subscribes line here is a common Chef pattern: it means "if the nginx package is updated, restart the nginx service."

The fundamental problem these tools solve is drift: the tendency for servers to deviate from their intended configuration over time due to manual changes, failed updates, or ad-hoc fixes. Without configuration management, auditing and correcting this drift becomes a massive manual effort, leading to inconsistencies and outages.

Internally, the main difference lies in their execution model. Ansible is often described as an "orchestration" tool that can also do configuration management, executing commands directly. Puppet and Chef are more pure "configuration management" tools, focusing on the desired state and using agents to enforce it.

  • Ansible: Agentless, uses SSH/WinRM, push-based execution. You run playbooks from a control node. Great for ad-hoc tasks, orchestration, and environments where installing agents is difficult.
  • Puppet: Agent-based, master-agent architecture, pull-based execution (agents pull configs from master). Strong emphasis on modeling infrastructure as code.
  • Chef: Agent-based, client-server or standalone, pull-based execution. Very flexible and powerful, often preferred for complex environments and developer-centric workflows.

When you’re defining a service state, like ensuring nginx is running, Ansible directly tells the service module to start it. Puppet and Chef’s agents, however, will check the current state of the nginx service. If it’s running, they do nothing. If it’s stopped, they start it. This continuous reconciliation is key to their declarative nature.

One often-overlooked aspect is how these tools handle idempotency. An idempotent operation is one that can be applied multiple times without changing the result beyond the initial application. All three tools strive for idempotency. For example, if nginx is already installed, Ansible’s package module with state: present will detect this and do nothing. Similarly, Puppet’s package { 'nginx': ensure => installed } will only act if nginx is not present. This is critical because you want to be able to re-run your automation without fear of breaking things.

The choice between them often comes down to your existing infrastructure, team expertise, and tolerance for agents. Ansible’s agentless nature is appealing for quick adoption, while Puppet and Chef’s agent-based models provide a robust, continuous enforcement mechanism.

The next hurdle you’ll likely encounter is managing secrets and sensitive data within your configuration.

Want structured learning?

Take the full DevOps & Platform Engineering course →