Ansible Custom Modules: Write Your Own Module in Python (Complete Guide)
By Luca Berton · Published 2024-01-01 · Category: installation
How to create custom Ansible modules in Python. Write, test, and distribute your own modules. AnsibleModule class, argument_spec, return values, error handling.
Ansible Custom Modules: Write Your Own Module in Python (Complete Guide)
When built-in modules don't cover your use case, you can write custom Ansible modules in Python. Custom modules follow a simple pattern: accept parameters, perform actions, and return JSON results.
See also: Ansible troubleshooting - Error markupsafe
Minimal Module
#!/usr/bin/python
# library/hello.py
from ansible.module_utils.basic import AnsibleModule
def main():
module = AnsibleModule(
argument_spec=dict(
name=dict(type='str', required=True),
),
)
name = module.params['name']
module.exit_json(changed=False, msg=f"Hello, {name}!")
if __name__ == '__main__':
main()
# Use it in a playbook
- name: Test custom module
hello:
name: "World"
register: result
- ansible.builtin.debug:
var: result.msg
# Output: "Hello, World!"
Module Structure
my_project/
├── playbook.yml
├── library/ # Ansible auto-discovers modules here
│ └── my_module.py
└── module_utils/ # Shared Python code
└── my_helper.py
See also: Crafting and Publishing Your Custom Ansible Collection on Automation Hub
Complete Module Template
#!/usr/bin/python
# -*- coding: utf-8 -*-
DOCUMENTATION = r'''
---
module: manage_app_config
short_description: Manage application configuration files
description:
- Creates or updates application configuration files.
- Supports JSON and YAML formats.
version_added: "1.0.0"
author:
- Your Name (@github_handle)
options:
path:
description: Path to the configuration file.
type: str
required: true
settings:
description: Dictionary of settings to apply.
type: dict
required: true
format:
description: Configuration file format.
type: str
choices: ['json', 'yaml']
default: json
backup:
description: Create backup before modifying.
type: bool
default: false
'''
EXAMPLES = r'''
- name: Create JSON config
manage_app_config:
path: /etc/myapp/config.json
settings:
database_host: db.example.com
database_port: 5432
debug: false
- name: Update YAML config with backup
manage_app_config:
path: /etc/myapp/config.yml
settings:
workers: 8
format: yaml
backup: true
'''
RETURN = r'''
path:
description: Path to the configuration file.
type: str
returned: always
sample: /etc/myapp/config.json
changed_settings:
description: Settings that were modified.
type: dict
returned: changed
backup_file:
description: Path to backup file if created.
type: str
returned: when backup is true
'''
import json
import os
import shutil
from datetime import datetime
from ansible.module_utils.basic import AnsibleModule
try:
import yaml
HAS_YAML = True
except ImportError:
HAS_YAML = False
def read_config(path, fmt):
"""Read existing config file."""
if not os.path.exists(path):
return {}
with open(path, 'r') as f:
if fmt == 'yaml':
return yaml.safe_load(f) or {}
return json.load(f)
def write_config(path, data, fmt):
"""Write config file."""
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, 'w') as f:
if fmt == 'yaml':
yaml.dump(data, f, default_flow_style=False)
else:
json.dump(data, f, indent=2)
def main():
module = AnsibleModule(
argument_spec=dict(
path=dict(type='str', required=True),
settings=dict(type='dict', required=True),
format=dict(type='str', choices=['json', 'yaml'], default='json'),
backup=dict(type='bool', default=False),
),
supports_check_mode=True,
)
path = module.params['path']
settings = module.params['settings']
fmt = module.params['format']
backup = module.params['backup']
if fmt == 'yaml' and not HAS_YAML:
module.fail_json(msg="PyYAML is required for YAML format. Install with: pip install pyyaml")
result = dict(changed=False, path=path)
# Read current config
try:
current = read_config(path, fmt)
except Exception as e:
module.fail_json(msg=f"Failed to read {path}: {e}")
# Determine what changed
changed_settings = {}
for key, value in settings.items():
if current.get(key) != value:
changed_settings[key] = value
if not changed_settings:
module.exit_json(**result)
result['changed'] = True
result['changed_settings'] = changed_settings
if module.check_mode:
module.exit_json(**result)
# Backup if requested
if backup and os.path.exists(path):
backup_path = f"{path}.{datetime.now().strftime('%Y%m%d%H%M%S')}.bak"
shutil.copy2(path, backup_path)
result['backup_file'] = backup_path
# Apply changes
try:
current.update(settings)
write_config(path, current, fmt)
except Exception as e:
module.fail_json(msg=f"Failed to write {path}: {e}")
module.exit_json(**result)
if __name__ == '__main__':
main()
Key Concepts
argument_spec
argument_spec = dict(
# Required string
name=dict(type='str', required=True),
# Optional with default
state=dict(type='str', choices=['present', 'absent'], default='present'),
# Integer with validation
port=dict(type='int', default=8080),
# Boolean
enabled=dict(type='bool', default=True),
# List
tags=dict(type='list', elements='str', default=[]),
# Dictionary
config=dict(type='dict', default={}),
# Path (expanded automatically)
path=dict(type='path', required=True),
# No-log (hides from output)
password=dict(type='str', no_log=True),
)
Return Values
# Success
module.exit_json(
changed=True,
msg="Configuration updated",
data={"key": "value"},
)
# Failure
module.fail_json(
msg="Could not connect to database",
error_code=42,
)
Check Mode
module = AnsibleModule(
argument_spec=spec,
supports_check_mode=True,
)
if module.check_mode:
# Don't make changes, just report what would happen
module.exit_json(changed=True, msg="Would update config")
Mutually Exclusive Parameters
module = AnsibleModule(
argument_spec=spec,
mutually_exclusive=[
['source_file', 'source_url'],
],
required_one_of=[
['source_file', 'source_url'],
],
)
See also: Creating a Custom Ansible Lookup Plugin in Python for Reading a File
Testing Your Module
# Test directly with JSON args
python library/my_module.py <<< '{"ANSIBLE_MODULE_ARGS": {"name": "test"}}'
# Test in a playbook
ansible-playbook -i localhost, -c local test.yml
# Run ansible-test sanity checks
ansible-test sanity --test validate-modules library/my_module.py
Distribute via Collection
my_namespace/my_collection/
├── galaxy.yml
├── plugins/
│ └── modules/
│ └── my_module.py
└── tests/
# galaxy.yml
namespace: my_namespace
name: my_collection
version: 1.0.0
FAQ
What language are Ansible modules written in?
Most Ansible modules are written in Python, using the AnsibleModule class from ansible.module_utils.basic. Modules can also be written in any language that outputs JSON to stdout, but Python provides the richest framework.
Where do I put custom Ansible modules?
Place them in a library/ directory next to your playbook, in a role's library/ directory, or in a collection under plugins/modules/. Ansible discovers modules from these paths automatically.
How do I make a custom module support check mode?
Pass supports_check_mode=True to AnsibleModule(), then check module.check_mode before making changes. In check mode, report what would change without actually modifying anything.
Can I write Ansible modules in Bash or other languages?
Yes. Any executable that accepts JSON on stdin and outputs JSON to stdout works as an Ansible module. However, Python modules get the AnsibleModule helper class for argument parsing, check mode, and error handling.
How do I test custom Ansible modules?
Use python module.py <<< '{"ANSIBLE_MODULE_ARGS": {...}}' for quick tests, ansible-test sanity for code quality checks, and ansible-test units or molecule for integration testing.
Conclusion
Custom modules extend Ansible's capabilities for your specific automation needs. Follow the AnsibleModule pattern for argument parsing, check mode support, and proper error handling. Distribute modules via collections for team sharing.
Related Articles
• Ansible Modules: Complete Reference • Ansible Collections: Create and Share • Ansible Development: Contributing to AnsibleCategory: installation