Production Readiness

Production Readiness #

Ada perbedaan besar antara “berjalan di production” dan “siap untuk production”. Sistem yang berjalan di production bisa saja tidak punya backup yang teruji, tidak ada runbook untuk skenario kegagalan, atau tidak ada cara untuk memverifikasi state yang diharapkan. Production readiness adalah tentang memastikan bahwa infrastruktur yang kamu kelola dengan Ansible tidak hanya berjalan hari ini, tapi bisa dipulihkan saat terjadi kegagalan dan bisa dioperasikan oleh siapapun di tim, kapanpun.

Checklist Production Readiness #

## Infrastructure Production Readiness Checklist

### Deployment
- [ ] Semua playbook sudah ditest di staging dengan beban yang representatif
- [ ] --check --diff sudah dijalankan ke production dan hasilnya di-review
- [ ] Rollback plan terdokumentasi dan pernah ditest
- [ ] Deployment window dikomunikasikan ke stakeholder

### Backup dan Recovery
- [ ] Backup otomatis berjalan dan schedule terdokumentasi
- [ ] Backup restore pernah ditest (bukan hanya backup yang dibuat)
- [ ] RTO (Recovery Time Objective) dan RPO (Recovery Point Objective) didefinisikan
- [ ] Prosedur disaster recovery terdokumentasi di runbook

### Observability
- [ ] Semua service termonitor (metrics, logging, health check)
- [ ] Alert dikonfigurasi untuk kondisi kritis
- [ ] Dashboard tersedia untuk investigasi insiden
- [ ] On-call rotation didefinisikan dan runbook tersedia

### Keamanan
- [ ] Semua secret di Ansible Vault (tidak ada plaintext di Git)
- [ ] Akses SSH terbatas ke IP/jaringan yang diizinkan
- [ ] Principle of least privilege diterapkan
- [ ] Audit log diaktifkan

### Dokumentasi
- [ ] README dengan cara menjalankan playbook
- [ ] Runbook untuk skenario insiden yang paling mungkin
- [ ] Dependency antar service terdokumentasi
- [ ] Kontak person untuk setiap komponen kritikal

Validasi State Sebelum Perubahan Besar #

Sebelum perubahan yang berpotensi disruptif, kumpulkan baseline state:

# playbooks/capture-baseline.yml
---
- name: Capture baseline state sebelum perubahan
  hosts: "{{ target_hosts | default('all') }}"
  gather_facts: true

  tasks:
    - name: Snapshot versi semua package penting
      package_facts:
        manager: auto

    - name: Simpan daftar package yang terinstal
      copy:
        content: >
          {{ ansible_facts.packages
             | dict2items
             | selectattr('value.0.version', 'defined')
             | map(attribute='key')
             | sort
             | join('\n') }}          
        dest: "/tmp/baseline-packages-{{ inventory_hostname }}.txt"
      delegate_to: localhost

    - name: Snapshot konfigurasi kritis
      fetch:
        src: "{{ item }}"
        dest: "/tmp/baseline-configs/{{ inventory_hostname }}/"
        flat: false
      loop:
        - /etc/nginx/nginx.conf
        - /etc/postgresql/15/main/postgresql.conf
        - /etc/sysctl.conf
      ignore_errors: true

    - name: Snapshot service state
      service_facts:

    - name: Simpan service state
      copy:
        content: >
          {{ ansible_facts.services
             | dict2items
             | selectattr('value.state', 'equalto', 'running')
             | map(attribute='key')
             | sort
             | join('\n') }}          
        dest: "/tmp/baseline-services-{{ inventory_hostname }}.txt"
      delegate_to: localhost

Verifikasi Post-Change #

Setelah perubahan, bandingkan dengan baseline:

# playbooks/verify-post-change.yml
---
- name: Verifikasi state setelah perubahan
  hosts: "{{ target_hosts | default('all') }}"
  gather_facts: true

  tasks:
    - name: Verifikasi semua critical service masih berjalan
      service_facts:

    - name: Pastikan service kritis tidak mati
      assert:
        that:
          - ansible_facts.services[item + '.service'].state == 'running'
        fail_msg: "KRITIS: Service {{ item }} tidak berjalan setelah perubahan!"
      loop: "{{ critical_services }}"
      ignore_errors: false

    - name: Verifikasi health endpoint semua aplikasi
      uri:
        url: "http://localhost:{{ item.port }}/health"
        status_code: 200
        timeout: 10
      loop: "{{ health_check_endpoints }}"
      loop_control:
        label: "{{ item.name }}"

    - name: Cek tidak ada error baru di log (30 menit terakhir)
      shell: |
        journalctl --since "30 minutes ago" -p err --no-pager | wc -l        
      register: error_count
      changed_when: false

    - name: Warning jika ada error baru di log
      debug:
        msg: "⚠️ Ada {{ error_count.stdout }} error baru di log dalam 30 menit terakhir"
      when: error_count.stdout | int > 0

Disaster Recovery Playbook #

# playbooks/disaster-recovery/restore-from-backup.yml
---
# PERINGATAN: Playbook ini memulihkan sistem dari backup
# Jalankan hanya dalam kondisi darurat setelah diskusi dengan tim
#
# Prasyarat:
# - Backup tersedia di S3 bucket
# - PostgreSQL sudah di-stop
# - Disk baru sudah di-mount (jika diperlukan)

- name: Restore sistem dari backup
  hosts: "{{ target_host | mandatory }}"
  become: true

  vars:
    backup_date: "{{ restore_date | mandatory }}"   # Format: YYYY-MM-DD
    s3_bucket: "company-backups"
    confirm_restore: "{{ confirm | default('no') }}"

  pre_tasks:
    - name: Konfirmasi restore dengan eksplisit
      assert:
        that:
          - confirm_restore == "yes-i-understand-this-will-overwrite-data"
        fail_msg: >
          Untuk menjalankan restore, tambahkan:
          -e "confirm=yes-i-understand-this-will-overwrite-data"
          Ini mencegah restore yang tidak disengaja.          

    - name: Log awal restore
      lineinfile:
        path: /var/log/disaster-recovery.log
        line: >
          {{ ansible_date_time.iso8601 }}
          RESTORE DIMULAI oleh {{ ansible_user }}
          dari backup {{ backup_date }}          
        create: true

  tasks:
    - name: Stop semua service yang terdampak
      systemd:
        name: "{{ item }}"
        state: stopped
      loop:
        - myapp
        - postgresql

    - name: Backup state saat ini sebelum restore (just in case)
      archive:
        path: /var/lib/postgresql
        dest: "/tmp/pre-restore-backup-{{ ansible_date_time.epoch }}.tar.gz"

    - name: Download backup dari S3
      amazon.aws.s3_object:
        bucket: "{{ s3_bucket }}"
        object: "backups/postgresql/{{ backup_date }}/pg_dump.sql.gz"
        dest: /tmp/restore.sql.gz
        mode: get

    - name: Restore database
      shell: |
        gunzip -c /tmp/restore.sql.gz | \
        psql -U postgres postgres        
      become_user: postgres

    - name: Start semua service
      systemd:
        name: "{{ item }}"
        state: started
      loop:
        - postgresql
        - myapp

    - name: Verifikasi restore berhasil
      uri:
        url: "http://localhost:{{ app_port }}/health"
        status_code: 200
      retries: 12
      delay: 10

  post_tasks:
    - name: Log restore selesai
      lineinfile:
        path: /var/log/disaster-recovery.log
        line: >
          {{ ansible_date_time.iso8601 }}
          RESTORE SELESAI dari backup {{ backup_date }}          
        create: true

    - name: Kirim notifikasi ke tim
      include_role:
        name: notify
      vars:
        status: "{{ 'success' if not ansible_failed_result is defined else 'failure' }}"
        title: "Disaster Recovery — Restore dari {{ backup_date }}"
        detail: "Restore ke {{ inventory_hostname }} oleh {{ ansible_user }}"

Ringkasan #

  • Production readiness bukan binary — ia adalah spektrum yang bisa selalu ditingkatkan. Mulai dari hal yang paling kritikal: backup yang teruji, health check yang bekerja, runbook untuk skenario paling mungkin.
  • Backup yang tidak pernah di-restore bukan backup — jadwalkan tes restore secara berkala, setidaknya sebulan sekali untuk sistem kritis.
  • Capture baseline state sebelum perubahan besar — snapshot konfigurasi dan service state yang bisa dibandingkan setelah perubahan untuk mendeteksi regresi.
  • Konfirmasi eksplisit untuk operasi destruktif (restore, rollback, drop database) — tambahkan assert yang memaksa operator mengetik string tertentu untuk mencegah eksekusi tidak disengaja.
  • Disaster recovery playbook harus ada bahkan sebelum disaster terjadi — menulis runbook saat sedang dalam krisis adalah cara yang paling buruk untuk menulis runbook.
  • Log semua operasi kritis (deployment, restore, rollback) dengan timestamp dan executor — audit trail ini tidak ternilai saat post-mortem.

← Sebelumnya: Team Workflow   Berikutnya: Lessons Learned →

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