Courseiva
EX294Chapter 17 of 18Objective 6.3

Custom Modules and Plugins

What do you do when Ansible's built-in modules don't have the exact action you need for a server? This is the moment you need to build your own tools. For the EX294 exam, knowing how to create and use custom modules and plugins separates someone who can only follow recipes from someone who can write new ones.

12 min read
Advanced
Updated Jul 23, 2026
Reviewed by Johnson Ajibi· Senior Network & Security Engineer · MSc IT Security

A simple way to picture Custom Modules and Plugins

The Modular Kitchen Appliance Analogy

Your kitchen, specifically the counter near the hob. You have a basic toaster that does one job: it turns bread into toast. That is like Ansible's built-in modules. But what if you want to make a particular type of Japanese omelette that requires a very specific, non-standard cooking plate? Your basic toaster cannot do it. So you go to a specialist kitchen tool shop and buy a custom attachment — a small, dedicated omelette plate that slots into your toaster's mechanism. That custom attachment is your custom module. It works with your toaster's existing power and control system, but it does a job the original designers never planned for.

Now, imagine you have a rule: whenever you finish cooking, you want the toaster to automatically tell your kitchen's central clock to update the timer for 'kitchen clean-up'. Your toaster doesn't have that feature built-in. So you write a small note and stick it on the toaster's side. That note doesn't change what the toaster does to bread. It only changes how the toaster interacts with the clock. That note is a plugin. It extends the ecosystem's behaviour without changing the core appliance. Modules give new cooking actions. Plugins give new communication rules. Both require you, the chef, to understand the basic recipe (your Ansible code) and the precise shape of the slot (the module/plugin interface). Without that understanding, your custom attachment won't fit, and your note will be ignored.

How It Actually Works

Ansible comes with a huge library of pre-built modules. Think of a module as a small, self-contained program that performs a specific task on a managed node (the server you are controlling). For example, the 'copy' module copies a file. The 'yum' module installs software. These are your default kitchen appliances. They cover most common jobs.

But real IT environments are unique. You might need to interact with a proprietary internal application that has a very specific API (Application Programming Interface — a set of rules that allows different software programs to communicate). Ansible doesn't ship with a module for that. Or you might need to read a specific log file format and make a decision based on its contents. The answer is to write your own custom module.

A custom module is a script, written in Python (the programming language Ansible uses), that follows a precise contract. Ansible runs this script on the managed node. The script must do three things: accept input, perform an action, and return JSON (JavaScript Object Notation — a structured text format for data) to Ansible that reports success, failure, or changed status. The script can be simple, like checking if a service is running, or complex, like orchestrating a multi-step database migration.

Here is a simplified example of what a custom module's output looks like:

{"changed": false, "msg": "The service was already running."}

This tells Ansible: 'I did my job, but nothing needed to change.' The key rule is that a module must be idempotent. Idempotent means you can run it a hundred times, and if the system is already in the desired state, it will do nothing and report 'changed: false'. This is crucial for Ansible's reliability.

Now, plugins are different. Plugins extend Ansible's own behaviour, not the behaviour of managed nodes. There are several types, but for EX294, you care most about 'filter' plugins and 'callback' plugins.

A filter plugin is like a custom function in a spreadsheet. It takes a piece of data (a variable) and transforms it. For example, the built-in 'to_nice_json' filter turns a variable into a human-readable JSON string. A custom filter plugin could do something like: take a server's hostname and extract the location code from the middle of it. You use filter plugins directly in your playbooks with the pipe '|' symbol.

A callback plugin changes what Ansible outputs to your screen or log file. By default, you see standard output. A custom callback plugin could make Ansible send a message to a Slack channel every time a playbook finishes, or log results to a database.

Why do these exist? Because no software vendor can predict every user's needs. Custom modules and plugins are the escape hatch. They let you adapt Ansible to your exact environment without waiting for a vendor update. The EX294 exam tests you on two things: recognising the basic framework (the directory structure and the required return format) and understanding the difference between a module and a plugin. You will not be asked to write a full custom module from scratch in the exam. You will be asked to identify the correct syntax, the required file location, and the minimum necessary code elements.

The standard structure for a custom module is a Python file placed in a 'library' directory alongside your playbook. For a plugin, it goes in a 'filter_plugins' or 'callback_plugins' directory. Ansible automatically looks for these directories when it runs. If you put your script in the right place, Ansible finds it and uses it as if it were built-in.

Understanding this concept replaces frustration with empowerment. Instead of hacking a workaround in a shell script, you write a clean, reusable, idempotent module that integrates perfectly with the rest of your Ansible automation.

Flowchart showing how a playbook discovers a custom module in the library directory, runs it on the managed node, checks state, and returns a JSON result.

Walk-Through

1

Identify the missing module

Look at the task you need to automate. If Ansible's built-in modules cannot do it (e.g., interacting with a custom internal API), you have found your need for a custom module. Write down exactly what the module must do, what input it needs, and what output 'changed' means.

2

Write the Python script

Create a Python file. It must import the 'AnsibleModule' class from 'ansible.module_utils.basic'. This gives you helper functions for handling input parameters and returning output. Define a 'main()' function that creates an instance of 'AnsibleModule', parses arguments, performs the task, and calls 'module.exit_json(changed=True)' or 'module.fail_json()'.

3

Place the module in the 'library' directory

Create a directory named 'library' in the same folder as your playbook. Move or copy the Python file there. The filename minus '.py' becomes the module name you will use in your playbook. For example, 'update_price.py' becomes the 'update_price' module.

4

Write a playbook to test the module

In your playbook, use the module exactly as you would a built-in one. Pass parameters under the module name. For example: 'update_price: product_id: 1234 new_price: 29.99'. Run the playbook with 'ansible-playbook site.yml' and check the output for errors.

5

Verify idempotency

Run the playbook a second time with the exact same parameters. The module should report 'changed: false' because the system is already in the desired state. If it reports 'changed: true' again, your module is not idempotent and needs to be fixed.

6

Create a filter plugin if needed

If you need to transform data (e.g., format a price with a currency symbol), create a file in a 'filter_plugins' directory. The file should define a filter function and register it in a 'class FilterModule' with a 'filters()' method that returns a dictionary mapping filter names to functions.

What This Looks Like on the Job

Imagine you work for a mid-sized e-commerce company. You manage hundreds of servers that run the company's custom-built inventory application. This application has a proprietary API for checking stock levels and updating prices. There is no Ansible module for it because it is unique to your company. Your daily task is to update product prices across thousands of items, but only during off-peak hours to avoid user disruption.

Here is what you do step by step:

First, you identify the precise action you need. You need to send an HTTP POST request (a specific type of web request that sends data to a server) to the inventory API with a product ID and a new price. You need the module to report whether the update succeeded, whether it failed because the product was not found, or whether no change was needed (price was already correct). This forms the specification for your custom module.

Next, you write the Python script. It must accept parameters like 'product_id' and 'new_price'. It uses Python's 'requests' library to call the API. It checks the API's response code. If the response is 200 and the price changed, it returns '{"changed": true}'. If the response is 200 and the price is the same, it returns '{"changed": false}'. If the response is 404 (not found), it returns '{"failed": true, "msg": "Product not found"}'. This strict return format is critical.

You save this script in a directory called 'library' in the same folder as your playbook. Your playbook's directory structure looks like this:

- site.yml (your main playbook) - library/ - update_price.py (your custom module) - filter_plugins/ - my_filters.py (if you need custom filters)

Now, in your playbook, you use the module exactly like any built-in one:

- name: Update price for product 1234 update_price: product_id: '1234' new_price: 29.99

When you run 'ansible-playbook site.yml', Ansible discovers the library directory, finds update_price.py, and executes it. It treats it as a first-class module.

Later, you might want a custom filter plugin to format prices with a currency symbol before they appear in a report. You write a filter plugin that takes a number and returns a string like '$29.99'. You place it in 'filter_plugins/my_filters.py', and in your playbook you use it as: '{{ price | format_currency }}'.

The advantage for you as an IT professional is immense. You are not limited by the tools someone else decided you should have. You build exactly what your unique environment needs. You also learn the fundamentals of how Ansible works internally, which helps you debug problems faster and write more reliable automation. You become the person on your team who can solve problems that the standard tools cannot touch.

How EX294 Actually Tests This

The EX294 exam tests your understanding of custom modules and plugins in a very specific, limited way. You will not be asked to write a complete custom module from memory. The exam is not a Python programming test. Instead, it tests your ability to recognise the correct structure and apply the core concepts.

Here are the exact topics you must master:

The location requirement: A custom module must be in a directory called 'library' relative to the playbook. A custom filter plugin must be in 'filter_plugins'. A custom callback plugin must be in 'callback_plugins'. The exam will give you a list of possible file paths and ask which one is valid. The trap is that other directory names like 'modules' or 'custom' are wrong.

The return format: A custom module must return a JSON dictionary with at least a 'changed' key (boolean) and optionally 'failed' (boolean) and 'msg' (string). The exam may show you a snippet of module output and ask if it is valid. If it is missing 'changed', it is invalid.

The idempotency requirement: The exam will describe a module that always performs its action (e.g., always restarts a service) regardless of current state. This is wrong. A module must check the current state and only act if needed. The question will ask you to choose the correct, idempotent version of a module's logic.

Module vs. plugin distinction: The exam loves to test this. A module runs on the managed node and changes something on that node. A plugin runs on the control node (the machine running Ansible) and changes how Ansible itself behaves. A filter plugin transforms data. A callback plugin changes output. A question might describe 'a piece of code that takes a variable and converts it to uppercase' — that is a filter plugin, not a module.

The 'args' parameter: When you call a custom module in a playbook, you pass arguments. The exam may test that arguments are passed as a dictionary under the module name, not as free-form variables.

Common traps in the exam include:

Confusing the 'library' directory with 'modules' directory. Only 'library' is correct.

Thinking a custom module can be written in any language. It must be Python (or another language Ansible can execute, but Python is the expected answer).

Believing that a module without a 'changed' key is acceptable. It is not.

Mixing up the directory for plugins vs. modules. Remember: modules go in 'library', plugins go in '[type]_plugins' (e.g., filter_plugins).

Memorise these key definitions:

Custom module: A Python script that runs on the managed node, changes system state, and returns JSON with 'changed'.

Filter plugin: Transforms data on the control node. Used with the pipe '|' operator.

Callback plugin: Changes output or behaviour on the control node.

Library directory: The directory where Ansible looks for modules. Must be an exact string 'library'.

Idempotency: Running the module multiple times produces the same end state without unintended side effects.

Key Takeaways

A custom module is a Python script that runs on the managed node and must return a JSON dictionary containing at least a 'changed' key.

The only valid directory name for placing custom modules alongside a playbook is 'library', not 'modules' or 'custom'.

A filter plugin transforms data on the control node and is used in playbooks with the pipe '|' operator, like '{{ var | my_filter }}'.

Idempotency is mandatory for custom modules: they must check the current state of the target before performing any action.

Plugins are distinguished from modules by their execution location: plugins run on the control node, while modules run on the managed node.

The EX294 exam tests your ability to recognise correct directory paths and module return formats, not your Python coding skills.

Easy to Mix Up

These come up on the exam all the time. Here's how to tell them apart.

Module

Runs on the managed node (target server)

Performs an action and changes system state

Must be placed in the 'library' directory

Plugin

Runs on the control node (where ansible is installed)

Transforms data (filter) or changes behaviour (callback)

Must be placed in a '[type]_plugins' directory (e.g., 'filter_plugins')

Custom Module

Written by the user as a Python script

Must be placed in a 'library' directory to be found

Requires understanding of Ansible's return format (JSON with 'changed')

Built-in Module

Provided with Ansible installation

Available system-wide without configuration

Has documentation and is known to work with any Ansible version

Filter Plugin

Transforms variable data (e.g., format text, extract substrings)

Used in playbooks with the pipe operator: '{{ var | filter_name }}'

Returns transformed data to the playbook for further use

Callback Plugin

Changes how Ansible outputs results or performs side actions

Not used directly in playbooks; configured in ansible.cfg

Can send notifications (e.g., to Slack) or log to a database

Watch Out for These

Mistake

I can write a custom module in any scripting language, like Bash or PowerShell, because Ansible just runs the script.

Correct

A custom module must be Python (or a compiled language with a Python wrapper) because Ansible uses a specific Python-based framework to pass arguments and expect return JSON in a strict format. Bash scripts cannot easily return the required JSON structure or handle Ansible's input handling.

People often come from a sysadmin background where they automate with Bash scripts. They assume Ansible can 'just run' any script. They do not realise the module interface demands a specific Python contract.

Mistake

A custom module is the same as a plugin, because both are ways to extend Ansible.

Correct

A module runs on the managed node and changes its state. A plugin runs on the control node and extends Ansible's own processing (e.g., transforming data with filter plugins or changing output with callback plugins). They have different code structures and different directory locations.

Both terms sound similar to a beginner: 'custom extension'. The conceptual difference (where the code runs) is not obvious until you understand Ansible's architecture.

Mistake

My custom module should always perform its action (e.g., always restart a service) because that is what the module is for.

Correct

A custom module must be idempotent. It should first check the current state of the target, and only perform the action if the state is not already the desired one. If the service is already running, it should report 'changed: false' and do nothing.

Many beginners think of modules as simple 'run this command' tools, similar to scripts. They do not understand idempotency as a core design principle of Ansible.

Mistake

If I put my custom module in a directory called 'my_modules' next to the playbook, Ansible will find it automatically.

Correct

The directory must be named exactly 'library' (lowercase, no variations). Ansible only searches for modules in the 'library' directory relative to the playbook, the roles, or the Ansible configuration. Any other name will be ignored.

Users assume Ansible is flexible about directory names because many other configuration tools are. But Ansible has very specific path conventions, and the exam tests this exact knowledge.

Mistake

A custom module only needs to print its result to standard output, and Ansible will understand it.

Correct

A custom module must output a valid JSON string to standard output with at least a 'changed' key. Ansible will fail if the output is not parseable JSON or if it contains extra text (e.g., debug print statements).

People are used to scripts that write human-readable messages. They do not realise Ansible's module contract expects machine-readable JSON output only.

Do You Actually Know This?

Reveal each answer, then mark whether you got it right. Score 60%+ to unlock the next chapter.

Frequently Asked Questions

Do I need to know Python to pass EX294 custom modules?

You need to understand the structure and return format of a Python custom module, but you will not be asked to write one from scratch. Focus on recognising valid directory paths, correct JSON output, and the idempotency concept.

What is the difference between a module and a plugin?

A module runs on the managed node and changes that node's state. A plugin runs on the control node and extends Ansible's own functionality, like transforming data (filter plugin) or changing output (callback plugin).

Where do I put my custom module file?

Place it in a directory named 'library' that is in the same folder as your playbook. Ansible automatically searches this directory for modules when you run the playbook.

What must a custom module return?

It must output a single JSON object to standard output (stdout) with at least a 'changed' key that is either 'true' or 'false'. Optionally, it can include 'failed' and 'msg' keys.

Can I use a custom module in a role?

Yes. If you place the Python file in the 'library' directory inside the role's directory tree (roles/your-role/library/), Ansible will find it when that role is used.

What does idempotent mean for a custom module?

It means the module checks the current state of the target system before acting. If the system is already in the desired state, the module does nothing and reports 'changed: false'. Running it multiple times gives the same result.

Terms Worth Knowing

Keep going

You've finished Custom Modules and Plugins. Continue through the EX294 study guide to build a complete picture of the exam.

Done with this chapter?