The most common fate of automation in a small team: one person writes a playbook, the playbook grows to two thousand lines, the person leaves, and the successor is afraid to touch it and starts from scratch. The problem is not Ansible but the absence of a structure that lets somebody understand the code without its author.
Repository layout
The directory structure is the first piece of documentation. A layout that has proven itself:
inventories/
prod/hosts.yml machines and groups of the production environment
prod/group_vars/ variables per group
test/...
roles/
<role_name>/ one role = one responsibility
playbooks/
<task>.yml thin playbooks composing roles
requirements.yml collections and external roles with pinned versions
README.md how to run, prerequisites, where the secrets are
A playbook in playbooks/ contains no tasks, only a list of roles and target groups. The logic lives in the roles.
A role: one responsibility
A role installs and configures one thing: a web server, a monitoring agent, time synchronisation. An “application server” role that does twenty things cannot be tested and cannot be reused.
Every role has:
defaults/main.ymlwith every variable it uses, with default values and a comment next to each,README.mdwith one paragraph: what it does, what it needs, an example of use,meta/main.ymlwith dependencies on other roles, if any.
Role variables carry the role name as a prefix: nginx_worker_processes, not worker_processes. Without the prefix, two roles will sooner or later use the same name for different things.
Variables: where things live
Ansible has more than a dozen levels of variable precedence. In practice three are enough, plus a rule not to use the rest:
defaults/in the role: values sensible for most cases.group_vars/in the inventory: differences between environments and server groups.host_vars/: exceptions for individual machines, each with a comment explaining why.
Variables inside the playbook body, in a task’s vars: or passed on the command line are acceptable only temporarily. A task should look identical for every environment; the inventory describes the differences.
Secrets
Passwords and keys never sit in the repository in plain text. Two acceptable options:
- Ansible Vault with a separate encrypted file per environment and the vault password kept outside the repository,
- an external secrets manager queried at run time through a lookup plugin.
The second option is better when more than one tool uses the secrets. Whichever you choose, tasks that handle secrets get no_log: true so that values do not end up in execution logs.
Idempotence and check mode
A playbook run twice in a row must report zero changes the second time. If it reports changes, something is written with shell or command without a creates condition or changed_when. Every such construct carries a comment explaining why a module was not sufficient.
--check mode must work for the whole playbook. Tasks that cannot support it get check_mode: false deliberately and with a comment, not by accident.
Tests and versions
The minimum that can be maintained without a dedicated tool: ansible-lint and --syntax-check run automatically on every change in the repository, plus a periodic run of the playbook against a clean test machine. Collections in requirements.yml have pinned versions; bumping a version is a separate, deliberate change.
Summary
A playbook that survives turnover is not cleverer than others, only predictable: the directory structure says where to look, roles do one thing, variables have one home, secrets stay outside the code, and two runs give the same result. The next administrator does not need to understand the author, only the convention.