How Ansible Runs a Playbook: Inventory, Idempotence, and Forks
An orientation to Ansible's agentless execution model, why idempotence matters, how forks control parallelism, and how Vault handles secrets.
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.
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.
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.
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 shipped default is 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. SSH pipelining reduces the number of round trips per task and is often a bigger win than raw parallelism, though it requires that requiretty is not enforced by sudo on the targets.
Strategy also matters. The default waits for every host to finish a task before starting the next, so one slow host paces the whole play. A free strategy lets each host advance independently, which helps when hosts vary widely in speed but makes output harder to follow.
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.
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.