Pada episode ini kita akan belajar memperluas Ansible dengan Python: membuat custom filter plugin untuk manipulasi data di Jinja2, membuat custom module dengan library AnsibleModule, dan memahami kontrak module dengan Ansible.

Setelah di episode 16 sebelumnya kita membahas asynchronous actions & polling, kalian sudah tahu cara menangani task panjang dengan elegan. Tapi ada satu tema yang selama ini diam-diam kita andalkan terus-menerus: semua yang kalian gunakan di Ansible — modul seperti ansible.builtin.copy, apt, filter seperti regex_replace, combine — adalah kode Python. Episode-episode sebelumnya adalah tentang mengonsumsi apa yang tersedia. Di episode 17 ini, giliran kita untuk memproduksi.
Ada kalanya modul bawaan tidak cukup. Bayangkan kalian bekerja dengan sebuah aplikasi internal yang punya format konfigurasi proprietary, atau sebuah API internal yang tidak punya modul Ansible khusus. Atau bayangkan kalian butuh mengubah data dengan cara yang tidak ada filter bawaannya — misalnya memformat ukuran byte menjadi "1.2 GB" yang bisa dibaca manusia, dan dipakai di 20 template yang berbeda. Menulis ulang logika yang sama berulang kali bukanlah solusi; itulah saatnya menulis ekstensi Python.
Ansible menyediakan dua "permukaan ekstensi" utama yang akan kita bahas:
Memahami perbedaan lokasi eksekusi ini sangat penting. Filter dieksekusi di tempat templating terjadi (control node), sedangkan modul dieksekusi di tempat aksi terjadi (managed node). Di episode inilah kemampuan programming Python kalian mulai benar-benar bersinar — dan ini juga gerbang menuju episode berikutnya tentang testing kode Ansible.
Sebelum menulis kode sendiri, tanyakan dulu: apakah ini benar-benar masalah yang tidak bisa diselesaikan modul bawaan? Menulis ekstensi punya biaya perawatan sendiri, jadi kita harus selektif. Beberapa indikator yang valid:
ansible.builtin.setup, misalnya hasil query ke database lokal atau status aplikasi khusus.when + facts.Sebaliknya, jangan menulis custom module untuk sesuatu yang sudah ada. Jika kalian mendapati diri ingin membuat modul "restart nginx" atau "install paket", berhenti — itu sudah ada. Aturan praktis: gunakan built-in dulu, ekstensi hanya untuk yang benar-benar tidak tersedia.
Perbedaan mendasar kedua jenis ekstensi:
| Aspek | Custom Filter Plugin | Custom Module |
|---|---|---|
| Lokasi eksekusi | Control node | Managed node |
| Berinteraksi dengan sistem remote? | Tidak | Ya |
| Dipanggil dari | Template/ekspresi: {{ x | my_filter }} | Task playbook |
| Output | Nilai terproses | JSON (dengan changed, ansible_facts, dll.) |
| Direktori default | filter_plugins/ (proyek) atau filters/ (role) | library/ |
| Kompleksitas | Rendah–sedang | Sedang–tinggi |
Filter plugin adalah cara termurah dan paling sering dipakai untuk memperluas Ansible. Sebuah filter hanyalah fungsi Python biasa yang menerima nilai sebagai argumen pertama (dan argumen ekstra opsional), lalu mengembalikan nilai hasil pemrosesan. Satu-satunya syarat "struktural" adalah mendefinisikan kelas FilterModule dengan method filters() yang mengembalikan dictionary berisi pemetaan nama filter → fungsi.
Mari kita buat dua filter yang sering dibutuhkan di dunia nyata: human_size (memformat byte menjadi "1.5 GB") dan mask_ip (menyembunyikan sebagian IP untuk keperluan logging/monitoring):
# filter_plugins/my_filters.py
# Custom filter plugin Ansible yang berjalan di control node.
def human_size(num_bytes):
"""Mengubah jumlah byte menjadi string yang mudah dibaca manusia."""
units = ["B", "KB", "MB", "GB", "TB", "PB"]
value = float(num_bytes)
for unit in units:
if value < 1024 or unit == units[-1]:
return f"{value:.1f} {unit}"
value /= 1024
def mask_ip(address):
"""Menyembunyikan dua oktet terakhir dari sebuah IPv4."""
parts = address.split(".")
if len(parts) != 4:
return address
return ".".join(parts[:2] + ["*", "*"])
class FilterModule:
"""Daftar filter yang teregistrasi ke Ansible."""
def filters(self):
return {
"human_size": human_size,
"mask_ip": mask_ip,
}Ada beberapa hal penting di sini:
{{ x | my_filter(2) }}, argumen tersebut diteruskan sebagai argumen kedua fungsi.FilterModule dan method filters() adalah kontrak wajib. Tanpa keduanya, Ansible tidak akan menemukan filter kalian.Sekarang gunakan filter tersebut di playbook dan template. Contoh di playbook:
---
- name: Gunakan custom filter plugin
hosts: webservers
tasks:
- name: Format ukuran file konfigurasi
ansible.builtin.debug:
msg: "Ukuran konfigurasi = {{ 153391689 | human_size }}"
- name: Masking IP untuk log
ansible.builtin.debug:
msg: "IP server dilaporkan sebagai {{ '103.42.77.19' | mask_ip }}"Hasil eksekusinya:
TASK [Format ukuran file konfigurasi] **************************************
ok: [web01] => {
"msg": "Ukuran konfigurasi = 146.3 MB"
}
TASK [Masking IP untuk log] ***********************************************
ok: [web01] => {
"msg": "IP server dilaporkan sebagai 103.42.*.*"
}Filter juga bisa dipakai di dalam template .j2 yang kita pelajari di episode 8, atau digabung dengan variabel dan facts — misalnya {{ ansible_facts['memtotal_mb'] | human_size }}:
Server: {{ ansible_facts['hostname'] }}
Memory: {{ ansible_facts['memtotal_mb'] * 1024 * 1024 | human_size }}
Client IP (masked): {{ ansible_facts['default_ipv4']['address'] | mask_ip }}Tip
Nama filter tidak boleh bentrok dengan filter bawaan Ansible/Jinja2 (seperti default, lower, map). Jika kalian menimpa nama bawaan, perilakunya bisa berubah di semua template proyek — bug yang sangat sulit dilacak. Gunakan prefiks nama proyek (misalnya myco_*).
filter_plugins/ vs library/Letak ekstensi menentukan di mana Ansible mencarinya. Berikut struktur yang benar di level proyek:
ansible-extension/
├── ansible.cfg
├── inventory.yml
├── filter_plugins/
│ └── my_filters.py
├── library/
│ └── fs_usage.py
└── playbook-extension.ymlDua perbedaan yang harus kalian hafal:
filter_plugins/, sedangkan module di library/. Keduanya relatif terhadap direktori playbook.filters/ (bukan filter_plugins/!), sedangkan module tetap di library/. Meletakkan filter di roles/<role>/filter_plugins/ adalah kesalahan klasik yang membuat filter "tidak ketemu".Untuk module, Ansible mencari di beberapa lokasi. Kalian juga bisa menentukan jalur tambahan lewat ansible.cfg:
[defaults]
library = ./library:./custom_modulesAnsibleModuleSekarang bagian yang lebih serius: custom module. Sebuah module adalah program Python mandiri yang dijalankan di managed node. Ia menerima argumen dari task (dalam bentuk JSON), melakukan pekerjaannya, lalu mencetak tepat satu dokumen JSON ke stdout dan keluar dengan exit code tertentu. Di atas itu, module memakai kelas AnsibleModule yang disediakan oleh ansible.module_utils.basic — saat runtime, ansible-core membungkus module kita dengan "AnsiballZ wrapper" yang menyuntikkan kode AnsibleModule ini, sehingga kita tidak perlu menginstal apa pun di managed node.
Contoh module yang berguna dan realistis: fs_usage — menghitung persentase penggunaan disk pada sebuah path, mengembalikannya sebagai facts, dan gagal dengan pesan yang jelas jika melebihi ambang batas:
#!/usr/bin/python
# library/fs_usage.py
"""Module kustom untuk memeriksa penggunaan disk pada sebuah path."""
from __future__ import absolute_import, division, print_function
__metaclass__ = type
import shutil
from ansible.module_utils.basic import AnsibleModule
DOCUMENTATION = r"""
---
module: fs_usage
short_description: Memeriksa penggunaan disk dan gagal jika melebihi threshold
description:
- Menghitung persentase penggunaan disk pada path tertentu.
- Mengembalikan hasilnya sebagai ansible_facts (fs_usage).
- Gagal (fail_json) jika penggunaan melebihi threshold yang ditentukan.
options:
path:
description: Path direktori yang akan diperiksa.
required: true
type: str
threshold:
description: Persentase penggunaan yang dianggap kritis (0-100).
required: false
type: int
default: 80
"""
EXAMPLES = r"""
- name: Cek penggunaan disk /var
fs_usage:
path: /var
threshold: 85
"""
def main():
module = AnsibleModule(
argument_spec=dict(
path=dict(type="str", required=True),
threshold=dict(type="int", default=80),
),
supports_check_mode=False,
)
path = module.params["path"]
threshold = module.params["threshold"]
usage = shutil.disk_usage(path)
used_percent = (usage.used / usage.total) * 100
result = dict(
path=path,
total=usage.total,
used=usage.used,
free=usage.free,
used_percent=round(used_percent, 1),
threshold=threshold,
)
if used_percent >= threshold:
module.fail_json(
msg="Penggunaan disk melebihi threshold!",
**result,
)
module.exit_json(
changed=False,
ansible_facts={"fs_usage": result},
)
if __name__ == "__main__":
main()Mari kita bedah setiap bagian penting:
argument_spec: deklarasi argumen yang diterima modul. path bertipe str dan wajib ada (required=True); threshold bertipe int dengan default 80. Ansible otomatis memvalidasi tipe dan keberadaan argumen — jika task memanggil tanpa path, modul akan menolak sebelum main() sempat berjalan. Untuk nilai boolean, ingat: gunakan type="bool", bukan string "true"/"false".module.params: dictionary berisi nilai argumen yang sudah tervalidasi.module.exit_json(...): sinyal sukses. Modul keluar dengan kode 0 dan JSON hasil dicetak ke stdout. Menaruh hasil di ansible_facts membuatnya tersedia sebagai variabel fs_usage untuk task berikutnya.module.fail_json(...): sinyal gagal. Modul keluar dengan kode non-nol, playbook melaporkan FAILED, dan pesan msg tampil di output.Kontrak module yang wajib dipatuhi setiap modul kustom:
print() debugging. Ansible men-parse stdout sebagai JSON; teks asing akan menghasilkan error MODULE FAILURE yang membingungkan.0 untuk sukses, non-0 untuk gagal. Jangan pernah sys.exit(0) pada kegagalan — itu akan membuat playbook mengira task sukses.DOCUMENTATION (dan idealnya EXAMPLES, RETURN) membuat modul terbaca oleh ansible-doc dan terbaca manusia. (Catatan: ANSIBLE_METADATA dulu wajib, tapi sudah dihapus dari ansible-core modern.)AnsibleModule harus tersedia di managed node, karena modul dieksekusi di sana. Jika kalian mengimpor library eksternal, pastikan sudah diinstal di server target — inilah bedanya dengan filter yang berjalan di control node.Warning
Jangan pernah menggunakan print() untuk debugging di dalam modul. Semua output ke stdout dianggap sebagai hasil JSON modul. Untuk debugging, gunakan module.log(...) atau tulis ke module.debug / file log di remote.
Karena module ini berada di library/ yang sejajar dengan playbook, Ansible akan menemukannya otomatis dan kita bisa memakainya seperti modul biasa — cukup dengan nama singkatnya fs_usage:
---
- name: Gunakan custom module fs_usage
hosts: all
become: true
tasks:
- name: Cek penggunaan disk /var
fs_usage:
path: /var
threshold: 85
register: disk_status
- name: Tampilkan hasil jika sehat
ansible.builtin.debug:
msg: "Disk /var terpakai {{ disk_status.fs_usage.used_percent }}%"
rescue:
- name: Beri peringatan jika disk melebihi threshold
ansible.builtin.debug:
msg: "DISK KRITIS: {{ ansible_failed_result.msg }} - path {{ ansible_failed_result.path }}"Jalankan playbooknya:
ansible-playbook -i inventory.yml playbook-use-module.ymlOutputnya — perhatikan bagaimana ansible_facts dari modul muncul sebagai fs_usage dan bisa diakses langsung:
TASK [Cek penggunaan disk /var] ********************************************
ok: [web01]
TASK [Tampilkan hasil jika sehat] ******************************************
ok: [web01] => {
"msg": "Disk /var terpakai 41.7%"
}
PLAY RECAP *****************************************************************
web01 : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0Module kustom juga bisa diuji langsung dengan ad-hoc command:
ansible all -m fs_usage -a "path=/tmp threshold=95"Note
Gabungkan custom module dengan pola error handling dari episode 10: blok block/rescue sangat cocok dipakai untuk menangkap fail_json dari modul kustom — seperti contoh playbook di atas yang menangani status disk kritis.
Berikut rangkuman jebakan yang paling sering menghantui developer yang baru menulis ekstensi Ansible:
1. Modul mencetak output non-JSON ke stdout. print(), pesan banner, atau logging ke console akan merusak parsing JSON → error MODULE FAILURE. Gunakan module.log() untuk logging.
2. Lupa kelas FilterModule. Filter Python tanpa kelas FilterModule dengan method filters() tidak akan pernah terdaftar. Ansible diam-diam mengabaikannya.
3. Filter/role ditempatkan di direktori salah. Di dalam role, filter harus di filters/, bukan filter_plugins/. Module role ada di library/, sama seperti proyek.
4. Modul tidak ditemukan ("couldn't resolve module"). Pastikan module ada di direktori yang dilalui Ansible (./library, ansible.cfg library =, atau ANSIBLE_LIBRARY env var), dan periksa dengan ansible-doc -l | grep fs_usage atau ansible <host> -m fs_usage untuk menguji.
5. Kesalahan tipe di argument_spec. required=True tanpa argumen → error validasi sebelum eksekusi. Boolean harus type="bool". Integer harus type="int", bukan string.
6. Exit code salah. sys.exit(0) pada kegagalan membuat Ansible mengira task sukses. Selalu module.fail_json() untuk kegagalan.
7. Import yang tidak ada di managed node. Module dieksekusi di remote; jika modul mengimpor pustaka yang tidak terinstal di sana, eksekusi gagal. Filter (control node) tidak punya masalah ini.
8. Menulis ulang yang sudah ada. Mengecek koleksi bawaan dan community (ingat episode 13: ansible-galaxy collection search) sebelum menulis custom module menghemat banyak waktu.
Pada episode 17 ini, kita telah membuka "mesin" Ansible dan melihat bahwa ekstensibilitasnya adalah Python murni. Kalian belajar mengenali kapan modul bawaan sudah tidak cukup, membuat custom filter plugin sederhana (human_size, mask_ip) yang berjalan di control node, memahami struktur direktori filter_plugins/ dan library/ (beserta varian filters/ di dalam role), dan membuat custom module fs_usage menggunakan library AnsibleModule — lengkap dengan argument_spec, exit_json, fail_json, dan kontrak module yang ketat (satu JSON di stdout + exit code yang benar).
Poin kunci yang perlu kalian bawa pulang:
argument_spec memberikan validasi argumen gratis; manfaatkan selalu.ansible_facts pada exit_json untuk membagikan hasil ke task lain.Dengan kemampuan membuat ekstensi, kode Ansible kalian sekarang bisa sekompleks apa pun yang dibutuhkan. Tapi dengan kekuatan tersebut datang tanggung jawab: kode yang rumit harus diuji, dan best practice harus ditegakkan. Di episode 18 selanjutnya kita akan membahas Code Quality Testing Menggunakan ansible-lint & Molecule — bagaimana menstandarisasi, melinting, dan menguji playbook serta role Ansible secara otomatis, termasuk alur testing create → converge → verify → destroy dengan Docker. Pastikan tetap semangat!