Chapter 7: Managing Complex Plays and Playbooks

Exercise: Managing complex plays and playbooks

You have inherited a working playbook from the previous administrator of four web servers. It does its job, but it lists every host by hand and keeps every task in one play. Make it easier to maintain with a host pattern, task files and one reusable, parameterised task file. Solutions are hidden under each task. Try each one yourself first.

The project ~/projects-review has an ansible.cfg, an inventory, the inherited playbook.yml, a small host-test.yml for trying out patterns, and files/tune.conf.

Managing Complex Plays and Playbooks

  • Simplify the hosts of a play with a host pattern.
  • Move tasks into task files and import them.
  • Replace similar tasks with one task file driven by variables.

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 projects-review

The 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.

Parameterize the tuning-file destination and explain which layer should supply its value.

Record the published result first. The grader checks the original requirements, so a changed name or value may intentionally fail those checks.

Refactor the inherited playbook without changing the desired host state.

  • One wildcard host pattern must select exactly servera through serverd; verify it with host-test.yml.
  • Web and firewall work must be separated into tasks/web_tasks.yml and tasks/firewall_tasks.yml.
  • Their shared package-and-service behavior must appear once in tasks/install_and_enable.yml, parameterized by package and service.
  • The refactored playbook.yml must leave both services running, HTTP allowed, and the supplied tuning configuration installed. Repeat it and check for unintended changes.

Read the inherited playbook

[student@workstation ~]$ cd ~/projects-review
[student@workstation projects-review]$ cat playbook.yml
---
- name: Install and configure web service
  hosts:
    - servera.lab.example.com
    - serverb.lab.example.com
    - serverc.lab.example.com
    - serverd.lab.example.com
  tasks:
    - name: Install httpd
      ansible.builtin.dnf:
        name: httpd
        state: latest

    - name: Enable and start httpd
      ansible.builtin.service:
        name: httpd
        enabled: true
        state: started

    - name: Tuning configuration installed
      ansible.builtin.copy:
        src: files/tune.conf
        dest: /etc/httpd/conf.d/tune.conf
        owner: root
        group: root
        mode: "0644"
      notify:
        - restart httpd

    - name: Install firewalld
      ansible.builtin.dnf:
        name: firewalld
        state: latest

    - name: Enable and start the firewall
      ansible.builtin.service:
        name: firewalld
        enabled: true
        state: started

    - name: Open the port for http
      ansible.posix.firewalld:
        service: http
        immediate: true
        permanent: true
        state: enabled

  handlers:
    - name: restart httpd
      ansible.builtin.service:
        name: httpd
        state: restarted

Six tasks in two groups of three: the web server, then the firewall. Both groups start with the same two steps: install a package, then enable and start its service.

Replace the host list with a pattern

Find one wildcard pattern that selects exactly the four servers. Test it in host-test.yml first, then use it in playbook.yml.

Put the pattern in host-test.yml and run it:

---
- name: List inventory hostnames
  hosts: server*.lab.example.com
  gather_facts: false
  tasks:
    - name: List inventory hostnames
      ansible.builtin.debug:
        msg: "{{ inventory_hostname }}"
[student@workstation projects-review]$ ansible-navigator run -m stdout host-test.yml
...output omitted...
ok: [servera.lab.example.com] => {
    "msg": "servera.lab.example.com"
}
ok: [serverb.lab.example.com] => {
    "msg": "serverb.lab.example.com"
}
ok: [serverc.lab.example.com] => {
    "msg": "serverc.lab.example.com"
}
ok: [serverd.lab.example.com] => {
    "msg": "serverd.lab.example.com"
}
...output omitted...

Exactly the four servers, so replace the list in playbook.yml:

- name: Install and configure web service
  hosts: server*.lab.example.com

Move the web tasks to a task file

Move the first three tasks into tasks/web_tasks.yml, and import that file where they were.

[student@workstation projects-review]$ mkdir tasks
---
- name: Install httpd
  ansible.builtin.dnf:
    name: httpd
    state: latest

- name: Enable and start httpd
  ansible.builtin.service:
    name: httpd
    enabled: true
    state: started

- name: Tuning configuration installed
  ansible.builtin.copy:
    src: files/tune.conf
    dest: /etc/httpd/conf.d/tune.conf
    owner: root
    group: root
    mode: "0644"
  notify:
    - restart httpd

In playbook.yml, in place of the three tasks:

    - name: Import the web_tasks.yml task file
      ansible.builtin.import_tasks: tasks/web_tasks.yml

The handler stays in the play. A task in an imported file can notify it, because handlers are looked up in the play.

Move the firewall tasks to a task file

Do the same with the last three tasks, in tasks/firewall_tasks.yml.

---
- name: Install firewalld
  ansible.builtin.dnf:
    name: firewalld
    state: latest

- name: Enable and start the firewall
  ansible.builtin.service:
    name: firewalld
    enabled: true
    state: started

- name: Open the port for http
  ansible.posix.firewalld:
    service: http
    immediate: true
    permanent: true
    state: enabled
    - name: Import the firewall_tasks.yml task file
      ansible.builtin.import_tasks: tasks/firewall_tasks.yml

One task file for both install-and-start steps

Both task files begin with the same pattern: install a package, enable and start a service. Write that pattern once, in tasks/install_and_enable.yml, with the variables package and service. Then replace those two tasks in web_tasks.yml and in firewall_tasks.yml with an import of the new file, passing the right values.

---
- name: Install {{ package }}
  ansible.builtin.dnf:
    name: "{{ package }}"
    state: latest

- name: Enable and start {{ service }}
  ansible.builtin.service:
    name: "{{ service }}"
    enabled: true
    state: started
---
- name: Install and start httpd
  ansible.builtin.import_tasks: install_and_enable.yml
  vars:
    package: httpd
    service: httpd

- name: Tuning configuration installed
  ansible.builtin.copy:
    src: files/tune.conf
    dest: /etc/httpd/conf.d/tune.conf
    owner: root
    group: root
    mode: "0644"
  notify:
    - restart httpd
---
- name: Install and start firewalld
  ansible.builtin.import_tasks: install_and_enable.yml
  vars:
    package: firewalld
    service: firewalld

- name: Open the port for http
  ansible.posix.firewalld:
    service: http
    immediate: true
    permanent: true
    state: enabled

Inside a task file, the path of another task file is relative to the file doing the importing, so install_and_enable.yml needs no tasks/ prefix. files/tune.conf, on the other hand, is still found next to the playbook.

Check the finished playbook, then run it

---
- name: Install and configure web service
  hosts: server*.lab.example.com
  tasks:
    - name: Import the web_tasks.yml task file
      ansible.builtin.import_tasks: tasks/web_tasks.yml

    - name: Import the firewall_tasks.yml task file
      ansible.builtin.import_tasks: tasks/firewall_tasks.yml

  handlers:
    - name: restart httpd
      ansible.builtin.service:
        name: httpd
        state: restarted
[student@workstation projects-review]$ ansible-navigator run \
> -m stdout playbook.yml --syntax-check
playbook: /home/student/projects-review/playbook.yml
[student@workstation projects-review]$ ansible-navigator run -m stdout playbook.yml

PLAY [Install and configure web service] ***************************************

TASK [Gathering Facts] *********************************************************
ok: [servera.lab.example.com]
...output omitted...

TASK [Install httpd] ***********************************************************
changed: [servera.lab.example.com]
...output omitted...

TASK [Enable and start httpd] **************************************************
changed: [servera.lab.example.com]
...output omitted...

TASK [Tuning configuration installed] ******************************************
changed: [servera.lab.example.com]
...output omitted...

TASK [Install firewalld] *******************************************************
ok: [servera.lab.example.com]
...output omitted...

TASK [Enable and start firewalld] **********************************************
ok: [servera.lab.example.com]
...output omitted...

TASK [Open the port for http] **************************************************
changed: [servera.lab.example.com]
...output omitted...

RUNNING HANDLER [restart httpd] ************************************************
changed: [servera.lab.example.com]
...output omitted...

PLAY RECAP *********************************************************************
servera.lab.example.com    : ok=8    changed=5    unreachable=0    failed=0  ...
serverb.lab.example.com    : ok=8    changed=5    unreachable=0    failed=0  ...
serverc.lab.example.com    : ok=8    changed=5    unreachable=0    failed=0  ...
serverd.lab.example.com    : ok=8    changed=5    unreachable=0    failed=0  ...

The task names come from the shared file, with the variables filled in. Run it again: every host reports ok=7 changed=0, one fewer ok because the handler is not notified.

Verify

[student@workstation projects-review]$ ssh devops@serverc \
> 'cat /etc/httpd/conf.d/tune.conf; systemctl is-active httpd firewalld; sudo firewall-cmd --list-services'
# Keep-alive tuning for the web servers
KeepAlive On
MaxKeepAliveRequests 200
KeepAliveTimeout 3
active
active
cockpit dhcpv6-client http ssh

In a classroom, the content of tune.conf may differ; what matters is that it is there, and that both services run with http allowed.

Grade and finish

Check your work with lab grade projects-review, then put the exercise away with lab finish projects-review.