Ansible: Up and Running (2015)
Chapter 2. Playbooks: A Beginning
Most of your time in Ansible will be spent writing playbooks. A playbook is the term that Ansible uses for a configuration management script. Let’s look at an example: installing the nginx web server and configuring it for secure communication.
If you’re following along in this chapter, you should end up with the files listed here:
§ playbooks/ansible.cfg
§ playbooks/hosts
§ playbooks/Vagrantfile
§ playbooks/web-notls.yml
§ playbooks/web-tls.yml
§ playbooks/files/nginx.key
§ playbooks/files/nginx.crt
§ playbooks/files/nginx.conf
§ playbooks/templates/index.html.j2
§ playbooks/templates/nginx.conf.j2
Some Preliminaries
Before we can run this playbook against our Vagrant machine, we need to expose ports 80 and 443, so we can access them. As shown in Figure 2-1, we are going to configure Vagrant so that requests to ports 8080 and 8443 on our local machine are forwarded to ports 80 and 443 on the Vagrant machine. This will allow us to access the web server running inside Vagrant at http://localhost:8080 and https://localhost:8443.
Figure 2-1. Exposing ports on Vagrant machine
Modify your Vagrantfile so it looks like this:
VAGRANTFILE_API_VERSION = "2"
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
config.vm.box = "ubuntu/trusty64"
config.vm.network "forwarded_port", guest: 80, host: 8080
config.vm.network "forwarded_port", guest: 443, host: 8443
end
This maps port 8080 on your local machine to port 80 of the Vagrant machine, and port 8443 on your local machine to port 443 on the Vagrant machine. Once you make the changes, tell Vagrant to have them go into effect by running:
$ vagrant reload
You should see output that includes:
==> default: Forwarding ports...
default: 80 => 8080 (adapter 1)
default: 443 => 8443 (adapter 1)
default: 22 => 2222 (adapter 1)
A Very Simple Playbook
For our first example playbook, we’ll configure a host to run an nginx web server. For this example, we won’t configure the web server to support TLS encryption. This will make setting up the web server simpler, but a proper website should have TLS encryption enabled, and we’ll cover how to do that later on in this chapter.
First, we’ll see what happens when we run the playbook in Example 2-1, and then we’ll go over the contents of the playbook in detail.
Example 2-1. web-notls.yml
- name: Configure webserver with nginx
hosts: webservers
sudo: True
tasks:
- name: install nginx
apt: name=nginx update_cache=yes
- name: copy nginx config file
copy: src=files/nginx.conf dest=/etc/nginx/sites-available/default
- name: enable configuration
file: >
dest=/etc/nginx/sites-enabled/default
src=/etc/nginx/sites-available/default
state=link
- name: copy index.html
template: src=templates/index.html.j2 dest=/usr/share/nginx/html/index.html
mode=0644
- name: restart nginx
service: name=nginx state=restarted
WHY DO YOU USE “TRUE” IN ONE PLACE AND “YES” IN ANOTHER?
Sharp-eyed readers might have noticed that Example 2-1 uses True in one spot in the playbook (to enable sudo) and yes in another spot in the playbook (to update the apt cache).
Ansible is pretty flexible on how you represent truthy and falsey values in playbooks. Strictly speaking, module arguments (like update_cache=yes) are treated differently from values elsewhere in playbooks (like sudo: True). Values elsewhere are handled by the YAML parser and so use the YAML conventions of truthiness, which are:
YAML truthy
true, True, TRUE, yes, Yes, YES, on, On, ON, y, Y
YAML falsey
false, False, FALSE, no, No, NO, off, Off, OFF, n, N
Module arguments are passed as strings and use Ansible’s internal conventions, which are:
module arg truthy
yes, on, 1, true
module arg falsey
no, off, 0, false
I tend to follow the examples in the official Ansible documentation. These typically use yes and no when passing arguments to modules (since that’s consistent with the module documentation), and True and False elsewhere in playbooks.
Specifying an nginx Config File
This playbook requires two additional files before we can run it. First, we need to define an nginx configuration file.
Nginx ships with a configuration file that works out of the box if you just want to serve static files. But you’ll almost always need to customize this, so we’ll overwrite the default configuration file with our own as part of this playbook. As we’ll see later, we’ll need to modify this configuration file to support TLS. Example 2-2 shows a basic nginx config file. Put it in playbooks/files/nginx.conf.1
Example 2-2. files/nginx.conf
server {
listen 80 default_server;
listen [::]:80 default_server ipv6only=on;
root /usr/share/nginx/html;
index index.html index.htm;
server_name localhost;
location / {
try_files $uri $uri/ =404;
}
}
NOTE
An Ansible convention is to keep files in a subdirectory named files and Jinja2 templates in a subdirectory named templates. I’ll follow this convention throughout the book.
Creating a Custom Homepage
Let’s add a custom homepage. We’re going to use Ansible’s template functionality so that Ansible will generate the file from a template. Put the file shown in Example 2-3 in playbooks/templates/index.html.j2.
Example 2-3. playbooks/templates/index.html.j2
<html>
<head>
<title>Welcome to ansible</title>
</head>
<body>
<h1>nginx, configured by Ansible</h1>
<p>If you can see this, Ansible successfully installed nginx.</p>
<p>{{ ansible_managed }}</p>
</body>
</html>
This template references a special Ansible variable named ansible_managed. When Ansible renders this template, it will replace this variable with information about when the template file was generated. Figure 2-2 shows a screenshot of a web browser viewing the generated HTML.
Figure 2-2. Rendered HTML
Creating a Webservers Group
Let’s create a “webservers” group in our inventory file so that we can refer to this group in our playbook. For now, this group will contain our testserver.
Inventory files are in the .ini file format. We’ll go into this format in detail later in the book. Edit your playbooks/hosts file to put a [webservers] line above the testserver line, as shown in Example 2-4. This indicates that testserver is in the webservers group.
Example 2-4. playbooks/hosts
[webservers]
testserver ansible_ssh_host=127.0.0.1 ansible_ssh_port=2222
You should now be able to ping the webservers group using the ansible command-line tool:
$ ansible webservers -m ping
The output should look like this:
testserver | success >> {
"changed": false,
"ping": "pong"
}
Running the Playbook
The ansible-playbook command executes playbooks. To run the playbook, do:
$ ansible-playbook web-notls.yml
Example 2-5 shows what the output should look.
Example 2-5. Output of ansible-playbook
PLAY [Configure webserver with nginx] *********************************
GATHERING FACTS ***************************************************************
ok: [testserver]
TASK: [install nginx] *********************************************************
changed: [testserver]
TASK: [copy nginx config file] ************************************************
changed: [testserver]
TASK: [enable configuration] **************************************************
ok: [testserver]
TASK: [copy index.html] *******************************************************
changed: [testserver]
TASK: [restart nginx] *********************************************************
changed: [testserver]
PLAY RECAP ********************************************************************
testserver : ok=6 changed=4 unreachable=0 failed=0
COWSAY
If you have the cowsay program installed on your local machine, then Ansible output will look like this instead:
_______________________________________
< PLAY [Configure webserver with nginx] >
---------------------------------------
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
If you don’t want to see the cows, you can disable cowsay by setting the ANSIBLE_NOCOWS environment variable like this:
$ export ANSIBLE_NOCOWS=1
You can also disable cowsay by adding the following to your ansible.cfg file.
[defaults]
nocows = 1
If you didn’t get any errors,2 you should be able to point your browser to http://localhost:8080 and see the custom HTML page, as shown in Figure 2-2.
NOTE
If your playbook file is marked as executable and starts with a line that looks like this:3
#!/usr/bin/env ansible-playbook
then you can execute it by invoking it directly, like this:
$ ./web-notls.yml
WHAT’S THIS “GATHERING FACTS” BUSINESS?
You might have noticed the following lines of output when Ansible first starts to run:
GATHERING FACTS **************************************************
ok: [testserver]
When Ansible starts executing a play, the first thing it does is collect information about the server it is connecting to, including which operating system is running, hostname, IP and MAC addresses of all interfaces, and so on.
You can then use this information later on in the playbook. For example, you might need the IP address of the machine for populating a configuration file.
You can turn off fact gathering if you don’t need it, in order to save some time. We’ll cover the use of facts and how to disable fact gathering in a later chapter.
Playbooks Are YAML
Ansible playbooks are written in YAML syntax. YAML is a file format similar in intent to JSON, but generally easier for humans to read and write. Before we go over the playbook, let’s cover the concepts of YAML that are most important for writing playbooks.
Start of File
YAML files are supposed to start with three dashes to indicate the beginning of the document:
---
However, if you forget to put those three dashes at the top of your playbook files, Ansible won’t complain.
Comments
Comments start with a number sign and apply to the end of the line, the same as in shell scripts, Python, and Ruby:
# This is a YAML comment
Strings
In general, YAML strings don’t have to be quoted, although you can quote them if you prefer. Even if there are spaces, you don’t need to quote them. For example, this is a string in YAML:
this is a lovely sentence
The JSON equivalent is:
"this is a lovely sentence"
There are some scenarios in Ansible where you will need to quote strings. These typically involve the use of {{ braces }} for variable substitution. We’ll get to those later.
Booleans
YAML has a native Boolean type, and provides you with a wide variety of strings that can be interpreted as true or false, which we covered in “Why Do You Use “True” in One Place and “Yes” in Another?”.
Personally, I always use True and False in my Ansible playbooks.
For example, this is a Boolean in YAML:
True
The JSON equivalent is:
true
Lists
YAML lists are like arrays in JSON and Ruby or lists in Python. Technically, these are called sequences in YAML, but I call them lists here to be consistent with the official Ansible documentation.
They are delimited with hyphens, like this:
- My Fair Lady
- Oklahoma
- The Pirates of Penzance
The JSON equivalent is:
[
"My Fair Lady",
"Oklahoma",
"The Pirates of Penzance"
]
(Note again how we didn’t have to quote the strings in YAML, even though they have spaces in them.)
YAML also supports an inline format for lists, which looks like this:
[My Fair Lady, Oklahoma, The Pirates of Penzance]
Dictionaries
YAML dictionaries are like objects in JSON, dictionaries in Python, or hashes in Ruby. Technically, these are called mappings in YAML, but I call them dictionaries here to be consistent with the official Ansible documentation.
They look like this:
address: 742 Evergreen Terrace
city: Springfield
state: North Takoma
The JSON equivalent is:
{
"address": "742 Evergreen Terrace",
"city": "Springfield",
"state": "North Takoma"
}
YAML also supports an inline format for dictionaries, which looks like this:
{address: 742 Evergreen Terrace, city: Springfield, state: North Takoma}
Line Folding
When writing playbooks, you’ll often encounter situations where you’re passing many arguments to a module. For aesthetics, you might want to break this up across multiple lines in your file, but you want Ansible to treat the string as if it were a single line.
You can do this with YAML using line folding with the greater than (>) character. The YAML parser will replace line breaks with spaces. For example:
address: >
Department of Computer Science,
A.V. Williams Building,
University of Maryland
city: College Park
state: Maryland
The JSON equivalent is:
{
"address": "Department of Computer Science, A.V. Williams Building,
University of Maryland",
"city": "College Park",
"state": "Maryland"
}
Anatomy of a Playbook
Let’s take a look at our playbook from the perspective of a YAML file. Here it is again, in Example 2-6.
Example 2-6. web-notls.yml
- name: Configure webserver with nginx
hosts: webservers
sudo: True
tasks:
- name: install nginx
apt: name=nginx update_cache=yes
- name: copy nginx config file
copy: src=files/nginx.conf dest=/etc/nginx/sites-available/default
- name: enable configuration
file: >
dest=/etc/nginx/sites-enabled/default
src=/etc/nginx/sites-available/default
state=link
- name: copy index.html
template: src=templates/index.html.j2 dest=/usr/share/nginx/html/index.html
mode=0644
- name: restart nginx
service: name=nginx state=restarted
In Example 2-7, we see the JSON equivalent of this file.
Example 2-7. JSON equivalent of web-notls.yml
[
{
"name": "Configure webserver with nginx",
"hosts": "webservers",
"sudo": true,
"tasks": [
{
"name": "Install nginx",
"apt": "name=nginx update_cache=yes"
},
{
"name": "copy nginx config file",
"template": "src=files/nginx.conf dest=/etc/nginx/
sites-available/default"
},
{
"name": "enable configuration",
"file": "dest=/etc/nginx/sites-enabled/default src=/etc/nginx/sites-available
/default state=link"
},
{
"name": "copy index.html",
"template" : "src=templates/index.html.j2 dest=/usr/share/nginx/html/
index.html mode=0644"
},
{
"name": "restart nginx",
"service": "name=nginx state=restarted"
}
]
}
]
NOTE
A valid JSON file is also a valid YAML file. This is because YAML allows strings to be quoted, considers true and false to be valid Booleans, and has inline lists and dictionary syntaxes that are the same as JSON arrays and objects. But don’t write your playbooks as JSON — the whole point of YAML is that it’s easier for people to read.
Plays
Looking at either the YAML or JSON representation, it should be clear that a playbook is a list of dictionaries. Specifically, a playbook is a list of plays.
Here’s the play4 from our example:
- name: Configure webserver with nginx
hosts: webservers
sudo: True
tasks:
- name: install nginx
apt: name=nginx update_cache=yes
- name: copy nginx config file
copy: src=files/nginx.conf dest=/etc/nginx/sites-available/default
- name: enable configuration
file: >
dest=/etc/nginx/sites-enabled/default
src=/etc/nginx/sites-available/default
state=link
- name: copy index.html
template: src=templates/index.html.j2
dest=/usr/share/nginx/html/index.html mode=0644
- name: restart nginx
service: name=nginx state=restarted
Every play must contain:
§ A set of hosts to configure
§ A list of tasks to be executed on those hosts
Think of a play as the thing that connects hosts to tasks.
In addition to specifying hosts and tasks, plays also support a number of optional settings. We’ll get into those later, but three common ones are:
name
A comment that describes what the play is about. Ansible will print this out when the play starts to run.
sudo
If true, Ansible will run every task by sudo’ing as (by default) the root user. This is useful when managing Ubuntu servers, since by default you cannot SSH as the root user.
vars
A list of variables and values. We’ll see this in action later in this chapter.
Tasks
Our example playbook contains one play that has five tasks. Here’s the first task of that play:
- name: install nginx
apt: name=nginx update_cache=yes
The name is optional, so it’s perfectly valid to write a task like this:
- apt: name=nginx update_cache=yes
Even though names are optional, I recommend you use them because they serve as good reminders for the intent of the task. (Names will be very useful when somebody else is trying to understand your playbook, including yourself in six months.) As we’ve seen, Ansible will print out the name of a task when it runs. Finally, as we’ll see in Chapter 14, you can use the --start-at-task <task name> flag to tell ansible-playbook to start a playbook in the middle of a task, but you need to reference the task by name.
Every task must contain a key with the name of a module and a value with the arguments to that module. In the preceding example, the module name is apt and the arguments are name=nginx update_cache=yes.
These arguments tell the apt module to install the package named nginx and to update the package cache (the equivalent of doing an apt-get update) before installing the package.
It’s important to understand that, from the point of the view of the YAML parser used by the Ansible frontend, the arguments are treated as a string, not as a dictionary. This means that if you want to break up arguments into multiple lines, you need to use the YAML folding syntax, like this:
- name: install nginx
apt: >
name=nginx
update_cache=yes
Ansible also supports a task syntax that will let you specify module arguments as a YAML dictionary, which is helpful when using modules that support complex arguments. We’ll cover that in “Complex Arguments in Tasks: A Brief Digression”.
Ansible also supports an older syntax that uses action as the key and puts the name of the module in the value. The preceding example also can be written as:
- name: install nginx
action: apt name=nginx update_cache=yes
Modules
Modules are scripts5 that come packaged with Ansible and perform some kind of action on a host. Admittedly, that’s a pretty generic description, but there’s enormous variety across Ansible modules. The modules we use in this chapter are:
apt
Installs or removes packages using the apt package manager.
copy
Copies a file from local machine to the hosts.
file
Sets the attribute of a file, symlink, or directory.
service
Starts, stops, or restarts a service.
template
Generates a file from a template and copies it to the hosts.
VIEWING ANSIBLE MODULE DOCUMENTATION
Ansible ships with the ansible-doc command-line tool, which shows documentation about modules. Think of it as man pages for Ansible modules. For example, to show the documentation for the service module, run:
$ ansible-doc service
If you use Mac OS X, there’s a wonderful documentation viewer called Dash that has support for Ansible. Dash indexes all of the Ansible module documentation. It’s a commercial tool ($19.99 as of this writing), but I find it invaluable.
Recall from the first chapter that Ansible executes a task on a host by generating a custom script based on the module name and arguments, and then copies this script to the host and runs it.
There are over 200 modules that ship with Ansible, and this number grows with every release. You can also find third-party Ansible modules out there, or write your own.
Putting It All Together
To sum up, a playbook contains one or more plays. A play associates an unordered set of hosts with an ordered list of task_. Each task is associated with exactly one module.
Figure 2-3 is an entity-relationship diagram that depicts this relationship between playbooks, plays, hosts, tasks, and modules.
Figure 2-3. Entity-relationship diagram
Did Anything Change? Tracking Host State
When you run ansible-playbook, Ansible outputs status information for each task it executes in the play.
Looking back at Example 2-5, notice that the status for some of the tasks is changed, and the status for some others is ok. For example, the install nginx task has status changed, which appears as yellow on my terminal.
TASK: [install nginx] *********************************************************
changed: [testserver]
The enable configuration, on the other hand, has status ok, which appears as green on my terminal:
TASK: [enable configuration] **************************************************
ok: [testserver]
Any Ansible task that runs has the potential to change the state of the host in some way. Ansible modules will first check to see if the state of the host needs to be changed before taking any action. If the state of the host matches the arguments of the module, then Ansible takes no action on the host and responds with a state of ok.
On the other hand, if there is a difference between the state of the host and the arguments to the module, then Ansible will change the state of the host and return changed.
In the example output just shown, the install nginx task was changed, which meant that before I ran the playbook, the nginx package had not previously been installed on the host. The enable configuration task was unchanged, which meant that there was already a configuration file on the server that was identical to the file I was copying over. The reason for this is that the nginx.conf file I used in my playbook is the same as the nginx.conf file that gets installed by the nginx package on Ubuntu.
As we’ll see later in this chapter, Ansible’s detection of state change can be used to trigger additional actions through the use of handlers. But, even without using handlers, it is still a useful form of feedback to see whether your hosts are changing state as the playbook runs.
Getting Fancier: TLS Support
Let’s move on to a more complex example: We’re going to modify the previous playbook so that our webservers support TLS. The new features here are:
§ Variables
§ Handlers
TLS VERSUS SSL
You might be familiar with the term SSL rather than TLS in the context of secure web servers. SSL is an older protocol that was used to secure communications between browsers and web servers, and it has been superseded by a newer protocol named TLS.
Although many continue to use the term SSL to refer to the current secure protocol, in this book, I use the more accurate TLS.
Example 2-8 shows what our playbook looks like with TLS support.
Example 2-8. web-tls.yml
- name: Configure webserver with nginx and tls
hosts: webservers
sudo: True
vars:
key_file: /etc/nginx/ssl/nginx.key
cert_file: /etc/nginx/ssl/nginx.crt
conf_file: /etc/nginx/sites-available/default
server_name: localhost
tasks:
- name: Install nginx
apt: name=nginx update_cache=yes cache_valid_time=3600
- name: create directories for ssl certificates
file: path=/etc/nginx/ssl state=directory
- name: copy TLS key
copy: src=files/nginx.key dest={{ key_file }} owner=root mode=0600
notify: restart nginx
- name: copy TLS certificate
copy: src=files/nginx.crt dest={{ cert_file }}
notify: restart nginx
- name: copy nginx config file
template: src=templates/nginx.conf.j2 dest={{ conf_file }}
notify: restart nginx
- name: enable configuration
file: dest=/etc/nginx/sites-enabled/default src={{ conf_file }} state=link
notify: restart nginx
- name: copy index.html
template: src=templates/index.html.j2 dest=/usr/share/nginx/html/index.html
mode=0644
handlers:
- name: restart nginx
service: name=nginx state=restarted
Generating TLS certificate
We need to manually generate a TLS certificate. In a production environment, you’d purchase your TLS certificate from a certificate authority. We’ll use a self-signed certificate, since we can generate those for free.
Create a files subdirectory of your playbooks directory, and then generate the TLS certificate and key:
$ mkdir files
$ openssl req -x509 -nodes -days 3650 -newkey rsa:2048 \
-subj /CN=localhost \
-keyout files/nginx.key -out files/nginx.crt
It should generate the files nginx.key and nginx.crt in the files directory. The certificate has an expiration date of 10 years (3,650 days) from the day you created it.
Variables
The play in our playbook now has a section called vars:
vars:
key_file: /etc/nginx/ssl/nginx.key
cert_file: /etc/nginx/ssl/nginx.crt
conf_file: /etc/nginx/sites-available/default
server_name: localhost
This section defines four variables and assigns a value to each variable.
In our example, each value is a string (e.g., /etc/nginx/ssl/nginx.key), but any valid YAML can be used as the value of a variable. You can use lists and dictionaries in addition to strings and Booleans.
Variables can be used in tasks, as well as in template files. You reference variables using the {{ braces }} notation. Ansible will replace these braces with the value of the variable.
Consider this task in the playbook:
- name: copy TLS key
copy: src=files/nginx.key dest={{ key_file }} owner=root mode=0600
Ansible will substitute {{ key_file }} with /etc/nginx/ssl/nginx.key when it executes this task.
WHEN QUOTING IS NECESSARY
If you reference a variable right after specifying the module, the YAML parser will misinterpret the variable reference as the beginning of an in-line dictionary. Consider the following example:
- name: perform some task
command: {{ myapp }} -a foo
Ansible will try to parse the first part of {{ myapp }} -a foo as a dictionary instead of a string, and will return an error. In this case, you must quote the arguments:
- name: perform some task
command: "{{ myapp }} -a foo"
A similar problem arises if your argument contains a colon. For example:
- name: show a debug message
debug: msg="The debug module will print a message: neat, eh?"
The colon in the msg argument trips up the YAML parser. To get around this, you need to quote the entire argument string.
Unfortunately, just quoting the argument string won’t resolve the problem, either.
- name: show a debug message
debug: "msg=The debug module will print a message: neat, eh?"
This will make the YAML parser happy, but the output isn’t what you expect:
TASK: [show a debug message] ************************************************
ok: [localhost] => {
"msg": "The"
}
The debug module’s msg argument requires a quoted string to capture the spaces. In this particular case, we need to quote both the whole argument string and the msg argument. Ansible supports alternating single and double quotes, so you can do this:
- name: show a debug message
debug: "msg='The debug module will print a message: neat, eh?'"
This yields the expected output:
TASK: [show a debug message] ************************************************
ok: [localhost] => {
"msg": "The debug module will print a message: neat, eh?"
}
Ansible is pretty good at generating meaningful error messages if you forget to put quotes in the right places and end up with invalid YAML.
Generating the Nginx Configuration Template
If you’ve done web programming, you’ve likely used a template system to generate HTML. In case you haven’t, a template is just a text file that has some special syntax for specifying variables that should be replaced by values. If you’ve ever received an automated email from a company, they’re probably using an email template as shown in Example 2-9.
Example 2-9. An email template
Dear {{ name }},
You have {{ num_comments }} new comments on your blog: {{ blog_name }}.
Ansible’s use case isn’t HTML pages or emails — it’s configuration files. You don’t want to hand-edit configuration files if you can avoid it. This is especially true if you have to reuse the same bits of configuration data (say, the IP address of your queue server or your database credentials) across multiple configuration files. It’s much better to take the info that’s specific to your deployment, record it in one location, and then generate all of the files that need this information from templates.
Ansible uses the Jinja2 template engine to implement templating. If you’ve ever used a templating library such as Mustache, ERB, or the Django template system, Jinja2 will feel very familiar.
Nginx’s configuration file needs information about where to find the TLS key and certificate. We’re going to use Ansible’s templating functionality to define this configuration file so that we can avoid hard-coding values that might change.
In your playbooks directory, create a templates subdirectory and create the file templates/nginx.conf.j2, as shown in Example 2-10.
Example 2-10. templates/nginx.conf.j2
server {
listen 80 default_server;
listen [::]:80 default_server ipv6only=on;
listen 443 ssl;
root /usr/share/nginx/html;
index index.html index.htm;
server_name {{ server_name }};
ssl_certificate {{ cert_file }};
ssl_certificate_key {{ key_file }};
location / {
try_files $uri $uri/ =404;
}
}
We use the .j2 extension to indicate that the file is a Jinja2 template. However, you can use a different extension if you like; Ansible doesn’t care.
In our template, we reference three variables:
server_name
The hostname of the web server (e.g., www.example.com)
cert_file
The path to the TLS certificate
key_file
The path to the TLS private key
We define these variables in the playbook.
Ansible also uses the Jinja2 template engine to evaluate variables in playbooks. Recall that we saw the {{ conf_file }} syntax in the playbook itself.
NOTE
Early versions of Ansible used a dollar sign ($) to do variable interpolation in playbooks instead of the braces. You used to dereference variable foo by writing $foo, where now you write {{ foo }}. The dollar sign syntax has been deprecated; if you encounter it in an example playbook you find on the Internet, then you’re looking at older Ansible code.
You can use all of the Jinja2 features in your templates, but we won’t cover them in detail here. Check out the Jinja2 Template Designer Documentation for more details. You probably won’t need to use those advanced templating features, though. One Jinja2 feature you probably will use with Ansible is filters; we’ll cover those in a later chapter.
Handlers
Looking back at our web-tls.yml playbook, note that there are two new playbook elements we haven’t discussed yet. There’s a handlers section that looks like this:
handlers:
- name: restart nginx
service: name=nginx state=restarted
In addition, several of the tasks contain a notify key. For example:
- name: copy TLS key
copy: src=files/nginx.key dest={{ key_file }} owner=root mode=0600
notify: restart nginx
Handlers are one of the conditional forms that Ansible supports. A handler is similar to a task, but it runs only if it has been notified by a task. A task will fire the notification if Ansible recognizes that the task has changed the state of the system.
A task notifies a handler by passing the handler’s name as the argument. In the preceding example, the handler’s name is restart nginx. For an nginx server, we’d need to restart it6 if any of the following happens:
§ The TLS key changes
§ The TLS certificate changes
§ The configuration file changes
§ The contents of the sites-enabled directory change
We put a notify statement on each of the tasks to ensure that Ansible restarts nginx if any of these conditions are met.
A few things to keep in mind about handlers
Handlers only run after all of the tasks are run, and they only run once, even if they are notified multiple times. They always run in the order that they appear in the play, not the notification order.
The official Ansible docs mention that the only common uses for handlers are for restarting services and for reboots. Personally, I’ve only ever used them for restarting services. Even then, it’s a pretty small optimization, since we can always just unconditionally restart the service at the end of the playbook instead of notifying it on change, and restarting a service doesn’t usually take very long.
Another pitfall with handlers that I’ve encountered is that they can be troublesome when debugging a playbook. It goes something like this:
1. I run a playbook.
2. One of my tasks with a notify on it changes state.
3. An error occurs on a subsequent task, stopping Ansible.
4. I fix the error in my playbook.
5. I run Ansible again.
6. None of the tasks report a state change the second time around, so Ansible doesn’t run the handler.
Running the Playbook
As before, we use the ansible-playbook command to run the playbook.
$ ansible-playbook web-tls.yml
The output should look something like this:
PLAY [Configure webserver with nginx and tls] *********************************
GATHERING FACTS ***************************************************************
ok: [testserver]
TASK: [Install nginx] *********************************************************
changed: [testserver]
TASK: [create directories for tls certificates] *******************************
changed: [testserver]
TASK: [copy TLS key] **********************************************************
changed: [testserver]
TASK: [copy TLS certificate] **************************************************
changed: [testserver]
TASK: [copy nginx config file] ************************************************
changed: [testserver]
TASK: [enable configuration] **************************************************
ok: [testserver]
NOTIFIED: [restart nginx] *****************************************************
changed: [testserver]
PLAY RECAP ********************************************************************
testserver : ok=8 changed=6 unreachable=0 failed=0
Point your browser to https://localhost:8443 (don’t forget the “s” on https). If you’re using Chrome, like I am, you’ll get a ghastly message that says something like, “Your connection is not private” (see Figure 2-4).
Figure 2-4. Browsers like Chrome don’t trust self-signed TLS certificates
Don’t worry, though; that error is expected, as we generated a self-signed TLS certificate, and web browsers like Chrome only trust certificates that have been issued from a proper authority.
We covered a lot of the “what” of Ansible in this chapter, describing what Ansible will do to your hosts. The handlers we discussed here are just one form of control flow that Ansible supports. In a later chapter, we’ll see iteration and conditionally running tasks based on the values of variables.
In the next chapter, we’ll talk about the “who”; in other words, how to describe the hosts that your playbooks will run against.
1 Note that while we call this file nginx.conf, it replaces the sites-enabled/default nginx server block config file, not the main /etc/nginx.conf config file.
2 If you encountered an error, you might want to skip to Chapter 14 for assistance on debugging.
3 Colloquially referred to as a “shebang.”
4 Actually, it’s a list that contains a single play.
5 The modules that ship with Ansible all are written in Python, but modules can be written in any language.
6 Alternatively, we could reload the configuration file using state=reloaded instead of restarting the service.