Welcome back to the Ansible 101 Guide. In this part of the tutorial, I will try to show you how to use playbooks. I will also show you what tasks and handlers are and how we use them.
Playbooks
We can use playbooks to define reusable Ansible code. A playbook consists of one or more plays. For example, we could use a playbook called "webserver.yml" to manage our web servers.
Let's try to learn the structure of a playbook using the following example:
- name: webserver install 
  hosts: webservers
  tasks: 
    - name: install apache2
      ansible.builtin.apt:
        name: apache2
        state: present
        update_cache: yes
      become: true
    - name: enable and start apache2 systemd service
      ansible.builtin.systemd:
        name: apache2
        enabled: true
        state: startedLet's investigate this line by line!
Line 1 - name
A descriptive name for the play can be assigned in the ‘name’ field in line 1. This parameter is optional, but I recommend naming each play as meaningfully as possible.
Line 2 - hosts
In line 2, we define the target systems for this play with the ‘hosts’ field. We have several options here:
Group(s)
We can define one or more groups from our inventory as a target:
- name: Play for hosts in one group
  hosts: Group1
  tasks:
    - name: Example Task 1
      ansible.builtin.apt:
        name: apache2
        state: present- name: Play for two groups
  hosts: 
    - Group1
    - Group2
  tasks:
    - name: Example Task 1
      ansible.builtin.apt:
        name: apache2
        state: presentHosts
Alternatively, we can also specify the hosts from the inventory directly as the target:
- name: Play for one host
  hosts: webserver1
  tasks:
    - name: Example Task 1
      ansible.builtin.apt:
        name: apache2
        state: present- name: Play for two hosts
  hosts: 
    - webserver1
    - webserver2
  tasks:
    - name: Example Task 1
      ansible.builtin.apt:
        name: apache2
        state: presentThere are a few other options in this section that we can use to influence the target list. For example, we can exclude individual hosts or groups from the target definition. More on this in a later article.
Line 3 - Begin of task definitions
This is where the list of individual tasks for this play begins. Lets see how a task is build in Ansible.
Lines 4 to 9 - Task definition
This brings us to the structure of a task. Note the indentation of each element. We distinguish between parameters that belong to the task and parameters that belong to the module.
The 'name' field (line 4) is optional, as with Play, but highly recommended, as it will also appear later in the output. After all, we want to know what is happening. Line 5 is the name of the module we want to use. In this case we want to use the 'apt' module to install a package. Everything that appears one indent further down is a module-level parameter, i.e. it refers to the module, not the task itself. This is where many people get confused.
Task parameters are parameters that affect the task itself, i.e. they can be defined for each task. Module parameters, on the other hand, are different for each module. For example, the Copy module needs information about the source and destination paths, while the 'apt' module needs the name of the package to be installed.
In our example, we use the 'apt' module to install Apache2 with the following parameters
- name: name of the package to install
 - state: target state:
- present = installed
 - absent = uninstalled
 - latest = always keep on latest version
 
 - update_cache: forces to package cache to be updated beforehand
 
Line 9 contains the definition of another task level parameter (note the indentation). With 'become: true' we basically specify that superuser privileges are required to run this task. We will need this parameter very often, so I will devote a separate part of this series to it.
A note on module parameters
It is important to note that there are some module parameters that are optional and some that must be specified. However, the Ansible documentation is very helpful here. For our example, we can find the information on these pages:
Documentation for apt module

Documentation for systemd module

Example
Let us now apply what we have learned to our example scenario. Let's assume that our first two hosts are web servers and the third is a database host.
We will therefore adapt our inventory.txt accordingly:
~/ansible-guide/inventory.txt
[webservers]
ansible-guide-1 ansible_ssh_user=ansible ansible_host=192.168.0.11
ansible-guide-2 ansible_ssh_user=ansible ansible_host=192.168.0.12
[db]
ansible-guide-3 ansible_ssh_user=ansible ansible_host=192.168.0.13I would now like to divide the setup into two playbooks. Depending on your own preferences and use case, we could also define two plays within one playbook.
Playbook 1: ~/ansible-guide/webservers.yml
- name: webserver setup
  hosts: webservers
  tasks:
    - name: Apache2 Setup
      ansible.builtin.apt:
        name: apache2
        state: present
        update_cache: true
      become: true
    - name: start and enable apache2
      ansible.builtin.systemd:
        name: apache2
        state: started
        enabled: true
      become: trueNow we can run the playbooks:
cd ~/ansible-guide
ansible-playbook -i inventory.txt webservers.ymlPLAY [webserver setup] ****************************************************************************
TASK [Gathering Facts] ****************************************************************************
ok: [ansible-guide-1]
ok: [ansible-guide-2]
TASK [Apache2 Setup] ****************************************************************************
changed: [ansible-guide-1]
changed: [ansible-guide-2]
TASK [start and enable apache2] ****************************************************************************
ok: [ansible-guide-1]
ok: [ansible-guide-2]
PLAY RECAP ****************************************************************************
ansible-guide-1                  : ok=3    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0 
ansible-guide-2                  : ok=3    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0 
It is interesting to note that the task 'start and enable apache2' returns the status 'ok' and not 'changed' as expected. This is because enabling and starting the service is already defined in the installation package. So there is nothing for Ansible to do at this point.
Now we can quickly test that our installation has worked by entering one of the IP addresses in a browser. We should now see the default Apache web server page.
Playbook 2: ~/ansible-guide/database.yml
- name: database setup
  hosts: db
  tasks:
    - name: MySQL Setup
      ansible.builtin.apt:
        name: mysql-server
        state: present
        update_cache: true
      become: true
    - name: start and enable MySQL
      ansible.builtin.systemd:
        name: mysql
        state: started
        enabled: true
      become: trueWe now do the same with the database server playbook:
cd ~/ansible-guide
ansible-playbook -i inventory.txt database.ymlPLAY [database setup] ****************************************************************************
TASK [Gathering Facts] ****************************************************************************
ok: [ansible-guide-3]
TASK [MySQL Setup] ****************************************************************************
changed: [ansible-guide-3]
TASK [start and enable MySQL] ****************************************************************************
ok: [ansible-guide-3]
PLAY RECAP ****************************************************************************
ansible-guide-3                 : ok=3    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   Deploy customized index.html
We have now done a standard installation of Apache2 and MySQL using Ansible. In the next step, I will show you how we can deploy our own index.html file, for example.
To do this, we will first create the file locally on the Ansible controller:
cd ~/ansible-guide
mkdir files
echo "<h1>My own index.html!</h1>" > files/index.html This results in the following file content:
<h1>My own index.html!</h1>Now we add a task to our webservers.yml playbook:
- name: webserver setup
  hosts: webservers
  tasks:
    - name: Apache2 Setup
      ansible.builtin.apt:
        name: apache2
        state: present
        update_cache: true
      become: true
    - name: start and enable apache2
      ansible.builtin.systemd:
        name: apache2
        state: started
        enabled: true
      become: true
    - name: copy index.html
      ansible.builtin.copy:
        src: files/index.html
        dest: /var/www/html/index.html
      become: trueWe are using the Ansible 'copy' module here. This can be used to copy files from the Ansible controller to the target systems. We give the module 2 parameters:
src: Source file path on the Ansible controller
dest: destination path on the remote hosts
We also tell Ansible again with 'become: true' that root permissions are required for this process.
We now run the updated playbook:
ansible-playbook -i inventory.txt webservers.ymlLAY [webserver setup] ****************************************************************************
TASK [Gathering Facts] ****************************************************************************
ok: [ansible-guide-1]
ok: [ansible-guide-2]
TASK [Apache2 Setup] ****************************************************************************
ok: [ansible-guide-1]
ok: [ansible-guide-2]
TASK [start and enable apache2] ****************************************************************************
ok: [ansible-guide-1]
ok: [ansible-guide-2]
TASK [copy index.html] ****************************************************************************
changed: [ansible-guide-1]
changed: [ansible-guide-2]
PLAY RECAP ****************************************************************************
ansible-guide-1                  : ok=4    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
ansible-guide-2                  : ok=4    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
Let's check that with curl:
apt install -y curl
curl http://192.168.0.11<h1>My own index.html!</h1>
Handlers
We have now installed our web server and a database. We have also uploaded our own content to our web server. Finally , I would like to show you how we can also manage the Apache configuration file using Ansible. We want to notify the
web server when changes are made to it (and only then!). For this
handlers.
So we store the Apache configuration on the
Ansible controller. To do this, I simply copied the contents of /etc/apache2 from one of the test hosts
I simply copied the contents of /etc/apache2/apache2.conf from one of the test hosts and trimmed the
comment lines a little to make the whole thing a little more
clearer.
We put it back in our files subdirectory:
~/ansible-guide/files/apache2.conf
DefaultRuntimeDir ${APACHE_RUN_DIR}
PidFile ${APACHE_PID_FILE}
Timeout 300
KeepAlive On
MaxKeepAliveRequests 100
KeepAliveTimeout 5
User ${APACHE_RUN_USER}
Group ${APACHE_RUN_GROUP}
HostnameLookups Off
ErrorLog ${APACHE_LOG_DIR}/error.log
LogLevel warn
IncludeOptional mods-enabled/*.load
IncludeOptional mods-enabled/*.conf
Include ports.conf
<Directory />
	Options FollowSymLinks
	AllowOverride None
	Require all denied
</Directory>
<Directory /usr/share>
	AllowOverride None
	Require all granted
</Directory>
<Directory /var/www/>
	Options Indexes FollowSymLinks
	AllowOverride None
	Require all granted
</Directory>
AccessFileName .htaccess
<FilesMatch "^\.ht">
	Require all denied
</FilesMatch>
LogFormat "%v:%p %h %l %u %t \"%r\" %>s %O \"%{Referer}i\" \"%{User-Agent}i\"" vhost_combined
LogFormat "%h %l %u %t \"%r\" %>s %O \"%{Referer}i\" \"%{User-Agent}i\"" combined
LogFormat "%h %l %u %t \"%r\" %>s %O" common
LogFormat "%{Referer}i -> %U" referer
LogFormat "%{User-agent}i" agent
IncludeOptional conf-enabled/*.conf
IncludeOptional sites-enabled/*.confWe extend our playbook with another task:
~/ansible-guide/webservers.yml
- name: webserver setup
  hosts: webservers
  tasks:
    - name: Apache2 Setup
      ansible.builtin.apt:
        name: apache2
        state: present
        update_cache: true
      become: true
    - name: start and enable apache2
      ansible.builtin.systemd:
        name: apache2
        state: started
        enabled: true
      become: true
    - name: copy index.html
      ansible.builtin.copy:
        src: files/index.html
        dest: /var/www/html/index.html
      become: true
    - name: copy apache2.conf
      ansible.builtin.copy:
        src: files/apache2.conf
        dest: /etc/apache2/apache2.conf
      become: trueIf we were to run the whole thing again now, Ansible would copy both apache2.conf and index.html to the target systems. However, we also want to implement the automatic restart of the Apache service when changes are made. So we will add a handler and call it directly in the new task. I have added comments to the most important lines:
- name: webserver setup
  hosts: webservers
  handlers:                     
    - name: restart-apache      
      ansible.builtin.systemd:
        name: apache2
        state: restarted
      become: true
  tasks:
    - name: Apache2 Setup
      ansible.builtin.apt:
        name: apache2
        state: present
        update_cache: true
      become: true
    - name: start and enable apache2
      ansible.builtin.systemd:
        name: apache2
        state: started
        enabled: true
      become: true
    - name: copy index.html
      ansible.builtin.copy:
        src: files/index.html
        dest: /var/www/html/index.html
      become: true
    - name: copy apache2.conf
      ansible.builtin.copy:
        src: files/apache2.conf
        dest: /etc/apache2/apache2.conf
      notify: restart-apache                 
      become: trueWe have now created another list on the same level as "Tasks". This contains our handler definitions. A handler has exactly the same structure as a task, but is not automatically executed when the playbook is called. This only happens if at least two things are specified:
- A task has the name of our handler - here "restart-apache" - defined in the "notify" parameter.
 - At least one task that fulfills point 1 returns the status "changed". Only then will the handlers be executed.
 
Now we run the playbook again:
ansible-playbook -i inventory.txt webserver.ymlPLAY [webserver setup] ***********************************************************************************
TASK [Gathering Facts] ***********************************************************************************
ok: [ansible-guide-1]
ok: [ansible-guide-2]
TASK [Apache2 Setup] ***********************************************************************************
ok: [ansible-guide-1]
ok: [ansible-guide-2]
TASK [start and enable apache2] ***********************************************************************************
ok: [ansible-guide-1]
ok: [ansible-guide-2]
TASK [copy index.html] ***********************************************************************************
ok: [ansible-guide-1]
ok: [ansible-guide-2]
TASK [copy apache2.conf] ***********************************************************************************
changed: [ansible-guide-1]
changed: [ansible-guide-2]
RUNNING HANDLER [restart-apache] ***********************************************************************************
changed: [ansible-guide-1]
changed: [ansible-guide-2]
PLAY RECAP ***********************************************************************************
ansible-guide-1                  : ok=6    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0 
ansible-guide-2                  : ok=6    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0 The apache2.conf is now also managed by Ansible and the service will be restarted if necessary! Try this out a few more times by changing some parameters in the configuration and running the playbook.
Important note about handlers
As you can see in the output above, handlers are always executed at the end of the play and not immediately after the calling task. This ensures that, for example, the Apache restart is only executed once, even if multiple tasks make changes and call the handler.
If you want to explicitly restart at this point, you must define the restart as a task.
That's it for now!
In the next part I will show you how to put values into variables and where you can define them. I will also introduce you to some other useful modules!
From my own experience, I can strongly recommend that you just try it all out for yourself and try to apply what you have learned to your own use cases. It's the best way to learn :)
As always, I welcome feedback and suggestions for improvement. Feel free to use the comment function here or send an email to: [email protected].
Have fun trying it out!
Mow
							