Error Handling

Error Handling #

Playbook yang hanya berjalan di kondisi sempurna tidak cukup tangguh untuk mengelola infrastruktur produksi. Di dunia nyata, kita akan menghadapi jaringan yang tidak stabil, repositori paket yang tiba-tiba offline, disk penuh, atau layanan pihak ketiga yang tidak merespons. Jika kita tidak merancang playbook dengan strategi penanganan kesalahan (error handling) yang matang, kegagalan kecil pada satu task dapat menghentikan seluruh proses deployment, membiarkan server target dalam kondisi tidak konsisten (setengah terkonfigurasi), dan menyulitkan proses pemulihan (recovery). Ansible menyediakan berbagai fitur bawaan untuk mendeteksi, mengabaikan, memodifikasi, dan memulihkan diri dari kegagalan secara otomatis guna menjaga integritas sistem kita.

Mengapa Strategi Penanganan Kesalahan Penting #

Ketika Ansible mengeksekusi playbook, perilakunya secara default sangat ketat: jika sebuah task gagal (mengembalikan status non-zero exit code atau mengalami error koneksi), Ansible akan langsung menghentikan semua task selanjutnya untuk host yang gagal tersebut, tetapi tetap melanjutkan eksekusi untuk host lain yang berhasil. Perilaku ini dirancang untuk keamanan agar kita tidak menumpuk konfigurasi baru di atas sistem yang sudah rusak. Namun, dalam skenario otomatisasi yang kompleks seperti deployment aplikasi zero-downtime atau migrasi database, penghentian mendadak ini bisa menjadi bencana jika tidak diantisipasi dengan rollback atau cleanup.

Berikut adalah diagram alir yang memvisualisasikan bagaimana Ansible menangani alur eksekusi saat menghadapi kegagalan task secara default:

flowchart TD
    Start["Mulai Playbook"] --> T1["Task 1: Eksekusi Sukses"]
    T1 --> T2{"Task 2: Apakah Sukses?"}
    T2 -- "Ya" --> T3["Task 3: Lanjutkan Eksekusi"]
    T2 -- "Tidak" --> StopHost["Hentikan Eksekusi pada Host yang Gagal"]
    T3 --> End["Selesai (Semua Host Up-to-date)"]
    StopHost --> FailState["Host Masuk State Gagal (Task Berikutnya Diabaikan)"]

Dengan memahami alur dasar ini, kita bisa mulai menerapkan mekanisme yang lebih fleksibel untuk mengontrol kapan sebuah task dianggap gagal dan bagaimana sistem harus merespons kegagalan tersebut.


Konstruksi block, rescue, dan always #

Ansible menyediakan struktur pengelompokan task yang disebut block. Struktur ini sangat mirip dengan blok penanganan eksepsi try-catch-finally yang biasa kita temukan di bahasa pemrograman modern seperti Java, Python, atau C#. Dengan menggunakan block, kita dapat mengelompokkan beberapa task yang saling terkait secara logis, mendefinisikan langkah pemulihan (rescue) jika salah satu task di dalam block tersebut gagal, dan menentukan task pembersihan (always) yang harus dieksekusi terlepas dari berhasil atau gagalnya block utama.

Menggunakan block untuk Mengelompokkan Task #

Block memungkinkan kita menerapkan direktif tingkat task (seperti become, when, atau vars) ke beberapa task sekaligus secara efisien. Namun, kegunaan utamanya adalah untuk membatasi ruang lingkup kesalahan. Jika ada task di dalam block yang mengembalikan error, Ansible akan menghentikan sisa task di dalam block tersebut dan langsung melompat ke bagian rescue.

Peran rescue dalam Rollback Otomatis #

Bagian rescue hanya akan dieksekusi jika salah satu task di dalam block mengalami kegagalan. Di dalam rescue, kita dapat menempatkan task yang bertugas melakukan rollback, seperti mengembalikan file konfigurasi cadangan, menyalakan kembali service yang sempat mati, atau mengirimkan notifikasi darurat ke tim operasional. Setelah task di dalam rescue selesai dieksekusi dengan sukses, Ansible akan menganggap play tersebut kembali berjalan lancar (tidak dianggap gagal) dan akan melanjutkan eksekusi ke task setelah blok tersebut.

Peran always untuk Cleanup #

Bagian always akan dieksekusi dalam kondisi apa pun: baik ketika semua task di dalam block berjalan sukses, maupun setelah bagian rescue selesai menangani kesalahan. Ini adalah tempat yang sangat ideal untuk menempatkan task pembersihan (cleanup) seperti menghapus file temporary, menutup koneksi database khusus, atau mencatat log audit deployment.

Berikut adalah contoh implementasi penerapan block, rescue, dan always untuk deployment aplikasi yang aman dengan rollback otomatis:

# ANTI-PATTERN: Menjalankan task secara linear tanpa penanganan error.
# Jika deployment gagal di tengah jalan, sistem dibiarkan dalam kondisi rusak/stuck.
- name: Deploy aplikasi secara naif
  hosts: app_servers
  tasks:
    - name: Hentikan service aplikasi
      ansible.builtin.systemd:
        name: webapp
        state: stopped

    - name: Deploy kode baru dari Git
      ansible.builtin.git:
        repo: "[email protected]:kita/webapp.git"
        dest: /var/www/webapp
        version: release-1.2.0

    - name: Jalankan migrasi database
      ansible.builtin.command: /var/www/webapp/bin/migrate
      # JANGAN: Jika migrasi gagal di sini, service webapp akan tetap mati selamanya
      # dan kode baru yang setengah jalan ter-deploy dibiarkan begitu saja.

    - name: Jalankan kembali service aplikasi
      ansible.builtin.systemd:
        name: webapp
        state: started

Sekarang kita bandingkan dengan implementasi yang benar menggunakan struktur penanganan kesalahan terstruktur:

# BENAR: Membungkus alur deployment kritis dengan block-rescue-always.
- name: Deploy aplikasi dengan proteksi rollback
  hosts: app_servers
  vars:
    backup_dir: /var/backups/webapp
    app_dir: /var/www/webapp
  tasks:
    - name: Blok Eksekusi Utama Deployment
      block:
        - name: Buat direktori backup
          ansible.builtin.file:
            path: "{{ backup_dir }}"
            state: directory
            mode: '0755'

        - name: Backup kode aplikasi lama sebelum update
          ansible.builtin.archive:
            path: "{{ app_dir }}"
            dest: "{{ backup_dir }}/app_prev.tar.gz"
            format: gz

        - name: Hentikan service aplikasi
          ansible.builtin.systemd:
            name: webapp
            state: stopped

        - name: Deploy kode baru dari Git
          ansible.builtin.git:
            repo: "[email protected]:kita/webapp.git"
            dest: "{{ app_dir }}"
            version: release-1.2.0

        - name: Jalankan migrasi database
          ansible.builtin.command: "{{ app_dir }}/bin/migrate"
          register: migration_result

        - name: Jalankan kembali service aplikasi
          ansible.builtin.systemd:
            name: webapp
            state: started

      rescue:
        - name: Peringatan kegagalan terdeteksi
          ansible.builtin.debug:
            msg: "Terjadi error pada block utama! Memulai proses rollback otomatis..."

        - name: Kembalikan kode aplikasi lama dari backup
          ansible.builtin.unarchive:
            src: "{{ backup_dir }}/app_prev.tar.gz"
            dest: "{{ app_dir }}"
            remote_src: true

        - name: Jalankan kembali service dengan kode lama (rollback state)
          ansible.builtin.systemd:
            name: webapp
            state: started

        - name: Laporkan status kegagalan deployment
          ansible.builtin.fail:
            msg: "Deployment gagal dan sistem berhasil di-rollback ke versi sebelumnya."

      always:
        - name: Bersihkan file backup temporary jika ada
          ansible.builtin.file:
            path: "{{ backup_dir }}/app_prev.tar.gz"
            state: absent

        - name: Log status akhir deployment ke control node
          delegate_to: localhost
          ansible.builtin.lineinfile:
            path: /var/log/ansible-deploy.log
            line: "{{ ansible_date_time.iso8601 }} - Host {{ inventory_hostname }} selesai diproses."
            create: true
            mode: '0644'

Alur eksekusi dari blok kode di atas digambarkan melalui diagram alir berikut:

flowchart TD
    subgraph Blok Utama ("block")
        A1["Backup Kode Lama"] --> A2["Stop Service"]
        A2 --> A3["Deploy Kode Baru"]
        A3 --> A4["Migrasi DB"]
        A4 --> A5["Start Service"]
    end
    subgraph Blok Pemulihan ("rescue")
        R1["Kembalikan Kode dari Backup"] --> R2["Start Service Lama"]
        R2 --> R3["Picu Kegagalan Playbook"]
    end
    subgraph Blok Pembersihan ("always")
        L1["Hapus File Backup Temp"] --> L2["Tulis Log Deployment"]
    end

    Start["Mulai Task"] --> A1
    A4 -- "Error Terjadi" --> R1
    A5 -- "Sukses" --> L1
    R3 --> L1
    L2 --> End["Selesai"]

failed_when: Kustomisasi Kriteria Kegagalan #

Secara default, Ansible menganggap sebuah task gagal jika program atau modul yang dijalankan mengembalikan exit code selain 0. Namun, kriteria ini tidak selalu sesuai dengan kebutuhan riil operasional kita. Kadang-kadang, sebuah command sukses berjalan dengan exit code 0 tetapi mengeluarkan teks error di stdout yang menandakan operasi gagal. Sebaliknya, ada kalanya program menghasilkan exit code non-zero (misalnya grep mengembalikan exit code 1 jika tidak menemukan kecocokan teks), padahal secara logika alur kerja kita hal tersebut adalah kondisi normal yang ingin kita terima.

Di sinilah kita menggunakan direktif failed_when. Dengan failed_when, kita bisa mengevaluasi status kembalian (registered variable) dari suatu task menggunakan ekspresi logika Jinja2 untuk menentukan secara presisi kapan task tersebut harus dianggap gagal.

Mengidentifikasi Error Melalui Output Teks #

Misalnya, ketika kita menjalankan command cli untuk memeriksa kesehatan API eksternal, cli tersebut mungkin selalu mengembalikan exit code 0 selama jaringannya terhubung, meskipun server merespons dengan "Status: 500 Internal Server Error". Kita bisa memantau variabel stdout dari task tersebut:

# BENAR: Menentukan kegagalan berdasarkan string spesifik di dalam stdout
- name: Periksa kesehatan API eksternal
  ansible.builtin.command: curl -s http://api.external.com/health
  register: api_check
  failed_when: 
    - "'ERROR' in api_check.stdout"
    - "'500' in api_check.stdout"
  changed_when: false

Menangani Toleransi Exit Code Tertentu #

Jika kita menggunakan utility seperti pg_isready untuk memeriksa status Postgres, kita tahu bahwa tool ini dapat mengembalikan exit code 1 jika server sedang memproses inisialisasi awal. Jika kita ingin menganggap exit code 1 sebagai hal yang aman dan hanya menganggap gagal jika exit code bernilai 2 (fatal error), kita bisa menggunakan failed_when secara fleksibel:

# BENAR: Memodifikasi toleransi exit code
- name: Periksa kesiapan database PostgreSQL
  ansible.builtin.command: pg_isready -h localhost -p 5432
  register: pg_status
  failed_when: "pg_status.rc == 2"
  changed_when: false

Mari kita bandingkan anti-pattern yang sering dilakukan developer dengan solusi yang lebih elegan menggunakan failed_when.

# ANTI-PATTERN: Menggunakan task tambahan dengan modul 'fail' untuk memeriksa status.
# Ini membuat kode sangat bertele-tele dan menghasilkan task tambahan di output logs.
- name: Jalankan query database secara naif
  ansible.builtin.command: mysql -e "SELECT * FROM users;"
  register: db_query
  ignore_errors: true

- name: Validasi hasil query secara manual
  ansible.builtin.fail:
    msg: "Query database gagal dieksekusi!"
  when: db_query.rc != 0 or 'Connection refused' in db_query.stderr


# BENAR: Menggabungkan validasi kegagalan langsung di dalam task menggunakan failed_when.
- name: Jalankan query database dengan efisien
  ansible.builtin.command: mysql -e "SELECT * FROM users;"
  register: db_query_optimal
  failed_when:
    - db_query_optimal.rc != 0 or 'Connection refused' in db_query_optimal.stderr
  changed_when: false

ignore_errors: Lanjutkan Meski Gagal #

Direktif ignore_errors: true memberi tahu Ansible bahwa jika task ini mengalami kegagalan, abaikan saja dan lanjutkan ke task berikutnya seolah-olah tidak terjadi apa-apa. Hal ini berguna untuk task-task opsional yang sifatnya non-blocking, seperti membersihkan cache direktori opsional yang barangkali sudah dihapus, atau mencoba mengirim statistik opsional ke server log internal.

Penyalahgunaan ignore_errors: true adalah bahaya besar! Jangan menggunakannya sebagai jalan pintas untuk menyembunyikan konfigurasi yang salah atau error sistem yang tidak dimengerti. Jika kita mengabaikan error kritis (seperti kegagalan mounting disk atau kegagalan instalasi pustaka keamanan), playbook akan terus berjalan dan pada akhirnya menyebabkan sistem runtuh di task berikutnya tanpa indikasi error yang jelas di awal.

Alternatif Lebih Baik daripada ignore_errors #

Sebagian besar penggunaan ignore_errors sebenarnya dapat digantikan dengan pengecekan state menggunakan modul stat atau memeriksa kondisi dengan when.

Sebagai contoh, jika kita ingin menghapus file konfigurasi lama tetapi tidak ingin task gagal jika file tersebut memang sudah tidak ada, kita bisa menggunakan pendekatan modular daripada mengabaikan error secara buta:

# ANTI-PATTERN: Menggunakan ignore_errors untuk operasi file yang tidak pasti.
- name: Hapus file konfigurasi lama jika ada
  ansible.builtin.file:
    path: /etc/nginx/conf.d/old_site.conf
    state: absent
  ignore_errors: true  # JANGAN: Jika ini error karena masalah permission disk, kita tidak akan tahu.


# BENAR: Memeriksa keberadaan file terlebih dahulu sebelum melakukan aksi.
- name: Periksa apakah file konfigurasi lama ada
  ansible.builtin.stat:
    path: /etc/nginx/conf.d/old_site.conf
  register: old_site_file

- name: Hapus file konfigurasi jika terbukti ada
  ansible.builtin.file:
    path: /etc/nginx/conf.d/old_site.conf
    state: absent
  when: old_site_file.stat.exists

Dengan cara ini, jika terjadi masalah kegagalan sistem berkas sesungguhnya (seperti permission denied pada direktori /etc/nginx/), Ansible akan memunculkan error secara tepat sehingga kita bisa mendeteksi adanya malkonfigurasi hak akses di server.


force_handlers: Menjamin Pemeliharaan State #

Ketika sebuah task mengubah state sistem (seperti memperbarui template konfigurasi), task tersebut biasanya akan memicu trigger notify ke handler (misalnya "Restart Nginx"). Namun, sesuai aturan bawaan Ansible, semua handler yang terpicu baru akan dieksekusi di akhir bagian play. Masalah muncul jika di tengah-tengah eksekusi task setelah pemicuan template terjadi kegagalan sistem.

Secara default, jika sebuah play gagal di tengah jalan, Ansible akan mengabaikan seluruh handler yang dijadwalkan berjalan di akhir play. Akibatnya, server kita bisa berada dalam kondisi tidak konsisten: file konfigurasi baru sudah tertulis ke disk, tetapi service belum di-restart untuk memuat konfigurasi baru tersebut. Ini sangat berbahaya karena jika server di-restart di kemudian hari secara manual, service mungkin gagal menyala akibat konfigurasi baru yang tidak tervalidasi.

Menggunakan force_handlers #

Untuk mencegah skenario ini, kita dapat menetapkan direktif force_handlers: true di tingkat play. Direktif ini memaksa Ansible untuk tetap mengeksekusi semua handler yang sudah ternotifikasi, bahkan jika eksekusi playbook terhenti akibat kegagalan pada task setelahnya.

Kita bisa mengonfigurasi ini di dalam playbook kita:

# BENAR: Menetapkan force_handlers di tingkat play untuk menjamin konsistensi service
- name: Kelola konfigurasi server web webapp
  hosts: web_servers
  force_handlers: true  # ✓ Menjamin semua handler yang ternotifikasi tetap dijalankan meski ada task berikutnya yang gagal
  tasks:
    - name: Update file konfigurasi Nginx
      ansible.builtin.template:
        src: nginx.conf.j2
        dest: /etc/nginx/nginx.conf
        mode: '0644'
      notify: Restart Nginx Service

    - name: Jalankan task verifikasi aset eksternal
      ansible.builtin.command: /usr/local/bin/verify-assets.sh
      # Jika task verifikasi ini gagal, handler 'Restart Nginx Service' akan tetap dieksekusi oleh Ansible
      # sehingga perubahan konfigurasi Nginx di atas tidak ditinggalkan menggantung tanpa restart.

  handlers:
    - name: Restart Nginx Service
      ansible.builtin.systemd:
        name: nginx
        state: restarted

Selain mendefinisikannya di tingkat playbook, kita juga bisa memaksa pengaturan ini secara global di dalam file konfigurasi ansible.cfg kita:

# ansible.cfg
[defaults]
# Mengaktifkan force_handlers secara default untuk seluruh playbook
force_handlers = True

any_errors_fatal dan max_fail_percentage untuk Skala Besar #

Ketika mengelola puluhan atau ratusan server sekaligus, mengizinkan eksekusi playbook berlanjut di host yang sehat sementara beberapa host lainnya gagal bisa menjadi keputusan yang kurang tepat. Terutama dalam proses deployment arsitektur microservices yang saling bergantung, atau saat melakukan update skema database terdistribusi. Kegagalan pada satu atau sebagian kecil node dapat merusak konsistensi cluster secara keseluruhan.

Ansible menyediakan dua cara canggih untuk mengontrol batas toleransi kesalahan di infrastruktur berskala besar: any_errors_fatal dan max_fail_percentage.

any_errors_fatal: Kematian Atomik #

Jika kita menyetel any_errors_fatal: true pada sebuah play, Ansible akan menghentikan seluruh eksekusi playbook secara instan di semua host aktif begitu ada satu host saja yang mengalami kegagalan task. Ini sangat krusial digunakan pada tahap persiapan awal (pre-tasks) atau ketika menjalankan migrasi database utama di mana kegagalan satu node harus membatalkan seluruh proses deployment demi menjaga integritas data global.

max_fail_percentage: Toleransi Batas Ambang #

Jika kita melakukan rolling update secara bertahap pada kluster web server yang besar (misalnya menggunakan direktif serial), menghentikan seluruh deployment hanya karena satu server mengalami kegagalan kecil mungkin terlalu sensitif. Kita bisa menentukan batas persentase kegagalan maksimum menggunakan max_fail_percentage. Selama jumlah server yang gagal masih di bawah persentase batas tersebut, Ansible akan terus melanjutkan eksekusi ke kelompok server berikutnya.

Berikut adalah ilustrasi komparatif penggunaannya dalam satu playbook:

# BENAR: Menggunakan any_errors_fatal pada setup database dan max_fail_percentage pada rolling update webapp.
- name: Inisialisasi Database Kritis
  hosts: database_cluster
  any_errors_fatal: true  # ✓ Hentikan semua database node jika satu node saja gagal saat bootstrap
  tasks:
    - name: Verifikasi replikasi database aktif
      ansible.builtin.command: check_replication_status.sh
      changed_when: false

- name: Rolling Update Kluster Web Server
  hosts: web_servers
  serial: 10%                  # Proses update 10% host sekaligus secara bertahap
  max_fail_percentage: 20      # ✓ Izinkan proses lanjut selama server yang gagal tidak melebihi 20% dari total host
  tasks:
    - name: Pasang pembaruan paket keamanan webapp
      ansible.builtin.apt:
        name: webapp-package
        state: latest

Diagram alir berikut mendeskripsikan bagaimana keputusan eksekusi diambil berdasarkan kriteria max_fail_percentage:

flowchart TD
    Start["Mulai Batch Baru (Serial 10%)"] --> Exec["Eksekusi Task di Batch Saat Ini"]
    Exec --> Eval{"Apakah Ada Node Gagal?"}
    Eval -- "Tidak" --> BatchSuccess["Batch Sukses"]
    Eval -- "Ya" --> Calc{"Apakah Persentase Kegagalan > max_fail_percentage (20%)?"}
    Calc -- "Ya" --> Terminate["Hentikan Playbook Secara Global (Fatal Error)"]
    Calc -- "Tidak" --> BatchSuccess
    BatchSuccess --> CheckNext{"Apakah Ada Batch Tersisa?"}
    CheckNext -- "Ya" --> Start
    CheckNext -- "Nu" --> End["Semua Batch Selesai Diproses"]

Pola Desain Rollback Otomatis di Produksi #

Untuk membangun automasi infrastruktur tingkat enterprise yang mandiri (self-healing), kita tidak boleh hanya mengandalkan deteksi error dasar. Kita harus menyusun playbook kita dengan arsitektur penanganan kegagalan yang lengkap, mencakup pengumpulan metrik status sebelum eksekusi (pre-flight checks), penyimpanan state versi sebelumnya secara dinamis, deployment aman menggunakan blok transaksi, verifikasi kesehatan pasca-deployment (health checks), serta pemulihan otomatis (rollback) jika verifikasi tersebut gagal.

Di bawah ini adalah contoh rancangan playbook produksi yang menerapkan semua konsep tersebut secara komprehensif:

# BENAR: Pola desain deployment aplikasi web terintegrasi dengan rollback otomatis dan verifikasi status kesehatan
---
- name: Deployment Mandiri dengan Sistem Pemulihan Otomatis
  hosts: web_servers
  become: true
  force_handlers: true  # Memastikan handler tetap berjalan untuk merapikan service
  vars:
    app_root: /opt/production_app
    backup_root: /opt/backups_app
    app_version_target: "v2.1.0"
    health_check_url: "http://127.0.0.1:8080/health"

  pre_tasks:
    - name: Kumpulkan informasi versi aplikasi saat ini
      ansible.builtin.slurp:
        src: "{{ app_root }}/version.txt"
      register: current_version_raw
      ignore_errors: true

    - name: Definisikan versi cadangan untuk rollback
      ansible.builtin.set_fact:
        app_version_previous: "{{ (current_version_raw.content | b64decode | trim) if current_version_raw.content is defined else 'none' }}"

    - name: Tampilkan informasi versi terdeteksi
      ansible.builtin.debug:
        msg: "Mendeteksi aplikasi versi lama: {{ app_version_previous }}. Target update: {{ app_version_target }}"

  tasks:
    - name: Blok Transaksi Deployment Aplikasi
      block:
        - name: Buat direktori backup jika belum ada
          ansible.builtin.file:
            path: "{{ backup_root }}"
            state: directory
            mode: '0700'

        - name: Backup direktori aplikasi saat ini jika versi sebelumnya ada
          ansible.builtin.archive:
            path: "{{ app_root }}"
            dest: "{{ backup_root }}/app_backup_{{ app_version_previous }}.tar.gz"
            format: gz
          when: app_version_previous != 'none'

        - name: Hentikan aplikasi lama secara aman
          ansible.builtin.systemd:
            name: production_app
            state: stopped

        - name: Bersihkan direktori aplikasi lama untuk mempersiapkan kode baru
          ansible.builtin.file:
            path: "{{ app_root }}"
            state: absent

        - name: Buat ulang direktori aplikasi kosong
          ansible.builtin.file:
            path: "{{ app_root }}"
            state: directory
            mode: '0755'

        - name: Unduh dan ekstrak paket aplikasi versi baru
          ansible.builtin.unarchive:
            src: "https://artifactory.kita.internal/apps/release-{{ app_version_target }}.tar.gz"
            dest: "{{ app_root }}"
            remote_src: true

        - name: Tulis versi baru ke file identitas aplikasi
          ansible.builtin.copy:
            content: "{{ app_version_target }}"
            dest: "{{ app_root }}/version.txt"
            mode: '0644'

        - name: Jalankan pembaruan modul aplikasi
          ansible.builtin.command: npm install --production
          args:
            chdir: "{{ app_root }}"

        - name: Jalankan kembali service aplikasi dengan kode baru
          ansible.builtin.systemd:
            name: production_app
            state: started
            enabled: true

        - name: Tunggu aplikasi melakukan booting awal
          ansible.builtin.pause:
            seconds: 5

        - name: Uji kesehatan aplikasi (Health Check pasca deployment)
          ansible.builtin.uri:
            url: "{{ health_check_url }}"
            status_code: 200
          register: health_status
          until: health_status.status == 200
          retries: 5
          delay: 3
          # Jika request API health check ini gagal dalam 5 kali percobaan,
          # task ini dianggap failed dan akan memicu eksekusi bagian rescue secara otomatis.

      rescue:
        - name: Peringatan Kritis - Uji kesehatan gagal! Memulai pemulihan sistem...
          ansible.builtin.debug:
            msg: "Kegagalan deployment pada host {{ inventory_hostname }}. Memulai pemulihan otomatis ke versi {{ app_version_previous }}."

        - name: Jalankan Rollback jika versi lama tersedia
          block:
            - name: Hentikan service yang gagal berjalan
              ansible.builtin.systemd:
                name: production_app
                state: stopped

            - name: Hapus direktori kode aplikasi yang rusak
              ansible.builtin.file:
                path: "{{ app_root }}"
                state: absent

            - name: Buat kembali direktori aplikasi
              ansible.builtin.file:
                path: "{{ app_root }}"
                state: directory
                mode: '0755'

            - name: Ekstrak kembali file cadangan versi lama
              ansible.builtin.unarchive:
                src: "{{ backup_root }}/app_backup_{{ app_version_previous }}.tar.gz"
                dest: "{{ app_root }}"
                remote_src: true

            - name: Hidupkan kembali service aplikasi dengan versi lama yang stabil
              ansible.builtin.systemd:
                name: production_app
                state: started
              # Kita hanya menjalankan rescue block ini jika memang ada backup versi lama yang berhasil disimpan
          when: app_version_previous != 'none'

        - name: Laporkan kesalahan deployment akhir ke dashboard pemantau
          ansible.builtin.fail:
            msg: "Deployment ke versi {{ app_version_target }} gagal di host {{ inventory_hostname }}. Sistem telah dipulihkan ke versi {{ app_version_previous }}."

      always:
        - name: Bersihkan arsip cadangan lama untuk menghemat kapasitas penyimpanan server
          ansible.builtin.file:
            path: "{{ backup_root }}/app_backup_{{ app_version_previous }}.tar.gz"
            state: absent
          when: app_version_previous != 'none'

Melalui kerangka kerja di atas, infrastruktur kita tidak akan pernah dibiarkan dalam keadaan mati atau tidak berfungsi. Keberadaan block-rescue memastikan bahwa kegagalan instalasi modul dependencies (npm install) atau kegagalan booting port aplikasi (health check) akan segera direspon dengan memulihkan folder kode lama dari arsip tarball dan menyalakan kembali daemon aplikasi yang stabil.


Ringkasan #

  • block / rescue / always merupakan struktur fondasi try-catch-finally dalam Ansible untuk mengelompokkan task kritis dan mengeksekusi aksi pemulihan (rollback) otomatis ketika terjadi kesalahan.
  • failed_when memungkinkan kita mendefinisikan aturan kegagalan task sendiri secara presisi menggunakan ekspresi Jinja2, alih-alih hanya mengandalkan exit code bernilai non-zero.
  • ignore_errors harus digunakan dengan sangat hati-hati dan selektif; hindari menggunakannya secara membabi buta untuk menutupi kesalahan konfigurasi yang sesungguhnya.
  • force_handlers: true menjamin semua handler (seperti restart service) yang ternotifikasi tetap dijalankan oleh Ansible meskipun ada task berikutnya dalam play yang mengalami error.
  • any_errors_fatal: true berguna untuk skenario deployment atomik di mana kegagalan pada satu node harus membatalkan eksekusi playbook secara global demi keamanan data.
  • max_fail_percentage menetapkan ambang batas toleransi kegagalan pada infrastruktur skala besar, memungkinkan deployment terus berlanjut jika jumlah server yang gagal masih di bawah batas toleransi.
  • Arsitektur deployment produksi yang baik selalu menyertakan langkah pre-flight checks untuk menyimpan state lama sebelum melakukan modifikasi berkas, sehingga proses rollback dapat berjalan secara akurat.

← Sebelumnya: Scheduled Task   Berikutnya: Delegation & Local Action →

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