Install a default set of applications using Ansible.

The Goal

To install a specified list of applications on a clean installation of an operating system to avoid maintaining a custom image of that operating system.

The Work

As I have continued to learn about Docker and Ansible I have run into situations where I would really like to have a specific set of tools available to work with such as vim and nmap. These tools are often not installed by default; or the name on the command line is an alias for a similar application. I would like the same tools available across my homelab for consistency. Instead of several custom OS images that I have to maintain I would prefer to do a clean install of the OS and run a playbook against the node in question in order to bring it in line with the rest of the equipment.

Currently my homelab consists of eight Raspberry Pis. Four of these are Raspberry Pi Zeros using a ClusterHAT on a Raspberry Pi 3 Model B as my small cluster for Docker. The others are a Model B original, 2 Model B, and 3 Model B+ providing other functionality such as file services and displaying the visualizer for the cluster. While Ansible can easily handle a heterogenous environment all of these use the apt package manager as I just have the default Raspberry Pi OS installed. This will simplify the yaml file for the playbook. After installing Ansible from the package manager I created a custom directory and change to that directory. Keeping playbooks localized allows for different groups to perform their work without stepping on each other’s files. For me this isn’t as big of an issue, but as I add applications I’m sure it will become more problematic if I don’t take care of it now.

mkdir ~/Documents/newAnsible
cd ~/Documents/newAnsible

I edit the hosts file found in /etc/ansible. I create a group using the INI syntax as right now my environment isn’t very complex and I don’t need a lot of custom information across the nodes. This may change in the future so I always copy the existing file to a .bak file before editing. This also allows me to roll back quickly in case the modifications don’t work out as I expect. I like vim for my text editing, but use emacs or nano or VS code if you have that set up for remote access.

vim /etc/ansible/hosts

Towards the end of the file you can add a group of hosts using a friendly name, such as webservers or in this case clusterhosts. Add the hosts that need to be managed by the group.

[clusterhosts]
10.1.0.11
10.1.0.12
10.1.0.13
10.1.0.14

Now I can create the yaml file for the cluster hosts. I shouldn’t need these applications across these nodes, but this will be used to build out more groups at a later date. In the new directory that was created earlier we need the new file.

vim basicpackages.yaml

That will create a new file in the current directory with the name basicpackages.yaml. Every yaml file starts with — and ends with … . In between are the following sections:

  1. Hosts – The hosts that will be affected when the playbook runs
  2. Vars – The variables that are needed in order to successfully complete the playbook
  3. Tasks – What the playbook does

These three are the ones needed to make this one work. In the Hosts section

- hosts: clusterhosts

The dash / hyphen is used to indicate a list item in YAML.

The Vars section is next. This is where I built out my list of applications. This could be contained elsewhere if you needed more granularity in some way. This could be achieved by calling another playbook to discover some fact that would change what list is provided in the Tasks section.

  vars:
          applications:
                  - htop
                  - nmap
                  - vim
                  - docker

Tasks is the final section. In this section we describe what we want Ansible to do with our group clusterhosts.

  tasks:
          - name: install packages
            become: yes
            apt:
                    state: present
                    name: "{{ applications }}"

In Ansible the playbook will take a list in the format “{{ listNameHere }}” to iterate through. The list name variable in this example is applications, which was defined earlier. The entire file looks like this:

---
#Hosts Section
- hosts: clusterhosts
#Vars Section
  vars:
          applications:
                  - htop
                  - nmap
                  - vim
                  - docker
#Tasks Section
  tasks:
          - name: install packages
            become: yes
            apt:
                    state: present
                    name: "{{ applications }}"
...

With the playbook completed the syntax of the playbook needs to be checked to make sure we didn’t enter something in the file incorrectly. The ansible-playbook command has the option –syntax-check which will verify if the playbooks follows the syntax rules for YAML.

ansible-playbook basicpackages.yaml --syntax-check

The command will return playbook: basicpackages.yaml if this playbook does not have any issues with its syntax.

The ansible-playbook has the –check option which allows for a dry run of the playbook without changing anything. This is often combined with –diff which will show the changes made to the systems. Without using –diff the –check option will just show what will change without specifying what will change. In this case if we didn’t have htop installed as it was a new application we had tested and decided should be part of our standard install. Below is the output of just the –check option:

ansible-playbook --check basicpackages.yaml

PLAY [clusterhosts] ***********************************************************************************

TASK [Gathering Facts] ********************************************************************************
ok: [10.1.0.13]
ok: [10.1.0.11]
ok: [10.1.0.14]
ok: [10.1.0.12]

TASK [install packages] *******************************************************************************
changed: [10.1.0.12]
changed: [10.1.0.14]
changed: [10.1.0.13]
changed: [10.1.0.11]

PLAY RECAP ********************************************************************************************
10.1.0.11               : ok=2    changed=1    unreachable=0    failed=0
10.1.0.12               : ok=2    changed=1    unreachable=0    failed=0
10.1.0.13               : ok=2    changed=1    unreachable=0    failed=0
10.1.0.14               : ok=2    changed=1    unreachable=0    failed=0

And now the output with –check and –diff

ansible-playbook --check --diff basicpackages.yaml

PLAY [clusterhosts] ***********************************************************************************

TASK [Gathering Facts] ********************************************************************************
ok: [10.1.0.14]
ok: [10.1.0.13]
ok: [10.1.0.12]
ok: [10.1.0.11]

TASK [install packages] *******************************************************************************
Suggested packages:
  lsof
The following NEW packages will be installed:
  htop
0 upgraded, 1 newly installed, 0 to remove and 0 not upgraded.
changed: [10.1.0.14]
Suggested packages:
  lsof
The following NEW packages will be installed:
  htop
0 upgraded, 1 newly installed, 0 to remove and 0 not upgraded.
changed: [10.1.0.13]
Suggested packages:
  lsof
The following NEW packages will be installed:
  htop
0 upgraded, 1 newly installed, 0 to remove and 0 not upgraded.
changed: [10.1.0.12]
Suggested packages:
  lsof
The following NEW packages will be installed:
  htop
0 upgraded, 1 newly installed, 0 to remove and 0 not upgraded.
changed: [10.1.0.11]

PLAY RECAP ********************************************************************************************
10.1.0.11               : ok=2    changed=1    unreachable=0    failed=0
10.1.0.12               : ok=2    changed=1    unreachable=0    failed=0
10.1.0.13               : ok=2    changed=1    unreachable=0    failed=0
10.1.0.14               : ok=2    changed=1    unreachable=0    failed=0

With this verified the command ansible-playbook basicpakages.yaml will run the playbook and install the applications. The –check and –diff can be used to verify that the applications have been installed successfully.

ansible-playbook basicpackages.yaml

PLAY [clusterhosts] ***********************************************************************************

TASK [Gathering Facts] ********************************************************************************
ok: [10.1.0.12]
ok: [10.1.0.14]
ok: [10.1.0.13]
ok: [10.1.0.11]

TASK [install packages] *******************************************************************************
changed: [10.1.0.14]
changed: [10.1.0.12]
changed: [10.1.0.13]
changed: [10.1.0.11]

PLAY RECAP ********************************************************************************************
10.1.0.11               : ok=2    changed=1    unreachable=0    failed=0
10.1.0.12               : ok=2    changed=1    unreachable=0    failed=0
10.1.0.13               : ok=2    changed=1    unreachable=0    failed=0
10.1.0.14               : ok=2    changed=1    unreachable=0    failed=0

The Summary

This configuration allows for the modular management of packages for any given system. While the examples here all use a unified OS, in this case Raspberry Pi OS, it easily could be expanded in order to allow for a mixed environment using different package managers such as where the server OS is different from the workstation OS.