Porting a Puppet manifest to an Ansible playbook by translating each resource line by line looks like the most direct path, and yet this port frequently breaks silently: the difference isn’t the language (Ruby DSL versus YAML), it’s how each tool determines the actual execution order of operations.

Puppet: a dependency graph resolved before execution

Puppet doesn’t describe a sequence of actions, but a set of resources and their declared relationships (require, before, notify): the Puppet agent builds a dependency graph from these relationships, then determines the application order by resolving that graph, independent of the order resources appear in the manifest.

# The writing order here doesn't matter at all:
# Puppet installs the package before starting the service
# purely because of the declared 'require' relationship
service { 'nginx':
  ensure  => running,
  require => Package['nginx'],
}

package { 'nginx':
  ensure => installed,
}

Ansible: the written order is the executed order

Ansible executes a playbook’s tasks sequentially, in the exact order they’re written, with no implicit dependency graph resolution: the writing order isn’t a suggestion, it’s the actual guaranteed behavior.

# Here, the writing order IS the execution order:
# installing the package must be written before starting
# the service, Ansible will never infer it on its own
- name: Install nginx
  apt:
    name: nginx
    state: present

- name: Start nginx
  service:
    name: nginx
    state: started

Why the line-by-line port fails silently

Translating a Puppet manifest into an Ansible playbook while preserving the resources’ order of appearance (rather than the actual order resolved by Puppet’s dependency graph) can produce a playbook that tries to start a service before the corresponding package is installed, a bug that often only shows up on the first deployment to a fresh machine, where the implicit ordering Puppet previously guaranteed no longer has any effect.

The actual migration work

A successful Puppet-to-Ansible migration never translates a manifest line by line: it first reconstructs the actual dependency order between resources (often by inspecting the graph generated by puppet catalog or puppet apply --graph), before writing Ansible tasks in that explicit order. This upfront work, longer than a simple syntactic translation, is what distinguishes a migration that holds up from a port that breaks on the first redeploy to a machine in a different state.

Takeaway

Puppet resolves a declared dependency graph (require, before, notify) before executing anything, the manifest’s writing order having no influence on the actual application order. Ansible executes tasks strictly in the order they’re written, with no implicit resolution of any kind, the same fundamental role distinction already drawn between Terraform and Ansible (declarative state versus ordered procedural execution). Porting a Puppet manifest to Ansible while preserving the order of appearance rather than the actual dependency order breaks silently, often only visible on the first deployment to a fresh machine: the migration requires explicitly reconstructing that order before translating the syntax, not the other way around.