Skip to content
Codeloom
DevOps

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.

·9 min read · By Codeloom
Intermediate 16 min read

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

FeatureAnsibleChefPuppet
LanguageYAML (playbooks)Ruby (recipes)Puppet DSL (manifests)
ArchitectureAgentless (SSH push)Client-server (agent pull)Client-server (agent pull)
Learning curveLowHighMedium
IdempotentYes (with proper modules)YesYes
Setup effortMinimal (SSH only)Moderate (Chef Server)Moderate (Puppet Server)
SpeedSlower (SSH per task)Fast (compiled catalog)Fast (compiled catalog)
Windows supportGood (via WinRM)GoodGood
CommunityVery largeLargeLarge
Cloud integrationExcellentGoodGood
TestingMolecule, TestinfraChefSpec, InSpecrspec-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.