Ansible 101: #009 - Conditionals 2

Hello everyone. As promised, I would like to show you some examples of how we can use conditionals in Ansible. For this purpose, I have added another host to my test setup - this time with a CentOS operating system:

ansible-guide-4

OS: CentOS 8
IP: 192.168.0.14

Our new inventory looks like this:

[webservers]
ansible-guide-1 ansible_ssh_user=ansible ansible_host=192.168.0.11
ansible-guide-2 ansible_ssh_user=ansible ansible_host=192.168.0.12

[db]
ansible-guide-3 ansible_ssh_user=ansible ansible_host=192.168.0.13
ansible-guide-4 ansible_ssh_user=ansible ansible_host=192.168.0.14

Now we want to manage 4 hosts with Ansible. Three of them running Ubuntu and one running CentOS. Our two web servers are the same. However, we use 2 different operating systems for the database servers.

Example 1: Installing packages on different distributions

Let's assume we want to install the PostgreSQL server on both database servers. In the previous examples we used the "apt" module. This will still work on our Ubuntu server. However, CentOS uses a tool called "yum" to manage packages. Fortunately, Ansible comes with a module for this out of the box:

https://docs.ansible.com/ansible/latest/collections/ansible/builtin/yum_module.html

But how do we get Ansible to use the right module for each host? We do it with (... drum roll...) conditionals! In other words, we use two tools we already know. First, we need facts, because they contain information about the host operating system. On the other hand, we need conditionals to make the tasks depend on the facts.

- hosts: db
  gather_facts: true
  tasks:
    - name: install postgresql on debian based systems (Ubuntu)
      become: true
      ansible.builtin.apt:
        name: postgresql
        update_cache: true
      when: "ansible_os_family == 'Debian'"
      
    - name: install postgresql on Redhat based systems (CentOS)
      become: true
      ansible.builtin.yum:
        name: postgresql
      when: "ansible_os_family == 'RedHat'"

We use the fact 'ansible_os_family' which contains the base type of the distribution used. Ubuntu is based on a Debian operating system, while CentOS is based on a RedHat operating system. Other possible values here would be "Suse" or "Gentoo", for example.

Let's run our playbook:

ansible-playbook -i inventory.txt setup-postgres.yml

The output should look like this:

PLAY [db] ********************************************************************************************************************************************************

TASK [Gathering Facts] **************************************************************************************************************************************************
ok: [ansible-guide-3]
ok: [ansible-guide-4]

TASK [install postgresql on debian based systems (Ubuntu)] **************************************************************************************************************
changed: [ansible-guide-3]
skipping: [ansible-guide-4]

TASK [install postgresql on Redhat based systems (CentOS)] **************************************************************************************************************
skipping: [ansible-guide-3]
changed: [ansible-guide-4]

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

As we can see, the host for which the condition did not apply has now been skipped for both installation tasks. So we can make tasks dependent on the operating system used. Especially for environments with different distributions, this makes our life much easier!
Example 2: Restricting a task to a specific host

Sometimes we want to run a task in the playbook on a specific host only. Let's say we want the second web server to be our development and test system, so we want to create another user for the developer.

To do this, I'm going to expand the playbook from Part 4, where we installed and started the web server:

 - name: webserver install 
   hosts: webservers
   tasks: 
     - name: install apache2
       become: true
       ansible.builtin.apt:
         name: apache2
         state: present
         update_cache: yes
       become: true

    - name: enable and start apache2 systemd service
      become: true
      ansible.builtin.systemd:
        name: apache2
        enabled: true
        state: started
    
    - name: add dev user on second webserver
      become: true
      ansible.builtin.user: 
        name: herbert
        state: present
      when: "inventory_hostname == 'ansible-guide-2'"

webserver.yml

Here we use the Ansible variable "inventory_hostname", which exists in every run (even with fact gathering disabled) and contains the current host name as defined in the inventory. Important: This does not have to be the actual hostname of the system. This is contained in the fact (gathering must be enabled) "ansible_hostname".

In the example playbook above, the task will only run if the name of the current host is exactly "ansible-guide-2".

Example 3: Run a task only if the host is a member of a particular group

If we want to run a play for a specific group, we define that using the "hosts" parameter in the playbook. But even if we run a play on all hosts, for example, we can make the execution of individual tasks dependent on group membership if we need to.

 - name: webserver install 
   hosts: all
   tasks: 
     - name: add default user to all systems
       become: true
       ansible.builtin.user:
         name: technik
         state: present
       become: true
    
    - name: add dbadmin user on db servers
      become: true
      ansible.builtin.user: 
        name: dbadmin
        state: present
      become: true
      when: "'db' in group_names"

all-hosts.yml

In this example, we run the game on all the hosts in our inventory. The user 'technik' will be created on all systems. For the user 'dbadmin' we have defined a condition that checks if the string 'db' is in the 'group_names' list provided by Ansible. This is a list that exists for each host in the current run and contains the names of all groups that the host belongs to.

Conclusion

By combining facts and conditions, we can make our playbooks very flexible, allowing us to use Ansible in heterogeneous infrastructures, for example. With Ansible, there are many ways to get to the same goal, so it is important to get a feel for which way is best for your particular use case. For example, do I use one playbook and work with conditions, or do I break the tasks into separate playbooks?

In the rest of this guide, I will try to include best practices and my own experiences whenever possible.

In the next part, we will look at another control structure in Ansible called loops. Hopefully the gap between posts will be a little shorter this time.

Till then!

Mow