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
- Helper scripts to activate the virtual environment
- Ansible roles
- A simple role
- Use role sin a playbook instead of tasks
- Ad-hoc Ansible commands
- Conclusion
Before you begin
Requirements
- 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
If you started the tutorial with this episode, clone the project from GitHub:
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
git checkout -b tutorial.episode.2b tutorial.episode.2
Have the inventory file
Copy the inventory template
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
Create the Python virtual environment
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
Helper scripts to activate the virtual environment
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
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
The above command started a new instance of the original shell in the agent, so you can source the environment:
source homelab-env.sh
If you want to override the name of the virtual environment, you can do that too.
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
HOMELAB_ENV="venv-$(uname -s | tr '[:upper:]' '[:lower:]')"
export HOMELAB_ENV
source ./homelab-env.sh
and source it
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
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
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
The main yml should have this content:
- 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.
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:
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.
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.
hello_world_user: "{{ ansible_user }}"
hello_world_dest: "/home/{{ hello_world_user }}/hello-world.txt"
Use roles in a playbook instead of tasks
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
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
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
Now you can try to run it:
ansible-playbook -i inventory.yml playbook.yml
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"
}
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
If your user can't use sudo
without password, you will get the following error message:
{
"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:
ansible-playbook -i inventory.yml playbook.yml --ask-become-pass
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
Ad-hoc Ansible commands
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"
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
Conclusion
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
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)