• No results found

Playbooks are collections of deployment instructions, also referred to as plays. The benefit of using playbooks is that you can combine the execution of multiple Ansible modules to perform application rollouts. Also, the imperative execution of playbooks dictates the order that your Ansible plays are executed in, making it easier for other system

administrators to understand the logical flow. Playbooks should be stored in Git or another source code management (SCM) system for easy maintenance of your configuration

management workflows.

Ansible playbooks are written in YAML, which you learned about in Chapter 5. So, the syntax should look a little familiar. Let’s take a look at a simple playbook to set up a web server on one of our managed nodes (see Listing 10-3).

Listing 10-3 Web Server Playbook: apache.yml

Click here to view code image

- hosts: web tasks:

- name: deploy apache

yum: pkg=httpd state=latest - name: apache config file

copy: src=‘files/httpd.conf’ dest=’/etc/httpd/conf/httpd.conf’

notify:

- restart httpd

- name: apache service runs

service: name=httpd state=started handlers:

- name: restart httpd

service: name=httpd state=restarted

The apache.yml playbook is stored in a directory called apache with two subdirectories:

files and templates. The first line of the playbook uses the keyword hosts: to specify which hosts we will run this play on (the web group of servers in our /etc/ansible/hosts file). The web host group and all the tasks that belong to it comprise a single play.

Note

It is possible to have multiple plays in a single playbook. If we want to execute tasks on another group of hosts (for example, a db group), that

hosts: db declaration and its tasks are considered another play within the playbook.

Next, the tasks: keyword marks the beginning of the modules that will be executed on the hosts in the web group. Each task consists of at least two components. The name: is an identifier that lets anyone reading the code know what this particular task is meant to do. We will see a little bit further down the code that the name value may be useful for reference purposes. The next component of the task is the Ansible module that will be executed. For our first task, this is the yum: module. There are only two arguments that we are concerned about: pkg to tell yum to deploy the httpd package, and state to specify that the latest version of httpd should be installed. Alternatively, you could have specified present if you are not concerned about the version of the package.

You’ll notice in our first task that we are explicitly calling the yum package manager for CentOS. Ansible does not have a generic package installer module. This allows the playbook author to be specific about which package manager she wants to use. In a later example, you take a look at using a conditional statement to act differently based on the operating system that is running (that is, use apt with Debian-based systems and yum with Red Hat-based systems).

The next task is a bit more interesting because we are using the notify: parameter. This task is aptly named apache config file because we are using it to define which configuration file that httpd will use. In this task, we use the copy: module with two arguments: src, the location relative to the playbook file that contains the file that we want to copy; and dest, the location on the target servers where the file will be copied to.

The notify: task parameter indicates the name of an Ansible handler that will execute a certain function in response to changes in our configuration file.

The last task tells Ansible to ensure that the httpd service is running once the package is deployed. This task uses the service: Ansible module with two arguments: name,

which specifies the service to manage; and states, to specify whether the service should be running or stopped.

The handlers: section of the play looks similar to the tasks: section. However, the items listed here serve the special purpose of responding to a notify action. It is here that we define our restart httpd handler using the name: line, and the Ansible module that we are using (service:) to restart the httpd service. An important handler behavior to be aware of is that the handler is executed only once for the Ansible run. So, if you make multiple changes to the configuration file, the service will be restarted only after all the changes are made.

After saving and closing the file, you can execute it at the command line by using the ansible-playbook binary:

Click here to view code image

root@mnode01:~/apache# ansible-playbook apache.yml

In its current state, the playbook must be executed from within the Apache folder. Later on, we will take a look at how Ansible roles can be used to execute plays against hosts without worrying about our paths.

Conditional Expressions and Variables

Our apache playbook is ready to deploy web servers for Red Hat-based systems. However, what if we also have Debian-based systems (for example, Ubuntu) in production? We would need to handle multiple operating system types gracefully.

The when: conditional statement allows us to adjust which Ansible tasks are executed on a host according to our preferences. In our example, we will decide which package to deploy, the configuration file to use, and the service to manage based on the operating system family that our managed nodes belong to. Listing 10-4 shows a modified playbook that contains instructions for managed nodes whose operating systems belong to the Red Hat and Debian operating system families.

Listing 10-4 Ansible when: Conditional Statement for Module Execution Based on Facts

Click here to view code image

- hosts: web tasks:

# Deploy the Web Server

- name: Deploy Apache for Red Hat Systems yum: pkg=httpd state=latest

when: ansible_os_family == ‘RedHat’

- name: Deploy Apache for Debian Systems apt: pkg=apache2 state=latest

when: ansible_os_family == ‘Debian’

# Copy the correct Config File

- name: Apache Config File for Red Hat Systems

copy: src=‘files/httpd.conf’ dest=’/etc/httpd/conf/httpd.conf’

notify:

- restart httpd redhat

when: ansible_os_family == ‘RedHat’

- name: Apache Config File for Debian Systems

copy: src=‘files/apache2.conf’ dest=’/etc/apache2/apache2.conf’

notify:

- restart httpd debian

when: ansible_os_family == ‘Debian’

# Generate the correct Web Content

- name: Apache Content File for Red Hat Systems

template: src=‘files/index.html’ dest=’/var/www/html/index.html’

mode=0644

when: ansible_os_family == ‘RedHat’

- name: Apache Content File for Debian Systems

template: src=‘files/index.html’ dest=’/var/www/index.html’ mode=0644 when: ansible_os_family == ‘Debian’

# Verify Web Service is running

- name: Apache Service Runs for Red Hat Systems service: name=httpd state=started

when: ansible_os_family == ‘RedHat’

- name: Apache Service Runs for Debian Systems service: name=apache2 state=started

when: ansible_os_family == ‘Debian’

# Restart Web Service in response to config file change handlers:

- name: restart httpd redhat

service: name=httpd state=restarted - name: restart httpd debian

service: name=apache2 state=restarted

Comments (prepended with #) have been added to provide clarity and to help with

understanding the playbook. Each major step in the web server deployment play has two sets of instructions: one set for Red Hat systems and another set for Debian systems.

Ansible will decide which task to execute on the target node by evaluating the when:

statement, which uses the ansible_os_family system fact to provide module execution guidance.

Alternatively, we can use Ansible’s group_by function, which can automatically create additional host groups based on some condition (for example, ansible_os_family).

Listing 10-5 shows another way of structuring our playbook using group_by. You start off with the web host group from your /etc/ansible/hosts file, and you tell Ansible to create additional groups from the web host group using the ansible_os_family system fact as selection criteria. With our systems, that means two groups (one for RedHat and one for Debian) will be created at runtime, and these host groups can be used in the

remainder of the playbook. Listing 10-5 shows separate instructions for RedHat and Debian host groups that do not rely on the when: conditional. It is safe to not include conditional statements in the structure because instructions will only be run according to the host’s operating system. For example, our Debian instructions will not be executed on our Red Hat servers.

Listing 10-5 Ansible group_by Function for Module Execution Based on Facts

Click here to view code image

- hosts: web tasks:

- name: Group Servers By Operating System Family action: group_by key={{ ansible_os_family }}

- hosts: RedHat tasks:

- name: Deploy Apache

yum: pkg=httpd state=latest - name: Apache Config File

copy: src=files/httpd.conf dest=/etc/httpd/conf/httpd.conf notify:

- restart httpd

- name: Apache Service Runs

service: name=httpd state=started handlers:

- name: restart httpd

service: name=httpd state=restarted - hosts: Debian

tasks:

- name: Deploy Apache

apt: pkg=apache2 state=latest - name: Apache Config File

copy: src=files/apache2.conf dest=/etc/apache2/apache2.conf notify:

- restart httpd

- name: Apache Service Runs

service: name=apache2 state=started handlers:

- name: restart httpd

service: name=apache2 state=restarted

Both examples from Listings 10-4 and 10-5 solve the problem of modifying playbook execution according to operating system type. However, there is a lot of repeated code, and Ansible has additional capabilities to help us optimize our plays. Listing 10-6 shows the use of variables to reduce the number of tasks needed in our playbook.

Listing 10-6 Ansible when: Conditional Statement for Module Execution Based on Facts

Click here to view code image

- hosts: web vars_files:

“vars/{{ ansible_os_family }}.yml”

tasks:

- name: Deploy Apache for Red Hat Systems yum: pkg=httpd state=latest

when: ansible_os_family == ‘RedHat’

- name: Deploy Apache for Debian Systems apt: pkg=apache2 state=latest

when: ansible_os_family == ‘Debian’

- name: Apache Config File

copy: src=files/{{ conffile }} dest={{ confpath }}

notify:

- restart httpd

- name: Apache Service Runs

service: name={{ webserver }} state=started handlers:

- name: restart httpd

service: name={{ webserver }} state=restarted

There is a new element in our playbook: vars_files. This keyword tells Ansible where we are storing variables for use in the playbook. Listing 10-7 shows the contents of the YAML files that contain our variable values.

Listing 10-7 RedHat.yml and Debian.yml Variable Files

Click here to view code image

#RedHat.yml

webserver: ‘httpd’

conffile: ‘httpd.conf’

confpath: ‘/etc/httpd/conf/httpd.conf’

contentpath: ‘/var/www/html/index.html’

#Debian.yml

webserver: ‘apache2’

conffile: ‘apache2.conf’

confpath: ‘/etc/apache2/apache2.conf’

contentpath: ‘/var/www/index.html’

In our playbook, our variables are stored in a directory called vars that is located in the same directory as the playbook. There are two files (RedHat.yml and Debian.yml) whose names correspond to the values returned by the ansible_os_family system fact.

When we want to use any variable, whether system fact or user defined, we need to enclose it with double curly brackets so that Ansible knows to substitute the text for the stored value (for example, {{ ansible_os_family }}).

Because Ansible requires you to explicitly indicate the package manager for your managed nodes’ operating system, we still need to have separate tasks for deploying Apache on a Red Hat–based system versus on a Debian-based system. However, the rest of our tasks use generic modules (copy, service, and so on), and this allows us to use our imported variables to eliminate repetitive tasks.

The Apache Config File task is the first to use variables. The src argument for the copy module uses the conffile variable, and the dest argument uses the confpath variable. Even the handler definition is able to make use of variables, and we see that it uses the webserver variable to indicate which services will be restarted when the configuration file is changed.

If you compare the playbook in Listing 10-4 with the playbook in Listing 10-6, you see that the optimized code requires three fewer tasks and one less handler. This savings might not seem significant. However, in a much larger playbook, variable use can lead to more significant code optimization.