When authoring an Ansible role, how do you decide whether a variable belongs in defaults/main.yml or vars/main.yml?
answer
- same loader, different height
- who is allowed to override it
- group_vars beats one, loses to the other
- public knobs versus private constants
- silent override loss is the symptom
basics
~20 sdefaults/main.yml holds the role's overridable knobs: it is the lowest-precedence variable source, so almost anything a consumer sets wins. vars/main.yml sits far higher, so reserve it for role-internal constants the consumer should not have to change.
solid answer
~50 sBoth files are loaded automatically, so the choice is purely about precedence, and precedence is really about who owns the value. Role defaults sit at the very bottom of Ansible's precedence ladder - inventory `group_vars`, `host_vars`, play `vars`, task vars and `-e` all beat them - which makes `defaults/main.yml` the role's documented public interface: every setting a consumer might reasonably tune goes there, with a sane value. Role vars sit near the top, above inventory and play vars, so anything you put in `vars/main.yml` cannot be changed from `group_vars` at all; only task-level vars, role parameters, `set_fact` and `-e` override it. That is right for internal derived values - a package name that differs per OS family, a path the role's own templates depend on - and wrong for anything a user might want to tune, because they will try `group_vars`, watch it silently do nothing, and file a bug against your role.
code
yaml · 8 lines# roles/nginx/defaults/main.yml - consumer-facing, lowest precedence
nginx_listen_port: 80
nginx_worker_processes: auto
nginx_manage_firewall: true
# roles/nginx/vars/main.yml - role-internal, high precedence
nginx_conf_dir: /etc/nginx
nginx_service_name: nginxgo deeper
Remember the direction: defaults/main.yml is the low-precedence file everything overrides, vars/main.yml is the high-precedence one. Say that consumer-tunable settings go in defaults.
Explain the precedence positions concretely - defaults lose to inventory and play vars, role vars beat both - and name what still overrides role vars: task vars, role parameters, set_fact and -e.
Frame it as interface design and diagnose the silent-override ticket: an operator's group_vars value ignored because the author used vars/. Show how you use vars/<os_family>.yml with include_vars for derived platform values.
Own the convention for shared roles: which variables form the supported interface, how they are named and prefixed, and what changing a default means for every downstream consumer when the role is versioned and pinned.
## The two files are the same mechanism at different heights `defaults/main.yml` and `vars/main.yml` are both loaded automatically when the role is applied. They differ in exactly one respect: where their values sit in Ansible's variable precedence order. Everything else - syntax, templating, timing - is identical. So the question "which file?" is really "who is allowed to override this?" ## Where each one sits Ansible resolves variables through a long precedence list, lowest to highest. The two positions that matter here: - **Role defaults** are second from the bottom, above only command-line connection settings. Inventory group vars, inventory host vars, playbook `group_vars`/`host_vars`, host facts, play `vars`, `vars_files`, block and task vars, `include_vars`, `set_fact`, role parameters and `-e` extra vars **all beat them**. - **Role vars** sit much higher - above inventory group/host vars, above host facts, and above play `vars` and `vars_files`. They are beaten only by block vars, task vars, `include_vars`, `set_fact`/registered vars, role and include parameters, and `-e` extra vars. The practical consequence: a value in `defaults/` can be changed from anywhere; a value in `vars/` cannot be changed from `group_vars` or `host_vars`, which is where operators naturally reach first. ## The authoring rule Treat `defaults/main.yml` as the role's **API documentation that also runs**. Every knob you expect a consumer to touch is declared there, with a working default and a comment: ```yaml # roles/nginx/defaults/main.yml nginx_listen_port: 80 nginx_worker_processes: auto nginx_manage_firewall: true ``` Treat `vars/main.yml` as the role's **private constants**: values the role's own tasks and templates depend on, that a consumer changing would simply break. ```yaml # roles/nginx/vars/main.yml nginx_service_name: nginx nginx_conf_dir: /etc/nginx ``` A useful test: if a wrong value would be the *consumer's* mistake, it belongs in `defaults/`. If a wrong value would be *your role's* bug, it belongs in `vars/`. ## The classic use of vars/: per-platform values The most defensible reason to use `vars/` is OS-family branching, where the value is derived rather than chosen. Roles commonly ship `vars/Debian.yml` and `vars/RedHat.yml` and load the right one at runtime: ```yaml - name: Load OS-specific values ansible.builtin.include_vars: "{{ ansible_facts['os_family'] }}.yml" ``` Note that this uses `include_vars`, whose precedence is higher still - which is deliberate, because these values must beat whatever the automatically loaded files set. Only `vars/main.yml` is auto-loaded; other files in `vars/` are inert until something includes them. ## The failure this prevents The canonical support ticket is: an operator sets `nginx_listen_port: 8080` in `group_vars/web.yml`, runs the play, and the config still says 80. Nothing errors, nothing warns. The cause is that the role author put `nginx_listen_port` in `vars/main.yml`, which outranks group vars. The operator's only escapes are `-e nginx_listen_port=8080` on the command line or passing it as a role parameter - both of which are worse than the role simply having used `defaults/`. Silent precedence loss is far more expensive to debug than a missing default, so when you are unsure, choose `defaults/`. ## Two related things that are not the same Role **parameters** - values passed at the call site, either in the `roles:` list or as `vars:` on `include_role` - sit near the very top of precedence, above role vars. That is the sanctioned way for a caller to force a value into a role regardless of which file it came from, and it is scoped to that one invocation rather than leaking across the play. Also, do not confuse "low precedence" with "optional". A variable declared only in `defaults/` still exists for every host the role touches; if you want a variable that has *no* default and must be supplied, omit it from both files and assert it early with `ansible.builtin.assert`, so the role fails loudly at the top instead of rendering a template with an undefined value halfway through. ## Naming Because role variables land in the play's flat namespace, prefix everything with the role name (`nginx_listen_port`, not `port`). Two roles that both define `port` in `defaults/` will collide, and the winner is decided by ordering rather than by intent - a bug that only appears once someone applies both roles in the same play.
- A user sets your role's variable in group_vars and nothing changes. What went wrong?The variable was declared in the role's `vars/main.yml`, which outranks inventory group vars, so the role's own value keeps winning and no error is raised. The fix is to move it to `defaults/main.yml`; the workaround for the user in the meantime is `-e` or passing it as a role parameter, both of which are worse than fixing the role.
- How do you make a role variable mandatory rather than defaulted?Leave it out of both `defaults/` and `vars/`, and assert it in the first task with `ansible.builtin.assert` and a clear `fail_msg`. The role then fails immediately with a readable message instead of rendering a template from an undefined value, and the requirement is visible in the role's code rather than only in its README.
- Why prefix every role variable with the role name?Role variables land in the play's flat namespace, so two roles that both define `port` collide and the winner depends on precedence and ordering rather than intent. Prefixing (`nginx_listen_port`) makes collisions impossible in practice and makes it obvious in a playbook which role a value belongs to.
saying these in an interview costs you the question
- Says defaults/ and vars/ are interchangeable
- Claims vars/main.yml cannot be overridden by anything
- Puts every tunable setting in vars/ for safety
- Thinks defaults/ makes a variable optional or nullable
- Uses unprefixed names like port or version in a role