This blog post deep dives into Ansible Variables, which allow us to parametrize different Ansible components. Variables store values for reuse inside an Ansible project.
If you are still learning how to use Ansible, you might also find helpful the introductory Ansible Tutorial or Working with Ansible Playbooks blog posts. You can find this article’s code on this repository if you wish to follow along.
Why Variables Are Useful in Ansible
The use of variables simplifies the management of dynamic values throughout an Ansible project and can potentially reduce the number of human errors. We have a convenient way to handle variations and differences between different environments and systems with variables.
Another advantage of variables in Ansible is that we have the flexibility to define them in multiple places with different precedence according to our use case. We can also register new variables in our playbooks by using the returned value of a task.
Ansible facts are a special type of variables that Ansible retrieves from any remote host for us to leverage them in Ansible projects. For example, we can get information regarding the operating system distribution with ansible_distribution, information about devices on the host, the python version that Ansible is using with ansible_python_version, and the system architecture, among others. To access this data, we have to reference the ansible_facts variable.
Variable Name Rules
Ansible has a strict set of rules to create valid variable names. Variable names can contain only letters, numbers, and underscores and must start with a letter or underscore. Some strings are reserved for other purposes and aren’t valid variable names, such as Python Keywords or Playbook Keywords.
Defining and Referencing Simple Variables
The simplest use case of variables is to define a variable name with a single value using standard YAML syntax. Although this pattern can be used in many places, we will show an example in a playbook for simplicity.
- name: Example Simple Variable
hosts: all
become: yes
vars:
username: bob
tasks:
- name: Add the user {{ username }}
ansible.builtin.user:
name: "{{ username }}"
state: present
In the above example, after the vars block, we define the variable username, and assign the value bob. Later, to reference the value in the task, we use Jinja2 syntax like this "{{ username }}"
If a variable’s value starts with curly braces, we must quote the whole expression to allow YAML to interpret the syntax correctly.
List, Dictionary & Nested Variables
There are many other options to define more complex variables like lists, dictionaries, and nested structures. To create a variable with multiple values, we can use YAML lists syntax:
vars:
version:
- v1
- v2
- v3
To reference a specific value from the list we must select the correct field. For example, to access the third value v3:
version: "{{ version[2] }}"
Another useful option is to store key-value pairs in variables as dictionaries. For example:
vars:
users:
- user_1: maria
- user_2: peter
- user_3: sophie
Similarly, to reference the third field from the dictionary, use the bracket or dot notation:
users['user_3']
users.user_3
Note that the bracket notation is preferred as you might encounter problems using the dot notation in special cases.
Sometimes, we have to create or use nested variable structures. For example, facts are nested data structures. We have to use a bracket or dot notation to reference nested variables.
vars:
cidr_blocks:
production:
vpc_cidr: "172.31.0.0/16"
staging:
vpc_cidr: "10.0.0.0/24"
tasks:
- name: Print production vpc_cidr
ansible.builtin.debug:
var: cidr_blocks['production']['vpc_cidr']
Special Variables
There are certain types of variables that we consider special in the context of Ansible. These include magic variables, connection variables, and facts. The names of these variables are reserved.
Ansible allows us to access information about itself, hosts, groups, inventory, roles, and other Ansible manifests with the so-called magic variables. For a complete list of different options, have a look here.
We already talked about facts. These variables contain all the information that Ansible can get from the current host. To use them, Ansible has to gather them first. To see all the facts that you can gather on a host, execute:
ansible <hostname> -m ansible.builtin.setup
Lastly, we have connection variables. They are used to configure Ansible execution behavior and actions on hosts. The most common ones configure the user that Ansible logs in, set privilege escalation, set the IP of the target host, etc.
Registering Variables
During our plays, we might find it handy to utilize the output of a task as a variable that we can use in the following tasks. We can use the keyword register to create our own custom variables from task output.
- name: Example Register Variable Playbook
hosts: all
tasks:
- name: Run a script and register the output as a variable
shell: "find hosts"
args:
chdir: "/etc"
register: find_hosts_output
- name: Use the output variable of the previous task
debug:
var: find_hosts_output
In the above example, we register the output of the command find /etc/hosts, and we showcase how we can use the variable in the next task by printing its value.
A powerful pattern is to combine registered variables with conditionals to create tasks that will only be executed when certain custom conditions are true.
- name: Example Registered Variables Conditionals
hosts: all
tasks:
- name: Register an example variable
shell: cat /etc/hosts
register: hosts_contents
- name: Check if hosts file contains the word "localhost"
debug:
msg: "/etc/hosts file contains the word localhost"
when: hosts_contents.stdout.find("localhost") != -1
var: find_hosts_output
Here, we registered in the variable hosts_contents the contents of /etc/hosts file, and we execute the second task only if the file contains the word localhost.
Since registered variables are stored in memory, it’s not possible to use them in future plays, and they are only available for the current playbook run.
Share Variables with YAML Anchors and Aliases
When we want to reuse and share variables, we can leverage Y_AML anchors and aliases_. They provide us with great flexibility in handling shared variables and help us reduce the repetition of data.
Anchors are defined with &, and then referenced with an alias denoted with *****. Let’s go and check a hands-on example in a playbook.
- name: Example Anchors and Aliases
hosts: all
become: yes
vars:
user_groups: &user_groups
- devs
- support
user_1:
user_info: &user_info
name: bob
groups: *user_groups
state: present
create_home: yes
user_2:
user_info:
<<: *user_info
name: christina
user_3:
user_info:
<<: *user_info
name: jessica
groups: support
tasks:
- name: Add several groups
ansible.builtin.group:
name: "{{ item }}"
state: present
loop: "{{ user_groups }}"
- name: Add several users
ansible.builtin.user:
<<: *user_info
name: "{{ item.user_info.name }}"
groups: "{{ item.user_info.groups }}"
loop:
- "{{ user_1 }}"
- "{{ user_2 }}"
- "{{ user_3 }}"
Here, since some options are shared between users, instead of rewriting the same values, we share the common ones with the anchor &user_info. For every subsequent user declaration, we use the alias *user_info to avoid repeating ourselves as much as possible.
The values for state and create_home are the same for all the users, while name and groups are replaced using the merge operator <<.
Similarly, we reuse the user_groups declaration in the definition of the user_info anchor. This way, we don’t have to type the same groups again for user_2 while we still have the flexibility to override the groups, as we do for user_3.
The result is that user_1 and user_2 are added to groups devs and support, while user_3 is added only to the support group.
Variable Scope
Ansible provides many options on setting variables, and the ultimate decision on where to set them lies with us based on the scope we would like them to have. Conceptually, there are three main options available for scoping variables.
First, we have the global scope where the values are set for all hosts. This can be defined by the Ansible configuration, environment variables, and command line.
We set values for a particular host or group of hosts using the host scope. For example, there is an option to define some variables per host in the inventory file.
Lastly, we have the play scope, where values are set for all hosts in the context of a play. An example would be the vars section we have seen in previous examples in each playbook.
Variable Setting Options & Precedence
Variables can be defined with Ansible in many different places. There are options to set variables in playbooks, roles, inventory, var files, and command line. Let’s go and explore some of these options.
As we have previously seen, the most straightforward way is to define variables in a play with the vars section.
- name: Set variables in a play
hosts: all
vars:
version: 12.7.1
Another option is to define variables in the inventory file. We can set variables per host or set shared variables for groups. This example defines a different ansible user to connect for each host as a host variable and the same HTTP port for all web servers as a group variable.
[webservers]
webserver1 ansible_host=10.0.0.1 ansible_user=user1
webserver2 ansible_host=10.0.0.2 ansible_user=user2
[webservers:vars]
http_port=80
To better organize our variables, we could gather them in separate host and group variables files. In the same directory where we keep our inventory or playbook files, we can create two folders named group_vars and host_vars that would contain our variable files. For example:
group_vars/databases
group_vars/webservers
host_vars/host1
host_vars/host2
Variables can also be set in custom var files. Let’s check an example that uses variables from an external file and the group_vars and host_vars directories.
- name: Example External Variables file
hosts: all
vars_files:
- ./vars/variables.yml
tasks:
- name: Print the value of variable docker_version
debug:
msg: "{{ docker_version}} "
- name: Print the value of group variable http_port
debug:
msg: "{{ http_port}} "
- name: Print the value of host variable app_version
debug:
msg: "{{ app_version}} "
The vars/variables.yml file:
docker_version: 20.10.12
The group_vars/webservers file:
http_port: 80
ansible_host: 127.0.0.1
ansible_user: vagrant
The host_vars/host1 file:
app_version: 1.0.1
ansible_port: 2222
ansible_ssh_private_key_file: ./.vagrant/machines/host1/virtualbox/private_key
The host_vars/host2 file:
app_version: 1.0.2
ansible_port: 2200
ansible_ssh_private_key_file: ./.vagrant/machines/host2/virtualbox/private_key
The inventory file contains a group named webservers that includes our two hosts, host1 and host2:
[webservers]
host1
host2
If we run this playbook, we notice the same value is used in both hosts for the group variable http_port but a different one for the host variable app_version.
A good use case for having separate variables files is that you can keep in them sensitive values without storing them in playbooks or source control systems.
Occasionally we might find it helpful to define or override variables at runtime by passing them at the command line with --extra-vars or –e argument. For example:
ansible-playbook example-external-vars.yml --extra-vars "app_version=1.0.3"
Since variables can be set in multiple places, Ansible applies variable precedence to select the variable value according to some hierarchy. The general rule is that variables defined with a more explicit scope have higher priority.
For example, role defaults are overridden by mostly every other option. Variables are also flattened to each host before each play so all group and hosts variables are merged. Host variables have higher priority than group variables.
Explicit variables definitions like the vars directory or an include_vars task override variables from the inventory. Finally, extra vars defined at runtime always win precedence. For a complete list of options and their hierarchy, look at the official documentation Understanding variable precedence.
Where to Set Variables & Best Practices
Since Ansible provides a plethora of options to define variables, it might be a bit confusing to figure out the best way and place to set them. Let’s go and check some common & best practices around setting variables that might help us better organize our Ansible projects.
- Always give descriptive and clear names to your variables. Taking a moment to properly think about how to name variables always pays off long-term.
- If there are default values for common variables, set them in group_vars/all
- Prefer setting group and host vars in group_vars and host_vars directories instead of in the inventory file.
- If variables related to geography or behavior are tied to a specific group, prefer to set them as group variables.
- If you are using roles, always set default role variables in roles/your_role/defaults/main.yml
- When you call roles, pass variables that you wish to override as parameters to make your plays easier to read.
roles:
- role: example_role
vars:
example_var: 'example_string'
- You can always use --extra-vars or –e to override every other option.
- Don’t store sensitive variables in your source code repository in plain text. You can leverage Ansible Vault in these cases.
In general, try to keep variables usage as simple as possible. You don’t have to use all the existing options and spread variables definition all over the place because that makes debugging your Ansible projects difficult. Try to find a structure that suits your needs best and stick to it!
Key Points
In this article, we deep-dived into Ansible Variables and saw how we can define and use them in playbooks. Moreover, we explored different options for sharing, setting, and referencing them, along with some guidelines and best practices to make our Ansible journey easier.
Thank you for reading, and I hope you enjoyed this “Ansible Variables” article as much as I did.