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:
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.
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