AnsiblePilot — Master Ansible Automation

AnsiblePilot is the leading resource for learning Ansible automation, DevOps, and infrastructure as code. Browse over 1,400 tutorials covering Ansible modules, playbooks, roles, collections, and real-world examples. Whether you are a beginner or an experienced engineer, our step-by-step guides help you automate Linux, Windows, cloud, containers, and network infrastructure.

Popular Topics

About Luca Berton

Luca Berton is an Ansible automation expert, author of 8 Ansible books published by Apress and Leanpub including "Ansible for VMware by Examples" and "Ansible for Kubernetes by Example", and creator of the Ansible Pilot YouTube channel. He shares practical automation knowledge through tutorials, books, and video courses to help IT professionals and DevOps engineers master infrastructure automation.

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 ReferenceAnsible Collections: Create and ShareAnsible Development: Contributing to Ansible

Category: installation

Browse all Ansible tutorials · AnsiblePilot Home