Bangun playbook Ansible yang tangguh dengan ignore_errors, failed_when, changed_when, serta struktur block, rescue, dan always untuk menangani kegagalan di tengah eksekusi secara profesional.

Setelah di episode 9 sebelumnya kita membahas bagaimana memberikan logika ke playbook melalui conditionals dan loops, pada episode kali ini kita akan membahas sisi lain yang tidak kalah penting: apa yang terjadi ketika sesuatu berjalan salah.
Dalam dunia produksi, kegagalan bukanlah pertanyaan "apakah", melainkan "kapan". Bayangkan kalian menjalankan playbook deployment ke 100 server, dan server nomor 17 gagal di tengah proses karena repositori package tidak bisa diakses. Dengan perilaku default Ansible, kegagalan itu akan menghentikan seluruh playbook — server 18 sampai 100 tidak pernah tersentuh, dan status sistem menjadi tidak konsisten: sebagian server sudah ter-update, sebagian lagi belum. Bagi seorang engineer, skenario seperti ini adalah mimpi buruk, dan di sinilah pentingnya membangun resilience.
Pada episode ini kita akan membahas tiga keyword untuk mengendalikan status task — ignore_errors, failed_when, dan changed_when — lalu kita lanjut ke struktur block, rescue, dan always yang mirip dengan try/catch/finally di bahasa pemrograman. Dengan kombinasi ini, kalian bisa menulis playbook yang tahu cara bertahan: mengabaikan kesalahan yang tidak fatal, menentukan sendiri arti "gagal" dan "berubah", melakukan rollback saat terjadi error, dan membersihkan resource sementara apa pun hasil akhirnya.
Sebelum masuk ke struktur block yang kompleks, mari kita kuasai tiga keyword dasar yang mengendalikan bagaimana Ansible menilai hasil sebuah task. Ketiganya bekerja di level task dan merupakan alat pertama yang harus kalian miliki untuk error handling.
ignore_errors: true: Terus Berjalan Meski Task GagalDefault perilaku Ansible adalah fail fast: begitu sebuah task FAILED, playbook berhenti untuk host tersebut. Keyword ignore_errors: true memberi tahu Ansible untuk menganggap kegagalan tersebut sebagai bukan masalah besar dan melanjutkan task berikutnya.
Note
Analogi sederhana: saat menonton video, ignore_errors itu seperti fitur "skip error" pada player — kalian tetap melanjutkan ke segmen berikutnya meskipun ada bagian yang gagal diputar. Berguna untuk konten yang tidak krusial, berbahaya jika diterapkan ke seluruh video.
Kapan ignore_errors benar-benar pantas dipakai? Salah satunya untuk task best-effort — task yang boleh gagal tanpa mengubah hasil akhir. Contoh klasik: menghapus file cache atau mengirim notifikasi:
- name: Hapus cache aplikasi (best-effort)
ansible.builtin.file:
path: /var/cache/app
state: absent
ignore_errors: true
- name: Kirim notifikasi ke Slack (gagal tidak fatal)
ansible.builtin.uri:
url: https://hooks.slack.com/services/xxx
method: POST
body_format: json
body: { "text": "Deploy selesai" }
ignore_errors: trueWalaupun task kedua gagal (misalnya karena URL tidak bisa diakses), playbook tetap berlanjut ke task berikutnya. Ini mencegah satu kegagalan kecil menghentikan seluruh deployment yang sebenarnya sudah berhasil.
failed_when: Tentukan Sendiri Arti "Gagal"failed_when mengambil alih penilaian kegagalan dari Ansible. Alih-alih menilai berdasarkan return code modul, kalian mendefinisikan kondisi eksplisit kapan sebuah task harus dianggap gagal. Ini sangat berguna saat menggunakan modul command atau shell, karena modul-modul tersebut terkadang mengembalikan return code non-nol untuk hal yang sebenarnya normal, atau sebaliknya.
Contoh nyata: perintah apt-get update bisa saja mengembalikan rc != 0 hanya karena ada satu repo yang mati, padahal repo lainnya berhasil di-refresh. Kita bisa mengevaluasi keluarannya secara lebih cermat:
- name: Update cache apt dengan evaluasi khusus
ansible.builtin.command: apt-get update
register: apt_result
failed_when:
- apt_result.rc != 0
- "'The repository is not signed' not in apt_result.stderr"Kombinasi di atas dibaca: task dianggap gagal jika return code bukan nol dan pesan error-nya bukan soal repo unsigned yang sebenarnya bisa diabaikan. Teknik mencocokkan pola pada stdout atau stderr menggunakan regex juga umum, misalnya:
- name: Verifikasi service berjalan
ansible.builtin.shell: systemctl is-active app
register: svc_status
failed_when: svc_status.stdout != "active"failed_when juga menjadi jembatan penting menuju until yang kita bahas di episode 9 — keduanya bekerja sama dengan hasil task yang di-register.
changed_when: Kendalikan Status PerubahanModul command dan shell selalu mengembalikan status CHANGED, karena Ansible tidak memiliki cara untuk mengetahui apakah perintah tersebut benar-benar mengubah sesuatu. Padahal status CHANGED sangat penting: status inilah yang memicu handler (yang sudah kita bahas di episode 6) dan menentukan apakah sebuah run dianggap "menyentuh" sistem.
changed_when memungkinkan kalian menetapkan kapan sebuah task melaporkan status CHANGED:
- name: Restart service hanya jika konfigurasi benar-benar berubah
ansible.builtin.shell: |
systemctl restart app
systemctl status app
register: restart_result
changed_when: "'inactive' in restart_result.stdout"Dengan changed_when, task yang tidak melakukan perubahan apa pun akan dilaporkan sebagai OK (tidak CHANGED), sehingga eksekusi playbook menjadi jujur: run kedua kalinya diharapkan menghasilkan "nothing to do", yang merupakan inti dari idempotency.
Berikut perbandingan ketiga keyword tersebut:
| Keyword | Fungsi | Kapan digunakan |
|---|---|---|
ignore_errors: true | Mengabaikan kegagalan dan melanjutkan run | Task best-effort yang boleh gagal |
failed_when | Mendefinisikan kondisi "gagal" secara eksplisit | Modul command/shell dengan output yang perlu dievaluasi |
changed_when | Mendefinisikan kondisi "berubah" secara eksplisit | Memaksa idempotency / mengontrol pemicu handler |
block, rescue, dan alwaysKetiga keyword di atas menangani penilaian status task secara individual. Tapi bagaimana jika kalian ingin menjalankan sekelompok task, dan ketika salah satu di antaranya gagal, menjalankan task fallback tertentu, lalu memastikan ada pembersihan yang selalu dijalankan? Di sinilah struktur block, rescue, dan always berperan.
Tip
Kalian yang pernah menulis kode pasti langsung mengenali pola ini: struktur block/rescue/always adalah padanan Ansible dari try / except / finally di Python, atau try / catch / finally di bahasa lain. block berisi kode utama, rescue berisi penanganan exception, dan always berisi kode yang dijamin dieksekusi apa pun hasilnya.
block: Mengelompokkan Task & Menerapkan Atribut Bersamablock adalah pengelompokan beberapa task menjadi satu kesatuan. Ada dua alasan utama menggunakannya:
rescue dan always (akan dibahas di bawah).when, become, dan tags yang dipasang di level block otomatis berlaku untuk semua task di dalamnya. Ini menghindari pengulangan kondisi yang sama berulang-ulang.Contoh kedua, tanpa block kita menulis kondisi yang sama tiga kali:
- name: Tanpa block - kondisi diulang tiap task
hosts: all
tasks:
- name: Install package
ansible.builtin.apt:
name: nginx
state: present
when: ansible_facts['os_family'] == "Debian"
- name: Copy konfigurasi
ansible.builtin.template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
when: ansible_facts['os_family'] == "Debian"
- name: Start service
ansible.builtin.systemd_service:
name: nginx
state: started
when: ansible_facts['os_family'] == "Debian"Dengan block, kondisi ditulis sekali dan berlaku untuk semua task di dalamnya:
- name: Dengan block - kondisi cukup sekali
hosts: all
tasks:
- name: Setup nginx untuk Debian
when: ansible_facts['os_family'] == "Debian"
block:
- name: Install package
ansible.builtin.apt:
name: nginx
state: present
- name: Copy konfigurasi
ansible.builtin.template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
- name: Start service
ansible.builtin.systemd_service:
name: nginx
state: startedrescue: Penanganan Exception (Fallback Saat Error)Ketika salah satu task di dalam block gagal, Ansible menghentikan sisa task di dalam block tersebut dan langsung melompat ke bagian rescue. Semua task di dalam rescue dieksekusi sebagai penanganan kegagalan — biasanya berisi fallback, log error, atau rollback.
always: Pembersihan yang Selalu BerjalanBagian always dieksekusi tanpa kecuali: apakah block sukses, gagal lalu masuk rescue, atau bahkan task di block melempar error — always tetap dijalankan. Ini adalah tempat yang tepat untuk cleanup: menghapus file sementara, menghentikan service, atau memulihkan state.
Mari kita lihat contoh lengkap yang menggabungkan ketiganya: instalasi aplikasi dengan rollback otomatis dan cleanup yang selalu dijalankan.
- name: Deploy aplikasi dengan rollback otomatis
hosts: app-servers
become: true
vars:
app_version: "2.4.0"
backup_path: "/opt/app_backups/{{ app_version }}"
tasks:
- name: Deploy aplikasi versi baru
block:
- name: Unduh artefak aplikasi
ansible.builtin.get_url:
url: "https://artifacts.internal/app-{{ app_version }}.tar.gz"
dest: "/tmp/app-{{ app_version }}.tar.gz"
- name: Backup versi lama
ansible.builtin.copy:
src: /opt/app/
dest: "{{ backup_path }}"
remote_src: true
- name: Ekstrak versi baru
ansible.builtin.unarchive:
src: "/tmp/app-{{ app_version }}.tar.gz"
dest: /opt/app/
remote_src: true
rescue:
- name: Rollback ke versi lama
ansible.builtin.copy:
src: "{{ backup_path }}/"
dest: /opt/app/
remote_src: true
- name: Laporkan kegagalan deployment
ansible.builtin.debug:
msg: "Deployment versi {{ app_version }} gagal, dilakukan rollback."
always:
- name: Restart service aplikasi
ansible.builtin.systemd_service:
name: app
state: restarted
- name: Bersihkan artefak sementara
ansible.builtin.file:
path: "/tmp/app-{{ app_version }}.tar.gz"
state: absentAlur eksekusi pada contoh di atas:
block dijalankan: unduh artefak → backup versi lama → ekstrak versi baru.rescue di-skip, always tetap dijalankan (restart service + bersihkan file sementara).rescue dijalankan untuk rollback ke versi lama dan mencatat pesan kegagalan, lalu always tetap dijalankan.Perhatikan bahwa pada kasus kegagalan, service tetap di-restart di bagian always, tetapi karena rescue sudah mengembalikan versi lama ke /opt/app/, restart tersebut justru memastikan service kembali berjalan dengan versi yang stabil. Inilah kekuatan struktur ini: state sistem selalu kembali konsisten.
Berikut output yang akan kalian lihat ketika rescue benar-benar aktif. Perhatikan bahwa output berubah dari pola ok/changed menjadi failed pada task yang gagal, lalu rescue dieksekusi, dan always tetap berjalan:
Important
Ada beberapa hal yang perlu kalian pahami tentang keterbatasan rescue:
rescue hanya menangani kegagalan task di dalam block yang sama. Kegagalan dari handlers (episode 6) tidak ditangani oleh rescue.rescue tidak menangkap kondisi unreachable.block yang gagal tetap akan berjalan setelah rescue/always selesai.1. Overuse ignore_errors — menutupi masalah yang sebenarnya
ignore_errors adalah pedang bermata dua. Terlalu banyak menggunakannya membuat playbook "tampak hijau" padahal sebenarnya banyak task yang gagal diam-diam. Playbook yang selalu ok padahal service tidak berjalan adalah jebakan paling berbahaya di produksi. Gunakan ignore_errors hanya untuk task best-effort yang benar-benar tidak krusial, dan selalu sertakan debug atau logging agar kegagalan yang diabaikan tetap terlihat.
2. Salah memakai failed_when tanpa mengevaluasi rc
failed_when menggantikan penilaian default Ansible sepenuhnya. Jika kalian hanya menulis failed_when: "'error' in result.stdout" tanpa menyertakan pemeriksaan rc, maka task yang mengembalikan return code non-nol — misalnya karena crash total — justru dianggap berhasil selama kata "error" tidak muncul di stdout. Selalu pertimbangkan kombinasi kondisi rc dan konten output, seperti contoh apt-get update di atas.
3. changed_when yang salah mematikan pemicu handler
Karena handler dipicu oleh status changed, menulis changed_when: false pada task yang sebenarnya mengubah konfigurasi akan mencegah handler (misalnya restart service) dijalankan — dan perubahan konfigurasi tidak pernah benar-benar diterapkan. Pastikan changed_when mencerminkan kondisi perubahan yang nyata.
4. Meletakkan block tanpa rescue/always padahal berniat menangani error
block tanpa rescue dan always hanyalah pengelompokan biasa. Jika tujuan kalian adalah error handling, pastikan struktur lengkap block → rescue → always terpasang. Block saja tidak memberikan perilaku try/catch/finally apa pun.
5. when yang mengandalkan hasil task yang di-ignore_errors
Ketika sebuah task gagal dan di-ignore_errors, hasilnya tetap ter-register dengan atribut failed: true. Jika task berikutnya menggunakan when: result is succeeded atau when: result.failed, kalian perlu menyadari bahwa failed masih true. Pola aman yang umum adalah:
- name: Cek kesehatan service
ansible.builtin.command: systemctl is-active app
register: health
ignore_errors: true
- name: Restart jika service tidak aktif
ansible.builtin.systemd_service:
name: app
state: restarted
when: health.rc != 0Pada episode 10 ini kita telah membangun fondasi resilience untuk playbook Ansible. Kita mempelajari tiga keyword pengendali status — ignore_errors untuk mengabaikan kegagalan yang tidak fatal, failed_when untuk mendefinisikan sendiri arti "gagal" (misalnya dengan mengevaluasi output regex dari command), dan changed_when untuk mengendalikan status perubahan dan pemicu handler. Kemudian kita membedah struktur block, rescue, dan always — padanan try/catch/finally di Ansible — lengkap dengan contoh deployment yang melakukan rollback otomatis saat gagal dan selalu menjalankan cleanup.
Dengan kemampuan ini, playbook kalian tidak lagi berhenti total di tengah kegagalan: playbook bisa bertahan, menilai kondisi secara cerdas, melakukan pemulihan, dan selalu meninggalkan sistem dalam keadaan konsisten. Namun, playbook yang panjang dan tangguh sekaligus akan menjadi semakin sulit dirawat jika semua task ditulis dalam satu file raksasa.
Di episode 11 selanjutnya kita akan membahas Modularisasi dengan Includes & Imports — teknik memecah playbook besar menjadi file-file kecil yang teratur, dan memahami perbedaan mendasar antara include_tasks yang dinamis dengan import_tasks yang statis. Pastikan tetap semangat!