DEV Community

Cover image for The first Ansible role
Ákos Takács
Ákos Takács

Posted on • Edited on

The first Ansible role

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

Table of contents

Before you begin

Requirements

» Back to 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
  • 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 «

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

git clone https://github.com/rimelek/homelab.git
cd homelab
Enter fullscreen mode Exit fullscreen mode

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

git checkout -b tutorial.episode.2b tutorial.episode.2
Enter fullscreen mode Exit fullscreen mode

Have the inventory file

» Back to table of contents «

Copy the inventory template

cp inventory-example.yml inventory.yml
Enter fullscreen mode Exit fullscreen mode

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

Create the Python virtual environment

» Back to table of contents «

How you activate the virtual environment, depends on how you created it. The episode of The first Ansible playbook 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:

python3 -m venv venv
Enter fullscreen mode Exit fullscreen mode

Helper scripts to activate the virtual environment

» Back to 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.

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

source ${HOMELAB_ENV:-venv}/bin/activate
Enter fullscreen mode Exit fullscreen mode

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.

ssh-agent $SHELL
Enter fullscreen mode Exit fullscreen mode

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

source homelab-env.sh
Enter fullscreen mode Exit fullscreen mode

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

HOMELAB_ENV=custom-env source homelab-env.sh
Enter fullscreen mode Exit fullscreen mode

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

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

export HOMELAB_ENV

source ./homelab-env.sh
Enter fullscreen mode Exit fullscreen mode

and source it

source homelab-env-os.sh
Enter fullscreen mode Exit fullscreen mode

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 «

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 «

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.

mkdir -p roles/hello_world/tasks
touch roles/hello_world/tasks/main.yml
Enter fullscreen mode Exit fullscreen mode

The main yml should have this content:

- name: Create Hello World file
  ansible.builtin.copy:
    content: "Hello World"
    dest: "{{ hello_world_dest }}"
Enter fullscreen mode Exit fullscreen mode

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.

mkdir -p touch roles/hello_world/defaults
touch roles/hello_world/defaults/main.yml
Enter fullscreen mode Exit fullscreen mode

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

hello_world_dest: /home/myuser/hello-world.txt
Enter fullscreen mode Exit fullscreen mode

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.

hello_world_dest: /home/{{ ansible_user }}/hello-world.txt
Enter fullscreen mode Exit fullscreen mode

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.

hello_world_user: "{{ ansible_user }}"
hello_world_dest: "/home/{{ hello_world_user }}/hello-world.txt"
Enter fullscreen mode Exit fullscreen mode

Use roles in a playbook instead of tasks

» Back to 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:

- name: Play 1
  hosts: all
  roles:

    - hello_world
Enter fullscreen mode Exit fullscreen mode

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

- name: Play 1
  hosts: all
  roles:

    - role: hello_world
      # additional parameters here
Enter fullscreen mode Exit fullscreen mode

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.

- name: Play 1
  hosts: all
  roles:

    - role: hello_world
      hello_world_dest: /opt/hello-world.txt
Enter fullscreen mode Exit fullscreen mode

Now you can try to run it:

ansible-playbook -i inventory.yml playbook.yml
Enter fullscreen mode Exit fullscreen mode

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

{
  "changed": false,
  "checksum": "0a4d55a8d778e5022fab701977c5d840bbc486d0",
  "msg": "Destination /opt not writable"
}
Enter fullscreen mode Exit fullscreen mode

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.

- name: Play 1
  hosts: all
  roles:

    - role: hello_world
      become: true
      hello_world_dest: /opt/hello-world.txt
Enter fullscreen mode Exit fullscreen mode

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

{
  "msg": "Missing sudo password"
}
Enter fullscreen mode Exit fullscreen mode

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:

ansible-playbook -i inventory.yml playbook.yml --ask-become-pass
Enter fullscreen mode Exit fullscreen mode

And now the full result is a working playbook:

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
Enter fullscreen mode Exit fullscreen mode

Ad-hoc Ansible commands

» Back to 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:

ansible all -i inventory.yml -m ansible.builtin.command -a "cat /opt/hello-world.txt"
Enter fullscreen mode Exit fullscreen mode

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:

ta-lxlt | CHANGED | rc=0 >>
Hello World
Enter fullscreen mode Exit fullscreen mode

Conclusion

» Back to 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

GitHub logo rimelek / homelab

Source code to create a home lab. Part of a video tutorial

README

This project was created to help you build your own home lab where you can test your applications and configurations without breaking your workstation, so you can learn on cheap devices without paying for more expensive cloud services.

The project contains code written for the tutorial, but you can also use parts of it if you refer to this repository.

Tutorial on YouTube in English: https://www.youtube.com/watch?v=K9grKS335Mo&list=PLzMwEMzC_9o7VN1qlfh-avKsgmiU8Jofv

Tutorial on YouTube in Hungarian: https://www.youtube.com/watch?v=dmg7lYsj374&list=PLUHwLCacitP4DU2v_DEHQI0U2tQg0a421

Note: The inventory.yml file is not shared since that depends on the actual environment so it will be different for everyone. If you want to learn more about the inventory file watch the videos on YouTube or read the written version on https://dev.to. Links in the video descriptions on YouTube.

You can also find an example inventory file in the project root. You can copy that and change the content, so you will use your IP…

Top comments (0)