Skip to main content
RunBook Academy

AnsibleXIII · Variables and PrecedenceDesign

defaults/ and vars/ are a policy decision

Intermediate⏱ ~18 minansible-playbook

What you'll learn

  • State the precedence distance between role defaults and role vars, and what it means
  • Decide which of the two files a value belongs in, as an interface decision
  • Predict exactly which sources can still override a role var
  • Recognise the failure mode where an inventory override silently does nothing

Prerequisites

Verified against ansible-core 2.21.x · ansible (community package) 14.x · Python (controller) 3.12+ · ansible-lint 26.x · Molecule 26.x · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · RHEL / Rocky / AlmaLinux 9.x · 2026-08-11

Not yet marked complete on this device.

A role ships two files that both hold variables and differ by one word in the path:

roles/webapp/defaults/main.yml
roles/webapp/vars/main.yml

They sit at entries 2 and 15 of the precedence ladder. Thirteen levels apart — nearly the whole system, with the entire inventory in between. Which file a value goes in is one of the more consequential decisions in a role, and it is routinely made by whichever file the author opened first.

The decision is an interface declaration:

  • defaults/main.yml says “callers may change this.”
  • vars/main.yml says “callers may not change this from the inventory.”

That second phrasing is deliberately narrower than the usual “callers may not change this”, and the reason is in the section after next.

The distance, demonstrated

Take a role with one value in each file, and an inventory that tries to override both:

# roles/webapp/defaults/main.yml
webapp_listen_port: 80

# roles/webapp/vars/main.yml
webapp_tls_min_version: '1.2'
# inventory/group_vars/web.yml - the operator overrides both
webapp_listen_port: 443
webapp_tls_min_version: '1.0'
Read-only / Safeone override lands, one does not
$ ansible-playbook site.yml
TASK [webapp : ansible.builtin.debug] ******************************************
ok: [web-01.example.com] => {
  "msg": "port=443 tls=1.2"
}

The port moved to 443. The TLS floor stayed at 1.2 despite a group_vars file explicitly setting 1.0. Same file, same syntax, same review — one line worked and the other was inert.

In this particular case that is exactly right: a role that refuses to let the inventory weaken its TLS floor is a role doing its job. But the mechanism is the same whether the intent was good or accidental, and an inert configuration line is dangerous precisely because it reads as a working control.

What can still override a role var

“Callers may not change this” is too strong. Role vars are at entry 15, and seven entries sit above them. Two of those matter in practice:

Read-only / Safea role parameter overrides a role var
$ ansible-playbook site2.yml
ok: [web-01.example.com] => {
  "msg": "port=443 tls=1.3"
}
Configuration changeand so does an extra var
$ ansible-playbook site.yml -e webapp_tls_min_version=1.1
ok: [web-01.example.com] => {
  "msg": "port=443 tls=1.1"
}

So the precise statement is:

A value in role vars/ cannot be overridden by anything in the inventory, nor by play vars or vars_files. It can be overridden by a role parameter, by set_fact or include_vars inside the role, and by -e.

That is a useful property rather than a loophole. It means vars/ makes a value not configurable per host or per environment, while leaving a deliberate, visible path for a caller who genuinely needs to change it — written at the call site, in the playbook, where a reviewer sees it.

The -e route is the one to be uneasy about, and lesson 6 is about exactly that.

How to choose

Ask one question: would a reasonable caller ever need a different value?

Yes, or possiblydefaults/main.yml. This covers most things: ports, paths, sizes, feature flags, package versions, log levels, timeouts.

No, and a different value would break the rolevars/main.yml. This is a much smaller set than people expect:

  • Internal constants. A lookup table the role’s own templates use. Not configuration; implementation detail that happens to live in YAML.
  • Platform maps. The package name that differs between Debian and RHEL. Derived from facts, not chosen by an operator.
  • Safety floors. A minimum TLS version, a refusal to write world-writable modes. Values where “configurable” is itself the defect. Prefer an assert for these where you can, because a failed assertion explains itself and a silently-ignored override does not.

Everything else goes in defaults/. When genuinely unsure, choose defaults/: an override that works when it should not is a visible mistake somebody can see and correct, while an override that silently does nothing is invisible and gets escalated to -e.

Platform maps: the good use of vars/

The strongest legitimate use of vars/ is not main.yml at all. It is per-platform files selected at run time:

roles/webapp/vars/
├── main.yml
├── Debian.yml
└── RedHat.yml
# roles/webapp/vars/Debian.yml
webapp_package_name: apache2
webapp_service_name: apache2
webapp_config_dir: /etc/apache2

# roles/webapp/vars/RedHat.yml
webapp_package_name: httpd
webapp_service_name: httpd
webapp_config_dir: /etc/httpd
# roles/webapp/tasks/main.yml
- name: load platform-specific names
  ansible.builtin.include_vars: "{{ ansible_facts['os_family'] }}.yml"

These are facts about the operating system, not decisions an operator gets to make. An inventory that overrode webapp_package_name would be declaring the wrong package name for the platform, so making it un-overridable from the inventory is correct.

Note that include_vars is entry 18 — higher than vars/main.yml at 15 — so a platform file loaded this way beats anything in main.yml. That is the intended layering: general internal values in main.yml, platform specialisation on top.

Knowledge check

Knowledge check · 4 questions

  1. Q1. An operator adds a variable to inventory/group_vars/prod.yml to change a role behaviour. The play runs clean and the behaviour does not change. What is the most likely cause?

  2. Q2. Which sources can still override a value set in a role vars/main.yml? Select all that apply.

  3. Q3. Moving a variable from defaults/main.yml to vars/main.yml is a safe internal refactor, because the role behaves identically.

  4. Q4. A role author is unsure whether a value belongs in defaults/ or vars/. Which way should they lean, and why?

Passing score: 75%. Answers are checked in this browser.