Ansible is an agentless automation tool that uses YAML-based playbooks to manage system configurations, deployments, and orchestration across remote systems.
One of its core features is the use of variables to customize tasks across different environments. These variables can be defined in multiple locations, each with its own level of precedence.
Understanding how Ansible resolves variable conflicts is essential for ensuring predictable, reliable automation behavior.
What we’ll cover in this article:
Variables in Ansible let you reuse values across tasks, making your workflows more flexible and easier to adapt to different environments. Instead of hardcoding things like app versions, hostnames, or file paths, you can assign them to variables. Using variables keeps your automation cleaner, safer, and easier to maintain.
These values can be set in inventory files, roles, or from the command line, simplifying configuration and management.
When the same variable is defined in multiple places, Ansible uses precedence rules to decide which one to use, ensuring consistent behavior.
You can scope variables globally, per host, or per group, giving you precise control.
They also work in conditionals and loops, helping reduce repetition. Tools like Ansible Vault, Azure Key Vault, or AWS Secrets Manager keep sensitive data like credentials or API keys secure.
Ansible variable precedence determines which variable value takes effect when the same variable is defined in multiple places.
Every time Ansible executes a task, it evaluates variables from different sources based on a specific hierarchy, choosing the value from the source with the highest priority. These sources include playbooks, roles, inventory files, or command-line parameters.
Without understanding the defined hierarchy, variables may unintentionally override each other, leading to configuration issues or deployment failures.
For example, defining a variable in both a role default and at the command line will result in Ansible using the command-line value, since it ranks higher in precedence.
Properly managing variable precedence ensures clarity and reduces the risk of mistakes. It also gives you more control over your automation and simplifies playbook maintenance in complex infrastructure environments.
In the next section, we will dive into all the levels in Ansible precedence in priority order.
In Ansible, variable precedence determines which variable value is used when multiple sources define the same variable. Higher precedence values always override lower ones when conflicts occur.
The effective order (from highest to lowest precedence) is:
- Extra vars (always highest)
- Task vars (including loop and include vars)
- Block vars
- Play vars_prompt and vars_files
- Play vars
- Facts (gathered or set)
- Playbook group_vars and host_vars
- Inventory group_vars and host_vars
- Inventory file variables
- Role defaults
- Command line values
In this section, we will go over the order of precedence that is used in Ansible variables. We will start with the top priority source that will override any other source that is in play during your playbook run.
For each source, we will provide an example to demonstrate further:
1. Extra variables (command-line parameter)
Ansible variables specified within the --extra-vars (or -e)
parameter have the highest precedence and override any other variables that may be defined elsewhere.
For example, in the following playbook, we specified a Tomcat version variable, and we can pass in the value from the command line. No matter where else we may have specified the value for this variable, the value from the command line -e
flag will override any other value.
Playbook (deploy_tomcat.yml):
---
- hosts: webservers
tasks:
- name: Install Tomcat from extra vars
yum:
name: "tomcat-{{ tomcat_version }}"
Command (using -e
flag):
ansible-playbook deploy_tomcat.yml -e "tomcat_version=10.1.5"
2. Task parameters
Within each Ansible task, we can use the include_tasks
directive to include tasks from another YAML file. In that YAML file, you can have tasks that use variables. The value for those variables needs to be passed in from the playbook level. This value has a higher precedence compared to other variable values.
In this example, we are installing the Tomcat application, but it is a part of a separate task file. We are including that within our main playbook and passing in the value for the variable from the playbook level.
Playbook (deploy_tomcat.yml):
---
- hosts: webservers
tasks:
- name: Include Tomcat installation tasks
include_tasks: install_tomcat.yml
vars:
tomcat_version: "10.1.4"
Included task install_tomcat.yml:
- name: Install Tomcat from include params
yum:
name: "tomcat-{{ tomcat_version }}"
3. Role (and include_role) params
There are different ways to specify variable values in Ansible roles. Adding the values of the variables used within a role on the playbook level has a higher precedence and will override the variable value specified in the vars files within the role.
Example 1 – Standard role
Using a standard role to install a Tomcat application by passing in the value for the tomcat_version
variable from the role parameter on the playbook level:
Playbook (deploy_tomcat.yml):
---
- hosts: webservers
roles:
- role: tomcat_role
tomcat_version: "10.2.1"
Role (/roles/tomcat_role/tasks/main.yml):
- name: Install specific Tomcat version
yum:
name: "tomcat-{{ tomcat_version }}"
Example 2 – Using include_role
Another common practice in using roles is using the include_role
directive. This is similar to using standard roles, but it is used under a regular Ansible task, and you include the role within that task.
We have a Tomcat configuration file that uses a variable for max memory. We can pass this in from the playbook level, and it will override any variable value specified within the role itself.
Playbook (deploy_tomcat.yml):
---
- hosts: webservers
tasks:
- name: Include Tomcat role with custom parameters
include_role:
name: tomcat_role
vars:
tomcat_version: "10.2.2"
tomcat_max_memory: "2048m"
Role tasks (/roles/tomcat_role/tasks/main.yml):
- name: Install Tomcat version
yum:
name: "tomcat-{{ tomcat_version }}"
- name: Set JVM max memory
lineinfile:
path: /etc/tomcat/tomcat.conf
regexp: '^JAVA_OPTS='
line: 'JAVA_OPTS="-Xmx{{ tomcat_max_memory }}"'
notify: restart tomcat
4. set_fact/registered vars
The set_fact
module allows you to set variable values as a separate task in your playbook. Another similar approach is to store the output of a playbook task into a variable within your tasks using the register parameter. Both of these approaches have a higher precedence than other variable values that are being passed in.
In this example, we store the value of the variable for tomcat_version
as a fact and pass that value into our Tomcat install task.
---
- hosts: webservers
tasks:
- name: Set tomcat_version fact
set_fact:
tomcat_version: "10.1.2"
- name: Install Tomcat from set_fact
yum:
name: "tomcat-{{ tomcat_version }}"
In this example, we grab the command’s output and store it in a variable that is passed into another task in the playbook:
- hosts: webservers
tasks:
- name: Get installed Tomcat version
shell: "rpm -q tomcat"
register: tomcat_check
- name: Print Tomcat version
debug:
msg: "Tomcat package installed: {{ tomcat_check.stdout }}"
- name: Skip install if Tomcat is already present
yum:
name: "tomcat"
state: present
when: "'not installed' in tomcat_check.stdout"
5. Incude_vars
In Ansible, you can create a variable YAML file that contains a list of all the variables and their values. You can use the include_vars
directive to pass the values from the variable file on to your playbook tasks.
In this example, we have a variable file (vars.yml) containing the variable value for the Tomcat version, which we are passing to the playbook using the include_vars
directive.
vars.yml:
tomcat_version: "10.1.2"
Playbook (tomcat_deploy.yml):
---
- hosts: webservers
tasks:
- name: Include Tomcat version variables
include_vars: tomcat_version.yml
- name: Install Tomcat from included vars
yum:
name: "tomcat-{{ tomcat_version }}"
6. Task vars
This is a standard approach: specify your variables only within the task itself instead of passing them in from a separate file or command line.
Let’s install a Tomcat application by specifying the Tomcat version variable value right under the task.
Playbook (tomcat_deploy.yml):
---
- hosts: webservers
tasks:
- name: Install Tomcat from task vars
yum:
name: "tomcat-{{ tomcat_version }}"
vars:
tomcat_version: "10.1.2"
7. Block vars
Ansible offers the option to specify blocks of tasks to which you want specific variables to apply.
We can use the - block
to list the tasks to install the Tomcat application and create a list of variables right under it.
Playbook (tomcat_deploy.yml):
---
- hosts: webservers
tasks:
- block:
- name: Install Tomcat from block vars
yum:
name: "tomcat-{{ tomcat_version }}"
vars:
tomcat_version: "10.0.9"
8. Role vars
A common practice when using roles in Ansible is to list all of the variable values under the vars directory located in (/roles/your_role/vars/main.yml). This gets passed into any variables you have in your Ansible role.
In this example, we have variables located in /roles/tomcat_role/vars/main.yml that specify the version of Tomcat we want to deploy. The variable will be passed into the tomcat_role
that is using it.
The role itself downloads the Tomcat archive, but it needs the specific version to run.
Role (roles/tomcat_role/vars/main.yml):
tomcat_version: "10.0.8"
Playbook (deploy_tomcat.yml):
---
- hosts: webservers
roles:
- tomcat_role
Role (/roles/tomcat_role/tasks/ main.yml):
- name: Download Tomcat archive
get_url:
url: "https://archive.apache.org/dist/tomcat/tomcat-{{ tomcat_version.split('.')[0] }}/v{{ tomcat_version }}/bin/apache-tomcat-{{ tomcat_version }}.tar.gz"
dest: "/tmp/apache-tomcat-{{ tomcat_version }}.tar.gz"
become: true
9. Play vars_files
vars_file
allows you to import variables from an external YAML file into your playbook. This approach helps you separate the variables from your Ansible tasks, making the playbooks cleaner and easier to manage.
In the following example, we are importing the variable for tomcat_version from the tomcatvars YAML file into our playbook by using the vars_files
parameter.
Playbook (deploy_tomcat.yml):
---
- hosts: webservers
vars_files:
- /tomcatvars.yml
tasks:
- name: Install specific Tomcat version
yum:
name: "tomcat-{{ tomcat_version }}"
tomcatvars.yml:
tomcat_version: "10.1.2"
10. Play vars_prompt
vars_prompt
is used to get interactively prompted during playbook execution for the variable. This has a lower precedence than some of the other mechanisms for using variables, but it can be useful in certain scenarios where you want to pass in sensitive values or environment-specific values at runtime without hard-coding them into your playbook or files.
In this playbook, we are using the vars_prompt directive to prompt the admin for the variable input during playbook execution. You can customize the prompt and enter an input value.
Playbook (deploy_tomcat.yml):
---
- hosts: webservers
vars_prompt:
- name: "tomcat_version"
prompt: "Enter the Tomcat version "
private: no
tasks:
- name: Install the chosen Tomcat version
yum:
name: "tomcat-{{ tomcat_version }}"
Playbook execution:
ansible-playbook deploy_tomcat.yml
Output:
Enter the Tomcat version: 10.1.2
11. Play vars
The vars block in playbooks allows you to specify variables on the main playbook level, allowing the variables to pass into all of your tasks. This has a lower precedence compared to the other playbook vars methods.
We are declaring the variable value at the playbook level. Therefore, any task in this playbook that uses that variable will receive the variable value.
Playbook (deploy_tomcat.yml):
---
- hosts: webservers
vars:
tomcat_version: "10.1.2"
tasks:
- name: Install Tomcat
yum:
name: "tomcat-{{ tomcat_version }}"
12. Host facts (Ansible facts)
Host facts are variables that Ansible gathers from each host during execution. Data includes OS type/version, hostname, IP addresses, etc. However, the Ansible facts tend to have a medium precedence in the chain of variables.
Here, we are using some of the automatically gathered fact variables Ansible provides, such as ansible_distribution
, ansible_distribution_version
, and ansible_os_family
. This will provide us with host data that we can use within our playbook conditionals to execute the install task based on that information.
Playbook (deploy_tomcat.yml):
---
- hosts: webservers
tasks:
- name: Display OS of Host
debug:
msg: "This server is running {{ ansible_distribution }} {{ ansible_distribution_version }}"
- name: Install Tomcat based on OS family
yum:
name: "{{ 'tomcat' if ansible_os_family == 'RedHat' else 'tomcat9' }}"
state: present
13. Playbook host_vars
Playbook host_vars
are variables that are specific to the host you run your playbook on, as you
have a separate file that is dedicated to each host in your inventory file. For playbook host_vars
, the host_vars
directory lives within the project level that contains the playbook (works differently when using inventory host_vars
).
This approach focuses more on declaring variables on an inventory level, which has a higher precedence compared to the other inventory variable methods we will cover next.
In this example, we have two hosts that require different versions of Tomcat installed. We will create an inventory file containing those two hosts, and in that same directory, we will create a new directory for host_vars
.
This will include a YAML file for each of our hosts and store any specific variables they may require during playbook execution. This approach is useful if we want to have fine-grained control over the configuration of our hosts.
Directory structure:
project/
├── deploy_tomcat.yml
├── inventory.ini
└── host_vars/
├── app1.yml
└── app2.yml
myhosts.ini:
[webservers]
app1
app2
app1.yml:
tomcat_version: "10.1.2"
app2.yml:
tomcat_version: "10.1.1"
Playbook deploy_tomcat.yml:
---
- hosts: webservers
tasks:
- name: Install host-specific Tomcat version
yum:
name: "tomcat-{{ tomcat_version }}"
state: present
14. Inventory host_vars
Inventory host_vars
are slightly different than playbook host_vars
. They require you to define the variables within a host_vars
file in a separate inventory directory within your project, which includes the separate YAML files for each host’s variable values.
This example is similar to the previous example using playbook host_vars, as the key difference is the directory under which the host_vars are located.
Directory structure (host_vars
directory is within the inventory directory):
inventory/
├── myhosts.ini
├── host_vars/
│ ├── app1.yml
│ └── app2.yml
deploy_tomcat.yml
myhosts.ini:
[webservers]
app1
app2
app1.yml:
tomcat_version: "10.1.2"
app2.yml:
tomcat_version: "10.1.1"
Playbook deploy_tomcat.yml:
---
- hosts: webservers
tasks:
- name: Install host-specific Tomcat version
yum:
name: "tomcat-{{ tomcat_version }}"
state: present
15. Inventory file or script host_vars
This approach allows you to declare variables in-line for each host in the inventory file. You can do this either in the INI file or the YAML-formatted file without using the host_vars
file. This is a very common practice when hard-coding variable values for each host in the inventory file.
Below, we will store the variable name and value in the inventory file. Our playbook will now be able to utilize the variable values to install the Tomcat application.
Directory structure:
project/
├── deploy_tomcat.yml
└── inventory.ini
Inventory file (inventory.ini):
[webservers]
web1 ansible_host=192.168.1.10 tomcat_version=10.1.1
web2 ansible_host=192.168.1.11 tomcat_version=10.1.2
Playbook deploy_tomcat.yml:
---
- name: Install Tomcat using inventory file host vars
hosts: webservers
gather_facts: no
tasks:
- name: Install the correct Tomcat version per host
yum:
name: "tomcat-{{ tomcat_version }}"
state: present
- name: Confirm installation
debug:
msg: "Tomcat {{ tomcat_version }} installed on {{ inventory_hostname }}"
16. Playbook group_vars
This method allows you to define group level variables in Ansible. group_vars
is a directory where you define variables for host groups.
For this group_vars to be considered a playbook group_vars
, the directory has to be on the same level as your playbook file, and the directory will include a YAML file with all of your variable names and values. Once you trigger your playbook, the specified variables retrieve their appropriate values from the file located in group_vars.
In this example, we are adding a group_vars
directory under the project we are working with that includes the playbook. Within that group_vars
directory, we will include the variables associated with our playbook.
Directory structure:
project/
├── deploy_tomcat.yml
├── group_vars/
│ └── app-servers.yml
└── inventory.ini
Inventory file:
[webservers]
web1
web2
app-servers.yml:
tomcat_version: "10.1.0"
tomcat_port: 8080
Playbook deploy_tomcat.yml:
---
- name: Deploy Tomcat using playbook group_vars
hosts: webservers
gather_facts: no
tasks:
- name: Install Tomcat version from group_vars
yum:
name: "tomcat-{{ tomcat_version }}"
state: present
- name: Display Tomcat configuration
debug:
msg: "Tomcat {{ tomcat_version }} will run on port {{ tomcat_port }}"
17. Inventory group_vars
Inventory group_vars
works the same way as playbook groups_vars, but this method requires the group_vars
directory to be under the inventory directory (the same concept as using playbook and inventory host_vars
) instead of the same level as the playbook. This has a lower precedence than playbook group_vars
.
Below, we will install Tomcat and display the admin user of the Tomcat application. We will retrieve the variables listed in the group_vars
directory under the inventory directory.
Directory structure:
project/
├── deploy_tomcat.yml
├── inventory/
│ ├── inventory_hosts.ini
│ └── group_vars/
│ └── app-servers.yml
inventory_hosts.ini file:
[webservers]
web1
web2
app-servers.yml:
tomcat_version: "10.1.1"
tomcat_user: "tomcat_user"
Playbook deploy_tomcat.yml:
---
- name: Install Tomcat using inventory group_vars
hosts: webservers
gather_facts: no
tasks:
- name: Install Tomcat
yum:
name: "tomcat-{{ tomcat_version }}"
state: present
- name: Display Tomcat Admin
debug:
msg: "Tomcat is managed by user {{ tomcat_user }}"
18. Playbook group_vars/all
Playbook group_vars/all
follows the same concept as playbook group_vars
with the directory of group_vars
being on the same level as the playbook file but this method uses a special file called all.yml, this is a reserved file in Ansible and applies all the variables listed in this file to all of the hosts in your inventory file.
Directory structure:
app/
├── deploy_tomcat.yml
├── group_vars/
│ └── all.yml
└── inventory.ini
19. Inventory group_vars/all
Inventory group_vars/all
follows the same concept as inventory group_vars
, with the group_vars directory under the inventory directory. It also uses the special Ansible file called all.yml and applies the variables listed here to all of the hosts in the inventory file.
Directory structure:
app/
├── deploy_tomcat.yml
├── inventory/
│ ├── hosts.ini
│ └── group_vars/
│ └── all.yml
20. Inventory file or script group vars
Previously, we covered using the inventory file with the script host_vars, which passed in variables to each specific host. However, the script group_vars
method with the inventory file applies the variables to all of the hosts. Therefore, group_vars
falls lower in precedence compared to host_vars
.
Let’s use group_vars
to pass in Tomcat variables into our playbook run; this will apply the variables to all of the hosts listed in our inventory file.
Directory structure:
project/
├── deploy_tomcat.yml
└── inventory.ini
inventory.ini file:
[webservers]
web1
web2
[webservers:vars]
tomcat_port=8080
tomcat_user=webgroup_admin
Playbook deploy_tomcat.yml:
---
- hosts: webservers
gather_facts: no
tasks:
- name: Show group-level Tomcat vars
debug:
msg: "Tomcat runs on port {{ tomcat_port }} as {{ tomcat_user }}"
21. Role defaults
One of the lowest precedence variables is role defaults. Under every Ansible role, there is a defaults directory (/roles/your_role/defaults/main.yml) that contains default variable values that a playbook execution can always fallback to in case no variable value is provided to the run.
Directory structure:
project/
├── deploy_tomcat.yml
└── roles/
└── tomcat/
├── defaults/
│ └── main.yml
└── tasks/
└── main.yml
Default variables (/roles/your_role/defaults/main.yml):
tomcat_version: "9.0.72"
tomcat_port: 8080
Role tasks (/roles/your_role/tasks/main.yml):
- name: Install Tomcat
yum:
name: "tomcat-{{ tomcat_version }}"
state: present
- name: Configure Tomcat port
template:
src: server.xml.j2
dest: "/etc/tomcat/server.xml"
Playbook (deploy_tomcat.yml):
---
- name: Deploy Tomcat using role defaults
hosts: webservers
roles:
- role: tomcat
22. Command line values
Command line values are not variables; they are the parameter values passed in via the command line for the ansible-playbook cmdlet
. These can include parameters such as -u
, which is used to pass in a username, or -k
, which prompts the admin to enter an SSH password.
If you need to pass in variables through the command line, you can use --extra-vars
(which has the highest precedence in Ansible variables), but the regular parameters of the ansible-playbook
command have the lowest precedence.
In the example below, we are running the ansible-playbook command against a playbook file with the standard command-line parameters.
ansible-playbook deploy_tomcat.yml -i inventory.ini -u ansibleadmin --private-key ~/.ssh/id_rsa --become
This is the complete list of the order of precedence with Ansible variables. Now we will cover a couple of practical use cases to demonstrate the importance of the order of precedence in your workflows.
The use cases below involve Ansible variables where understanding precedence is key, since multiple files may define the same variable.
Scenario 1: Deploy Tomcat across different environments
You usually deploy applications through Dev, Staging, and Prod to avoid production disruptions. To control versions, you can define default variables across environments and use Ansible’s precedence rules to manage updates.
In this example, we host the app on Tomcat and first deploy the latest version to Dev using higher-precedence variables. Other environments retain a default version set during each playbook run.
Ansible Roles handle the deployment. The default Tomcat version in /roles/tomcat/defaults/main.yml ensures consistency, unless overridden per environment. Currently, only Production has this version set.
tomcat_version: "10.0.0"
For Dev and Staging, we’ll assume they’ve already been upgraded to a newer Tomcat version that is not yet in production. We’ll set this version in their group_vars
, which takes precedence over Role defaults and overrides them.
group_vars/dev.yml, this is a separate Dev YAML file dedicated to Dev environment variables only. We will specify the new version in this file:
tomcat_version: "10.0.1"
group_vars/staging.yml, the same applies for the Staging environment, as we will specify a new version for Staging as well:
tomcat_version: "10.0.1"
We plan to deploy version 10.0.1 to Production, but a newer Tomcat version (10.0.2) was just released, which we want to test in Dev.
To do this, we’ll override Dev’s current group_vars
using a higher-precedence Ansible variable. In this case, we’ll use --extra-vars
to ensure Dev gets the specific version for testing.
ansible-playbook deploy_tomcat.yml —-extra-vars "tomcat_version=10.0.2"
This way, we ensure the Production version never gets accidentally modified during playbook execution, we properly test the latest versions, and we roll them out carefully through each environment in a clean manner by going through Dev, Staging, and Production.
Scenario 2: Provision Linux users across different environments
Organizations often use separate environments like Dev, Staging, and Production to manage infrastructure and access according to security and operational requirements. Ansible’s variable precedence helps provision users with environment-specific SSH keys, sudo access, and permissions in a clean and flexible way.
In this example, the user_mgmt
role defines default user settings in defaults/main.yml, creating a limited-access fallback account only if no higher-precedence variables are provided.
Default variable values (/roles/user_mgmt/defaults/main.yml):
users:
- name: "defaultuser"
shell: "/bin/bash"
state: "present"
sudo: false
Now we’ll configure environment-specific settings. Each environment, like Dev or Production, will have different users and keys. We’ll define variables in group_vars
to override defaults and control each environment separately.
group_vars/dev.yml:
users:
- name: "devadmin"
shell: "/bin/bash"
sudo: true
ssh_key: "ssh-rsa AAAAB3SDJNfSMCgdfds3432"
- name: "devread"
shell: "/bin/sh"
sudo: false
ssh_key: "ssh-rsa AAAAB3DSGLGJOM2403fdgw3"
group_vars/production.yml:
users:
- name: "prodadmin"
shell: "/bin/bash"
sudo: true
ssh_key: "ssh-rsa AAAAB3gfd4SDGJHIOjhggdfg3"
We’ve separated Dev and Production variables under a shared role. Dev has both read-only and admin users, while Production has only a privileged admin. Playbooks now load the correct user settings from group_vars
, overriding role defaults.
If a consultant needs temporary Dev access, instead of editing group_vars
, we can use --extra-vars
during the playbook run. This method has the highest precedence in Ansible and lets us override the user list just for that execution.
ansible-playbook deploy_users.yml -i inventory/hosts.ini --limit dev -e 'users=[{"name":"tempadmin","sudo":true,"ssh_key":"ssh-rsa AAAAdfdKJGIWgfSdsdfdg"}]'
Using --extra-vars
ensures the tempadmin
user is created on Dev servers without changing files or affecting other environments. This method overrides group_vars
, enabling safe access testing and permission management without impacting Production.
Scenario 3: Controlling CI/CD pipeline behavior using variable precedence
In a typical CI/CD pipeline, stages like Build, Test, and Deploy guide the application through a structured release process. Each serves a distinct role — Build compiles code, Test runs quality checks, and Deploy pushes changes live.
To standardize behavior across these stages, we define default variable values in the playbook. We then adjust behavior dynamically using Ansible’s variable precedence. This allows the playbook to react contextually. For instance, enabling dry-run mode in Build, verbose logging in Test, and database migrations in Deploy.
Instead of managing separate playbooks or complex conditionals, we can override defaults with higher-precedence inputs, like --extra-vars
, during pipeline execution. In this setup, we’ll use a role to define defaults, ensuring consistent behavior unless explicitly overridden.
deploy_app.yml playbook
---
- name: Deploy application
hosts: app_servers
become: true
gather_facts: false
vars:
ansible_check_mode: "{{ dry_run | default(false) }}"
roles:
- deploy
Deploy role (roles/deploy/defaults/main.yml):
- name: Show current stage behavior
debug:
msg: >
Running with dry_run={{ dry_run }},
verbose_logging={{ verbose_logging }},
run_migrations={{ run_migrations }}
- name: Set log level based on verbosity flag
lineinfile:
path: /opt/myapp/config.properties
regexp: '^log.level='
line: "log.level={{ 'DEBUG' if verbose_logging else 'INFO' }}"
when: not dry_run
- name: Deploy application package
shell: "echo 'Deploying app...'"
when: not dry_run
- name: Run database migrations
shell: "echo 'Running DB migrations...'"
when: run_migrations and not dry_run
Default variable values (/roles/deploy/defaults/main.yml):
dry_run: false
verbose_logging: false
run_migrations: false
In our pipeline, each CI/CD stage can selectively override specific variables by using the --extra-vars
parameter. This allows us to provide only the values needed for a given stage, keeping configurations concise and focused.
For instance, during the Build stage, we typically want to avoid making any real changes. To achieve this, we override the dry_run variable via --extra-vars
, enabling Ansible’s check mode. This mode executes all tasks without applying changes, allowing us to verify the playbook’s behavior safely.
ansible-playbook deploy_app.yml -i inventory/hosts.ini --extra-vars "dry_run=true"
In the Test stage, we want to allow the playbook to run normally, but we want to capture additional logs during the testing process.
For that, we will use --extra-vars
again, but this time we will override the verbose_logging
variable while ensuring the other defaults remain unchanged:
ansible-playbook deploy_app.yml -i inventory/hosts.ini --extra-vars "verbose_logging=true"
Lastly, in the Deploy stage, we want to apply the changes fully and run database migrations. To do this, we only need to set the run_migrations
variable to true.
This will trigger the database migration block in the playbook while allowing the other default behaviors to continue as expected:
ansible-playbook deploy_app.yml -i inventory/hosts.ini --extra-vars "run_migrations=true"
With this method, we can enable the use of a single playbook across all CI/CD stages, with behavior that adapts dynamically to the execution context. By taking advantage of Ansible’s variable precedence and using --extra-vars
to override values as needed, we eliminate code duplication and reduce the overhead of managing multiple playbooks.
When working with Ansible, it’s easy to lose track of where variables are defined and how they’re used across playbooks, roles, and other components. That’s why following a standardized approach and best practices for managing variables is crucial, which we’ll explore next.
If you’ve already started using variables throughout your Ansible setup, you may encounter issues where a variable value is not what you expected. This often happens when variables are defined in multiple places, making it tricky to trace their source.
Take a simple example: deploying a Tomcat application with a variable like app_version
. You might define a default version (e.g., 1.0.0
) in /roles/app/defaults/main.yml, but during a playbook run, a different value gets passed, leaving you wondering where it came from.
app_version: "1.0.0"
We also have a group variable set for app_version
in the environments inventory group set to another version in /inventory/group_vars/prod.yml
app_version: "1.2.0"
We also have a host variable version set to a different version in /inventory/host_vars/app01.yml.
app_version: "1.3.0"
Lastly, we are passing in version 2.0.0 from the command line with the --extra-vars
parameter.
ansible-playbook deploy_app.yml -i inventory/hosts.ini --limit app01 --extra-vars "app_version=2.0.0"
For this example, we only wanted the command run to set this version to Dev and not Production. However, we realized Production also received this version, and now we need to figure out where the issue is.
We have a few methods we can follow to troubleshoot and pinpoint the issue. We want to be able to pull up all the different Ansible precedents that are in play for this particular job. In order to do this, we can do the following:
1. Run the playbook with verbose logging
Enabling verbose on your Ansible playbook run allows you to see what variables are being loaded and their values during runtime, preventing you from going through each file and trying to find out where the variable might be passing in from.
There are different levels when using verbose logging:
Verbose level 1 (-v)
This will show you task-level outputs, which include changes, skipped tasks, and command results.
ansible-playbook deploy_app.yml -i inventory/hosts.ini --limit app01 --extra-vars "app_version=2.0.0" -v
You would receive a similar output as the following when applying a single -v
flag with the ansible-playbook
command:
TASK [deploy : debug current app_version] **************************************
ok: [app01] => (item=None) => {
"msg": "Deploying application version: 2.0.0"
}
TASK [deploy : Set log level based on verbosity flag] **************************
changed: [app01]
As you can see, the version being displayed is the value we passed in using –extra-vars (2.0.0), which had the highest precedence.
Verbose level 2-3 (-vv or -vvv)
This will display all loaded variables, lookup values, evaluated conditions, included files, and variable overrides in playbooks and roles. We want to focus on this most of the time when troubleshooting variables.
ansible-playbook deploy_app.yml -i inventory/hosts.ini --limit app01 --extra-vars "app_version=2.0.0" -vvv
Here is a sample output of running this command with the -vvv
flag, which would display the location where the variable values are coming from and also give you the set value for the variable, see below:
Loaded vars from: roles/deploy/defaults/main.yml
app_version = 1.0.0
Loaded vars from: inventory/group_vars/prod.yml
app_version = 1.2.0
Loaded vars from: inventory/host_vars/app01.yml
app_version = 1.3.0
Loaded extra vars (command line):
app_version = 2.0.0
The other levels, or verbose logging, don’t display much of the variable values and location; instead, it gives you more logging details about connection-level debugging and SSH outputs, which is useful when debugging connectivity or authentication problems.
2. Create a debug task directly in your playbook
In Ansible, we also have the option to use the debug module to display the variable values during your playbook run and see the value in the output. You can structure your playbook task as follows:
- name: Show current application version
debug:
msg: "Deploying version: {{ app_version }}"
And receive an output as this:
TASK [Show current application version] ****************************************
ok: [app01] => {
"msg": "Deploying version: 2.0.0"
}
This can also be used to display multiple variable values during your playbook run as follows:
- name: Print all deployment flags
debug:
msg: |
dry_run = {{ dry_run }}
verbose_logging = {{ verbose_logging }}
run_migrations = {{ run_migrations }}
Giving you the following output:
TASK [Print all deployment flags] **********************************************
ok: [app01] => {
"msg": "dry_run = False\nverbose_logging = True\nrun_migrations = False"
}
3. Variable isolation by testing without overrides:
In order to fully isolate the issue, you can exclude the --extra-vars
parameter during your playbook run to focus more on the other variable precedence in your workflow and find out what values might be getting loaded in from.
This removes the highest precedence method from being used and allows you to work directly with the other methods you have in use, as the following example:
ansible-playbook deploy_app.yml -i inventory/hosts.ini -v
Once you have eliminated the --extra-vars
method, you can begin by reintroducing group_vars one at a time and start testing with host_vars being commented out. This gives you the ability to eliminate the other variable methods incrementally, one by one, to see where the issue lies.
However, if there are multiple sources bringing forth the same value, then you should look into cleaning up your variable strategy. It is very important to have a clean variable strategy so you can avoid any variable conflicts and unexpected deployments.
In this section, we will go over a couple of best practices you can follow when using Ansible variables across your playbooks and roles.
- Use role defaults for fallbacks – Set safe default variable values in roles (e.g., roles/user_mgmt/defaults/main.yml) to prevent missing-var failures; defaults carry the lowest precedence and can always be overridden
- Define each variable in one place – Declare each variable only once (e.g., app_version) to avoid shadowing across
group_vars
,host_vars
,play_vars
, orextra_vars
, and keep a single source of truth
- Keep environment-specific variables in group_vars – Store Dev, Staging, and Prod values in their respective files (group_vars/dev.yml, etc.) so environment overrides are isolated and transparent
- Use debug tasks to identify variable values – Add debug tasks such as
msg: "Deploying version: {{ app_version }}"
to confirm the value loaded at runtime and quickly spot precedence issues
- Avoid overusing
--extra-vars
in production deployments – Reserve extra_vars for testing; in production, they can inadvertently override higher-priority variables and cause drift
- Create a variable layering model – Follow a clear hierarchy: role defaults as the base,
group_vars
for environment overrides,host_vars
only when necessary, andextra_vars
for ad-hoc overrides to avoid surprises during playbook runs
Read more: 50 Ansible Best Practices to Follow [Tips & Tricks]
Spacelift’s vibrant ecosystem and excellent GitOps flow are helpful for managing and orchestrating Ansible. By introducing Spacelift on top of Ansible, you can easily create custom workflows based on pull requests and apply any necessary compliance checks for your organization.
Another advantage of using Spacelift is that you can manage infrastructure tools like Ansible, Terraform, Pulumi, AWS CloudFormation, and even Kubernetes from the same place and combine their stacks with building workflows across tools.
Our latest Ansible enhancements solve three of the biggest challenges engineers face when they are using Ansible:
- Having a centralized place in which you can run your playbooks
- Combining IaC with configuration management to create a single workflow
- Getting insights into what ran and where
Provisioning, configuring, governing, and even orchestrating your containers can be performed with a single workflow, separating the elements into smaller chunks to identify issues more easily.
Would you like to see this in action, or just get a tl;dr? Check out this video showing you Spacelift’s Ansible functionality:
If you want to learn more about using Spacelift with Ansible, check our documentation, read our Ansible guide, or book a demo with one of our engineers.
Ansible variable precedence is essential for maintaining clean, scalable, and reliable automation workflows. It defines how and where variables can be overridden, allowing for environment-specific settings, ad-hoc changes, and controlled defaults.
Understanding this hierarchy helps avoid conflicts, supports flexible deployments, and simplifies troubleshooting. Best practices include isolating environment values in group_vars
, using defaults in roles, and limiting --extra-vars
use in production. Mastering precedence ensures consistent, adaptable, and maintainable Ansible operations.
Manage Ansible better with Spacelift
Managing large-scale playbook execution is hard. Spacelift enables you to automate Ansible playbook execution with visibility and control over resources, and seamlessly link provisioning and configuration workflows.