Chapter #4: Ansible Variables, Facts and Registers

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:

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 toCentOSfrom 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:

  1. Create a facts file on your control node.
  2. Create the /etc/ansible/facts.d directory on your managed node(s).
  3. 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.

Ansible Loops: Complete Beginner’s Guide
This is the fifth chapter of RHCE Ansible EX 294 exam preparation series. And in this, you’ll learn about using loops in Ansible. The tutorial will be available to public after a week. Become a free member to access it today.

Stay tuned for next tutorial as you are going to learn to use loops in Ansible.