ANSIBLE TUTORIALS #3: ROLES
In this section, our focus will be on executing tasks with the use of Ansible roles. I am confident you would love the look of our playbook when we are done with this tutorial. This is because implementing Ansible roles makes our work look neat and professional. Let’s dive in!
This is the third tutorial in my Ansible series, so do well to start with tutorials 1 and 2, or else you would be missing out on a lot.
At the end of this tutorial, you will learn step-by-step how to write Ansible roles to:
- deploy a website,
- install and start Apache, and restart it with an event handler,
- install Terraform,
- install Jenkins and open port 8080,
- install Docker, and run an Ubuntu container with it.
You would also learn how to create a new user on Ansible with a hashed password. This is going to be so much fun!
Tasks:
For this tutorial, we would work with 2 CentOS and 2 Ubuntu VMs to demonstrate the following tasks:
- Write a role to install Apache, and another role to copy a default HTML page to the default Apache webpage root directory. (Target the CentOS VMs only).
- Write a handler to restart Apache.
- Write a role to install Terraform.
- Write a role to install Docker, and run an Ubuntu container with it using Ansible.
- Write a role to install Jenkins and open port 8080 with Firewall.
Tasks 3–5 should target the Ubuntu VMs only.
Writing the Hosts File
Before moving any further, we must write our hosts or inventory file. This would be quite similar to the host file from our previous tutorial, ANSIBLE TUTORIALS #2: GROUPS. We would group all the servers under the parent group, the CentOS VMs as the webserver, and the Ubuntu VMs as the workstation.
[parent]
ubuntu-node-1
ubuntu-node-2
centos-node-1
centos-node-2
[workstation]
ubuntu-node-1
ubuntu-node-2
[webserver]
centos-node-1
centos-node-2
Creating a New User with Hashed Password
We have already seen how to create a sudo user in the first tutorial of this series, but here it is going to be a little different. This time, we are going to create this user and give it a hashed password. This is what happens when you try to create a user password without hashing it:
This implies that your new user will be created but the password will have no effect except you hash it.
You need to have the password library package, “passlib” installed on your control machine to enable you to hash your passwords. To do this, you have to first install python3-pip.
$ sudo apt install python3-pip -y
$ sudo pip install passlib
Alright!
Create a new directory (ansible_tasks3), and write a playbook named create-user.yml:
vagrant@ubuntu-control:~$ mkdir ansible_tasks3; cd ansible_tasks3
vagrant@ubuntu-control:~/ansible_tasks3$ sudo vi create-user.yml
Copy the playbook below:
---
- name: create new sudo user
hosts: parent
tasks:
- name: create user, ansible3
tags: always
user:
name: ansible
password: "{{ password | password_hash('sha512') }}"
groups: "{{ sudo_group }}"
- name: add ssh_key to ansible3
tags: always
authorized_key:
user: ansible3
key: "{{lookup('ansible.builtin.file', '~/.ssh/id_ed25519.pub') }}"
- name: copy authentication key file to the new user, ansible3
tags: always
copy:
src: ~/.ssh/id_ed25519.pub
dest: /home/ansible3/id_ed25519.pub
mode: 0444
owner: ansible3
group: ansible3
The name of our new user will be ansible3, and with this user, we would execute every task on our playbook. We have to run this as the default user, vagrant so make sure you set remote_user to vagrant on your ansible.cfg file. When executing subsequent commands, we would change the value of the remote_user to the name of our new user, ansible3.
[defaults]
inventory=./hosts
host_key_checking=false
private_key_file=~/.ssh/id_ed25519
remote_user=vagrant
[privilege_escalation]
become=true
become_method=sudo
become_user=root
become_ask_pass=false
Also, we have used some variables, therefore, we must define these variables on each of our host_var files.
# This is our centos-node-1
ansible_host: 192.168.53.61
password: ansible3
sudo_group: sudo
We already learned how to create host variable files in our previous tutorial. Do this for all the files in your host variables’ directory. We would return to define more variables later in this tutorial.
After running your playbook,
vagrant@ubuntu-control:~/ansible_tasks3$ ansible-playbook create-user.yml
you should have a similar outcome to the snapshot below:
Great! Now we can change the remote_user from vagrant to ansible3 to carry out our main tasks.
Before we carry on, it is good practice to first update our servers. Also, looking at the sort of tasks we are going to execute, there are several dependency packages we ought to pre-install on our servers. Dependency packages are packages necessary for a desired software to work effectively. So on our general playbook, we are going to write some pre_tasks to target the parent group of our hosts’ file. You would soon understand why I refer to this playbook as general, so please stay with me.
vagrant@ubuntu-control:~/ansible_tasks3$ sudo vi playbook.yml
Copy the code below:
---
- name: update and install packages
hosts: parent
pre_tasks:
- name: install updates
package:
update_cache: yes
state: latest
- name: install packages
package:
name:
- "{{ php }}"
- python3
- python3-pip
- python3-setuptools
- curl
state: latest
update_cache: true
⚠️Please note that this particular playbook will be the only one to have the initial three dashes at its top. The rest of the YAML files (main.yml) we would create (apart from the create-user.yml file we already created) in all our roles will not have those three dashes at the top.
We head back to our host_var files to declare the php variable. You may do this once and for all at the end of the tutorial, but we do not want to leave any stone unturned:
# On CentOS host files
php: php
# On Ubuntu host files
php: libapache2-mod-php
# 1: Write a role to install Apache, and another role to copy a default HTML page to the default Apache webpage root directory on the webserver group only
The issue about the target hosts will be addressed with the playbook I refer to as general — remember the only playbook.yml file with the three dashes at its top? Alright!
We would create a directory that is conventionally called roles. In this directory, we would create another directory and name it setup_apache/tasks. Then, in the conventional Ansible tasks directory, we would create a conventional Ansible main.yml file.
vagrant@ubuntu-control:~/ansible_tasks3$ sudo mkdir -p roles/setup_apache/tasks
⚠️Please adhere strictly to the exact name of any file or directory I refer to as conventional, or else you would run into errors.
vagrant@ubuntu-control:~/ansible_tasks3$ sudo vi roles/setup_apache/tasks/main.yml
Copy the playbook below to the setup_apache’s main.yml file.
- name: install Apache
tags: install_apache
package:
name: "{{ apache }}"
state: latest
- name: start Apache
tags: start_apache
service:
name: "{{ apache }}"
state: started
enabled: true
Save and exit (:wq). Then go to the host variable files to define the variable we have used for Apache:
# On the CentOS host_var files
apache: httpd
#On the Ubuntu host_var files
apache: apache2
I hope you can see how the use of variables comes in handy when a particular package or software has different official names on different Operating Systems.
Deploying our Default Web Page
We would also create a role for this task, and name it “deploy_webpage.” Within the tasks directory of this role, create
- a main.yml file to write the tasks to deploy your web page, and
- a directory named “files” by convention where the HTML file of our web page will be saved.
Let us write our HTML file. We would name this file “default_site.html”, but you can give it any name you want.
vagrant@ubuntu-control:~/ansible_tasks3$ sudo mkdir -p roles/deploy_webpage/tasks/files
vagrant@ubuntu-control:~/ansible_tasks3$ sudo vi roles/deploy_webpage/tasks/files/default_site.html
Copy this code to the HTML file:
<html>
<head>
<title>Ansible-tutorial</title>
</head>
<body style="text-align:center;">
<div>
<span style="font-size:5rem; color:SteelBlue;"><b>CLOUDLORD</b></span><br>
<span style="font-size:2rem;">DEVOPS | CLOUD</span>
</div>
<h1>Welcome to Ansible Tutorials by Cloudlord!</h1>
<p>Learn Ansible the simplest way possible.</p>
</body>
</html>
Save and exit. Next, we would create the main.yml file to copy this HTML file from our control server to the host servers’ Apache web page root directory at /var/www/html/
vagrant@ubuntu-control:~/ansible_tasks3$ sudo mkdir roles/deploy_webpage/tasks
vagrant@ubuntu-control:~/ansible_tasks3$ sudo vi roles/deploy_webpage/tasks/main.yml
Copy and paste this code to the text editor:
- name: copy webpage
tags: always
copy:
src: default_site.html
dest: /var/www/html/index.html
owner: root
group: root
mode: 0644
notify: restart_apache
We have renamed our HTML file to index.html in our destination directory. This is because according to the default configuration of Apache (at /etc/apache2/mods-available/dir.conf), index.html comes first in preference as the default web document, except you modify this configuration. Our HTML file will replace Apache’s default welcome page which is usually named index.html and stored at /var/www/html.
Also, we have used the “notify handler” operation to trigger the handler we would create to restart Apache as soon as our HTML file is copied to our hosts’ web directory.
#2: Write a handler to restart Apache
For the newly copied HTML file to appear on our browser as our web page, Apache must be restarted. Therefore, within the setup_apache directory, we would create a directory named handlers by convention, and within it our main.yml file where we would write the actual task that restarts Apache:
vagrant@ubuntu-control:~/ansible_tasks3$ sudo mkdir roles/setup_apache/handlers
vagrant@ubuntu-control:~/ansible_tasks3$ sudo vi roles/setup_apache/handlers/main.yml
Copy the content below into your main.yml file:
- name: restart_apache
tags: restart_apache
service:
name: "{{ apache }}"
state: restarted
Save and exit.
You might be asking why we have multiple main.yml files or playbooks. Well, with roles we have a main.yml file for each role to execute a specified set of task(s). Ansible knows quite well how to sort them out, as long as we name and arrange our files correctly, so we have zero worries. However, we would finally create the general playbook that brings out the big picture of our work, so hang on.
Let’s update our general playbook.yml file, run it, and view our default web page on our web browser.
---
- name: update and install packages
hosts: parent
pre_tasks:
- name: install updates
package:
update_cache: yes
state: latest
- name: install packages
package:
name:
- "{{ php }}"
- python3
- python3-pip
- python3-setuptools
- curl
state: latest
update_cache: true
- name: setup Apache and deploy our web page
hosts: webserver
roles:
- setup_apache
- deploy_webpage
Take note of how we included our roles in our playbook. The pre_tasks will run first on all our servers, while the roles for setting up Apache and deploying it will run only on our CentOS web servers.
Now, cat the public SSH key (id_ed25519.pub) we created, and copy it,
vagrant@ubuntu-control:~/ansible_tasks3$ cat ~/.ssh/id_ed25519.pub
Since we are now working with our new user (ansible3), we would run the ansible-playbook command with the -kK flags. The -kK flags stand for
- -k, — ask-pass: ask for the connection password (our connection is SSH)
- -K, — ask-become-pass: ask for the privilege escalation (sudo) password of our user
I hope you can still recall the password we assigned to our new user, ansible3 — this is our sudo password.
vagrant@ubuntu-control:~/ansible_tasks3$ ansible-playbook playbook.yml -kK
Once you press enter, you would get the prompt in the snapshot below:
Paste the public SSH key you copied earlier into the “SSH password”, and enter the sudo password of the ansible3 user. If you do not want to get this prompt, then you have to change the remote_user on your ansible.cfg file to the default user “vagrant”, and run your playbook without the -kK flags. Also, you may declare the values for the connection and the sudo password in your ansible.cfg file to avoid using the -kK flags on the command line. First, save your ansible3 user password in a file named “.become_pass” in the home directory, then update the ansible.cfg file as follows:
[defaults]
inventory=./hosts
host_key_checking=false
private_key_file=~/.ssh/id_ed25519
remote_user=ansible3
# become_password_file instead of -K
become_password_file=~/.become_pass
# connection_password_file instead of -k
connection_password_file=~/.ssh/ansible3key.pub
[privilege_escalation]
become=true
become_method=sudo
become_user=root
become_ask_pass=false
Ensure your playbook runs successfully, and copy the IP address of any of the CentOS web servers to the web browser to view your web page.
Awesome!
#3: Write a role to install Terraform (on the workstation)
We would follow the same pattern here, but this time we would target the workstation group which has our Ubuntu VMs.
vagrant@ubuntu-control:~/ansible_tasks3$ sudo mkdir -p roles/install_terraform/tasks
vagrant@ubuntu-control:~/ansible_tasks3$ sudo vi roles/install_terraform/tasks/main.yml
Then copy the code snippet below into install_terraform’s main.yml file:
- name: install terraform
unarchive:
src: https://releases.hashicorp.com/terraform/1.4.6/terraform_1.4.6_linux_amd64.zip
dest: /usr/local/bin
remote_src: yes
mode: 0755
owner: root
group: root
To unarchive a zip file, we need the unzip package along with some other dependency packages on our workstation servers. So we would create a new role which I will name as install_dependencies, to install the necessary packages.
vagrant@ubuntu-control:~/ansible_tasks3$ sudo mkdir -p roles/install_dependencies/tasks
vagrant@ubuntu-control:~/ansible_tasks3$ sudo vi roles/install_dependencies/tasks/main.yml
Then, copy this to your file:
- name: Install required system packages
package:
pkg:
- aptitude
- apt-transport-https
- ca-certificates
- software-properties-common
- virtualenv
- unzip
- gnupg
state: latest
update_cache: true
Let’s update our playbook again:
---
- name: update and install packages
hosts: parent
pre_tasks:
- name: install updates
package:
update_cache: yes
state: latest
- name: install packages
package:
name:
- "{{ php }}"
- python3
- python3-pip
- python3-setuptools
- curl
state: latest
update_cache: true
# - name: setup Apache and deploy a web page
# hosts: webserver
# roles:
# - setup_apache
# - deploy_webpage
- name: install work tools the workstation group
hosts: workstation
roles:
- install_dependencies
- install_terraform
To save time, we are going to leave our previously executed roles as comments. Also, since we want Terraform installed only on our Ubuntu workstation group, we had to create a new play with hosts set to the workstation group.
Ensure your playbook runs successfully. To confirm that Terraform was successfully installed, go to any of the hosts in the workstation group, and run:
$ terraform --version
…and voila!
# 4: Write a role to install Docker, and run an Ubuntu container with it using Ansible
This task will also be executed on the workstation group. Follow the same procedures we used in the previous tasks. Create a new directory in the roles directory, and name it install_docker. Create the tasks directory within it, and the main.yml file within the tasks directory. Then copy the code below to the main.yml file.
- name: Add Docker GPG apt Key
apt_key:
url: https://download.docker.com/linux/ubuntu/gpg
state: present
- name: Add Docker Repository
apt_repository:
repo: deb https://download.docker.com/linux/ubuntu focal stable
state: present
- name: Update apt and install docker-ce
package:
name: docker-ce
state: latest
update_cache: true
- name: Install Docker Module for Python
pip:
name: docker
- name: Pull default Docker image
community.docker.docker_image:
name: "{{ container_image }}"
source: pull
- name: Create default containers
community.docker.docker_container:
name: "{{ container_name }}"
image: "{{ container_image }}"
command: "{{ container_cmd }}"
state: present
# - name: Create default containers
# community.docker.docker_container:
# name: "{{ container_name }}{{ item }}"
# image: "{{ container_image }}"
# command: "{{ container_cmd }}"
# state: present
# with_sequence: count={{ container_count }}
The task I left as comments is virtually the same as the preceding task, only that it is used for creating multiple containers, but we only need one for now. So we return to our host_var files to define our new variables. These variables will only be defined in the Ubuntu hosts’ files since they are the only targets for this task.
# docker container variables
container_count: 1
container_name: docker
container_image: ubuntu
container_cmd: sleep 1
We return to our playbook.yml file to make a few changes. First, we would only focus on the workstation group in all our plays, since that is the only group relevant to this task. Then we are going to update it with the install_docker role and leave the previously executed Terraform role as a comment.
---
- name: update and install packages
hosts: workstation
pre_tasks:
- name: install updates
package:
update_cache: yes
state: latest
- name: install packages
package:
name:
- "{{ php }}"
- python3
- python3-pip
- python3-setuptools
- curl
state: latest
update_cache: true
# - name: setup apache and deploy web page
# hosts: webserver
# roles:
# - setup_apache
# - deploy_webpage
- name: install work-tools on workstation
hosts: workstation
roles:
- install_dependencies
# - install_terraform
- install_docker
After running your playbook successfully, run the following commands on your hosts to confirm the installation of docker and the presence of the Ubuntu docker image.
$ docker --version; sudo docker image ls
There you go!
On to the next task…
# 5: Write a role to install Jenkins and open port 8080 with Firewall
According to the instructions, this task should also be executed on the workstation group. Following the same steps as in our previous tasks, we will create an install_jenkins/tasks directory in our roles directory.
vagrant@ubuntu-control:~/ansible_tasks3$ sudo mkdir -p roles/install_docker/tasks
vagrant@ubuntu-control:~/ansible_tasks3$ sudo vi roles/install_docker/tasks/main.yml
Then copy the code below:
- name: install openJDK Java
package:
name: openjdk-11-jre
update_cache: yes
- name: install Jenkins apt-repo key
apt_key:
url: https://pkg.jenkins.io/debian/jenkins.io-2023.key
state: present
- name: install Jenkins repo
apt_repository:
repo: deb https://pkg.jenkins.io/debian-stable binary/
state: present
- name: install Jenkins
package:
name: jenkins
update_cache: yes
- name: start Jenkins
service:
name: jenkins
state: started
enabled: true
- name: Allow all access to port 8080
community.general.ufw:
rule: allow
port: '8080'
We have used the Ansible ufw (uncomplicated firewall) module to open Jenkins’ administrative port 8080. Now let’s update our playbook.
---
- name: update and install packages
hosts: workstation
pre_tasks:
- name: install updates
package:
update_cache: yes
state: latest
- name: install packages
package:
name:
- "{{ php }}"
- python3
- python3-pip
- python3-setuptools
- curl
state: latest
update_cache: true
# - name: setup apache and deploy web page
# hosts: webserver
# roles:
# - setup_apache
# - deploy_webpage
- name: install work-tools on workstation
hosts: workstation
roles:
- install_dependencies
# - install_terraform
# - install_docker
- install_jenkins
You may have to run this a couple of times due to timeout errors. Eventually, you will have a successful run. When we go to our web browser, we open Jenkins’ default port 8080 (http://192.168.53.21:8080) to view the Jenkins login page.
Awesome!
Conclusion
We have now come to the end of “ANSIBLE TUTORIALS #3: ROLES.” You can access the files created in this tutorial on our GitHub repository.
Thank you for making it this far! If you encountered any challenge not addressed within this tutorial or if you have any other Ansible concept you want us to learn about, I look forward to hearing from you — please leave a message on WhatsApp.