Ansible runs from your workstation, connects to remote machines over SSH, and performs tasks automatically based on the Ansible files you write. You can use it to run the same tasks on multiple machines instead of doing them one by one.
With Ansible variables, you can adjust values across different systems without having to rewrite playbooks, inventory files, or roles.
Find out how to use Ansible variables to bundle repetitive tasks and keep your automation easy to maintain.

What Are Ansible Variables?
Ansible variables replace fixed values in playbooks, roles, and inventory, allowing you to reuse the same automation logic across different systems or environments.
Variable names in Ansible must start with a letter or underscore and cannot contain spaces. Numbers are allowed.
Note: If you haven't installed Ansible yet, see our guides on installing it on Ubuntu, Windows, and Rocky Linux.
Even though there is no hard rule, it is best practice to use snake_case to keep the variable names consistent and easy to read throughout your automation:
some_variable
Variable values are usually defined in files outside playbooks, such as inventory, group_vars/, and host_vars/. After defining a variable, you can reference it anywhere in Ansible using double curly braces:
{{ some_variable }}
For example, to update packages on servers running different Linux distributions, you do not need to write separate playbooks for each operating system. Instead, just reference the defined variable for the package name in the task:
name: "{{ package_name }}"
Ansible resolves the variable at runtime using the correct value for each system.
How Do Ansible Variables Work?
When you run a playbook:
1. Ansible reads the inventory file and builds a list of hosts that match the hosts: definition in the playbook.
2. It connects to the first host over SSH using the connection details from the inventory, such as the remote machine's hostname or IP address, user, SSH key, and port number.
3. All the variables that apply to that host from the inventory, group_vars/, host_vars/, playbook variables, role defaults, and any runtime overrides are gathered.
4. Ansible determines the final value of some_variable for that host. If the variable is defined in more than one place, Ansible applies precedence rules to decide which value to use.

5. It replaces {{ some_variable }} with the resolved value for that host.
6. The task is executed using the substituted value, and Ansible records whether anything changed and if the task succeeded.
7. Ansible moves on to the next host and repeats the same lookup and substitution process using the same playbook.
Note: To better understand Ansible, learn about the core concepts behind Infrastructure as Code (IaC).
Types of Ansible Variables
The variable types below are grouped by how often they are used in Ansible projects, starting with those beginners encounter first and moving on to more advanced runtime features.
Commonly Used Ansible Variable Types
Most Ansible projects rely on the following variable types to define system configuration and behavior across hosts.
| Variable Type | Description | Defined In | Precedence (lower number=higher precedence) |
|---|---|---|---|
| Group variables | Use group variables to define values that apply to a group of hosts, for example, all web servers or database servers. | group_vars/ or inventory file. | 8 |
| Host variables | Used to define IP addresses, port numbers, environment settings, and other values specific to a single host. | host_vars/ or inventory file | 7 |
| Role default variables | These variables control how a role runs, like which port to use or whether to enable SSL. The defaults apply wherever a role is used and can be easily overridden. | roles/<role_name>/defaults/main.yml | 11 |
| Playbook variables | Playbook variables define global values, such as ports or package names, within a playbook or an individual play. They are applied when a play runs. | vars: section in a playbook | 5 |
| Task variables | Task variables are used to make temporary changes for a single task without affecting the rest of the play. | vars: section in a task | 3 |
| Fact variables | Ansible automatically collects system information (OS, IP address, memory, CPU, etc.) from the host it connects to. You can use these system facts to guide Ansible's behaviour. | Gathered by Ansible | 10 |
| Inventory variables | Inventory variables are defined directly in the inventory file for each host or host group. They are useful in small setups but are often replaced by group_vars/ and host_vars/ as projects grow. | Inventory file | 9 |
| Set fact variables | set_fact allows you to create or modify variables dynamically while a play is running. Usually, they are applied conditionally based on the results of earlier tasks. | set_fact task | 4 |
| Role-specific variables | Role-specific variables define how a role runs and are not intended to be easily overridden. They apply wherever the role is used. | roles/<role_name>/vars/main.yml | 6 |
Advanced Ansible Variable Types
These variable types are situational. You are likely to run into them when working with advanced playbook logic and runtime overrides.
| Variable Type | Description | Defined In | Precedence (lower number=higher precedence) |
|---|---|---|---|
| Extra variables | To pass extra variables via the command line (CLI) when running a playbook, use the -e flag. These variables override most other variable values for that run. | CLI | 1 |
| Prompted variables | You can use variables to prompt the user for input at runtime, such as a password or confirmation, before continuing. | vars_prompt section in a playbook | 2 |
| Magic variables | Magic variables are provided by Ansible by default. Use them to access host names, inventory data, group membership, and other information about the systems Ansible manages. | Provided by Ansible | Not resolved via precedence |
| Environment variables | These variables are used in Ansible playbooks to access environment variables on the machine running Ansible or on the remote host. | Environment | Not resolved via precedence |
If you are using Ansible variables for the first time, start with group variables, host variables, and role defaults. They are the most intuitive and map directly to how systems are grouped and configured in real environments.
Ansible Variable Precedence
If you define the same variable in more than one place, Ansible needs to know which value to use. It follows a built-in set of rules called variable precedence and always uses the value with the highest precedence.
For example, you might define a standard size for virtual machines (VMs) that Ansible deploys on remote hosts in the group_vars/ directory:
# group_vars/vms.yml
vm_size: standard
vm_specs:
standard:
cpu: 2
memory: 4GB
large:
cpu: 4
memory: 8GB
To deploy a larger VM without editing group variable files, use the -e flag (extra variable) to override the value at runtime:
ansible-playbook vmdeploy.yml -e vm_size=large
Ansible will apply vm_size=large for this run because extra variables have higher precedence than group variables. The values in group_vars/vms.yml are not affected.
Defining Variables in Ansible
It is very important where you define a variable because it determines how broadly it applies and if it can be overridden. The ability to override variables is central to Ansible's flexibility.
group_vars/ Variables
In this example, the ubuntu.yml file defines the default operating system and baseline configuration for all VMs in the ubuntu inventory group. These values are applied whenever a playbook or role creates or configures a VM.
Note: Starting with a single variable file is common. Many projects begin this way and later split variables into multiple files.
# group_vars/ubuntu.yml
# --- Operating system & provisioning ---
vm_os_family: ubuntu
vm_os_version: "24.04"
vm_timezone: UTC
vm_locale: en_US.UTF-8
# --- VM sizing profiles ---
vm_size: standard
vm_specs:
standard:
cpu: 2
memory: 4GB
disk: 25GB
large:
cpu: 4
memory: 6GB
disk: 50GB
# --- Users ---
vm_admin_user: pnapadmin
vm_admin_groups:
- sudo
- adm
vm_ssh_allow_root: false
vm_ssh_port: 22
# --- Packages ---
vm_base_packages:
- curl
- vim
- htop
- rsync
- ca-certificates
vm_enable_firewall: true
vm_firewall_allowed_ports:
- 22
- 80
- 443
# --- Logs ---
vm_enable_monitoring: true
vm_monitoring_agent: prometheus_node_exporter
vm_log_retention_days: 7
# --- Backups ---
vm_backup_enabled: true
vm_backup_policy: daily
vm_backup_time: "01:00"
# --- Environment metadata ---
environment: production
owner_team: platform
cost_center: infra-01
This file is loaded automatically for hosts in the ubuntu inventory group. Each section in this YAML file can later be split into separate files as your automation grows.
host_vars/ Variables
Unlike group variables, host variables are used for exceptions. Use them when a single machine needs settings different from those of the rest of its group.
host_vars files are usually much smaller than group_vars files. They override only what is different for a specific host and inherit everything else from the group variables.
In this example, one Ubuntu virtual machine needs more resources and a different backup policy than the rest of the group:
# host_vars/ubuntu-large.yml
# Override VM sizing
vm_size: large
# Host-specific backup settings
vm_backup_policy: hourly
vm_backup_time: "00:00"
# Disable monitoring for this host
vm_enable_monitoring: false
All shared VM settings still come from group_vars. Ansible merges these values at runtime and applies the host-specific overrides, as host_vars variables take precedence over group variables.
Role Default Variables
Role defaults are starter values that live inside a role and allow it to work without any additional setup. They are designed to be overridden by other variables.
You can adapt the same role for different environments or hosts using group_vars/, host_vars/, inventory variables, or playbook variables. No changes to the role code are required.
For example, a role can provision or configure a VM using safe default values:
# roles/ubuntu/defaults/main.yml
vm_size: standard
vm_timezone: UTC
vm_locale: en_US.UTF-8
vm_enable_firewall: true
vm_firewall_allowed_ports:
- 22
vm_enable_monitoring: true
vm_log_retention_days: 14
vm_backup_enabled: true
vm_backup_policy: daily
vm_backup_time: "02:00"
You can override any of these values by defining the same variables in group or host variables. Ansible will automatically find and override the role defaults because they have the lowest precedence.
Role-Specific Variables
Role-specific variables live inside a role and are not meant to change across environments or hosts. They represent internal settings that the role depends on to function correctly.
Role-specific variables are defined in:
roles/<role_name>/vars/main.yml
Because role-specific variables have high precedence, they are not easily overridden. This ensures the role behaves consistently regardless of how or where it's used.
Here, the role configures baseline VM behavior that relies on fixed internal values:
# roles/ubuntu/vars/main.yml
vm_service_user: root
vm_config_dir: /etc/ubuntu
vm_systemd_service: ubuntu-agent
These values are required for the role to work correctly. Ansible assigns them higher precedence so users of the role cannot accidentally override them and break expected behavior.
Playbook Variables
You can define variables directly inside a playbook. These variables are useful when a setting is relevant only to that specific play. In this example, the play initiates a maintenance run to temporarily change behavior during a deployment or patch window:
- hosts: ubuntu
vars:
maintenance_mode: true
patch_window_reason: "Security updates (monthly)"
tasks:
- name: Maintenance message
debug:
msg: >
Running in maintenance_mode={{ maintenance_mode }}
({{ patch_window_reason }})
The maintenance_mode and patch_window_reason variables apply only to this playbook execution. They do not describe the system like variables in group_vars/ or host_vars/, and they are not stored in inventory.
Inventory Variables
Inventory variables are values defined directly in the inventory file alongside hosts or groups. Because everything lives in one place, they are mostly used in small or early setups. As projects grow, these variables usually move into group_vars/ and host_vars/ for better structure.
Inventory variables can be defined at the group level so that all hosts in the group inherit the same values:
[ubuntu]
vm1
vm2
[ubuntu:vars]
vm_os_family=ubuntu
vm_os_version=24.04
vm_size=standard
vm1 and vm2 will inherit the variables defined under [ubuntu:vars]. You can also define variables directly on individual hosts:
[ubuntu]
vm1 vm_size=standard
vm2 vm_size=large
Both VMs still belong to the ubuntu group, but vm2 receives a larger size. You do not need to create a separate host_vars/ file.
Extra Vars
You can use the -e flag to apply a variable value for a single playbook execution. Extra vars are intended as temporary overrides for changing Ansible's behavior without editing its files.
In this example, daily backups are enabled for all Ubuntu VMs:
# group_vars/ubuntu.yml
vm_backup_enabled: true
vm_backup_policy: daily
vm_backup_time: "01:00"
To disable backups for a single play, you can override the value at runtime:
ansible-playbook deploy.yml -e vm_backup_enabled=false
Extra vars are for last-minute overrides. Use them sparingly as they override almost everything else, and can make automations harder to track.
Prompting the User for Variables
In some cases, you may want Ansible to prompt the user running the playbook for a value at runtime rather than reading it from a file. Especially when the variable should not be stored in plain text, such as a password.
For example, you can prompt the user for input at the start of the playbook using vars_prompt:
- hosts: ubuntu
vars_prompt:
- name: admin_password
prompt: "Enter the VM admin password"
private: yes
tasks:
- name: Confirm password
debug:
msg: "Password received (hidden)."
When the playbook starts, Ansible pauses and asks the user to enter a value. The value is stored in admin_password and can be used like any other variable during the playbook run.
Another common use case is to ask the user to confirm before continuing with an important action:
- hosts: ubuntu
vars_prompt:
- name: confirm_deploy
prompt: "Deploy changes to production? (yes/no)"
private: no
tasks:
- name: Stop if user did not confirm
fail:
msg: "Deployment cancelled."
when: confirm_deploy != "yes"
In this example, the playbook stops execution unless the user explicitly confirms the deployment.
Accessing Ansible Variables
You can use a defined variable anywhere in Ansible by referencing its name inside double curly braces:
{{ variable_name }}
Ansible replaces variables with their actual values at runtime when executing tasks, rendering templates, or evaluating conditions.
Variables in Tasks
Once variables are defined in group_vars/, host_vars/, the inventory, or a role, you can use them inside tasks.
Using the same ubuntu.yml example, the group variables in the ubuntu group include a package list:
# group_vars/ubuntu.yml
vm_base_packages:
- curl
- vim
- htop
- rsync
- ca-certificates
You can install all packages from the list in a single task:
- hosts: ubuntu
become: true
tasks:
- name: Install baseline packages
apt:
name: "{{ vm_base_packages }}"
state: present
update_cache: true
This approach lets you change the list of base packages from ubuntu.yml, the inventory, roles, or other sources without modifying the task itself.
Using Variables with Facts
At the start of each play, Ansible automatically gathers system details from the host it connects to. These system details, known as facts, can be used to make decisions in playbooks, tasks, and templates.
If working with a mixed group of operating systems, you can prevent OS-specific tasks from running on the wrong host. In this example, Ansible stops the play if the host is not running Ubuntu:
- hosts: ubuntu
become: true
tasks:
- name: Stop if not Ubuntu
fail:
msg: "This playbook is for Ubuntu hosts only."
when: ansible_facts['distribution'] != "Ubuntu"
Facts can also be used to make decisions based on hardware characteristics. Here, Ansible installs Prometheus only on hosts with enough memory:
- hosts: ubuntuvm
become: true
tasks:
- name: Install monitoring agent only on larger VMs
apt:
name: prometheus-node-exporter
state: present
update_cache: true
when: ansible_facts['memtotal_mb'] >= 4096
In these cases, Ansible uses the host's real runtime state to determine what to do next.
Variables Created at Runtime
Some variables do not exist until Ansible starts running tasks. These variables are created during execution, based on command output, system state, or earlier task results.
This can happen in two ways: registered variables and set_fact variables.
Registered Variables
Registered variables store the results of a task so you can use them later in the play. In this example, Ansible checks disk usage on an Ubuntu host and saves the output:
- hosts: ubuntu
become: true
tasks:
- name: Check root disk usage
command: df -h /
register: disk_usage
You can then inspect the result:
- debug:
var: disk_usage.stdout
This is an example of disk_usage.stdout output:
Filesystem Size Used Avail Use% Mounted on
/dev/sda1 40G 32G 8.0G 80% /
The stored results can also be used in conditional logic:
- name: Warn if disk is almost full
debug:
msg: "Disk usage high on {{ inventory_hostname }}"
when: disk_usage.stdout is search("9[0-9]%")
If disk usage exceeds 90% on a host, Ansible prints the warning message that includes the host name.
Set fact Variables
The set_fact module allows you to create or modify variables from existing values during a play. Let's assume VM size is defined in the group_vars/ubuntu.yml file:
vm_size: standard
You can create a derived variable based on that value:
- name: Mark large VMs
set_fact:
is_large_vm: true
when: vm_size == "large"
Later tasks can use this variable:
- name: Enable extra monitoring on large VMs
debug:
msg: "Extra monitoring enabled"
when: is_large_vm | default(false)
Once created, set_fact variables behave like normal variables for the rest of the play.
Variables in Templates
A template is a reusable .j2 file that Ansible renders by filling in the correct values for each host. Templates are used to generate configuration files at runtime. The same template can produce different outputs for each host.
For example, you can use a template to give Ubuntu VMs a config file that reflects the variables you defined:
# templates/vm-profile.conf.j2
# Generated by Ansible — do not edit manually
environment={{ environment }}
timezone={{ vm_timezone }}
locale={{ vm_locale }}
admin_user={{ vm_admin_user }}
ssh_port={{ vm_ssh_port }}
allow_root_ssh={{ vm_ssh_allow_root }}
backup_enabled={{ vm_backup_enabled }}
backup_policy={{ vm_backup_policy }}
backup_time={{ vm_backup_time }}
monitoring_enabled={{ vm_enable_monitoring }}
monitoring_agent={{ vm_monitoring_agent }}
log_retention_days={{ vm_log_retention_days }}
You can now deploy the template with the template module:
- hosts: ubuntu
become: true
tasks:
- name: Render VM profile configuration
template:
src: vm-profile.conf.j2
dest: /etc/vm-profile.conf
owner: root
group: root
mode: "0644"
Ansible generates a separate configuration file for each host, using the resolved variable values for that host.
Variables in Conditions
The when condition instructs Ansible to run a task only if a variable matches the specified condition. In this example, monitoring is enabled by default in ubuntu.yml using a boolean flag:
# group_vars/ubuntu.yml
vm_enable_monitoring: true
You can configure a task to install Prometheus only when this flag is set to true:
- name: Install monitoring agent
apt:
name: prometheus-node-exporter
state: present
update_cache: true
when: vm_enable_monitoring
If vm_enable_monitoring is false, Ansible skips the task.
Accessing Variables from Other Hosts
Sometimes a task requires data from another host. Ansible supports this through the hostvars dictionary. You can access variables from another host using the following syntax:
hostvars['hostname']['variable_name']
For instance, if you have a load balancer and multiple backend servers, the backend servers need the load balancer's IP address and port to receive incoming traffic.
Here, the inventory includes one load balancer and two backend servers:
[loadbalancer]
lb1
[backend]
web1
web2
The load balancer defines its connection details in host_vars/:
# host_vars/lb1.yml
lb_ip: 10.0.0.5
lb_port: 443
When you run a play on the backend hosts, they can access the load balancer's IP and port:
- hosts: backend
tasks:
- name: Show load balancer endpoint
debug:
msg: >
Load balancer endpoint:
https://{{ hostvars['lb1'].lb_ip }}:{{ hostvars['lb1'].lb_port }}
Note: Learn why mainstreaming Infrastructure as Code (IaC) is one of the top DevOps trends.
Combining Variables in Ansible
Variables in Ansible can be combined to build more complex values, where some parts change across hosts or runs while others remain fixed.
Combining Variables in Tasks
When task behavior depends on both inventory and playbook variables, you can combine them to control whether a task should run.
For example, a VM may support backups, but you want to skip backup configuration during a maintenance run:
- hosts: ubuntu
vars:
maintenance_mode: true
maintenance_reason: "Monthly security updates"
tasks:
- name: Configure backup schedule
debug:
msg: >
Configuring backup policy={{ vm_backup_policy }}
at {{ vm_backup_time }}
(maintenance_mode={{ maintenance_mode }},
reason={{ maintenance_reason }})
when:
- vm_backup_enabled
- not maintenance_mode
Here, the vm_backup_* variables describe the VM's capabilities, while maintenance_mode and maintenance_reason describe the context of the current run.
The task executes only when both conditions are met.
Combining Variables with Facts
You can combine variables and facts when a task needs to adapt to a system's state at runtime.
In the example, ansible_hostname, ansible_distribution, and ansible_distribution_version are facts Ansible discovers at runtime. vm_size and vm_admin_user come from group_vars/ and describe the VM configuration:
- hosts: ubuntu
tasks:
- name: System identity
debug:
msg: >
Host={{ inventory_hostname }}
(hostname={{ ansible_hostname }},
os={{ ansible_distribution }} {{ ansible_distribution_version }},
size={{ vm_size }},
admin={{ vm_admin_user }})
The task combines the host's discovered state with its configuration into a single output:
Host=vm03 (hostname=vm03, os=Ubuntu 24.04, size=standard, admin=pnapadmin)
Note: If you are worried about making changes on remote servers, test your playbook in Ansible dry run mode first.
Simple Conditional Combinations
Variables can be combined with conditions to calculate a new value, not just to control if a task runs (like with when). This is useful when a configuration value depends on inventory data, such as the host's environment.
For example, you want to use a different SSH port in production than in a test environment:
- hosts: ubuntu
tasks:
- name: Select effective SSH port
set_fact:
effective_ssh_port: >-
{{ 2222 if environment == 'production' else 22 }}
- name: Show selected SSH port
debug:
msg: "Using SSH port {{ effective_ssh_port }}"
The environment variable is sourced from inventory. If the host is in the production environment, Ansible sets the SSH port to 2222. Otherwise, it uses the default port 22.
The result is a new variable, effective_ssh_port, which can be reused by later tasks without repeating the condition.
Combining Variables in Strings
To generate human-readable output, like file paths, log messages, or identifiers, you need to combine variables into strings. In this example, multiple variables are combined to build a descriptive identifier for a virtual machine:
- hosts: ubuntu
vars:
change_id: "CHG-2026-0122"
tasks:
- name: Show VM id
debug:
msg: >
{{ inventory_hostname }} |
{{ vm_os_family }} {{ vm_os_version }} |
size={{ vm_size }} |
admin={{ vm_admin_user }} |
change={{ change_id }}
The vm_os_family, vm_os_version, vm_size, and vm_admin_user variables come from group_vars/ and describe the system. change_id is defined in the playbook and describes the context of the current run.
The output combines both into a single meaningful message:
vm03 | ubuntu 24.04 | size=large | admin=pnapadmin | change=CHG-2026-0122
Note: To check if a file or directory exists on a remote host, use the Ansible stat module before running a task.
Combining Variables in Templates
Templates use variables to generate configuration files. Besides inserting single variables, you can combine multiple variables into one value. This allows Ansible to render the same template differently for each host, environment, or role, without duplicating files.
A basic example is to construct a service name or hostname from multiple variables:
server_name {{ app_name }}.{{ domain_name }};
Each part of the value comes from a separate variable, and Ansible combines them when rendering the template. The following variables are defined in group_vars/:
# group_vars/app.yml
app_name: supercool
domain_name: someurl.com
This is the template file:
# templates/nginx.conf.j2
server {
listen 80;
server_name {{ app_name }}.{{ domain_name }};
}
When Ansible renders the template on a host, the output looks like this:
server_name supercool.someurl.com;
Each host gets the correct configuration, while the template stays the same and can be reused.
Ansible Variables: Common Pitfalls
Some of the most common mistakes people who are new to Ansible variables make include:
- Using variables in too many places. If you define the same variable in inventory, group_vars, playbooks, and roles, it can quickly become unclear which value Ansible is using. Define a variable in one logical place and override it only when there is a clear reason to do so.
- Putting too many variables in the same place. Storing too many variables in a playbook results in an endless list that's hard to read and maintain. Use variables as building blocks and place them where they make most sense: system definitions in inventory, and run-specific values in playbooks.
- Not keeping track of precedence. If you change a variable in a file and nothing happens, another variable with higher precedence is most likely overriding it. Refer to the table at the beginning of this guide to see how different variable types are prioritized.
- Overusing extra variables. Passing overrides from the command line using the
-eflag may feel like an easy shortcut at first. But over time, the automation will become harder to reproduce, document, and troubleshoot. - Thinking that Ansible is a programming language. The term variable can be misleading. Ansible variables do not behave like variables in programming languages like Python. They are declarative configuration values that are resolved at runtime. Think of them as describing a desired state, not as temporary values in code.
Ansible Variables: Best Practices
To ensure that your Ansible projects stay well-organized:
- Give variables clear and descriptive names. You will use a lot of variables. Make variable names explicit so it is immediately clear if they represent a system or environment and how they are used.
- Use group_vars/ and host_vars/ extensively. Any value shared by multiple systems should live in group and host variables. This will give you more flexibility and make overrides easier and safer than rewriting inventory or playbooks for special cases.
- Do not store sensitive variables in plain text. Avoid committing secrets, such as passwords, tokens, or keys, to source control. When dealing with sensitive values, prompt for them at runtime or use Ansible Vault instead of plain-text files.
- Don’t try to use every existing variable option. You will spread variable definitions, making your Ansible projects difficult to debug. Try to find a structure that suits your scenario and stick to it.
- Use snake_case for variable names. Mixing naming styles, such as camelCase and snake_case, makes Ansible files harder to read. Stick to snake_case for consistency across playbooks, roles, and inventory.
- Keep environment and host-specific values in inventory. Instead of scattering environment-specific values across multiple files, define them directly in inventory, where they are easiest to understand.
Conclusion
You've seen where Ansible variables are defined, how they interact, and how precedence determines the final value at runtime.
If you are looking for an environment to run your Ansible automation, PhoenixNAP's Bare Metal Cloud (BMC) integrates with Ansible through the Bare Metal Cloud plugin.



