AnsiblePilot — Master Ansible Automation

AnsiblePilot is the leading resource for learning Ansible automation, DevOps, and infrastructure as code. Browse over 1,400 tutorials covering Ansible modules, playbooks, roles, collections, and real-world examples. Whether you are a beginner or an experienced engineer, our step-by-step guides help you automate Linux, Windows, cloud, containers, and network infrastructure.

Popular Topics

About Luca Berton

Luca Berton is an Ansible automation expert, author of 8 Ansible books published by Apress and Leanpub including "Ansible for VMware by Examples" and "Ansible for Kubernetes by Example", and creator of the Ansible Pilot YouTube channel. He shares practical automation knowledge through tutorials, books, and video courses to help IT professionals and DevOps engineers master infrastructure automation.

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?

FactorChef/PuppetAnsible
ArchitectureAgent + ServerAgentless (SSH)
LanguageRuby DSL / Puppet DSLYAML
InfrastructureChef Server / Puppet MasterController only (or none)
Learning curveSteep (Ruby/Puppet DSL)Gentle (YAML + modules)
Agent managementRequired on every nodeNone
CostServer + agents + certificatesSSH access only
Certificate managementRequired (agent ↔ server)None
Ad-hoc executionLimitedBuilt-in

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

See also: A Preview of Ansible Journey in 2024

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 ConceptAnsible Equivalent
RecipePlaybook / Role tasks
CookbookRole or Collection
ResourceModule
notifiesnotify + handlers
node['attr']Variables (vars, host_vars, group_vars)
Data Bagsansible-vault encrypted files
Chef ServerAAP Controller (optional)
knifeansible / ansible-playbook CLI
Berkshelf/Policyfilerequirements.yml
Test KitchenMolecule
Ohaisetup module (gather_facts)
ERB templatesJinja2 templates
run_listPlaybook task list
EnvironmentsInventory 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

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 ConceptAnsible Equivalent
ManifestPlaybook / Role tasks
ModuleRole or Collection
Resource typeModule
require/beforeTask ordering (top-to-bottom)
notify/subscribenotify + handlers
Hieragroup_vars / host_vars / Vault
Puppet MasterAAP Controller (optional)
Factersetup module (gather_facts)
ERB templatesJinja2 templates
Puppet ForgeAnsible Galaxy / Automation Hub
puppet applyansible-playbook
Classification (ENC)Inventory + groups
CatalogPlaybook 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

See also: Manage Ansible Collection Changelogs with Antsibull-Changelog

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:

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

See also: Ansible Debugger: Interactive Debug & Troubleshoot Playbooks

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

ERBJinja2
<%= variable %>{{ variable }}
<% if condition %>{% if condition %}
<% end %>{% endif %} / {% endfor %}
<% array.each doitem%>{% 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.

Conclusion

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

Browse all Ansible tutorials · AnsiblePilot Home