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
AnsibleModulehelper dariansible.module_utils.basic— ia menangani parsing argumen, check mode, dan format output JSON secara otomatis.supports_check_mode=Truedanmodule.check_modewajib diimplementasikan — module yang tidak mendukung check mode membuat--checktidak berguna.no_log=Trueuntuk 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, danRETURNdi modul — ini digunakan olehansible-docdan membantu pengguna lain memahami cara menggunakan module.