ONLY FOR SELF STUDY, NO COMMERCIAL USAGE!!!
Contents
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,!datacenter1
shows 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 aswhen
, 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 aswhen
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, theansible.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…