unit 6 · Making it real

Roles: structure, defaults vs vars

The folder layout every repo assumes you know

essential · 8 min · linux + windows

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)
the standard role layout — roles/nginx/

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 order
site.yml — the playbook orchestrates, the roles execute

Listed 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 must
roles/nginx/defaults/main.yml — the parameter surface

Note 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. 1 Where does a role's entry point live, and what else loads automatically when a play applies the role?

  2. 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. 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.

next up 25 · import vs include 6 min · unit 6