How to Use Different Types of Ansible Variables(Examples)
Published: 2022-05-15 18:21:47

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](https://spacelift.io/blog/ansible-tutorial) or [Working with Ansible Playbooks](https://spacelift.io/blog/ansible-playbooks) blog posts. You can find this article’s code on this [repository](https://github.com/spacelift-io-blog-posts/Blog-Technical-Content/tree/master/ansible-variables) 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](https://docs.ansible.com/ansible/latest/user_guide/playbooks_vars_facts.html#vars-and-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](https://docs.python.org/3/reference/lexical_analysis.html#keywords) or [Playbook Keywords](https://docs.ansible.com/ansible/latest/reference_appendices/playbooks_keywords.html#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](https://docs.ansible.com/ansible/latest/user_guide/playbooks_templating.html) like this `"{{ username }}"`

If a variable’s value starts with curly braces, [we must quote the whole expression](https://docs.ansible.com/ansible/latest/user_guide/playbooks_variables.html#when-to-quote-variables-a-yaml-gotcha) 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](https://docs.ansible.com/ansible/latest/reference_appendices/special_variables.html#magic-variables).

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.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ppt1v9uyeaobymeeuvhd.png)

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_.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/r7ifmanti0vlvqfd1uhq.png)

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.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4o3dc7swbdemj82l5eks.png)

## 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_.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9miqoef7uj5zegwi5v8k.png)

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](https://docs.ansible.com/ansible/latest/user_guide/playbooks_variables.html#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](https://docs.ansible.com/ansible/latest/user_guide/vault.html#creating-encrypted-variables) 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.