Configuration Management: Ansible, Chef, and Puppet
Compare Ansible, Chef, and Puppet for configuration management. Learn how each tool works with practical examples for server provisioning and application deployment.
What you'll learn
- ✓What configuration management is and why manual server setup fails at scale
- ✓Ansible playbooks, roles, and inventory for agentless automation
- ✓Chef recipes, cookbooks, and the client-server model
- ✓Puppet manifests, modules, and declarative infrastructure
Prerequisites
- •Linux system administration basics
- •SSH key-based authentication setup
- •Understanding of package managers (apt, yum)
- •Basic YAML or Ruby knowledge is helpful but not required
What Is Configuration Management?
Configuration management is the practice of automating the setup and maintenance of servers so they stay in a known, consistent state. Instead of SSHing into each machine and running commands manually, you write code that describes what the machine should look like, and a tool enforces that state.
Without configuration management, servers drift. One engineer installs a security patch on server A but forgets server B. Someone tweaks an nginx config on one box but not the others. Over time, no two servers are exactly the same, and debugging becomes a nightmare because you cannot reproduce the problem on a different machine.
Configuration management tools solve this by making server configuration repeatable, testable, and version-controlled. The three most established tools are Ansible, Chef, and Puppet. Each takes a different approach to the same problem.
Ansible
Ansible is the most widely adopted configuration management tool today. Its key differentiator is that it is agentless: you do not need to install anything on the target machines. Ansible connects via SSH (or WinRM for Windows) and executes tasks remotely.
Architecture
Control Node (your laptop or CI server)
│
├── SSH → Server A
├── SSH → Server B
└── SSH → Server C
No agents, no daemons, no central server required. You just need SSH access and Python on the target machines (which almost every Linux system has).
Inventory
The inventory file defines which machines Ansible manages:
# inventory/hosts.ini
[webservers]
web1.example.com ansible_host=192.168.1.10
web2.example.com ansible_host=192.168.1.11
web3.example.com ansible_host=192.168.1.12
[databases]
db1.example.com ansible_host=192.168.1.20
db2.example.com ansible_host=192.168.1.21
[all:vars]
ansible_user=deploy
ansible_python_interpreter=/usr/bin/python3
[webservers:vars]
http_port=8080
app_env=production
For dynamic environments (cloud), use dynamic inventory plugins:
# inventory/aws_ec2.yml
plugin: amazon.aws.aws_ec2
regions:
- us-east-1
filters:
tag:Environment: production
keyed_groups:
- key: tags.Role
prefix: role
Playbooks
Playbooks are YAML files that describe the desired state of your servers:
# playbooks/webserver.yml
---
- name: Configure web servers
hosts: webservers
become: true
vars:
app_version: "2.1.0"
nginx_worker_processes: auto
tasks:
- name: Update apt cache
apt:
update_cache: true
cache_valid_time: 3600
- name: Install required packages
apt:
name:
- nginx
- python3-pip
- certbot
- python3-certbot-nginx
state: present
- name: Create application directory
file:
path: /opt/myapp
state: directory
owner: deploy
group: deploy
mode: "0755"
- name: Deploy application
unarchive:
src: "https://releases.example.com/myapp-{{ app_version }}.tar.gz"
dest: /opt/myapp
remote_src: true
notify: Restart application
- name: Configure nginx
template:
src: templates/nginx.conf.j2
dest: /etc/nginx/sites-available/myapp
owner: root
group: root
mode: "0644"
notify: Reload nginx
- name: Enable nginx site
file:
src: /etc/nginx/sites-available/myapp
dest: /etc/nginx/sites-enabled/myapp
state: link
notify: Reload nginx
- name: Configure application service
template:
src: templates/myapp.service.j2
dest: /etc/systemd/system/myapp.service
notify:
- Reload systemd
- Restart application
- name: Ensure services are running
systemd:
name: "{{ item }}"
state: started
enabled: true
loop:
- nginx
- myapp
handlers:
- name: Reload systemd
systemd:
daemon_reload: true
- name: Restart application
systemd:
name: myapp
state: restarted
- name: Reload nginx
systemd:
name: nginx
state: reloaded
The Jinja2 template for nginx:
# templates/nginx.conf.j2
upstream myapp {
{% for host in groups['webservers'] %}
server {{ hostvars[host]['ansible_host'] }}:{{ http_port }};
{% endfor %}
}
server {
listen 80;
server_name {{ inventory_hostname }};
location / {
proxy_pass http://127.0.0.1:{{ http_port }};
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
Run the playbook:
# Dry run (check mode)
ansible-playbook -i inventory/hosts.ini playbooks/webserver.yml --check --diff
# Apply
ansible-playbook -i inventory/hosts.ini playbooks/webserver.yml
# Limit to specific hosts
ansible-playbook -i inventory/hosts.ini playbooks/webserver.yml --limit web1.example.com
Ansible Roles
Roles organize playbooks into reusable components:
# Create a role structure
ansible-galaxy init roles/nginx
roles/nginx/
├── defaults/
│ └── main.yml # Default variables (lowest precedence)
├── handlers/
│ └── main.yml # Handlers
├── tasks/
│ └── main.yml # Tasks
├── templates/
│ └── nginx.conf.j2 # Templates
└── vars/
└── main.yml # Variables (higher precedence)
# roles/nginx/tasks/main.yml
---
- name: Install nginx
apt:
name: nginx
state: present
- name: Configure nginx
template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
notify: Reload nginx
- name: Ensure nginx is running
systemd:
name: nginx
state: started
enabled: true
# roles/nginx/defaults/main.yml
---
nginx_worker_processes: auto
nginx_worker_connections: 1024
nginx_keepalive_timeout: 65
Use roles in playbooks:
# playbooks/site.yml
---
- name: Configure all servers
hosts: all
become: true
roles:
- common
- security
- name: Configure web servers
hosts: webservers
become: true
roles:
- nginx
- myapp
- name: Configure databases
hosts: databases
become: true
roles:
- postgresql
Chef
Chef uses a client-server model with a Ruby-based DSL. A central Chef Server stores your configurations (called “cookbooks”), and Chef Client agents on each node periodically pull their configurations and apply them.
Architecture
Chef Workstation ──push cookbooks──▶ Chef Server
│
┌─────────┼─────────┐
▼ ▼ ▼
Chef Client Chef Client Chef Client
(Server A) (Server B) (Server C)
Recipes and Cookbooks
A cookbook is the fundamental unit of configuration. A recipe is a Ruby file within a cookbook:
# cookbooks/webserver/recipes/default.rb
# Install packages
package %w[nginx curl] do
action :install
end
# Create application directory
directory '/opt/myapp' do
owner 'deploy'
group 'deploy'
mode '0755'
recursive true
action :create
end
# Deploy configuration from template
template '/etc/nginx/sites-available/myapp' do
source 'nginx.conf.erb'
owner 'root'
group 'root'
mode '0644'
variables(
server_name: node['myapp']['server_name'],
port: node['myapp']['port']
)
notifies :reload, 'service[nginx]'
end
# Create symlink to enable the site
link '/etc/nginx/sites-enabled/myapp' do
to '/etc/nginx/sites-available/myapp'
notifies :reload, 'service[nginx]'
end
# Manage the service
service 'nginx' do
action [:enable, :start]
supports reload: true, restart: true, status: true
end
Attributes define configurable values:
# cookbooks/webserver/attributes/default.rb
default['myapp']['server_name'] = 'app.example.com'
default['myapp']['port'] = 8080
default['myapp']['version'] = '2.1.0'
ERB template:
# cookbooks/webserver/templates/default/nginx.conf.erb
server {
listen 80;
server_name <%= @server_name %>;
location / {
proxy_pass http://127.0.0.1:<%= @port %>;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
Upload and apply:
# Upload cookbook to Chef Server
knife cookbook upload webserver
# Add recipe to a node's run list
knife node run_list add web1.example.com 'recipe[webserver]'
# The Chef Client on the node will apply it on its next run
# Or trigger it manually:
knife ssh 'name:web1.example.com' 'sudo chef-client'
Puppet
Puppet uses a declarative language to describe system state. Like Chef, it uses a client-server model (Puppet Server and Puppet Agent), but its DSL is purpose-built rather than Ruby-based.
Architecture
Puppet Server (stores catalogs and modules)
│
├── Puppet Agent (Server A) - pulls catalog every 30 min
├── Puppet Agent (Server B) - pulls catalog every 30 min
└── Puppet Agent (Server C) - pulls catalog every 30 min
Manifests and Modules
Puppet configurations are called manifests (.pp files):
# modules/webserver/manifests/init.pp
class webserver (
String $server_name = 'app.example.com',
Integer $port = 8080,
String $app_version = '2.1.0',
) {
# Install packages
package { ['nginx', 'curl']:
ensure => installed,
}
# Create directory
file { '/opt/myapp':
ensure => directory,
owner => 'deploy',
group => 'deploy',
mode => '0755',
}
# Deploy nginx config
file { '/etc/nginx/sites-available/myapp':
ensure => file,
owner => 'root',
group => 'root',
mode => '0644',
content => template('webserver/nginx.conf.erb'),
notify => Service['nginx'],
require => Package['nginx'],
}
# Enable site
file { '/etc/nginx/sites-enabled/myapp':
ensure => link,
target => '/etc/nginx/sites-available/myapp',
notify => Service['nginx'],
require => File['/etc/nginx/sites-available/myapp'],
}
# Manage service
service { 'nginx':
ensure => running,
enable => true,
hasrestart => true,
require => Package['nginx'],
}
}
Assign classes to nodes:
# manifests/site.pp
node 'web1.example.com' {
class { 'webserver':
server_name => 'app.example.com',
port => 8080,
}
}
node 'web2.example.com' {
class { 'webserver':
server_name => 'app2.example.com',
port => 8080,
}
}
Or use Hiera for data separation:
# data/nodes/web1.example.com.yaml
webserver::server_name: 'app.example.com'
webserver::port: 8080
webserver::app_version: '2.2.0'
Comparison
| Feature | Ansible | Chef | Puppet |
|---|---|---|---|
| Language | YAML (playbooks) | Ruby (recipes) | Puppet DSL (manifests) |
| Architecture | Agentless (SSH push) | Client-server (agent pull) | Client-server (agent pull) |
| Learning curve | Low | High | Medium |
| Idempotent | Yes (with proper modules) | Yes | Yes |
| Setup effort | Minimal (SSH only) | Moderate (Chef Server) | Moderate (Puppet Server) |
| Speed | Slower (SSH per task) | Fast (compiled catalog) | Fast (compiled catalog) |
| Windows support | Good (via WinRM) | Good | Good |
| Community | Very large | Large | Large |
| Cloud integration | Excellent | Good | Good |
| Testing | Molecule, Testinfra | ChefSpec, InSpec | rspec-puppet, Beaker |
Which Tool to Choose
Choose Ansible when:
- You want the fastest path to automation with minimal infrastructure.
- Your team prefers YAML over programming languages.
- You need to automate tasks beyond configuration (deployments, orchestration, network devices).
- You do not want to manage agent software on target machines.
Choose Chef when:
- You have complex configuration logic that benefits from a full programming language.
- Your team is comfortable with Ruby.
- You need tight integration with compliance frameworks (Chef InSpec).
- You have a large fleet where agent-based convergence is more efficient.
Choose Puppet when:
- You prioritize a strongly declarative approach where order of operations is handled automatically.
- You manage a large, stable infrastructure with many similar machines.
- Your organization already has Puppet expertise.
- You need robust reporting and compliance dashboards (Puppet Enterprise).
For most teams starting fresh today, Ansible is the practical choice due to its low barrier to entry, agentless architecture, and broad applicability beyond just configuration management. It works for server provisioning, application deployment, network configuration, and cloud orchestration with a single tool.
Wrapping Up
Configuration management transforms server administration from a manual, error-prone process into a repeatable, version-controlled practice. Ansible provides the simplest entry point with agentless SSH-based automation and YAML playbooks. Chef offers the power of Ruby for complex configuration logic with a client-server model. Puppet provides a purpose-built declarative language with strong enforcement of desired state. All three tools are mature and production-proven. Start with whichever matches your team’s skills and infrastructure needs, and invest in writing testable, modular configurations that you can reuse across environments.
Related articles
- DevOps Ansible vs Terraform: Configuration vs Infrastructure
Compare Ansible and Terraform for DevOps automation. Learn when to use each tool, how they differ in approach, and how to combine them effectively.
- DevOps Ansible Playbooks for Configuration Management
A hands-on guide to writing Ansible playbooks that configure servers, deploy apps, and enforce desired state across your fleet.
- DevOps Chaos Engineering: Test Your System's Resilience
Learn chaos engineering principles and tools. Run controlled experiments with Chaos Monkey, Litmus, and Gremlin to find weaknesses before they cause real outages.
- DevOps Container Orchestration: Docker Swarm vs Kubernetes
Compare Docker Swarm and Kubernetes for container orchestration. Learn the architecture, setup, scaling, and networking of both platforms with practical examples.