In this post, you will learn how to create an Ansible playbook. As an exercise, you will install an Apache Webserver onto two target machines and change the welcome page.

1. Introduction

In the two previous Ansible posts, you learned how to setup an Ansible test environment and how to create an Ansible inventory. This post continues this series, but it is not necessary to read the first two posts. In this post, you will learn how to create an Ansible playbook. A playbook consists out of one or more plays which execute tasks. The tasks call Ansible modules. Do not worry if you do not understand this yet, this is what you will learn. It is also advised to read the introduction to playbooks in the Ansible documentation.

In case you did not read the previous blogs or just as a reminder, the environment consists out of one Controller and two Target machines. The Controller and Target machines run in a VirtualBox VM. Development of the Ansible scripts is done with IntelliJ on the host machine. The files are synchronized from the host machine to the Controller by means of a script. In this blog, the machines have the following IP addresses:

  • Controller: 192.168.2.11
  • Target 1: 192.168.2.12
  • Target 2: 192.168.2.13

The files being used in this blog are available in the corresponding git repository at GitHub.

2. Prerequisites

The following prerequisites apply to this blog:

  • You need an Ansible test environment, see a previous blog how to set up a test environment;
  • You need to have basic knowledge about Ansible Inventory and Ansible Vault, see a previous blog if you do not have this knowledge;
  • If you use your own environment, you should know that Ubuntu 22.04 LTS is used for the Controller and Target machines and Ansible version 2.13.3;
  • Basic Linux knowledge.

3. Your First Playbook

As a first playbook, you will create a playbook which will ping the Target1 and Target2 machines. The playbook can be found in the git repository as playbook-ping-targets-success.yml and looks as follows:

- name: Ping target1
  hosts: target1

  tasks:
    - name: Ping test
      ansible.builtin.ping:

- name: Ping target2
  hosts: target2

  tasks:
    - name: Ping test
      ansible.builtin.ping:

Let’s see how this playbook looks like. A playbook consists out of plays. In this playbook, two plays can be found with name Ping target1 and Ping target2. For each playbook, you indicate where it needs to run by means of the hosts parameter which refers to a name in the inventory file.

A play consists out of tasks. In both plays, only one task is defined with name Ping test.

A task calls an Ansible module. A list of modules which can be used, can be found here. It is important to learn which modules exists, how to find them, how to use them, etc. The documentation for the Ping module is what you need for this example, so take the time and have a look at it.

Last thing to note is that the FQCN (Fully Qualified Collection Name) is used. This is considered to be a best practice.

Run the playbook from the Controller machine. If you use the files as-is from the git repository, you will need to enter the vault password, which is itisniceweather.

$ ansible-playbook playbook-ping-targets-success.yml -i inventory/inventory.ini --ask-vault-pass
Vault password: 

PLAY [Ping target1] ***********************************************************************************************

TASK [Gathering Facts] ********************************************************************************************
ok: [target1]

TASK [Ping test] **************************************************************************************************
ok: [target1]

PLAY [Ping target2] ***********************************************************************************************

TASK [Gathering Facts] ********************************************************************************************
ok: [target2]

TASK [Ping test] **************************************************************************************************
ok: [target2]

PLAY RECAP ********************************************************************************************************
target1                    : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
target2                    : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

The logging shows exactly which plays and which tasks are executed and whether they executed successfully.

The Ping module also provides the option to crash the command. In the Target1 play, the parameter data is added in order to let the command crash. The playbook can be found in the git repository as playbook-ping-targets-failure.yml.

- name: Ping target1
  hosts: target1

  tasks:
    - name: Ping test
      ansible.builtin.ping:
        data: crash
...

Executing this playbook will crash the Target1 play and the playbook just ends.

$ ansible-playbook playbook-ping-targets-failure.yml -i inventory/inventory.ini --ask-vault-pass
Vault password: 

PLAY [Ping target1] ***********************************************************************************************

TASK [Gathering Facts] ********************************************************************************************
ok: [target1]

TASK [Ping test] **************************************************************************************************
An exception occurred during task execution. To see the full traceback, use -vvv. The error was: Exception: boom
fatal: [target1]: FAILED! => {"changed": false, "module_stderr": "Shared connection to 192.168.2.12 closed.\r\n", "module_stdout": "Traceback (most recent call last):\r\n  File \"/home/osboxes/.ansible/tmp/ansible-tmp-1662800777.2553337-6094-259627128894774/AnsiballZ_ping.py\", line 107, in <module>\r\n    _ansiballz_main()\r\n  File \"/home/osboxes/.ansible/tmp/ansible-tmp-1662800777.2553337-6094-259627128894774/AnsiballZ_ping.py\", line 99, in _ansiballz_main\r\n    invoke_module(zipped_mod, temp_path, ANSIBALLZ_PARAMS)\r\n  File \"/home/osboxes/.ansible/tmp/ansible-tmp-1662800777.2553337-6094-259627128894774/AnsiballZ_ping.py\", line 47, in invoke_module\r\n    runpy.run_module(mod_name='ansible.modules.ping', init_globals=dict(_module_fqn='ansible.modules.ping', _modlib_path=modlib_path),\r\n  File \"/usr/lib/python3.10/runpy.py\", line 209, in run_module\r\n    return _run_module_code(code, init_globals, run_name, mod_spec)\r\n  File \"/usr/lib/python3.10/runpy.py\", line 96, in _run_module_code\r\n    _run_code(code, mod_globals, init_globals,\r\n  File \"/usr/lib/python3.10/runpy.py\", line 86, in _run_code\r\n    exec(code, run_globals)\r\n  File \"/tmp/ansible_ansible.builtin.ping_payload_xnphtwh8/ansible_ansible.builtin.ping_payload.zip/ansible/modules/ping.py\", line 89, in <module>\r\n  File \"/tmp/ansible_ansible.builtin.ping_payload_xnphtwh8/ansible_ansible.builtin.ping_payload.zip/ansible/modules/ping.py\", line 79, in main\r\nException: boom\r\n", "msg": "MODULE FAILURE\nSee stdout/stderr for the exact error", "rc": 1}

PLAY RECAP ********************************************************************************************************
target1                    : ok=1    changed=0    unreachable=0    failed=1    skipped=0    rescued=0    ignored=0   

4. Install Apache Webserver

In this second exercise, you will install Apache Webserver on a target machine and change the welcome page. The final playbook can be found in the git repository as playbook-httpd-target1.yml. You will learn in this section how to create this final version.

4.1 Install Package

For installing packages, you can use the Apt module. It contains many parameters, you will only use a few:

  • name: the name of the package to be installed;
  • update_cache: runs apt-get update before installation;
  • state: indicates the desired package state, present is just fine here.

The other items in this playbook should be quite familiar by now.

- name: Install Apache webserver
  hosts: target1

  tasks:
    - name: Install apache httpd  (state=present is optional)
      ansible.builtin.apt:
        name: apache2
        update_cache: yes
        state: present

Run the playbook.

$ ansible-playbook playbook-httpd-target1.yml -i inventory/inventory.ini --ask-vault-pass
Vault password: 

PLAY [Install Apache webserver] *****************************************************************************************

TASK [Gathering Facts] **************************************************************************************************
ok: [target1]

TASK [Install apache httpd  (state=present is optional)] ****************************************************************

This playbook does not end. It hangs and you can stop it with CTRL+C.

So what is happening here? As you probably know, in order to install packages you need sudo privileges. One way or the other, Ansible needs to know whether privilege escalation is needed and you will need to provide the sudo password to Ansible. A detailed description can be read in the Ansible documentation. The short version is, that you need to add the become parameter with value yes. But that is not all, you also need to add the command line parameter --ask-become-pass when running the Ansible playbook. This way, Ansible will ask you for the sudo password.

The playbook with the added become parameter looks as follows:

- name: Install Apache webserver
  hosts: target1
  become: yes

  tasks:
    - name: Install apache httpd  (state=present is optional)
      ansible.builtin.apt:
        name: apache2
        update_cache: yes
        state: present

Running this playbook is successfull. As you can see, the become password and the vault password need to be entered.

$ ansible-playbook playbook-httpd-target1.yml -i inventory/inventory.ini --ask-vault-pass --ask-become-pass
BECOME password: 
Vault password: 

PLAY [Install Apache webserver] ****************************************************************************************

TASK [Gathering Facts] *************************************************************************************************
ok: [target1]

TASK [Install apache httpd  (state=present is optional)] ***************************************************************
changed: [target1]

PLAY RECAP *************************************************************************************************************
target1                    : ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0  

In the output logging, you also notice that Target1 has been changed at line 11. Remember this, this will be important later on when the playbook is run again.

Navigate via your browser (or by means of the curl command) to the IP address of the Target1 machine: http://192.16.2.12. You can execute this from your host machine if you have a similar test environment as used in this blog. As you can see, the Apache Webserver default welcome page is shown.

4.2 Change Welcome Page

In the playbook, you can also change the contents of the welcome page. You can use the copy module for that. Add the following task to the playbook.

- name: Create index page
  ansible.builtin.copy:
    content: 'Hello world from target 1'
    dest: /var/www/html/index.html

Execute the playbook.

$ ansible-playbook playbook-httpd-target1.yml -i inventory/inventory.ini --ask-vault-pass --ask-become-pass
BECOME password: 
Vault password: 

PLAY [Install Apache webserver] ****************************************************************************************************************************

TASK [Gathering Facts] *************************************************************************************************************************************
ok: [target1]

TASK [Install apache httpd  (state=present is optional)] ***************************************************************************************************
ok: [target1]

TASK [Create index page] ***********************************************************************************************************************************
changed: [target1]

PLAY RECAP *************************************************************************************************************************************************
target1                    : ok=3    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0 

First, take a closer look at the logging. The task Install apache httpd now just returns ok and not changed. This means that Ansible did not install Apache Webserver again. Ansible tasks are idempotent. This means that you can execute them over and over again, the result will be the same. Also note that the welcome page has been changed now. Verify this via the browser or via curl.

$ curl http://192.168.2.12
Hello world from target 1

4.3 Install Target2

As a last exercise, you can add a second play for installing Apache Webserver on Target2 and change the welcome page accordingly in order that it welcomes you from Target2. The playbook can be found in the git repository as playbook-httpd-target1-and-target2.yml.

- name: Install Apache webserver for target 1
  hosts: target1
  become: yes

  tasks:
    - name: Install apache httpd  (state=present is optional)
      ansible.builtin.apt:
        name: apache2
        update_cache: yes
        state: present

    - name: Create index page for target 1
      ansible.builtin.copy:
        content: 'Hello world from target 1'
        dest: /var/www/html/index.html

- name: Install Apache webserver for target2
  hosts: target2
  become: yes

  tasks:
    - name: Install apache httpd  (state=present is optional)
      ansible.builtin.apt:
        name: apache2
        update_cache: yes
        state: present

    - name: Create index page for target 2
      ansible.builtin.copy:
        content: 'Hello world from target 2'
        dest: /var/www/html/index.html

Execute the playbook, you are now confident enough to explore the logging yourself.

$ ansible-playbook playbook-httpd-target1-and-target2.yml -i inventory/inventory.ini --ask-vault-pass --ask-become-pass
BECOME password: 
Vault password: 

PLAY [Install Apache webserver for target 1] *****************************************************************************************************************************

TASK [Gathering Facts] ***************************************************************************************************************************************************
ok: [target1]

TASK [Install apache httpd  (state=present is optional)] *****************************************************************************************************************
ok: [target1]

TASK [Create index page for target 1] ************************************************************************************************************************************
ok: [target1]

PLAY [Install Apache webserver for target2] ******************************************************************************************************************************

TASK [Gathering Facts] ***************************************************************************************************************************************************
ok: [target2]

TASK [Install apache httpd  (state=present is optional)] *****************************************************************************************************************
changed: [target2]

TASK [Create index page for target 2] ************************************************************************************************************************************
changed: [target2]

PLAY RECAP ***************************************************************************************************************************************************************
target1                    : ok=3    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
target2                    : ok=3    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0  

Verify whether the welcome pages are changed correctly.

$ curl http://192.168.2.12
Hello world from target 1
$ curl http://192.168.2.13
Hello world from target 2

Just as expected!

5. Conclusion

In this post, you continued your journey towards learning Ansible. You learned the basics about Ansible playbooks and you wrote and executed a playbook which installs Apache Webserver onto the two target machines. You are now able to write your own playbooks and continue to learn.