Skip to content

Lesson 9 of 9 · 24m

The 200-Playbook Codebase: Layout, Testing, and Conventions

The repo structure, ansible.cfg, tagging, testing with Molecule, linting, and the naming conventions that decide whether your Ansible ages into infrastructure or into archaeology.

Every team that adopts Ansible follows the same arc: one playbook, then ten, then a shared repo nobody can navigate. The ones that survive past playbook ~50 all converged on the same ideas — a predictable layout, entry points that mean something, automated testing of roles, and conventions enforced by lint instead of by memory. This lesson is the repo I'd hand a new hire.

The layout that scales

ansible/
  ansible.cfg            # forks, roles_path, vault_identity_list — in the repo
  requirements.yml       # pinned collections and external roles
  site.yml               # the entry point — converges an environment
  inventories/
    production/  { hosts.yml, group_vars/, host_vars/ }
    staging/     { hosts.yml, group_vars/, host_vars/ }
  playbooks/             # web.yml, db.yml, maintenance/ — one concern each
  roles/                 # common/, nginx/, postgres/ — project-owned

The rule: ansible-playbook -i inventories/production site.yml converges the whole environment. Everything else is a subset you reach via -l or --tags. Environments differ by inventory, never by forked playbooks.

Tags and include discipline

# site.yml — tags on import_playbook apply to everything inside
- import_playbook: playbooks/common.yml
  tags: [base]
- import_playbook: playbooks/web.yml
  tags: [web]
- import_playbook: playbooks/db.yml
  tags: [db]

Tag at the import level, not on every task. A small, agreed tag vocabulary (base, web, db, deploy, certs) beats forty bespoke tags nobody can spell. --list-tasks --tags deploy should tell an operator exactly what a deploy touches.

Test roles, not playbooks

Playbooks orchestrate; roles hold logic — so test the roles. Molecule spins a container or VM, runs your role, and lets you assert:

# roles/postgres/molecule/default/molecule.yml
driver:
  name: docker
platforms:
  - name: pg-test
    image: geerlingguy/docker-debian12-ansible
molecule test          # create → converge → idempotence → verify → destroy
molecule converge      # iterate on the role without the full cycle

The idempotence step is the gift: it runs your role twice and fails if the second run reports changed. Half the "is this role honest?" questions answer themselves.

Conventions worth writing down

  • FQCN everywhere, every task named — ansible-lint enforces both; the log is the runbook.
  • Lint and dry-run in CI — ansible-lint on every PR plus site.yml --check --diff against staging, so reviewers see the delta, not the YAML.
  • One role = one concern — nginx configures nginx; TLS certs are their own role.
  • No secrets outside vault; pin everything — gitleaks in CI, pinned collection versions, ansible-core in requirements.txt.

Field notes

  • Keep ansible.cfg in the repo — team config beats "ask Dave what his forks setting is." (Ansible ignores it in world-writable dirs, so mind CI checkout permissions.)
  • Retire playbooks aggressively: a playbooks/oneoff/ folder of 2019 migrations is a trap someone will -l all one day.
  • The codebase is healthy when a new hire can predict where a change lives from the hostname it affects. If they can't, the layout's failing them — not the other way around.