Bootstrap

Chapter 6 RH294 RHEL Automation with Ansible

ONLY FOR SELF STUDY, NO COMMERCIAL USAGE!!!


Chapter 6. Managing Complex Plays and Playbooks

Selecting Hosts with Host Patterns

In a play, the hosts directive specifies the managed hosts to run the play against.

The following example inventory is used throughout this section to illustrate host patterns.

[student@controlnode ~]$ cat myinventory
web.example.com
data.example.com

[lab]
labhost1.example.com
labhost2.example.com

[test]
test1.example.com
test2.example.com

[datacenter1]
labhost1.example.com
test1.example.com

[datacenter2]
labhost2.example.com
test2.example.com

[datacenter:children]
datacenter1
datacenter2

[new]
192.168.2.1
192.168.2.2

To demonstrate how host patterns are resolved, the following examples run playbook.yml Ansible Playbook, which contains a play that is edited to have different host patterns to target different subsets of managed hosts from the preceding example inventory.

Managed Hosts using IP addr

The most basic host pattern is the name of a single managed host listed in the inventory.

When the playbook runs, the first Gathering Facts task should run on all managed hosts that match the host pattern. A failure during this task causes the managed host to be removed from the play.

You can only use an IP address in a host pattern if it is explicitly listed in the inventory. If the IP address is not listed in the inventory, then you cannot use it to specify the host even if the IP address resolves to that host name in DNS.

[student@controlnode ~]$ cat playbook.yml
---
...output omitted...
  hosts: 192.168.2.1
...output omitted...

[student@controlnode ~]$ ansible-navigator run \
> -m stdout playbook.yml

PLAY [Test Host Patterns] **************************************************

TASK [Gathering Facts] *****************************************************
ok: [192.168.2.1]
...output omitted...

Note:

You can point an alias at a particular IP address in your inventory by setting the ansible_host host variable. For example, you could have a host in your inventory named host.example that you could use for host patterns and inventory groups, and direct connections using that name to the IP address 192.168.2.1 by creating a host_vars/host.example file containing the following host variable:

ansible_host: 192.168.2.1
Specifying Hosts Using a Group

You can use the names of inventory host groups as host patterns.

  • group_name
  • all
  • ungrouped
[student@controlnode ~]$ cat playbook.yml
---
...output omitted...
  hosts: lab
...output omitted...
[student@controlnode ~]$ ansible-navigator run \
> -m stdout playbook.yml

PLAY [Test Host Patterns] **************************************************

TASK [Gathering Facts] *****************************************************
ok: [labhost1.example.com]
ok: [labhost2.example.com]
...output omitted...

Remember that there is a special group named all that matches all managed hosts in the inventory.

[student@controlnode ~]$ cat playbook.yml
...output omitted...
  hosts: all
...output omitted...
[student@controlnode ~]$ ansible-navigator run \
> -m stdout playbook.yml

PLAY [Test Host Patterns] **************************************************

TASK [Gathering Facts] *****************************************************
ok: [labhost2.example.com]
ok: [test2.example.com]
ok: [web.example.com]
ok: [data.example.com]
ok: [labhost1.example.com]
ok: [192.168.2.1]
ok: [test1.example.com]
ok: [192.168.2.2]

There is also a special group named ungrouped, which includes all managed hosts in the inventory that are not members of any other group:

[student@controlnode ~]$ cat playbook.yml
...output omitted...
  hosts: ungrouped
...output omitted...
[student@controlnode ~]$ ansible-navigator run \
> -m stdout playbook.yml

PLAY [Test Host Patterns] **************************************************

TASK [Gathering Facts] *****************************************************
ok: [web.example.com]
ok: [data.example.com]
Matching Multiple Hosts with Wildcards

Another method of accomplishing the same thing as the all host pattern is to use the asterisk (*) wildcard character, which matches any string. If the host pattern is just a quoted asterisk, then all hosts in the inventory match.

[student@controlnode ~]$ cat playbook.yml
...output omitted...
  hosts: '*'
...output omitted...
[student@controlnode ~]$ ansible-navigator run \
> -m stdout playbook.yml

PLAY [Test Host Patterns] **************************************************

TASK [Gathering Facts] *****************************************************
ok: [labhost2.example.com]
ok: [test2.example.com]
ok: [web.example.com]
ok: [data.example.com]
ok: [labhost1.example.com]
ok: [192.168.2.1]
ok: [test1.example.com]
ok: [192.168.2.2]

Important:

Some characters that are used in host patterns also have meaning for the shell. If you are using any special wildcards or list characters in an Ansible Playbook, then you must put your host pattern in single quotes to ensure it is parsed correctly.

  hosts: '!test1.example.com,development'

The asterisk character can also be used to match any managed hosts or groups that contain a particular substring.

For example, the following wildcard host pattern matches all inventory names that end in .example.com:

[student@controlnode ~]$ cat playbook.yml
...output omitted...
  hosts: '*.example.com'
...output omitted...
[student@controlnode ~]$ ansible-navigator run \
> -m stdout playbook.yml

PLAY [Test Host Patterns] **************************************************

TASK [Gathering Facts] *****************************************************
ok: [labhost1.example.com]
ok: [test1.example.com]
ok: [labhost2.example.com]
ok: [test2.example.com]
ok: [web.example.com]
ok: [data.example.com]

The following example uses a wildcard host pattern to match the names of hosts or host groups that start with 192.168.2.:

[student@controlnode ~]$ cat playbook.yml
...output omitted...
  hosts: '192.168.2.*'
...output omitted...
[student@controlnode ~]$ ansible-navigator run \
> -m stdout playbook.yml

PLAY [Test Host Patterns] **************************************************

TASK [Gathering Facts] *****************************************************
ok: [192.168.2.1]
ok: [192.168.2.2]

The next example uses a wildcard host pattern to match the names of hosts or host groups that begin with data.

[student@controlnode ~]$ cat playbook.yml
...output omitted...
  hosts: 'data*'
...output omitted...
[student@controlnode ~]$ ansible-navigator run \
> -m stdout playbook.yml

PLAY [Test Host Patterns] **************************************************

TASK [Gathering Facts] *****************************************************
ok: [labhost1.example.com]
ok: [test1.example.com]
ok: [labhost2.example.com]
ok: [test2.example.com]
ok: [data.example.com]

Important:

The wildcard host patterns match all inventory names, hosts, and host groups. They do not distinguish between names that are DNS names, IP addresses, or groups, which can lead to some unexpected matches.

Lists

Multiple entries in an inventory can be referenced using logical lists. A comma-separated list of host patterns matches all hosts that match any of those host patterns.

If you provide a comma-separated list of managed hosts, then all those managed hosts are targeted:

[student@controlnode ~]$ cat playbook.yml
...output omitted...
  hosts: labhost1.example.com,test2.example.com,192.168.2.2
...output omitted...
[student@controlnode ~]$ ansible-navigator run \
> -m stdout playbook.yml

PLAY [Test Host Patterns] **************************************************

TASK [Gathering Facts] *****************************************************
ok: [labhost1.example.com]
ok: [test2.example.com]
ok: [192.168.2.2]

If you provide a comma-separated list of groups, then all hosts in any of those groups are targeted:

[student@controlnode ~]$ cat playbook.yml
...output omitted...
  hosts: lab,datacenter1
...output omitted...
[student@controlnode ~]$ ansible-navigator run \
> -m stdout playbook.yml

PLAY [Test Host Patterns] **************************************************

TASK [Gathering Facts] *****************************************************
ok: [labhost1.example.com]
ok: [labhost2.example.com]
ok: [test1.example.com]

You can also mix managed hosts, host groups, and wildcards, as shown below:

[student@controlnode ~]$ cat playbook.yml
...output omitted...
  hosts: lab,data*,192.168.2.2
...output omitted...
[student@controlnode ~]$ ansible-navigator run \
> -m stdout playbook.yml

PLAY [Test Host Patterns] **************************************************

TASK [Gathering Facts] *****************************************************
ok: [labhost1.example.com]
ok: [labhost2.example.com]
ok: [test1.example.com]
ok: [test2.example.com]
ok: [data.example.com]
ok: [192.168.2.2]

Note:

The colon character (😃 can be used instead of a comma. However, the comma is the preferred separator, especially when working with IPv6 addresses as managed host names.

Lists with special char &/!

If an item in a list starts with an ampersand character (&), similarly to a logical AND, then hosts must match that item in order to match the host pattern.

For example, based on our example inventory, the following host pattern matches machines in the lab group only if they are also in the datacenter1 group:

[student@controlnode ~]$ cat playbook.yml
...output omitted...
  hosts: lab,&datacenter1
...output omitted...
[student@controlnode ~]$ ansible-navigator run \
> -m stdout playbook.yml

PLAY [Test Host Patterns] **************************************************

TASK [Gathering Facts] *****************************************************
ok: [labhost1.example.com]

You could also specify that machines in the datacenter1 group match only if they are in the lab group with the host patterns &lab,datacenter1 or datacenter1,&lab.

You can exclude hosts that match a pattern from a list by using the exclamation point or “bang” character (!) in front of the host pattern. This operates like a logical NOT.

This example matches all hosts defined in the datacenter group, except test2.example.com based on the example inventory:

[student@controlnode ~]$ cat playbook.yml
...output omitted...
  hosts: datacenter,!test2.example.com
...output omitted...
[student@controlnode ~]$ ansible-navigator run \
> -m stdout playbook.yml

PLAY [Test Host Patterns] **************************************************

TASK [Gathering Facts] *****************************************************
ok: [labhost1.example.com]
ok: [test1.example.com]
ok: [labhost2.example.com]

The pattern '!test2.example.com,datacenter' could have been used in the preceding example to achieve the same result.

The pattern hosts: all,!datacenter1shows the use of a host pattern that matches all hosts in the test inventory, except the managed hosts in the datacenter1 group.

References

Patterns: targeting hosts and groups — Ansible Documentation

How to build your inventory — Ansible Documentation

Example

inventory files:

[student@workstation projects-host]$ cat inventory1
srv1.example.com
srv2.example.com
s1.lab.example.com
s2.lab.example.com

[web]
jupiter.lab.example.com
saturn.example.com

[db]
db1.example.com
db2.example.com
db3.example.com

[lb]
lb1.lab.example.com
lb2.lab.example.com

[boston]
db1.example.com
jupiter.lab.example.com
lb2.lab.example.com

[london]
db2.example.com
db3.example.com
file1.lab.example.com
lb1.lab.example.com

[dev]
web1.lab.example.com
db3.example.com

[stage]
file2.example.com
db2.example.com

[prod]
lb2.lab.example.com
db1.example.com
jupiter.lab.example.com

[function:children]
web
db
lb
city

[city:children]
boston
london
environments

[environments:children]
dev
stage
prod
new

[new]
172.25.252.23
172.25.252.44
172.25.252.32
[student@workstation projects-host]$ cat inventory2
workstation.lab.example.com

[london]
servera.lab.example.com

[berlin]
serverb.lab.example.com

[tokyo]
serverc.lab.example.com

[atlanta]
serverd.lab.example.com

[europe:children]
london
berlin

playbook.yml

---
- name: Resolve host patterns
  # hosts: db1.example.com
  # hosts: 172.25.252.32
  # hosts: all
  # hosts: '*example.com'
  # hosts: "*example.com,!*.lab.example.com"
  # hosts: lb1.lab.example.com,s1.lab.example.com,db1.example.com
  # hosts: '172.25.*'
  # hosts: 's*'
  # hosts: 'prod,172*,*lab*'
  # hosts: db,&london
  # hosts: london
  # hosts: europe
  # hosts: ungrouped
  hosts: chicken  # invalid group name


  gather_facts: false
  tasks:
  - name: Display managed hosts matching the host pattern
    ansible.builtin.debug:
      msg: "{{ inventory_hostname }}"

Including and Importing Files

Purpose: Managing Large Playbooks

Including or Importing Files:

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

When you include content, it is a dynamic operation. Ansible processes included content during the run of the playbook, as content is reached.

When you import content, it is a static operation. Ansible preprocesses imported content when the playbook is initially parsed, before the run starts.

Importing Playbooks

Use the ansible.builtin.import_playbook module 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.

Because the content being imported is a complete playbook, the ansible.builtin.import_playbook module can only be used at the top level of a playbook and cannot be used inside a play. 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: Prepare the web server
  ansible.builtin.import_playbook: web.yml

- name: Prepare the database server
  ansible.builtin.import_playbook: db.yml

You can also interleave plays in your main playbook with imported playbooks.

---
- name: Play 1
  hosts: localhost
  tasks:
    - name: Display a message
      ansible.builtin.debug:
        msg: Play 1

- name: Import Playbook
  ansible.builtin.import_playbook: play2.yml

In the preceding example, the Play 1 play runs first, followed by the plays imported from the play2.yml playbook.

Importing and Including Tasks

You can import or include a list of tasks from a task file into a play. A task file is a file that contains a flat list of tasks:

[user@host ~]$ cat webserver_tasks.yml
---
- name: Install the httpd package
  ansible.builtin.dnf:
    name: httpd
    state: latest

- name: Start the httpd service
  ansible.builtin.service:
    name: httpd
    state: started
Importing Task Files

You can statically 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. The location of the task in the playbook that uses the ansible.builtin.import_tasks module controls where the tasks are inserted and the order in which multiple imports are run.

---
- name: Install web server
  hosts: webservers
  tasks:
    - name: Import webserver tasks
      ansible.builtin.import_tasks: webserver_tasks.yml

When you import a task file, the tasks in that file are directly inserted when the playbook is parsed. Because the ansible.builtin.import_tasks module statically imports the tasks when the playbook is parsed, the following items must be considered:

  • When using the ansible.builtin.import_tasks module, conditional statements set on the import, such as when, are applied to each of the tasks that are imported.
  • You cannot use loops with the ansible.builtin.import_tasks module.
  • If you use a variable to specify the name of the file to import, then you cannot use a host or group inventory variable.
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.

---
- name: Install web server
  hosts: webservers
  tasks:
    - name: Include webserver tasks
      ansible.builtin.include_tasks: webserver_tasks.yml

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. The order in which playbook content is processed impacts how the ansible.builtin.include_tasks module works.

  • When using the ansible.builtin.include_tasks module, conditional statements such as when set on the include determine whether the tasks are included in the play at all.
  • If you run ansible-navigator run --list-tasks to list the tasks in the playbook, then tasks in the included task files are not displayed. The tasks that include the task files are displayed. By comparison, the ansible.builtin.import_tasks module would not list tasks that import task files, but instead would list the individual tasks from the imported task files.
  • You cannot use ansible-navigator run --start-at-task to start playbook execution from a task that is in an included task file.
  • You cannot use a notify statement to trigger a handler name that is in an included task file. You can trigger a handler in the main playbook that includes an entire task file, in which case all tasks in the included file run.
Importing and Including with Conditionals

Conditional statements behave differently depending on whether you are importing or including tasks.

  • When you add a conditional to a task that uses an ansible.builtin.import_* module, Ansible applies the condition to all tasks within the imported file. In other words, each task in the imported content performs that conditional check before it runs.
  • When you use a conditional on a task that uses an ansible.builtin.include_* module, the condition is applied only to the include task itself and not to any other tasks within the included file. in other words, the conditional determines whether the include happens or not. If the include happens, then all the tasks that are included run normally.

Refer to the Ansible User Guide for a more detailed discussion of the differences in behavior between the ansible.builtin.import_tasks module and the ansible.builtin.include_tasks module when conditionals are used.

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 servers require complete configuration, then administrators could create various sets of tasks for creating users, installing packages, configuring services, configuring privileges, setting up access to a shared file system, hardening the servers, installing security updates, and installing a monitoring agent. Each of these sets of tasks could be managed through a separate self-contained task file.
  • If servers are managed collectively by the developers, the system administrators, and the database administrators, then every organization can write its own task file which can then be reviewed and integrated by the system manager.
  • If a server 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 servers needs to run a particular task or set of tasks, then the tasks might only be run on a server if it is part of a specific host group.
Managing Task Files

You can create a dedicated directory for task files, and save all task files in that directory. Then your playbook can include or import task files from that directory. This allows construction of a complex playbook and makes it easy to manage its structure and components.

Defining Variables for 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. To maximize the possibility of reuse, these task and play files should be as generic as possible. Variables can be used to parameterize play and task elements to expand the application of tasks and plays.

If you parameterize the package and service elements as shown in the following example, then the task file can also be used for the installation and administration of other software and their services, rather than being useful for a web service only.

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

Subsequently, when incorporating the task file into a playbook, define the variables to use for the task execution as follows:

...output omitted...
  tasks:
    - name: Import task file and set variables
      ansible.builtin.import_tasks: task.yml
      vars:
        package: httpd
        service: httpd

Ansible makes the passed variables available to the tasks imported from the external file.

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

...output omitted...
- name: Import play file and set the variable
  ansible.builtin.import_playbook: play.yml
  vars:
    package: mariadb

Important NOTE:

Earlier versions of Ansible used the ansible.builtin.include module to include both playbooks and task files, depending on context. This functionality is being deprecated for a number of reasons.

Before Ansible 2.0, the ansible.builtin.include module operated like a static import. In Ansible 2.0 it was changed to operate dynamically, but this created some limitations. In Ansible 2.1 it became possible for the ansible.builtin.include module to be dynamic or static depending on task settings, which was confusing and error-prone. There were also issues with ensuring that the ansible.builtin.include module worked correctly in all contexts.

Thus, ansible.builtin.include was replaced in Ansible 2.4 with new directives such as ansible.builtin.include_tasks, import_tasks, and ansible.builtin.import_playbook. You might find examples of the ansible.builtin.include module in earlier playbooks, but you should avoid using it in new ones.

References

Including and Importing — Ansible Documentation

Creating Reusable Playbooks — Ansible Documentation

Conditionals — Ansible Documentation

Chapter 6 Example

[student@workstation projects-file]$ tree -F
.
├── ansible.cfg
├── ansible-navigator.log
├── inventory
├── playbok.yml
├── plays/
│   └── test.yml
└── tasks/
    ├── environment.yml
    ├── firewall.yml
    └── placeholder.yml

Contents as below:

[student@workstation projects-file]$ cat plays/test.yml 
---
- name: Test web service
  hosts: server*.lab.example.com
  become: false
  tasks:
    - name: Connect to internet web server
      ansible.builtin.uri:
        url: "{{ url }}"
        status_code: 200


[student@workstation tasks]$ cat environment.yml 
---
  - 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


[student@workstation tasks]$ cat firewall.yml 
---
  - name: Install the firewall
    ansible.builtin.dnf:
      name: "{{ firewall_pkg }}"
      state: latest

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

  - name: Open the port for {{ rule }}
    ansible.posix.firewalld:
      service: "{{ item }}"
      immediate: true
      permanent: true
      state: enabled
    loop: "{{ rule }}"


[student@workstation tasks]$ cat placeholder.yml 
---
  - name: Create placeholder file
    ansible.builtin.copy:
      content: "{{ ansible_facts['fqdn'] }} has been customized using Ansible.\n"
      dest: "{{ file }}"

Main playbook:

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

  tasks:
    - name: Installing web service
      ansible.builtin.include_tasks: tasks/environment.yml
      vars:
        package: httpd
        service: httpd
    
    - name: Configuring firewall settings
      ansible.builtin.import_tasks: tasks/firewall.yml
      vars:
        firewall_pkg: firewalld
        firewall_svc: firewalld
        rule: 
          - http
          - https

    - name: Creating placeholder firewall
      ansible.builtin.import_tasks: tasks/placeholder.yml
      vars:
        file: /var/www/html/index.html

- name: Importing test.yml playbook
  ansible.builtin.import_playbook: plays/test.yml
  vars:
    url: 'http://servera.lab.example.com'

TO BE CONTINUED…

悦读

道可道,非常道;名可名,非常名。 无名,天地之始,有名,万物之母。 故常无欲,以观其妙,常有欲,以观其徼。 此两者,同出而异名,同谓之玄,玄之又玄,众妙之门。

;