unit 6 · Making it real
Roles: structure, defaults vs vars
The folder layout every repo assumes you know
you may want to read 16 · Variables & the precedence that matters , 20 · Handlers, notify & listen first — but nothing is locked
tl;dr
- A role packages tasks, handlers, templates, files and vars in a fixed layout; every main.yml auto-loads.
- defaults/ holds user-tunable knobs (lowest precedence); vars/ holds internal constants (high).
- Roles never name hosts — playbooks orchestrate roles against hosts. And a role is not a collection.
why it matters Real repos are roles all the way down — reading the layout cold, and knowing which variables are knobs vs constants, is the difference between navigating the codebase and being lost in it.
Lesson 16 left you an IOU. Two of its five precedence levels — role defaults at the bottom of the ladder, role vars above your inventory — belonged to something you hadn’t met. Meet it: the role , Ansible’s unit of reuse. A role bundles tasks with everything they lean on — handlers, templates, files, variables — into one directory tree. Variables parameterize it, so the same role configures dev and prod without an edit.
The tree is convention, not configuration. Ansible finds everything by directory name:
roles/nginx/
├── tasks/ # main.yml — entry point: what the role does
├── handlers/ # main.yml — handlers its tasks notify
├── templates/ # .j2 files for ansible.builtin.template
├── files/ # static files for ansible.builtin.copy
├── defaults/ # main.yml — user-tunable knobs (lowest precedence)
├── vars/ # main.yml — internal constants (high precedence)
├── meta/ # main.yml — dependencies + role metadata
├── tests/ # scaffold for a quick smoke-test playbook
└── library/ # role-local custom modules (rare)Every directory you use holds a main.yml, and each one auto-loads
when a play applies the role. No imports, no manifest. tasks/main.yml
runs, handlers/main.yml becomes notifiable, both variable files enter
the ladder — all by naming convention. Inside a role, src: nginx.conf.j2
resolves into the role’s own templates/ directory, and copy sources
resolve into files/ — no paths to build. Directories you don’t need
simply don’t exist; a minimal role is tasks/main.yml alone.
Roles do the work, playbooks aim it
Nothing in that tree names a host. That’s the division of labor: a role is
the reusable what, a playbook is the where and when — it targets hosts
and orchestrates roles against them. The classic wiring is a play’s
roles: list:
- name: Configure web tier
hosts: web
become: true
roles:
- nginx # runs roles/nginx — tasks, handlers, vars, all of it
- app_deploy # then this one, in listed orderListed roles run in order, before any tasks: the play defines itself.
Lesson 25 adds the task-level alternatives — import_role and
include_role — and when each is worth it. The roles: list is the form
you’ll read most in existing repos.
defaults vs vars — lesson 16’s loop, closed
Both files define variables; the ladder is the entire difference.
defaults/main.yml sits at the very bottom — anything beats it. That
makes it the role’s parameter surface: document every knob a consumer may
tune there, with a sane value. vars/main.yml sits above inventory, so
group_vars/ can’t touch it. That makes it the place for internal
constants: package names, paths, values the role’s logic depends on and
nobody should “configure”.
nginx_listen_port: 8080 # override per environment in group_vars/
nginx_worker_count: 2 # sane default, tune if you mustNote the names. All variables share one flat namespace across every role
in a run, so prefix each role variable with the role name —
nginx_listen_port, never listen_port. A second role defining its own
listen_port would silently collide with yours. The sibling convention:
one concern per role — nginx, not web_server_and_firewall_and_users.
Small, focused roles compose; kitchen-sink roles get copy-pasted and
forked.
meta/, dependencies and the two-noun trap
meta/main.yml carries the role’s metadata — author, license, supported
platforms — and its dependencies: list. Roles named there run
automatically before this role does. Ansible deduplicates: a dependency
shared by several roles runs only once per play unless it sets
allow_duplicates: true. A nginx role can depend on a common
hardening role and assume its work is done. Use it sparingly — an
explicit roles: list in the playbook is easier to read than a
dependency chain three levels deep.
You rarely type that tree by hand. ansible-galaxy role init nginx
scaffolds the skeleton in one command — every directory above except the
rare library/, empty main.yml files included. Fill in tasks/,
expose your knobs in defaults/, and you’ve built the thing lesson 16
could only point at. Next lesson: import_role versus include_role,
and the parse-time-versus-runtime difference that decides between them.
self-check — recall, not a test
1 Where does a role's entry point live, and what else loads automatically when a play applies the role?
2 Consumers of your role should be able to override its port, but never its internal install path. Which file gets each value, and why?
3 A colleague says "the role is in that collection — install it." Untangle the two nouns.
lab (optional — never required)
Scaffold a role with ansible-galaxy role init nginx, put one debug task in tasks/main.yml printing a defaults/ variable, and apply it from a one-play site.yml.
needs: The control node from lesson 5.