Ansible: How to work with inventory, variables, and facts

This guide will show you how to work with the inventory file, variables, and facts in Ansible. Understanding these concepts in Ansible is critical. Generally, Ansible uses the information provided in the inventory and variables to discover information about your remote systems. And, the information gathered is then stored as facts.

Let’s discuss each of these items in more detail and learn how to use them.

Inventory file

The inventory file in Ansible stores information about physical and virtual servers, network devices, cloud instances, and much more. In fact, any device that provides SSH access can be added to the inventory file.

Ansible requires SSH to execute commands against remote hosts. In addition, Ansible must provide the username and password to the remote host to access it. Finally, you can use the Inventory file to set privilege escalation and more.

Ansible then reads the inventory file by default in the location /etc/ansible/hosts. However, you can specify a different inventory file using the -i <path> option on the command line.

As you can imagine, the Inventory can get quite complex. Thus, it is important to understand what tools and syntax features are available in Ansible to simplify this complexity and order all the managed nodes correctly.

Basic Inventory file

The most common format for an Ansible inventory file is INI or YAML.

The example inventory file below is an INI file stored in /etc/ansible/hosts:

email.domain.com

[frontend]
foo.domain.com
bar.domain.com

[backend]
192.168.10.10
192.168.10.20
192.168.10.30

As you can see, there are two named groups in this file. You can use “frontend” or “backend” references in your playbook to scope your nodes.

The following example is a YAML inventory file:

all:
  hosts:
    email.domain.com:
  children:
    webservers:
      hosts:
        host1.example.com:
        host2.example.com:
    dbservers:
      hosts:
        serv1.example.com:
        serv2.example.com:
        serv3.example.com:

We use “webservers” or “dbservers” references in your playbook to scope your nodes in this example.

As mentioned before, you can also set a custom inventory file at the command line using the -i <path> option.

The example below shows a file called company-inventory:

[webservers]
server-[a:d].domain.com

[dbservers]
192.168.30.[20:50]

In this example, we use ranges to specify a pool of nodes without listing every node. Thus, it works for both the letter and the numbers.

Next, we can view our inventory using the following command:

ansible-inventory -i company-inventory --list

Also, you can execute your playbook against a specific inventory file by adding the flag -i.

ansible-playbook -i company-inventory playbook.yml

Lastly, grouping nodes can be a powerful tool. The best way to grasp it is to look at the following example:


[routers]
10.10.10.1
10.10.10.2

[switches]
172.16.1.1
172.16.1.2

[firewalls]
192.168.1.1

[network:children]
routers
switches
firewalls

[security:children]
routers
firewalls

[lan:children]
routers
switches

Consequently, we grouped smaller groups into larger groups based on their purpose. Additionally, this concept allows you to have a nested structure of your Ansible Inventory file and provides more flexibility.

Ansible YAML inventory file examples

The first example is a YAML inventory file with four hosts, grouped under “all” and one host with two variables:

all:
  hosts:
      srv1.example.com:
          ansible_ssh_host: 191.168.1.2
          myvariable: value
      srv2.example.com:
      192.168.1.8:
      192.168.1.10:

The second example is a YAML inventory file containing hosts belonging to the ‘webservers’ group, with shared group var:

webservers:
  hosts:
     srv1.example.com:
     srv2.example.com:
     192.168.1.7:
     192.168.1.11:
  vars:
    http_port: 8080

Finally, the third example is a YAML inventory file with hosts using ranges and children groups:

webservers:
  hosts:
    srv1.example.com:
    srv2.example.com:
testing:
  hosts:
    tst[001:006].example.com:
  vars:
    myvar: value1
  children:
    webservers:
other:
  children:
    webservers:
      mark1.example.com

Ansible INI inventory file examples

The first example is an INI inventory file with simple grouping:

[fooland]
foo1
foo2

[barland]
bar1
bar2

The second example of an Ansible inventory INI file is more complex with grouping and variables:

# fmt: ini
# Example 1
[web]
host1
host2 ansible_port=222 # defined inline, interpreted as an integer

[web:vars]
http_port=8080 # all members of 'web' will inherit these
myvar=23 # defined in a :vars section, interpreted as a string

[web:children] # child groups will automatically add their hosts to parent group
apache
nginx

[apache]
tomcat1
tomcat2 myvar=34 # host specific vars override group vars
tomcat3 mysecret="'03#pa33w0rd'" # proper quoting to prevent value changes

[nginx]
jenkins1

[nginx:vars]
has_java = True # vars in child groups override same in parent

[all:vars]
has_java = False # 'all' is 'top' parent

# Example 2
host1 # this is 'ungrouped'

# both hosts have same IP but diff ports, also 'ungrouped'
host2 ansible_host=127.0.0.1 ansible_port=44
host3 ansible_host=127.0.0.1 ansible_port=45

[g1]
host4

[g2]
host4 # same host as above, but member of 2 groups, will inherit vars from both
      # inventory hostnames are unique

Source: https://docs.ansible.com/ansible/latest/collections/ansible/builtin/ini_inventory.html.

Variables

Variables are the principal idea in any programming language or automation flow. The simplest way to define variables is to put a vars section in your playbook with the names and values.

Using the NGINX example, we can set the following parameters as variables:

vars:
  key_file: /etc/nginx/ssl/nginx.key 
  port: 443
  server_name: localhost

It is handy to group variables into files to separate them from the body of the playbook. However, variables lists can be exceptionally long. Therefore, it is a best practice to keep them in the dedicated files.

Var files are also YAML files; let’s create an nginx-vars.yml file:

  key_file: /etc/nginx/ssl/nginx.key 
  port: 443
  server_name: localhost

Consequently, you refer to this file in the playbook, and we can use this statement:

vars_files: 
  - nginx-vars.yml

When we run the playbooks, we often want to print the value of the variable, so we should use the following syntax:

- debug: var=varname

Facts

There is one specific type of variable that is called Ansible facts. For example, you might have noticed the following output at the top before your tasks when running your Ansible playbook.

    GATHERING FACTS **************************************************
    ok: [name]

When Ansible gathers facts, it connects to the node and collects various info about the host such as CPU, memory, IP address, OS, etc. This information is stored in variables called facts, and they behave just like any other variable.

The example below will gather all the facts for localhost:

- hosts: localhost
  tasks:
  - debug:
      var: ansible_facts

Result:

PLAY [localhost] ************************************************************************************************************************************************************************

TASK [Gathering Facts] ******************************************************************************************************************************************************************
ok: [localhost]

TASK [debug] ****************************************************************************************************************************************************************************
ok: [localhost] => {
    "ansible_facts": {
        "_facts_gathered": true,
        "all_ipv4_addresses": [
            "172.39.0.1",
        ],
        "all_ipv6_addresses": [
       PLAY [localhost] ************************************************************************************************************************************************************************

TASK [Gathering Facts] ******************************************************************************************************************************************************************
ok: [localhost]

TASK [debug] ****************************************************************************************************************************************************************************
ok: [localhost] => {
    "ansible_facts": {
        "architecture": "x86_64",
        "bios_date": "07/11/2011",
        "bios_version": "A06",
...

Example to display OS distribution fact

For example, we can write this Playbook and print out the facts to find the Linux build.

- name: Display OS distribution 
  hosts: all
  gather_facts: True
  tasks:
    - debug: var=ansible_distribution

The output will therefore show the facts that we are looking for:

TASK: [debug var=ansible_distribution]****************************** 
ok: [srv1] => {
    "ansible_distribution": "SUSE" 
}
ok: [server2] => {
     "ansible_distribution": "Debian"
}

What are Ansible facts used for?

Well, as you know, package names differ among distributions. So, for example, the Apache webserver package is httpd for some distributions, while for other distributions, it’s named apache2.

Consequently, Ansible facts allow you, amongst others, to make your playbook distribution agnostic. For example, the script below will collect Ansible facts and then perform the required task accordingly. When installing on non-Debian systems, it automatically skips the Debian-based task and vice versa.

- hosts: all
  tasks:
  - package:
      name: "httpd"
      state: present
    when ansible_facts["os_name"] == "non-debian"
  - package:
      name: "apache2"
      state: present
    when ansible_facts["os_name"] == "Debian"

Frequently asked question(s)

Which module is used to gather Ansible facts?

Ansible has a module called: gather_facts. This module runs by default at the start of each playbook to gather the facts about remote hosts. 

Wrapping up

In conclusion, we looked at three important concepts about the Ansible inventory file, variables, and facts. Firstly, these tools make your playbooks reusable and applicable in many scenarios and infrastructure. Lastly, using them will help you write abstract automation flows, which will work in some infrastructures.

You may also be interested in

About Anto Online

Anto, a seasoned technologist with over two decades of experience, has traversed the tech landscape from Desktop Support Engineer to enterprise application consultant, specializing in AWS serverless technologies. He guides clients in leveraging serverless solutions while passionately exploring cutting-edge cloud concepts beyond his daily work. Anto's dedication to continuous learning, experimentation, and collaboration makes him a true inspiration, igniting others' interest in the transformative power of cloud computing.

View all posts by Anto Online

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.