Ansible OEL 8 DevOps · OEL 8 · Fundamentals

AnsibleJinja2 — Advanced Patterns

Custom filters, the do extension, complex data manipulation with map and selectattr, conditional defaults, and the YAML safety patterns that prevent silent template bugs.

Jinja2 has two related concepts: filters (transform a value, used after |) and tests (return true/false, used after is). Confusing them is a common beginner mistake.

JINJA2 — tests vs filters
# Filter — transforms the value
{{{{ "  hello  " | trim }}}}
# → "hello"

# Test — returns boolean
{{% if name is defined %}}
  hello {{{{ name }}}}
{{% endif %}}

# More tests
{{{{ value is none }}}}
{{{{ list is iterable }}}}
{{{{ x is divisibleby 2 }}}}
{{{{ string is match("^db") }}}}     {{# regex match #}}
{{{{ string is search("error") }}}}  {{# regex search #}}

Most variables in real-world Ansible are lists of dictionaries. The map, selectattr, and rejectattr filters are how you reshape them:

JINJA2 — list-of-dicts patterns
{{# Given:
users:
  - {{ name: alice, admin: true,  shell: /bin/bash }}
  - {{ name: bob,   admin: false, shell: /bin/zsh  }}
  - {{ name: carol, admin: true,  shell: /bin/bash }}
#}}

# Pluck just the names
{{{{ users | map(attribute='name') | list }}}}
# → ['alice', 'bob', 'carol']

# Filter to only admins
{{{{ users | selectattr('admin') | list }}}}
# → [{{ name: alice, admin: true, ... }}, {{ name: carol, admin: true, ... }}]

# Filter to non-admins
{{{{ users | rejectattr('admin') | list }}}}

# Match attribute against a value
{{{{ users | selectattr('shell', 'equalto', '/bin/zsh') | list }}}}

# Combine: get names of admins only
{{{{ users | selectattr('admin') | map(attribute='name') | list }}}}
# → ['alice', 'carol']

# Sort by an attribute
{{{{ users | sort(attribute='name') }}}}
JINJA2 — Python-style ternary
# value if condition else other_value
{{{{ "primary" if mysql_role == "primary" else "replica" }}}}

# Used in default — a common idiom for "use X if defined, else Y"
{{{{ custom_port | default(3306) }}}}

# Default that depends on another variable
{{{{ buffer_pool | default((ansible_memtotal_mb * 0.7) | int) }}}}

# Strict default — error if undefined
{{{{ required_var | mandatory }}}}

Many Ansible features (loops, includes) want a list of dicts but you have a dict. These filters convert in both directions:

YAML — dict2items in a loop
---
- hosts: localhost
  vars:
    ports:
      mysql: 3306
      postgres: 5432
      redis: 6379

  tasks:
    - name: Open each port
      ansible.posix.firewalld:
        port: "{{{{ item.value }}}}/tcp"
        state: enabled
        permanent: true
      loop: "{{{{ ports | dict2items }}}}"
      loop_control:
        label: "{{{{ item.key }}}} → {{{{ item.value }}}}"

# Reverse direction (less common)
# items2dict converts list of {{key: K, value: V}} dicts back to a flat dict
JINJA2 — regex examples
# Replace
{{{{ "db1.example.com" | regex_replace("\\.", "_") }}}}
# → "db1_example_com"

# Search returns the match (or None)
{{{{ "version 8.0.31-debian" | regex_search("\\d+\\.\\d+\\.\\d+") }}}}
# → "8.0.31"

# Find all matches
{{{{ "ports 3306, 33060, 33061" | regex_findall("\\d+") }}}}
# → ['3306', '33060', '33061']

# Capture groups
{{{{ "user=alice" | regex_replace("user=(\\w+)", "\\1") }}}}
# → "alice"
⚠ Warning: Regex inside YAML and Jinja2 needs double escaping — every literal backslash is \\\\. The example above shows what you actually type. regex_search returns None on no match, which Jinja2 silently prints as empty — always combine with a default or test for safety.
JINJA2 — converting between data and YAML
# Pretty-print a dict as YAML in your output
{{{{ users | to_nice_yaml(indent=2) }}}}

# Read a string of YAML back into a dict
{{% set parsed = yaml_string | from_yaml %}}

# Same for JSON
{{{{ data | to_nice_json }}}}
{{% set parsed = json_string | from_json %}}

If the built-in filters aren't enough, you can write your own in Python and drop them in filter_plugins/ next to your playbook. Ansible auto-loads them.

PYTHON — filter_plugins/my_filters.py
def to_systemd_env(d):
    """Convert a dict into systemd Environment= lines."""
    if not isinstance(d, dict):
        raise TypeError("expected a dict")
    return "\n".join(f'Environment="{k}={v}"' for k, v in d.items())

class FilterModule(object):
    def filters(self):
        return {
            'to_systemd_env': to_systemd_env,
        }
JINJA2 — using the custom filter
{{# In a template #}}
[Service]
{{{{ env_vars | to_systemd_env }}}}

# expands to:
# [Service]
# Environment="DB_HOST=db1"
# Environment="DB_PORT=3306"
✅ Tip: Custom filters keep templates clean — instead of stuffing complex string-mangling logic into Jinja2 expressions, name the operation in Python where it can be unit-tested.