Migrations fail on assumptions
A Puppet master or Chef server that has run for years holds more than configuration. It holds decisions nobody wrote down: that one node class exists because of a 2019 incident, that a certain cookbook is only used by a team that was disbanded, that a module quietly sets a kernel parameter every other tool depends on. Teams that start a migration by converting files one by one usually discover this halfway through, when the new tool produces a server that is subtly different.
Our approach under the Ansible configuration management service starts with understanding and finishes with a clean shutdown of the old system. In between, both tools coexist so nothing is a leap of faith.
Should you migrate at all?
Be sceptical of any consultant who says yes before looking. Reasons to migrate include licence or support costs, a team that cannot hire for the old tool, a wish to unify automation with network and application tasks, and a desire to drop agents and certificates. Reasons to stay include a stable estate with a happy team and strong enforcement needs that the existing tool already meets.
There is a third possibility. Part of what your old tool does may belong elsewhere: packages baked into images with Packer, cloud resources managed by Terraform, container workloads handled by Kubernetes. Migrating that part to Ansible would be moving it to the wrong place.
What good looks like
- Every behaviour you need is captured in a tested role, and every behaviour you do not need is gone.
- The old and new tools never manage the same resource on the same host at the same time.
- Cutover happens in small groups with a rollback path.
- At the end the old server, agents and certificates are decommissioned, not left running.
How it works in practice
Concept mapping
| Puppet | Chef | SaltStack | Ansible |
|---|---|---|---|
| Module | Cookbook | Formula | Role |
| Manifest | Recipe | State file | Tasks file or playbook |
| Hiera | Attributes, data bags | Pillar | group_vars, host_vars, Vault |
| Facter | Ohai | Grains | Facts |
| Node classification | Run list, roles | Top file | Inventory groups and playbooks |
| Agent pulls | Client pulls | Minion (agent) or SSH | Controller pushes over SSH or WinRM |
The mapping is a guide, not a translation table. Puppet’s declarative resource graph and Ansible’s ordered tasks behave differently, so ordering assumptions need checking. Idempotence is a given in Puppet types and must be earned in Ansible tasks, which is why we write idempotent, tested roles.
Discovery
We extract the real picture rather than trusting the repository:
- Which modules or cookbooks are applied to at least one live node? Use the master’s reports or the Chef server’s node data.
- Which nodes are in which classes?
- Which resources actually changed in the last month, versus sat unchanged?
- Where do secrets come from, and who can read them?
Modules applied to no nodes are deleted, not migrated. In our experience, a meaningful portion of a long-lived codebase is dead, but we will not guess the share for you until we have looked.
Rebuilding one unit
Take a Puppet class that manages ntp:
class profile::time {
package { 'chrony': ensure => installed }
file { '/etc/chrony.conf':
content => template('profile/chrony.conf.erb'),
notify => Service['chronyd'],
}
service { 'chronyd': ensure => running, enable => true }
}
The Ansible role does the same, expressed as ordered tasks with a handler:
- name: Install chrony
ansible.builtin.package:
name: chrony
state: present
- name: Configure chrony
ansible.builtin.template:
src: chrony.conf.j2
dest: /etc/chrony.conf
mode: "0644"
notify: Restart chronyd
- name: Enable and start chronyd
ansible.builtin.service:
name: chronyd
state: started
enabled: true
ERB templates need rewriting as Jinja2, and the variables that fed them move from Hiera to group variables. Each role gets a Molecule scenario whose assertions describe the end state the old tool produced.
Parallel running
Choose pilot hosts that represent each role. On those hosts, put the old agent in report-only mode (Puppet’s --noop, or an equivalent for Chef and Salt) so it flags any difference from what Ansible has applied, without fighting it. Differences are either bugs in the new role or accidental behaviour in the old one, and each is resolved on purpose. Only then do real groups move, in waves from least to most critical.
The parts people forget
- Node classification. Where the old tool decided what a node should be, Ansible needs inventory groups that reproduce that, usually from tags.
- Secrets. Hiera-eyaml or Chef vault contents move to Ansible Vault or HashiCorp Vault.
- Scheduling and drift. An agent converges every thirty minutes. Without it, drift appears between runs. We schedule runs from AWX, usually daily in check mode with an alert, and apply changes through the pipeline.
- Reporting. PuppetDB or the Chef server offered dashboards. Replace them with run reports and monitoring.
- Windows. Needs its own plan using WinRM or SSH and the
ansible.windowscollection.
Decisions and trade-offs
Big bang or incremental. Incremental takes longer and costs more in the short term, but a big bang ties your entire estate to one cutover weekend. We do not recommend it.
Convert or rewrite. Automated converters exist for some constructs, but they produce literal translations. We prefer rewriting from the desired behaviour, using the old code as a specification.
Push or pull afterwards. Push models need credentials to reach servers. Pull models need agents on them. Either is a legitimate security trade-off, which we explain for your environment.
Common failure modes
- Two tools managing one file. Hosts flap between versions. Migrate by resource and keep ownership clear.
- Hidden dependencies. One cookbook relied on a side effect of another. Parallel running exposes it.
- Lost knowledge. The only person who understood the old tool leaves mid-migration. We document as we go.
- No decommissioning. The old master keeps running “just in case”. Put the shutdown date in the plan.
Delivery and handover
You receive the migrated roles in Git, passing tests, a mapping document from every old unit to its new role, inventory and secrets structures, cutover records per wave, and a verified decommissioning of the old tooling. The documentation ensures the next engineer can learn Ansible from your repository rather than from memory.
How to tell it is working
- The old server’s node count reaches zero and it is switched off.
- Pilot comparison reports show no unexplained differences.
- New servers are built by Ansible with no manual fixes.
- Engineers stop asking which tool owns a given file.
To get an assessment of whether migration makes sense for you, book a consultation.