这本书其实挺不错的,我还总结了在工作中ansible的常用模块在另一个blog里面。
Could we remove major architectural components from the IT automation stack? Eliminating management daemons and relying instead on OpenSSH meant the system could start managing a computer fleet immediately, without having to set up anything on the managed machines.
the “Making Ansible Go Even Faster” chapter now covers asynchronous tasks, and the “Debugging Ansible Playbooks” chapter now covers the debugger that was introduced in version 2.1.
we are all slowly turning into system engineers.
Chapter 1. Introduction
When we talk about configuration management, we are typically talking about writing some kind of state description for our servers, and then using a tool to enforce that the servers are, indeed, in that state: the right packages are installed, configuration files contain the expected values and have the expected permissions, the right services are running, and so on.
Ansible is a great tool for deployment as well as configuration management. Using a single tool for both configuration management and deployment makes life simpler for the folks responsible for operations.
Some people talk about the need for orchestration of deployment. This is where multiple remote servers are involved, and things have to happen in a specific order.
How Ansible works
In Ansible, a script is called a playbook. A playbook describes which hosts (what Ansible calls remote servers) to configure, and an ordered list of tasks to perform on those hosts.
Ansible will make SSH connections in parallel to web1, web2, and web3. It will execute the first task on the list on all three hosts simultaneously
To manage a remote server with Ansible, the server needs to have SSH and Python 2.5 or later installed, or Python 2.4 with the Python simplejsonlibrary
installed. There’s no need to preinstall an agent or anyother software on the host.
The control machine (the one that you use to control remote machines) needs to have Python 2.6 or later installed.
Ansible is push based, and has been used successfully in production with thousands of nodes, and has excellent support for environments where servers are dynamically added and removed.
Ansible modules are declarative; you use them to describe the state you want the server to be in. Modules are also idempotent.
Ansible has excellent support for templating, as well as defining variables at different scopes. Anybody who thinks Ansible is equivalent to working with shell scripts has never had to maintain a nontrivial program written in shell. I’ll always choose Ansible over shell scripts for config management tasks if given a choice.
To be productive with Ansible, you need to be familiar with basic Linux system administration tasks. Ansible makes it easy to automate your tasks, but it’s not the kind of tool that “automagically” does things that you otherwise wouldn’t know how to do.
Ansible uses the YAML file format and the Jinja2 templating languages, so you’ll need to learn some YAML and Jinja2 to use Ansible, but both technologies are easy to pick up.
If you prefer not to spend the money on a public cloud, I recommend you install Vagrant on your machine. Vagrant is an excellent open source tool for managing virtual machines. You can use Vagrant to boot a Linux virtual machine inside your laptop, and you can use that as a test server.
Vagrant needs the VirtualBox virtualizer to be installed on your machine. Download VirtualBox and then download Vagrant.
1 | mkdir playbooks |
The first time you use vagrant up, it will download the virtual machine image file, which might take a while, depending on your internet connection.
You should be able to SSH into your new Ubuntu 14.04 virtual machine by running the following:
1 | vagrant ssh |
This approach lets us interact with the shell, but Ansible needs to connect to the virtual machine by using the regular SSH client, not the vagrant ssh command.
Tell Vagrant to output the SSH connection details by typing the following:
1 | vagrant ssh-config |
1 | ssh vagrant@127.0.0.1 -p 2222 -i /Users/lorin/dev/ansiblebook/ch01/ |
1 | testserver ansible_host=127.0.0.1 ansible_port=2222 ansible_user=vagrant ansible_private_key_file=.vagrant/machines/default/virtualbox/private_key |
1 | ~/.ansible.cfg |
Ansible supports the ssh-agent program, so you don’t need to explicitly specify SSH key files in your inventory files. See “SSH Agent” for more details if you haven’t used ssh-agent before
If Ansible did not succeed, add the -vvvv
flag to see more details about the error:
1 | ansible testserver -i hosts -m ping -vvvv |
Simplify by ansible.cfg file
we’ll use one such mechanism, the ansible.cfg
file, to set some defaults so we don’t need to type as much.
Ansible looks for an ansible.cfg
file in the following places, in this order:
- File specified by the ANSIBLE_CONFIG environment variable
- ./ansible.cfg (ansible.cfg in the current directory)
- ~/.ansible.cfg (.ansible.cfg in your home directory)
- /etc/ansible/ansible.cfg
I typically put ansible.cfg in the current directory, alongside my playbooks. That way, I can check it into the same version-control repository that my playbooks are in.
1 | [defaults] |
Disables SSH host-key checking. Otherwise, we need to edit our ~/.ssh/known_hosts file every time we destroy and re-create a nodes.
Ansible uses /etc/ansible/hosts
as the default location for the inventory file. However, I never use this because I like to keep my inventory files version-controlled alongside my playbooks.
The command
module is so commonly used that it’s the default module, so we can omit it
1 | ansible testserver -a uptime |
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.
vargant virtual machine ports mapping in vagrantfile:
1 | VAGRANTFILE_API_VERSION = "2" |
1 | vagrant reload |
TLS VERSUS SSL
You might be familiar with the term SSL rather than TLS(Transport Layer Security) 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.
WHY DO YOU USE TRUE IN ONE PLACE AND YES IN ANOTHER?
Strictly speaking, module arguments (for example, update_cache=yes) are treated differently from values elsewhere in playbooks (for example, sudo: True). Values elsewhere are handled by the YAML parser and so use the YAML conventions of truthiness:
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:
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.
NOTE
An Ansible convention is to keep files in a subdirectory named files, and Jinja2 templates in a subdirectory named templates. I follow this convention throughout the book.
For .j2
file, when Ansible renders this template, it will replace this variable with the real value.
Inventory files are in the .ini
file format.
COWSAY
If you have the cowsay program installed on your local machine, Ansible output will look like this instead:
you can download cowsay rpm and install it:
1 | cowsay -l |
set what animal you like:
1 | export ANSIBLE_COW_SELECTION=tux |
enable cowsay:
1 | export ANSIBLE_NOCOWS=0 |
then if you run playbook:
1 | < PLAY [Configure webserver with nginx] > |
If you don’t want to see the cows, you can disable cowsay by setting the ANSIBLE_NOCOWS
environment variable like this:
1 | export ANSIBLE_NOCOWS=1 |
You can also disable cowsay by adding the following to your ansible.cfg file:
1 | [defaults] |
TIP
If your playbook file is marked as executable and starts with a line that looks like this
1 | #!/usr/bin/env ansible-playbook |
then you can execute it by invoking it directly, like this:
1 | ./playbook-file.yml |
YAML syntax
Start of file
1 | --- |
However, if you forget to put those three dashes at the top of your playbook files, Ansible won’t complain.
String
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.
In some scenarios in Ansible, you will need to quote strings. These typically involve the use of for variable substitution.
Boolean
YAML has a native Boolean type, and provides you with a wide variety of strings that can be interpreted as true or false.
List
1 | - My Fair Lady |
Dictionary
1 | address: 742 Evergreen Terrace |
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 by using line folding with the greater than (>
) character. The YAML parser will replace line breaks with spaces. For example:
1 | address: > |
Anatomy of a Playbook
A playbook is a list of plays:
1 | --- |
you’ll see in Chapter 16, you can use the --start-at-task <task name>
flag to tell ansible-playbook
to start a playbook in the middle of a play, 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
.
The arguments are treated as a string, not as a dictionary. This means that if you want to break arguments into multiple lines, you need to use the YAML folding syntax, like this:
1 | - name: install nginx |
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. For example:
1 | - name: Install docker |
Modules
Modules are scripts that come packaged with Ansible and perform some kind of action on a host.
commonly used modules:
- yum
- copy
- file
- service
- template
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.
1 | ansible-doc yum |
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.
More than 200 modules 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 tasks. Each task is associated with exactly one module.
1 | +-------------+ +------------+ +-------------+ |
Did Anything Change? Tracking Host State
Ansible modules will first check to see whether 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, Ansible takes no action on the host and responds with a state of ok
.
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.
Variables and Handlers
1 | - name: Configure webserver with nginx and tls |
Generating a TLS Certificate
In a production environment, you’d purchase your TLS certificate from a certificate authority, or use a free service such as Let’s Encrypt, which Ansible supports via the letsencrypt module.
Here we use self-signed certificate generated free of charge:
1 | mkdir files |
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
Variables can be used in tasks, as well as in template files. You reference variables by using the notation. Ansible replaces these braces with the value of the variable.
WHEN QUOTING IS NECESSARY
bad:
1 | - name: perform some task |
good:
1 | - name: perform some task |
A similar problem arises if your argument contains a colon. For example, bad:
1 | - name: show a debug message |
good:
1 | - name: show a debug message |
Generating the Template
We put templates in templates
folder, 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.
You can use all of the Jinja2 features in your templates, you probably won’t need to use those advanced templating features, though. One Jinja2 feature you probably will use with Ansible is filters: Jinja2 Template Designer Documentation.
Handlers
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.
A FEW THINGS TO KEEP IN MIND ABOUT HANDLERS
Handlers usually run after all of the tasks are run at the end of the play. They run only once, even if they are notified multiple times. If a play contains multiple handlers, the handlers always run in the order that they are defined in the handlers section, 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 always used them only 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.
Chapter 3. Inventory: Describing Your Servers
The collection of hosts that Ansible knows about is called the inventory. In this chapter, you will learn how to describe a set of hosts as an Ansible inventory.
Ansible automatically adds one host to the inventory by default: localhost. Ansible understands that localhost refers to your local machine, so it will interact with it directly rather than connecting by SSH.
Preliminaries: Multiple Vagrant Machines
Before you modify your existing Vagrantfile, make sure you destroy your existing virtual machine by running the following:
1 | vagrant destroy --force |
vagrantfile for three servers:
1 | VAGRANTFILE_API_VERSION = "2" |
Using the same key on each host simplifies our Ansible setup because we can specify a single SSH key in the ansible.cfg file:
1 | [defaults] |
In the inventory file, you can use ansible_host
to explicitly specify IP and ansible_port
indicates SSH port number:
1 | vagrant1 ansible_host=127.0.0.1 ansible_port=2222 |
Behavioral Inventory Parameters
1 | | Name | Default | Description | |
Explanation:
-
ansible_connection Ansible supports multiple transports, which are mechanisms that Ansible uses to connect to the host. The default transport,
smart
, will check whether the locally installed SSH client supports a feature called ControlPersist. If the SSH client supports ControlPersist, Ansible will use the local SSH client. If the SSH client doesn’t support ControlPersist, the smart transport will fall back to using a Python-based SSH client library called Paramiko. -
ansible_shell_type Ansible works by making SSH connections to remote machines and then invoking scripts. By default, Ansible assumes that the remote shell is the Bourne shell located at /bin/sh, and will generate the appropriate command-line parameters that work with Bourne shell.
Ansible also accepts
csh
,fish
, and (on Windows)powershell
as valid values for this parameter. I’ve never encountered a need for changing the shell type. -
ansible_python_interpreter Because the modules that ship with Ansible are implemented in Python 2, Ansible needs to know the location of the Python interpreter on the remote machine. You might need to change this if your remote host does not have a Python 2 interpreter at /usr/bin/python. For example, if you are managing hosts that run Arch Linux, you will need to change this to /usr/bin/python2, because Arch Linux installs Python 3 at /usr/bin/python, and Ansible modules are not (yet) compatible with Python 3.
-
ansible_*_interpreter If you are using a custom module that is not written in Python, you can use this parameter to specify the location of the interpreter (e.g., /usr/bin/ruby).
Note: You can override some of the behavioral parameter default values in the defaults section of the ansible.cfg
file
1 | | Behavioral inventory parameter | ansible.cfg option | |
Groups
Ansible automatically defines a group called all
(or *
), which includes all of the hosts in the inventory. For example:
1 | ansible all -a "date" |
We can define our own groups in the inventory file. Ansible uses the .ini file format for inventory files. In the .ini format, configuration values are grouped together into sections. For example:
1 | [master] |
Alias and Ports:
In inventory file:
1 | [master] |
Groups of groups
Ansible also allows you to define groups that are made up of other groups. Here web and task are groups, diango subgroup wrap them up.
1 | [django:children] |
Numbered hosts
1 | [host] |
Host and Group Variables
Format: [
1 | [all:vars] |
Additionally, though Ansible variables can hold Booleans, strings, lists, and dictionaries, in an inventory file, you can specify only Booleans and strings.
Ansible offers a more scalable approach to keep track of host and group variables: you can create a separate variable file for each host and each group. Ansible expects these variable files to be in YAML format.
Ansible looks for host variable files in a directory called host_vars
and group variable files in a directory called group_vars
. Ansible expects these directories to be either in the directory that contains your playbooks or in the directory adjacent to your inventory file. For example:
1 | playbooks folder: |
If we use YAML dictionary format in group variable file:
1 | db: |
when reference in playbook:
1 | {{ db.primary.host }} |
Dynamic Inventory
If the inventory file is marked executable, Ansible will assume it is a dynamic inventory script and will execute the file instead of reading it.
If some other system, such as AWS EC2, will keep track of the virtual machine information for us, we don’t necessarily need to write the inventory file manually, we can use dynamic inventory script to query about which machines are running and use them.
Adding Entries at Runtime with add_host and group_by
Ansible will let you add hosts and groups to the inventory during the execution of a playbook. not use yet
Chapter 4. Variables and Facts
Ansible is not a full-fledged programming language, but it does have several programming language features, and one of the most important of these is variable substitution. This chapter presents Ansible’s support for variables in more detail, including a certain type of variable that Ansible calls a fact.
Defining Variables in Playbooks
There are two scenarios, for example:
1 | - name: Configure Kubeadm |
Viewing the Values of Variables
We use the debug
module to print out an arbitrary message. We can also use it to output the value of the variable.
1 | - debug: var=<myvarname> |
Registering Variables
Often, you’ll find that you need to set the value of a variable based on the result of a task.To do so, we create a registered variable using the register
clause when invoking a module. Below shows how to capture the output of the whoami
command to a variable named login
.
1 | - name: capture output of whoami command |
Note, if you want to use login
variable registered here, it’s not like that you can call it as
In order to use the login variable later, we need to know the type of value to expect. The value of a variable set using the register clause is always a dictionary, but the specific keys of the dictionary are different, depending on the module that was invoked.
Unfortunately, the official Ansible module documentation doesn’t contain information about what the return values look like for each module. The module docs do often contain examples that use the register clause, which can be helpful. I’ve found the simplest way to find out what a module returns is to register a variable and then output that variable with the debug module.
1 | - name: show return value of command module |
The shell
module has the same output structure as the command
module, but other modules contain different keys, the output here is:
1 | TASK: [debug var=login] ******************************************************* |
-
The
changed
key is present in the return value of all Ansible modules, and Ansible uses it to determine whether a state change has occurred. For thecommand
andshell
module, this will always be set totrue
unless overridden with thechanged_when
clause. -
The
cmd
key contains the invoked command as a list of strings. -
The
rc
key contains the return code. If it is nonzero, Ansible will assume the task failed to execute. -
The
stdout
key contains any text written to standard out, as a single string. -
The
stdout_lines
key contains any text written to split by newline. It is a list, and each element of the list is a line of output.
So now you can access login
with:
1 | - name: capture output of id command |
ACCESSING DICTIONARY KEYS IN A VARIABLE
If a variable contains a dictionary, you can access the keys of the dictionary by using either a dot (.) or a subscript ([]).
1 | {{ login.stdout }} |
CAUTION
If your playbooks use registered variables, make sure you know the content of those variables, both for cases where the module changes the host’s state and for when the module doesn’t change the host’s state. Otherwise, your playbook might fail when it tries to access a key in a registered variable that doesn’t exist.
Facts
As you’ve already seen, when Ansible runs a playbook, before the first task runs, this happens:
1 | GATHERING FACTS ************************************************** |
When Ansible gathers facts, it connects to the host and queries it for all kinds of details about the host: CPU architecture, operating system, IP addresses, memory info, disk info, and more. This information is stored in variables that are called facts, and they behave just like any other variable. For example:
1 | - name: print out operating system |
List of facts variable can be found here
Viewing All Facts Associated with a Server
Ansible implements fact collecting through the use of a special module called the setup
module. You don’t need to call this module in your playbooks because Ansible does that automatically when it gathers facts.
1 | ansible server1 -m setup |
1 | server1 | success >> { |
Note that the returned value is a dictionary whose key is ansible_facts
and whose value is a dictionary that contains the name and value of the actual facts.
Viewing a Subset of Facts
The setup
module supports a filter
parameter that lets you filter by fact name by specifying a glob.
1 | ansible web -m setup -a 'filter=ansible_eth*' |
Any Module Can Return Facts
The use of ansible_facts
in the return value is an Ansible idiom. If a module returns a dictionary that contains ansible_facts
as a key, Ansible will create variable names in the environment with those values and associate them with the active host.
For modules that return facts, there’s no need to register variables, since Ansible creates these variables for you automatically.
1 | - name: get ec2 facts |
Several modules ship with Ansible that return facts. You’ll see another one of them, the docker
module.
Local Facts
Ansible provides an additional mechanism for associating facts with a host. You can place one or more files on the remote host machine in the /etc/ansible/facts.d
directory.
not used yet
Using set_fact to Define a New Variable
Ansible also allows you to set a fact (effectively the same as defining a new variable) in a task by using the set_fact
module. I often like to use set_fact
immediately after register
to make it simpler to refer to a variable.
1 | - name: get snapshot id |
Built-in Variables
Ansible defines several variables that are always available in a playbook
1 | | Parameter | Description | |
-
hostvars This is a dictionary that contains all of the variables defined on all of the hosts, keyed by the hostname as known to Ansible. If Ansible has not yet gathered facts on a host, you will not be able to access its facts by using the hostvars variable, unless fact caching is enabled.
1
{{ hostvars['db.example.com'].ansible_eth1.ipv4.address }}
-
inventory_hostname The inventory_hostname is the hostname of the current host, as known by Ansible.
-
groups The groups variable can be useful when you need to access variables for a group of hosts.
I encounter this in playbook vars section:
1 | - name: xxx |
Setting Variables on the Command Line
Variables set by passing -e var=value
to ansible-playbook
have the highest precedence, which means you can use this to override variables that are already defined.
1 | ansible-playbook greet.yml -e 'greeting="hi there"' |
Ansible also allows you to pass a file containing the variables instead of passing them directly on the command line by passing @filename.yml as the argument to -e
1 | ansible-playbook greet.yml -e @greetvars.yml |
content in greetvars.yml
:
1 | greeting: hiya |
Precedence
We’ve covered several ways of defining variables, and it can happen that you define the same variable multiple times for a host, using different values. Avoid this when you can, but if you can’t, then keep in mind Ansible’s precedence rules.
1 | 1. (Highest) ansible-playbook -e var=value |
Chapter 5. Introducing Mezzanine: Our Test Application
Let’s take a little detour and talk about the differences between running software in development mode on your laptop versus running the software in production. Mezzanine is a great example of an application that is much easier to run in development mode than it is to deploy.
1 | virtualenv venv |
You’ll be prompted to answer several questions. I answered “yes” to each yes/no question, and accepted the default answer whenever one was available.
Now, let’s look at what happens when you deploy to production.
-
PostgreSQL: The Database In production, we want to run a server-based database, because those have better support for multiple, concurrent requests, and server-based databases allow us to run multiple HTTP servers for load balancing. This means we need to deploy a database management system such as MySQL or PostgreSQL (aka Postgres).
1
2
3
4
5
6Install the database software.
Ensure the database service is running.
Create the database inside the database management system.
Create a database user who has the appropriate permissions for the database system.
Configure our Mezzanine application with the database user credentials and
connection information. -
Gunicorn: The Application Server
-
Nginx: The Web Server
Note: Application server and Web server, their usage is different. Here Nginx is like a reverse proxy for Gunicorn.
- Supervisor: The Process Manager
Chapter 7. Roles: Scaling Up Your Playbooks
Ansible scales down well because simple tasks are easy to implement. It scales up well because it provides mechanisms for decomposing complex jobs into smaller pieces.
In Ansible, the role
is the primary mechanism for breaking a playbook into multiple files. This simplifies writing complex playbooks, and it makes them easier to reuse.
Basic Structure of a Role
An Ansible role has a name, such as database
. Files associated with the database
role go in the roles/database directory, which contains the following files and directories:
1 | | roles/database/tasks/main.yml | Tasks | |
Each individual file is optional; if your role doesn’t have any handlers, there’s no need to have an empty handlers/main.yml file.
WHERE DOES ANSIBLE LOOK FOR MY ROLES?
Ansible looks for roles in the roles directory alongside your playbooks. It also looks for systemwide roles in /etc/ansible/roles. You can customize the systemwide location of roles by setting the roles_path
setting in the defaults
section of your ansible.cfg file.
1 | [defaults] |
Using Roles in Your Playbooks
1 | - name: deploy mezzanine on vagrant |
Note that we can pass in variables when invoking the roles. If these variables have already been defined in the role (either in vars/main.yml or defaults/main.yml), then the values will be overridden with the variables that were passed in.
Pre-Tasks and Post-Tasks
Sometimes you want to run tasks before or after you invoke your roles. Let’s say you want to update the apt cache before you deploy Mezzanine, and you want to send a notification to a Slack channel after you deploy.
Ansible allows you to define a list of tasks that execute before the roles with a pre_tasks
section, and a list of tasks that execute after the roles with a post_tasks
section.
1 | - name: deploy mezzanine on vagrant |
Note: you need to define and put variables in right place, for example, the variables that would be used by multiple roles or playbooks should be put in
group_vars/all
file.
WHY ARE THERE TWO WAYS TO DEFINE VARIABLES IN ROLES?
When Ansible first introduced support for roles, there was only one place to define role variables, in vars/main.yml. Variables defined in this location have a higher precedence than those defined in the vars
section of a play, which meant you couldn’t override the variable unless you explicitly passed it as an argument to the role.
Ansible later introduced the notion of default role variables that go in defaults/main.yml.This type of variable is defined in a role, but has a low precedence, so it will be overridden if another variable with the same name is defined in the playbook.
If you think you might want to change the value of a variable in a role, use a default variable. If you don’t want it to change, use a regular variable.
Some role practices
Note that if for role variables, it’s better to add prefix like <role name>_<var name>
. It’s good practice to do this with role variables because Ansible doesn’t have any notion of namespace across roles. This means that variables that are defined in other roles, or elsewhere in a playbook, will be accessible everywhere. This can cause some unexpected behavior if you accidentally use the same variable name in two different roles. For example, for the role called mezzanine
, in roles/mezzanine/vars/main.yml
file:
1 | mezzanine_user: "{{ ansible_user }}" |
For role variables in default/main.yml, no need to add prefix because we may intentionally override them elsewhere.
If the role task is very long, you can break it into several task files, for example:
1 | roles/mezzanine/tasks/django.yml |
In the main.yml
task file you can invoke other tasks by using include
statement:
1 | - name: install apt packages |
Note: there’s one important difference between tasks defined in a role and tasks defined in a regular playbook, and that’s when using the copy
or template
modules.
When invoking copy
in a task defined in a role, Ansible will first check the rolename/files/ directory for the location of the file to copy. Similarly, when invoking template
in a task defined in a role, Ansible will first check the rolename/templates directory for the location of the template to use.
This means that a task that used to look like this in a playbook:
1 | - name: set the nginx config file |
now looks like this when invoked from inside the role (note the change of the src parameter):
1 | - name: set the nginx config file |
Creating Role Files and Directories with ansible-galaxy
Ansible ships with another command-line tool we haven’t talked about yet, ansible-galaxy
. Its primary purpose is to download roles that have been shared by the Ansible community. But it can also be used to generate scaffolding, an initial set of files and directories involved in a role:
1 | ansible-galaxy init <path>/roles/web |
1 | └── roles |
Dependent Roles
Ansible supports a feature called dependent roles to deal with this scenario. When you define a role, you can specify that it depends on one or more other roles. Ansible will ensure that roles that are specified as dependencies are executed first.
Let’s say that we create anntp
role that configures a host to synchronize its time with an NTP server. Ansible allows us to pass parameters to dependent roles, so let’s also assume that we can pass the NTP server as a parameter to that role.
We specify that the web
role depends on the ntp
role by creating a roles/web/meta/main.yml file and listing ntp
as a role, with a parameter:
1 | dependencies: |
Ansible Galaxy
Whether you want to reuse a role somebody has already written, or you just want to see how someone else solved the problem you’re working on, Ansible Galaxy can help you out. Ansible Galaxy is an open source repository of Ansible roles contributed by the Ansible community. The roles themselves are stored on GitHub.
Chapter 8. Complex Playbooks
This chapter touches on those additional features, which makes it a bit of a grab bag.
Dealing with Badly Behaved Commands
What if we didn’t have a module that could invoke equivalent commands (wasn’t idempotent)? The answer is to use changed_when
and failed_when
clauses to change how Ansible identifies that a task has changed state or failed.
First, we need to understand the output of this command the first time it’s run, and the output when it’s run the second time.
1 | - name: initialize the database |
failed_when: False
is to close task fail, so ansible play will continue to execute. We can run several times of the playbook and see different register variable output.
fail
statement here is to stop the execution.
Some module may not report changed state even though it did make change in target machine, so we can check if state changed ourselves by using changed_when
clause:
1 | - name: initialize the database |
We use filter here in changed_when
since register variable sometimes doesn’t have out
field. Alternatively, we could provide a default value for result.out
if it doesn’t exist by using the Jinja2 default filter.
Filter
Filters are a feature of the Jinja2 templating engine. Since Ansible uses Jinja2 for evaluating variables, as well as for templates, you can use filters inside in your playbooks, as well as inside your template files. Using filters resembles using Unix pipes, whereby a variable is piped through a filter. Jinja2 ships with a set of built-in filters. In addition, Ansible ships with its own filters to augment the Jinja2 filters.
The Default Filter
1 | "HOST": "{{ database_host | default('localhost') }}" |
If the variable database_host
is defined, the braces will evaluate to the value of that variable. If the variable database_host
is not defined, the braces will evaluate to the string localhost.
Filters for Registered Variables
Let’s say we want to run a task and print out its output, even if the task fails. However, if the task does fail, we want Ansible to fail for that host after printing the output.
1 | - name: Run myprog |
a list of filters you can use on registered variables to check the status:
1 | | Name | Description | |
The basename filter will let us extract the index.html part of the filename from the full path, allowing us to write the playbook without repeating the filename:
1 | vars: |
Lookups
Sometimes a piece of configuration data you need lives somewhere else. Maybe it’s in a text file or a .csv file, and you don’t want to just copy the data into an Ansible variable file because now you have to maintain two copies of the same data.
Ansible has a feature called lookups
that allows you to read in configuration data from various sources and then use that data in your playbooks and template.
1 | | Name | Description | |
You can invoke lookups in your playbooks between , or you can put them in templates.
Note all Ansible lookup plugins execute on the control machine, not the remote host.
file
Let’s say you have a text file on your control machine that contains a public SSH key that you want to copy to a remote server.
1 | - name: Add my public key as an EC2 key |
pipe
The pipe
lookup invokes an external program on the control machine and evaluates to the program’s output on standard out.
For example, if our playbooks are version controlled using git
, and we want to get the SHA-1
value of the most recent git commit
, we could use the pipe
lookup
1 | - name: get SHA of most recent commit |
1 | TASK: [get the sha of the current commit] ************************************* |
env
The env
lookup retrieves the value of an environment variable set on the control machine.For example, we could use the lookup like this:
1 | - name: get the current shell |
1 | TASK: [get the current shell] ************************************************* |
password
The password
lookup evaluates to a random password, and it will also write the password to a file specified in the argument. For example, if we want to create a Postgres user named deploy
with a random password and write that password to deploy-password.txton the control machine, we can do this:
1 | - name: create deploy postgres user |
template
The template
lookup lets you specify a Jinja2 template file, and then returns the result of evaluating the template.
csvfile
The csvfile
lookup reads an entry from a .csv file.
For example, we have a users.csv
file:
1 | username,email |
1 | lookup('csvfile', 'sue file=users.csv delimiter=, col=1') |
In the case of csvfile
, the first argument is an entry that must appear exactly once in column 0 (the first column, 0-indexed) of the table.
In our example, we want to look in the file named users.csv
and locate where the fields are delimited by commas, look up the row where the value in the first column is sue
, and return the value in the second column (column 1, indexed by 0). This evaluates to sue@example.org.
etcd
Etcd is a distributed key-value store, commonly used for keeping configuration data and for implementing service discovery. You can use the etcd
lookup to retrieve the value of a key.
For example, let’s say that we have an etcd server running on our control machine, and we set the key weather to the value cloudy by doing something like this:
1 | curl -L http://127.0.0.1:4001/v2/keys/weather -XPUT -d value=cloudy |
1 | - name: look up value in etcd |
By default, the etcd lookup looks for the etcd server at http://127.0.0.1:4001, but you can change this by setting the ANSIBLE_ETCD_URL
environment variable before invoking ansible-playbook.
More Complicated Loops
Up until this point, whenever we’ve written a task that iterates over a list of items, we’ve used the with_items
clause to specify a list of items. Although this is the most common way to do loops, Ansible supports other mechanisms for iteration.
1 | | Name | Input | Looping strategy | |
The official documentation covers these quite thoroughly, so I’ll show examples from just a few of them to give you a sense of how they work.
with_lines
The with_lines looping construct lets you run an arbitrary command on your control machine and iterate over the output, one line at a time. For example, read a file and iterate over its contents line by line.
1 | - name: Send out a slack message |
with_fileglob
The with_fileglob construct is useful for iterating over a set of files on the control machine.
For example, iterate over files that end in .pub
in the /var/keys directory, as well as a keys directory next to your playbook. It then uses the file
lookup plugin to extract the contents of the file, which are passed to the authorized_key module.
1 | - name: add public keys to account |
with_dict
The with_dict
construct lets you iterate over a dictionary instead of a list. When you use this looping construct, the item
loop variable is a dictionary with two fields:
1 | - name: iterate over ansible_eth0 |
Looping Constructs as Lookup Plugins
Ansible implements looping constructs as lookup plugins. That means you can alter the form of lookup to perform as a loop:
1 | - name: Add my public key as an EC2 key |
Here we prefix with_
with file
lookup plugin. Typically, you use a lookup plugin as a looping construct only if it returns a list,
Loop Controls
With version 2.1, Ansible provides users with more control over loop handling.
Setting the Variable Name
The loop_var
control allows us to give the iteration variable a different name than the default name, item
:
1 | - user: |
Next one is a advanced usage, use include
with with_items
, we loop over multiple task at once, in current task we include a task called vhosts.yml
which will be executed 3 times with different parameters passed in:
1 | - name: run a set of tasks in one loop |
The vhosts.yml
file that is going to be included may also contain with_items
in some tasks. This would produce a conflict, as the default loop_var item
is used for both loops at the same time.
To prevent a naming collision, we specify a different name for loop_var in the outer loop.
1 | - name: create nginx directories |
Labeling the Output
The label
control was added in Ansible 2.2 and provides some control over how the loop output will be shown to the user during execution.
1 | - name: create nginx vhost configs |
1 | TASK [create nginx vhost configs] ********************************************** |
Includes
The include
feature allows you to include tasks or even whole playbooks, depending on where you define an include. It is often used in roles to separate or even group tasks and task arguments to each task in the included file.
For example, you can extract different part of tasks, put them into a separate yml file and include it into another task along with common arguments:
1 | # nginx_include.yml file |
1 | - include: nginx_include.yml |
Ansible Tags: If you have a large playbook, it may become useful to be able to run only a specific part of it rather than running everything in the playbook. Ansible supports a tags:
attribute for this reason.
Dynamic includes
A common pattern in roles is to define tasks specific to a particular operating system into separate task files.
1 | - include: Redhat.yml |
Since version 2.0, Ansible allows us to dynamically include a file by using variable substitution:
1 | - include: "{{ ansible_os_family }}.yml" |
However, there is a drawback to using dynamic includes: ansible-playbook --list-tasks
might not list the tasks from a dynamic include if Ansible does not have enough information to populate the variables that determine which file will be included.
You can use ansible-playbook <playbook> --list-tasks
to list all the tasks in it.
Role includes
A special include is the include_role
clause. In contrast with the role
clause, which will use all parts of the role, the include_role
not only allows us to selectively choose what parts of a role will be included and used, but also where in the play.
1 | - name: install php |
This will include and run main.yml from the php role, remember a role can have multiple tasks yml files: main.yml and others.
1 | - name: install php |
This will include and run install.yml from php role.
Blocks
Much like the include
clause, the block
clause provides a mechanism for grouping tasks. The block
clause allows you to set conditions or arguments for all tasks within a block at once:
1 | - block: |
The become
and when
apply for both tasks.
Error Handling with Blocks
Dealing with error scenarios has always been a challenge. Historically, Ansible has been error agnostic in the sense that errors and failures may occur on a host. Ansible’s default error-handling behavior is to take a host out of the play if a task fails and continue as long as there are hosts remaining that haven’t encountered errors.
1 | - block: |
If you have some programming experience, the way error handling is implemented may remind you of the try-catch-finally
paradigm, and it works much the same way.
Encrypting Sensitive Data with Vault
Ansible provides an alternative solution: instead of keeping the secrets.yml
file out of version control, we can commit an encrypted version. That way, even if our version-control repository were compromised, the attacker would not have access to the contents of the secrets.yml
file unless he also had the password used for the encryption.
The ansible-vault
command-line tool allows you to create and edit an encrypted file that ansible-playbook
will recognize and decrypt automatically, given the password.
1 | ansible-vault create secrets.yml |
You will be prompted for a password, and then ansible-vault
will launch a text editor so that you can populate the file. It launches the editor specified in the $EDITOR
environment variable. If that variable is not defined, it defaults to vim
.
1 | ansible-playbook <playbook> --ask-vault-pass |
If the argument to --vault-password-file
has the executable bit set, Ansible will execute it and use the contents of standard out as the vault password. This allows you to use a script to provide the password to Ansible.
1 | | Command | Description | |
Chapter 9. Customizing Hosts, Runs, and Handlers
In this chapter, we cover Ansible features that provide customization by controlling which hosts to run against, how tasks are run, and how handlers are run.
Patterns for Specifying Hosts
Instead of specifying a single host or group for a play, you can specify a pattern. You’ve already seen the all
pattern, which will run a play against all known hosts:
1 | hosts: all |
You can specify a union of two groups with a colon. You specify all dev and staging machines as follows:
1 | hosts: dev:staging |
1 | | Action | Example Usage | |
Ansible supports multiple combinations of patterns—for example:
1 | hosts: dev:staging:&database:!queue |
Limiting Which Hosts Run
Use the -l hosts
or --limit hosts
flag to tell Ansible to limit the hosts to run the playbook against the specified list of hosts
1 | ansible-playbook -l hosts playbook.yml |
Running a Task on the Control Machine
Sometimes you want to run a particular task on the control machine instead of on the remote host. Ansible provides the local_action
clause for tasks to support this. For example, when we check the node ready status in k8s cluster.
Imagine that the server we want to install Mezzanine onto has just booted, so that if we run our playbook too soon, it will error out because the server hasn’t fully started up yet. We could start off our playbook by invoking the wait_for
module to wait until the SSH server is ready to accept connections before we execute the rest of the playbook.
1 | - name: wait for ssh server to be running |
Note that inventory_hostname
evaluates to the name of the remote host, not localhost
. That’s because the scope of these variables is still the remote host, even though the task is executing locally.
If your play involves multiple hosts, and you use local_action
, the task will be executed multiple times, one for each host. You can restrict this by using run_once
Running a Task on a Machine Other Than the Host
Sometimes you want to run a task that’s associated with a host, but you want to execute the task on a different server. You can use the delegate_to
clause to run the task on a different host.
1 | - name: enable alerts for web servers |
In this example, Ansible would execute the nagios task
on nagios.example.com, but the inventory_hostname
variable referenced in the play would evaluate to the web host.
Note: if you specify
delegate_to: localhost
to control machine, it’s the same aslocal_action
, also the same asconnection: local
Running on One Host at a Time
By default, Ansible runs each task in parallel across all hosts. Sometimes you want to run your task on one host at a time. The canonical example is when upgrading application servers that are behind a load balancer. Typically, you take the application server out of the load balancer, upgrade it, and put it back. But you don’t want to take all of your application servers out of the load balancer, or your service will become unavailable.
You can use the serial
clause on a play to tell Ansible to restrict the number of hosts that a play runs on.
1 | - name: upgrade packages on servers behind load balancer |
In our example, we pass 1 as the argument to the serial clause, telling Ansible to run on only one host at a time. If we had passed 2, Ansible would have run two hosts at a time.
Normally, when a task fails, Ansible stops running tasks against the host that fails, but continues to run against other hosts. In the load-balancing scenario, you might want Ansible to fail the entire play before all hosts have failed a task. Otherwise, you might end up with the situation where you have taken each host out of the load balancer, and have it fail, leaving no hosts left inside your load balancer.
You can use a max_fail_percentage
clause along with the serial
clause to specify the maximum percentage of failed hosts before Ansible fails the entire play. For example, assume that we specify a maximum fail percentage of 25%, as shown here:
1 | - name: upgrade packages on servers behind load balancer |
If you want Ansible to fail if any of the hosts fail a task, set the max_fail_percentage
to 0.
Note:
any_errors_fatal: true
is just like setmax_fail_percentage
to 0, with theany_errors_fatal
option, any failure on any host in a multi-host play will be treated as fatal and Ansible will exit immediately without waiting for the other hosts.
We can get even more sophisticated. For example, you might want to run the play on one host first, to verify that the play works as expected, and then run the play on a larger number of hosts in subsequent runs.
1 | - name: configure CDN servers |
In the preceding play with 30 CDN hosts, on the first batch run Ansible would run against one host, and on each subsequent batch run it would run against at most 30% of the hosts (e.g., 1, 10, 10, 9).
Running Only Once
Using run_once
can be particularly useful when using local_action
if your playbook involves multiple hosts, and you want to run the local task only once:
1 | - name: run the task locally, only once |
Running Strategies
The strategy clause on a play level gives you additional control over how Ansible behaves per task for all hosts.
The default behavior we are already familiar with is the linear
strategy. This is the strategy in which Ansible executes one task on all hosts and waits until the task has completed (of failed) on all hosts before it executes the next task on all hosts. As a result, a task takes as much time as the slowest host takes to complete the task.
Linear
Note: I forget that host file can define variable, here sleep_seconds
can be referred in task:
1 | one sleep_seconds=1 |
Note that the orders show up is the complete order in target host, first done at top.
1 | TASK [setup] ******************************************************************* |
Free
Another strategy available in Ansible is the free
strategy. In contrast to linear
, Ansible will not wait for results of the task to execute on all hosts. Instead, if a host completes one task, Ansible will execute the next task on that host.
1 | - hosts: all |
Advanced Handlers
When we covered handlers, you learned that they are usually executed after all tasks, once, and only when they get notified. But keep in mind there are not only tasks
, but pre_tasks
, tasks
, and post_tasks
.
Each tasks section in a playbook is handled separately; any handler notified in pre_tasks
, tasks
, or post_tasks
is executed at the end of each section. As a result, it is possible to execute one handler several times in one play:
Note: the rest of Chapter 9 is useless for me now, just skip it.
Chapter 16. Debugging Ansible Playbooks
Humane Error Messages
Enable the plugin by adding the following to the defaults
section of ansible.cfg:
1 | [defaults] |
the debug
callback plugin makes this output much easier for a human to read, for example the format is like this:
1 | TASK [check out the repository on the host] ************************************* |
Debugging SSH Issues
Sometimes Ansible fails to make a successful SSH connection with the host. When this happens, it’s helpful to see exactly what arguments Ansible is passing to the underlying SSH client so you can reproduce the problem manually on the command line.
If you invoke ansible-playbook
with the -vvv
argument, you can see the exact SSH commands that Ansible invokes. This can be handy for debugging.
Note that usually I use
-v
flag
Sometimes you might need to use -vvvv when debugging a connection issue, in order to see an error message that the SSH client is throwing.
The Debug Module
We’ve used the debug
module several times in this book. It’s Ansible’s version of a print
statement.
1 | - debug: var=myvariable |
Playbook Debugger
Ansible 2.1 added support for an interactive debugger. To enable debugging, add strategy: debug
to your play; for example:
1 | - name: an example play |
If debugging is enabled, Ansible drops into the debugger when a task fails, for example, I write a task like this:
1 | - name: install xxx package |
1 | TASK [install.components : interactive debug] ************************************************************************ |
Let’s see the command list
1 | | Command | Description | |
variables supported by the debugger
1 | | Command | Description | |
The Assert Module
The assert
module will fail with an error if a specified condition is not met. For example, to fail the playbook if there’s no eth1
interface:
1 | - name: assert that eth1 interface exists |
When debugging a playbook, it can be helpful to insert assertions so that a failure happens as soon as any assumption you’ve made has been violated.
Keep in mind that the code in an assert
statement is Jinja2, not Python.
Checking Your Playbook Before Execution
The ansible-playbook
command supports several flags that allow you to sanity check your playbook before you execute it.
Syntax Check
The --syntax-check
flag checks that your playbook’s syntax is valid, but it does not execute it.
1 | ansible-playbook --syntax-check -i <host file> playbook.yml |
List Hosts
The --list-hosts flag outputs the hosts that the playbook will run against, but it does not execute the playbook.
1 | ansible-playbook --list-hosts -i <host file> playbook.yml |
List Tasks
Outputs the tasks that the playbook will run against. It does not execute the playbook.
1 | ansible-playbook --list-tasks -i <host file> playbook.yml |
Check Mode
The -C
and --check
flags run Ansible in check mode (sometimes known as dry-run), which tells you whether each task in the playbook will modify the host, but does not make any changes to the server.
1 | ansible-playbook --check -i <host file> playbook.yml |
One of the challenges with using check mode is that later parts of a playbook might succeed only if earlier parts of the playbook were executed.
Diff (Show File Changes)
The -D
and -diff
flags output differences for any files that are changed on the remote machine. It’s a helpful option to use in conjunction with --check
to show how Ansible would change the file if it were run normally:
1 | ansible-playbook --diff --check -i <host file> playbook.yml |
If Ansible would modify any files (e.g., using modules such as copy
, template
, and lineinfile
), it will show the changes in .diff format, like this:
1 | TASK: [set the gunicorn config file] ****************************************** |
Limiting Which Tasks Run
Sometimes you don’t want Ansible to run every single task in your playbook, particularly when you’re first writing and debugging the playbook. Ansible provides several command-line options that let you control which tasks run.
Step
The --step
flag, shown in Example 16-7, has Ansible prompt you before running each task, like this:
1 | Perform task: install packages (y/n/c): |
You can choose to execute the task (y), skip it (n), or tell Ansible to continue running the rest of the playbook without prompting you ©.
1 | ansible-playbook -i <host file> --step playbook.yml |
Start-at-Task
The --start-at-task taskname
flag tells Ansible to start running the playbook at the specified task, instead of at the beginning. This can be handy if one of your tasks failed because there was a bug in one of your tasks, and you want to rerun your playbook starting at the task you just fixed.
1 | ansible-playbook -i <host file> --start-at-task="install packages" playbook.yml |
Tags
Ansible allows you to add one or more tags to a task or a play. For example, here’s a play that’s tagged with foo
and a task that’s tagged with bar
and quux
:
1 | - hosts: myservers |
Use the -t tagnames
or --tags tagnames
flag to tell Ansible to run only plays and tasks that have certain tags. Use the --skip-tags tagnames
flag to tell Ansible to skip plays and tasks that have certain tags.
1 | ansible-playbook -t foo,bar playbook.yml |