Ansible Variable Precedence
Introduction
When working with Ansible, variables can be defined in multiple places. But what happens when the same variable is defined in different locations with different values? Which value does Ansible actually use?
This is where variable precedence comes into play. Variable precedence is the hierarchy that determines which variable definition takes priority when the same variable is defined in multiple places.
Understanding this hierarchy is crucial for debugging and writing predictable Ansible playbooks. In this guide, we'll explore the order of precedence for variables in Ansible and provide practical examples to clarify this concept.
The Variable Precedence Hierarchy
Ansible evaluates variables in a specific order, with later sources overriding earlier ones. Here's the complete precedence hierarchy from lowest (first to be overridden) to highest (last to be overridden):
- Command line values (for example,
-e "user=john"
) - Role defaults (defined in
role/defaults/main.yml
) - Inventory file or script group vars
- Inventory group_vars/all
- Playbook group_vars/all
- Inventory group_vars/*
- Playbook group_vars/*
- Inventory file or script host vars
- Inventory host_vars/*
- Playbook host_vars/*
- Host facts / cached set_facts
- Play vars
- Play vars_prompt
- Play vars_files
- Role vars (defined in
role/vars/main.yml
) - Block vars (only for tasks in block)
- Task vars (only for the task)
- Include variables
- Role (and include_role) params
- Include params
- Extra vars (always win precedence)
Let's break this down with examples to better understand how this works in practice.
Basic Variable Precedence Examples
Example 1: Role Defaults vs. Inventory Variables
Role defaults are designed to be overridden, and they have one of the lowest precedences in the hierarchy.
# roles/webserver/defaults/main.yml
http_port: 80
max_clients: 200
If you define the same variable in your inventory:
# inventory/group_vars/webservers.yml
http_port: 8080
When you run a playbook that uses the webserver
role for hosts in the webservers
group, the http_port
value will be 8080
because inventory group variables have higher precedence than role defaults.
Example 2: Playbook Variables vs. Extra Variables
Let's say you have a playbook with a defined variable:
# deploy_app.yml
---
- hosts: webservers
vars:
app_version: 1.0.2
tasks:
- name: Display app version
debug:
msg: "Deploying application version {{ app_version }}"
If you run this playbook with an extra variable:
ansible-playbook deploy_app.yml -e "app_version=1.0.3"
The output would show:
TASK [Display app version] *******************************************
ok: [web01] => {
"msg": "Deploying application version 1.0.3"
}
The -e
command line extra variables will always override any other variable definitions because they have the highest precedence.
Visualizing Variable Precedence
To better understand the hierarchy, let's visualize it:
In this diagram, variables defined at the top will override variables defined at the bottom when they share the same name.
Real-World Applications
Let's explore some practical scenarios where understanding variable precedence is important.
Environment-Specific Configuration
One common use case is to have different configurations for different environments (development, staging, production).
# roles/app/defaults/main.yml (lowest precedence)
db_host: localhost
db_user: app
db_password: default_password
# group_vars/production.yml (higher precedence)
db_host: prod-db.example.com
db_password: "{{ vault_db_password }}" # Using Ansible Vault for sensitive data
# host_vars/specific-server.yml (even higher precedence)
db_host: dedicated-db.example.com
With this setup:
- Most servers use the defaults
- Production servers use the production database
- One specific server uses a dedicated database
Overriding Role Behavior
Another common pattern is to create flexible roles with sensible defaults that can be overridden as needed:
# roles/nginx/defaults/main.yml
nginx_worker_processes: auto
nginx_worker_connections: 1024
nginx_keepalive_timeout: 65
You can then override these values at different levels:
# group_vars/high_traffic_servers.yml
nginx_worker_processes: 8
nginx_worker_connections: 4096
And for special cases, use command-line overrides:
ansible-playbook deploy.yml -e "nginx_keepalive_timeout=30"
Practical Example: Complete Deployment Scenario
Let's walk through a complete example that demonstrates variable precedence in action.
Directory Structure
project/
├── ansible.cfg
├── inventory/
│ ├── hosts
│ ├── group_vars/
│ │ ├── all.yml
│ │ └── webservers.yml
│ └── host_vars/
│ └── web01.yml
├── roles/
│ └── webapp/
│ ├── defaults/
│ │ └── main.yml
│ └── vars/
│ └── main.yml
└── deploy.yml
File Contents
# roles/webapp/defaults/main.yml
app_port: 8000
app_workers: 4
app_debug: false
app_version: 1.0.0
# roles/webapp/vars/main.yml
app_service_name: webapp
app_config_path: /etc/webapp
# inventory/group_vars/all.yml
app_debug: true # Enable debug in development
# inventory/group_vars/webservers.yml
app_port: 8080
app_workers: 8
# inventory/host_vars/web01.yml
app_port: 9000 # Special port for this server
# deploy.yml
---
- hosts: webservers
vars:
app_version: 1.1.0
roles:
- webapp
tasks:
- name: Display application configuration
debug:
msg: |
Application Configuration:
- Port: {{ app_port }}
- Workers: {{ app_workers }}
- Debug: {{ app_debug }}
- Version: {{ app_version }}
- Service: {{ app_service_name }}
- Config Path: {{ app_config_path }}
Execution and Results
When you run the playbook for web01
:
ansible-playbook deploy.yml -l web01
The output would be:
TASK [Display application configuration] *******************************************
ok: [web01] => {
"msg": "Application Configuration:
- Port: 9000
- Workers: 8
- Debug: true
- Version: 1.1.0
- Service: webapp
- Config Path: /etc/webapp"
}
Let's analyze why each variable has this value:
app_port: 9000
- From host_vars/web01.yml (host-specific override)app_workers: 8
- From group_vars/webservers.yml (group override)app_debug: true
- From group_vars/all.yml (overrides role default)app_version: 1.1.0
- From the playbook vars (overrides role default)app_service_name: webapp
- From role/vars/main.yml (no overrides)app_config_path: /etc/webapp
- From role/vars/main.yml (no overrides)
If you run the same playbook with an extra variable:
ansible-playbook deploy.yml -l web01 -e "app_debug=false"
The app_debug
would be false
because extra vars have the highest precedence.
Using ansible_facts
and Registered Variables
Ansible facts and registered variables also fit into the precedence hierarchy. Facts gathered about hosts are available as variables and typically override role defaults but can be overridden by more specific variable definitions.
---
- hosts: webservers
tasks:
- name: Get application status
command: /usr/bin/app_status
register: app_status
changed_when: false
- name: Display status
debug:
msg: "Application status: {{ app_status.stdout }}"
- name: Use ansible_facts
debug:
msg: "OS: {{ ansible_facts['distribution'] }} {{ ansible_facts['distribution_version'] }}"
Registered variables like app_status
are available within the play but won't override extra vars if they exist with the same name.
Best Practices for Variable Management
To avoid confusion with variable precedence:
-
Be consistent with variable definitions: Try to define variables in the same place for similar purposes.
-
Use descriptive variable names: Prefix variables with the role or function name to avoid collisions.
-
Document your variables: Add comments to explain what variables are used for and where they might be overridden.
-
Use
vars_files
for complex variable structures: Keep your playbooks clean by storing complex variables in separate files. -
Debug variable values when needed:
yaml- name: Debug all variables
debug:
var: vars
verbosity: 2 -
Use the
vars
lookup for introspection:yaml- name: Check specific variable
debug:
msg: "{{ lookup('vars', 'app_port') }}"
Summary
Understanding Ansible's variable precedence is essential for creating flexible, maintainable automation. The key points to remember:
- Variables can be defined in multiple locations
- Later sources in the precedence hierarchy override earlier ones
- Extra vars (command line
-e
) always have the highest precedence - Role defaults have the lowest precedence and are meant to be overridden
- Host-specific variables override group variables
- Play variables override inventory variables
By strategically placing your variable definitions according to this hierarchy, you can create playbooks that are both flexible and predictable.
Additional Resources
Exercises
- Create a role with default variables and override them at different levels (group_vars, host_vars, play vars, and extra vars).
- Write a playbook that displays the effective value of variables defined at different precedence levels.
- Use
ansible -m debug -a "var=hostvars[inventory_hostname]"
to explore all variables defined for a host. - Create a playbook that uses conditional logic based on variables that might be defined at different precedence levels.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)