If I were to pick the next most important feature of Ansible after playbooks, I think I’d say it’s the variables (both custom and built-in, known as Ansible Facts). Variables are very important as they can speed up your playbooks writing process and allow your plays to be far more advanced.

Intro to variables

But what are variables you ask? Well, variables should be well known to anyone who ever had to script or code. In general terms, variables are used to store data in “containers” (not the docker/k8s kind); you can imagine variables are “labels” you assign to your data.

Variables are so important, we actually used one of them in the previous episodes of this series:

---
- name: "Simple Playbook - Play 1"
  hosts: all
  become: yes
  tasks:
    - name: "set hostname on all 5 test servers"
      hostname: 
        name: "test{{ play_hosts.index(inventory_hostname) }}"

Can you spot a variable in there? Of course, it’s:{{ play_hosts.index(inventory_hostname) }} This particular variable is an Ansible Fact, a pre-built variable that Ansible “learns” from your hosts at run time. In this case, it is a Fact which returns an inventory index of the host being processed (a number representing a position of the host in the inventory; first host would return 1, second would return 2, and so on).

To be perfectly honest, the Fact above is a pretty complicated one and I wouldn’t expect you to use it in your first few playbooks. A much better example of a simple variable would be something like this:

vars:
    packages:
    - httpd
    - httpd-tools

The packages variable defined above contains two text strings: httpd and httpd-tools. Such a variable could be, for example, used to install multiple packages in a single play using yum module. Without using variables, to install both httpd and httpd-tools, you’d have to write two plays like so:

---
- name: "Install two pkgs without vars"
  hosts: all
  become: yes
  tasks:
    - name: "Install httpd"
      yum:
        name: httpd
        state: present
    - name: "Install httpd-tools"
      yum:
        name: httpd-tools
        state: present

Doesn’t look too bad, does it? Perhaps… but, imagine writing a playbook in this style which would be responsible for installing 100s of individual packages - seems tedious, seems boring… that’s not what Ansible is about! Let’s try that again - this time with variables:

---
name: "Install two pkgs with vars"
hosts: all
become: yes
tasks:
  - name: "Install all pkgs"
    yum:
      name: packages
      state: present
    vars:
      packages:
        - httpd
        - httpd-tools

Ah, now that’s better.

Similarly, we can use variables for other types of data to avoid writing the same stuff over and over again. You could have a DNSServers variable containing IP addresses of all DNS servers you wish to configure on your hosts, or a simple DelFiles variable with the paths to all files you wish to get rid of - possibilities are endless.

While the most common use of a variable is to store multiple string, integers etc. under one “label”, you could also use them to avoid writing a single data object in multiple places. Let’s say you want to write a playbook that would often refer to a file on your server stored in: /home/ansible/data/bin/prod/test/script/monday/tldr. Writing this path once could end up in a typo or two - imagine including it in a playbook 20 times. Bugs, bugs everywhere I tell ye.

Instead, you could just use a variable and write that monster of a path just once and refer to it as monsterPath from that point on:

vars:
  monsterPath: "/home/ansible/data/bin/prod/test/script/monday/tldr"

Ansible Facts

We’ve talked about the variables we defined ourselves, but that’s not all variables Ansible has to offer. As mentioned earlier, Ansible comes “pre-built” with loads of variables that are being discovered from our hosts at run time.

Data learned “at run time” is dynamic and can change each time we run a playbook. An example of this would be a Fact storing the amount of disk space available on a host.

To see all Facts available to us, we can run an ad-hoc Ansible script using thesetup module. Let’s run it now against our client machine:

vlku@client.ansible.lab : ansible localhost -m setup
localhost | SUCCESS => {
    "ansible_facts": {
        "ansible_all_ipv4_addresses": [
            "192.168.122.1",
            "192.168.0.115",
            "172.17.0.1",
            "172.18.0.1"
        ],
        "ansible_all_ipv6_addresses": [
            "fe80::b418:8106:9c33:4b6b"
        ],
        "ansible_apparmor": {
            "status": "disabled"
        },
        "ansible_architecture": "x86_64",
        "ansible_bios_date": "03/26/2020",
        "ansible_bios_version": "1.5.1",
(...)
pastebin with complete output: https://pastebin.com/9EQr2qEd

As you can see, Ansible Facts can be used to retrieve pretty much every possible info about your hosts. For example, you could retrieve the main IP address of a host with: {{ ansible_facts["eth0"]["ipv4"]["address"] }} or {{ ansible_facts.eth0.ipv4.address }}

There are also “Special Facts” or “Special Variables”, but we’ll talk about these once we move on to writing advanced Ansible playbooks. If you’re really interested now, you can read about them here.

Using variables and facts in a playbook

Right, that’s enough theory for now, let’s finish this lesson by writing a playbook using both variables and Ansible Facts.

Here’s the scenario:

In our inventory, we have both Red Hat and CentOS servers. These servers are not in any custom groups. We need to install httpd and httpd-tools on Red Hat boxes only, while vim, mysql and php should be exclusively installed on CentOS systems.

---
name: "Install pkgs on specific OS only"
hosts: all
become: yes
tasks:
  - name: "install http pkgs on Red Hat"
    yum:
      name: packages
      state: present
    vars:
      packages:
        - httpd
        - httpd-tools
    when: ansible_facts['os_family'] == "Red Hat"
  - name: "install vim on CentOS"
    yum:
      name: packages
      state: present
    vars:
      packages:
        - vim
        - php
        - mysql
    when: ansible_facts['os_family'] == "CentOS"

Right, we had to use a conditional here… hopefully that’s easy enough to understand. The when conditional checks if a given condition is met; module used in the same task as when will only be executed if said condition is met. In this example, yum will only run if ansible_facts['os_family'] matches the required OS name… Again, we’ll discuss conditionals in detail as part of one of the future episodes.