Ansible, part IV – Variables and Jinja2 Templates

Variables are used to store various types of data. Their values usually depend on the location or context in which they are located. Variables can be completely independent entities or grouped together with other variables, to describe or parameterize a given object. For this purpose, we can use the structure of a dictionary, which groups variables called keys and allows you to assign values to them.

An example of such a dictionary structure in YAML can be seen below:

Variables must start with a letter, and may additionally contain numbers and the underscore character “_”. YAML allows you to reference variables within a dictionary by using the period “.” or square brackets “[]”:

Above, in both cases we referenced the value “2001: db8: c1sc0 :: a“. While the second is safer, the first is more often used for convenience. If we decide to use the dot “.”, it is worth checking the limitations described in the Ansible documentation.

Variables are most often used to parameterize hosts and inventory groups. Thus, they can be handled by Ansible based on the values assigned to the various variables. As the static inventory is most often created in the INI file format, we are only referring to it at the moment. When we want to assign variables directly to a host, they are given in the format “key=value“, on the same line as the host definition. Here is an example of an inventory with variables assigned directly to hosts:

In our example, we used the names “web1” and “web2“. If the given hostnames are not FQDN (Fully Qualified Domain Name) or IP resolvable names, then we should use the “ansible_host” variable. It indicates the IP address to which the connection will be established. By default, an attempt will be made to establish an SSH connection to port 22. If you want to change the port number, you must append it after a colon “:” to the hostname or IP address, or use the “ansible_port” variable.

In practice, the variables are most often defined within groups. They are then located in a special section called by the name of the group with the suffix “:vars“. In the variable section, one variable per line is provided. Using group level variables does not exclude the possibility of using other variables or even overwriting the same variables at the host level. This is shown below:

In our example you can see the definition of the “ansible_port” variable for all hosts in the “web_servers” group. However, for host “10.8.232.124” a different port will be used as this has been overridden at the host level.

Variable definitions can span multiple scopes, that is all hosts, parent or child groups, as well as a single host. The definition of a variable within a more specific scope has higher priority. Only when it is absent, the available definitions in wider ranges are checked. In other words, as long as child groups inherit the values ​​of the variables of the parent groups, a more precise definition always applies. Hence, at the child group level, you can override the parent group variables. If the same host belongs to many groups of the same level, the groups are arranged alphabetically and each subsequent group on the list overwrites the variables of the previous ones. It is also possible to manually set the priority for each group.

Please also note, that we may have multiple inventories. There too, the definitions of hosts and groups as well as their variables can duplicate. Both the “ansible” and “ansible-playbook” commands allow you to specify several “-i” or “–inventory” options, and as arguments you can also specify a directory with multiple inventory files (static and dynamic simultaneously). In the event that we use several inventories, each subsequent inventory on the list overwrites the variables of the previous one. When a directory is given, its files are in alphabetical order as for groups. Hence, it is then recommended to start the inventory file names with numbers, which makes the whole thing clearer.

In the case of complex environments and a large number of managed nodes, it is recommended to keep the variables out of the inventory in separate files per group and per host. The directory “group_vars/” (group variables) and the directory “host_vars/” (host variables) are used for this purpose, respectively. Ansible tries to find these directories based on the location of the inventory file (“ansible” command) or the current directory (“ansible-playbook” command). The names of these files correspond to the names of groups or hosts. The contents of the files must conform to the YAML syntax. These files do not have to have any extension or have to have the extension: “.yml“, “.yaml” or “.json“.

In our example, the “group_vars/web_servers” file should contain:

Above you can see three dashes “” and the typical YAML mapping “key: value“. Variables inside the playbook can also be created in the same way. It is also possible to point to entire files and directories with variables inside it. Also, additional variables may be passed as arguments to the “-e” or “–extra-vars” options of the “ansible” and “ansible-playbook” commands. There are many possibilities, but fortunately we do not need to know them all. The full list of variable sources and their priorities is available in the Ansible documentation.

The variables are referenced through the Jinja2 template system, known to many from the Python language. It is very flexible. It supports both conditional expressions and loops. We reference the value of a variable in Jinja2 by placing its name inside double curly braces “{{ }}“.

In the previous article, we mentioned the possibility of using the collected information from managed nodes to build more universal playbooks. This information is called facts. They are obtained during the “Gathering Facts” quest. It is always launched by default at the beginning of every game. During this, Ansible creates the appropriate variables for each of the nodes that can be referenced in the playbook and Jinja2 templates. The number of these variables is very large. The easiest way to view them all is to use the ad-hoc command with the “setup” module mentioned in the previous article in this series.

In a playbook, we usually refer to selected facts in order to condition the way of performing a given task on the value that is contained in them. It also happens that the collected facts are used as configuration values for various services or for building files. Facts can also be used to generate notifications both on the playbook exit, as well as those sent by e-mail or to the Cisco Webex Teams room.

While the playbook is running, the “debug” module is used to display the values of variables, including facts.

We can use one of the two options of the “debug” module to display the information: “var” or “msg“. They are mutually exclusive, so you have to choose:

  • var” only displays the value of the specified variable and does not require the explicit use of curly braces around the variable name “{{ }}“.
  • msg” displays a message prepared by us, in which if variables are used, they should be explicitly placed inside double curly braces “{{ }}” – according to the Jinja2 format.

The “debug” module also provides the “verbosity” parameter, which determines the minimum debug level at which this information will be displayed for us. By default, “verbosity” is set to 0, which causes the information to be displayed every time the playbook is called.

Above you can see the result of making a very simple and short playbook, the content of which is slightly above it. It uses the “debug” module discussed above to display variables while the playbook is running.

Next, we will follow a more complex case of using variables and the Jinja2 template. We will be working on a very simple inventory that only defines 4 hosts.

None of these 4 hosts have an HTTP service running:

The task of our playbook will be to configure these 4 hosts to work as web servers. To do this, they must have the “httpd” software, an appropriately parameterized “index.html” file, an active “httpd” service and an open port for this service in the system firewall. The “index.html” file is parameterized as each of the web servers is to provide slightly different content. For this purpose, we used the Jinja2 template. Jinja2 template files should have the “.j2” extension.

In place of the Jinja2 template “index.html.j2variables, there should be appropriate values that will be collected during the “Gathering Facts” task. They can be different for each host. The “template” module is used to handle Jinja2 templates, the “yum” module to install the necessary software, the “service” module to start the service, and the “firewalld” module to open the appropriate port in the firewall. At this stage, I encourage you to read the output of the “ansible-doc” command for these modules.

The above playbook consists of two sets of tasks (“plays”). The first deals with the configuration of web servers, and the second with the verification of what has been done. The second set is executed on “localhost” and has the “Gathering Facts” task disabled. In the first task, we assigned the contents of our inventory file “./inventory” to the variable “web_servers“. For this purpose, we have recorded the result of the appropriate “command” under this variable. The “register” parameter is used for this.

In the second task of the second set of tasks, we use the “uri” module to establish an HTTP connection to each of our web servers in turn. The success of this task depends on the received HTTP code. By default, the code 200 means success. Performing these tests from the “localhost” management station ensures that the “httpd” service is working and the appropriate port on the firewall has been opened.

The newly registered “web_servers” variable consists of many lines. Each of them is accessed via the “web_servers.stdout_lines” list. When we give this list as an argument to the “loop” expression, the module “uri” will be executed as many times as there are values in the list. In each subsequent invocation of the module “uri“, the variable “item” will take the value of the next value in the list, which is an argument of the expression “loop“. This is the first time we’ve used a loop in a playbook. We’ll cover loops more in the next article.

You can see in “PLAY RECAP” that all the tasks were successful.

Now we can repeat our test from the beginning, and check the content returned by the web servers.

Here we just covered the basics of Jinja2 variables and templates. They are a key element of playbooks and we will come back to them often in future articles.

Author:

CEO, Network Engineer and System Administrator at networkers.pl

Marcin Ślęczek

Marcin works as CEO, Network Engineer and System Administrator at networkers.pl, which designs and implements IT Systems, Data Centers and DevOps environments. networkers.pl also sells software and hardware for building such environments and is a partner of well-known manufacturers such as Red Hat, Cisco Systems, IBM, Storware and VMware.