Chapter 7: Managing Complex Plays and Playbooks

Exercise: Including and importing files

You will assemble a playbook almost entirely out of existing pieces: three task files, pulled into a play with one include and two imports, each given its own variables, and a whole play imported from another playbook to test the result.

The project ~/projects-file has an ansible.cfg, an inventory with servera.lab.example.com and workstation, a tasks directory with three task files, and a plays directory with one playbook.

Including and importing files

  • Include and import task files, and import a playbook, passing variables to each.

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-file

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.

Reuse the environment task file for a different service in a separate disposable play without editing that file.

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

Compose a web deployment from the supplied reusable files in playbook.yml.

  • Servera must run Apache with the supplied placeholder page and persistent HTTP/HTTPS firewall access.
  • Reuse tasks/environment.yml dynamically, and the firewall and placeholder task files statically, with parameters for their package, service, rule, and path values.
  • Reuse plays/test.yml as a separate play to verify the deployed URL.
  • Compare the task listing before a run with the tasks executed during the run. Explain which reused content can be expanded before execution.

The last play runs on workstation itself, as the user devops, using the key set up in section 1.6.

Read the task files

[student@workstation ~]$ cd ~/projects-file

None of the three files names a package, a service or a path. Everything that changes from one use to the next is a variable, to be filled in by whoever imports or includes the file.

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

- name: Start the {{ service }} service
  ansible.builtin.service:
    name: "{{ service }}"
    enabled: true
    state: started
---
- name: Install the firewall
  ansible.builtin.dnf:
    name: "{{ firewall_pkg }}"
    state: latest

- name: Start the firewall
  ansible.builtin.service:
    name: "{{ firewall_svc }}"
    enabled: true
    state: started

- name: Open the port for {{ rule }}
  ansible.posix.firewalld:
    service: "{{ item }}"
    immediate: true
    permanent: true
    state: enabled
  loop: "{{ rule }}"
---
- name: Create placeholder file
  ansible.builtin.copy:
    content: "{{ ansible_facts['fqdn'] }} has been customized using Ansible.\n"
    dest: "{{ file }}"

rule in firewall.yml is a list: the loop opens one firewall service per item.

Read the test play

plays/test.yml is a complete playbook with one play. It runs on workstation, without privilege escalation, and asks for a URL that it expects to answer with HTTP status 200:

---
- name: Test web service
  hosts: workstation
  become: false
  tasks:
    - name: Connect to the web server
      ansible.builtin.uri:
        url: "{{ url }}"
        status_code: 200

Start the playbook

Create playbook.yml with a play named Configure web server for servera.lab.example.com, and an empty tasks: section.

---
- name: Configure web server
  hosts: servera.lab.example.com
  tasks:

Include the environment tasks

As the first task, include tasks/environment.yml, with package and service both set to httpd:

    - name: Include the environment task file and set the variables
      ansible.builtin.include_tasks: tasks/environment.yml
      vars:
        package: httpd
        service: httpd

vars on an include or import applies only to the tasks it brings in.

Import the firewall tasks

As the second task, import tasks/firewall.yml. Install and start firewalld, and open both http and https:

    - name: Import the firewall task file and set the variables
      ansible.builtin.import_tasks: tasks/firewall.yml
      vars:
        firewall_pkg: firewalld
        firewall_svc: firewalld
        rule:
          - http
          - https

Import the placeholder task

As the third task, import tasks/placeholder.yml and have it write /var/www/html/index.html:

    - name: Import the placeholder task file and set the variable
      ansible.builtin.import_tasks: tasks/placeholder.yml
      vars:
        file: /var/www/html/index.html

Import the test play

After the first play, at the top level of the file, import plays/test.yml and set url to the new web server:

- name: Import test play file and set the variable
  ansible.builtin.import_playbook: plays/test.yml
  vars:
    url: http://servera.lab.example.com
---
- name: Configure web server
  hosts: servera.lab.example.com
  tasks:
    - name: Include the environment task file and set the variables
      ansible.builtin.include_tasks: tasks/environment.yml
      vars:
        package: httpd
        service: httpd

    - name: Import the firewall task file and set the variables
      ansible.builtin.import_tasks: tasks/firewall.yml
      vars:
        firewall_pkg: firewalld
        firewall_svc: firewalld
        rule:
          - http
          - https

    - name: Import the placeholder task file and set the variable
      ansible.builtin.import_tasks: tasks/placeholder.yml
      vars:
        file: /var/www/html/index.html

- name: Import test play file and set the variable
  ansible.builtin.import_playbook: plays/test.yml
  vars:
    url: http://servera.lab.example.com

See the difference before running

Check the syntax, then list the tasks Ansible knows about before the run:

[student@workstation projects-file]$ ansible-navigator run \
> -m stdout playbook.yml --syntax-check
playbook: /home/student/projects-file/playbook.yml
[student@workstation projects-file]$ ansible-navigator run \
> -m stdout playbook.yml --list-tasks

playbook: /home/student/projects-file/playbook.yml

  play #1 (servera.lab.example.com): Configure web server	TAGS: []
    tasks:
      Include the environment task file and set the variables	TAGS: []
      Install the firewall	TAGS: []
      Start the firewall	TAGS: []
      Open the port for {{ rule }}	TAGS: []
      Create placeholder file	TAGS: []

  play #2 (workstation): Test web service	TAGS: []
    tasks:
      Connect to the web server	TAGS: []

The imported tasks are listed one by one, and the imported play appears as play 2. The include is a single line: what is inside environment.yml is not read until the play reaches that task.

Run it

[student@workstation projects-file]$ ansible-navigator run -m stdout playbook.yml

PLAY [Configure web server] ****************************************************

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

TASK [Include the environment task file and set the variables] *****************
included: /home/student/projects-file/tasks/environment.yml for servera.lab.example.com

TASK [Install the httpd package] ***********************************************
changed: [servera.lab.example.com]

TASK [Start the httpd service] *************************************************
changed: [servera.lab.example.com]

TASK [Install the firewall] ****************************************************
ok: [servera.lab.example.com]

TASK [Start the firewall] ******************************************************
ok: [servera.lab.example.com]

TASK [Open the port for ['http', 'https']] *************************************
changed: [servera.lab.example.com] => (item=http)
changed: [servera.lab.example.com] => (item=https)

TASK [Create placeholder file] *************************************************
changed: [servera.lab.example.com]

PLAY [Test web service] ********************************************************

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

TASK [Connect to the web server] ***********************************************
ok: [workstation]

PLAY RECAP *********************************************************************
servera.lab.example.com    : ok=8    changed=4    unreachable=0    failed=0  ...
workstation                : ok=2    changed=0    unreachable=0    failed=0  ...

The included: line is where the include is processed during the run; the task names now show the values of the variables. firewalld was already installed and running, so those two tasks report ok.

Check the page

[student@workstation projects-file]$ curl http://servera.lab.example.com
servera.lab.example.com has been customized using Ansible.

Finish

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

Try it again, differently

Change the first task from include_tasks to import_tasks and run --list-tasks again: the two httpd tasks now appear in the list, with {{ package }} and {{ service }} not yet filled in, because variables are only applied when the tasks run. Then add when: false to the firewall import and run the playbook: each of its three tasks is listed and skipped, because an import copies the condition onto every task.