Aller au contenu
Infrastructure as Code medium

Filter plugins Ansible : écrire ses propres filtres Jinja en Python

11 min de lecture

Logo Ansible

Un filter plugin transforme une variable Jinja avec du code Python, exactement comme | upper ou | length mais avec votre logique. Cas d’usage typiques : conversion de format (snake_case → camelCase), normalisation de données métier, calculs maison qui pollueraient un playbook si on les écrivait en Jinja inline. Le code tourne côté control node au moment du templating, pas sur le managed node.

À la fin de cette page vous saurez écrire un filter plugin testé, lever une erreur Ansible propre quand l’entrée est invalide, et le packager dans une collection pour le partager — pas dans un dossier filter_plugins/ racine, qui est la voie historique sortie de la mode.

  • Différencier un filter plugin custom des filtres Jinja natifs (map, selectattr, regex_replace).
  • Écrire la classe FilterModule minimale qui expose une fonction Python.
  • Lever une AnsibleFilterError propre sur entrée invalide.
  • Passer des arguments supplémentaires au filtre (pattern value | filter(arg1, arg2)).
  • Packager dans une collection (plugins/filter/) plutôt qu’en filter_plugins/ racine.
  • Tester localement avant de publier.

Un filtre Jinja natif (upper, length, default, map, selectattr) est intégré à Ansible. Un filter plugin custom est une fonction Python que vous écrivez et exposez via une classe FilterModule. Les deux s’utilisent avec la même syntaxe valeur | filtre.

CasFiltre natif suffisantFilter plugin custom
Mettre en majusculesupper
Sélectionner par attributselectattr('attr', 'equalto', 'x')
Convertir snake_case en camelCaseimpossible en pur Jinjafilter plugin
Calculer un hash métier (ex. format CRC custom)impossiblefilter plugin
Transformer JSON Cloud → format internegymnastique Jinja illisiblefilter plugin

Règle pratique : si vous écrivez plus de 2 filtres Jinja chaînés sur une variable, un filter plugin sera plus lisible et plus testable.

Tout filter plugin Ansible comprend deux éléments :

  1. Une fonction Python qui prend la valeur en entrée, applique la transformation, retourne le résultat.
  2. Une classe FilterModule qui expose la fonction sous un nom utilisable dans un playbook via la méthode filters().
plugins/filter/string_utils.py
def snake_to_camel(text):
words = text.split("_")
return words[0].lower() + "".join(w.title() for w in words[1:])
class FilterModule(object):
def filters(self):
return {
"snake_to_camel": snake_to_camel,
}

Convention de nommage : la clé du dictionnaire (ici snake_to_camel) est ce que le playbook utilisera. La fonction Python peut s’appeler comme vous voulez, mais aligner les deux noms évite la confusion.

Sans gestion d’erreur, un filtre qui reçoit un type inattendu plante avec une trace Python brute — AttributeError: 'int' object has no attribute 'split' — peu lisible pour quelqu’un qui débogue un playbook.

from ansible.errors import AnsibleFilterError
def snake_to_camel(text, suffix=""):
if not isinstance(text, str):
raise AnsibleFilterError(
f"snake_to_camel: expected string, got {type(text).__name__}"
)
words = text.split("_")
return suffix + words[0].lower() + "".join(w.title() for w in words[1:])

Le résultat côté playbook devient parlant :

The filter plugin 'snake_to_camel' failed: snake_to_camel: expected string, got int

AnsibleFilterError est la classe d’exception idiomatique pour les filtres — préférée à AnsibleError (réservée aux modules) ou à un raise Python brut.

Un filter plugin peut recevoir des arguments en plus de la valeur principale, exactement comme default('valeur') ou regex_replace('pattern', 'replacement') :

def snake_to_camel(text, suffix="", upper_first=False):
if not isinstance(text, str):
raise AnsibleFilterError(
f"snake_to_camel: expected string, got {type(text).__name__}"
)
words = text.split("_")
head = words[0].title() if upper_first else words[0].lower()
return suffix + head + "".join(w.title() for w in words[1:])

Usage côté playbook :

- ansible.builtin.debug:
msg:
- "{{ 'user_name' | snake_to_camel }}" # → userName
- "{{ 'user_name' | snake_to_camel('var_') }}" # → var_userName
- "{{ 'user_name' | snake_to_camel(upper_first=true) }}" # → UserName

Les arguments positionnels et nommés fonctionnent comme en Python standard.

Historiquement, on plaçait les filtres dans un dossier filter_plugins/ à la racine d’un projet Ansible. Cette voie marche toujours mais elle est non partageable, non versionnable indépendamment du projet, et incompatible avec Automation Hub. La voie moderne est la collection.

ma-collection/
├── galaxy.yml
└── plugins/
└── filter/
└── string_utils.py

Le galaxy.yml minimal :

namespace: mon_org
name: utils
version: 1.0.0
description: Filtres custom pour le projet
authors:
- Stéphane Robert

Build et installation locale pour tester :

Fenêtre de terminal
ansible-galaxy collection build .
ansible-galaxy collection install mon_org-utils-1.0.0.tar.gz --force

Usage côté playbook avec FQCN (Fully Qualified Collection Name) :

- ansible.builtin.debug:
msg: "{{ 'user_name' | mon_org.utils.snake_to_camel }}"

Le FQCN évite les collisions quand deux collections exposent un filtre du même nom.

Une approche pragmatique : un fichier de test isolé qui couvre les cas nominaux et les cas d’erreur.

test-filtres.yml
---
- name: Tests des filter plugins
hosts: localhost
gather_facts: false
vars:
cas_nominal: "user_name"
cas_erreur: 42
tasks:
- name: Cas nominal — snake_to_camel
ansible.builtin.assert:
that:
- "(cas_nominal | snake_to_camel) == 'userName'"
- "(cas_nominal | snake_to_camel('var_')) == 'var_userName'"
- name: Cas erreur — type non-string lève AnsibleFilterError
ansible.builtin.debug:
msg: "{{ cas_erreur | snake_to_camel }}"
register: result
ignore_errors: true
- name: Le filtre a bien échoué
ansible.builtin.assert:
that:
- result is failed
- "'expected string' in result.msg"

Lancement :

Fenêtre de terminal
ansible-playbook test-filtres.yml

Pour une collection publiable, ansible-test units avec des tests pytest reste la référence — c’est ce que valide la CI Galaxy.

Quand un filter plugin n’est PAS la bonne réponse

Section intitulée « Quand un filter plugin n’est PAS la bonne réponse »
SymptômeBonne piste plutôt qu’un filtre
Vous voulez exécuter une commande sur la cibleModule (cf. Modules Python)
Vous voulez orchestrer un appel API côté control nodeAction plugin (cf. Action plugins)
Vous voulez transformer un dict avec des règles simplesFiltre Jinja natif (map, selectattr, combine)
Vous voulez valider une donnéeansible.builtin.assert avec une expression Jinja

Un filter plugin ne récupère pas d’état distant et ne modifie pas le système — c’est uniquement de la transformation pure.

SymptômeCauseFix
Could not find or load filter pluginMauvais chemin (collection non installée, filter_plugins/ mal placé)ansible-galaxy collection list pour vérifier
AttributeError Python brutPas de isinstance() en gardeToujours type-checker + AnsibleFilterError
Filtre non rechargé après modifCache AnsibleVider ~/.ansible/tmp ou redémarrer le shell
Filtre marche en local mais pas en CIfilter_plugins/ racine non commitéMigrer en collection
  • FilterModule.filters() retourne un dict {nom: fonction} — c’est tout l’API.
  • AnsibleFilterError sur entrée invalide — jamais d’exception Python brute remontée.
  • Packager en collection (plugins/filter/) — pas en filter_plugins/ racine.
  • FQCN (namespace.collection.filtre) en 2026 — évite les collisions.
  • Testable : un playbook assert sur cas nominal + cas d’erreur tient en 30 lignes.
  • Périmètre : transformation de données pure. Pour exécuter sur la cible → module. Pour orchestrer côté control node → action plugin.

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracing. Aujourd'hui, ce site ne couvre même pas mes frais d'hébergement, d'électricité, de matériel, de logiciels, mais surtout de cafés.

Un soutien régulier, même symbolique, m'aide à garder ces ressources gratuites et à continuer de produire des guides de qualité. Merci pour votre appui.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn