Scope & Precedence #
Variables are the main foundation of dynamic, flexible automation. In Ansible, we use variables to store dynamic values like package names, service ports, directory paths, API keys, and database credentials. However, as automation project complexity grows, you’ll often find situations where a variable is defined in several different places simultaneously. When the playbook executes, the active variable value turns out to be different from what you expected.
That problem is almost always caused by failing to understand two vital concepts: variable scope and variable precedence. Ansible has one of the most complex priority systems in the configuration management tool world, with 22 levels evaluated in sequence. This article comprehensively dissects how Ansible limits variable accessibility by scope, details the 22 priority levels in depth, compares the four most commonly used variable levels, and presents namespacing strategies to avoid variable name collisions in production environments.
Three Variable Scopes #
Before learning how strongly a variable can override another, you must first understand the boundaries of the space where variables live and can be accessed. In Ansible, all variables are grouped into three scope levels:
flowchart TD
Global["Global Scope (Available on all hosts, plays, and roles)<br/>Example: Extra vars via CLI (-e), ansible.cfg configuration"]
Play["Play Scope (Available to all hosts in one play)<br/>Example: vars:, vars_files:, vars_prompt: in the playbook"]
Host["Host Scope (Only available to a specific host)<br/>Example: host_vars/, group_vars/, host facts, registered vars"]
Global --> Play
Play --> Host1. Global Scope #
Variables in the global scope can be read from anywhere during playbook execution. They aren’t bound to a specific host or play. The main sources of the global scope are command line extra variables entered using the -e or --extra-vars options, as well as internal configuration from the ansible.cfg file.
2. Play Scope #
Variables in the play scope are defined inside the play structure in the playbook. They’re available to all hosts targeted by that play, plus all roles included in it. Examples are variables declared under the vars: option, external variable files under vars_files:, or interactive user input under vars_prompt:.
3. Host Scope #
Variables in the host scope are very specific and only bound to one particular host. Even if you define them in a group (like files inside the group_vars/ directory), Ansible internally associates those variables with each host that’s a member of that group during execution. Other examples of host scope are system fact data (host facts), variables defined in the host_vars/ directory, and variables captured from tasks using the register keyword.
Ansible’s 22 Precedence Levels #
When an identical variable name is defined in various scopes and locations, Ansible uses a very strict tie-breaking algorithm to determine which value to use. This algorithm consists of 22 priority levels.
Here’s the complete list of variable priority order from the weakest (lowest) to the strongest (highest):
Lowest Priority (Easiest to Override):
1. command line values (Built-in CLI argument values, e.g. -u REMOTE_USER)
2. role defaults (defaults/main.yml file inside a role)
3. inventory file or script group vars (Group variables written directly in hosts.ini/yaml)
4. inventory group_vars/all (Global all group variables from the inventory directory)
5. playbook group_vars/all (Global all group variables from the playbook directory)
6. inventory group_vars/* (Specific group variables from the inventory directory)
7. playbook group_vars/* (Specific group variables from the playbook directory)
8. inventory file or script host vars (Host variables written directly in hosts.ini/yaml)
9. inventory host_vars/* (Specific host variables from the inventory directory)
10. playbook host_vars/* (Specific host variables from the playbook directory)
11. host facts / cached set_facts (System collection data or cached set_fact)
12. play vars (vars: block at the play level in the playbook)
13. play vars_prompt (Interactive vars_prompt: variables at the play level)
14. play vars_files (vars_files: variable files at the play level)
15. role vars (vars/main.yml file inside a role)
16. block vars (vars: block attached to a block structure)
17. task vars (vars: block attached directly to one specific task)
18. include_vars (Variables dynamically loaded mid-task using include_vars)
19. set_facts / registered vars (Dynamic set_fact or register variables at runtime)
20. role (and include_role) params (Argument parameters passed when calling a role)
21. include params (Argument parameters passed when calling include_tasks/block)
22. extra vars (CLI variables passed using -e or --extra-vars) <-- Highest Priority (Always Wins)
This priority hierarchy can be visualized with the following Mermaid diagram to make the override flow easier to understand:
flowchart TD
subgraph LowPrecedence["Weak Levels (Defaults & Inventory)"]
Level2["Role Defaults (defaults/main.yml)"] --> Level3["Inventory Group Vars (hosts.ini)"]
Level3 --> Level4["Inventory group_vars/all"]
Level4 --> Level6["Inventory group_vars/group_name"]
Level6 --> Level9["Inventory host_vars/host_name"]
end
subgraph MidPrecedence["Mid Levels (Play & Role Vars)"]
Level9 --> Level11["Host Facts (setup module)"]
Level11 --> Level12["Playbook Vars (vars:)"]
Level12 --> Level14["Playbook vars_files"]
Level14 --> Level15["Role Vars (vars/main.yml)"]
end
subgraph HighPrecedence["Strong Levels (Task, Runtime & Extra)"]
Level15 --> Level17["Task Vars (vars:)"]
Level17 --> Level19["set_fact & registered vars"]
Level19 --> Level20["Role Parameters (include_role vars)"]
Level20 --> Level22["Extra Vars (-e CLI parameter)"]
end
style Level2 stroke:#f43f5e,stroke-width:2px
style Level15 stroke:#eab308,stroke-width:2px
style Level22 stroke:#22c55e,stroke-width:2pxThe Four Most Commonly Used Variable Levels #
Although memorizing all 22 levels sounds intimidating, in day-to-day project development you actually only actively interact with four main priority levels. You must understand the tactical function of each level to design clean automation.
1. Role Defaults (defaults/main.yml)
#
The defaults/main.yml file inside the role directory sits at level 2 priority. This is the weakest variable storage location in Ansible.
- Function: Used to set “safe” default values for all variables your role needs.
- Philosophy: “These are my recommended values for standard usage. Feel free to override them if your infrastructure has a different configuration.”
- Example:
# roles/nginx/defaults/main.yml --- nginx_port: 80 nginx_worker_connections: 1024 nginx_enable_ssl: false
2. Inventory Variables (group_vars/ and host_vars/)
#
Inventory variables sit at levels 3 through 10 priority. They automatically override role defaults.
- Function: Used to adjust configuration based on the operational environment (Development, Staging, Production) or specific physical host characteristics.
- Philosophy: “For the production server group, we must use port 443 with larger connections. The role default values above need adjusting.”
- Example:
# inventory/production/group_vars/webservers.yml --- nginx_port: 443 nginx_worker_connections: 4096 nginx_enable_ssl: true
3. Role Vars (vars/main.yml)
#
The vars/main.yml file sits at level 15 priority. This is one of the fairly high priority levels that’s hard to override.
- Function: Used to set internal constant values that form the basis of the code working inside that role. Variables here are not intended to be changed by role users from outside.
- Philosophy: “These values are patented internal configuration. If they’re replaced carelessly, our role could hit system errors.”
- Example:
# roles/nginx/vars/main.yml --- nginx_config_dir: /etc/nginx nginx_pid_path: /var/run/nginx.pid nginx_binary_path: /usr/sbin/nginx
4. Extra Vars (-e on the CLI)
#
Extra variables sent through the terminal CLI sit at level 22 priority. This is the absolute priority that nothing can override.
- Function: Used for temporary overrides during debugging, ad-hoc testing, or emergency execution.
- Philosophy: “I want to run this playbook right now specifically with these parameters, ignore all built-in configuration written in the files.”
- Example:
ansible-playbook -i inventory/ site.yml -e "nginx_port=8080 nginx_enable_ssl=false"
Comparative Analysis: The Philosophical Difference Between Defaults vs Vars in Roles #
One of the biggest confusions developers often experience is the difference between the defaults/ and vars/ directories in the Ansible Role structure. Both define variables for the role, but they have opposite purposes.
The table below explains the difference so you don’t choose the wrong storage location:
| Characteristic | defaults/main.yml | vars/main.yml |
|---|---|---|
| Priority Order | Level 2 (Very Weak). | Level 15 (Very Strong). |
| Usage Purpose | Provides customizable built-in configuration values. | Provides system constants, OS mappings, and internal paths. |
| Override Ease | Very easy to override by inventory, playbook, vars_files, or CLI. | Very hard to override (only overridable by task vars, role parameters, or CLI). |
| Team Collaboration | Intended to be exposed as the “API” or configuration input for role users. | Intended as a private space for the role author’s internal logic. |
Anti-Pattern Code vs Practical Solution Comparison #
Let’s observe the mistake of putting variables that should be dynamic into the vars/main.yml file (anti-pattern) and how to fix it:
# ANTI-PATTERN: Putting the port variable in vars/main.yml
# FILE: roles/nginx/vars/main.yml
---
nginx_port: 80
# ✗ Error: If the role user wants to change the webserver port to 8080
# through the inventory file group_vars/webservers.yml, that value will be IGNORED.
# The vars/main.yml priority (Level 15) is far higher than group_vars/ (Level 6).
# SOLUTION: Separate dynamic variables into defaults/ and constants into vars/
# FILE: roles/nginx/defaults/main.yml
---
nginx_port: 80
# ✓ Correct: Users can easily override this value from inventory.
# FILE: roles/nginx/vars/main.yml
---
nginx_system_user: www-data
nginx_mime_types_path: /etc/nginx/mime.types
# ✓ Correct: These OS constants are safe from accidental overrides.
Mitigating Variable Name Collisions (Namespacing) #
Because Ansible evaluates variables flat at the end of a play for each host, there’s a big risk of variable name collisions if you use overly generic variable names in several different roles.
For example, if you have a mysql role and an nginx role, and both define a variable named port in their defaults files:
mysqldefinesport: 3306nginxdefinesport: 80
When both roles run in one play on the same host, one port value overrides the other depending on the role calling order. This causes one of the services to be fatally configured with the wrong port.
Solution Tactic: Applying Role Prefixes (Namespacing) #
To avoid this disaster in production environments, you must apply disciplined variable writing rules by including the role name as a prefix for all declared variables.
# ANTI-PATTERN: Overly generic variable names prone to collisions
# Inside the mysql role:
port: 3306
config_file: /etc/my.cnf
# Inside the nginx role:
port: 80
config_file: /etc/nginx/nginx.conf
# CORRECT: Applying role-name-based namespacing
# Inside the mysql role:
mysql_port: 3306
mysql_config_file: /etc/my.cnf
# Inside the nginx role:
nginx_port: 80
nginx_config_file: /etc/nginx/nginx.conf
# ✓ Safe: No variable collisions even if both roles run simultaneously.
Securing Variable Values with the default Filter
#
Sometimes you want to make sure your playbook still runs safely even if a certain variable hasn’t been defined at all in inventory or role files. You can prevent undefined variable errors by leveraging the Jinja2 default filter.
# Securing execution by providing a dynamic fallback value
- name: Create a custom configuration file
template:
src: custom_settings.conf.j2
dest: /opt/app/settings.conf
vars:
# If the 'app_max_memory' variable isn't defined anywhere,
# use the fallback default value '512m'.
max_mem: "{{ app_max_memory | default('512m') }}"
Using the default filter is highly recommended to maintain the resiliency of your Jinja2 templates and task modules.
Summary #
- Three Main Scopes: Ansible variables are isolated into three scope levels: Global, Play, and Host scopes.
- 22 Precedence Levels: Ansible has a 22-level priority evaluation rule, running from the lowest priority (
role defaults) to the highest (extra vars -e).defaults/Philosophy: Use thedefaults/main.ymlfile to declare role default values intended to be easily overridden from outside.vars/Philosophy: Use thevars/main.ymlfile to store internal role constant values that users shouldn’t replace carelessly.- Namespacing: Always prefix all variables with the role name (for example
nginx_portinstead ofport) to prevent cross-role variable name collisions.- Default Filter: Use the
| default()filter inside Jinja2 templates to provide a safe fallback value if the target variable hasn’t been defined yet.