Notify & Listen

Notify & Listen #

Otomatisasi sistem yang sukses sangat bergantung pada kemampuan kita untuk mengisolasi setiap bagian kode agar tetap modular dan mudah dipelihara. Ketika sebuah playbook berkembang dari beberapa baris tugas menjadi puluhan role yang saling berhubungan, ketergantungan antar komponen sering kali menjadi rumit. Jika kita menggunakan metode konvensional di mana satu task utama harus mengetahui secara persis nama tugas pemulihan (recovery task) di bagian lain, kita sedang menciptakan tingkat keterikatan yang sangat erat (tight coupling).

Untuk mengatasi masalah ini, Ansible menyediakan dua mekanisme pemicuan peristiwa yang sangat kuat: notify dan listen. Kombinasi keduanya memungkinkan kita mengimplementasikan pola arsitektur Publish-Subscribe (Pub/Sub) yang memisahkan pengirim notifikasi dari penerima aksi secara longgar (loose coupling). Artikel ini akan membahas secara mendalam bagaimana merancang rantai reaksi peristiwa yang bersih, memahami algoritma internal pengurutan eksekusi, serta strategi memecahkan masalah (debugging) ketika handler tidak terpicu sebagaimana mestinya.


Pola Arsitektur: Notify by Name vs Listen (Pub/Sub) #

Secara historis, Ansible hanya mendukung pemicuan handler berdasarkan nama handlernya (dikenal sebagai Notify by Name). Namun, sejak versi 2.2, Ansible memperkenalkan kata kunci listen yang mendefinisikan sebuah topik atau peristiwa (event). Perbedaan filosofis antara kedua pendekatan ini sangat menentukan kualitas struktur playbook kita.

1. Notify by Name (Tightly-Coupled) #

Pada pola ini, task utama menunjuk langsung ke nama spesifik dari handler yang ingin dijalankan.

  • Kelebihan: Sangat mudah dipahami pada proyek skala kecil dan menengah. Hubungan antara pemicu dan pelaksana bersifat eksplisit.
  • Kekurangan: Task utama harus mengetahui nama persis dari handler pelaksana. Jika kita mengubah nama handler untuk perbaikan tata bahasa atau refaktorisasi, kita harus mencari dan mengubah seluruh baris notify di semua file task yang merujuk padanya. Pola ini menyulitkan pembuatan role yang bersifat independen dan dapat digunakan kembali (reusable).

2. Listen Topic (Loosely-Coupled Publish-Subscribe) #

Pada pola ini, task utama hanya bertugas “menerbitkan” (publish) sebuah pesan atau nama peristiwa ke broker internal Ansible. Di sisi lain, satu atau beberapa handler mendaftarkan diri untuk “mendengarkan” (subscribe) peristiwa tersebut menggunakan kata kunci listen.

  • Kelebihan: Task utama tidak perlu tahu handler apa saja yang mendengarkan event tersebut, atau bahkan apakah ada handler yang meresponsnya. Pemisahan tanggung jawab ini membuat penulisan role menjadi sangat bersih. Beberapa handler dalam role terpisah dapat mendengarkan satu event yang sama tanpa perlu memodifikasi kode pemanggil.
  • Kekurangan: Penelusuran hubungan antara task dan handler menjadi bersifat implisit, sehingga membutuhkan dokumentasi nama topik event yang disiplin.

Tabel perbandingan di bawah ini menggambarkan perbedaan teknis kedua pola tersebut:

Kriteria Notify by Name Listen Topic (Pub/Sub)
Pemicu (notify) Nama handler secara spesifik. Nama event/topik bebas.
Penerima (listen) Tidak menggunakan kata kunci listen. Menggunakan kata kunci listen: <nama_event>.
Hubungan 1-ke-1 (satu notifikasi ke satu handler). 1-ke-Banyak (satu notifikasi dapat memicu banyak handler sekaligus).
Tingkat Ketergantungan Tinggi (Tightly-Coupled). Rendah (Loosely-Coupled).
Skalabilitas Role Sulit diintegrasikan antar role yang berbeda tim pembuat. Sangat mudah diintegrasikan lintas role melalui standarisasi event.

Berikut adalah visualisasi arsitektur model Publish-Subscribe pada Ansible menggunakan diagram Mermaid:

flowchart TD
    subgraph PublisherTasks["Sisi Pengirim (Tasks)"]
        T1["Task: Update nginx.conf"] -->|"notify: 'webserver config changed'"| EventBroker(("Event Broker (Ansible Engine)"))
        T2["Task: Update php.ini"] -->|"notify: 'webserver config changed'"| EventBroker
    end

    subgraph EventBrokerSub["Fase Distribusi Event"]
        EventBroker -->|"Mendistribusikan Event ke semua Subscriber"| SubQueue{{"Topik: 'webserver config changed'"}}
    end

    subgraph SubscribersHandlers["Sisi Penerima (Handlers)"]
        SubQueue -->|"Listen: 'webserver config changed'"| H1["Handler: Restart Nginx"]
        SubQueue -->|"Listen: 'webserver config changed'"| H2["Handler: Reload PHP-FPM"]
        SubQueue -->|"Listen: 'webserver config changed'"| H3["Handler: Clear OpCache"]
    end

Implementasi Skenario Nyata: Penggunaan Listen Lintas Role #

Mari kita tinjau skenario nyata di mana arsitektur Publish-Subscribe menggunakan listen terbukti sangat superior dibandingkan dengan Notify by Name.

Bayangkan kita memiliki playbook untuk mengelola stack web server dinamis yang melibatkan tiga role berbeda:

  1. common: Mengelola konfigurasi keamanan global dan parameter jaringan.
  2. nginx: Mengelola server proxy depan.
  3. php_fpm: Mengelola prosesor backend PHP.

Ketika ada perubahan pada konfigurasi keamanan sistem di role common (misalnya penyesuaian sertifikat SSL root baru), kita ingin kedua layanan (nginx dan php_fpm) melakukan reload agar dapat memuat sertifikat baru tersebut. Tanpa listen, role common harus mengetahui nama handler internal milik role nginx dan php_fpm, yang merusak batasan isolasi role.

Berikut adalah bagaimana kita memecahkan masalah ini secara elegan dengan listen:

# ==============================================================================
# BERKAS: roles/common/tasks/main.yml
# ==============================================================================
---
- name: Memperbarui sertifikat SSL Root sistem
  copy:
    src: corporate-ca.crt
    dest: /usr/local/share/ca-certificates/corporate-ca.crt
  notify: system certificates updated
  # ✓ Role common hanya memicu peristiwa "system certificates updated".
  # Role ini tidak peduli siapa yang akan merespons peristiwa tersebut.
# ==============================================================================
# BERKAS: roles/nginx/handlers/main.yml
# ==============================================================================
---
- name: Memuat Ulang Layanan Nginx
  systemd:
    name: nginx
    state: reloaded
  listen: system certificates updated
  # ✓ Handler di dalam role nginx mendengarkan event tersebut dan merespons secara mandiri.
# ==============================================================================
# BERKAS: roles/php_fpm/handlers/main.yml
# ==============================================================================
---
- name: Memuat Ulang Layanan PHP-FPM
  systemd:
    name: php-fpm
    state: reloaded
  listen: system certificates updated
  # ✓ PHP-FPM juga ikut mendengarkan event yang sama dan melakukan reload.

Dengan pola di atas, jika di masa depan kita menambahkan role baru, misalnya apache atau varnish, kita cukup menambahkan handler yang mendengarkan event system certificates updated di dalam role baru tersebut. Kita tidak perlu menyentuh atau memodifikasi file task di dalam role common sama sekali.


Algoritma Urutan Eksekusi Handler (Execution Ordering) #

Satu detail teknis yang sangat penting dan wajib dipahami oleh setiap developer Ansible adalah: urutan pemanggilan notify di dalam bagian tasks sama sekali tidak mempengaruhi urutan eksekusi handler.

Ansible memiliki algoritma internal yang mengevaluasi dan menjalankan handler berdasarkan urutan definisi handler di dalam file konfigurasi, bukan urutan kronologis pemicuan notifikasi.

Mari kita pelajari contoh kasus di bawah ini untuk melihat bagaimana algoritma ini bekerja:

# Playbook untuk memanipulasi aplikasi web
- name: Deployment Web Application
  hosts: webservers
  tasks:
    - name: Menyalin kode program terbaru
      git:
        repo: https://github.com/example/app.git
        dest: /var/www/html
      notify:
        - Jalankan Migrasi Database
        - Nyalakan Layanan Aplikasi
        - Matikan Layanan Aplikasi
      # Kita memicu notifikasi dalam urutan: Migrasi -> Nyalakan -> Matikan.

  handlers:
    # DEFINISI HANDLER DIBAWAH INI DITULIS DENGAN URUTAN LOGIS YANG BERBEDA:
    - name: Matikan Layanan Aplikasi
      systemd:
        name: webapp
        state: stopped

    - name: Jalankan Migrasi Database
      command: /var/www/html/bin/migrate.sh

    - name: Nyalakan Layanan Aplikasi
      systemd:
        name: webapp
        state: started

Analisis Alur Eksekusi #

Meskipun pada bagian tasks kita memanggil Jalankan Migrasi Database terlebih dahulu sebelum memanggil Matikan Layanan Aplikasi, Ansible akan mengeksekusi antrian dengan urutan sebagai berikut:

  1. Matikan Layanan Aplikasi (Didefinisikan pertama di blok handlers:)
  2. Jalankan Migrasi Database (Didefinisikan kedua di blok handlers:)
  3. Nyalakan Layanan Aplikasi (Didefinisikan ketiga di blok handlers:)

Urutan ini sangat logis untuk pemeliharaan aplikasi (mematikan aplikasi terlebih dahulu sebelum migrasi database dilakukan untuk mencegah korupsi data, kemudian menyalakannya kembali). Namun, jika kita salah menuliskan urutan definisi di bagian handlers:, misalnya menempatkan Nyalakan Layanan Aplikasi di atas Matikan Layanan Aplikasi, maka Ansible akan menyalakan aplikasi terlebih dahulu kemudian mematikannya, yang akan membuat aplikasi kita mati permanen setelah playbook selesai.

[!IMPORTANT] Selalu susun daftar handler di dalam berkas handlers/main.yml atau blok handlers: sesuai dengan urutan ketergantungan kronologis yang benar (misalnya, stop service -> migrasi database -> start service -> bersihkan cache). Jangan mengandalkan urutan pemanggilan notify di dalam task untuk mengatur alur logika eksekusi handler.


4 Penyebab Utama Kegagalan Pemicuan Handler #

Sering kali dalam menulis otomatisasi, kita menemukan situasi di mana handler yang kita harapkan berjalan ternyata diabaikan oleh Ansible. Berikut adalah analisis mendalam mengenai empat penyebab utama masalah tersebut beserta solusi penanganannya.

1. Perbedaan Karakter dan Ukuran Huruf (Typo & Case-Sensitivity) #

Ansible mencocokkan string notifikasi dengan nama handler secara literal dan sensitif terhadap huruf besar-kecil.

  • Masalah:
    notify: Restart Postgresql  # Menggunakan huruf kecil pada 'l'
    
    Sedangkan handlernya didefinisikan sebagai:
    - name: Restart PostgreSQL  # Menggunakan huruf kapital pada 'L'
    
  • Solusi: Pastikan nama di kedua tempat sama persis. Gunakan fitur Search and Replace pada kode editor kita untuk memverifikasi kecocokan string secara menyeluruh.

2. Task Tidak Menghasilkan Status changed (Idempotensi Terpenuhi) #

Handler dirancang khusus untuk meminimalkan aksi tidak perlu. Jika task yang memiliki baris notify tidak melakukan perubahan apa pun pada managed node (kembali ke status ok), notifikasi tidak akan dikirimkan ke broker event.

  • Masalah: Kita menjalankan playbook untuk kedua kalinya. File konfigurasi sudah ter-copy dengan isi yang identik. Status task adalah ok. Kita heran mengapa service nginx tidak ikut me-restart.
  • Solusi: Ini adalah perilaku yang benar dan diharapkan. Namun, jika kita memang ingin memaksa task tersebut selalu melaporkan status changed (misalnya untuk keperluan debugging atau task pembaca konfigurasi dinamis), kita bisa menambahkan parameter changed_when: true pada task tersebut.

3. Playbook Berhenti Sebelum Akhir Play (Play Failed) #

Jika ada task utama yang gagal dieksekusi di tengah jalan, Ansible secara default menghentikan eksekusi playbook untuk host tersebut demi mencegah kerusakan sistem lebih lanjut. Akibatnya, fase eksekusi handler yang ditangguhkan di akhir play tidak pernah tercapai.

  • Masalah: Konfigurasi apache berhasil diubah (status changed). Task berikutnya (install modul php) gagal karena masalah koneksi internet. Playbook berhenti. Konfigurasi apache yang baru belum aktif di memori karena handler restart tidak pernah jalan.
  • Solusi: Gunakan opsi force_handlers: true pada deklarasi playbook kita untuk memaksa Ansible tetap menjalankan handler yang telah di-notify meskipun terjadi kegagalan pada task utama setelahnya.

4. Isolasi Cakupan Role (Role Scope Isolation) #

Saat kita menyertakan role menggunakan modul dinamis seperti include_role di tengah-tengah play, handler yang didefinisikan di dalam role tersebut mungkin belum terdaftar di memori Ansible saat task utama di luar role memanggilnya.

  • Masalah: Task di tingkat playbook utama mencoba mengirimkan notifikasi ke handler yang berada di dalam berkas roles/db/handlers/main.yml saat role db dimasukkan menggunakan perintah include_role.
  • Solusi: Gunakan import_role alih-alih include_role untuk memastikan handler di dalam role tersebut di-parse secara statis di awal eksekusi playbook, sehingga handlernya langsung terdaftar dan dapat dipanggil kapan saja.

Strategi Debugging: Melacak Pemicuan Event secara Presisi #

Ketika rantai event kita tidak berjalan dengan benar, kita membutuhkan visibilitas yang lebih dalam ke dalam mesin eksekusi Ansible. Jangan menebak-nebak di mana letak kegagalannya. Gunakan instrumen pencatatan bawaan Ansible.

Menggunakan Modifikasi Verbose CLI (-v, -vv) #

Kita dapat meningkatkan tingkat verbositas output terminal saat menjalankan ansible-playbook untuk melihat proses pemicuan handler secara real-time.

# Menjalankan playbook dengan verbose tingkat 2 untuk melihat notifikasi handler
ansible-playbook -i hosts.ini site.yml -vv

Pada output tingkat -vv, perhatikan baris yang menunjukkan registrasi event seperti di bawah ini:

META: ran handlers
NOTIFIED HANDLER webserver | Restart Nginx for host-01

Jika baris NOTIFIED HANDLER tidak muncul setelah task yang bersangkutan dieksekusi, berarti status task tersebut adalah ok (bukan changed), atau ada kesalahan nama penulisan notifikasi sehingga Ansible mengabaikan pemicuan tersebut.

Memeriksa Status Registrasi Variabel #

Metode debug lain yang sangat akurat adalah mendaftarkan output task ke dalam variabel menggunakan register, lalu mengevaluasi nilainya menggunakan modul debug.

# Menggunakan modul debug untuk memverifikasi perubahan status
- name: Mengonfigurasi parameter aplikasi
  template:
    src: settings.conf.j2
    dest: /etc/settings.conf
  register: app_config_result
  notify: Reload Application

- name: Cetak detail status pembaruan konfigurasi
  debug:
    var: app_config_result.changed
  # ✓ Baris ini akan mencetak "true" jika terjadi perubahan nyata,
  # yang mengonfirmasi bahwa sinyal notify seharusnya berhasil dikirimkan.

Memaksa Pemicuan Menggunakan changed_when #

Meskipun prinsip dasar handler adalah hanya merespons status changed asli, ada beberapa kasus di mana modul yang kita gunakan tidak dapat mendeteksi perubahan status secara otomatis. Contoh paling umum adalah penggunaan modul command atau shell.

Modul command secara default selalu mengembalikan status changed karena Ansible tidak tahu apa yang dilakukan oleh perintah biner eksternal tersebut di dalam sistem operasi. Sebaliknya, modul seperti uri yang melakukan panggilan API REST mungkin hanya mengembalikan status ok meskipun data di server tujuan sebenarnya telah dimodifikasi.

Kita dapat mengontrol penentuan status ini secara presisi menggunakan parameter changed_when untuk memastikan pemicuan handler berjalan akurat.

# Skenario 1: Memaksa status "changed" pada modul shell hanya jika ada output tertentu
- name: Memeriksa integritas database aplikasi
  shell: /usr/local/bin/check_db_schema.sh
  register: db_check
  # Kita tahu script mengembalikan teks "SCHEMA_OUTDATED" jika ada tabel yang kurang
  changed_when: "'SCHEMA_OUTDATED' in db_check.stdout"
  notify: Perbarui Skema Database

# Skenario 2: Memaksa status "changed" selalu True untuk task trigger manual
- name: Kirim sinyal pemicu manual untuk pembersihan cache
  command: /usr/bin/true
  changed_when: true  # ✓ Selalu mengembalikan status "changed"
  notify: Bersihkan Redis Cache

Dengan menggunakan changed_when secara bijak, kita menjaga keakuratan otomatisasi playbook kita tanpa merusak prinsip idempotensi. Kita hanya memicu restart atau aksi pemulihan ketika kondisi sistem benar-benar membutuhkannya.


Ringkasan #

  • Decoupling dengan listen: Kata kunci listen memisahkan ketergantungan langsung (tight coupling) antara task dan handler, menggantikannya dengan model Publish-Subscribe yang bersih.
  • Topik Event: Task menggunakan notify untuk memicu suatu event, sementara satu atau beberapa handler mendengarkan event tersebut menggunakan listen: <nama_event>.
  • Urutan Definisi: Urutan eksekusi handler mutlak ditentukan oleh urutan penulisannya di dalam berkas definisi handlers:, bukan berdasarkan urutan pemanggilan notify di dalam task.
  • Penyebab Gagal: Kegagalan pemicuan biasanya disebabkan oleh kesalahan penulisan nama (sensitif huruf kapital), task yang bernilai ok, playbook crash sebelum play berakhir, atau cakupan role dinamis.
  • Instrumen Debugging: Gunakan opsi CLI -vv untuk memantau status NOTIFIED HANDLER secara mendalam di layar terminal.
  • Kondisional changed_when: Gunakan parameter changed_when pada modul perintah eksternal untuk mengontrol status perubahan secara logis sebelum memicu handler.

← Sebelumnya: Handler   Berikutnya: Condition & Loop →

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