Migrating from Chef or Puppet to Ansible: Complete Step-by-Step Guide
By Luca Berton · Published 2024-01-01 · Category: installation
Practical guide for migrating from Chef or Puppet to Ansible. Learn recipe/manifest to playbook conversion, inventory mapping, role migration, phased rollout.
Migrating from Chef or Puppet to Ansible is one of the most common infrastructure automation transitions. Ansible's agentless architecture, simple YAML syntax, and lower operational overhead make it an attractive replacement. This guide provides a practical, step-by-step migration strategy with real code conversion examples, inventory mapping, and phased rollout patterns.
Why Migrate to Ansible?
| Factor | Chef/Puppet | Ansible | |--------|-------------|---------| | Architecture | Agent + Server | Agentless (SSH) | | Language | Ruby DSL / Puppet DSL | YAML | | Infrastructure | Chef Server / Puppet Master | Controller only (or none) | | Learning curve | Steep (Ruby/Puppet DSL) | Gentle (YAML + modules) | | Agent management | Required on every node | None | | Cost | Server + agents + certificates | SSH access only | | Certificate management | Required (agent ↔ server) | None | | Ad-hoc execution | Limited | Built-in |
See also: A Preview of Ansible Journey in 2024
Migration Strategy Overview
Phase 1: Audit & Plan (1-2 weeks)
├── Inventory current Chef/Puppet configs
├── Map cookbooks/modules to Ansible equivalents
└── Identify dependencies and data bags/hiera
Phase 2: Convert & Test (2-6 weeks)
├── Convert recipes/manifests to playbooks
├── Migrate roles, data, and secrets
└── Test in staging environment
Phase 3: Parallel Run (2-4 weeks)
├── Run both tools side-by-side
├── Compare configuration drift
└── Validate Ansible produces identical state
Phase 4: Cutover (1-2 weeks)
├── Disable Chef/Puppet agents
├── Switch to Ansible-only management
└── Decommission Chef Server/Puppet Master
Phase 5: Optimize (ongoing)
├── Refactor for Ansible best practices
├── Add Ansible-native features (EDA, AWX)
└── Remove Chef/Puppet artifacts
Chef to Ansible Conversion
Chef Recipe → Ansible Playbook
Chef Recipe (Ruby DSL):
# recipes/webserver.rb
package 'nginx' do
action :install
end
template '/etc/nginx/nginx.conf' do
source 'nginx.conf.erb'
owner 'root'
group 'root'
mode '0644'
variables(
worker_processes: node['nginx']['worker_processes'],
worker_connections: node['nginx']['worker_connections']
)
notifies :restart, 'service[nginx]'
end
directory '/var/www/html' do
owner 'www-data'
group 'www-data'
mode '0755'
recursive true
end
service 'nginx' do
action [:enable, :start]
supports restart: true, reload: true
end
Equivalent Ansible Playbook:
---
- name: Configure web server
hosts: webservers
become: true
vars:
nginx_worker_processes: "{{ ansible_processor_vcpus }}"
nginx_worker_connections: 1024
tasks:
- name: Install nginx
ansible.builtin.package:
name: nginx
state: present
- name: Configure nginx
ansible.builtin.template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
owner: root
group: root
mode: "0644"
notify: Restart nginx
- name: Create web root directory
ansible.builtin.file:
path: /var/www/html
owner: www-data
group: www-data
mode: "0755"
state: directory
- name: Enable and start nginx
ansible.builtin.systemd:
name: nginx
enabled: true
state: started
handlers:
- name: Restart nginx
ansible.builtin.systemd:
name: nginx
state: restarted
Key Differences: Chef → Ansible
| Chef Concept | Ansible Equivalent |
|-------------|-------------------|
| Recipe | Playbook / Role tasks |
| Cookbook | Role or Collection |
| Resource | Module |
| notifies | notify + handlers |
| node['attr'] | Variables (vars, host_vars, group_vars) |
| Data Bags | ansible-vault encrypted files |
| Chef Server | AAP Controller (optional) |
| knife | ansible / ansible-playbook CLI |
| Berkshelf/Policyfile | requirements.yml |
| Test Kitchen | Molecule |
| Ohai | setup module (gather_facts) |
| ERB templates | Jinja2 templates |
| run_list | Playbook task list |
| Environments | Inventory groups + group_vars |
Chef Data Bags → Ansible Vault
Chef Data Bag:
{
"id": "database",
"password": "s3cret",
"host": "db.example.com",
"port": 5432
}
Ansible Vault equivalent:
ansible-vault create group_vars/all/vault.yml
# group_vars/all/vault.yml (encrypted)
vault_db_password: "s3cret"
vault_db_host: "db.example.com"
vault_db_port: 5432
Chef Roles → Ansible Roles
Chef Role:
{
"name": "webserver",
"run_list": [
"recipe[base]",
"recipe[nginx]",
"recipe[monitoring]"
],
"default_attributes": {
"nginx": {
"worker_processes": 4
}
}
}
Ansible Role structure:
roles/webserver/
├── defaults/main.yml # Default variables
├── handlers/main.yml # Handlers
├── tasks/main.yml # Main task list
├── templates/ # Jinja2 templates
└── vars/main.yml # Role variables
# roles/webserver/tasks/main.yml
---
- name: Include base configuration
ansible.builtin.include_role:
name: base
- name: Include nginx configuration
ansible.builtin.include_role:
name: nginx
- name: Include monitoring
ansible.builtin.include_role:
name: monitoring
# roles/webserver/defaults/main.yml
nginx_worker_processes: 4
See also: Manage Ansible Collection Changelogs with Antsibull-Changelog
Puppet to Ansible Conversion
Puppet Manifest → Ansible Playbook
Puppet Manifest:
# manifests/webserver.pp
class webserver (
Integer $worker_processes = $facts['processors']['count'],
Integer $worker_connections = 1024,
) {
package { 'nginx':
ensure => installed,
}
file { '/etc/nginx/nginx.conf':
ensure => file,
content => template('webserver/nginx.conf.erb'),
owner => 'root',
group => 'root',
mode => '0644',
require => Package['nginx'],
notify => Service['nginx'],
}
file { '/var/www/html':
ensure => directory,
owner => 'www-data',
group => 'www-data',
mode => '0755',
}
service { 'nginx':
ensure => running,
enable => true,
}
}
Equivalent Ansible Playbook:
---
- name: Configure web server
hosts: webservers
become: true
vars:
nginx_worker_processes: "{{ ansible_processor_vcpus }}"
nginx_worker_connections: 1024
tasks:
- name: Install nginx
ansible.builtin.package:
name: nginx
state: present
- name: Configure nginx
ansible.builtin.template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
owner: root
group: root
mode: "0644"
notify: Restart nginx
- name: Create web root
ansible.builtin.file:
path: /var/www/html
owner: www-data
group: www-data
mode: "0755"
state: directory
- name: Ensure nginx is running
ansible.builtin.systemd:
name: nginx
enabled: true
state: started
handlers:
- name: Restart nginx
ansible.builtin.systemd:
name: nginx
state: restarted
Key Differences: Puppet → Ansible
| Puppet Concept | Ansible Equivalent |
|---------------|-------------------|
| Manifest | Playbook / Role tasks |
| Module | Role or Collection |
| Resource type | Module |
| require/before | Task ordering (top-to-bottom) |
| notify/subscribe | notify + handlers |
| Hiera | group_vars / host_vars / Vault |
| Puppet Master | AAP Controller (optional) |
| Facter | setup module (gather_facts) |
| ERB templates | Jinja2 templates |
| Puppet Forge | Ansible Galaxy / Automation Hub |
| puppet apply | ansible-playbook |
| Classification (ENC) | Inventory + groups |
| Catalog | Playbook execution |
| Agent (daemon) | None (agentless SSH) |
Puppet Hiera → Ansible Variable Hierarchy
Puppet Hiera:
# hiera.yaml
---
version: 5
hierarchy:
- name: "Node-specific"
path: "nodes/%{::fqdn}.yaml"
- name: "OS-specific"
path: "os/%{facts.os.family}.yaml"
- name: "Environment"
path: "environments/%{::environment}.yaml"
- name: "Common"
path: "common.yaml"
Ansible equivalent (inventory structure):
inventory/
├── production/
│ ├── hosts.yml
│ ├── group_vars/
│ │ ├── all.yml # = common.yaml
│ │ ├── redhat.yml # = os/RedHat.yaml
│ │ ├── debian.yml # = os/Debian.yaml
│ │ └── webservers.yml # = role-based
│ └── host_vars/
│ ├── web-01.example.com.yml # = nodes/web-01.yaml
│ └── web-02.example.com.yml
└── staging/
├── hosts.yml # = environments/staging.yaml
└── group_vars/
└── all.yml
Puppet Resource Ordering → Ansible Task Order
Puppet uses explicit require/before/notify for ordering. Ansible runs tasks top-to-bottom:
# Puppet - explicit ordering needed
package { 'nginx': ensure => installed }
file { '/etc/nginx/nginx.conf':
require => Package['nginx'], # Must declare dependency
notify => Service['nginx'],
}
service { 'nginx': ensure => running }
# Ansible - natural top-to-bottom order
- name: Install nginx
ansible.builtin.package:
name: nginx
state: present
# This runs AFTER the package install automatically
- name: Configure nginx
ansible.builtin.template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
notify: Restart nginx
# This runs AFTER the template
- name: Ensure nginx running
ansible.builtin.systemd:
name: nginx
state: started
Inventory Migration
Chef Node Attributes → Ansible Inventory
# Export Chef nodes to JSON
knife node list | while read node; do
knife node show "$node" -F json > "nodes/${node}.json"
done
Convert to Ansible inventory:
# inventory/hosts.yml
all:
children:
webservers:
hosts:
web-01.example.com:
nginx_worker_processes: 4
web-02.example.com:
nginx_worker_processes: 8
databases:
hosts:
db-01.example.com:
postgresql_max_connections: 200
Puppet ENC → Ansible Inventory Groups
# Map Puppet classes to Ansible groups
all:
children:
# Puppet: class webserver
webservers:
hosts:
web-01.example.com:
web-02.example.com:
# Puppet: class database
databases:
hosts:
db-01.example.com:
# Puppet: class monitoring
monitoring:
hosts:
mon-01.example.com:
See also: Mastering Ansible-Creator: Simplify Your Ansible Collection Development
Phased Cutover Strategy
Phase 3: Parallel Run
---
- name: Parallel run validation
hosts: all
tasks:
- name: Check if Chef client is still running
ansible.builtin.systemd:
name: chef-client
register: chef_status
failed_when: false
changed_when: false
- name: Report Chef client status
ansible.builtin.debug:
msg: "Chef client on {{ inventory_hostname }}: {{ chef_status.status.ActiveState | default('not found') }}"
- name: Validate Ansible-managed config matches Chef
ansible.builtin.command:
cmd: "diff /etc/nginx/nginx.conf /tmp/ansible-nginx.conf"
register: config_diff
changed_when: false
failed_when: false
- name: Alert on configuration drift
ansible.builtin.debug:
msg: "WARNING: Config drift detected on {{ inventory_hostname }}"
when: config_diff.rc != 0
Phase 4: Agent Removal
---
- name: Remove Chef client
hosts: all
become: true
tasks:
- name: Stop Chef client service
ansible.builtin.systemd:
name: chef-client
state: stopped
enabled: false
failed_when: false
- name: Remove Chef packages
ansible.builtin.package:
name:
- chef
- chef-client
state: absent
- name: Remove Chef configuration directory
ansible.builtin.file:
path: /etc/chef
state: absent
- name: Remove Chef cache
ansible.builtin.file:
path: /var/chef
state: absent
---
- name: Remove Puppet agent
hosts: all
become: true
tasks:
- name: Stop Puppet agent
ansible.builtin.systemd:
name: puppet
state: stopped
enabled: false
failed_when: false
- name: Remove Puppet packages
ansible.builtin.package:
name:
- puppet-agent
- puppet
state: absent
- name: Remove Puppet configuration
ansible.builtin.file:
path: "{{ item }}"
state: absent
loop:
- /etc/puppetlabs
- /opt/puppetlabs
- /var/log/puppetlabs
Template Conversion: ERB → Jinja2
ERB (Chef/Puppet) to Jinja2 (Ansible)
# ERB template (Chef/Puppet)
worker_processes <%= @worker_processes %>;
events {
worker_connections <%= @worker_connections %>;
}
<% if @enable_ssl %>
server {
listen 443 ssl;
server_name <%= @server_name %>;
}
<% end %>
<% @upstream_servers.each do |server| %>
server <%= server %>;
<% end %>
{# Jinja2 template (Ansible) #}
worker_processes {{ nginx_worker_processes }};
events {
worker_connections {{ nginx_worker_connections }};
}
{% if nginx_enable_ssl %}
server {
listen 443 ssl;
server_name {{ nginx_server_name }};
}
{% endif %}
{% for server in nginx_upstream_servers %}
server {{ server }};
{% endfor %}
Quick Reference: ERB → Jinja2
| ERB | Jinja2 |
|-----|--------|
| <%= variable %> | {{ variable }} |
| <% if condition %> | {% if condition %} |
| <% end %> | {% endif %} / {% endfor %} |
| <% array.each do |item| %> | {% for item in array %} |
| <%= node['attr'] %> | {{ ansible_variable }} |
Common Migration Pitfalls
1. Declarative vs Procedural Thinking
Chef/Puppet are declarative (desired state, any order). Ansible is procedural (tasks run in order). Don't try to replicate the exact Chef/Puppet resource graph — embrace top-to-bottom execution.
2. Agent Convergence vs One-Shot
Chef/Puppet agents run periodically (every 30 min) to correct drift. Ansible runs when you tell it to. Consider:
• Use ansible-pull for periodic runs
• Use AWX/AAP for scheduled playbook execution
• Use Ansible EDA for event-driven remediation
3. Certificate Management Disappears
One huge advantage: no more managing agent certificates, CA rotation, or certificate signing.
4. Don't Convert 1:1
Resist the urge to translate every Chef resource or Puppet type into an Ansible task. Use native Ansible patterns:
• Chef ruby_block → Ansible set_fact or command
• Puppet custom types → Ansible modules or custom modules
• Complex Ruby logic → Jinja2 filters or custom filter plugins
Frequently Asked Questions
How long does it take to migrate from Chef/Puppet to Ansible?
For a typical environment (50-200 servers, 20-50 cookbooks/modules), plan 2-3 months. Small environments can migrate in 2-4 weeks. Enterprise environments with hundreds of cookbooks may take 6-12 months with a phased approach.
Can I run Ansible and Chef/Puppet simultaneously?
Yes, and you should during migration. Run both tools in parallel, comparing their outputs to ensure Ansible produces identical system state. Gradually shift workloads to Ansible before decommissioning Chef/Puppet agents.
What about Chef/Puppet features that Ansible doesn't have?
Chef's chef-client daemon and Puppet's agent provide continuous enforcement. Ansible achieves this through scheduled runs (cron/AWX), ansible-pull, or Event-Driven Automation (EDA). For most environments, scheduled runs + EDA provide equivalent coverage.
Do I need Ansible Automation Platform (AAP) to replace Chef Server/Puppet Master?
No. Ansible works fine with just the CLI. However, AAP provides a web UI, RBAC, job scheduling, credential management, and audit logging — features that teams often relied on from Chef Server or Puppet Enterprise.
How do I handle Chef encrypted data bags in Ansible?
Use ansible-vault to encrypt sensitive variables. You can encrypt entire files or individual variables within YAML files. Vault integrates seamlessly with playbook execution.
Related Articles
• Ansible Playbook Best Practices • Ansible Automation Platform RBAC • Ansible Execution Environments • What is AnsibleConclusion
Migrating from Chef or Puppet to Ansible reduces operational complexity by eliminating agents, servers, and certificate management. The key to a successful migration is phasing: audit first, convert incrementally, run in parallel, then cut over. Don't try to replicate your Chef/Puppet architecture in Ansible — embrace Ansible's simpler model. The result is an automation platform that's easier to learn, cheaper to operate, and more accessible to your entire team.
Category: installation