Ansible 101: #005 - Variables

Hello and welcome back to the Ansible Starter Guide. In this fifth part of the series, I want to show you how variables work and how we can use them to deal with differences between different systems.

The simplest use of variables is to store multiple values (such as file paths) in one variable.

Let's start with an example. Let's say we want to serve an index.html and a style.css to our web server. For the sake of clarity, I'm going to put the whole thing in its own playbook. You can also just extend the webservers.yml from the last part.

- name: Play without variables
  hosts: webservers
  tasks:
    - name: copy index.html
      ansible.builtin.copy: 
        src: files/index.hmlt
        dest: /var/www/html/index.html
      become: true
      
   - name: copy style.css
     ansible.builtin.copy:
       src: files/style.css
       dest: /var/www/html/style.css
     become: true

~/ansible-guide/playbook-without-vars.yml

The further we build our playbook, the more we will need to use the "/var/www/html" path. It would be great if we could just put this in a variable. So let's do that:

- name: Play with variable
  hosts: webservers
  vars:
    my_docroot: /var/www/html 
  tasks:
    - name: copy index.html
      ansible.builtin.copy: 
        src: files/index.hmlt
        dest: "{{ my_docroot }}/index.html"
      become: true
      
   - name: copy style.css
     ansible.builtin.copy:
       src: files/style.css
       dest: "{{ my_docroot }}/style.css"
     become: true

~/ansible-guide/playbook-with-vars.yml

This is the simplest definition of a variable. We have simply defined the variable "my_docroot" in the playbook and can now access it throughout the whole "Playing with Variables" piece. To use variables we use the following format:

"{{ variable_name }}"

It is important to note that we must enclose the entire string in quotes ("). Variables can also be defined at task level. This would look like this:

- name: Play mit Variablen auf Taskebene
  hosts: webservers
  tasks:
    - name: copy index.html
      ansible.builtin.copy: 
        src: files/index.html
        dest: "{{ my_docroot }}/index.html"
      become: true
      vars:
        my_docroot: /var/www/html
      
   - name: copy style.css
     ansible.builtin.copy:
       src: files/style.css
       dest: "{{ my_docroot }}/style.css"
     become: true
     vars:     
       my_docroot: /var/www/html

However, this only makes sense in very special cases, when only one specific task actually requires that variable.

In the examples above, we use a variable of type "string", which is a simple string of characters. However, there are other types of variables that we will discuss in more detail later in this guide.

Variable types

# Strings 
my_string: Hello World!

# Numbers
number_of_files: 8

# Float - we always use this when floating point numbers are involved
my_float: 2.5

# Lists - Lists containing one or more entries
files_to_copy:
  - /path/to/file1.txt
  - /path/to/file2.txt

# Dictionary - List with structured data
files_to_copy:
  - name: first file
    path: /path/to/file1.txt
    copy_to: /new/file/path1.txt
  - name: second file
    path: /path/to/file2.txt
    copy_to: /new/file/path2.txt

# Boolean - True ore False
enable_service: true
overwrite_config: false
    

Host and Group Variables

The variable defined in the first example now applies to all hosts with the same value. But what do we do if we want to define different values for each host? Let's return to our example scenario. Let's assume that the content of index.html of our two web servers should be different for each host, so that the first one outputs "I am webserver1" and the second one outputs "I am webserver2".

To do this, we use host variables (host_vars). To use them, we create a new directory in our example setup:

cd ~/ansible-guide
mkdir -p host_vars/ansible-guide-1
mkdir -p host_vars/ansible-guide-2

In each of the created directories we add a file "main.yml".

my_welcome_text: I am webserver 1

~/ansible-guide/host_vars/ansible-guide-1/main.yml

my_welcome_text: I am webserver 2

~/ansible-guide/host_vars/ansible-guide-2/main.yml

When we start our playbook, Ansible will automatically read all the files under "host_vars" and assign the variable values to the appropriate hosts. It is important that the names of the directories match the names of the hosts in our inventory.

To test this, we need to make a small adjustment to our web server playbook. Using the "copy" module, we can define the desired contents of the target file directly, instead of using a source file. It will then look like this:

- name: webserver setup
  hosts: webservers
  tasks:
    - name: Apache2 Setup
      ansible.builtin.apt:
        name: apache2
        state: present
        update_cache: true
      become: true

    - name: start and enable apache2
      ansible.builtin.systemd:
        name: apache2
        state: started
        enabled: true
      become: true

    - name: copy index.html
      ansible.builtin.copy:
        dest: /var/www/html/index.html
        content: "{{ my_welcome_text }}"
      become: true

    - name: copy apache2.conf
      ansible.builtin.copy:
        src: files/apache2.conf
        dest: /etc/apache2/apache2.conf
      become: true

~/ansible-guide/webservers.yml

Notice the change in the "copy index.html" task. Instead of defining a source file for the copy module, we enter the desired content of the target file directly, using our variable "my_welcome_text".

Then let's run the playbook:

cd ~/ansible-guide
ansible-playbook -i inventory.txt webservers.yml
PLAY [webserver setup] ****************************************************************************

TASK [Gathering Facts] ****************************************************************************
ok: [ansible-guide-1]
ok: [ansible-guide-2]

TASK [Apache2 Setup] ****************************************************************************
ok: [ansible-guide-1]
ok: [ansible-guide-2]

TASK [start and enable apache2] ****************************************************************************
ok: [ansible-guide-1]
ok: [ansible-guide-2]

TASK [copy index.html] ****************************************************************************
changed: [ansible-guide-1]
changed: [ansible-guide-2]

PLAY RECAP ****************************************************************************
ansible-guide-1          : ok=4    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
ansible-guide-2          : ok=4    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

The "copy index.html" task now has the status "modified" on both hosts, and has thus adopted the new content for index.html. We can now check again with curl or the browser to see if we have achieved the desired effect:

curl http://192.168.0.11
<h1>I am webserver1!</h1>

curl http://192.168.0.12
<h1>I am webserver2!</h1>

BAM! Looks good! So we successfully used host-specific variables.

The whole thing works with groups as well. For this we use group_vars. We also create a new playbook for this and use the useful debug module to test the variables.

In part 4 of this guide, we created an inventory with two different groups, "webservers" and "db". So we will create a playbook that uses exactly those two.

- name: group_vars showcase
  hosts:
    - webservers
    - db
  tasks:
    - name: Debug Ausgabe
      debug:
        msg: "{{ test_text }}"

~/ansible-guide/groupvars-test.yml

The debug module is very useful when we are creating playbooks and want to test them in between. For example, we can use it to display the values of variables. In our case, we define a parameter for the module:

msg: Any string that will be output during the play.

Now we just need to define the variable "test_text". Differently for each group. We create another directory on the same level as "host_vars":

mkdir ~/ansible-guide/group_vars
mkdir ~/ansible-guide/group_vars/webservers
mkdir ~/ansible-guide/group_vars/db

In both directories we add a "main.yml":

test_text: I am a host in the group 'webservers'!

~/ansible-guide/group_vars/webservers/main.yml

test_text: I am a host in the group 'db'

~/ansible-guide/group_vars/db/main.yml

So we have done the same as with the host_vars, only this time at group level. Now let's run the new playbook:

cd ~/ansible-guide
ansible-playbook -i inventory.txt groupvars-test.yml
ASK [Gathering Facts] ****************************************************************************
ok: [ansible-guide-1]
ok: [ansible-guide-2]
ok: [ansible-guide-3]

TASK [Debug Ausgabe] ****************************************************************************
ok: [ansible-guide-1] => {
    "msg": "I am a host in the group 'webservers'!"
}
ok: [ansible-guide-2] => {
    "msg": "I am a host in the group 'webservers'!"
}
ok: [ansible-guide-3] => {
    "msg": "I am a host in the group 'db'!"
}
PLAY RECAP ****************************************************************************
ansible-guide-1                  : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
ansible-guide-2                  : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
ansible-guide-3                  : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

As we can see above, each of the three hosts now pulls the group-specific variable "test_text" and outputs it accordingly.

Summary

In this chapter you have learned what variables are and how we can define them in different places. In the next chapter I will introduce you to facts. These are very similar to variables, but there are some differences!

See you then!

Mow