Embedding Jinja2 templates in YAML files is one of Ansible's distinguishing features. This feature enables the creation of generic roles and playbooks that work without modification for multiple environments, or even multiple projects by simply overriding variables in the group_vars/ directory, the playbook, or even the command line.

Jinja2 comes with a few dozen built-in filters, and Ansible adds more of it's own for working with IP addresses, set logic, version comparisons, and more. Most of the time the built-in filters are sufficient, but sometimes you find yourself wishing you had a filter that does not exist. Fortunately, filters are easy to create, but Ansible punts on documenting them, shrugs, and blows you off with a discouraging, "Read the source, Luke!"

Well, I've read the source, written several of my own useful filters, and now I give you this guide for writing your own Ansible filter plugins.

Introducing the pluck filter

Recently, I wished I had a filter that extracts the value of a key from every item in a dictionary (a.k.a. hash or associative array), exactly like the Ruby on Rails #pluck method.

For example, given an array of dictionaries,

  - name: web
    cluster: gamma
    url: "https://example.com"
  - name: worker
    cluster: alpha
    count: 7
  - name: relay
    cluster: omega
    relay_host: relay.example.com

I can pluck the name keys from each hash in the array,

and now I have an array of app names.

['web', 'worker', 'relay']

Writing a test for the pluck filter

Because the pluck filter is intended for use in Ansible roles and playbooks, I am going to write a simple Ansible playbook to test the filter.

- name: test the pluck filter plugin
  hosts: localhost
  connection: local
  gather_facts: false
      — name: web
        cluster: gamma
        url: "https://example.com"
      — name: worker
        cluster: alpha
        count: 7
      — name: relay
        cluster: omega
        relay_host: relay.example.com

    — set_fact:
        app_names: ""

    - debug: var=app_names
      failed_when: ""

Running the test playbook fails as expected because I haven’t created the pluck filter yet.

$ ansible-playbook filter-plugin-test.yml

PLAY [test the pluck filter plugin]

TASK [set_fact]
fatal: []: FAILED! => {"failed": true, "msg": "ERROR! template error while templating string: no filter named 'pluck'"}

PLAY RECAP ********************************************************************* : ok=0 changed=0 unreachable=0 failed=1

Implementing the pluck filter

In the Ansible best practices guide, they suggest that Jinja2 filter plugins unsurprisingly belong in the filter_plugins/ directory of your repository, so that is where we will place ours. We will name the file collection.py, but you can choose any name you like as long as you use the .py extension (Jinja2 and Ansible are both written in the Python programming language). Your Ansible repo should now look like the following:

    ▼ filter_plugins/
    ▶ filter-plugin-test.yml

Now it's time to write our pluck function, which if you recall, has the following syntax:

The Python code that makes this filter work, looks like this.

1: def pluck(collection, key):
2:     '''
3:     extract x[key] for every item x in the collection
4:     '''
6:     return [x[key] for x in collection]

In line 1, we define the pluck function with 2 arguments, collection, which appears on the left-hand side of the pipe | operator, and key which represents the first argument passed to the Jinja2 filter. Lines 2–4 are a Python docstring describing the function. Finally, line 6 uses a Python generator expression to transform our array of dictionaries into an array of values plucked form the dictionaries and return the result.

The final step is to tell Ansible that the pluck function implements a Jinja2 filter expression, and we do that by creating a FilterModule class that implements a filters method that returns a dictionary of filter name/function pairings.

class FilterModule(object):
    custom jinja2 filters for working with collections

    def filters(self):
        return {
            'pluck': pluck

Place this code after the pluck function in collection.py. Seeing the pluck filter in action Now that we've written the pluck filter, we can run our playbook again, and see that it works.

$ ansible-playbook filter-plugin-test.yml

PLAY [test the pluck filter plugin]

TASK [set_fact]
ok: []

TASK [debug]
ok: [] => {
 "app_names": [
 "failed": false,
 "failed_when_result": false

PLAY RECAP ********************************************************************* : ok=2 changed=0 unreachable=0 failed=0

Looking at the play output, you can see the app_names variable contains the expected result of ["web", "worker", "relay"], and the playbook finished with all tasks completing successfully.

Next steps

In this post, you learned how to extend Ansible by writing a custom Jinja2 filter. If you use this post to build your own Ansible Jinja2 filter, please share it with me in the comments :-)