Rollback Strategy

Rollback Strategy #

Deployment yang tidak bisa di-rollback adalah deployment yang belum siap untuk production. Tidak peduli seberapa banyak testing yang dilakukan, selalu ada kemungkinan masalah yang hanya muncul saat kode berjalan di production dengan traffic nyata. Kemampuan untuk kembali ke versi yang bekerja dengan cepat dan andal adalah properti sistem yang tidak boleh menjadi afterthought — ia harus dirancang dari awal. Artikel ini membahas strategi rollback yang efektif menggunakan Ansible.

Menyimpan State untuk Rollback #

Langkah pertama rollback yang efektif adalah memastikan informasi yang diperlukan tersedia:

# playbooks/deploy.yml
---
- name: Deploy dengan kemampuan rollback
  hosts: appservers
  vars:
    deploy_version: "{{ version | mandatory }}"

  pre_tasks:
    - name: Catat versi yang sedang berjalan
      command: cat /opt/app/VERSION
      register: current_version_file
      changed_when: false
      ignore_errors: true

    - name: Simpan versi saat ini ke variabel rollback
      set_fact:
        rollback_version: "{{ current_version_file.stdout | default('unknown') | trim }}"

    - name: Simpan rollback info ke file
      copy:
        content: |
          version={{ rollback_version }}
          deployed_at={{ ansible_date_time.iso8601 }}
          deployed_by={{ lookup('env', 'USER') | default('ci-pipeline') }}          
        dest: /opt/app/PREVIOUS_VERSION
        mode: '0644'

    - name: Log deployment dimulai
      lineinfile:
        path: /var/log/deployments.log
        line: "{{ ansible_date_time.iso8601 }} START v{{ deploy_version }} (rollback: v{{ rollback_version }}) @ {{ inventory_hostname }}"
        create: true
      delegate_to: localhost

Rollback Otomatis Saat Health Check Gagal #

  tasks:
    - block:
        - name: Deploy versi baru
          git:
            repo: https://github.com/company/app.git
            dest: /opt/app
            version: "v{{ deploy_version }}"

        - name: Install dependencies
          pip:
            requirements: /opt/app/requirements.txt
            virtualenv: /opt/app/venv

        - name: Restart aplikasi
          systemd:
            name: myapp
            state: restarted

        - name: Tunggu aplikasi siap (health check)
          uri:
            url: "http://localhost:{{ app_port }}/health"
            status_code: 200
          register: health_check
          until: health_check.status == 200
          retries: 12
          delay: 10

        - name: Verifikasi versi yang berjalan
          uri:
            url: "http://localhost:{{ app_port }}/api/version"
            return_content: true
          register: version_check
          failed_when: deploy_version not in version_check.content

        - name: Tulis VERSION file baru
          copy:
            content: "{{ deploy_version }}\n"
            dest: /opt/app/VERSION

      rescue:
        - name: Health check gagal — memulai rollback otomatis
          debug:
            msg: >
              Deployment v{{ deploy_version }} gagal di {{ inventory_hostname }}.
              Melakukan rollback ke v{{ rollback_version }}.              

        - name: Rollback ke versi sebelumnya
          git:
            repo: https://github.com/company/app.git
            dest: /opt/app
            version: "v{{ rollback_version }}"
          when: rollback_version != 'unknown'

        - name: Restart dengan versi lama
          systemd:
            name: myapp
            state: restarted
          when: rollback_version != 'unknown'

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

        - name: Gagalkan play untuk memberitahu pipeline
          fail:
            msg: >
              Deployment v{{ deploy_version }} gagal dan rollback ke
              v{{ rollback_version }} sudah dilakukan. Periksa log untuk detail.              

      always:
        - name: Log hasil deployment
          lineinfile:
            path: /var/log/deployments.log
            line: >
              {{ ansible_date_time.iso8601 }}
              {{ 'ROLLBACK' if ansible_failed_result is defined else 'SUCCESS' }}
              v{{ deploy_version }} @ {{ inventory_hostname }}              
            create: true
          delegate_to: localhost

Rollback Manual via Pipeline #

Buat Job Template atau workflow khusus untuk rollback manual:

# playbooks/rollback.yml
---
- name: Rollback deployment manual
  hosts: appservers
  vars:
    # target_version harus dipass: -e target_version=2.0.5
    target_version: "{{ target_version | mandatory }}"

  pre_tasks:
    - name: Konfirmasi versi yang akan di-rollback-ke tersedia di registry
      command: "docker manifest inspect registry.company.com/myapp:{{ target_version }}"
      delegate_to: localhost
      changed_when: false

    - name: Catat versi yang sedang berjalan (sebelum rollback)
      command: cat /opt/app/VERSION
      register: pre_rollback_version
      changed_when: false
      ignore_errors: true

  tasks:
    - name: Log rollback dimulai
      debug:
        msg: >
          Rollback dari v{{ pre_rollback_version.stdout | default('unknown') }}
          ke v{{ target_version }}          

    - name: Pull image target rollback
      community.docker.docker_image:
        name: "registry.company.com/myapp:{{ target_version }}"
        source: pull

    - name: Jalankan container versi rollback
      community.docker.docker_container:
        name: myapp
        image: "registry.company.com/myapp:{{ target_version }}"
        state: started
        restart_policy: unless-stopped
        recreate: true

    - name: Tunggu aplikasi siap
      uri:
        url: "http://localhost:{{ app_port }}/health"
        status_code: 200
      retries: 12
      delay: 5

    - name: Update VERSION file
      copy:
        content: "{{ target_version }}\n"
        dest: /opt/app/VERSION

    - name: Log rollback selesai
      debug:
        msg: "Rollback berhasil ke v{{ target_version }}"

Rollback Database: Masalah yang Berbeda #

Rollback kode mudah. Rollback database schema jauh lebih kompleks:

# Prinsip untuk database yang bisa di-rollback:
#
# 1. Backward-compatible migrations — kode lama harus bisa berjalan
#    dengan schema baru SEBELUM rollback dilakukan
#
# 2. Pisahkan migrasi destructive — hapus kolom/tabel di migration terpisah,
#    bukan bersamaan dengan tambah kolom baru
#
# 3. Expand-Contract pattern:
#    Fase 1 (Expand): Tambah kolom baru, deploy kode baru yang menulis ke KEDUA kolom
#    Fase 2 (Contract): Hapus kolom lama setelah verifikasi tidak ada yang butuh

# Jika migrasi down tersedia (Django, Alembic, dll.):
- name: Rollback database migration
  command: "python manage.py migrate {{ app_name }} {{ target_migration }}"
  args:
    chdir: /opt/app
  when:
    - rollback_db | default(false) | bool
    - target_migration is defined

Blue-Green Deployment untuk Rollback Instan #

Blue-green adalah strategi deployment yang memungkinkan rollback dengan mengganti pointer, bukan mengulang deployment:

# playbooks/blue-green-deploy.yml
---
- name: Blue-Green Deployment
  hosts: loadbalancer
  vars:
    current_slot: "{{ lookup('file', '/etc/app/active-slot') | default('blue') }}"
    new_slot: "{{ 'green' if current_slot == 'blue' else 'blue' }}"

  tasks:
    - name: Deploy ke slot yang tidak aktif ({{ new_slot }})
      include_tasks: deploy-to-slot.yml
      vars:
        slot: "{{ new_slot }}"

    - name: Verifikasi slot baru sehat
      uri:
        url: "http://{{ new_slot }}.internal:{{ app_port }}/health"
        status_code: 200
      retries: 12
      delay: 5

    - name: Switch traffic ke slot baru
      template:
        src: nginx-upstream.conf.j2
        dest: /etc/nginx/conf.d/upstream.conf
      vars:
        active_slot: "{{ new_slot }}"
      notify: Reload nginx

    - name: Tunggu reload nginx
      meta: flush_handlers

    - name: Simpan slot aktif
      copy:
        content: "{{ new_slot }}\n"
        dest: /etc/app/active-slot

    - name: Instruksi rollback instan jika diperlukan
      debug:
        msg: >
          Deployment berhasil ke slot {{ new_slot }}.
          Untuk rollback instan: ansible-playbook rollback-blue-green.yml
          (mengembalikan traffic ke slot {{ current_slot }} tanpa re-deploy)          

Ringkasan #

  • Selalu catat versi yang berjalan sebelum deployment dimulai — ini adalah prasyarat untuk rollback yang andal.
  • block/rescue untuk rollback otomatis saat health check gagal — jangan biarkan deployment yang gagal membiarkan sistem dalam kondisi setengah jalan.
  • Buat playbook rollback terpisah yang bisa dipicu dari pipeline atau manual — rollback tidak boleh memerlukan kemampuan teknis khusus saat terjadi insiden.
  • Rollback database adalah masalah terpisah dari rollback kode — gunakan expand-contract pattern untuk migrasi yang aman di-rollback.
  • Blue-green deployment memungkinkan rollback instan dengan switch pointer, bukan re-deploy — ideal untuk sistem yang tidak bisa toleransi downtime rollback.
  • Log setiap deployment dan rollback dengan timestamp dan versi — audit trail yang lengkap sangat berharga saat investigasi insiden.

← Sebelumnya: Environment Management   Berikutnya: Artifact Management →

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