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.

Category: installation

Browse all Ansible tutorials · AnsiblePilot Home