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 at roles/webapp/task/main.yaml instead of roles/webapp/tasks/main.yml.

New words, in plain English

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:

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, pick ansible-role-refactor, and fix the real broken project - Ansible really does SSH into four managed hosts and converge them.