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.
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
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
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
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.
-name:Create Hello World fileansible.builtin.copy:content:"HelloWorld"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.
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.
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.
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 1hosts:allroles:-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 1hosts:allroles:-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.
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.
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:
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:
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:
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.
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…