So far when we have worked with playbooks, we have been creating one single play per playbook (which logically is the minimum you can do). However, you can have more than one play in a playbook, and a "play" in Ansible terms is simply a set of tasks (and roles, handlers, and other Ansible facets) associated with a host (or group of hosts). A task is the smallest possible element of a play and is responsible for running a single module with a set of arguments to achieve a specific goal. Of course, in theory, this sounds quite complex, but when backed up by a practical example, it becomes quite simple to understand.
If we refer to our example inventory, this describes a simple two-tier architecture (we've left out the database tier for now). Now, suppose we want to write a single playbook to configure both the frontend servers and the application servers. We could use two separate playbooks to configure the front end and application servers, but this risks fragmenting your code and making it difficult to organize. However, front end servers and application servers are going to be (by their very nature) fundamentally different and so are unlikely to be configured with the same set of tasks.
The solution to this problem is to create a single playbook with two plays in it. The start of each play can be identified by the line at the lowest indentation (that is, zero spaces in front of it). Let's get started with building up our playbook:
Add the first play to the playbook and define some simple tasks to set up the Apache server on the front end, as shown here:
--- - name: Play 1 - configure the frontend servers hosts: frontends become: yes
tasks: - name: Install the Apache package yum: name: httpd state: latest - name: Start the Apache server service: name: httpd state: started
Immediately below this, in the same file, add the second play to configure the application tier servers:
- name: Play 2 - configure the application servers hosts: apps become: true
tasks: - name: Install Tomcat yum: name: tomcat state: latest - name: Start the Tomcat server service: name: tomcat state: started
Now, you have two plays: one to install web servers in the frontends group and one to install application servers in the apps group, all combined into one simple playbook.
When we run this playbook, we'll see the two plays performed sequentially, in the order they appear in the playbook. Note the presence of the PLAY keyword, which denotes the start of each play:
$ ansible-playbook -i hosts playandtask.yml
PLAY [Play 1 - configure the frontend servers] *********************************
There we have it—one playbook, yet two distinct plays operating on different sets of hosts from the provided inventories. This is very powerful, especially when combined with roles (which will be covered later in this book). Of course, you can have just one play in your playbook—you don't have to have multiple ones, but it is important to be able to develop multi-play playbooks as you will almost certainly find them useful as your environment gets more complex.
Playbooks are the lifeblood of Ansible automation—they extend it beyond single task/commands (which in themselves are incredibly powerful) to a whole series of tasks organized in a logical fashion. As you extend your library of playbooks, however, how do you keep your work organized? How do you efficiently reuse the same blocks of code? In the preceding example, we installed Apache, and this might be a requirement on a number of your servers. However, should you attempt to manage them all from one playbook? Or should you perhaps keep copying and pasting the same block of code over and over again? There is a better way, and in Ansible terms, we need to start looking at roles, which we shall do in the very next section.