Ansible Conditionals Lab

In this lab, we will work through exercises to help us understand how to use conditional elements in our playbooks to allow for decisions to be made based on information provided. Conditionals provide a way for you to build more advanced playbooks that are efficient and streamlined.

Goals

Lab Setup

  1. Ensure you are in the ciq-basics directory

  2. Create a folder for this lab, let’s call it lab01

You are now ready to start the lab.

Registering Variables

Often in a playbook it may be useful to store the result of a given command in a variable and access it later. Use of the shell module in this way can in many ways eliminate the need to write site specific facts, for instance, you could test for the existence of a particular program.

The register module decides what variable to save a result in. The resulting variables can be used in templates, action lines, or when statements. In our first exercise we will build a playbook that runs a command and registers the standard out response to a variable. We’ll then call that variable and display it in the debug output.

Create and run your playbook

  1. Create a new file called 01_register.yml:

  2. Enter the following lines into your playbook:

- name: Register a Variable and send the content to the debug log
  hosts: all
  gather_facts: false

  tasks:
      - shell: hostname
        register: my_hostname

      - debug:
          msg: "my hostname is {{ my_hostname.stdout }}"
  1. Save the playbook and run it. You will need to create a new template in Ascender to accomplish this.

Sample Output

    PLAY [Register a Variable and send the content to the debug log] ***************
    
    TASK [shell] *******************************************************************
    changed: [node-1]
    
    TASK [debug] *******************************************************************
    ok: [node-1] => {
        "msg": "my hostname is node1"
    }
    
    PLAY RECAP *********************************************************************
    node-1                     : ok=2    changed=1    unreachable=0    failed=0

If all things go well, you should see output that looks similar to the sample output above. Note that in log output we see the debug task displaying our message with our registered variable included.

Things to try

Using Facts

Registering variables is great but what’s even better is to have some mechanism for Ansible to gather a baseline of information about the systems we are automating and keep track of them so that you don’t have to run commands. You can do this with gather_facts. You will note that in our previous playbook we had a line in this playbook that told ansible to NOT gather facts. In this next exercise we are going to turn that off and see what Ansible discovers about the system.

Copy and run your playbook

  1. Copy your existing playbook from 01_register.yml to 02_facts.yml:

  2. Edit 02_facts.yml and change the contents to look like the playbook below:

- name: Use gather_facts to send the content to the debug log
  hosts: all
  gather_facts: true

  tasks:

    - debug:
        msg: "my hostname is {{ ansible_fqdn }}"

As you see we changed gather_facts to be true. We remove the need of setting a variable and we changed our debug message to use a new variable. Let’s run the playbook and see what happens.

  1. Save the playbook and run it. You can create a new template in Ascender to accomplish this, or you can edit your old one and change the playbook used.

Sample Output

    PLAY [Register a Variable and send the content to the debug log] ***************
    
    TASK [Gathering Facts] *********************************************************
    ok: [node-1]
    
    TASK [debug] *******************************************************************
    ok: [node-1] => {
        "msg": "my hostname is node1.example.com"
    }
    
    PLAY RECAP *********************************************************************
    node-1                     : ok=2    changed=0    unreachable=0    failed=0

Isn’t that interesting, we removed code and didn’t set a variable, yet we were able to use a variable to bring in information?! The reason we can do this is because when we set gather_facts to be true, we were telling Ansible that we want it to go out and find out all the information it can about the systems we are managing. This is a very good way of making your automation more efficient. In this example I told you what the fact name was that would give us the fully qualified name of the host. You might be asking yourself, how do I find out all the other facts about this host? One way to do this is to run the Ansible setup module as an ad-hoc command.

  1. From Ascender, click on the hostname in the Gathering Facts task in the output above in Ascender, and it will pop up a window. Then change to the JSON tab.

Sample Output

    ...
    "type": "ether"
    },
    "ansible_fips": false,
    "ansible_form_factor": "Other",
    "ansible_fqdn": "node1.example.com",
    "ansible_hostname": "node1",
    "ansible_interfaces": [
      "lo",
      "eth0"
    ],
    "ansible_is_chroot": true,
    "ansible_iscsi_iqn": "",
    "ansible_kernel": "3.10.0-327.36.3.el7.x86_64",
    "ansible_lo": {
      "active": true,
      "device": "lo",
      "features": {
          "busy_poll": "off [fixed]",
          "fcoe_mtu": "off [fixed]",
    ...

You should see a long list of JSON formatted data speed by. You can scroll up in your browswer and see all of the information about the system that Ansible was able to gather when we ran setup (e.g. gathered facts). I snipped out some of the data so you can see where I found the ansible_fqdn variable that contained the data i wanted. What is great is that you will see that we are gathering facts about every machine we are managing. So if you had 200 servers in your inventory we would have all of that information and could use it to uniquely automate each host.

Things to try

Using When

Sometimes you will want to skip certain tasks you your playbook depending on certain criteria. This could be something as simple as not installing a certain package if the operating system is a particular version, or it could be something like performing some cleanup steps if a filesystem is getting full. This is easy to do in Ansible with the when clause. As you might guess, when allows you to determine when an action runs. Let’s take a look at that in action.

Create and run your playbook

  1. Create a new file called 03_when.yml:

  2. Enter the following lines into your playbook:

- name: Use gather_facts and when to control which block of code to use
  hosts: all
  gather_facts: true

  tasks:
    - name: "Print out a custom message when the OS is Debian"
      debug:
        msg: "Im running Debian"
      when: ansible_os_family == "Debian"

    - name: "Print out a custom message when the OS is RedHat"
      debug:
        msg: "Im running Rocky"
      when: ansible_os_family == "RedHat"

In this playbook we have created two different tasks that only run when a certain condition is met. In this case we are matching the value of the ansible_os_family fact that is gathered when we run the playbook. Run the playbook and check the output.

  1. Save the playbook and run it:

Sample Output

    PLAY [Use gather_facts and when to control which block of code to use] *********
    
    TASK [Gathering Facts] *********************************************************
    ok: [node-1]
    
    TASK [Print out a custom message when the OS is Debian] ************************
    skipping: [node-1]
    
    TASK [Print out a custom message when the OS is RedHat] ************************
    ok: [node-1] => {
        "msg": "Im running Rocky"
    }
    
    PLAY RECAP *********************************************************************
    node-1                     : ok=2    changed=0    unreachable=0    failed=0

Here we see that Ansible matched the value of the fact and then ran the appropriate block of code.

Things to try

Using Loop

There are often times when you may want to do a number of tasks on each host you are managing. For this example we can imagine being tasked with adding a number of users to our web servers. We can employ the use of a loop and iterate through a list of users, ensuring that every one is added to the system. In our exercise below we will do just that.

Create and run your playbook

  1. Create a new file called 04_loop.yml:
- name: Create multiple users using a loop
  hosts: all
  gather_facts: false
  become: true

  tasks:
    - name: add several users
      user:
        name: "{{ item }}"
        state: present
        groups: "wheel"
        password: "{{ 'Ans1bl3R0cks!' | password_hash('sha512') }}"
      loop:
        - testuser1
        - testuser2
        - testuser3

Notice that we are using the command loop and providing it a list of users below it. We are then using the variable item to provide each username to the name parameter. the combination of the loop command and the item variable we create a loop inside the user module that has the effect of creating each user. Without using loop we would have to have 3 different tasks in our playbook that each called out the testusers that we want to create. Run the playbook and let’s observe the results.

  1. Save the playbook and run it:

Sample Output

    PLAY [Create multiple users using a loop] **************************************
    
    TASK [add several users] *******************************************************
    changed: [node-1] => (item=testuser1)
    changed: [node-1] => (item=testuser2)
    changed: [node-1] => (item=testuser3)
    
    PLAY RECAP *********************************************************************
    node-1                     : ok=1    changed=1    unreachable=0    failed=0

Take note of the become: true, since we are running as a normal user, we need to elevate to the root user to run privileged commands. This parameter tells it to do so. We can do it at the top level like this to run every command elevated, or we can add this parameter per task.

Here we see quite a few changes that were made to the systems we are managing. In a few seconds we created 3 users, that is 3 changes you would be making that is probably quite easy to do by hand. However, What if we had 50 users on 100 servers, that’s 5000 changes, probably not something you would want to tackle by hand. In our case using ansible and a short and simple playbook we can execute this in a matter of a minute or two and we’d be assured that it was done perfectly every time.

Things to try

Combining Register & Loop

We now have some really good conditionals that we can use to make our playbook more efficient and more modular. In this exercise we will combine register and loop to set variables and iterate through them in loops.

Create and run your playbook

  1. Create a new file called 05_register_loop.yml:

  2. Enter the following lines into your playbook:

- name: loop through a set of registered variables
  hosts: all
  gather_facts: false

  tasks:
    - name: echo the contents of a loop
      shell: "echo {{ item }}"
      loop:
        - "one"
        - "two"
        - "three"
      register: echo

    - name: Fail if return code is not 0
      fail:
        msg: "The command ({{ item.cmd }}) did not have a 0 return code"
      when: item.rc != 0
      loop: "{{ echo.results }}"

You can see that we are defining a loop with the values one, two, three and using those to run a shell command. We then can take take the results of that and use that in another loop and combine it with a when statement to check the contents of the output. In this case we are looking for the result codes that are returned when we echo one, two and three. if the result code returned by that command failed and returned something other than 0 it would fail and spit out a failure message. Let’s see it in action.

  1. Save the playbook and run it:

Sample Output

    PLAY [loop through a set of registered variables] ******************************
    
    TASK [echo the contents of a loop] *********************************************
    changed: [node-1] => (item=one)
    changed: [node-1] => (item=two)
    changed: [node-1] => (item=three)
    
    TASK [Fail if return code is not 0] ********************************************
    skipping: [node-1] => (item={'_ansible_parsed': True, 'stderr_lines': [], '_ansible_item_result': True, u'end': u'2018-09-26 15:59:58.930947', '_ansible_no_log': False, u'stdout': u'one', u'cmd': u'echo one', u'rc': 0, 'item': u'one', u'delta': u'0:00:00.002395', '_ansible_item_label': u'one', u'stderr': u'', u'changed': True, u'invocation': {u'module_args': {u'warn': True, u'executable': None, u'_uses_shell': True, u'_raw_params': u'echo one', u'removes': None, u'argv': None, u'creates': None, u'chdir': None, u'stdin': None}}, 'stdout_lines': [u'one'], u'start': u'2018-09-26 15:59:58.928552', '_ansible_ignore_errors': None, 'failed': False})
    skipping: [node-1] => (item={'_ansible_parsed': True, 'stderr_lines': [], '_ansible_item_result': True, u'end': u'2018-09-26 15:59:59.065822', '_ansible_no_log': False, u'stdout': u'two', u'cmd': u'echo two', u'rc': 0, 'item': u'two', u'delta': u'0:00:00.002410', '_ansible_item_label': u'two', u'stderr': u'', u'changed': True, u'invocation': {u'module_args': {u'warn': True, u'executable': None, u'_uses_shell': True, u'_raw_params': u'echo two', u'removes': None, u'argv': None, u'creates': None, u'chdir': None, u'stdin': None}}, 'stdout_lines': [u'two'], u'start': u'2018-09-26 15:59:59.063412', '_ansible_ignore_errors': None, 'failed': False})
    skipping: [node-1] => (item={'_ansible_parsed': True, 'stderr_lines': [], '_ansible_item_result': True, u'end': u'2018-09-26 15:59:59.199715', '_ansible_no_log': False, u'stdout': u'three', u'cmd': u'echo three', u'rc': 0, 'item': u'three', u'delta': u'0:00:00.002401', '_ansible_item_label': u'three', u'stderr': u'', u'changed': True, u'invocation': {u'module_args': {u'warn': True, u'executable': None, u'_uses_shell': True, u'_raw_params': u'echo three', u'removes': None, u'argv': None, u'creates': None, u'chdir': None, u'stdin': None}}, 'stdout_lines': [u'three'], u'start': u'2018-09-26 15:59:59.197314', '_ansible_ignore_errors': None, 'failed': False})
    
    PLAY RECAP *********************************************************************
    node-1                  : ok=1    changed=1    unreachable=0    failed=0

We can see from the output that in the first task it looped through the shell module. We then see ansible skipping the three results because they didn’t return a non zero return code.

Things to try

Using Loop Labels for Better Output

When working with complex registered variables in loops, the output can become very verbose and difficult to read. Ansible provides loop_control with loop_label to make the output more readable by displaying only specific parts of the loop items.

Create and run your playbook

  1. Create a new file called 05b_register_loop_label.yml:

  2. Enter the following lines into your playbook:

- name: loop through a set of registered variables with cleaner output
  hosts: all
  gather_facts: false

  tasks:
    - name: echo the contents of a loop
      shell: "echo {{ item }}"
      loop:
        - "one"
        - "two" 
        - "three"
      register: echo

    - name: Fail if return code is not 0
      fail:
        msg: "The command ({{ item.cmd }}) did not have a 0 return code"
      when: item.rc != 0
      loop: "{{ echo.results }}"
      loop_control:
        loop_var: item
        label: "{{ item.item }}"
  1. Save the playbook and run it:

Sample Output

    PLAY [loop through a set of registered variables with cleaner output] **********
    
    TASK [echo the contents of a loop] *********************************************
    changed: [node-1] => (item=one)
    changed: [node-1] => (item=two)
    changed: [node-1] => (item=three)
    
    TASK [Fail if return code is not 0] ********************************************
    skipping: [node-1] => (item=one) 
    skipping: [node-1] => (item=two) 
    skipping: [node-1] => (item=three) 
    
    PLAY RECAP *********************************************************************
    node-1                  : ok=1    changed=1    unreachable=0    failed=0

Notice how the output is much cleaner and more readable compared to the previous example. The loop_label shows only the original item value (one, two, three) instead of the entire registered result dictionary.

Things to try

Pausing Loops

Another way to control your loops is to pause your loops. This capability allows you to control the time (in seconds) between execution of items in a task loop. One way you might use this is when you have tasks that take a random amount of time to execute. Before moving on to the next task in your loop you could have it pause 2 minutes to wait for the task to be completed.

Create and run your playbook

  1. Create a new file called 06_pause_loop.yml:

  2. Enter the following lines into your playbook:

- name: loop through a set of shell commands pausing between each loop item
  hosts: all
  gather_facts: false

  tasks:
    - name: list a set of directories, pause 30s between loop items
      shell: "ls {{ item }}"
      loop:
        - /etc
        - /home
        - /var
      loop_control:
        pause: 30
  1. Save the playbook and run it:

Sample Output

    PLAY [loop through a set of shell commands pausing between each] ***************
    
    TASK [ls as set of directories, pause 30s between loop items] ******************
    changed: [node-1] => (item=/etc)
    changed: [node-1] => (item=/home)
    changed: [node-1] => (item=/var)
    
    PLAY RECAP *********************************************************************
    node-1                     : ok=1    changed=1    unreachable=0    failed=0

We can see in our output that between each directory we list Ansible pauses 30 seconds before moving to the next directory in the loop.

Things to try

Indexing Loops

If you need to keep track of where you are in a loop, you can use the index_var option in loop_control to specify a variable name to contain the current loop index.

Create and run your playbook

  1. Create a new file called 07_index_loop.yml:

  2. Enter the following lines into your playbook:

- name: loop through a list of items and display its index
  hosts: all
  gather_facts: false

  tasks:
    - name: count our items
      debug:
        msg: "{{ item }} with index {{ my_idx }}"
      loop:
        - pen
        - pineapple
        - apple
        - pen
      loop_control:
        index_var: my_idx
  1. Save the playbook and run it:

Sample Output

    PLAY [loop through a list of items and display its index] *********************
    
    TASK [count our fruit] *********************************************************
    ok: [node-1] => (item=pen) => {
        "msg": "pen with index 0"
    }
    ok: [node-1] => (item=pineapple) => {
        "msg": "pineapple with index 1"
    }
    ok: [node-1] => (item=apple) => {
        "msg": "apple with index 2"
    }
    ok: [node-1] => (item=pen) => {
        "msg": "pen with index 3"
    }
    
    PLAY RECAP *********************************************************************
    node-1                  : ok=1    changed=0    unreachable=0    failed=0

Having access to the index of a loop allows you to have a numeric value that represents both the number of items in your loop as well as where you are numerically when looping through items. An example of how you might build upon this is to add a when statement that does a task once you’ve looped through a certain number of items.

Things to try

Using Until

There are often times that you may want to retry a task until a certain condition is met. Perhaps we are waiting for a socket to open up to connect to or maybe we want to update a system with irregular connectivity we can use the until command to create a loop condition with a specified number of retries and time between retries.

Create and run your playbook

  1. Create a new file called 08_until.yml:

  2. Enter the following lines into your playbook:

- name: run a command until it displays a certain message
  hosts: all
  gather_facts: false

  tasks:
    - name: run a command until it displays a certain message
      shell: badcommand
      register: result
      until: result.stdout.find("all systems go") != -1
      retries: 5
      delay: 5

We are using some new elements in this playbook besides the until command. Along with the until command is a bit of code that allows us to take the variable that we registered “result” and grab the standard out and then use a filter called find to look for specific words in the result variable. We then apply a condition. In this case the command we are running will never work so when it fails it returns a return code of -1. Our condition says that as long as we are getting a return code of -1 we will keep retrying the task every 5 seconds for a total of 5 times. Let’s see it in action.

  1. Save the playbook and run it:

Sample Output

    PLAY [run a command until it displays a certain message] ***********************
    
    TASK [run a command until it displays a certain message] ***********************
    FAILED - RETRYING: run a command until it displays a certain message (5 retries left).
    FAILED - RETRYING: run a command until it displays a certain message (4 retries left).
    FAILED - RETRYING: run a command until it displays a certain message (3 retries left).
    FAILED - RETRYING: run a command until it displays a certain message (2 retries left).
    FAILED - RETRYING: run a command until it displays a certain message (1 retries left).
    fatal: [node-1]: FAILED! => {"attempts": 5, "changed": true, "cmd": "badcommand", "delta": "0:00:00.002652", "end": "2018-09-26 18:08:13.711280", "msg": "non-zero return code", "rc": 127, "start": "2018-09-26 18:08:13.708628", "stderr": "badcommand: command not found", "stderr_lines": ["badcommand: command not found"], "stdout": "", "stdout_lines": []}
    
    PLAY RECAP *********************************************************************
    node-1                  : ok=0    changed=0    unreachable=0    failed=1

As we said, this particular playbook is one that we built intentionally to fail so that we could see the retry in action.

Things to try

Nested Loops with Include Tasks

When working with complex automation scenarios, you might need to loop through multiple dimensions of data while including external task files. This requires careful management of loop variables to avoid conflicts between the main playbook and included tasks.

An important limitation in Ansible is that you cannot directly loop over a block of tasks. However, you can achieve similar functionality by using include_tasks with loops. This approach allows you to effectively “loop over a block” by placing multiple related tasks in a separate file and then including that file multiple times with different variables.

Create and run your playbook

  1. Create a new file called 09_nested_include.yml:

  2. Enter the following lines into your playbook:

- name: Process multiple services with their configurations
  hosts: all
  gather_facts: false
  vars:
    services:
      - name: "webserver"
        configs: ["httpd.conf", "ssl.conf", "vhosts.conf"]
      - name: "database"
        configs: ["my.cnf", "users.conf"]
      - name: "cache"
        configs: ["redis.conf", "cluster.conf", "sentinel.conf"]

  tasks:
    - name: Process each service
      include_tasks: process_service.yml
      loop: "{{ services }}"
      loop_control:
        loop_var: service_item
  1. Create the included tasks file process_service.yml:
- name: Display service name
  debug:
    msg: "Processing service: {{ service_item.name }}"

- name: Process each configuration file for {{ service_item.name }}
  debug:
    msg: "  Configuring {{ service_item.name }} with {{ item }}"
  loop: "{{ service_item.configs }}"
  1. Save both files and run the main playbook:

Sample Output

    PLAY [Process multiple services with their configurations] *********************
    
    TASK [Process each service] *****************************************************
    included: /path/to/process_service.yml for node-1
    included: /path/to/process_service.yml for node-1
    included: /path/to/process_service.yml for node-1
    
    TASK [Display service name] *****************************************************
    ok: [node-1] => {
        "msg": "Processing service: webserver"
    }
    
    TASK [Process each configuration file for webserver] ***************************
    ok: [node-1] => (item=httpd.conf) => {
        "msg": "  Configuring webserver with httpd.conf"
    }
    ok: [node-1] => (item=ssl.conf) => {
        "msg": "  Configuring webserver with ssl.conf"
    }
    ok: [node-1] => (item=vhosts.conf) => {
        "msg": "  Configuring webserver with vhosts.conf"
    }
    
    TASK [Display service name] *****************************************************
    ok: [node-1] => {
        "msg": "Processing service: database"
    }
    
    TASK [Process each configuration file for database] ****************************
    ok: [node-1] => (item=my.cnf) => {
        "msg": "  Configuring database with my.cnf"
    }
    ok: [node-1] => (item=users.conf) => {
        "msg": "  Configuring database with users.conf"
    }

Note how we use loop_var: service_item in the main playbook to avoid conflicts with the item variable used in the included tasks file. This allows both loops to work independently.

Things to try

In this lab we dove into what conditionals can do for you inside your playbooks. Hopefully you’ve seen how, just like Ansible modules, conditionals can be used together and in conjunction with another to answer all of your automation decision points.

Return to Exercises