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:
| roles/database/tasks/main.yml | Tasks |
| roles/database/files/ | Holds files to be uploaded to hosts |
| roles/database/templates/ | Holds Jinja2 template files |
| roles/database/handlers/main.yml | Handlers |
| roles/database/vars/main.yml | Variables that shouldn’t be overridden |
| roles/database/defaults/main.yml | Default variables that can be overridden |
| roles/database/meta/main.yml | Dependency information about a role |
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.
[defaults]
roles_path = ~/ansible_roles
Using Roles in Your Playbooks
- name: deploy mezzanine on vagrant
## target hosts
hosts: web
vars_files:
- secrets.yml
## role section
roles:
- role: database
## pass variables into role task
database_name: "{{ mezzanine_proj_name }}"
database_user: "{{ mezzanine_proj_name }}"
- role: mezzanine
live_hostname: 192.168.33.10.xip.io
domains:
- 192.168.33.10.xip.io
- www.192.168.33.10.xip.io
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.
- name: deploy mezzanine on vagrant
hosts: web
vars_files:
- secrets.yml
pre_tasks:
- name: update the apt cache
apt: update_cache=yes
roles:
- role: mezzanine
post_tasks:
- name: notify Slack that the servers have been updated
local_action: >
slack
domain=acme.slack.com
token={{ slack_token }}
msg="web server {{ inventory_hostname }} configured"
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:
mezzanine_user: "{{ ansible_user }}"
mezzanine_venv_home: "{{ ansible_env.HOME }}"
mezzanine_venv_path: "{{ mezzanine_venv_home }}/{{ mezzanine_proj_name }}"
mezzanine_repo_url: [email protected]:lorin/mezzanine-example.git
mezzanine_proj_dirname: project
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:
roles/mezzanine/tasks/django.yml
roles/mezzanine/tasks/main.yml
roles/mezzanine/tasks/nginx.yml
In the main.yml
task file you can invoke other tasks by using include
statement:
- name: install apt packages
apt: pkg={{ item }} update_cache=yes cache_valid_time=3600
become: True
with_items:
- git
- supervisor
- include: django.yml
- include: nginx.yml
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:
- name: set the nginx config file
template: src=templates/nginx.conf.j2 \
dest=/etc/nginx/sites-available/mezzanine.conf
now looks like this when invoked from inside the role (note the change of the src parameter):
- name: set the nginx config file
template: src=nginx.conf.j2 dest=/etc/nginx/sites-available/mezzanine.conf
notify: restart nginx
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:
ansible-galaxy init <path>/roles/web
└── roles
└── web
├── README.md
├── defaults
│ └── main.yml
├── files
├── handlers
│ └── main.yml
├── meta
│ └── main.yml
├── tasks
│ └── main.yml
├── templates
├── tests
│ ├── inventory
│ └── test.yml
└── vars
└── main.yml
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:
dependencies:
- { role: ntp, ntp_server=ntp.ubuntu.com }
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.
- name: initialize the database
django_manage:
command: createdb --noinput --nodata
app_path: "{{ proj_path }}"
virtualenv: "{{ venv_path }}"
failed_when: False
register: result
- debug: var=result
- fail:
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:
- name: initialize the database
django_manage:
command: createdb --noinput --nodata
app_path: "{{ proj_path }}"
virtualenv: "{{ venv_path }}"
register: result
changed_when: '"Creating tables" in result.out|default("")'
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 {{ braces }}
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
"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.
- name: Run myprog
command: /opt/myprog
register: result
ignore_errors: True
- debug: var=result
- debug: msg="Stop running the playbook if myprog failed"
failed_when: result|failed
a list of filters you can use on registered variables to check the status:
| Name | Description |
|--------- |------------------------------------------------------- |
| failed | True if a registered value is a task that failed |
| changed | True if a registered value is a task that changed |
| success | True if a registered value is a task that succeeded |
| skipped | True if a registered value is a task that was skipped |
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:
vars:
homepage: /usr/share/nginx/html/index.html
tasks:
- name: copy home page
copy: src=files/{{ homepage | basename }} dest={{ homepage }}
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.
| Name | Description |
|---------- |------------------------------------ |
| file | Contents of a file |
| password | Randomly generate a password |
| pipe | Output of locally executed command |
| env | Environment variable |
| template | Jinja2 template after evaluation |
| csvfile | Entry in a .csv file |
| dnstxt | DNS TXT record |
| redis_kv | Redis key lookup |
| etcd | etcd key lookup |
You can invoke lookups in your playbooks between {{ braces }}
, 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.
- name: Add my public key as an EC2 key
ec2_key: name=mykey key_material="{{ lookup('file', '/Users/lorin/.ssh/id_rsa.pub') }}"
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
- name: get SHA of most recent commit
debug: msg="{{ lookup('pipe', 'git rev-parse HEAD') }}"
TASK: [get the sha of the current commit] *************************************
ok: [myserver] => {
"msg": "e7748af0f040d58d61de1917980a210df419eae9"
}
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:
- name: get the current shell
debug: msg="{{ lookup('env', 'SHELL') }}"
TASK: [get the current shell] *************************************************
ok: [myserver] => {
"msg": "/bin/zsh"
}
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:
- name: create deploy postgres user
postgresql_user:
name: deploy
password: "{{ lookup('password', 'deploy-password.txt') }}"
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:
username,email
lorin,[email protected]
john,[email protected]
sue,[email protected]
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 [email protected].
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:
curl -L http://127.0.0.1:4001/v2/keys/weather -XPUT -d value=cloudy
- name: look up value in etcd
debug: msg="{{ lookup('etcd', 'weather') }}"
TASK: [look up value in etcd] *************************************************
ok: [localhost] => {
"msg": "cloudy"
}
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.
| Name | Input | Looping strategy |
|-------------------------- |---------------------- |----------------------------------- |
| with_items | List | Loop over list elements |
| with_lines | Command to execute | Loop over lines in command output |
| with_fileglob | Glob | Loop over filenames |
| with_first_found | List of paths | First file in input that exists |
| with_dict | Dictionary | Loop over dictionary elements |
| with_flattened | List of lists | Loop over flattened list |
| with_indexed_items | List | Single iteration |
| with_nested | List | Nested loop |
| with_random_choice | List | Single iteration |
| with_sequence | Sequence of integers | Loop over sequence |
| with_subelements | List of dictionaries | Nested loop |
| with_together | List of lists | Loop over zipped list |
| with_inventory_hostnames | Host pattern | Loop over matching hosts |
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.
- name: Send out a slack message
slack:
domain: example.slack.com
token: "{{ slack_token }}"
msg: "{{ item }} was in the list"
with_lines:
- cat files/turing.txt
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.
- name: add public keys to account
authorized_key: user=deploy key="{{ lookup('file', item) }}"
with_fileglob:
- /var/keys/*.pub
- keys/*.pub
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:
- name: iterate over ansible_eth0
debug: msg={{ item.key }}={{ item.value }}
with_dict: "{{ ansible_eth0.ipv4 }}"
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:
- name: Add my public key as an EC2 key
ec2_key: name=mykey key_material="{{ item }}"
with_file: /Users/lorin/.ssh/id_rsa.pub
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
:
- user:
name: "{{ user.name }}"
with_items:
## list of dict
- { name: gil }
- { name: sarina }
- { name: leanne }
loop_control:
loop_var: 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:
- name: run a set of tasks in one loop
include: vhosts.yml
with_items:
- { domain: www1.example.com }
- { domain: www2.example.com }
- { domain: www3.example.com }
loop_control:
loop_var: vhost
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.
- name: create nginx directories
file:
path: /var/www/html/{{ vhost.domain }}/{{ item }}
state: directory
with_items:
- logs
- public_http
- public_https
- includes
- name: create nginx vhost config
template:
src: "{{ vhost.domain }}.j2"
dest: /etc/nginx/conf.d/{{ vhost.domain }}.conf
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.
- name: create nginx vhost configs
template:
src: "{{ item.domain }}.conf.j2"
dest: "/etc/nginx/conf.d/{{ item.domain }}.conf"
with_items:
- { domain: www1.example.com, ssl_enabled: yes }
- { domain: www2.example.com }
- { domain: www3.example.com,
aliases: [ edge2.www.example.com, eu.www.example.com ] }
loop_control:
label: "for domain {{ item.domain }}"
TASK [create nginx vhost configs] **********************************************
ok: [localhost] => (item=for domain www1.example.com)
ok: [localhost] => (item=for domain www2.example.com)
ok: [localhost] => (item=for domain www3.example.com)
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:
# nginx_include.yml file
- name: install nginx
package:
name: nginx
- name: ensure nginx is running
service:
name: nginx
state: started
enabled: yes
- include: nginx_include.yml
tags: nginx
become: yes
when: ansible_os_family == 'RedHat'
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.
- include: Redhat.yml
when: ansible_os_family == 'Redhat'
- include: Debian.yml
when: ansible_os_family == 'Debian'
Since version 2.0, Ansible allows us to dynamically include a file by using variable substitution:
- include: "{{ ansible_os_family }}.yml"
static: no
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.
- name: install php
include_role:
name: 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.
- name: install php
include_role:
name: php
tasks_from: install
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:
- block:
- name: install nginx
package:
name: nginx
- name: ensure nginx is running
service:
name: nginx
state: started
enabled: yes
become: yes
when: "ansible_os_family == 'RedHat'"
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.
- block:
- debug: msg="You will see a failed tasks right after this"
- command: /bin/false
- debug: "You won't see this message"
rescue: # Tasks to be executed in case of a failure in block clause
- debug: "You only see this message in case of an failure in the block"
always: # Tasks to always be executed
- debug: "This will be always executed"
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.
ansible-vault create secrets.yml
ansible-vault encrypt 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
.
ansible-playbook <playbook> --ask-vault-pass
ansible-playbook <playbook> --vault-password-file ~/password.txt
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.
| Command | Description |
|-------------------------------- |--------------------------------------------------- |
| ansible-vault encrypt file.yml | Encrypt the plain-text file.yml file |
| ansible-vault decrypt file.yml | Decrypt the encrypted file.yml file |
| ansible-vault view file.yml | Print the contents of the encrypted file.yml file |
| ansible-vault create file.yml | Create a new encrypted file.yml file |
| ansible-vault edit file.yml | Edit an encrypted file.yml file |
| ansible-vault rekey file.yml | Change the password on an encrypted file.yml file |
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:
hosts: all
You can specify a union of two groups with a colon. You specify all dev and staging machines as follows:
hosts: dev:staging
| Action | Example Usage |
|--------------------------- |----------------------------- |
| All hosts | all |
| All hosts | * |
| Union | dev:staging |
| Intersection | dev:&database |
| Exclusion | dev:!queue |
| Wildcard | *.example.com |
| Range of numbered servers | web[5:12] |
| Regular expression | ~web\d+\.example\.(com|org) |
Ansible supports multiple combinations of patterns—for example:
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
ansible-playbook -l hosts playbook.yml
ansible-playbook --limit hosts playbook.yml
ansible-playbook -l 'staging:&database' 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.
- name: wait for ssh server to be running
local_action: wait_for port=22 host="{{ inventory_hostname }}" search_regex=OpenSSH
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.
- name: enable alerts for web servers
hosts: web
tasks:
- name: enable alerts
nagios: action=enable_alerts service=web host={{ inventory_hostname }}
delegate_to: nagios.example.com
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.
- name: upgrade packages on servers behind load balancer
hosts: myhosts
serial: 1
tasks:
- name: get the ec2 instance id and elastic load balancer id
ec2_facts:
- name: take the host out of the elastic load balancer
local_action: ec2_elb
args:
instance_id: "{{ ansible_ec2_instance_id }}"
state: absent
- name: upgrade packages
apt: update_cache=yes upgrade=yes
- name: put the host back in the elastic load balancer
local_action: ec2_elb
args:
instance_id: "{{ ansible_ec2_instance_id }}"
state: present
ec2_elbs: "{{ item }}"
with_items: ec2_elbs
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:
- name: upgrade packages on servers behind load balancer
hosts: myhosts
serial: 1
max_fail_percentage: 25
tasks:
# tasks go here
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.
- name: configure CDN servers
hosts: cdn
serial:
- 1
- 30%
tasks:
# tasks go here
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:
- name: run the task locally, only once
local_action: command /opt/my-custom-command
run_once: true
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:
one sleep_seconds=1
two sleep_seconds=6
three sleep_seconds=10
Note that the orders show up is the complete order in target host, first done at top.
TASK [setup] *******************************************************************
ok: [two]
ok: [three]
ok: [one]
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.
- hosts: all
strategy: free
tasks:
...