
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.
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- Différencier un filter plugin custom des filtres Jinja natifs (
map,selectattr,regex_replace). - Écrire la classe
FilterModuleminimale qui expose une fonction Python. - Lever une
AnsibleFilterErrorpropre 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’enfilter_plugins/racine. - Tester localement avant de publier.
Prérequis
Section intitulée « Prérequis »- Avoir lu Développer un module Python — vous gagnerez en repères sur l’API Ansible côté Python.
- Connaître les filtres Jinja natifs — un filter plugin custom n’a d’intérêt que si aucun filtre natif ne couvre le besoin.
Filter plugin vs filtre Jinja natif
Section intitulée « Filter plugin vs filtre Jinja natif »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.
| Cas | Filtre natif suffisant | Filter plugin custom |
|---|---|---|
| Mettre en majuscules | upper | — |
| Sélectionner par attribut | selectattr('attr', 'equalto', 'x') | — |
| Convertir snake_case en camelCase | impossible en pur Jinja | filter plugin |
| Calculer un hash métier (ex. format CRC custom) | impossible | filter plugin |
| Transformer JSON Cloud → format interne | gymnastique Jinja illisible | filter 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.
Squelette minimal d’un filter plugin
Section intitulée « Squelette minimal d’un filter plugin »Tout filter plugin Ansible comprend deux éléments :
- Une fonction Python qui prend la valeur en entrée, applique la transformation, retourne le résultat.
- Une classe
FilterModulequi expose la fonction sous un nom utilisable dans un playbook via la méthodefilters().
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.
Lever une AnsibleFilterError propre
Section intitulée « Lever une AnsibleFilterError propre »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 intAnsibleFilterError est la classe d’exception idiomatique pour les filtres — préférée à AnsibleError (réservée aux modules) ou à un raise Python brut.
Passer des arguments supplémentaires
Section intitulée « Passer des arguments supplémentaires »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) }}" # → UserNameLes arguments positionnels et nommés fonctionnent comme en Python standard.
Packager dans une collection (la voie 2026)
Section intitulée « Packager dans une collection (la voie 2026) »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.pyLe galaxy.yml minimal :
namespace: mon_orgname: utilsversion: 1.0.0description: Filtres custom pour le projetauthors: - Stéphane RobertBuild et installation locale pour tester :
ansible-galaxy collection build .ansible-galaxy collection install mon_org-utils-1.0.0.tar.gz --forceUsage 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.
Tester localement avant de publier
Section intitulée « Tester localement avant de publier »Une approche pragmatique : un fichier de test isolé qui couvre les cas nominaux et les cas d’erreur.
---- 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 :
ansible-playbook test-filtres.ymlPour 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ôme | Bonne piste plutôt qu’un filtre |
|---|---|
| Vous voulez exécuter une commande sur la cible | Module (cf. Modules Python) |
| Vous voulez orchestrer un appel API côté control node | Action plugin (cf. Action plugins) |
| Vous voulez transformer un dict avec des règles simples | Filtre Jinja natif (map, selectattr, combine) |
| Vous voulez valider une donnée | ansible.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.
Pièges courants
Section intitulée « Pièges courants »| Symptôme | Cause | Fix |
|---|---|---|
Could not find or load filter plugin | Mauvais chemin (collection non installée, filter_plugins/ mal placé) | ansible-galaxy collection list pour vérifier |
AttributeError Python brut | Pas de isinstance() en garde | Toujours type-checker + AnsibleFilterError |
| Filtre non rechargé après modif | Cache Ansible | Vider ~/.ansible/tmp ou redémarrer le shell |
| Filtre marche en local mais pas en CI | filter_plugins/ racine non commité | Migrer en collection |
À retenir
Section intitulée « À retenir »FilterModule.filters()retourne un dict{nom: fonction}— c’est tout l’API.AnsibleFilterErrorsur entrée invalide — jamais d’exception Python brute remontée.- Packager en collection (
plugins/filter/) — pas enfilter_plugins/racine. - FQCN (
namespace.collection.filtre) en 2026 — évite les collisions. - Testable : un playbook
assertsur 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.