Chapter 8: Roles & Reuse
Topic 46

Role Variables

RolesVariables

A role has two places to put variables, and they sit at opposite ends of the 22-level precedence hierarchy from Chapter 5. defaults/main.yml is the role's public interface — the weakest precedence in the entire system, so any inventory, group_vars, or playbook value overrides it. vars/main.yml is high precedence, reserved for internal constants the caller has no business changing. Getting these backwards is the single most common reason a role "ignores" a variable a user set, or why a value the role needs locked gets silently overridden.

The two files look almost identical on disk, which is the trap — both are flat YAML maps in the role. The difference is entirely about precedence, and that difference is the role's contract with whoever applies it. Decide which file a variable lives in and you have decided whether the caller controls it or the role does.

defaults/main.yml — the Public Knobs

defaults/main.yml holds the values a user is meant to override: larkspur_web_nginx_port: 443, larkspur_web_workers: 4. These sit at the very bottom of precedence — below inventory, group_vars, host_vars, and play vars — so the caller always wins. That is the point. A default is a sensible starting value the role ships with, not a decision the role enforces; set the same name anywhere more specific and it takes over without the role objecting.

defaults/main.yml — the role's documented, overridable API
# roles/larkspur_web/defaults/main.yml
larkspur_web_nginx_port: 443        # listen port; any caller value wins
larkspur_web_workers: 4            # gunicorn worker processes
larkspur_web_app_path: /opt/larkspur  # where the app is deployed

Every name carries the role prefix and a comment, so the file reads as the role's documentation — open it and you know exactly what is tunable and what each knob does. Nothing here is enforced; everything here is a suggestion the caller can replace from group_vars or the command line.

vars/main.yml — Internal Constants

vars/main.yml is high precedence, for values the role depends on internally that a caller overriding would break — a package name, a fixed path layout, a service unit name. Almost nothing belongs here. Role vars outrank group_vars and host_vars, so anything you put in vars/ the caller cannot change short of an even higher level. Reaching for vars/main.yml is usually a sign that a value should have been a default instead, exposed to the caller rather than locked away.

Where They Land in the 22 Levels

The gap between the two files spans nearly the whole hierarchy. Role defaults sit at the very bottom — the lowest-precedence variable source, with only the non-variable command-line connection flags ranked beneath them. Role vars sit high up, above group_vars and host_vars, near the top of the inventory-derived levels. That distance is the design, not an accident: defaults are deliberately easy to override and vars are deliberately hard, and the same role ships both so it can offer knobs and protect constants at once.

A role's two variable files, at opposite ends of precedence
defaults/main.yml
Weakest precedence — the public knobs. Below inventory, group_vars, and play vars, so any caller value wins. The role's documented, overridable interface.
vars/main.yml
High precedence — internal constants. Above group_vars and host_vars, so a caller can't change them. For values that would break the role if touched.

Namespacing Role Variables

Prefix every role variable with the role name — larkspur_web_port, never bare port. Roles share one variable namespace per play, so two roles that both define port or version collide, and the second one's value silently clobbers the first. The prefix is the only thing keeping larkspur_web and a database role from stepping on each other's variables. Unprefixed names are how two well-behaved roles produce a bug that belongs to neither.

vars/main.yml — locked internals, still namespaced
# roles/larkspur_web/vars/main.yml
larkspur_web_pkg: nginx          # the OS package name the role installs
larkspur_web_service: nginx.service  # systemd unit the role manages

These are not knobs — a caller who changed larkspur_web_pkg would break the role, which is exactly why they live in vars/ at high precedence. They are still prefixed, because namespace collisions do not care which file a variable came from.

The Interface Contract

defaults/ is the documented surface a user reads and tunes; vars/ is implementation detail they should never touch. Treating defaults as the API and vars as private is what makes a role safe to reuse without reading its tasks — a caller configures larkspur_web from its defaults file alone, the same way they would read a Terraform module's input variables without opening its resources. That contract is the difference between a role you can hand to another team and one only its author can drive.

Common Mistakes
  • Putting a value a user needs to override in vars/main.yml, so their group_vars setting is silently ignored because role vars outrank group_vars — the user swears they set the port and the role keeps using its own number.
  • Putting a value the role depends on internally in defaults/, so an unrelated group_var with the same name silently changes the role's behavior and breaks it mid-run.
  • Leaving role variables unprefixed — port, version — so two roles in the same play collide and the second one's default quietly wins, producing a bug in neither role's own code.
  • Hardcoding a value in tasks/main.yml that should have been a default, so reusing the role for web2 means editing its source — at which point it is no longer reusable.
  • Reading "defaults are weak" as "defaults do not matter" and skipping them, leaving a role with no documented interface and forcing every caller to read the tasks to learn what is tunable.
Best Practices
  • Put every value a caller might tune in defaults/main.yml and prefix it with the role name, making the defaults file the role's readable, overridable API.
  • Reserve vars/main.yml for true internal constants a caller must not change, and treat needing it often as a smell that a value should be a default instead.
  • Verify the effective value with ansible-inventory --host or a debug: task when a role "ignores" a variable, since the cause is almost always a precedence level you forgot sits above the default.
  • Document each default with an inline comment stating its purpose and valid range, so the interface is self-describing without a separate doc.
  • Prefix every variable with the role name in both defaults/ and vars/, so the role can share a play with any other role and never collide on a bare port or path.
Comparable tools Terraform input variables with default — the same weak-precedence public interface Puppet class parameters · Hiera data layering plays the same role Chef attribute precedence default vs override is the direct conceptual twin

Knowledge Check

A user sets larkspur_web_workers in group_vars, but the role uses its own value anyway. What is the most likely cause?

  • The variable was defined in the role's vars/main.yml, which outranks group_vars, so the user's value is overridden
  • group_vars are simply never visible inside a role and must instead be passed in as command-line extra-vars
  • The role caches the very first value it ever sees for that variable and then ignores every later override for the rest of the run
  • Role defaults always win out over group_vars, so the default value baked into the role is what applies

Which file is the role's public interface — the surface a caller reads and tunes?

  • defaults/main.yml — it sits at the weakest precedence, so any caller value overrides it
  • vars/main.yml — because it carries the highest precedence, it defines the role's canonical, authoritative values
  • meta/main.yml — it formally declares the full set of input variables the role accepts
  • tasks/main.yml — the caller reads through the tasks themselves to learn what to set

Why must role variables carry a prefix like larkspur_web_?

  • Roles share one variable namespace per play, so an unprefixed port in two roles collides and one silently clobbers the other
  • Ansible flatly refuses to load any role at all whose variables are not consistently prefixed with that role's own directory name
  • The prefix itself raises that variable's precedence up above both group_vars and host_vars
  • Only variables carrying the prefix can be overridden from the command line with -e

Where do role defaults and role vars sit relative to each other in the 22-level precedence hierarchy?

  • Defaults are near the bottom (below inventory and group_vars) and vars are high up (above group_vars) — they bracket nearly the whole hierarchy
  • Both sit at exactly the same precedence level, and any ties between them are broken alphabetically by filename
  • Vars sit right at the very bottom of the precedence hierarchy and defaults sit at the top, the exact reverse of how the inventory variables are ranked
  • Both tiers outrank essentially everything else except command-line extra-vars

You got correct