Ansible Guide
Isometric ring hub linked by dashed lines to stacked server blocks of varying size, representing a control node pushing playbook tasks across an inventory
Execution

Ansible Playbook Execution: Forks and Idempotence

How Ansible's agentless model executes a playbook, what the fork count really limits, why idempotence is a discipline, and where Vault secrets belong.

By Ansible Guide Editorial · ·Updated August 18, 2026 · 5 min read

Ansible automates configuration by pushing work out to managed hosts over SSH rather than running a permanent agent on them. Understanding that execution model explains most of the behaviour that surprises newcomers, including why runs feel slow, why a task that worked once fails the second time, and where secrets belong.

The agentless push model

The control node holds the playbooks, the inventory, and the credentials. When you run a playbook, Ansible connects to each target, copies the module code it needs for the current task, executes it there, collects the result as structured data, and cleans up. Nothing stays resident on the target between runs.

The practical consequences are worth internalising. The control node needs Python and Ansible; the targets need an interpreter and a reachable SSH service. There is no daemon to keep patched on hundreds of machines, but there is also nothing enforcing state between runs. Drift is corrected when you next run the playbook, not continuously.

That also sets the shape of the cost. Work is not free per task; each one carries connection setup, a module transfer, remote execution and cleanup. On short tasks the overhead dominates the useful work, which is why removing round trips usually beats optimising the task itself.

Inventory is the source of truth for scope

Inventory lists the hosts and organises them into groups, and groups are how you scope both plays and variables. Variables can be set per host or per group, with group hierarchies allowing general defaults to be overridden by more specific groups. Inventory can be a static file or generated dynamically from a cloud provider or CMDB.

Getting inventory structure right early pays off, because a playbook targeting a well named group stays readable as the estate grows. A playbook full of conditionals testing hostnames does not. The group hierarchy, the merge order between same-level groups, and the pattern syntax that scopes a run are covered in inventory groups, variables and patterns.

Idempotence is the discipline, not a feature you get free

An idempotent task produces the same end state whether it runs once or repeatedly, and reports changed only when it actually altered something. Most built in modules are written this way: a package module installs only if the package is missing, a file module adjusts only what differs.

Idempotence breaks when you reach for shell or command modules to run arbitrary commands. Those modules cannot know what your command does, so they report changed every time and may repeat destructive work. Use a purpose built module when one exists. When you genuinely need a raw command, guard it with a condition or a creates argument so it becomes safe to rerun.

There is a testing consequence that catches most teams. A play validated only against a freshly built machine has never exercised its second run. The cheap check is to run it twice against the same host and require the second run to report zero changed tasks; anything still reporting changed on a converged host is the part that is not idempotent.

Forks control how much runs at once

Ansible executes a play task by task across the inventory. For each task, it dispatches work to hosts in parallel, and the fork count sets how many hosts are worked on simultaneously. The documented default is 5, deliberately conservative so that a first run on a laptop does not exhaust local resources.

Raising forks is the single most effective change for large inventories, but the ceiling is the control node itself. Each fork is a process consuming memory, CPU, and an outbound connection. Increase it in steps and watch the control node rather than picking a large number blindly. The inventory sizing calculator models how host count, forks and per-task cost combine into a batch runtime, which is a useful sanity check before changing the setting.

SSH pipelining reduces the number of connection operations needed to execute a module, by running many modules without an actual file transfer, and the connection plugin documentation notes it can be a very significant performance improvement. It is disabled by default because it conflicts with privilege escalation: when using sudo you must first disable requiretty in the sudoers file on the targets. On a fleet where that is already done, enabling pipelining is often a bigger win than raising forks.

Strategy decides the order, forks decide the width

The default linear strategy waits for every host to finish a task before starting the next, so one slow host paces the whole play. The free strategy lets each host advance independently, which helps when hosts vary widely in speed but makes output harder to follow, and host_pinned bounds how many hosts can be mid-play at once. None of them changes how many hosts run concurrently; that is still the fork count. The tradeoffs are compared in linear vs free vs host_pinned.

Secrets belong in Vault, not in the repository

Ansible Vault encrypts variable files or individual values so they can live in version control safely. Decryption happens at run time using a password or a password script. Encrypt whole variable files when the entire file is sensitive, and encrypt single values when you want the surrounding file to stay readable in diffs.

Two operational details matter more than the encryption itself. Vault credentials are supplied per run, so a scheduled run needs a password file or script that is itself protected. And verbose or diff output can print decrypted values, so any task handling a secret should carry no_log: true.

Preview a run before it changes anything

Every change should be previewed against one host before it touches an estate. --syntax-check catches structural errors, --list-hosts confirms the pattern resolves to what you meant, and --check --diff reports what would change, with the caveat that modules without check-mode support report nothing at all. The flags worth knowing, and the traps in each, are collected in ansible-playbook options.

Common mistakes

Committing plaintext credentials next to encrypted ones. Wrapping shell commands instead of using modules and losing idempotence. Raising forks until the control node thrashes. Building host specific logic into tasks rather than into inventory groups. Testing only against a clean machine, so reruns against drifted hosts are never exercised. Assuming a strategy change adds capacity when only the fork count does.

Sources

  1. Ansible documentation: Ansible concepts
  2. Ansible documentation: Controlling playbook execution: strategies and more
  3. Ansible documentation: ansible.builtin.ssh connection plugin
  4. Ansible documentation: Protecting sensitive data with Ansible Vault
#ansible #configuration-management #playbooks #ansible-vault #devops

Related