10 min read

In this article by the author, Daniel Hall, of the book, Ansible Configuration Management – Second Edition, we learn to start digging a bit deeper into playbooks.

We will be covering the following topics:

  • External data lookups
  • Storing results
  • Processing data
  • Debugging playboks

(For more resources related to this topic, see here.)

External data lookups

Ansible introduced the lookup plugins in version 0.9. These plugins allow Ansible to fetch data from outside sources. Ansible provides several plugins, but you can also write your own. This really opens the doors and allows you to be flexible in your configuration.

Lookup plugins are written in Python and run on the controlling machine. They are executed in two different ways: direct calls and with_* keys. Direct calls are useful when you want to use them like you would use variables. Using the with_* keys is useful when you want to use them as loops.

In the next example, we use a lookup plugin directly to get the http_proxy value from environment and send it through to the configured machine. This makes sure that the machines we are configuring will use the same proxy server to download the file.

---
- name: Downloads a file using a proxy
hosts: all
tasks:
   - name: Download file
     get_url:
       dest: /var/tmp/file.tar.gz         url: http://server/file.tar.gz
     environment:
       http_proxy: "{{ lookup('env', 'http_proxy') }}"

You can also use lookup plugins in the variable section. This doesn’t immediately lookup the result and put it in the variable as you might assume; instead, it stores it as a macro and looks it up every time you use it. This is good to know if you are using something, the value of which might change over time.

Using lookup plugins in the with_* form will allow you to iterate over things you wouldn’t normally be able to. You can use any plugin like this, but ones that return a list are most useful. In the following code, we show how to dynamically register a webapp farm.

---
- name: Registers the app server farm
hosts: localhost
connection: local
vars:
   hostcount: 5
tasks:
   - name: Register the webapp farm
     local_action: add_host name={{ item }} groupname=webapp
     with_sequence: start=1 end={{ hostcount }} format=webapp%02x

If you were using this example, you would append a task to create each as a virtual machine and then a new play to configure each of them.

Situations where lookup plugins are useful are as follows:

  • Copying a whole directory of Apache config to a conf.d style directory
  • Using environment variables to adjust what the playbooks does
  • Getting configuration from DNS TXT records
  • Fetching the output of a command into a variable

Storing results

Almost every module outputs something, even the debug module. Most of the time, the only variable used is the one named changed. The changed variable helps Ansible decide whether to run handlers or not and which color to print the output in. However, if you wish to, you can store the returned values and use them later in the playbook. In this example, we look at the mode in the /tmp directory and create a new directory named /tmp/subtmp with the same mode as shown here.

---
- name: Using register
hosts: ansibletest
user: root
tasks:
   - name: Get /tmp info
   file:
       dest: /tmp
       state: directory
     register: tmp
 
   - name: Set mode on /var/tmp
     file:
       dest: /tmp/subtmp
       mode: "{{ tmp.mode }}"
       state: directory

Some modules, such as the file module in the previous example, can be configured to simply give information. By combining this with the register feature, you can create playbooks that can examine the environment and calculate how to proceed.

Combining the register feature and the set_fact module allows you to perform data processing on data you receive back from the modules. This allows you to compute values and perform data processing on these values. This makes your playbooks even smarter and more flexible than ever.

Register allows you to make your own facts about hosts from modules already available to you. This can be useful in many different circumstances:

  • Getting a list of files in a remote directory and downloading them all with fetch
  • Running a task when a previous task changes, before the handlers run
  • Getting the contents of the remote host SSH key and building a known_hosts file

Processing data

Ansible uses Jinja2 filters to allow you to transform data in ways that aren’t possible with basic templates. We use filters when the data available to us in our playbooks is not in the format we want, or require further complex processing before it can be used with modules or templates. Filters can be used anywhere we would normally use a variable, such as in templates, as arguments to modules, and in conditionals. Filters are used by providing the variable name, a pipe character, and then the filter name. We can use multiple filter names separated with pipe characters to use multiple pipes, which are then applied left to right. Here is an example where we ensure that all users are created with lowercase usernames:

---
- name: Create user accounts
hosts: all
vars:
   users:
tasks:
   - name: Create accounts
     user: name={{ item|lower }} state=present
     with_items:
       - Fred
       - John
       - DanielH

Here are a few popular filters that you may find useful:

Filter

Description

min

When the argument is a list it returns only the smallest value.

max

When the argument is a list it returns only the largest value.

random

When the argument is a list it picks a random item from the list.

changed

When used on a variable created with the register keyword, it returns true if the task changed anything; otherwise, it returns false.

failed

When used on a variable created with the register keyword, it returns true if the task failed; otherwise, it returns false.

skipped

When used on a variable created with the register keyword, it returns true if the task changed anything; otherwise, it returns false.

default(X)

If the variable does not exist, then the value of X will be used instead.

unique

When the argument is a list, return a list without any duplicate items.

b64decode

Convert the base64 encoded string in the variable to its binary representation. This is useful with the slurp modules, as it returns its data as a base64 encoded string.

replace(X, Y)

Return a copy of the string with any occurrences of X replaced by Y.

join(X)

When the variable is a list, return a string with all the entries separated by X.

Debugging playbooks

There are a few ways in which you can debug a playbook. Ansible includes both a verbose mode and a debug module specifically for debugging. You can also use modules such as fetch and get_url for help. These debugging techniques can also be used to examine how modules behave when you wish to learn how to use them.

The debug module

Using the debug module is really quite simple. It takes two optional arguments, msg and fail.msg to set the message that will be printed by the module and fail, if set to yes, indicates a failure to Ansible, which will cause it to stop processing the playbook for that host. We used this module earlier in the skipping modules section to bail out of a playbook if the operating system was not recognized.

In the following example, we will show how to use the debug module to list all the interfaces available on the machine:

---
- name: Demonstrate the debug module
hosts: ansibletest
user: root
vars:
   hostcount: 5
tasks:
   - name: Print interface
     debug:
       msg: "{{ item }}"
     with_items: ansible_interfaces

The preceding code gives the following output:

PLAY [Demonstrate the debug module] *********************************
 
GATHERING FACTS *****************************************************
ok: [ansibletest]
 
TASK: [Print interface] *********************************************
ok: [ansibletest] => (item=lo) => {"item": "lo", "msg": "lo"}
ok: [ansibletest] => (item=eth0) => {"item": "eth0", "msg": "eth0"}
 
PLAY RECAP **********************************************************
ansibletest               : ok=2   changed=0   unreachable=0   failed=0

As you can see, the debug module is easy to use to see the current value of a variable during the play.

The verbose mode

Your other option for debugging is the verbose option. When running Ansible with verbose, it prints out all the values that were returned by each module after it runs. This is especially useful if you are using the register keyword introduced in the previous section. To run ansible-playbook in verbose mode, simply add –verbose to your command line as follows:

ansible-playbook –verbose playbook.yml

The check mode

In addition to the verbose mode, Ansible also includes a check mode and a diff mode. You can use the check mode by adding –check to the command line, and –diff to use the diff mode. The check mode instructs Ansible to walk through the play without actually making any changes to remote systems. This allows you to obtain a listing of the changes that Ansible plans to make to the configured system.

It is important here to note that the check mode of Ansible is not perfect. Any modules that do not implement the check feature are skipped. Additionally, if a module is skipped that provides more variables, or the variables depend on a module actually changing something (such as file size), then they will not be available. This is an obvious limitation when using the command or shell modules

The diff mode shows the changes that are made by the template module. This limitation is because the template file only works with text files. If you were to provide a diff of a binary file from the copy module, the result would almost be unreadable. The diff mode also works with the check mode to show you the planned changes that were not made due to being in check mode.

The pause module

Another technique is to use the pause module to pause the playbook while you examine the configured machine as it runs. This way, you can see changes that the modules have made at the current position in the play, and then watch while it continues with the rest of the play.

Summary

In this article, we explored the more advanced details of writing playbooks. You should now be able to use features such as delegation, looping, conditionals, and fact registration to make your plays much easier to maintain and edit. We also looked at how to access information from other hosts, configure the environment for a module, and gather data from external sources. Finally, we covered some techniques for debugging plays that are not behaving as expected.

Resources for Article:


Further resources on this subject:


LEAVE A REPLY

Please enter your comment!
Please enter your name here