Since the Perforce acquisition folded Puppet into a commercial bundle, the per-node math on Puppet Enterprise has pushed a lot of teams to re-price their config management. Ansible is the usual landing spot. Before you start translating code, understand the one architectural difference that drives every decision below.
The mental-model change
Puppet is a pull system. Every node runs an agent that wakes up on an interval (default 30 minutes), pulls catalog from the master, and converges local state to match. Enforcement is continuous and automatic. Nobody has to “run” anything.
Ansible is a push system. A control node opens SSH, ships modules to the target, executes them, and disconnects. Nothing runs on the managed host between plays. There is no agent, no certificate authority, no PuppetDB. That removes a whole tier of infrastructure, but it also removes the thing that quietly corrected drift every half hour. Hold that thought, because it dictates your enforcement strategy at the end.
Translating the code
The vocabulary maps cleanly once you accept push vs pull:
- A Puppet manifest (
.pp) becomes a playbook or a task file (.yml). - A Puppet module becomes an Ansible role with the same
tasks/,templates/,files/,defaults/layout. - A Puppet resource (
package,file,service) becomes an Ansible module call (ansible.builtin.package,template,service). - Puppet facts become Ansible facts, gathered by the
setupmodule and namespaced underansible_facts.
A concrete before/after. Puppet:
package { 'nginx': ensure => installed }
service { 'nginx': ensure => running, enable => true }
Ansible:
- name: nginx present and running
block:
- ansible.builtin.package:
name: nginx
state: present
- ansible.builtin.service:
name: nginx
state: started
enabled: true
Both are idempotent. The difference: Puppet enforces this ordering through its dependency graph, while Ansible runs top to bottom. You make ordering explicit instead of relying on require and before arrows.
Hiera becomes group_vars
Hiera is where most of the real logic lives, and it ports more naturally than people expect. Your hierarchy of YAML data files maps to Ansible’s group_vars/ and host_vars/ directories, resolved by inventory group and hostname. A common.yaml in Hiera becomes group_vars/all.yml. Per-environment Hiera layers become inventory groups (group_vars/production.yml).
For secrets, eyaml becomes ansible-vault. Encrypt the file or individual values:
ansible-vault encrypt_string 's3cr3t' --name 'db_password'
Migrate node by node, run both
Do not flip the fleet at once. The agent and SSH coexist fine on the same host, so run both systems in parallel during transition:
- Pick one role (say, base hardening). Port it to Ansible.
- Build an inventory of a few canary nodes.
- Run the playbook with
--check --diffagainst those nodes while Puppet still owns them. - When Ansible’s diff is empty, remove that resource from the Puppet manifest so the two do not fight.
- Repeat role by role until the agent manages nothing, then uninstall it.
The --check --diff step is your safety net. It tells you exactly what Ansible would change before it changes anything, which Puppet’s noop mode does too, so the workflow feels familiar.
Proving each role, then wiring up CI
Because the two systems run side by side, your validation story during the transition is a diff, not a leap of faith. For each ported role, the canary loop above, run in --check --diff, confirm an empty diff, then remove the resource from Puppet, is the whole safety mechanism. Treat a non-empty diff on a node Puppet still manages as a signal that your translation is incomplete, not as license to apply.
Wiring this into CI changes shape from Puppet’s model. Puppet’s pipeline typically validated manifests and let agents pull the result; Ansible’s pipeline runs the playbooks itself against inventory. Lint the roles, run them in check mode against a staging inventory, and gate promotion on a clean diff there before any production run. Keep the inventory itself under version control alongside the roles, since in an agentless world the inventory is the source of truth for what gets managed, replacing the agent’s implicit membership in the Puppet master’s catalog.
Rollback is worth thinking through explicitly, because Ansible has no automatic reconvergence to lean on. If a play makes a change you need to undo, you undo it with another play or by re-running the previous, known-good role. Keeping roles idempotent is what makes that safe: re-running the prior version returns the host to its intended state. During the migration itself, the ultimate rollback is that Puppet still owns anything you have not yet cut over, so a botched role is contained to its canary set rather than the fleet.
Where Puppet still wins
Be honest with yourself here, because this is where migrations get regretted.
- Continuous enforcement and drift correction. Puppet’s agent re-converges every 30 minutes with no scheduler. Ansible only enforces when you run it. If a host is your security baseline and someone edits a config by hand, Puppet silently fixes it; Ansible does not notice until the next push.
- Very large fleets. Pull scales better than push. Ten thousand agents checking in independently beats one control node opening ten thousand SSH sessions. Ansible needs
mitogen, forks tuning, or pull mode to compete at that size. - PuppetDB reporting and inventory. The queryable history of every catalog run is hard to replicate. Ansible’s reporting is thinner.
To get enforcement back, you need a scheduler. AWX (or commercial AAP) runs playbooks on a cron-like schedule against your inventory, which restores the “every N minutes” convergence. A plain cron job calling ansible-pull on each host gets you closer to Puppet’s actual pull model. Budget for one of these; agentless does not mean enforcement-less for free.
So, is it worth it?
Puppet to Ansible trades an agent, a CA, and per-node licensing for SSH and a control node, and the code translation (manifests to playbooks, modules to roles, Hiera to group_vars) is mechanical once you internalize push vs pull. The real cost is rebuilding continuous enforcement with AWX or ansible-pull, plus the reporting you lose with PuppetDB. Model your node count, your enforcement interval, and the AWX infrastructure in the calculator above before you commit, because at large fleet sizes the licensing savings can get eaten by the operational rebuild.