Incident Response

Incident Response #

Alert sudah berbunyi. Seseorang di on-call duty harus bertindak. Tanpa persiapan, setiap insiden dimulai dari nol: buka terminal, SSH ke server, coba ingat perintah diagnostik yang relevan, baca log yang tersebar, coba beberapa solusi. Dengan runbook yang bisa dieksekusi — playbook Ansible yang mendokumentasikan langkah-langkah respons sekaligus menjalankannya — tim bisa merespons lebih cepat, lebih konsisten, dan dengan lebih sedikit kesalahan. Artikel ini membahas cara membangun infrastruktur runbook yang efektif.

Playbook Diagnostik: Kumpulkan Dulu, Putuskan Kemudian #

Langkah pertama saat insiden bukanlah melakukan perubahan — melainkan mengumpulkan informasi untuk memahami situasi:

# playbooks/diagnose.yml
---
- name: Kumpulkan informasi diagnostik saat insiden
  hosts: "{{ target_hosts | default('all') }}"
  gather_facts: true

  tasks:
    - name: Kumpulkan status semua service kritis
      systemd:
        name: "{{ item }}"
      register: service_status
      loop: "{{ critical_services }}"
      ignore_errors: true
      changed_when: false

    - name: Kumpulkan penggunaan resource
      command: "{{ item.cmd }}"
      register: "{{ item.name }}"
      loop:
        - { name: top_processes, cmd: "ps aux --sort=-%cpu | head -15" }
        - { name: disk_usage,    cmd: "df -h" }
        - { name: memory_usage,  cmd: "free -h" }
        - { name: network_conn,  cmd: "ss -tnp | head -30" }
        - { name: open_files,    cmd: "lsof | wc -l" }
      ignore_errors: true
      changed_when: false

    - name: Kumpulkan log error terbaru
      command: "journalctl -u {{ item }} --since '30 minutes ago' --no-pager -p err"
      register: recent_errors
      loop: "{{ critical_services }}"
      ignore_errors: true
      changed_when: false

    - name: Simpan semua diagnostik ke file lokal
      local_action:
        module: copy
        content: |
          ===== DIAGNOSTIK INSIDEN =====
          Host: {{ inventory_hostname }}
          Waktu: {{ ansible_date_time.iso8601 }}

          === STATUS SERVICE ===
          {% for result in service_status.results %}
          {{ result.item }}: {{ result.status.ActiveState | default('unknown') }}
          {% endfor %}

          === RESOURCE USAGE ===
          {{ disk_usage.stdout }}

          {{ memory_usage.stdout }}

          === TOP PROCESSES ===
          {{ top_processes.stdout }}

          === LOG ERRORS (30 MENIT TERAKHIR) ===
          {% for result in recent_errors.results %}
          --- {{ result.item }} ---
          {{ result.stdout | default('(tidak ada error)') }}
          {% endfor %}          
        dest: "/tmp/incident-diag-{{ inventory_hostname }}-{{ ansible_date_time.date }}.txt"
      delegate_to: localhost

- name: Tampilkan ringkasan diagnostik
  hosts: localhost
  gather_facts: false
  tasks:
    - name: Lokasi file diagnostik
      debug:
        msg: "File diagnostik tersimpan di /tmp/incident-diag-*.txt"

Self-Healing: Automasi untuk Kondisi Umum #

Beberapa kondisi insiden yang berulang bisa diatasi secara otomatis tanpa intervensi manusia:

# playbooks/self-heal.yml
---
- name: Self-healing untuk kondisi umum
  hosts: appservers
  become: true

  tasks:
    - name: Kumpulkan informasi kondisi saat ini
      gather_facts: true

    # Kondisi 1: Disk hampir penuh — bersihkan log lama dan Docker images
    - name: Cek penggunaan disk
      command: df / --output=pcent
      register: disk_pcent
      changed_when: false

    - name: Bersihkan log lama jika disk > 85%
      block:
        - name: Hapus log lama (lebih dari 7 hari)
          find:
            paths: /var/log
            age: 7d
            patterns: "*.log.*"
            recurse: true
          register: old_logs

        - name: Hapus file log lama
          file:
            path: "{{ item.path }}"
            state: absent
          loop: "{{ old_logs.files }}"

        - name: Bersihkan Docker images yang tidak digunakan
          community.docker.docker_prune:
            images: true
            volumes: true
            builder_cache: true
          ignore_errors: true

        - name: Catat tindakan self-healing
          debug:
            msg: "Self-heal: disk cleanup dilakukan (penggunaan disk > 85%)"
      when: disk_pcent.stdout | trim | replace('%', '') | int > 85

    # Kondisi 2: Service crash — restart otomatis
    - name: Cek status service kritis
      systemd:
        name: "{{ item }}"
      register: svc_check
      loop: "{{ critical_services }}"
      changed_when: false
      ignore_errors: true

    - name: Restart service yang tidak aktif
      systemd:
        name: "{{ item.item }}"
        state: restarted
      loop: "{{ svc_check.results }}"
      when:
        - item.status is defined
        - item.status.ActiveState != "active"
      loop_control:
        label: "{{ item.item }}"

    # Kondisi 3: Memory pressure — restart service dengan memory leak
    - name: Cek penggunaan memory
      command: free -m --output=used
      register: mem_used
      changed_when: false

    - name: Restart aplikasi jika memory sangat tinggi
      systemd:
        name: myapp
        state: restarted
      when:
        - ansible_memtotal_mb > 0
        - (mem_used.stdout_lines[-1] | trim | int) / ansible_memtotal_mb > 0.90

Runbook sebagai Playbook yang Terdokumentasi #

Runbook yang paling efektif adalah yang bisa dijalankan sekaligus menjelaskan apa yang dilakukannya:

# playbooks/runbooks/database-connection-exhausted.yml
---
# RUNBOOK: Database Connection Pool Exhausted
#
# Gejala: Error "remaining connection slots are reserved for non-replication superuser connections"
# Penyebab umum: Connection leak di aplikasi, traffic spike, atau pool size terlalu kecil
# Eskalasi: Jika runbook ini tidak menyelesaikan masalah, hubungi tim database

- name: Runbook — Database Connection Pool Exhausted
  hosts: dbservers
  become: true

  tasks:
    - name: "[Diagnostik] Lihat semua koneksi aktif ke database"
      command: >
        psql -U postgres -c
        "SELECT client_addr, state, count(*) as count
         FROM pg_stat_activity
         GROUP BY client_addr, state
         ORDER BY count DESC"        
      register: db_connections
      changed_when: false
      become_user: postgres

    - name: "[Info] Tampilkan koneksi per client"
      debug:
        var: db_connections.stdout_lines

    - name: "[Cek] Berapa koneksi idle yang lama?"
      command: >
        psql -U postgres -c
        "SELECT count(*) FROM pg_stat_activity
         WHERE state = 'idle'
         AND state_change < now() - interval '10 minutes'"        
      register: idle_connections
      changed_when: false
      become_user: postgres

    - name: "[Aksi] Terminasi koneksi idle lama jika > 20"
      command: >
        psql -U postgres -c
        "SELECT pg_terminate_backend(pid)
         FROM pg_stat_activity
         WHERE state = 'idle'
         AND state_change < now() - interval '10 minutes'
         AND pid <> pg_backend_pid()"        
      become_user: postgres
      when: idle_connections.stdout | regex_search('\d+') | int > 20
      register: terminate_result

    - name: "[Hasil] Tampilkan jumlah koneksi yang diterminasi"
      debug:
        msg: "Koneksi yang diterminasi: {{ terminate_result.stdout | default('0 (tidak ada yang perlu diterminasi)') }}"

    - name: "[Verifikasi] Cek jumlah koneksi sekarang"
      command: >
        psql -U postgres -c
        "SELECT count(*) FROM pg_stat_activity"        
      register: current_connections
      changed_when: false
      become_user: postgres

    - name: "[Status] Kondisi koneksi saat ini"
      debug:
        var: current_connections.stdout_lines

Workflow Eskalasi #

# playbooks/escalate.yml
---
- name: Kirim eskalasi insiden
  hosts: localhost
  vars:
    incident_id: "{{ lookup('pipe', 'date +%Y%m%d%H%M%S') }}"

  tasks:
    - name: Buat tiket insiden di sistem tiket
      uri:
        url: "{{ pagerduty_events_url }}"
        method: POST
        body_format: json
        body:
          routing_key: "{{ vault_pagerduty_routing_key }}"
          event_action: trigger
          dedup_key: "ansible-incident-{{ incident_id }}"
          payload:
            summary: "{{ incident_summary }}"
            severity: "{{ incident_severity | default('critical') }}"
            source: "ansible-runbook"
            custom_details:
              environment: "{{ env }}"
              affected_hosts: "{{ ansible_play_hosts | join(', ') }}"
              runbook: "{{ playbook_dir | basename }}"
        status_code: 202
      no_log: true

    - name: Kirim notifikasi Slack darurat
      uri:
        url: "{{ vault_slack_webhook_url }}"
        method: POST
        body_format: json
        body:
          text: ":rotating_light: *INSIDEN KRITIS*"
          attachments:
            - color: danger
              title: "{{ incident_summary }}"
              fields:
                - title: Environment
                  value: "{{ env }}"
                  short: true
                - title: Incident ID
                  value: "{{ incident_id }}"
                  short: true
      no_log: true

Ringkasan #

  • Kumpulkan diagnostik sebelum bertindak — playbook diagnostik yang mengumpulkan status service, resource usage, dan log error memberikan gambaran situasi tanpa membuat perubahan.
  • Self-healing untuk kondisi yang berulang dan terprediksi (disk penuh, service crash, idle connection) — kurangi intervensi manual untuk hal-hal yang bisa diotomasi.
  • Runbook sebagai playbook adalah cara terbaik mendokumentasikan prosedur insiden — dokumentasi dan eksekusi ada di tempat yang sama dan selalu sinkron.
  • Tambahkan komentar naratif di playbook runbook (Gejala, Penyebab, Eskalasi) — orang yang on-call jam 3 pagi butuh konteks, bukan hanya perintah.
  • Workflow eskalasi harus otomatis — jika runbook tidak menyelesaikan masalah, sistem harus mengirim alert eskalasi tanpa membutuhkan keputusan manual.
  • Simpan semua runbook di Git dan review secara berkala — runbook yang tidak di-update menjadi dokumen yang menyesatkan saat paling dibutuhkan.

← Sebelumnya: Health Check   Berikutnya: SLO & SLA →

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