The Layout Ansible Loads by Convention
Scenario: Someone 'converted the playbook to a role'. Ansible reports "the role 'webapp' was not found". The directory exists - but the task file is atroles/webapp/task/main.yamlinstead ofroles/webapp/tasks/main.yml.
New words, in plain English
- Role - a directory with a fixed layout that packages tasks, handlers, templates, files, defaults and variables as one reusable unit.
- Convention over configuration - Ansible finds each part by its standard path - you never list the files.
roles_path- where Ansible looks for roles. Defaults toroles/next to the playbook, plus~/.ansible/rolesand/etc/ansible/roles.
A role is the unit of reuse, and it is defined entirely by directory names. Ansible does not read a manifest; it looks for specific paths and loads whatever it finds:
tasks/main.yml- the tasks. This is the only genuinely required file.handlers/main.yml- handlers, automatically available to the whole play.defaults/main.yml- lowest-precedence variables, meant to be overridden.vars/main.yml- high-precedence variables, for internal constants only.templates/- Jinja2 templates.template:resolvessrc:here automatically.files/- static files.copy:resolvessrc:here automatically.meta/main.yml- metadata and dependencies on other roles.tests/,README.md- documentation and test scaffolding.
Every one of those names is exact. task/ is not tasks/, and main.yaml is not main.yml. Ansible does not warn about a directory it does not recognise - it simply does not load it, which is why the scenario above produces a confusing error about the role not existing at all.
The payoff for that rigidity is that src: webapp.conf.j2 inside a role resolves to roles/webapp/templates/webapp.conf.j2 with no path at all. Every role in the world is laid out the same way, so any engineer can open an unfamiliar one and know where to look.
Analogy: A role is a flat-pack furniture box. The panels, the screws and the instructions are always in the same compartments, so anyone can open any box and get started. Put the screws in the instruction sleeve and the assembly line stops - not because the screws are wrong, but because nobody looks there.
A worked example
roles/
└── webapp/
├── defaults/
│ └── main.yml # webapp_port: 8080 (overridable)
├── vars/
│ └── main.yml # internal constants (hard to override)
├── tasks/
│ └── main.yml # the work
├── handlers/
│ └── main.yml # reload/restart handlers
├── templates/
│ └── webapp.conf.j2 # referenced as src: webapp.conf.j2
├── files/
│ └── ca-bundle.crt # referenced as src: ca-bundle.crt
├── meta/
│ └── main.yml # dependencies, supported platforms
└── README.md
# Using it
- hosts: webservers
roles:
- webapp
The defaults versus vars distinction is the one that determines whether your role is usable by anyone else. defaults/main.yml sits at the very bottom of the precedence order, so a consumer can override it from group_vars, host_vars or the command line. vars/main.yml sits near the top and beats all of those - so anything you put there is effectively a constant.
The practical rule: if a user might ever want to change it, it belongs in defaults. If changing it would break the role, it belongs in vars. Getting this backwards produces a role that looks configurable and quietly ignores every attempt to configure it.
Warning: Ansible silently ignores directories whose names it does not recognise. A role with its tasks in task/ fails with an error about the role not existing, which sends people looking in entirely the wrong place.
Goal: Put this to work in the ansible-role-refactor lab. Open/labs/ansible, pickansible-role-refactor, and fix the real broken project - Ansible really does SSH into four managed hosts and converge them.