Bookmark this page

Chapter 6.  Simplifying Playbooks with Roles and Ansible Content Collections

Abstract

Goal

Use Ansible Roles and Ansible Content Collections to develop playbooks more quickly and to reuse Ansible code.

Objectives
  • Manage large playbooks by importing or including other playbooks or tasks from external files, either unconditionally or based on a conditional test.

  • Describe the purpose of an Ansible Role, its structure, and how roles are used in playbooks.

  • Create a role in a playbook's project directory and run it as part of one of the plays in the playbook.

  • Select and retrieve roles from external sources such as Git repositories or Ansible Galaxy, and use them in your playbooks.

  • Obtain a set of related roles, supplementary modules, and other content from an Ansible Content Collection and use them in a playbook.

Sections
  • Including and Importing Files (and Guided Exercise)

  • Ansible Role Structure (and Quiz)

  • Creating Roles (and Guided Exercise)

  • Deploying Roles from External Content Sources (and Guided Exercise)

  • Getting Roles and Modules from Ansible Content Collections (and Guided Exercise)

Lab
  • Simplifying Playbooks with Roles and Ansible Content Collections

Including and Importing Files

Objectives

  • Manage large playbooks by importing or including other playbooks or tasks from external files, either unconditionally or based on a conditional test.

Managing Large Playbooks

When a playbook gets long or complex, you can divide it up into smaller files to make it easier to manage. You can combine multiple playbooks into a main playbook, or insert lists of tasks from a file into a play.

A file that contains only a list of tasks is called a task file. The following is an example of the content of a task file:

---
- name: Add Cisco IOS VLAN configuration
  cisco.ios.ios_vlans:
    config: "{{ vlans }}"
    state: merged

- name: Verify Cisco IOS VLAN configuration
  cisco.ios.ios_command:
    commands: show vlans
  register: vlan_data

- name: Display configured VLANs
  ansible.builtin.debug:
    var: vlan_data

Splitting a playbook into smaller files can make it easier to reuse plays or sequences of tasks in different projects.

Importing or Including Files

Ansible supports two methods for bringing content into a playbook. You can include content, or you can import content.

Importing content is a static operation. Ansible preprocesses imported content when the playbook is initially parsed, before the run starts.

Including content is a dynamic operation. Ansible processes included content while the playbook is running, as content is reached.

Importing Playbooks

The ansible.builtin.import_playbook module enables you to import external files containing lists of plays into a playbook. In other words, you can have a main playbook that imports one or more additional playbooks. If you import multiple playbooks, then they are imported and run in order.

The following is a simple example of a main playbook that imports two additional playbooks:

---
- name: Enable NETCONF service
  ansible.builtin.import_playbook: juniper_netconf.yml

- name: Configure SNMP settings
  ansible.builtin.import_playbook: juniper_snmp.yml

Notice that you must specify the imported playbook in the same line as the module.

You can also interleave plays in your main playbook with imported playbooks. In the following example, the Back up the IOS configuration play runs first, followed by the plays imported from the ios_vlans.yml playbook.

---
- name: Back up the IOS configuration
  hosts: ios
  gather_facts: false
  tasks:
    - name: Back up the current configuration
      cisco.ios.ios_config:
        backup: true
        backup_options:
          filename: "{{ inventory_hostname }}.txt"

- name: Import IOS VLANs playbook
  ansible.builtin.import_playbook: ios_vlans.yml

Importing Task Files

You can import a task file into a play inside a playbook by using the ansible.builtin.import_tasks module. When you import a task file, the tasks in that file are directly inserted when the playbook is parsed.

In the following example, instead of writing a list of tasks in the playbook, the task named Import Juniper Junos VLANs tasks imports the tasks from the junos_vlan.yml task file:

---
- name: Juniper Junos VLANs configuration
  hosts: junos
  gather_facts: false
  tasks:
  - name: Import Juniper Junos VLANs tasks
    ansible.builtin.import_tasks:
      file: junos_vlan.yml

To import a task file, you can use the file parameter of the ansible.builtin.import_tasks module to indicate the name of the task file, or you can give the name of the task file in the same line as the module:

---
- name: Juniper Junos VLANs configuration
  hosts: junos
  gather_facts: false
  tasks:
  - name: Import Juniper Junos VLANs tasks
    ansible.builtin.import_tasks: junos_vlan.yml

Because the ansible.builtin.import_tasks module statically imports the tasks when the playbook is parsed, you must consider the following with respect to loops and conditionals:

Using loops in imported tasks

You cannot use loops with the ansible.builtin.import_tasks module.

Using conditionals in imported tasks

Conditional statements set on the import, such as when, are applied to each of the tasks that are imported. In other words, if you use a conditional on a task that imports content, then each task in the imported content performs that conditional verification before it runs.

In the following example, all the tasks in the junos_security.yml task file run against the Juniper Junos managed nodes, and all the tasks in the ios_security.yml task file run against the Cisco IOS managed nodes:

---
- name: Apply security settings on the managed nodes
  hosts: all
  gather_facts: false
  tasks:
  - name: Import Juniper Junos security settings tasks
    ansible.builtin.import_tasks: junos_security.yml
    when: ansible_network_os == "junipernetworks.junos.junos"

  - name: Import Cisco IOS security settings tasks
    ansible.builtin.import_tasks: ios_security.yml
    when: ansible_network_os == "cisco.ios.ios"

Including Task Files

You can also dynamically include a task file into a play inside a playbook by using the ansible.builtin.include_tasks module. To include a task file, you can use the file parameter to indicate the name of the file with the task list, or you can give the name of this file in the same line as the module.

The following example uses the ansible.builtin.include_tasks module to include a task file named ios_ntp.yml in the playbook:

---
- name: Cisco IOS NTP configuration
  hosts: ios
  gather_facts: false
  tasks:
  - name: Include Cisco IOS NTP tasks
    ansible.builtin.include_tasks: ios_ntp.yml

Because the ansible.builtin.include_tasks module does not process content in the playbook until the play is running and that part of the play is reached, you must consider the following with respect to loops and conditionals:

Using loops in included tasks

Unlike the ansible.builtin.import_tasks module, you can use loops with the ansible.builtin.include_tasks module. The loop applies to each task in the included content.

In the following example, the task file named ios_add_users.yml contains the list of tasks to perform to create users in the Cisco IOS managed nodes. The list of users is given using the loop.

---
- name: Cisco IOS users
  hosts: ios
  gather_facts: false
  tasks:
  - name: Add users to Cisco IOS managed nodes
    ansible.builtin.include_tasks: ios_add_users.yml
    loop: "{{ expected_users }}"
Using conditionals in included tasks

Conditional statements, such as when set on the ansible.builtin.include_tasks module, determine whether the tasks are included in the play at all. The condition is applied only to the included task itself and not to any other tasks within the included file.

In other words, if you put a conditional on a task that includes content, then the conditional determines whether the content is included or not. If the condition is met, then all the tasks that are included run normally.

In the following example, if the vlans variable is defined, then the tasks in the ios_vlans.yml task file are run. The condition is no longer verified for tasks in the ios_vlans.yml task file:

---
- name: Cisco IOS VLANs configuration
  hosts: ios
  gather_facts: false
  tasks:
  - name: Apply conditional to imported task
    ansible.builtin.include_tasks: ios_vlans.yml
    when: vlans is defined

Use Cases for Task Files

Consider the following examples where it might be useful to manage sets of tasks as external files separate from the playbook:

  • If new managed nodes require complete configuration, then network administrators could create various sets of tasks for creating users and setting up their access, configuring the login banner on the managed nodes, hardening the managed nodes, installing security updates, configuring DNS, Simple Network Management Protocol (SNMP), logging, VLANs, NTP settings, and so on. Each of these sets of tasks could be managed through a separate self-contained task file.

  • If different groups of network administrators are in charge of the managed nodes, then each group can write their own task file, which can then be reviewed and integrated by the network manager.

  • If a managed node requires a particular configuration, then it can be integrated as a set of tasks that are executed based on a conditional. In other words, including the tasks only if specific criteria are met.

  • If a group of managed nodes needs to run a particular task or set of tasks, then the tasks might only be run on a managed node if it is part of that specific group.

Working with External Plays and Tasks

The incorporation of plays or tasks from external files into playbooks using the Ansible import and include features enhances the ability to reuse tasks and playbooks across an Ansible environment.

Managing Task Files

You can create a dedicated directory for task files, and save all task files in that directory. Your playbook can then include or import task files from that directory.

For example, suppose that you have the following task file named ios_verification.yml that verifies the NTP configuration in the Cisco IOS managed nodes, and then displays the existing NTP configuration:

---
- name: Verifying NTP configuration
  cisco.ios.ios_command:
    commands:
      - show run | include ntp
  register: ios_value

- name: Display NTP configuration
  ansible.builtin.debug:
    var: ios_value['stdout_lines']

If the previous task file was saved in a tasks directory, then you might incorporate the tasks as follows:

...output omitted...

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

...output omitted...

This enables you to construct a complex playbook and makes it easy to manage its structure and components.

Defining Variables

To maximize the possibility of reuse, plays and tasks should be as generic as possible. Use variables to parameterize play and task elements to expand the application of plays and tasks.

For example, if you parameterize the configuration to be verified from the previous example, then the ios_verification.yml task file can also be used for the verification of other configurations, rather than being useful only for verifying the NTP configuration:

- name: Verifying {{ ios_service }} configuration
  cisco.ios.ios_command:
    commands: "{{ ios_commands }}"
  register: ios_value

- name: Display {{ ios_service }} configuration
  ansible.builtin.debug:
    var: ios_value['stdout_lines']

When incorporating the task file in your playbook, you can define the variables to use for the task execution as follows:

...output omitted...

    - name: Import task file and set variables
      ansible.builtin.import_tasks: tasks/ios_verification.yml
      vars:
        ios_service: NTP
        ios_commands:
          - show run | include ntp

...output omitted...

The following example shows how to use the same task file to verify DNS on the managed nodes:

...output omitted...

    - name: Import task file and set variables
      ansible.builtin.import_tasks: tasks/ios_verification.yml
      vars:
        ios_service: DNS
        ios_commands:
          - show ip name-server
          - show ip dns view | include list

...output omitted...

You can use the same technique to make playbooks more reusable. When incorporating a playbook into another playbook, pass the variables to use for the play execution as follows:

- name: Import play file and set the variable
  ansible.builtin.import_playbook: play.yml
  vars:
    managed_nodes: junos

...output omitted...

Revision: do457-2.3-7cfa22a