Chapter #4: Ansible Variables, Facts and Registers
In the fourth chapter of RHCE Ansible EX 294 exam preparation series, you'll learn about variables, facts and registers in this chapter.
There will always be a lot of variances across your managed systems. For this reason, you need to learn how to work with Ansible variables.
In this tutorial, you will learn how to define and reference variables Ansible. You will also learn how to use Ansible facts to retrieve information on your managed nodes.
Furthermore, you will also learn how to use registers to capture task output.
If it is your first time here, please have a look at previous chapters in this series:
- Ansible RHCE #1: Introduction and installation
- Ansible RHCE #1: Running ad-hoc commands
- Ansible RHCE #1: Understanding playbooks
Part 1: Working with variables in Ansible
Let's start with variables first. Keep in mind that all this is written in your YML file.
Defining and referencing variables
You can use the vars keyword to define variables directly inside a playbook.
For example, you can define a fav_color variable and set its value to yellow as follows:
---
- name: Working with variables
hosts: all
vars:
fav_color: yellow
Now how do you use (reference) the fav_color variable? Ansible uses the Jinja2 templating system to work with variables. There will be a dedicated tutorial that discusses Jinja2 in this series but for now you just need to know the very basics.
To get the value of the fav_color variable; you need to surround it by a pair of curly brackets as follows:
My favorite color is {{ fav_color }}
Notice that if your variable is the first element (or only element) in the line, then using quotes is mandatory as follows:
"{{ fav_color }} is my favorite color."
Now let’s write a playbook named variables-playbook.yml that puts all this together:
[elliot@control plays]$ cat variables-playbook.yml
---
- name: Working with variables
hosts: node1
vars:
fav_color: yellow
tasks:
- name: Show me fav_color value
debug:
msg: My favorite color is {{ fav_color }}.
I have used the debug module along with the msg module option to print the value of the fav_color variable.
Now run the playbook and you shall see your favorite color displayed as follows:
[elliot@control plays]$ ansible-playbook variables-playbook.yml
PLAY [Working with variables] **************************************************
TASK [Gathering Facts] *********************************************************
ok: [node1]
TASK [Show me fav_color value] *************************************************
ok: [node1] => {
"msg": "My favorite color is yellow."
}
PLAY RECAP *********************************************************************
node1 : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Creating lists and dictionaries
You can also use lists and dictionaries to define multivalued variables. For example, you may define a list named port_nums and set its value as follows:
vars:
port_nums: [21,22,23,25,80,443]
You could have also used the following way to define port_nums which is equivalent:
vars:
port_nums:
- 21
- 22
- 23
- 25
- 80
- 443
You can print all the values in port_nums as follows:
All the ports {{ port_nums }}
You can also access a specific list element:
First port is {{ port_nums[0] }}
Notice that you use an index (position) to access list elements.
You can also define a dictionary named users as follows:
vars:
users:
bob:
username: bob
uid: 1122
shell: /bin/bash
lisa:
username: lisa
uid: 2233
shell: /bin/sh
There are two different ways you can use to access dictionary elements:
- dict_name['key'] -> users['bob']['shell']
- dict_name.key -> users.bob.shell
Notice that you use a key to access dictionary elements.
Now you can edit the variables-playbook.yml playbook to show lists and dictionaries in action:
[elliot@control plays]$ cat variables-playbook.yml
---
- name: Working with variables
hosts: node1
vars:
port_nums: [21,22,23,25,80,443]
users:
bob:
username: bob
uid: 1122
shell: /bin/bash
lisa:
username: lisa
uid: 2233
shell: /bin/sh
tasks:
- name: Show 2nd item in port_nums
debug:
msg: SSH port is {{ port_nums[1] }}
- name: Show the uid of bob
debug:
msg: UID of bob is {{ users.bob.uid }}
You can now run the playbook to display the second element in port_nums and show bob’s uid:
[elliot@control plays]$ ansible-playbook variables-playbook.yml
PLAY [Working with variables] **************************************************
TASK [Show 2nd item in port_nums] **********************************************
ok: [node1] => {
"msg": "SSH port is 22"
}
TASK [Show the uid of bob] *****************************************************
ok: [node1] => {
"msg": "UID of bob is 1122"
}
Including external variables
Just like you can import (or include) tasks in a playbook. You can do the same thing with variables as well. That is, in a playbook, you can include variables defined in an external file.
To demonstrate, let’s create a file named myvars.yml that contains our port_nums list and users dictionary:
[elliot@control plays]$ cat myvars.yml
---
port_nums: [21,22,23,25,80,443]
users:
bob:
username: bob
uid: 1122
shell: /bin/bash
lisa:
username: lisa
uid: 2233
shell: /bin/sh
Now you can use the vars_files keyword in your variables-playbook.yml to include the variables in myvars.yml as follows:
[elliot@control plays]$ cat variables-playbook.yml
---
- name: Working with variables
hosts: node1
vars_files: myvars.yml
tasks:
- name: Show 2nd item in port_nums
debug:
msg: SSH port is {{ port_nums[1] }}
- name: Show the uid of bob
debug:
msg: UID of bob is {{ users.bob.uid }}
Keep in mind that vars_files preprocesses and load the variables right at the start of the playbook. You can also use the include_vars module to dynamically load your variables in your playbook:
[elliot@control plays]$ cat variables-playbook.yml
---
- name: Working with variables
hosts: node1
tasks:
- name: Load the variables
include_vars: myvars.yml
- name: Show 2nd item in port_nums
debug:
msg: SSH port is {{ port_nums[1] }}
Getting user input
You can use the vars_prompt keyword to prompt the user to set a variable’s value at runtime.
For example, the following greet.yml playbook asks the running user to enter his name and then displays a personalized greeting message:
[elliot@control plays]$ cat greet.yml
---
- name: Greet the user
hosts: node1
vars_prompt:
- name: username
prompt: What's your name?
private: no
tasks:
- name: Greet the user
debug:
msg: Hello {{ username }}
Notice I used private: no so that you can see your input on the screen as you type it; by default, it’s hidden.
Now run the playbook and enter your name:
[elliot@control plays]$ ansible-playbook greet.yml
What's your name?: Elliot
PLAY [Greet the user] **********************************************************
TASK [Greet the user] **********************************************************
ok: [node1] => {
"msg": "Hello Elliot"
}
Setting host and group variables
You can set variables that are specific to your managed hosts. By doing so, you can create much more efficient playbooks as you don’t need to write repeated tasks for different nodes or groups.
To demonstrate, edit your inventory file so that your managed nodes are grouped in the following three groups:
[elliot@control plays]$ cat myhosts
[proxy]
node1
[webservers]
node2
node3
[dbservers]
node4
Now to create variables that are specific to your managed nodes; first, you need to create a directory named host_vars. Then inside host_vars, you can create variables files with filenames that corresponds to your node hostnames:
[elliot@control plays]$ mkdir host_vars
[elliot@control plays]$ echo "message: I am a Proxy Server" >> host_vars/node1.yml
[elliot@control plays]$ echo "message: I am a Web Server" >> host_vars/node2.yml
[elliot@control plays]$ echo "message: I am a Web Server" >> host_vars/node3.yml
[elliot@control plays]$ echo "message: I am a Database Server" >> host_vars/node4.yml
Now let’s create a playbook named motd.yml that demonstrates how host_vars work:
[elliot@control plays]$ cat motd.yml
---
- name: Set motd on all nodes
hosts: all
tasks:
- name: Set motd = value of message variable.
copy:
content: "{{ message }}"
dest: /etc/motd
I used the copy module to copy the contents of the message variable onto the /etc/motd file on all nodes. Now after running the playbook; you should see that the contents of /etc/motd has been updated on all nodes with the corresponding message value:
[elliot@control plays]$ ansible all -m command -a "cat /etc/motd"
node1 | CHANGED | rc=0 >>
I am a Proxy Server
node2 | CHANGED | rc=0 >>
I am a Web Server
node3 | CHANGED | rc=0 >>
I am a Web Server
node4 | CHANGED | rc=0 >>
I am a Database Server
Awesome! Similarly, you can use create a group_vars directory and then include all group related variables in a filename that corresponds to the group name as follows:
[elliot@control plays]$ mkdir group_vars
[elliot@control plays]$ echo "pkg: squid" >> group_vars/proxy
[elliot@control plays]$ echo "pkg: httpd" >> group_vars/webservers
[elliot@control plays]$ echo "pkg: mariadb-server" >> group_vars/dbservers
I will let you create a playbook that runs on all nodes; each node will install the package that is set in the node’s corresponding group pkg variable.
Understanding variable precedence
As you have seen so far; Ansible variables can be set at different scopes (or levels).
If the same variable is set at different levels; the most specific level gets precedence. For example, a variable that is set on a play level takes precedence over the same variable set on a host level (host_vars).
Furthermore, a variable that is set on the command line using the --extra-vars takes the highest precedence, that is, it will overwrite anything else.
To demonstrate, let’s create a playbook named variable-precedence.yml that contains the following content:
[elliot@control plays]$ cat variable-precedence.yml
---
- name: Understanding Variable Precedence
hosts: node1
vars:
fav_distro: "Ubuntu"
tasks:
- name: Show value of fav_distro
debug:
msg: Favorite distro is {{ fav_distro }}
Now let’s run the playbook while using the -e (--extra-vars) option to set the value of the fav_distro variable to“CentOS” from the command line:
[elliot@control plays]$ ansible-playbook variable-precedence.yml -e "fav_distro=CentOS"
PLAY [Understanding Variable Precedence] ***************************************
TASK [Show value of fav_distro] ************************************************
ok: [node1] => {
"msg": "Favorite distro is CentOS"
}
Notice how the command line’s fav_distro value “CentOS” took precedence over the play’s fav_distro value “Ubuntu”.
Part 2: Gathering and showing facts
You can retrieve or discover variables that contain information about your managed hosts. These variables are called facts and Ansible uses the setup module to gather these facts. The IP address on one of your managed nodes is an example of a fact.
You can run the following ad-hoc command to gather and show all the facts on node1:
[elliot@control plays]$ ansible node1 -m setup
Here's the output:
node1 | SUCCESS => {
"ansible_facts": {
"ansible_all_ipv4_addresses": [
"10.0.0.5"
],
"ansible_all_ipv6_addresses": [
"fe80::20d:3aff:fe0c:54aa"
],
"ansible_apparmor": {
"status": "disabled"
},
"ansible_architecture": "x86_64",
"ansible_bios_date": "06/02/2017",
"ansible_bios_version": "090007",
"ansible_cmdline": {
"BOOT_IMAGE": "(hd0,gpt1)/vmlinuz-4.18.0-193.6.3.el8_2.x86_64",
"console": "ttyS0,115200n8",
"earlyprintk": "ttyS0,115200",
"ro": true,
"root": "UUID=6785aa9a-3d19-43ba-a189-f73916b0c827",
"rootdelay": "300",
"scsi_mod.use_blk_mq": "y"
},
"ansible_default_ipv4": {
"address": "10.0.0.5",
"alias": "eth0",
"broadcast": "10.0.0.255",
"gateway": "10.0.0.1",
"interface": "eth0",
"macaddress": "00:0d:3a:0c:54:aa",
This is only a fraction of all the facts related to node1 that you are going to see displayed on your terminal. Notice how the facts are stored in dictionaries or lists and they all belong to the ansible_facts dictionary.
By default, the setup module is automatically called by playbooks to do the facts discovery. You may have noticed that facts discovery happens right at the start when you run any of your playbooks:
[elliot@control plays]$ ansible-playbook motd.yml
PLAY [Set motd on all nodes] ***************************************************
TASK [Gathering Facts] *********************************************************
ok: [node4]
ok: [node3]
ok: [node2]
ok: [node1]
You can turn off facts gathering by setting gather_facts boolean to false right in your play header as follows:
[elliot@control plays]$ cat motd.yml
---
- name: Set motd on all nodes
gather_facts: false
hosts: all
tasks:
- name: Set motd = value of message variable.
copy:
content: "{{ message }}"
dest: /etc/motd
If you run the motd.yaml playbook again; it will skip facts gathering:
[elliot@control plays]$ ansible-playbook motd.yml
PLAY [Set motd on all nodes] ***************************************************
TASK [Set motd = value of message variable.] ********************************
The same way you show a variable’s value; you can also use to show a fact’s value. The following show-facts.yml playbook displays the value of few facts on node1:
[elliot@control plays]$ cat show-facts.yml
---
- name: show some facts
hosts: node1
tasks:
- name: display node1 ipv4 address
debug:
msg: IPv4 address is {{ ansible_facts.default_ipv4.address }}
- name: display node1 fqdn
debug:
msg: FQDN is {{ ansible_facts.fqdn }}
- name: display node1 OS distribution
debug:
msg: OS Distro is {{ ansible_facts.distribution }}
Now run the playbook to display the facts values:
[elliot@control plays]$ ansible-playbook show-facts.yml
PLAY [show some facts] ******
TASK [Gathering Facts] *******
ok: [node1]
TASK [display node1 ipv4 address] *******
ok: [node1] => {
"msg": "IPv4 address is 10.0.0.5"
}
TASK [display node1 fqdn] ********
ok: [node1] => {
"msg": "FQDN is node1.linuxhandbook.local"
}
TASK [display node1 OS distribution] ******
ok: [node1] => {
"msg": "OS Distro is CentOS"
}
PLAY RECAP **********
node1 : ok=4 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Creating customs facts
You may want to create your own custom facts. To do this, you can either use the set_fact module to add temporarily facts or the /etc/ansible/facts.d directory to add permanent facts on your managed nodes.
I am going to show you how to add permanent facts to your managed nodes. It’s a three steps process:
- Create a facts file on your control node.
- Create the /etc/ansible/facts.d directory on your managed node(s).
- Copy your facts file (step 1) from the control node to your managed node(s).
So, first, let’s create a cool.fact file on your control node that includes some cool facts:
[elliot@control plays]$ cat cool.fact
[fun]
kiwi=fruit
matrix=movie
octupus='8 legs'
Notice that your facts filename must have the .fact extension.
For the second step, you are going to use the file module to create and the /etc/ansible/facts.d directory on the managed node(s). And lastly for the third step, you are going to use the copy module to copy cool.fact file from the control node to the managed node(s).
The following custom-facts.yml playbook combines step 2 and 3:
[elliot@control plays]$ cat custom-facts.yml
---
- name: Adding custom facts to node1
hosts: node1
tasks:
- name: Create the facts.d directory
file:
path: /etc/ansible/facts.d
owner: elliot
mode: 775
state: directory
- name: Copy cool.fact to the facts.d directory
copy:
src: cool.fact
dest: /etc/ansible/facts.d
Now run the playbook:
[elliot@control plays]$ ansible-playbook custom-facts.yml
PLAY [Adding custom facts to node1] **********************************
TASK [Gathering Facts] ****************************
ok: [node1]
TASK [Create the facts.d directory] ******************************
changed: [node1]
TASK [Copy cool.fact to the facts.d directory] **********************
changed: [node1]
The cool facts are now permanently part of node1 facts; you can verify with the following ad hoc command:
[elliot@control plays]$ ansible node1 -m setup -a "filter=ansible_local"
"ansible_local": {
"cool": {
"fun": {
"kiwi": "fruit",
"matrix": "movie",
"octupus": "'8 legs'"
}
}
}
You can now display the octopus’s fact in a playbook as follows:
An octopus has {{ ansible_local.cool.fun.octupus }}
Part 3: Capturing output with registers in Ansible
Some tasks will not show any output when running a playbook. For instance, running commands on your managed nodes using the command, shell, or raw modules will not display any output when running a playbook.
You can use a register to capture the output of a task and save it to a variable. This allows you to make use of a task output elsewhere in a playbook by simply addressing the registered variable.
The following register-playbook.yml shows you how to capture a task output in a registered variable and later display its content:
[elliot@control plays]$ cat register-playbook.yml
---
- name: Register Playbook
hosts: proxy
tasks:
- name: Run a command
command: uptime
register: server_uptime
- name: Inspect the server_uptime variable
debug:
var: server_uptime
- name: Show the server uptime
debug:
msg: "{{ server_uptime.stdout }}"
The playbook starts by running the uptime command on the proxy group hosts (node1) and registers the command output in the server_uptime variable.
Then, you use the debug module along with the var module option to inspect the server_uptime variable. Notice, that you don’t need to surround the variable with curly brackets here.
Finally, the last task in the playbook shows the output (stdout) of the registered variable server_uptime.
Run the playbook to see all this in action:
[elliot@control plays]$ ansible-playbook register-playbook.yml
PLAY [Register Playbook Showcase] **********************************************
TASK [Gathering Facts] *********************************************************
ok: [node1]
TASK [Run a command] ***********************************************************
changed: [node1]
TASK [Inspect the server_uptime variable] **************************************
ok: [node1] => {
"server_uptime": {
"changed": true,
"cmd": [
"uptime"
],
"delta": "0:00:00.004221",
"end": "2020-10-29 05:04:36.646712",
"failed": false,
"rc": 0,
"start": "2020-10-29 05:04:36.642491",
"stderr": "",
"stderr_lines": [],
"stdout": " 05:04:36 up 3 days, 6:56, 1 user, load average: 0.24, 0.07, 0.02",
"stdout_lines": [
" 05:04:36 up 3 days, 6:56, 1 user, load average: 0.24, 0.07, 0.02"
]
}
}
TASK [Show the server uptime] **************************************************
ok: [node1] => {
"msg": " 05:04:36 up 3 days, 6:56, 1 user, load average: 0.24, 0.07, 0.02"
}
PLAY RECAP *********************************************************************
node1 : ok=4 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Notice how the registered variable server_uptime is actually a dictionary that contains a lot of other keys beside the stdout key. It also contains other keys like rc (return code), start (time when command run), end (time when command finished), stderr (any errors), etc.
Alright! This takes us to the end of this tutorial.
Stay tuned for next tutorial as you are going to learn to use loops in Ansible.
A Linux sysadmin who likes to code for fun. I have authored Learn Linux Quickly book to help people learn Linux easily. I also like watching the NBA and going for a cruise with my skateboard.