The first Ansible role
Tags: ansible, devops, tutorial
Published: 2023-06-24 16:51:47

## Introduction

We learned about some basic Ansible features in the previous posts, so we can finally create a simple Ansible playbook and use SSH keys and SSH agent to connect to a remote server. When you have two tasks in a playbook there is no point of having a more complicated folder structure. However, when you know you want to create a home lab with a bunch of optional and required features and support different environments with parameters it is a good idea to group your tasks and even your variables. Ansible supports roles which are basically groups of tasks in a special folder and responsible for a specific feature which you want to add to multiple playbooks or even in one playbook but multiple times.

You can share these roles and even publish it on [Ansible Galaxy](https://galaxy.ansible.com/). You might not want to do that if you aren't sure you can support that, but even if you create a role for yourself to run in different environments, variables become much more important than they were before.

I already used some variables and I also used one in a template but that was directly in a playbook, so it's time to create our first Ansible role.

{% embed https://youtu.be/mf8V5sibEr4 %}

## Table of contents

- [Before you begin](#before-you-begin)
  - [Requirements](#requirements)
  - [Download the already written code of the previous episode](#download-the-already-written-code-of-the-previous-episode)
  - [Have an inventory file](#have-the-inventory-file)
  - [Create the Python virtual environment](#create-the-python-virtual-environment)
- [Helper scripts to activate the virtual environment](#helper-scripts-to-activate-the-virtual-environment)
- [Ansible roles](#ansible-roles)
- [A simple role](#a-simple-role)
- [Use role sin a playbook instead of tasks](#use-roles-in-a-playbook-instead-of-tasks)
- [Ad-hoc Ansible commands](#adhoc-ansible-commands)
- [Conclusion](#conclusion)

## Before you begin

### Requirements

[» Back to table of contents «](#table-of-contents)

- The project requires Python 3.11. If you have an older version and you don't know how you could install a new version, read about Nix in [Install Ansible 8 on Ubuntu 20.04 LTS using Nix](https://dev.to/rimelek/install-ansible-8-on-ubuntu-2004-lts-using-nix-46hm)
- You will also need to create a virtual Python environment. In this tutorial I used the "venv" Python module and the name of the folder of the virtual environment will be "venv".
- You will also need an Ubuntu remote server. I recommend an Ubuntu 22.04 virtual machine.

### Download the already written code of the previous episode

[» Back to table of contents «](#table-of-contents)

If you started the tutorial with this episode, clone the project from GitHub:

```bash
git clone https://github.com/rimelek/homelab.git
cd homelab
```

If you cloned the project now, or you want to make sure you are using the exact same code I did, switch to the previous episode in a new branch

```bash
git checkout -b tutorial.episode.2b tutorial.episode.2
```

### Have the inventory file

[» Back to table of contents «](#table-of-contents)

Copy the inventory template

```bash
cp inventory-example.yml inventory.yml
```

And change `ansible_host` to the IP address of your Ubuntu server that you use for this tutorial, and change `ansible_user` to the username on the remote server that Ansible can use to log in. If you still don't have an SSH private key, read the [Generate an SSH key part of Ansible playbook and SSH keys](https://dev.to/rimelek/ansible-playbook-and-ssh-keys-33bo#generate-an-ssh-key)

### Create the Python virtual environment

[» Back to table of contents «](#table-of-contents)

How you activate the virtual environment, depends on how you created it. The episode of [The first Ansible playbook](https://dev.to/rimelek/the-first-ansible-playbook-579h#install-ansible) describes the way to create and activate the virtual environment using the "venv" Python module, but in this one we will create helper scripts for activation, in case you don't have the environment yet, you just need to create it like this: 

```bash
python3 -m venv venv
```

## Helper scripts to activate the virtual environment

[» Back to table of contents «](#table-of-contents)

In the previous post we started to use ssh-agent. We had to run multiple commands to start the agent, add the SSH key and activate the environment. You can simplify this with the following script named as `homelab-env.sh`.

```bash
if [ "${SSH_AGENT_PID:-}" != "" ] && [ -f ~/.ssh/ansible ]; then
  ssh-add ~/.ssh/ansible
fi

source ${HOMELAB_ENV:-venv}/bin/activate
```

Now instead of sourcing the original script you cn source `homelab-env.sh` and optionally
start the agent before that. The script will recognize it and add a default SSH key if it exists. We created it before, so it should be there.

```bash
ssh-agent $SHELL
```

The above command started a new instance of the original shell in the agent, so you can source the environment:

```bash
source homelab-env.sh
```

If you want to override the name of the virtual environment, you can do that too.

```bash
HOMELAB_ENV=custom-env source homelab-env.sh
```

If you want `venv-linux` on linux and `venv-darwin` on macOS, you can add another script in the project root with the following content and name it as `homelab-env-os.sh`

```bash
HOMELAB_ENV="venv-$(uname -s | tr '[:upper:]' '[:lower:]')"

export HOMELAB_ENV

source ./homelab-env.sh
```

and source it

```bash
source homelab-env-os.sh
```

Since I had to use multipass on macOS to run a Linux virtual machine, I had two different environments. This is why I created this script.

## Ansible roles

[» Back to table of contents «](#table-of-contents)

In order for Ansible to recognize the roles automatically, we need a subfolder in our Ansible project and the name of the folder has to be "roles".

An Ansible role has some special folders as well. I don't want to write about all of them in one post because that would just confuse you, so we will use the following folders:

- **defaults**: The `main.yml` in this folder can contain default values for the required variables.
- **tasks**: This folder is the most important of all, in which the `main.yml` will contain the tasks.

There are two more commonly used folders which I will not use in this post:

- **files**: You can have some static files that you just want to copy to the remote server.
- **templates**: Jinja template files can be stored here.

## A simple role

[» Back to table of contents «](#table-of-contents)

Let's move our hello world copy task from the playbook to a role.

The nam of the role will be "hello_world" and we need tha "tasks" folder with
a file called `main.yml`.

```bash
mkdir -p roles/hello_world/tasks
touch roles/hello_world/tasks/main.yml
```

The main yml should have this content:

```yaml
- name: Create Hello World file
  ansible.builtin.copy:
    content: "Hello World"
    dest: "{{ hello_world_dest }}"
```

All I changed compared to the previous post is that I used a template variable the entire destination path. Variables in a role should be started with the name of the role and an additional underscore. After that you can specify any syntactically correct name. We want to set the destination of the file so the final name can be `hello_world_dest`.

This variable would be empty by default, so you can set a default value in the `main.yml` in the `defaults` folder.

```bash
mkdir -p touch roles/hello_world/defaults
touch roles/hello_world/defaults/main.yml
```

The content of this file can be as simple as the following:

```yaml
hello_world_dest: /home/myuser/hello-world.txt
```

Or we can use a template again. A role should be independent of the environment as much as possible, but you can use some default values if that can be overridden.

In the inventory file we actually defined a variable to set the username for the SSH connection. We used `ansible_user`. You can use it as a default value, so you don't have to run a command to get the username.

```yaml
hello_world_dest: /home/{{ ansible_user }}/hello-world.txt
```

Sometimes you want to be able to override the username without overriding the whole path. In that case you can define your own variable and set `{{ ansible_user }}` as the default value.

```yaml
hello_world_user: "{{ ansible_user }}"
hello_world_dest: "/home/{{ hello_world_user }}/hello-world.txt"
```

## Use roles in a playbook instead of tasks

[» Back to table of contents «](#table-of-contents)

We have a role, now we have to use it. Instead of a list of tasks, we need a list of roles. The list of roles can be defined two ways. The shortest is the following in `playbook.yml`:

```yaml
- name: Play 1
  hosts: all
  roles:

    - hello_world
```

If you want to add parameters to the role as close as it is possible, you can also define the list this way:

```yaml
- name: Play 1
  hosts: all
  roles:

    - role: hello_world
      # additional parameters here
```

If you run it now, it will just run as before and make sure you have a file in your home folder with the name `hello-world.txt` and the content "Hello World". Now let's change the folder, because you don't want to have it in your home, but at `/opt/hello-world.txt`.

```yaml
- name: Play 1
  hosts: all
  roles:

    - role: hello_world
      hello_world_dest: /opt/hello-world.txt
```

Now you can try to run it:

```bash
ansible-playbook -i inventory.yml playbook.yml
```

This probably doesn't work, and you get the following error message (in a single line):

```json
{
  "changed": false,
  "checksum": "0a4d55a8d778e5022fab701977c5d840bbc486d0",
  "msg": "Destination /opt not writable"
}
```

This is because your user doesn't have permission to write `/opt`. In order to become a root user, you can use the `become: true` parameter in the task definition, but you don't know what the user will set as a path and whether that would require root privileges or not, so you can just use the parameter under the name of the role, right where you defined the path.

```yaml
- name: Play 1
  hosts: all
  roles:

    - role: hello_world
      become: true
      hello_world_dest: /opt/hello-world.txt
```

If your user can't use `sudo` without password, you will get the following error message:

```json
{
  "msg": "Missing sudo password"
}
```

Now we can use the `--ask-become-pass` flag which doesn't require sshpass on the ansible controller, since this will be handled by Anisble and not SSH, so it will work on macOS as well:

```bash
ansible-playbook -i inventory.yml playbook.yml --ask-become-pass
```

And now the full result is a working playbook:

```text
PLAY [Play 1] ************************************************************************************************

TASK [Gathering Facts] ***************************************************************************************
ok: [ta-lxlt]

TASK [hello_world : Create Hello World file] *****************************************************************
changed: [ta-lxlt]

PLAY RECAP ***************************************************************************************************
ta-lxlt                    : ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
```

## Ad-hoc Ansible commands

[» Back to table of contents «](#table-of-contents)

And now the final trick to check the content of the file on the remote server. We can use ad-hoc Ansible commands, which means we don't need playbooks, but we can run only one task. Since this is an ad-hoc command, we will also see the standard output:

```bash
ansible all -i inventory.yml -m ansible.builtin.command -a "cat /opt/hello-world.txt"
```

We used the `ansible` command instead of `ansible-playbook` and as the first argument, we had to define the group of hosts or a specific host. Since I have only one host, I used the "all" and I also had to define the inventory file the same way as I did with `ansible-playbook`.

`-m ansible.builtin.command` means we wanted to use the command module, and we could define the argument of this module after `-a`. In this case the argument was the command itself.
The output is something like this:

```text
ta-lxlt | CHANGED | rc=0 >>
Hello World
```

## Conclusion

[» Back to table of contents «](#table-of-contents)

Now that you can finally create your own reusable roles, you can start thinking about creating more complex roles to create and start virtual machines which will be required for our home lab. Of course there is a huge step between creating a simple role and creating virtual machines with roles, but at least you know what you need to learn more about.

If you want to see me configuring everything that I wrote about, you can watch it on YouTube: <https://youtu.be/mf8V5sibEr4>

The final source code of this episode can be found on GitHub:

<https://github.com/rimelek/homelab/tree/tutorial.episode.3>

{% embed https://github.com/rimelek/homelab %}