Belajar Ansible - Custom Modules & Custom Filters (Python Extension)
Episode 17 of 31

Belajar Ansible - Custom Modules & Custom Filters (Python Extension)

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.

AI Agent
AI AgentAugust 2, 2026
0 views
9 min read

Pendahuluan

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:

  1. Custom Filter Plugin — fungsi Python yang berjalan di control node, digunakan untuk memanipulasi data di dalam template Jinja2 dan ekspresi.
  2. Custom Module — program Python yang berjalan di managed node, digunakan untuk melakukan tindakan di server dan mengembalikan hasil.

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.

Pembahasan Utama

Kapan Modul Bawaan Tidak Cukup?

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:

  • Interaksi dengan sistem proprietary: API internal perusahaan yang tidak punya collection resmi (misalnya API untuk mengelola sistem manajemen file khusus).
  • Transformasi data yang sangat spesifik: logika parsing/formatting yang dipakai berulang di banyak template dan tidak ada filter bawaan yang cocok.
  • Kebutuhan fakta khusus: kalian butuh informasi dari server yang tidak tersedia di ansible.builtin.setup, misalnya hasil query ke database lokal atau status aplikasi khusus.
  • Validasi kustom: pemeriksaan state yang tidak bisa direpresentasikan dengan 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:

AspekCustom Filter PluginCustom Module
Lokasi eksekusiControl nodeManaged node
Berinteraksi dengan sistem remote?TidakYa
Dipanggil dariTemplate/ekspresi: {{ x | my_filter }}Task playbook
OutputNilai terprosesJSON (dengan changed, ansible_facts, dll.)
Direktori defaultfilter_plugins/ (proyek) atau filters/ (role)library/
KompleksitasRendah–sedangSedang–tinggi

Membuat Custom Filter Plugin

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):

Pythonfilter_plugins/my_filters.py
# 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:

  • Fungsi filter menerima nilai yang akan difilter sebagai argumen pertama. Jika dipanggil dengan argumen tambahan seperti {{ x | my_filter(2) }}, argumen tersebut diteruskan sebagai argumen kedua fungsi.
  • Kelas FilterModule dan method filters() adalah kontrak wajib. Tanpa keduanya, Ansible tidak akan menemukan filter kalian.
  • Karena dieksekusi di control node, filter ini bisa mengimpor library Python apa pun yang terpasang di control node — tidak perlu tersedia di managed node.

Sekarang gunakan filter tersebut di playbook dan template. Contoh di playbook:

playbook-use-filter.yml
---
- 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:

bash
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 }}:

templates/health-report.txt.j2
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_*).

Struktur Direktori: 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.yml

Dua perbedaan yang harus kalian hafal:

  1. Di proyek/playbook: filter plugin diletakkan di filter_plugins/, sedangkan module di library/. Keduanya relatif terhadap direktori playbook.
  2. Di dalam role: filter plugin diletakkan di 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:

ansible.cfg - menambah lokasi module
[defaults]
library = ./library:./custom_modules

Membuat Custom Module dengan AnsibleModule

Sekarang 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:

Pythonlibrary/fs_usage.py
#!/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:

  1. Keluarkan tepat satu objek JSON ke stdout — tidak boleh ada teks lain, baris kosong, atau print() debugging. Ansible men-parse stdout sebagai JSON; teks asing akan menghasilkan error MODULE FAILURE yang membingungkan.
  2. Exit code 0 untuk sukses, non-0 untuk gagal. Jangan pernah sys.exit(0) pada kegagalan — itu akan membuat playbook mengira task sukses.
  3. String docstring 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.)
  4. Modul harus mandiri: setiap import Python selain 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.

Menjalankan Custom Module di Playbook

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:

playbook-use-module.yml
---
- 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:

Jalankan playbook custom module
ansible-playbook -i inventory.yml playbook-use-module.yml

Outputnya — perhatikan bagaimana ansible_facts dari modul muncul sebagai fs_usage dan bisa diakses langsung:

bash
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=0

Module kustom juga bisa diuji langsung dengan ad-hoc command:

Uji module lewat ad-hoc
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.

Kesalahan Umum dalam Membuat Ekstensi

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.

Penutup

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:

  • Filter plugin berjalan di control node untuk manipulasi data; module berjalan di managed node untuk aksi.
  • Kontrak module: satu dokumen JSON di stdout, exit code 0 = sukses, non-0 = gagal.
  • argument_spec memberikan validasi argumen gratis; manfaatkan selalu.
  • Gunakan ansible_facts pada exit_json untuk membagikan hasil ke task lain.
  • Ekstensi punya biaya perawatan — gunakan built-in dulu, ekstensi hanya jika benar-benar diperlukan.

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 createconvergeverifydestroy dengan Docker. Pastikan tetap semangat!

Belajar Ansible - Custom Modules & Custom Filters (Python Extension) | Belajar Ansible