Skip to main content
RunBook Academy

AnsibleXXII · Roles and ReuseRoles and reuse

Role anatomy and where roles are found

Intermediate⏱ ~18 minansible-playbookansible-galaxy

What you'll learn

  • Name every directory in a role and state what Ansible loads from it without being asked
  • Predict which file a role reads for a given kind of content
  • State the role search path in order and read it off a failure message
  • Detect a role name that resolves to a different role than you intended

Prerequisites

Verified against ansible-core 2.21.x · ansible (community package) 14.x · Python (controller) 3.12+ · ansible-lint 26.x · Molecule 26.x · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · RHEL / Rocky / AlmaLinux 9.x · 2026-08-11

Not yet marked complete on this device.

A role is a directory with a conventional layout. There is no registration step, no manifest listing its contents, and no import statement naming its files. You write roles: [webapp] in a play, and Ansible walks a fixed set of subdirectory names underneath roles/webapp/, loading whatever it finds.

That is the whole mechanism, and it is worth stating plainly because the convention does all the work. Nothing tells you that defaults/main.yml holds variables — the path is the declaration. A file in the wrong directory is not an error. It is silence.

The directories

ansible-galaxy init writes the canonical skeleton. This is the fastest way to see what the convention actually is, rather than what you remember of it:

Read-only / Safegenerate a role skeleton
ansible-galaxy init webapp
find webapp -type f | sort
Read-only / Safethe skeleton ansible-core 2.21.3 produces
$ ansible-galaxy init webapp && find webapp | sort
- Role webapp was created successfully
webapp
webapp/README.md
webapp/defaults
webapp/defaults/main.yml
webapp/files
webapp/handlers
webapp/handlers/main.yml
webapp/meta
webapp/meta/main.yml
webapp/tasks
webapp/tasks/main.yml
webapp/templates
webapp/tests
webapp/tests/inventory
webapp/tests/test.yml
webapp/vars
webapp/vars/main.yml

Six directories carry a main.yml that Ansible loads by itself when the role runs. Two do not, and the difference is the thing people get wrong:

DirectoryAuto-loaded main.yml?What it is for
tasks/Yes — the role’s entry pointThe tasks the role runs
handlers/YesHandlers, merged into the parent play’s namespace
defaults/YesVariables at the bottom of precedence — the role’s public interface
vars/YesVariables high in precedence — internal constants
meta/YesRole metadata: dependencies, allow_duplicates, Galaxy info
tests/No — not part of a runA sample inventory and playbook for the role author
files/NoChanges where copy, script and unarchive look for a relative source
templates/NoChanges where template looks for a relative source

files/ and templates/ do not load anything. They alter lookup: inside a role, ansible.builtin.template: src=nginx.conf.j2 resolves against roles/webapp/templates/ first. That is why a role’s templates need no path prefix and why moving a task out of a role breaks its src silently.

Three more directories exist and are not in the skeleton, because most roles do not need them:

  • library/ — modules shipped inside the role, available to it without a collection
  • module_utils/ — shared Python for those modules
  • lookup_plugins/, filter_plugins/, action_plugins/ and the rest of the plugin families — plugins scoped to the role

Where a bare role name is looked up

roles: [webapp] is a name, not a path. Ansible resolves it against a search path, and the fastest way to see that path exactly is to ask for a role that does not exist:

Read-only / Safethe search path, read off a failure
$ ansible-playbook -i inventory.ini missing.yml
[ERROR]: The role 'nonexistent_role' was not found in:
/srv/automation/roles:/home/deploy/.ansible/roles:/usr/share/ansible/roles:/etc/ansible/roles:/srv/automation

Origin: /srv/automation/missing.yml:3:11

1 - hosts: local
2   gather_facts: false
3   roles: [nonexistent_role]
          ^ column 11

In order:

  1. roles/ beside the playbook/srv/automation/roles here. This is the project’s own roles directory and it wins.
  2. roles_path — the configured list, three entries by default.
  3. The playbook’s own directory — a fallback that occasionally surprises people who have a stray directory named like a role.

Collections come before all of this when you use a fully qualified name. namespace.collection.rolename bypasses the search path entirely and looks inside the installed collection, which is the unambiguous form and the one to prefer for anything you did not write.

Read-only / Safewhat roles_path is configured to
ansible-config dump | grep -i ROLES_PATH
Read-only / Safethe default, unmodified
$ ansible-config dump | grep -i ROLES_PATH
DEFAULT_ROLES_PATH(default) = ['/home/deploy/.ansible/roles', '/usr/share/ansible/roles', '/etc/ansible/roles']

The shadowing failure

Two roles named nginx — one in the repository under roles/nginx, one installed into ~/.ansible/roles/nginx by a requirements.yml run months ago. The repository copy wins for you. On a colleague’s controller, where the repository copy was never added, the Galaxy copy wins. Same playbook, same inventory, same command, two different sets of tasks against production.

Nothing warns you. The name resolved; a role ran; the play succeeded.

Read-only / Safefind every role that shadows another
for d in ./roles ~/.ansible/roles /usr/share/ansible/roles /etc/ansible/roles; do
[ -d "$d" ] || continue
for r in "$d"/*/; do
  [ -d "$r" ] && printf '%s\t%s\n' "$(basename "$r")" "$d"
done
done | sort | uniq -c | sort -rn | head -20

The operational habit that costs nothing: run --list-tasks before a real run and read the role prefixes. Every task inside a role is displayed as rolename : task name, so a role that resolved somewhere unexpected shows up as a task list you do not recognise.

Read-only / Saferole prefixes in --list-tasks
$ ansible-playbook -i inventory.ini site.yml --list-tasks
playbook: site.yml

play #1 (web): configure the web tier	TAGS: []
  tasks:
    webapp : install packages	TAGS: []
    webapp : write configuration	TAGS: []
    webapp : start and enable the service	TAGS: []

Knowledge check

Knowledge check · 4 questions

  1. Q1. A role author puts a Jinja template at roles/webapp/templates/nginx.conf.j2 and a task in the role uses src: nginx.conf.j2 with no path. What makes that work?

  2. Q2. Which role directories does Ansible load a main.yml from automatically when the role runs? Select all that apply.

  3. Q3. A file written to roles/webapp/task/main.yml, with the directory name in the singular, produces a warning when the role runs.

  4. Q4. You run a play on your controller and it behaves differently from the same play on a colleague controller, same repository and same inventory. Which cause fits that symptom best?

Passing score: 75%. Answers are checked in this browser.