Ansible: Up and Running, 2nd Edition, Chapter 7-10

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_taskssection.

- 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 as local_action, also the same as connection: 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 set max_fail_percentage to 0, with the any_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:
     ...

Advanced Handlers

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章