Chapter 8: Simplifying Playbooks with Roles and Collections
Exercise: Creating roles
You will turn the setup of an Apache virtual host into a role, myvhost: a skeleton from ansible-galaxy, tasks, a handler and a template. Then you will use it from a play that also has tasks before and after the role, and check the result with three small verification playbooks.
The project ~/role-create has an ansible.cfg, an inventory with servera.lab.example.com in the webservers group, a template vhost.conf.j2, and three playbooks named verify-*.yml.
Creating roles
- Create a role that uses variables and a handler, and use it in a play together with pre_tasks and post_tasks.
Prerequisites: Complete control-node setup and the preceding lessons in this chapter. Run commands as student on workstation unless a step names another machine.
From the project directory, run the relevant home-lab check. For an exercise with several checkpoints, grade each state before changing or removing it:
lab grade role-createThe grader reads project files and host state. A passing report covers the listed checks; also perform the task’s independent connection, repeat-run, and reboot checks where requested.
Apply the role to one additional lab host with a different host name and verify its document root.
Record the published result first. The grader checks the original requirements, so a changed name or value may intentionally fail those checks.
Build myvhost as a reusable role and apply it through use-vhost-role.yml to webservers.
- The role must install Apache, start and enable it, and deploy the supplied virtual-host template with a restart on configuration change.
- Keep the template and handler within the role; web-page content belongs to this project outside the role.
- The play must announce before/after stages and publish the page to the host-specific document root.
- Verify configuration, content, handler timing, and a repeat run. At home, HTTP must also be allowed through the firewall.
Create the role skeleton
[student@workstation ~]$ cd ~/role-create
[student@workstation role-create]$ mkdir -v roles
mkdir: created directory 'roles'
[student@workstation role-create]$ cd roles
[student@workstation roles]$ ansible-galaxy init myvhost
- Role myvhost was created successfullyThis role needs no defaults, internal variables or tests. Remove those directories, then go back to the project:
[student@workstation roles]$ rm -rvf myvhost/{defaults,vars,tests}
removed 'myvhost/defaults/main.yml'
removed directory 'myvhost/defaults'
removed 'myvhost/vars/main.yml'
removed directory 'myvhost/vars'
removed 'myvhost/tests/test.yml'
removed 'myvhost/tests/inventory'
removed directory 'myvhost/tests'
[student@workstation roles]$ cd ..ansible-galaxy init and ansible-galaxy role init are the same command; the short form is older.
Write the tasks
Replace the contents of roles/myvhost/tasks/main.yml with three tasks: install httpd, start and enable it, and install the virtual host configuration from a template, notifying a handler.
---
# tasks file for myvhost
- name: Ensure httpd is installed
ansible.builtin.dnf:
name: httpd
state: latest
- name: Ensure httpd is started and enabled
ansible.builtin.service:
name: httpd
state: started
enabled: true
- name: vhost file is installed
ansible.builtin.template:
src: vhost.conf.j2
dest: /etc/httpd/conf.d/vhost.conf
owner: root
group: root
mode: "0644"
notify:
- restart httpdsrc: vhost.conf.j2 has no directory: inside a role, the template module looks in the role's own templates/ directory.
Write the handler
---
# handlers file for myvhost
- name: restart httpd
ansible.builtin.service:
name: httpd
state: restartedMove the template into the role
Look at the template the project provides. Every host-specific value comes from a fact, so the role needs no variables of its own:
# {{ ansible_managed }}
<VirtualHost *:80>
ServerAdmin webmaster@{{ ansible_facts['fqdn'] }}
ServerName {{ ansible_facts['fqdn'] }}
ErrorLog logs/{{ ansible_facts['hostname'] }}-error.log
CustomLog logs/{{ ansible_facts['hostname'] }}-common.log common
DocumentRoot /var/www/vhosts/{{ ansible_facts['hostname'] }}/
<Directory /var/www/vhosts/{{ ansible_facts['hostname'] }}/>
Options +Indexes +FollowSymlinks +Includes
Require all granted
</Directory>
</VirtualHost>[student@workstation role-create]$ mv -v vhost.conf.j2 roles/myvhost/templates/
renamed 'vhost.conf.j2' -> 'roles/myvhost/templates/vhost.conf.j2'Create the web content
The content is not part of the role: it belongs to this project. Put it in files/html/:
[student@workstation role-create]$ mkdir -pv files/html
mkdir: created directory 'files'
mkdir: created directory 'files/html'
[student@workstation role-create]$ echo 'simple index' > files/html/index.htmlUse the role in a play
Create use-vhost-role.yml. The play for webservers prints a message before the role, applies myvhost, then copies the content and prints a message after it:
---
- name: Use myvhost role playbook
hosts: webservers
pre_tasks:
- name: pre_tasks message
ansible.builtin.debug:
msg: 'Ensure web server configuration.'
roles:
- myvhost
post_tasks:
- name: HTML content is installed
ansible.builtin.copy:
src: files/html/
dest: "/var/www/vhosts/{{ ansible_facts['hostname'] }}"
- name: post_tasks message
ansible.builtin.debug:
msg: 'Web server is configured.'The trailing / on src: files/html/ copies the contents of the directory. Without it, copy would create an html directory inside the destination.
Run it
[student@workstation role-create]$ ansible-navigator run \
> -m stdout use-vhost-role.yml --syntax-check
playbook: /home/student/role-create/use-vhost-role.yml
[student@workstation role-create]$ ansible-navigator run -m stdout use-vhost-role.yml
PLAY [Use myvhost role playbook] ***********************************************
TASK [Gathering Facts] *********************************************************
ok: [servera.lab.example.com]
TASK [pre_tasks message] *******************************************************
ok: [servera.lab.example.com] => {
"msg": "Ensure web server configuration."
}
TASK [myvhost : Ensure httpd is installed] *************************************
changed: [servera.lab.example.com]
TASK [myvhost : Ensure httpd is started and enabled] ***************************
changed: [servera.lab.example.com]
TASK [myvhost : vhost file is installed] ***************************************
changed: [servera.lab.example.com]
RUNNING HANDLER [myvhost : restart httpd] **************************************
changed: [servera.lab.example.com]
TASK [HTML content is installed] ***********************************************
changed: [servera.lab.example.com]
TASK [post_tasks message] ******************************************************
ok: [servera.lab.example.com] => {
"msg": "Web server is configured."
}
PLAY RECAP *********************************************************************
servera.lab.example.com : ok=8 changed=5 unreachable=0 failed=0 ...Notice where the handler ran: after the role's tasks and before post_tasks. Handlers notified in each stage of a play run at the end of that stage.
Verify the service
The project's verification playbooks each run a command and print what it returned:
[student@workstation role-create]$ ansible-navigator run -m stdout verify-httpd.yml
...output omitted...
TASK [Is the httpd service installed] ******************************************
ok: [servera.lab.example.com] => {
"installed.stdout": "httpd-2.4.62-13.el9_8.6.x86_64"
}
...output omitted...
TASK [Is the httpd service started] ********************************************
ok: [servera.lab.example.com] => {
"started.stdout": "active"
}
...output omitted...
TASK [Is the httpd service enabled] ********************************************
ok: [servera.lab.example.com] => {
"enabled.stdout": "enabled"
}
...output omitted...The exact httpd version depends on your repositories.
Verify the configuration and the content
[student@workstation role-create]$ ansible-navigator run -m stdout verify-config.yml
...output omitted...
TASK [What does the httpd config file contain] *********************************
ok: [servera.lab.example.com] => {
"config.stdout_lines": [
"# Ansible managed",
"",
"<VirtualHost *:80>",
" ServerAdmin webmaster@servera.lab.example.com",
" ServerName servera.lab.example.com",
...output omitted...
[student@workstation role-create]$ ansible-navigator run -m stdout verify-content.yml
...output omitted...
TASK [What does the index.html file contain] ***********************************
ok: [servera.lab.example.com] => {
"content.stdout": "simple index"
}
...output omitted...Finally, ask the web server itself:
[student@workstation role-create]$ curl http://servera.lab.example.com
simple indexThe home-lab servers run firewalld with only SSH open, and this role does not touch the firewall. Open http with an ad hoc command first, then ask:
[student@workstation role-create]$ ansible webservers -m ansible.posix.firewalld \
> -a 'service=http permanent=true immediate=true state=enabled'
servera.lab.example.com | CHANGED => {
...output omitted...
[student@workstation role-create]$ curl http://servera.lab.example.com
simple indexRun use-vhost-role.yml once more: ok=7 changed=0. Nothing changed, so the handler did not run.
Finish
Check your work with lab grade role-create, then put the exercise away with lab finish role-create.
Try it again, differently
Give the role a defaults/main.yml with vhost_admin: webmaster, use it in the template as ServerAdmin {{ vhost_admin }}@{{ ansible_facts['fqdn'] }}, and override it in the play with vars: under the role entry. Run it and look at the diff with --diff.