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,
apps:
- 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
vars:
apps:
— name: web
cluster: gamma
url: "https://example.com"
— name: worker
cluster: alpha
count: 7
— name: relay
cluster: omega
relay_host: relay.example.com
tasks:
— 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: [127.0.0.1]: FAILED! => {"failed": true, "msg": "ERROR! template error while templating string: no filter named 'pluck'"}
PLAY RECAP *********************************************************************
127.0.0.1 : 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:
~/projects/ansible-repo/
▼ filter_plugins/
collections.py
▶ 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: '''
5:
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: [127.0.0.1]
TASK [debug]
*******************************************************************
ok: [127.0.0.1] => {
"app_names": [
"web",
"worker",
"relay"
],
"failed": false,
"failed_when_result": false
}
PLAY RECAP *********************************************************************
127.0.0.1 : 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 :-)