Deployment zero-downtime

Disiplin migration dan deploy bergulir tanpa mematikan layanan.

Daftar isi

Rilis yang menghentikan aplikasi lebih dahulu memutus layanan selama prosesnya berjalan, sedangkan zero-downtime deployment menjaga aplikasi tetap merespons selama versi baru menggantikan versi lama. Topik ini membahas dua pendekatan untuk mencapainya pada aplikasi catatan, yaitu mode pemeliharaan sebagai langkah paling sederhana dan deploy bergulir (rolling deploy) yang memperbarui instance satu per satu, beserta aturan migration yang memungkinkan deploy bergulir berjalan. Setelah topik ini dibaca, sebuah migration dapat dinilai apakah aman dijalankan ketika aplikasi masih melayani permintaan.

Mode pemeliharaan#

Cara paling sederhana untuk menjembatani masa pergantian versi adalah mode pemeliharaan (maintenance mode). Perintah php artisan down menolak setiap permintaan web dengan respons 503, sedangkan php artisan up membuka aplikasi kembali. Selama mode aktif, middleware memeriksa keberadaan berkas penanda pada storage/framework di setiap permintaan, sehingga tidak ada proses lain yang perlu dijalankan ulang ketika mode diaktifkan.

bash
# tidak-dijalankan
# menutup lalu membuka kembali aplikasi dengan mode pemeliharaan
cd ~/aplikasi-catatan
php artisan down
php artisan up

Keluaran berikut diambil dari eksekusi nyata pasangan perintah di atas, yang dilakukan dalam waktu kurang dari satu detik.

text Hasil perintah php artisan down lalu up
   INFO  Application is now in maintenance mode.

   INFO  Application is now live.

Perintah down menerima beberapa opsi yang mengurangi dampak mode ini. Opsi --secret=perbaikan-singkat memberi pemilik aplikasi pengecualian berupa cookie yang diperoleh dengan mengunjungi URL https://domain/perbaikan-singkat sekali, sehingga pemeliharaan dapat dikerjakan sambil melihat aplikasi. Opsi --retry=60 menyarankan perayap web untuk kembali setelah enam puluh detik, dan tampilan yang dilihat pengguna dapat diganti melalui templat resources/views/errors/503.blade.php.

Kelemahan mode pemeliharaan terletak pada periode downtime, yaitu waktu ketika pengguna melihat halaman penolakan alih-alih aplikasi. Untuk aplikasi catatan yang digunakan satu tim kecil, periode sepuluh detik mungkin masih dapat diterima, sedangkan aplikasi yang digunakan sepanjang hari tidak dapat menutup layanan sedetik pun. Deploy bergulir menghilangkan periode tersebut dengan syarat migration pada rilis mengikuti aturan di bagian berikutnya.

Deploy bergulir satu per satu#

Deploy bergulir menjalankan beberapa instance aplikasi yang identik di balik satu load balancer. Instance diperbarui satu per satu, sedangkan sisanya terus melayani permintaan, sehingga kapasitas layanan hanya berkurang sepertiga pada tiga instance, bukan hilang seluruhnya. Diagram berikut menunjukkan keadaan di tengah proses tersebut.

load balancermengalihkan trafikinstance 1versi lamamelayaniinstance 2versi lamamelayaniinstance 3versi barusedang diperbaruiInstance diperbarui satu per satu, sedangkan sisanya terus melayani permintaan.
Keadaan di tengah deploy bergulir tiga instance
Load balancer mengalihkan trafik ke instance versi baru setelah pemeriksaan kesehatan instance itu lolos, misalnya ketika endpoint kesehatan yang disediakan aplikasi kembali merespons 200. Instance versi lama tidak dihentikan mendadak, melainkan ditarik dari daftar tujuan load balancer lebih dahulu agar permintaan yang sedang berjalan selesai dengan normal. Setelah instance terakhir diperbarui, seluruh trafik mengalir ke versi baru tanpa satu pun permintaan yang ditolak.

Urutan langkah pada setiap instance dipadatkan dalam satu skrip agar setiap rilis menjalankan hal yang sama dalam urutan yang sama. Skrip berikut menggabungkan langkah-langkah yang dipraktikkan pada topik Daftar periksa deployment. Setiap tahapnya diberi nomor pada komentar agar urutannya mudah diikuti saat pemeriksaan rilis.

bash deploy.sh
# tidak-dijalankan
# Urutan rilis untuk satu instance aplikasi catatan.
# Sesuaikan DIREKTORI dengan lokasi proyek pada server.

set -eu

DIREKTORI=/var/www/aplikasi-catatan
cd "$DIREKTORI"

# 1. ambil kode versi terbaru dari repository
git pull origin main

# 2. pasang dependensi tanpa paket pengembangan
composer install --no-interaction --prefer-dist --optimize-autoloader --no-dev

# 3. terapkan migration secara paksa
php artisan migrate --force

# 4. bangun ulang seluruh cache aplikasi
php artisan optimize

# 5. tandai worker agar memuat kode baru setelah job berjalan selesai
php artisan queue:restart

Urutan pada skrip itu menentukan keberhasilan rilis. Kode ditarik dan dependensinya dipasang lebih dahulu, kemudian migrate --force dijalankan sebelum cache dibangun agar konfigurasi yang baru ikut ter-cache. queue:restart menjadi langkah penutup karena worker memuat kode ke memori ketika prosesnya dimulai, sehingga worker yang tidak dimulai ulang terus mengeksekusi versi lama, sedangkan aplikasi web sudah beralih ke versi baru.

Migration yang aman saat aplikasi berjalan#

Pada deploy bergulir, ada periode ketika instance versi lama dan versi baru berjalan bersamaan di atas skema database yang sama. Versi lama tidak mengenal kolom baru, sedangkan versi baru membutuhkannya. Satu-satunya migration yang aman karenanya bersifat tambahan, yaitu menambah kolom atau tabel tanpa mengubah dan menghapus yang sudah digunakan. Pola ini dikenal sebagai expand dan contract: skema diperluas pada rilis ini, sedangkan penghapusannya ditunda ke rilis berikutnya setelah kode lama benar-benar tidak berjalan lagi.

Kolom arsip pada tabel notes menjadi contoh yang aman. Migration berikut hanya menambahkan kolom baru dengan nilai default, sehingga versi lama yang tidak mengenalnya tetap berjalan, sedangkan versi baru mulai mengisinya.

php 2026_11_04_000000_add_arsip_column_to_notes_table.php
// tidak-dijalankan
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Menambahkan kolom baru tanpa mengubah kolom yang sudah ada.
     */
    public function up(): void
    {
        Schema::table('notes', function (Blueprint $table) {
            $table->boolean('arsip')->default(false);
        });
    }

    /**
     * Mengembalikan skema ke keadaan sebelum kolom ditambahkan.
     */
    public function down(): void
    {
        Schema::table('notes', function (Blueprint $table) {
            $table->dropColumn('arsip');
        });
    }
};

Siklus lengkapnya dipraktikkan dengan menempatkan migration di atas pada database/migrations, menerapkannya, memeriksa status, lalu mengembalikannya untuk membersihkan percobaan. Nama berkas pada contoh dibiarkan apa adanya karena urutan eksekusi migration mengikuti urutan tanggal pada namanya. Seluruh siklus selesai hanya dalam beberapa detik pada proyek uji.

bash
# tidak-dijalankan
# menerapkan migration tambahan, memeriksa status, lalu mengembalikannya
cd ~/aplikasi-catatan
php artisan migrate --force
php artisan migrate:status
php artisan migrate:rollback --step=1

Keluaran berikut diambil dari eksekusi nyata rangkaian di atas, ketika berkas migration masih berada di proyek uji.

text Hasil perintah migrate, migrate:status, dan rollback
   INFO  Running migrations.

  2026_11_04_000000_add_arsip_column_to_notes_table .............. 1.63ms DONE

  Migration name .............................................. Batch / Status
  0001_01_01_000000_create_users_table ............................... [1] Ran
  0001_01_01_000001_create_cache_table ............................... [1] Ran
  0001_01_01_000002_create_jobs_table ................................ [1] Ran
  2026_11_03_003023_create_categories_table .......................... [1] Ran
  2026_11_03_003024_create_notes_table ............................... [1] Ran
  2026_11_03_085521_create_notifications_table ....................... [1] Ran
  2026_11_03_152518_create_personal_access_tokens_table .............. [2] Ran
  2026_11_04_000000_add_arsip_column_to_notes_table .................. [3] Ran

   INFO  Rolling back migrations.

  2026_11_04_000000_add_arsip_column_to_notes_table .............. 3.46ms DONE

Keluaran tersebut memperlihatkan tiga keadaan sekaligus. Perintah migrate menambahkan kolom tanpa menyentuh data yang ada, migrate:status mencatatnya sebagai batch ketiga di atas tujuh migration sebelumnya, dan rollback mengembalikan skema ke keadaan awal untuk membersihkan percobaan. Pada rilis sungguhan, langkah rollback tidak dijalankan, melainkan kolom tersebut dibiarkan ada sampai tahap contract pada rilis berikutnya.

Migration yang aman belum menyelesaikan seluruh masalah, karena worker versi lama masih memegang kode lama di memori setelah skema berubah. Selama worker belum dimulai ulang, job tetap dieksekusi dengan definisi kelas versi sebelumnya. Perintah queue:restart menandai seluruh worker agar berhenti tepat setelah job yang sedang berjalan selesai, kemudian supervisor memulainya kembali dengan kode versi baru.

bash
# menandai worker agar dimulai ulang setelah job berjalan selesai
cd ~/aplikasi-catatan
php artisan queue:restart
text Keluaran
   INFO  Broadcasting queue restart signal.

Kesalahan umum#

Kesalahan pertama adalah menggabungkan perubahan destruktif dengan rilis kode. Menjalankan renameColumn pada rilis yang sama ketika kode baru mulai digunakan membuat instance versi lama gagal dengan error kolom tidak ditemukan, karena query pada kode lama masih menunjuk nama kolom yang sudah berganti. Perbaikannya adalah memecah perubahan itu menjadi dua rilis, yaitu menambah kolom baru dan menulis data ke keduanya pada rilis pertama, kemudian menghapus kolom lama pada rilis berikutnya setelah versi lama tidak berjalan lagi.

Kesalahan kedua adalah menjalankan migration dari setiap instance secara bersamaan. Beberapa proses migrate yang berjalan hampir bersamaan dapat menerapkan migration yang sama dua kali atau gagal karena mengunci tabel yang sama, dan keadaan tabel migrations pun menjadi tidak konsisten antar-instance. Migration dijalankan tepat satu kali dari satu instance, misalnya instance pertama yang selesai diperbarui, sementara instance lainnya melewatinya.

Kesalahan ketiga adalah melewatkan queue:restart pada akhir rilis. Aplikasi web sudah melayani versi baru, tetapi worker terus mengeksekusi kode lama dari memori, sehingga job yang gagal pada versi baru tetap direproduksi oleh worker lama dan gejalanya sulit dihubungkan dengan rilis. Menjadikan queue:restart langkah terakhir yang tetap ada pada skrip, sebagaimana deploy.sh di atas, menutup celah ini secara permanen.

Ringkasan#

  • Mode pemeliharaan menolak permintaan web dengan respons 503 dan membukanya kembali dengan php artisan up, tetapi tidak menghentikan worker maupun cron.
  • Deploy bergulir memperbarui instance satu per satu di balik load balancer, sehingga tidak ada periode downtime selama pemeriksaan kesehatan tiap instance lolos.
  • Migration yang aman untuk layanan yang sedang berjalan bersifat tambahan, sedangkan perubahan destruktif ditunda ke rilis berikutnya melalui pola expand dan contract.
  • queue:restart menutup setiap rilis karena worker memuat kode ke memori dan hanya memuat versi baru setelah prosesnya dimulai ulang.

Referensi#