Ansible Deep Dive Part 12: Best Practices — the Style Guide I Actually Follow
Eleven parts of individual mechanisms. This one is the distilled version — the rules I actually follow, and more usefully, why each one earns its place rather than being cargo-culted from a style guide someone else wrote.
Name every task, always
# Don't
- ansible.builtin.package:
name: nginx
state: present
# Do
- name: Install nginx
ansible.builtin.package:
name: nginx
state: present
An unnamed task shows up in playbook output as the module invocation itself — readable enough in a five-task playbook, unreadable in a two-hundred-task run where you’re scanning output at 2am trying to find which of six package tasks just failed. ansible-lint (Part 8) flags this by default; there’s no good reason to skip it, ever.
Reach for the purpose-built module before command/shell
This is Part 3’s idempotency contract, restated as a rule: every time you write ansible.builtin.command or ansible.builtin.shell, ask whether a real module already exists for what you’re doing. community.general alone covers hundreds of tools most people reach for command on reflex. When no module exists and command/shell genuinely is the right call, pair it with creates:, removes:, or changed_when: so the task can still report accurately rather than claiming “changed” on every single run regardless of whether anything actually happened.
Directory layout: one shape, applied consistently
project/
inventory/
production/hosts.yml
staging/hosts.yml
group_vars/
host_vars/
roles/
playbooks/
site.yml
deploy.yml
requirements.yml
ansible.cfg
Separate inventories per environment (production/, staging/), never a single inventory with a staging group you have to remember to exclude. The entire point is that running a playbook against the wrong environment should require actively pointing at the wrong inventory file, not forgetting a --limit flag.
Vault everything that’s actually a secret, and nothing that isn’t
Part 6’s vault_ prefix convention, restated as policy: encrypt credentials, tokens, and keys. Don’t encrypt things that aren’t secrets just because they feel sensitive — a hostname, an internal IP range, or a non-secret config value gains nothing from encryption except making the file undiffable and un-greppable for no security benefit. Vault is for the specific, narrow set of values that would be a real problem if they leaked; treat it as a scalpel, not a blanket.
Idempotency is a design constraint, not an afterthought
Test every role’s idempotency, not just its correctness — Part 8’s Molecule converge-twice check exists precisely because a role that looks fine on a single run can still fail this quietly (a lineinfile regex that’s slightly too loose, a template that re-renders with cosmetically different but semantically identical whitespace). Treat “does this report zero changes on a second run against unchanged state” as a real, non-optional test, not a nice-to-have.
Tag sparingly; split playbooks generously
Part 3 touched on this, worth restating as a hard rule: if a playbook needs so many tags that you’re routinely running it with --tags to get useful behavior out of it, that’s a sign it’s actually several playbooks wearing one file. Split it. A playbook that does one clear thing, run in full, every time, is easier to reason about and easier to test than one enormous playbook with a tag-driven menu of partial behaviors.
Pin your dependencies, always
requirements.yml (Part 5) with explicit version pins on every collection and role, checked into the same repo as the playbooks that depend on them. “Whatever’s currently on Galaxy” is not a reproducible build, and a collection’s minor version bump changing a module’s default behavior out from under you is a genuinely common way for a playbook that worked yesterday to misbehave today with zero local changes.
Check mode before anything that matters
--check --diff (Part 3) before any playbook run against something you actually care about, every time, no exceptions for “I’m confident this one’s fine.” The five seconds it costs is cheap insurance against the one time your confidence was misplaced — and the one time it catches something is the only time it needed to.
Gate merges on lint + Molecule, not on hope
Part 8’s CI pipeline — ansible-lint and molecule test as required checks on every pull request — isn’t a nice-to-have for a solo project either. The value isn’t really about catching a colleague’s mistake; it’s that six months from now, you are the person who’s forgotten the details of a role you wrote once, and the CI gate is what stops you from breaking it without noticing.
Write for the person who inherits this with no context
Every convention above compounds into one underlying goal: a project should be legible to someone — quite possibly a future version of you — who’s never seen it before and has thirty minutes to understand what it does before they need to change something. Named tasks, purpose-built modules, a predictable directory layout, pinned dependencies, and a passing CI gate are what make that thirty minutes actually sufficient instead of a multi-hour archaeology exercise.
Twelve parts of “how to do this well.” The last one asks a harder question: with an AI agent that can SSH into a box and just improvise a fix, does any of this discipline still matter?