
Un action plugin Ansible s’exécute côté control node, pas sur la cible. C’est la différence fondamentale avec un module Python classique : un module est transféré et exécuté sur le managed node ; un action plugin tourne localement sur le poste qui lance Ansible, avec accès aux variables, aux facts collectés, et au moteur de template. C’est l’outil idéal pour orchestrer plusieurs modules, pré-calculer des données complexes avant de les pousser, ou fusionner des fichiers de configuration sans impliquer la cible. Cette page couvre les cas d’usage légitimes, la structure d’un action plugin, et la frontière avec les modules classiques.
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- Distinguer un module classique (exécuté sur la cible) d’un action plugin (exécuté sur le control node).
- Identifier les 3 cas d’usage légitimes d’un action plugin et ceux où un filtre / module suffit.
- Implémenter un action plugin minimal avec
ActionBaseet_VALID_ARGS. - Déléguer à un module classique depuis l’action plugin (pattern « action wrapper »).
- Tester un action plugin avec
ansible-test sanity --docker.
Prérequis
Section intitulée « Prérequis »- Python maîtrisé : classes, héritage, gestion d’exceptions, manipulation de dicts. Voir doc Python.
- Avoir développé un module classique pour comprendre la frontière.
- Connaître la structure d’une collection.
Module vs action plugin — où s’exécute le code ?
Section intitulée « Module vs action plugin — où s’exécute le code ? »Pour clarifier la différence : quand vous écrivez - ansible.builtin.copy: ..., ce qui se passe vraiment dépend du type de plugin :
┌─────────────────┐ ┌──────────────────┐│ Control node │ │ Managed node ││ (votre poste) │ ─── SSH ────► │ (web1.lab) │└─────────────────┘ └──────────────────┘
Module classique : 1. Le control node prépare les arguments + le module Python 2. Transfert via SSH (ou Podman si EE) 3. Exécution Python SUR LA CIBLE 4. Retour du résultat
Action plugin : 1. Le code tourne SUR LE CONTROL NODE (votre poste) 2. Accès à task_vars, facts collectés, secrets déchiffrés 3. Peut appeler des modules classiques DE-PUIS le control node 4. Retour directConséquences pratiques :
| Aspect | Module | Action plugin |
|---|---|---|
| Prérequis Python sur la cible | Oui | Non (sauf si délègue à un module) |
Accès à ansible_facts.* | Oui (transmis par Ansible) | Oui (directement via task_vars) |
| Accès aux variables Vault | Oui (déchiffrées avant transfert) | Oui (en mémoire) |
| Performance sur grand parc | Limitée par SSH × N hôtes | Locale, peut paralléliser librement |
| Accès aux fichiers du projet (templates, files) | Non | Oui |
Les 3 cas d’usage légitimes
Section intitulée « Les 3 cas d’usage légitimes »Cas 1 — Pré-traitement avant déploiement
Section intitulée « Cas 1 — Pré-traitement avant déploiement »Vous avez N fichiers de configuration partiels qu’il faut fusionner et templater avant de pousser sur la cible :
# Sans action plugin : 3 tâches successives- ansible.builtin.template: src: base.conf.j2 dest: /tmp/part1.conf- ansible.builtin.template: src: prod.conf.j2 dest: /tmp/part2.conf- ansible.builtin.assemble: # module assemble côté cible src: /tmp/ dest: /etc/myapp/full.conf
# Avec action plugin : 1 tâche, fusion sur control node- mycollection.merge_configs: sources: - templates/base.conf.j2 - templates/prod.conf.j2 dest: /etc/myapp/full.confLe pré-traitement côté control node évite 3 transferts SSH et la création de fichiers temporaires sur la cible.
Cas 2 — Wrapper qui ajoute de la magie
Section intitulée « Cas 2 — Wrapper qui ajoute de la magie »Vous voulez un module mycollection.copy_with_audit qui délègue à ansible.builtin.copy mais ajoute un log d’audit dans une BDD locale au control node :
class ActionModule(ActionBase): _VALID_ARGS = frozenset(('src', 'dest', 'audit_user'))
def run(self, tmp=None, task_vars=None): result = super().run(tmp, task_vars) del tmp
# Déléguer à ansible.builtin.copy copy_result = self._execute_module( module_name='ansible.builtin.copy', module_args={ 'src': self._task.args.get('src'), 'dest': self._task.args.get('dest'), }, task_vars=task_vars, )
# Ajouter notre logique : auditer en local if copy_result.get('changed'): self._log_audit( user=self._task.args.get('audit_user'), target=self._task.args.get('dest'), host=task_vars['inventory_hostname'], )
result.update(copy_result) return resultL’utilisateur du plugin écrit mycollection.copy_with_audit: exactement comme copy:, mais bénéficie de l’audit transparent.
Cas 3 — Orchestration multi-modules
Section intitulée « Cas 3 — Orchestration multi-modules »Un action plugin peut appeler plusieurs modules dans un ordre conditionnel — utile pour des opérations atomiques où le rollback est intégré :
def run(self, tmp=None, task_vars=None): result = super().run(tmp, task_vars)
# 1. Snapshot snap = self._execute_module(module_name='community.general.snapshot', ...) if snap.get('failed'): result['failed'] = True return result
# 2. Migration migrate = self._execute_module(module_name='ansible.builtin.command', ...) if migrate.get('failed'): # Rollback automatique self._execute_module(module_name='community.general.snapshot_restore', ...) result['failed'] = True result['msg'] = "Migration échouée, rollback appliqué" return result
result['changed'] = True return resultPattern utile pour les migrations BDD ou les opérations critiques qui demandent un rollback intégré.
Ce qui ne justifie pas un action plugin
Section intitulée « Ce qui ne justifie pas un action plugin »| Vous pensez avoir besoin… | Préférer |
|---|---|
| Manipuler une string/liste/dict | Filtre Jinja custom (plus léger) |
| Faire une boucle complexe | loop: + selectattr + map |
| Capturer une valeur et la réutiliser | set_fact: + register: |
| Conditionner l’exécution selon un fact | when: |
Un action plugin ajoute de la complexité (Python, packaging dans collection, tests). Le réserver aux vraies orchestrations multi-étapes ou aux cas où le pré-traitement local apporte un gain net.
Structure d’un projet
Section intitulée « Structure d’un projet »mycollection/├── galaxy.yml├── plugins/│ ├── action/│ │ └── myaction.py # le code Python du plugin│ └── modules/│ └── myaction.py # facultatif — un module compagnon├── meta/│ └── runtime.yml└── README.mdPattern courant : un fichier dans plugins/action/<nom>.py (logique côté control node) et un fichier dans plugins/modules/<nom>.py (module compagnon avec DOCUMENTATION, EXAMPLES, RETURN). Quand l’utilisateur appelle mycollection.<nom>:, Ansible exécute le action/ ; le modules/ sert pour la doc et le fallback si _execute_module est utilisé.
Squelette d’un action plugin moderne (2026)
Section intitulée « Squelette d’un action plugin moderne (2026) »#!/usr/bin/pythonfrom ansible.errors import AnsibleActionFailfrom ansible.plugins.action import ActionBasefrom ansible.utils.display import Display
display = Display()
class ActionModule(ActionBase): """Action plugin qui fusionne plusieurs templates en un seul fichier."""
TRANSFERS_FILES = True _VALID_ARGS = frozenset(('sources', 'dest', 'mode', 'owner', 'group'))
def run(self, tmp=None, task_vars=None): result = super().run(tmp, task_vars) del tmp
# 1. Validation des arguments sources = self._task.args.get('sources') dest = self._task.args.get('dest') if not sources or not dest: raise AnsibleActionFail("'sources' et 'dest' sont obligatoires")
# 2. Templating local de chaque source merged = [] for src in sources: content = self._loader.get_real_file(src) templated = self._templar.template(open(content).read()) merged.append(templated)
# 3. Écriture d'un fichier temporaire local merged_file = '/tmp/merged_{}.conf'.format(task_vars['inventory_hostname']) with open(merged_file, 'w') as f: f.write('\n'.join(merged))
# 4. Délégation à copy: pour transférer sur la cible copy_args = { 'src': merged_file, 'dest': dest, } for opt in ('mode', 'owner', 'group'): if self._task.args.get(opt): copy_args[opt] = self._task.args[opt]
copy_result = self._execute_module( module_name='ansible.builtin.copy', module_args=copy_args, task_vars=task_vars, )
# 5. Nettoyage du fichier temporaire local import os os.remove(merged_file)
result.update(copy_result) return resultPoints-clés :
_VALID_ARGSvalide les arguments fournis — typo détecté tôt.AnsibleActionFailpour échouer proprement avec un message clair (préféré àresult['failed'] = True).self._templar.template()pour interpoler les variables Jinja dans une string.self._execute_modulepour déléguer à un module classique depuis l’action plugin.- Nettoyage explicite du fichier temporaire local (sinon il s’accumule).
Comme pour les modules, ansible-test sanity --docker default valide :
- La conformité PEP8.
- Les imports.
- L’absence d’erreurs lors du chargement.
cd mycollection/ansible-test sanity --docker defaultansible-test units --docker default plugins/action/Pour des tests fonctionnels, écrire un playbook de test dans tests/ qui appelle l’action sur localhost.
À retenir
Section intitulée « À retenir »- Action plugin = code côté control node, pas sur la cible. Différent du module classique qui s’exécute distant.
- 3 cas légitimes : pré-traitement avant déploiement, wrapper avec auto-magic, orchestration multi-modules.
- Pas de surcouche sur ce qu’un filtre Jinja, un
loop:ou unset_fact:peut faire. _VALID_ARGS(Ansible 2.13+) +AnsibleActionFailpour validation et erreurs propres.self._execute_modulepour déléguer à un module classique depuis le plugin.- Packager dans une collection dès le départ —
plugins/action/est le dossier standard.