Ansible: From Beginner to Abandonment (Part 23)

Ansible Variable Usage

Ansible can improve the efficiency of writing Playbooks through the use of variables. For example, when installing packages with Ansible, you can write the package names into a variable and then call that variable in the Playbook. This way, when you need to modify the packages to be installed, you only need to change the variable’s value without modifying the Playbook itself.

Ansible also has some built-in variables, which can be found in the later sections on facts and magic variables.

Setting Variables

There are several ways to set variables, the simplest being to set them directly in the Playbook:

- name: var
  hosts: all
  vars:
    list_var:
    - servera
    - serverb
    dict_var:
      servera_info:
        username: root
        password: redhat
      serverb_info:
        username: admin
        password: redhat

<span>list_var</span> is a list variable, while <span>dict_var</span> is a dictionary variable.

The above sets global variables, but you can also set them under individual tasks:

- name: print var
  ansible.builtin.debug:
    var: test_var
  vars:
    test_var: "Hello World!"

You can also set variables using a file. Assume there is a file <span>vars/vars.yml</span> in the directory where the Playbook is located:

- name: var
  hosts: all
  vars_files:
  - vars/vars.yml

<span>vars/vars.yml</span> file content:

list_var2:
- var1
- var2
dict_var2:
  ssh_username: root
  ssh_password: redhat

You can also set variables via the command line:

[root@study ansible]# ansible localhost -m debug -a 'var=username' \
    --extra-vars '{"username":"root","password":"redhat"}'
localhost | SUCCESS => {
    "username": "root"
}

# If the variable involves many special characters, it is recommended to set it via a file and reference it
[root@study ansible]# cat vars/vars.yml
list_var2:
- var1
- var2
dict_var2:
  ssh_username: root
  ssh_password: redhat
[root@study ansible]# ansible localhost -m debug -a 'var=list_var2' \
    -e '@vars/vars.yml'
localhost | SUCCESS => {
    "list_var2": [
        "var1",
        "var2"
    ]
}

Referencing Variables

Using Variables in Playbooks

To reference a variable, simply wrap it with <span>{{ }}</span>. Below is a Playbook that uses existing variables to set a new variable:

vars:
  list_var:
  - servera
  - serverb
  dict_var:
    servera_info:
      username: root
      password: redhat
    serverb_info:
      username: admin
      password: redhat
  servera_ssh_info: "username: {{ dict_var.servera_info.username }}, password: {{ dict_var.servera_info.password }}"

Printing Variables

You can print variables using <span>ansible.builtin.debug</span>:

tasks:
- name: print var
  ansible.builtin.debug:
    var: list_var[1]
- name: print var
  ansible.builtin.debug:
    var: dict_var['servera_info']

<span>list_var[1]</span> prints the second item of the list variable, while <span>dict_var['servera_info']</span> prints the dictionary variable where the KEY is <span>servera_info</span>.

Dictionary variables support nested variable references. The single quotes in <span>dict_var['servera_info']</span> indicate that <span>server_info</span> is a regular string. If you omit the single quotes (<span>dict_var[servera_info]</span>), <span>servera_info</span> would be treated as a variable name, leading to different references. See the example below.

Here is an example of variable nesting:

- name: var
  hosts: all
  gather_facts: false
  vars:
    servera_info: servera
    dict_var:
      servera:
        username: redhat
        password: redhat
  tasks:
  - name: print var
    ansible.builtin.debug:
      var: dict_var[servera_info]

<span>servera_info</span> has the value <span>servera</span>, and then through <span>dict_var[servera_info]</span> you can access the value of <span>dict_var['servera']</span><code><span>.</span>

Registering Variables

Unlike setting variables, registering variables involves capturing the results of module execution into a variable for later reference.

Registering a variable uses <span>register</span>.

Below is an example of registering a variable:

- name: register
  hosts: localhost
  gather_facts: false
  tasks:
  - name: register
    ansible.builtin.command: ls /tmp
    register: get_tmp
    changed_when: false
  - name: print register var
    ansible.builtin.debug:
      var: get_tmp
  - name: if testfile not in /tmp, create /tmp/testfile
    ansible.builtin.file:
      path: /tmp/testfile
      state: touch
    when: "'testfile' not in get_tmp.stdout_lines"

The first tasks in this Playbook will execute <span>ls /tmp</span> on the managed node and register the result into a variable named <span>get_tmp</span>. The second tasks can output the content of the <span>get_tmp</span> variable, and the third tasks checks whether the <span>/tmp</span> directory contains a <span>testfile</span>. If it does not, it creates it; otherwise, it skips this tasks.

Referencing Variables via Jinja2

Jinja2 is a Python template engine used to insert dynamic content into text, commonly used for generating HTML, configuration files, YAML, Shell scripts, etc. It is often used in conjunction with <span>ansible.builtin.template</span>.

For example, configuring an HTTP server. If the HTTP server is a single node, the listening port is 80. If it is a multi-node load balancer, the listening port is set to 8080. If neither applies, use 8443. Using Jinja2, you can write a conditional statement as follows (snippet):

{% if NODE_TYPE == 'SINGLE' %}
Listen 80
{% elif NODE_TYPE == 'LOAD_BALANCING' %}
Listen 8080
{% else %}
Listen 8443
{% endif %}

Here, <span>NODE_TYPE</span> is a variable that needs to be set in the inventory or Playbook.

The above example uses a conditional statement, but Jinja2 also supports loops and variables. Below is an example of a loop:

127.0.0.1   localhost localhost.localdomain localhost4 localhost4.localdomain4
::1         localhost localhost.localdomain localhost6 localhost6.localdomain6

{% for host in groups['all'] %}
{{ hostvars[host]['ansible_ssh_host'] }}        {{ hostvars[host]['ansible_facts']['fqdn'] }}   {{ hostvars[host]['ansible_facts']['hostname'] }}
{% endfor %}

This example not only uses a loop but also variables. Jinja2 supports using Ansible’s facts, magic variables, and custom variables.

The above Jinja2 file, when copied, produces the following effect:

[root@ansible-controller ansible-navigator]# cat templates/hosts.j2
127.0.0.1   localhost localhost.localdomain localhost4 localhost4.localdomain4
::1         localhost localhost.localdomain localhost6 localhost6.localdomain6

{% for host in groups['all'] %}
{{ hostvars[host]['ansible_ssh_host'] }}        {{ hostvars[host]['ansible_facts']['fqdn'] }}   {{ hostvars[host]['ansible_facts']['hostname'] }}
{% endfor %}

[root@ansible-controller ansible-navigator]# cat template.yml
---
- name: template
  hosts: all
  tasks:
  - name: template
    ansible.builtin.template:
      src: ./templates/hosts.j2
      dest: /etc/hosts

[root@ansible-controller ansible-navigator]# ansible-navigator run --ep template.yml -- -b -K
BECOME password:
...output omitted...

[root@ansible-controller ansible-navigator]# ansible-navigator exec -- ansible all -m command -a 'cat /etc/hosts'
master1 | CHANGED | rc=0 >>
127.0.0.1   localhost localhost.localdomain localhost4 localhost4.localdomain4
::1         localhost localhost.localdomain localhost6 localhost6.localdomain6

192.168.221.142 master1.example.com     master1
192.168.221.143 worker1.example.com     worker1
worker1 | CHANGED | rc=0 >>
127.0.0.1   localhost localhost.localdomain localhost4 localhost4.localdomain4
::1         localhost localhost.localdomain localhost6 localhost6.localdomain6

192.168.221.142 master1.example.com     master1
192.168.221.221 worker1.example.com     worker1

When creating a Jinja2 file, it is recommended to use the <span>.j2</span> suffix (e.g., <span>hosts.j2</span>).

Ansible Facts Variables

Viewing Ansible Facts Variables

Ansible can collect information from managed hosts and register it as facts variables, with the variable name <span>ansible_facts</span>. You can print the facts variables of the managed host using the <span>ansible.builtin.debug</span> module:

- name: Print all available facts
  ansible.builtin.debug:
    var: ansible_facts

Ansible facts variables can provide a lot of information:

  • • Hardware: CPU, memory, motherboard, etc.
  • • Software: addresses, hostnames, filesystem mounts, etc.

For reference on Ansible facts variable output, see: https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_vars_facts.html#ansible-facts.

In addition to obtaining facts variables through <span>ansible_facts</span>, you can also access specific host facts variables using <span>hostvars['servera']['ansible_facts']</span>. This method can only be used within a Playbook.

Caching Ansible Facts Variables

By default, Ansible collects facts variables during Playbook execution and stores them in memory. After the Playbook execution ends, the collected facts variables are cleared. You can cache the variables to a file or a specific database. Here are two caching methods:

  • • Cache to file
  • • Cache to Redis

Caching to File

Modify the following lines in the Ansible configuration file:

# (string) Chooses which cache plugin to use, the default 'memory' is ephemeral.
fact_caching=jsonfile

# (string) Defines connection or path information for the cache plugin.
fact_caching_connection=./cache

# (string) Prefix to use for cache plugin files/tables.
fact_caching_prefix=ansible_facts_

# (integer) Expiration timeout for the cache plugin data.
fact_caching_timeout=86400

These lines indicate that facts variables will be cached in JSON format to a file in the <span>./cache</span> directory, with filenames prefixed by <span>ansible_facts_</span> and a cache validity of 86400 seconds.

Regarding cache expiration, if cached to a file, Ansible will check whether the cache is within the validity period during Playbook execution. If it is not within the validity period, it will not use the cache.

Testing the cache:

[root@study ansible]# ls cache/
ls: cannot access 'cache/': No such file or directory
[root@study ansible]# ansible localhost -m setup &amp;&gt; /dev/null
[root@study ansible]# ls cache/
ansible_facts_localhost
[root@study ansible]# cat ansible_facts_cache.yml
- name: test ansible facts cache
  hosts: localhost
  gather_facts: false
  tasks:
  - name: Print ansible facts
    ansible.builtin.debug:
      var: ansible_facts['hostname']
[root@study ansible]# ansible-playbook ansible_facts_cache.yml

PLAY [test ansible facts cache] ***********************************************************************************************************************************

TASK [Print ansible facts] ****************************************************************************************************************************************
ok: [localhost] => {
    "ansible_facts['hostname']": "study"
}

PLAY RECAP ********************************************************************************************************************************************************
localhost                  : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

Caching to Redis

To cache facts variables to Redis, you need to install the <span>community.general</span> collection by executing <span>ansible-galaxy collection install community.general</span>.

Check the newly installed cache plugins:

[root@study ansible]# ansible-doc -t cache -l
ansible.builtin.jsonfile    JSON formatted files
ansible.builtin.memory      RAM backed, non persistent
community.general.memcached Use memcached DB for cache
community.general.pickle    Pickle formatted files
community.general.redis     Use Redis DB for cache
community.general.yaml      YAML formatted files

Install and start Redis:

# Install Redis
[root@study ansible]# dnf install -y redis

# Start Redis
[root@study ansible]# systemctl start redis
[root@study ansible]# ss -ntlp | grep 6379
LISTEN 0      511        127.0.0.1:6379       0.0.0.0:*    users:(("redis-server",pid=45313,fd=6))

# Check Redis data
[root@study ansible]# redis-cli
127.0.0.1:6379> KEYS *
(empty list or set)
127.0.0.1:6379> exit

Be cautious when using <span>KEYS *</span>.

Modify the Ansible configuration file:

# (string) Chooses which cache plugin to use, the default 'memory' is ephemeral.
fact_caching=redis

# (string) Defines connection or path information for the cache plugin.
fact_caching_connection=127.0.0.1:6379:0

# (string) Prefix to use for cache plugin files/tables.
fact_caching_prefix=ansible_facts_

# (integer) Expiration timeout for the cache plugin data.
fact_caching_timeout=86400

When expired, the information in Redis will be deleted.

Collect facts variables and check the cache:

[root@study ansible]# ansible localhost -m setup &amp;&gt; /dev/null
[root@study ansible]# redis-cli
127.0.0.1:6379> KEYS *
1) "ansible_facts_localhost"
2) "ansible_cache_keys"
127.0.0.1:6379> exit
[root@study ansible]#

Additionally, since Redis can be password-protected and supports encryption, the configuration for <span>fact_caching_connection</span> can be found in the documentation for <span>community.general.redis</span>:

[root@study ansible]# ansible-doc -t cache community.general.redis
...output omitted...
= _uri
        A colon separated string of connection information for Redis.
        The format is `host:port:db:password', for example `localhost:6379:0:changeme'.
        To use encryption in transit, prefix the connection with `tls://', as in `tls://localhost:6379:0:changeme'.
        To use redis sentinel, use separator `;', for example `localhost:26379;localhost:26379;0:changeme'. Requires redis>=2.9.0.
        set_via:
          env:
          - name: ANSIBLE_CACHE_PLUGIN_CONNECTION
          ini:
          - key: fact_caching_connection
            section: defaults
        type: string

Disabling Ansible Facts Variables

By default, Ansible collects facts variables during Playbook execution. If you explicitly do not want to use facts variables, you can disable the collection of facts variables by setting <span>gather_facts: false</span> to improve execution efficiency.

- hosts: whatever
  gather_facts: false

Adding Custom Facts

You can add custom facts variables in the <span>/etc/ansible/facts.d</span> directory on the managed nodes.

Below is the definition and viewing of custom variables:

[root@study ansible]# cat /etc/ansible/facts.d/local_var.fact
[webserver_vars]
linux = true
webserver_port = 80
dbserver_port = 3306
[root@study ansible]# ansible localhost -m setup -a 'filter=ansible_local'
localhost | SUCCESS => {
    "ansible_facts": {
        "ansible_local": {
            "local_var": {
                "webserver_vars": {
                    "dbserver_port": "3306",
                    "linux": "true",
                    "webserver_port": "80"
                }
            }
        }
    },
    "changed": false
}

<span>local_var</span>, <span>webserver_vars</span>, and <span>webserver_port</span> correspond to different KEY names.

Below is how to call local facts:

- name: Print custom facts
  ansible.builtin.debug:
    var: ansible_facts.ansible_local

The KEY of facts variables will convert uppercase KEY names to lowercase. For example, <span>WebServer_Port = 80</span> will convert to <span>webserver_port = 80</span>.

Ansible Magic Variables

Calling Magic Variables

Unlike facts variables, magic variables do not require information to be collected from managed hosts. They mainly come from two sources:

  • • Variables set through the inventory, which can be new variables added in the inventory or built-in Ansible variables (connection variables: <span>ansible_ssh_user</span> and <span>ansible_ssh_password</span>)
  • • Variables added during Playbook execution

Here are some commonly used magic variables:

  • <span>hostvars</span>
  • <span>groups</span>
  • <span>group_names</span>
  • <span>inventory_hostname</span>

These variables are all included in <span>hostvars</span>, which can be viewed using the <span>ansible.builtin.debug</span> module (<span>ansible localhost -m debug -a 'var=hostvars'</span>).

Here are some examples of using magic variables:

Getting Variable Information for a Specific Host

{{ hostvars['test.example.com']['ansible_facts']['distribution'] }}

Looping Through Host Groups

{% for host in groups['app_servers'] %}
   {{ hostvars[host]['ansible_facts']['eth0']['ipv4']['address'] }}
{% endfor %}

Checking if Host servera is in the webserver Group

{% if 'servera' in groups['webserver'] %}
   {{ hostvars[host]['ansible_facts']['eth0']['ipv4']['address'] }}
{% endif %}

In addition to the variables seen in <span>hostvars</span>, there are also the following variables:

  • <span>ansible_play_hosts</span>
  • <span>ansible_play_batch</span>
  • <span>role_path</span> (contains the path name of the current role and only works within the role).

Examples of Calling Magic Variables

Now assume there is a batch of hosts (hostnames and IP addresses configured, IP addresses uniformly configured on the eth0 network card), you can configure the <span>/etc/hosts</span> file using the following Playbook:

[root@study ansible]# cat set_hosts.yml
- name: set all hosts
  hosts: all
  gather_facts: true
  tasks:
  - name: Configure the hosts file with the template
    ansible.builtin.template:
      src: ./templates/hosts.j2
      dest: /etc/hosts
      owner: root
      group: root
      mode: '0644'

[root@study ansible]# cat templates/hosts.j2
127.0.0.1   localhost localhost.localdomain localhost4 localhost4.localdomain4
::1         localhost localhost.localdomain localhost6 localhost6.localdomain6

{% for host in groups['all'] %}
{{ hostvars[host]['ansible_facts']['eth0']['ipv4']['address'] }}        {{ hostvars[host]['ansible_facts']['hostname'] }}
{% endfor %}

This is done using the <span>Jinja2</span> template and the <span>ansible.builtin.template</span> module to configure the <span>/etc/hosts</span> file.

  1. 1. The <span>./templates/hosts.j2</span> file is a <span>Jinja2</span> template that loops through the <span>all</span> host group, printing the IP addresses and hostnames of all hosts in the group line by line.
  2. 2. Ansible then uses the <span>ansible.builtin.template</span> module to execute this <span>Jinja2</span> template on the <span>all</span> host group.

Leave a Comment