Where a Role's Values Really Come From

Scenario: A team sets webapp_port in group_vars/webservers.yml, but every host still serves the role's default of 8080. The role declared the variable in vars/main.yml.

New words, in plain English

This section exists because the defaults versus vars distinction causes more confusion than any other part of roles, and the symptom is always the same: an override that appears to be ignored.

Recall the precedence order. Role defaults are the weakest thing in Ansible - below inventory group vars, below host vars, below everything. Role vars are near the top - above group vars, above host vars, above play vars. The two directories differ by three letters and by nearly the entire precedence range.

So a value in vars/main.yml cannot be overridden from group_vars, from host_vars, or from a play's vars: block. Only role params at the call site or -e on the command line will beat it. If your role's users are reporting that configuration "does not work", this is the first thing to check.

The decision rule is simple: defaults for anything a user might reasonably change; vars for internal constants whose modification would break the role. A package name that differs per OS family belongs in vars/ (loaded via include_vars); a port number does not.

Analogy: defaults is a suggested retail price - everyone expects to negotiate it. vars is the serial number stamped into the casing. Putting the price on the casing does not stop customers asking; it just stops you from ever answering.

A worked example

# roles/webapp/defaults/main.yml   -> WEAKEST. Users can override freely.
webapp_port: 8080
webapp_workers: 4

# roles/webapp/vars/main.yml       -> NEAR THE TOP. Effectively constant.
webapp_config_path: /etc/webapp/webapp.conf   # changing this breaks the role

# group_vars/webservers.yml
webapp_port: 8090        # WINS over defaults        -> 8090 applied
webapp_config_path: /tmp # LOSES to role vars        -> ignored entirely

# The escape hatches that do beat role vars:
- role: webapp
  webapp_config_path: /opt/webapp/webapp.conf     # role param

ansible-playbook site.yml -e webapp_config_path=/opt/x   # extra vars

There is a legitimate and common use of vars/ that is worth knowing: OS-conditional values loaded at run time. A role can keep vars/Debian.yml and vars/RedHat.yml with the right package and path names for each family, then load the correct one with include_vars: "{{ ansible_facts['os_family'] }}.yml". Those genuinely are internal constants for the platform, and their high precedence is appropriate.

When you are debugging an override that seems ignored, the fastest confirmation is a debug task placed inside the role: it prints the value the role will actually use, at the point it will use it, which settles the question in one run.

Tip: Symptom-to-cause shortcut: 'my group_vars override is being ignored by a role' almost always means the role declared that variable in vars/ instead of defaults/.
Goal: Put this to work in the ansible-variable-precedence lab. Open /labs/ansible, pick ansible-variable-precedence, and fix the real broken project - Ansible really does SSH into four managed hosts and converge them.