Ansible: Modules and Action Plugins
The Problem
After some time spent writing Ansible playbooks, one often discovers repetitive patterns of actions that she would like to extract in a single function in order to stay DRY and increase readability.
For instance, my team has a frequent need to backup files and directories before applying any modifications to some machines. Those machines are legacy ones that have been setup a long time ago and cannot be treated as phoenix servers. We decided nonetheless to code some operations for them with Ansible, but we take extra care to ensure that every error is detected as soon as it occurs and that the modification is immediately rolled back. And in any case, all sensitive bits of these machines are backed up before touching them.
That last part translates to the following sequence of actions (which translates into 6 Ansible actions):
- feed a timestamp variable
- if the path to backup is a folder, make a tarsal
- copy the file to backup to
/.bckp.
- create or replace a symbolic link from the parent directory of the original file to the latest backup (to advertise the backup and make it easy to restore it)
After some playbooks had been written that way, my team got tired of those verbose sequences of actions, and we searched for a way to write them in a simpler and readable way. The natural solution seemed to write our own module.
Modules
Modules are single scripts that will be deployed on target hosts. Custom modules must be placed in some folder present in the ANSIBLE_LIBRARY
path variable, or alongside playbook under ./library
. Module files must be executable and they must be given the name one want to use to call them (thus, without extension). They are then used in the same way as standard modules:
Here is the structure of a simple backup module (./library/backup
), using Ansible Python API:
The previous module can be used as follows:
That’s already a big improvement:
- We reduced 6 actions into one.
- The result is far more readable.
But:
- One still have to specify the backup directory for each execution of the module.
- We would like to share the same timestamp for all executions of the module within the same playbook run.
A solution would be to define the backup directory as a variable, and to have the module read this variable. Similarly, the module could write the generated timestamp into a variable that it would read upon further executions.
Unfortunately, modules can’t read or write variables. All they can access is the facts of the target machine they are executed on. That led us to action plugins.
Action Plugins
As explained on Ansible’s Google Group: “action_plugins are a special type of module, or a compliment to existing modules. action_plugins get run on the ‘master’ instead of on the target, for modules like file/copy/template, some of the work needs to be done on the master before it executes things on the target. The action plugin executes first and can then execute (or not) the normal module”.
For instance, the copy action plugin calls (_execute_module
) the copy module.
Custom action plugins must be placed under the configured action_plugins
path, or alongside playbooks under ./action_plugins
.
Then they can be called as follows:
Or, if a file with the same name — even an empty one — is placed in the module path (see previous section) they can be called as a module:
As said above, action_plugins are executed on the “master”, so they are part of the playbook run and can see existing variables or create new variables:
So now we have a way to work on the target machine but without seeing variables, and a way to see variables but on the orchestration machine only. To solve our problem, we will have to combine those two.
A Solution Tying Them Together
Here is the trick: as quoted in the previous section, an action plugin and a module can have the same name, in which case the action plugin will be executed rather than the plugin, but since an action plugin can execute a module, we can write an action plugin that will call its module counterpart.
Here is a working example.
In the module (./library/backup
), all parameters are required:
But the action plugin (./action_plugins/backup.py
) will attempt to provide default values from variables:
This implementation allows for the following use cases:
The complete implementation can be found on Github.
blog comments powered by Disqus