Example42 blog

Ansible vs Puppet: Choose the Operating Model, Not the Syntax

Every Ansible-vs-Puppet argument I’ve ever watched start begins in the wrong place: YAML versus the Puppet DSL, agentless versus agent-based, tabs versus curly braces and mutual contempt. Fun bar conversation. Useless decision criteria.

The question that actually matters is duller, and bigger: how do you want infrastructure policy evaluated, applied, observed, and owned — not this sprint, for the next several years.

Puppet and Ansible overlap more than either camp likes to admit. Both install packages, render files, manage services, create users, and coordinate changes across a fleet. Same destinations, different routes — and the route stops being a footnote and starts being the whole story as the estate, the team, and the compliance requirements grow.

The short answer

Choose Puppet when you want continuous desired-state enforcement, per-node catalogs, rich run reporting, policy that outlives whoever wrote it, and convergence that happens locally without anyone pushing a button.

Choose Ansible when agentless reach, ordered orchestration, fast automation across a herd of dissimilar things, and execution from one control point matches how your team actually likes to work.

Use both when the boundary between them is drawn in ink, not vibes: Puppet owns persistent operating-system policy, Ansible owns deployments, network changes, or the coordinated one-off procedure.

Don’t migrate because one DSL looked shorter in a conference demo. Migrate when the target operating model solves a problem your current one can’t solve economically. Should you migrate at all? The answer might be yes. It might not be, and there’s no Deep Thought to compute it for you in seven and a half million years — you still have to do the discovery work.

Ansible and Puppet compared

Dimension Puppet Ansible
Primary model Declarative resources compiled into a per-node catalog Ordered plays and tasks executed against selected inventory
Typical execution Agents request and apply catalogs on a schedule A control node connects to targets, commonly over SSH
State enforcement Recurring convergence is the default State is evaluated when a playbook runs
Language Puppet DSL, Hiera data, EPP templates YAML playbooks and roles, Jinja templates
Ordering Resource relationships form a dependency graph Task order is explicit; handlers add event-driven actions
Inventory and classification Facts, certificates, environments, Hiera, and classification produce a node catalog Static or dynamic inventory, groups, variables, and play targeting select hosts
Change preview No-op catalog application and catalog compilation Check mode and diff mode where modules support them
Reporting Agent reports can include every resource status, event, log, and run metric Play recap, callback output, controller logs, and automation-platform history
Strongest fit Persistent system policy and continuous compliance Orchestration, deployments, remote automation, and heterogeneous targets

Puppet’s own documentation calls a catalog a node-specific desired-state document with resource dependencies baked in: the agent compares actual state against it and corrects what doesn’t match. Ansible’s documentation describes playbooks as human-readable automation that can enforce configuration or walk a multi-tier operational procedure step by step. Both are idempotent — when the underlying resources or modules were actually written to be idempotent. Neither tool makes an arbitrary shell command idempotent by osmosis. I wish it did. I’ve written enough exec resources to know better.

Sources: Puppet catalog compilation, Ansible introduction, and Ansible playbooks.

Desired state versus an ordered procedure

Puppet code declares resources and relationships, then lets the primary server do the arguing. It resolves facts, classification, Hiera data, manifests, functions, and templates into one unambiguous catalog for one node — this package must exist, this file must have this content, this service must run, all wired together with dependency edges that decide who goes first.

Ansible playbooks read like what they are: a procedure. Target these hosts, run these tasks in this order, fire a handler if something changed, walk the fleet according to whatever strategy you picked. The modules underneath — package, file, service — can still be idempotent. But the playbook keeps a narrative running the whole time. It tells a story about execution, not just about outcome.

That distinction shapes design, whether you notice it or not:

  • A well-designed Puppet profile describes the policy that must remain true.
  • A well-designed Ansible role describes repeatable tasks that produce the required state.
  • A poor Puppet migration wraps every old script in exec resources. I’ve been that migration. More than once.
  • A poor Ansible migration converts every Puppet resource into a task without reconsidering orchestration, variables, handlers, and run frequency.

Continuous convergence and drift

Puppet agents normally run on a schedule, so drift detection and remediation come baked into the model whether anyone asked for them or not. Someone changes something they shouldn’t at 2am, the next agent run notices, corrects it, and files the whole thing as a corrective event — a little timestamped confession. Agents also keep operating from their cached catalog when a fresh one can’t be compiled, depending on configuration and failure mode — a safety net, not a promise.

Ansible normally changes or checks a system only when it’s invoked. Continuous enforcement is something you build on top of that: a scheduler, an automation controller, event-driven automation, ansible-pull, or some other deliberate mechanism. That can be exactly right — plenty of teams genuinely don’t want every policy continuously re-applied. But it means the drift model has to be designed on purpose. It doesn’t come free in the box.

Ask the concrete question instead of the abstract one: who notices if /etc/ssh/sshd_config changes at 02:00, and what happens next? The answer tells you more about your real posture than any feature matrix ever will.

Agentless does not mean infrastructure-less

Ansible skips the dedicated management agent on normal SSH-reachable nodes, and that genuinely cuts endpoint packaging and lifecycle work. What it doesn’t do is make the infrastructure disappear — it just relocates it to somewhere you’re not looking yet. You still need:

  • Reliable remote connectivity and credentials.
  • Python or other target-side runtime requirements for many modules.
  • Inventory ownership and freshness.
  • A secured execution environment and dependency management.
  • Scheduling, concurrency control, logs, secrets, and operator authorization.

Puppet asks for an agent, and usually a server-side platform to go with it. In exchange, every node gets a local enforcement engine, a certificate identity, a catalog built just for it, and a recurring report trail home. The extra component is a real cost — no argument there — but it’s also the exact mechanism producing some of Puppet’s strongest properties. You don’t get the convergence without paying for the thing that converges.

Change safety: no-op and check mode

Puppet no-op compiles the real catalog, evaluates it against actual resources, and reports what it would have changed. Useful, sometimes uncannily so. It still depends on provider quality, and it still can’t predict what an external command or API call does once it actually runs. No-op reads the map, not the territory.

Ansible check mode simulates changes for modules that support it. Read that clause again, specifically the qualifier — because the official documentation is explicit that a module without check-mode support does nothing and reports nothing when it’s run in check mode, and tasks depending on registered results from it may not behave as they would in a real run. Silence here isn’t reassuring. It might just mean the module shrugged. Diff mode adds before-and-after detail on top, where supported.

Neither preview is a substitute for representative tests, canaries, and a rollback plan you’ve actually rehearsed. Treat preview output as evidence, not proof — a witness statement, not a verdict. See the Ansible check and diff mode documentation.

Scale is more than node count

Both tools handle large estates just fine. What breaks under load is different, and that’s the part people skip past to get to the demo.

Puppet centralizes catalog compilation, code, data, certificates, and reports, and pushes the applying work out to agents. Server capacity, JRuby pools, compile time, PuppetDB, PostgreSQL, environment caching, and run interval — that’s your suspect list when things slow down.

Ansible initiates connections and work from wherever it’s executing. Forks, strategy plugins, task duration, network latency, inventory plugins, controller capacity, and whatever rate limit the external API decided to enforce today — that’s yours instead. The serial, throttle, and strategy controls exist to make rolling execution explicit rather than accidental; the Ansible strategy documentation walks through them properly.

“We have 5,000 nodes” tells me nothing on its own. Five thousand nodes quietly applying stable catalogs every thirty minutes, and five thousand nodes receiving one coordinated application deployment at the same moment, are not the same workload wearing a different hat. They’re different animals that happen to share a headcount.

When Puppet is the stronger fit

  • Long-lived Linux and Windows systems must continuously conform to policy.
  • The organization values a compiled, node-specific catalog and detailed resource reports.
  • Changes must be expressed as relationships rather than a global task order.
  • Nodes may need to converge independently of a central push window.
  • Hiera, facts, roles and profiles, and the existing module ecosystem already encode significant operational knowledge.
  • Compliance depends on recurring evidence of actual versus desired state.

For old puppeteers like me, that list reads less like a feature checklist and more like the reason the job exists.

When Ansible is the stronger fit

  • The estate includes network devices, appliances, APIs, or systems where an agent is undesirable.
  • Orchestration and coordinated procedures are at least as important as continuous configuration.
  • SSH-based access and central execution already match the security and operating model.
  • Teams need rapid automation across changing or short-lived targets.
  • Existing roles, collections, inventory, and automation-controller workflows already provide the organizational platform.

When a hybrid is better than a migration

A boundary I see actually hold up, over and over:

  • Puppet manages operating-system baselines, accounts, packages, services, and persistent configuration.
  • Ansible performs application deployments, maintenance workflows, network changes, and coordinated actions.

This only works if the same property never has two owners. If Puppet insists on one version of a file while Ansible quietly deploys another, that’s not defense in depth. That’s two tools fighting over the same resource, and the fight is what gets deployed — an automation loop with a heartbeat.

Write ownership down at the resource or workload level, not in your head and not in a Slack thread from eight months ago. Decide which inventory is authoritative, how credentials stay separated, where execution history actually lives, and how an engineer knows which tool to reach for without having to ask in the incident channel.

A safe migration method

1. Inventory behavior, not files

List managed resources, schedules, integrations, reports, secrets, classification rules, and operator workflows. Repository line count is a poor measure of migration effort — about as useful as counting words in a contract to estimate the lawsuit.

2. Classify workloads

Separate persistent policy, application configuration, provisioning, deployment, orchestration, compliance, and one-off operations. Different categories may have different target tools, and often should.

3. Define acceptance evidence

For each workload, record the resulting files, packages, services, commands, reports, and failure behavior. Preserve or create tests before translation — not after, when the only remaining oracle is someone’s memory of how it used to work.

4. Build native target patterns

Design roles and inventory for Ansible, or profiles, modules, and Hiera for Puppet. Do not make the target imitate the source DSL. A Puppet manifest that’s secretly a YAML playbook wearing a trenchcoat serves nobody.

5. Pilot a representative slice

Choose a workload containing data, dependencies, templates, service notification, and more than one operating system. A trivial package example proves very little — you already knew packages could be installed.

6. Prevent double ownership

During transition, only one tool should manage a resource property. Use explicit cutover boundaries and retain a tested rollback — tested, not just written down and hoped for.

7. Measure the new operating cost

Compare failure rate, drift, execution duration, change visibility, access management, test burden, and operator effort after the pilot. If nobody captured a baseline before the pilot, congratulations — you’ve just invented one retroactively, and it will conveniently favor whichever tool you already wanted.

The practical conclusion

Ansible is not “Puppet without agents,” and Puppet is not “Ansible that runs every thirty minutes.” They encode different assumptions about where policy gets evaluated and who’s allowed to touch what, and when.

Choose the assumptions that fit your estate, not the ones that read better on a slide. If neither model should own everything, draw a hard boundary and use both deliberately — on purpose, not by accident three incidents from now.

example42 supports migrations in both directions. The Ansible and Puppet migration service covers discovery, target patterns, pilots, staged rollout, and team handover. [Shameless plug].

Alvabot