Skip to main content

Configuration Management — Ansible, Chef, and Puppet Compared

· 7 min read
Goel Academy
DevOps & Cloud Learning Hub

You have 50 servers. They all need the same packages, the same firewall rules, the same user accounts, and the same application config. You could SSH into each one manually, or you could write a script and hope it handles every edge case. Or you could use a configuration management tool that guarantees the desired state of every server, every time, no matter how many you have.

What Configuration Management Solves

Configuration management tools enforce a desired state on your infrastructure. Instead of writing imperative scripts ("install nginx, then copy this file, then restart the service"), you declare what the system should look like and the tool figures out how to get there. If nginx is already installed, it skips that step. If the config file changed, it updates it and restarts the service. This property is called idempotency — you can run the same configuration 100 times and the result is always the same.

Without configuration management:

  • Servers drift apart over time (someone SSHed in and changed something).
  • Rebuilding a server from scratch takes hours of manual work.
  • There is no audit trail for infrastructure changes.
  • Scaling from 5 to 500 servers is a nightmare.

Push vs Pull Architecture

Configuration management tools fall into two architectural models:

ModelHow It WorksTools
PushA central controller pushes configuration to target nodes on demandAnsible
PullAn agent on each node periodically pulls configuration from a central serverChef, Puppet
Push Model (Ansible):
┌──────────────┐
│ Control Node │──SSH──▶ Node 1
│ (your │──SSH──▶ Node 2
│ laptop) │──SSH──▶ Node 3
└──────────────┘

Pull Model (Chef/Puppet):
┌──────────────┐
│ Central │◀──poll── Agent on Node 1
│ Server │◀──poll── Agent on Node 2
│ │◀──poll── Agent on Node 3
└──────────────┘

Ansible — Agentless Automation

Ansible is the simplest configuration management tool. It requires no agent on target nodes — just SSH access and Python. Configuration is written in YAML playbooks.

Inventory

# inventory/production.ini
[webservers]
web-01 ansible_host=10.0.1.10
web-02 ansible_host=10.0.1.11
web-03 ansible_host=10.0.1.12

[databases]
db-01 ansible_host=10.0.2.10
db-02 ansible_host=10.0.2.11

[all:vars]
ansible_user=deploy
ansible_python_interpreter=/usr/bin/python3

Playbook — Web Server Setup

# playbooks/webserver.yml
---
- name: Configure web servers
hosts: webservers
become: yes

vars:
nginx_port: 80
app_user: www-data
app_root: /var/www/myapp

tasks:
- name: Update apt cache
apt:
update_cache: yes
cache_valid_time: 3600

- name: Install required packages
apt:
name:
- nginx
- certbot
- python3-certbot-nginx
- ufw
state: present

- name: Create application directory
file:
path: "{{ app_root }}"
state: directory
owner: "{{ app_user }}"
group: "{{ app_user }}"
mode: "0755"

- name: Deploy nginx configuration
template:
src: templates/nginx.conf.j2
dest: /etc/nginx/sites-available/myapp.conf
owner: root
group: root
mode: "0644"
notify: Restart nginx

- name: Enable site
file:
src: /etc/nginx/sites-available/myapp.conf
dest: /etc/nginx/sites-enabled/myapp.conf
state: link
notify: Restart nginx

- name: Configure firewall
ufw:
rule: allow
port: "{{ item }}"
proto: tcp
loop:
- "22"
- "80"
- "443"

- name: Enable UFW
ufw:
state: enabled
default: deny

handlers:
- name: Restart nginx
service:
name: nginx
state: restarted

Ansible Roles

For reusable configuration, Ansible uses roles:

# Create a role structure
ansible-galaxy init roles/nginx

# roles/nginx/
# ├── defaults/main.yml ← default variables
# ├── files/ ← static files
# ├── handlers/main.yml ← handlers (restart, reload)
# ├── meta/main.yml ← role metadata and dependencies
# ├── tasks/main.yml ← main task list
# ├── templates/ ← Jinja2 templates
# └── vars/main.yml ← role variables

# Use the role in a playbook:
# playbooks/site.yml
# ---
# - hosts: webservers
# become: yes
# roles:
# - nginx
# - certbot
# - app-deploy

Chef — Ruby DSL and Cookbooks

Chef uses Ruby as its DSL. Configuration is organized into cookbooks containing recipes. Chef uses a client-server architecture where a Chef Infra Client agent runs on each managed node.

# cookbooks/webserver/recipes/default.rb

# Update package cache
apt_update 'update' do
frequency 3600
action :periodic
end

# Install packages
%w(nginx certbot ufw).each do |pkg|
package pkg do
action :install
end
end

# Create application directory
directory '/var/www/myapp' do
owner 'www-data'
group 'www-data'
mode '0755'
recursive true
action :create
end

# Deploy nginx configuration from template
template '/etc/nginx/sites-available/myapp.conf' do
source 'nginx.conf.erb'
owner 'root'
group 'root'
mode '0644'
variables(
server_name: node['webserver']['server_name'],
app_root: '/var/www/myapp'
)
notifies :restart, 'service[nginx]', :delayed
end

# Enable the site
link '/etc/nginx/sites-enabled/myapp.conf' do
to '/etc/nginx/sites-available/myapp.conf'
notifies :restart, 'service[nginx]', :delayed
end

# Ensure nginx is running and enabled
service 'nginx' do
action [:enable, :start]
end

Puppet — Declarative DSL and Manifests

Puppet uses its own declarative DSL. Configuration is written in manifests organized into modules. Like Chef, Puppet uses a client-server model with an agent on each node that pulls configuration from a Puppet Server.

# modules/webserver/manifests/init.pp

class webserver (
String $server_name = $facts['fqdn'],
String $app_root = '/var/www/myapp',
String $app_user = 'www-data',
) {

# Ensure packages are installed
package { ['nginx', 'certbot', 'ufw']:
ensure => installed,
}

# Create application directory
file { $app_root:
ensure => directory,
owner => $app_user,
group => $app_user,
mode => '0755',
require => Package['nginx'],
}

# Deploy nginx configuration from template
file { '/etc/nginx/sites-available/myapp.conf':
ensure => file,
owner => 'root',
group => 'root',
mode => '0644',
content => template('webserver/nginx.conf.erb'),
notify => Service['nginx'],
require => Package['nginx'],
}

# Enable the site
file { '/etc/nginx/sites-enabled/myapp.conf':
ensure => link,
target => '/etc/nginx/sites-available/myapp.conf',
notify => Service['nginx'],
require => File['/etc/nginx/sites-available/myapp.conf'],
}

# Ensure nginx is running and enabled
service { 'nginx':
ensure => running,
enable => true,
}
}

Comparison Table

FeatureAnsibleChefPuppet
LanguageYAML (playbooks)Ruby (recipes)Puppet DSL (manifests)
ArchitectureAgentless (push via SSH)Agent + Server (pull)Agent + Server (pull)
Learning CurveLowHigh (Ruby required)Medium
IdempotentYesYesYes
ScalabilityGood (thousands)Excellent (tens of thousands)Excellent (tens of thousands)
SpeedSlower (SSH per task)Fast (agent-based)Fast (agent-based)
CommunityLargest (Ansible Galaxy)Large (Supermarket)Large (Puppet Forge)
Cloud SupportExcellentGoodGood
Windows SupportGood (WinRM)GoodExcellent
Configuration Drift DetectionManual (check mode)Automatic (agent runs)Automatic (agent runs)
Secrets ManagementAnsible VaultChef Vault, encrypted data bagsHiera eyaml

When to Use Each Tool

Choose Ansible when:
✓ You need quick setup with no agent installation
✓ Your team knows YAML but not Ruby
✓ You have fewer than 1000 nodes
✓ You want a single tool for config management AND orchestration
✓ You are already in the Red Hat ecosystem

Choose Chef when:
✓ You need to manage thousands of nodes at scale
✓ Your team is comfortable with Ruby
✓ You want fine-grained testing (ChefSpec, InSpec)
✓ You need complex conditional logic in your configs
✓ You want automatic drift correction via agent

Choose Puppet when:
✓ You have a large, heterogeneous environment (Linux + Windows)
✓ You need strong compliance and reporting features
✓ You want a mature, battle-tested tool
✓ You prefer declarative over imperative configuration
✓ Automatic drift correction is a hard requirement

Modern Alternatives

Configuration management has evolved. Depending on your stack, you may not need a traditional CM tool at all:

# Cloud-init — first-boot configuration for cloud VMs
# Built into AWS, Azure, and GCP machine images
cat > cloud-init.yml << 'EOF'
#cloud-config
packages:
- nginx
- certbot
write_files:
- path: /etc/nginx/sites-available/myapp.conf
content: |
server { listen 80; server_name example.com; }
runcmd:
- systemctl enable nginx
- systemctl start nginx
EOF

# Terraform provisioners — run scripts after resource creation
# (Use sparingly — Terraform is for provisioning, not configuration)
# resource "aws_instance" "web" {
# provisioner "remote-exec" {
# inline = ["sudo apt-get install -y nginx"]
# }
# }

# Docker / containers — immutable infrastructure
# Instead of configuring servers, build a container image with
# everything baked in. No configuration drift possible.
# docker build -t myapp:v1.0.0 .
# docker run myapp:v1.0.0

If you are running containers on Kubernetes, traditional configuration management is largely replaced by container images and Kubernetes manifests. But if you manage VMs, bare metal, or need to configure the hosts that run your containers, Ansible, Chef, or Puppet remain essential tools.


Your servers are configured and consistent. But how do you manage what gets deployed to them? In the next post, we will explore GitOps — using ArgoCD and Flux to manage your Kubernetes deployments declaratively with Git as the single source of truth.