Migrating from Puppet and Chef to Ansible With AI as Your Draft Translator
Map Puppet manifests and Chef cookbooks to Ansible roles, using AI to draft the translation while you review every change, run check mode, and prove idempotency.
- #iac
- #ansible
- #puppet
- #chef
- #migration
I inherited an estate held together by two config-management tools that nobody who built them still worked here. Roughly forty Chef cookbooks crusted with years of conditional logic, plus a Puppet control repo that bootstrapped the older fleet. The Chef server license was up for renewal, the Puppet master was running an EOL release, and the mandate from above was blunt: get us to Ansible, agentless, and do it without a maintenance window we can’t sell to the business.
I’d done Ansible greenfield before. Migrating a living, breathing estate from two different declarative systems at once was a different animal. What actually made it tractable was treating an AI assistant as a fast junior engineer who could produce a first-draft translation of each cookbook and manifest — leaving me to do the part that matters: reviewing every line, running it in check mode, and proving the new role did exactly what the old one did. Here’s how the migration actually went.
Why agentless changes the mental model
Puppet and Chef both push a long-running agent onto every node. The agent pulls catalog or convergence data on an interval, compiles it locally, and enforces state in the background. Ansible flips this: there’s no agent, no daemon, no certificate dance. A control node SSHes in, pushes Python over the wire, runs it, and leaves. Nothing persists.
That has two consequences for a migration. First, the “every 30 minutes the agent re-converges” safety net disappears — your runs are now explicit, which is honestly clearer but means drift detection becomes a scheduled job you own, not a freebie. Second, ordering changes. Puppet builds a dependency graph from require/before/notify. Chef runs resources top-to-bottom in the order they appear in the recipe. Ansible is closer to Chef here: tasks run in order, with handlers deferred to the end. Internalize that before you trust any AI translation, because the AI will happily reorder things and you need to catch it.
The concept map I gave the AI
Before asking for a single line of translated code, I wrote down the mapping table and pasted it into the prompt as ground rules. A translation is only as good as the rosetta stone you hand the model.
- Puppet manifest / class / module and Chef cookbook / recipe become an Ansible role.
- Puppet/Chef
package,service,fileresources become the matching Ansible modules (ansible.builtin.package,service,copy/template). - Hiera data and Chef data bags / attributes become
group_vars,host_vars, and Ansible Vault for secrets. - Ordering: Puppet
requireand Chefnotifiesbecome Ansible task order plushandlersandnotify. - Facts: Facter and Ohai become
ansible_facts(e.g.ansible_facts['os_family']).
I keep this map, plus the prompt that enforces it, in a saved workspace so every cookbook gets translated against the same rules — you can stash reusable prompts like this in the Prompt Workspace instead of retyping them, and there are migration-specific starters in the prompt library.
From a Chef recipe to an Ansible role
Here’s a representative slice of a Chef recipe that installs and configures nginx:
package 'nginx' do
action :install
end
template '/etc/nginx/nginx.conf' do
source 'nginx.conf.erb'
owner 'root'
group 'root'
mode '0644'
notifies :reload, 'service[nginx]', :delayed
end
service 'nginx' do
action [:enable, :start]
end
I fed that to the assistant with the mapping rules above and asked for an Ansible role skeleton. The draft it returned, after my review and corrections, looked like this — roles/nginx/tasks/main.yml:
---
- name: "Install nginx"
ansible.builtin.package:
name: nginx
state: present
- name: "Deploy nginx.conf"
ansible.builtin.template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
owner: root
group: root
mode: "0644"
notify: "Reload nginx"
- name: "Enable and start nginx"
ansible.builtin.service:
name: nginx
enabled: true
state: started
And roles/nginx/handlers/main.yml, which is where Chef’s notifies :reload ... :delayed lands cleanly:
---
- name: "Reload nginx"
ansible.builtin.service:
name: nginx
state: reloaded
Notice the notify keyword references the handler by its name string — that string has to match exactly, and a stray rename is the single most common bug I saw in AI drafts. The model also tried to convert the ERB template by guessing at Jinja2 syntax. It got the simple <%= %> substitutions right but mangled an ERB loop. That’s the tell: AI is a fast junior engineer producing a draft, not a finished change. Every template, every handler name, every mode (quoted, so YAML doesn’t eat the leading zero as octal) gets human eyes.
Pro Tip: Ask the AI to emit the role with ansible.builtin. fully-qualified collection names from the start. It makes the diff against future Ansible versions stable and surfaces immediately when the model invented a module that doesn’t exist.
Variables, Hiera, and where secrets must not go
Chef attributes and Hiera both externalize data, and both map to Ansible’s group_vars/host_vars hierarchy. A non-secret Hiera value like nginx::worker_processes: 4 becomes a line in group_vars/web.yml:
---
nginx_worker_processes: 4
nginx_keepalive_timeout: 65
Secrets are different, and this is a hard line. Hiera-eyaml blocks and Chef encrypted data bags hold passwords, API tokens, and TLS keys. Those go into Ansible Vault, and the AI never sees the plaintext. I let the model scaffold the structure — the variable names, the vars_files reference, the no_log: true on tasks that touch them — but the actual ansible-vault encrypt step is done by a human against the real secret store. You do not paste a decrypted data bag into a chat window, ever. Treat the vault password like the production root key it effectively is.
---
- name: "Write TLS private key"
ansible.builtin.copy:
content: "{{ vault_nginx_tls_key }}"
dest: /etc/nginx/tls/server.key
owner: root
group: root
mode: "0600"
no_log: true
Coexistence: don’t big-bang it
We never had a day where Chef and Puppet were off and Ansible was on. The estate ran all three simultaneously for about six weeks. The strategy was carve-out by responsibility, not by host:
- Pick one cookbook with a clean boundary (nginx was first — well-isolated, low blast radius).
- Disable just that recipe in the Chef run-list while leaving the rest of Chef converging.
- Apply the Ansible role to the same hosts and confirm parity.
- Move to the next cookbook only once the previous one is fully off Chef.
The risk during coexistence is two tools fighting over the same file — Chef rewriting what Ansible just wrote on its next convergence. Auditing run-lists and Puppet node classifications for overlap is tedious pattern-matching across dozens of files, which is exactly the kind of grind an AI is good at flagging. I also ran the translated roles through an automated diff review before merge; a code-review pass caught a handful of places where the draft silently dropped a when: guard that the original Chef only_if had enforced.
Validate with check mode and prove idempotency
This is the step that separates a migration from an incident. Ansible’s --check flag is a dry run: it reports what would change without changing anything. Run it against a host that Chef has already converged, and a correct translation should report zero changes — because the desired state already matches.
ansible-playbook -i inventory/staging site.yml --check --diff --limit web01
If check mode wants to rewrite a file Chef already manages identically, your translation diverges somewhere — usually whitespace in a template or a mode mismatch. Fix until check mode is clean.
Then prove idempotency for real: run the playbook twice in a row against a fresh host.
ansible-playbook -i inventory/staging site.yml --limit web01
ansible-playbook -i inventory/staging site.yml --limit web01
The first run shows changed=N. The second must show changed=0. Any task that reports changed on the second pass isn’t idempotent — it’s a command that runs unconditionally, which is the classic trap when an AI translates a Chef execute block into an Ansible command without a creates: guard or a proper module.
Pro Tip: Wire the double-run idempotency check into CI against an ephemeral container. A second-run changed=0 assertion is the single most valuable gate you can add to a config-management migration.
Keep a human in the loop after cutover
Once a role replaces its cookbook in production, the work isn’t done — it’s just less visible. Because there’s no agent re-converging every half hour, drift now accumulates silently until your next scheduled run. I tied the post-cutover Ansible runs to alerting so an unexpected changed= count during a routine apply surfaces instead of hiding in a log; routing those signals through monitoring alerts closed the gap the agent used to cover.
You can lean on whatever assistant fits your editor — Claude, Cursor, or Copilot all draft Ansible competently — and there are bundled migration prompts in the prompt packs if you’d rather start from a tested template than a blank file.
Conclusion
Migrating off Puppet and Chef wasn’t a translation problem so much as a verification problem. AI compressed the tedious part — turning forty cookbooks and a Puppet control repo into draft Ansible roles — from weeks of typing into a few days of review. But the draft is the cheap 80%. The expensive, non-negotiable 20% is human: read every change, run check mode until it’s clean, prove changed=0 on the second pass, and keep the vault keys far away from the model. Treat the AI as a fast junior who never gets tired and never gets it entirely right, and the migration becomes boring. Boring is exactly what you want when you’re swapping the thing that configures every server you own. Browse more in Infrastructure as Code.
Download the Free 500-Prompt DevOps AI Toolkit
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.