← All TILs · ansible

Where a variable should live: role defaults, group_vars, host_vars, and extra vars as safety switches

ansible - 2026-10-03

Eighth entry in the Ansible inventory from scratch series. Item 2 gave variables a place in the inventory, and item 7 kept discovered facts out of it. Ansible accepts variables in 22 places, though, ranked by precedence: when two places set the same name, the higher one wins. This entry takes a playbook that uses several of them, moves each setting where the Red Hat Community of Practice's automation good practices say it belongs, and looks at the one place inside a role that catches people out, vars/. Everything below ran with ansible-core 2.21.4.

The ranked list

A role is a packaged set of tasks, templates and variables that configures one thing; here, pgconf renders a postgresql.conf per host. The good practices, Restrict your usage of variable types, keep only a handful of the 22 places, and explain their order:

. role defaults (defined in defaults/main.yml), they are... defaults and can be overwritten by anything. . inventory vars, they truly represent your desired state.

and, further down, "role vars (defined in vars/main.yml) represent constants used by the role", runtime variables from register and set_fact, and "lastly, extra_vars overwrite everything else". They also say to "avoid playbook and play variables, as well as include_vars". In the precedence list of the Ansible docs, Using Variables, those places rank:

Place Rank of 22 What belongs there
role defaults/main.yml 2 every setting the role reads, with a safe value
inventory group_vars/ 6 the desired state of a group
group_vars/ beside the playbook 7 nothing, if the inventory is the source of truth
inventory host_vars/ 9 the desired state of one host
play vars: 12 nothing, by the good practices
role vars/main.yml 15 the role's own constants, never something a user sets
set_fact, register 19 values computed during the run
extra vars, -e 22 safety switches and troubleshooting, not desired state

The same eight places as a ladder: each rung overrides every rung below it, and the colour says what belongs there.

flowchart BT
    R2["2 · role defaults/main.yml<br/>every setting, with a safe value"]
    R6["6 · inventory group_vars/<br/>desired state of a group"]
    R7["7 · group_vars/ beside the playbook<br/>nothing"]
    R9["9 · inventory host_vars/<br/>desired state of one host"]
    R12["12 · play vars:<br/>nothing"]
    R15["15 · role vars/main.yml<br/>the role's constants"]
    R19["19 · set_fact, register<br/>computed during the run"]
    R22["22 · extra vars, -e<br/>safety switches"]
    R2 -->|overridden by| R6 --> R7 --> R9 --> R12 --> R15 --> R19 --> R22
    classDef defaults fill:#e8f0fe,stroke:#4a6fa5,color:#1a1a1a
    classDef desired fill:#d9f2e3,stroke:#2e8b57,color:#1a1a1a
    classDef avoid fill:#eeeeee,stroke:#999999,color:#555555,stroke-dasharray:4 3
    classDef constant fill:#fff1d6,stroke:#c98a00,color:#1a1a1a
    classDef runtime fill:#e6e0f8,stroke:#6a5acd,color:#1a1a1a
    classDef switch fill:#fde0dc,stroke:#c0392b,color:#1a1a1a
    class R2 defaults
    class R6,R9 desired
    class R7,R12 avoid
    class R15 constant
    class R19 runtime
    class R22 switch

Green is where desired state lives; grey, dashed, is what the good practices leave empty; orange sits above the inventory, which is why a role's vars/ can't be overridden from it (below).

A playbook with its settings in the wrong places

The messy version keeps its settings in the play, a set_fact and the command line:

- name: Configure PostgreSQL, settings scattered
  hosts: postgresql
  vars:
    pgconf_max_connections: 200
  tasks:
    - name: Give db1 bigger buffers
      ansible.builtin.set_fact:
        pgconf_shared_buffers: 1GB
      when: inventory_hostname == 'db1'
    - name: Apply the role
      ansible.builtin.include_role:
        name: pgconf

run with -e pgconf_port=5433. It rendered port = 5433 max_connections = 200 shared_buffers = 1GB for db1, but:

The same result, each setting in its place

The tidy version moves every value:

- name: Configure PostgreSQL
  hosts: postgresql
  roles:
    - role: pgconf

It rendered the same values, on the standard port, and ansible-inventory --host db1 now listed them: "pgconf_max_connections": 200, "pgconf_shared_buffers": "1GB". Whoever reads the inventory reads the configuration.

group_vars beside the playbook, or inside a role

group_vars/ isn't only read in the inventory. Ansible also reads a group_vars/ directory next to the playbook, in the project's root folder when the playbook sits there, and ranks it just above the inventory's: playbook group_vars/* is 7th in the list, inventory group_vars/* 6th, and the same holds for host_vars/. The inventory guide, Organizing host and group variables, says it outright: "the variables that Ansible sources relative to the playbook override the variables that it sources relative to the inventory source."

With pgconf_max_connections: 250 in a group_vars/postgresql/ directory beside the tidy playbook, and 200 in the inventory's:

So a group_vars/ beside the playbook is a second, stronger source of desired state that the inventory doesn't show. In a project where the inventory directory and the playbooks are separate, keep group_vars/ and host_vars/ in the inventory only. A role has no group_vars/ of its own: a group_vars/all/ directory inside roles/pgconf/, setting 999, was never read. Settings a role reads go in its defaults/.

Extra vars as safety switches

Extra vars, the variables given with -e on the command line, win over everything, rank 22. That makes them dangerous for desired state, which then exists only for one run, and useful for one thing the good practices name: a switch that protects a destructive step, "something like are_you_really_really_sure: true/false". The role's defaults include pgconf_allow_restart: false, and its task reported "restart skipped: pgconf_allow_restart is false" on a normal run and "restart allowed" with -e pgconf_allow_restart=true. The role tests it with | bool: an extra var given as key=value arrives as the string "true", which part 9 of the data-shaping series explains.

A role's vars/ beats the inventory

The second role, pgconf_constants, has its setting in vars/main.yml, pgconf_constants_max_connections: 50, where a default belongs. group_vars/postgresql set it to 300:

Role vars rank 15, above every inventory variable and below extra vars. The docs put it plainly: "Anything in the vars directory of the role overrides previous versions of that variable in the namespace." The inventory then looks right and has no effect, and the only override left is the command line, which the good practices reserve for safety switches. A role should put anything a user might set in defaults/, and keep vars/ for constants, ideally with a __ prefix, the good practices' convention for names that are internal to the role, so that no inventory variable can share their name.

In short

The example repository

The series' companion repository, abdelhousni/ansible-inventory-series, holds both roles and both versions. run.sh renders the messy and tidy versions into its out/ directory, prints what ansible-inventory --host db1 shows for each, runs the safety switch both ways, and compares the vars/ role's value with the inventory's and with -e. Its CI runs it on every push and compares the output with the expected one.

Sources

Created 2026-10-03T11:51:13+02:00, updated 2026-10-03T13:13:49+02:00 · History · Edit