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?

| 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 PracticesAnsible Automation Platform RBACAnsible Execution EnvironmentsWhat is Ansible

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