Custom Module

Custom Module #

Ansible menyediakan ratusan module bawaan yang menangani sebagian besar kebutuhan otomasi. Tapi ada situasi di mana tidak ada module yang cukup tepat: kamu perlu berinteraksi dengan API internal perusahaan, mengelola resource yang sangat spesifik, atau membungkus logika bisnis yang kompleks agar bisa digunakan di banyak playbook. Di sinilah custom module berperan — kamu bisa menulis module sendiri dengan Python dan menggunakannya persis seperti module bawaan Ansible.

Kapan Membuat Custom Module #

Sebelum menulis module, pertimbangkan alternatifnya:

Gunakan command/shell jika:
  ✓ Logika sederhana, satu atau dua perintah
  ✓ Tidak butuh idempotency yang kompleks
  ✓ Digunakan hanya di satu tempat

Gunakan script/executable jika:
  ✓ Logika cukup kompleks tapi tidak perlu reusable antar role
  ✓ Kamu nyaman dengan bahasa selain Python

Buat custom module jika:
  ✓ Perlu idempotency yang tepat (check mode, changed/ok)
  ✓ Akan digunakan di banyak role dan playbook
  ✓ Perlu integrasi yang baik dengan ekosistem Ansible (diff, check mode)
  ✓ Berinteraksi dengan API atau sistem yang tidak punya module

Anatomi Module Python #

Module Ansible adalah script Python biasa yang menerima argumen via stdin dan menulis JSON ke stdout:

#!/usr/bin/python
# -*- coding: utf-8 -*-

# library/my_module.py
# Module untuk mengelola konfigurasi aplikasi via REST API

DOCUMENTATION = r'''
---
module: my_module
short_description: Mengelola konfigurasi aplikasi via REST API
description:
  - Module ini membuat, mengupdate, atau menghapus konfigurasi aplikasi
    melalui REST API internal.
options:
  name:
    description: Nama konfigurasi
    required: true
    type: str
  value:
    description: Nilai konfigurasi
    required: false
    type: str
  state:
    description: State yang diinginkan
    choices: [present, absent]
    default: present
    type: str
  api_url:
    description: URL base API
    required: true
    type: str
  api_token:
    description: Token autentikasi API
    required: true
    type: str
    no_log: true
author:
  - Tim Infrastruktur
'''

EXAMPLES = r'''
- name: Set konfigurasi aplikasi
  my_module:
    name: "max_connections"
    value: "100"
    api_url: "https://api.internal.com"
    api_token: "{{ vault_api_token }}"
    state: present

- name: Hapus konfigurasi
  my_module:
    name: "deprecated_setting"
    api_url: "https://api.internal.com"
    api_token: "{{ vault_api_token }}"
    state: absent
'''

RETURN = r'''
config:
  description: Detail konfigurasi yang dibuat atau diupdate
  returned: when state is present and changed
  type: dict
  sample:
    name: max_connections
    value: "100"
    created_at: "2024-03-15T14:30:00Z"
'''

# Import Ansible helpers
from ansible.module_utils.basic import AnsibleModule
import json

try:
    import requests
    HAS_REQUESTS = True
except ImportError:
    HAS_REQUESTS = False


def get_config(api_url, api_token, name):
    """Ambil konfigurasi yang ada. Return None jika tidak ada."""
    headers = {"Authorization": f"Bearer {api_token}"}
    response = requests.get(
        f"{api_url}/api/config/{name}",
        headers=headers,
        timeout=10
    )
    if response.status_code == 404:
        return None
    response.raise_for_status()
    return response.json()


def create_or_update_config(api_url, api_token, name, value):
    """Buat atau update konfigurasi. Return (result, changed)."""
    headers = {
        "Authorization": f"Bearer {api_token}",
        "Content-Type": "application/json"
    }
    payload = {"name": name, "value": value}

    # Cek apakah sudah ada
    existing = get_config(api_url, api_token, name)

    if existing and existing.get("value") == value:
        # Tidak ada perubahan
        return existing, False

    if existing:
        # Update
        response = requests.put(
            f"{api_url}/api/config/{name}",
            headers=headers,
            json=payload,
            timeout=10
        )
    else:
        # Create
        response = requests.post(
            f"{api_url}/api/config",
            headers=headers,
            json=payload,
            timeout=10
        )

    response.raise_for_status()
    return response.json(), True


def delete_config(api_url, api_token, name):
    """Hapus konfigurasi. Return changed."""
    existing = get_config(api_url, api_token, name)
    if not existing:
        return False  # Sudah tidak ada, tidak ada perubahan

    headers = {"Authorization": f"Bearer {api_token}"}
    response = requests.delete(
        f"{api_url}/api/config/{name}",
        headers=headers,
        timeout=10
    )
    response.raise_for_status()
    return True


def main():
    # Definisikan argumen module
    module_args = dict(
        name=dict(type='str', required=True),
        value=dict(type='str', required=False),
        state=dict(type='str', default='present', choices=['present', 'absent']),
        api_url=dict(type='str', required=True),
        api_token=dict(type='str', required=True, no_log=True),
    )

    # Inisialisasi AnsibleModule
    module = AnsibleModule(
        argument_spec=module_args,
        supports_check_mode=True,  # Support --check mode
        required_if=[
            ('state', 'present', ['value']),  # value wajib jika state=present
        ]
    )

    # Validasi dependency
    if not HAS_REQUESTS:
        module.fail_json(msg="Library 'requests' diperlukan. Install dengan: pip install requests")

    # Ambil parameter
    name = module.params['name']
    value = module.params.get('value')
    state = module.params['state']
    api_url = module.params['api_url'].rstrip('/')
    api_token = module.params['api_token']

    result = dict(changed=False)

    try:
        if state == 'present':
            # Check mode: simulasikan perubahan tanpa benar-benar mengubah
            if module.check_mode:
                existing = get_config(api_url, api_token, name)
                result['changed'] = not existing or existing.get('value') != value
                module.exit_json(**result)

            config_result, changed = create_or_update_config(
                api_url, api_token, name, value
            )
            result['changed'] = changed
            result['config'] = config_result

        elif state == 'absent':
            if module.check_mode:
                existing = get_config(api_url, api_token, name)
                result['changed'] = existing is not None
                module.exit_json(**result)

            result['changed'] = delete_config(api_url, api_token, name)

    except requests.exceptions.ConnectionError as e:
        module.fail_json(msg=f"Tidak bisa terhubung ke API: {str(e)}")
    except requests.exceptions.HTTPError as e:
        module.fail_json(msg=f"API mengembalikan error: {e.response.status_code}{e.response.text}")
    except Exception as e:
        module.fail_json(msg=f"Error tidak terduga: {str(e)}")

    # Sukses
    module.exit_json(**result)


if __name__ == '__main__':
    main()

Struktur Direktori untuk Module Custom #

project/
  ├── library/               # Module custom untuk project ini
  │   └── my_module.py
  │
  ├── module_utils/          # Helper functions yang digunakan beberapa module
  │   └── api_helper.py
  │
  └── playbooks/
      └── site.yml

Untuk module yang akan digunakan di banyak project, simpan dalam role atau collection:

roles/my_role/
  └── library/
      └── my_module.py       # Tersedia hanya saat role ini digunakan

Menggunakan Module Custom #

- name: Konfigurasi aplikasi via API
  hosts: localhost
  tasks:
    - name: Set connection limit
      my_module:
        name: "max_connections"
        value: "{{ max_connections }}"
        api_url: "{{ app_api_url }}"
        api_token: "{{ vault_api_token }}"
        state: present
      no_log: true   # Jangan tampilkan api_token di output

    - name: Hapus konfigurasi deprecated
      my_module:
        name: "old_setting"
        api_url: "{{ app_api_url }}"
        api_token: "{{ vault_api_token }}"
        state: absent

Ringkasan #

  • Buat custom module saat butuh idempotency yang presisi, integrasi dengan API khusus, atau logika yang akan digunakan di banyak role — bukan untuk logika satu kali pakai.
  • Gunakan AnsibleModule helper dari ansible.module_utils.basic — ia menangani parsing argumen, check mode, dan format output JSON secara otomatis.
  • supports_check_mode=True dan module.check_mode wajib diimplementasikan — module yang tidak mendukung check mode membuat --check tidak berguna.
  • no_log=True untuk parameter sensitif seperti token dan password — mencegah nilai tersebut muncul di output atau log.
  • Simpan module di direktori library/ di root project, di dalam role, atau kemas sebagai Ansible Collection untuk distribusi yang lebih luas.
  • Tulis DOCUMENTATION, EXAMPLES, dan RETURN di modul — ini digunakan oleh ansible-doc dan membantu pengguna lain memahami cara menggunakan module.

← Sebelumnya: Best Practice   Berikutnya: Custom Plugin →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact