Deployment yang sehat terasa membosankan: perintah yang sama, langkah yang sama, hasil yang bisa diprediksi, setiap kali dijalankan. Kesannya berlawanan dengan intuisi, tetapi justru kebosanan itulah tujuannya — kejutan hanya boleh datang dari perubahan kode, bukan dari proses penyalinannya. Pada chapter ini kita bedah anatomi workflow deployment, membandingkan pendekatan berbasis artefak dan berbasis git, lalu membangun mekanisme release berbasis symlink yang atomik dan bisa di-rollback dalam hitungan detik.
Anatomi Workflow Deployment #
Workflow deployment adalah rantai tahap yang saling bergantung; setiap tahap hanya berjalan bila tahap sebelumnya lulus. Menggabungkan semuanya menjadi satu skrip raksasa tanpa gerbang membuat kegagalan di tahap akhir merusak apa yang sudah dibangun di tahap awal.
Diagram di atas memperlihatkan alurnya: build menghasilkan artefak, test memvalidasi artefak tersebut, package menyiapkannya untuk distribusi, release mengaktifkannya di server, verify memastikan rilis sehat, dan monitor mengawasinya setelah trafik nyata mengalir. Kegagalan di tahap mana pun menghentikan rantai sebelum menyentuh produksi.
Peran tiap tahap dirangkum pada tabel berikut:
| Tahap | Masukan | Keluaran | Jika gagal |
|---|---|---|---|
| Build | Source code | Artefak (arsip rilis atau image) | Perbaiki kode, ulangi dari awal |
| Test | Artefak hasil build | Laporan PHPUnit, analisis statis | Deploy dibatalkan |
| Package | Artefak teruji | Image final / arsip siap distribusi | Build ulang |
| Release | Artepak final | Rilis baru aktif di server | Rollback otomatis |
| Verify | Rilis aktif | Status health check, smoke test | Rollback dan alert |
| Monitor | Trafik produksi | Error rate, latensi | Rollback atau investigasi |
Berbasis Artefak versus Berbasis Git #
Ada dua filosofi tentang apa yang dideploy. Pendekatan berbasis artefak membangun sekali lalu menyebarkan hasil yang sama ke semua server; pendekatan berbasis git menarik kode terbaru ke server lalu membangun di sana.
| Aspek | Berbasis artefak | Berbasis git |
|---|---|---|
| Yang dideploy | Artefak hasil build sekali di CI | Repo di-pull, composer install di server |
| Konsistensi antar server | Identik — build once, deploy many | Bisa berbeda bila pull terjadi di commit berbeda |
| Beban build di server produksi | Tidak ada | Ada (Composer, kompilasi asset) |
| Cocok untuk | Tim dengan pipeline CI/CD | Proyek kecil dengan satu server |
Prinsip build once, deploy many adalah alasan utama pendekatan artefak: artefak yang lulus test adalah benda yang sama persis dengan yang berjalan di produksi, sehingga tidak ada ruang bagi perbedaan versi dependensi antara mesin CI dan server. Pendekatan git lebih sederhana untuk mulai, tetapi seiring pertumbuhan jumlah server, menjaga hasil build agar tetap seragam menjadi beban tersendiri.
Struktur Direktori Release #
Mekanisme release yang atomik bertumpu pada satu ide sederhana: setiap rilis adalah direktori terpisah yang tidak pernah diubah setelah dibuat, dan sebuah symlink current menunjuk ke rilis yang sedang aktif. Struktur di server terlihat seperti ini:
/var/www/toko/
├── releases/
│ ├── 20260115120000/
│ └── 20260116090000/
├── current -> releases/20260116090000
└── shared/
└── storage/Direktori releases/ berisi rilis bernama timestamp, current adalah symlink ke rilis aktif, dan shared/ menampung data yang hidup lintas rilis seperti unggahan dan log. Nginx atau PHP-FPM dikonfigurasi membaca lewat path current, sehingga berganti rilis cukup memindahkan tujuan symlink. Mari kita simulasikan pembuatan struktur dan pergantian symlink dalam PHP:
<?php
function siapkanStrukturRelease(string $deployDir, string $releaseId): string
{
$releaseDir = $deployDir . "/releases/" . $releaseId;
if (!is_dir($releaseDir)) {
mkdir($releaseDir, 0755, true);
}
return $releaseDir;
}
function aktifkanRelease(string $deployDir, string $releaseId): string
{
$sementara = $deployDir . "/.current-baru";
$tujuan = $deployDir . "/current";
if (is_link($sementara)) {
unlink($sementara);
}
// symlink baru dibuat dengan nama sementara ...
symlink("releases/" . $releaseId, $sementara);
// ... lalu di-rename agar pergantian bersifat atomik
rename($sementara, $tujuan);
return readlink($tujuan);
}
function bersihkanDemo(string $deployDir): void
{
if (is_link($deployDir . "/current")) {
unlink($deployDir . "/current");
}
if (is_link($deployDir . "/.current-baru")) {
unlink($deployDir . "/.current-baru");
}
foreach (glob($deployDir . "/releases/*") ?: [] as $path) {
is_dir($path) ? rmdir($path) : unlink($path);
}
if (is_dir($deployDir . "/releases")) {
rmdir($deployDir . "/releases");
}
if (is_dir($deployDir)) {
rmdir($deployDir);
}
}
$deployDir = "/tmp/demo-deploy";
if (is_dir($deployDir)) {
bersihkanDemo($deployDir);
}
siapkanStrukturRelease($deployDir, "20260115120000");
siapkanStrukturRelease($deployDir, "20260116090000");
echo "current -> " . aktifkanRelease($deployDir, "20260115120000") . "\n";
echo "current -> " . aktifkanRelease($deployDir, "20260116090000") . "\n";
echo "rollback, current -> " . aktifkanRelease($deployDir, "20260115120000") . "\n";
bersihkanDemo($deployDir);
echo "demo dibersihkan: " . var_export(is_dir($deployDir), true) . "\n";Output yang Dihasilkan:
current -> releases/20260115120000
current -> releases/20260116090000
rollback, current -> releases/20260115120000
demo dibersihkan: falseMari kita bedah bagian intinya. siapkanStrukturRelease() membuat direktori rilis baru dengan mkdir() rekursif — pada deployment nyata, langkah ini diisi ekstraksi artefak. aktifkanRelease() tidak menghapus symlink lama lalu membuat yang baru, karena ada jeda microsecond ketika current tidak ada dan request bisa gagal. Sebagai gantinya, symlink baru dibuat dengan nama sementara .current-baru, lalu rename() menggantikan current dalam satu operasi atomik di level filesystem — pembaca tidak pernah melihat keadaan setengah jadi. bersihkanDemo() hanya membersihkan simulasi di /tmp agar tidak meninggalkan sampah.
Rollback Cepat dengan Symlink #
Rollback pada pola ini tidak membangun ulang apa pun: direktori rilis lama masih utuh di disk, jadi mengembalikannya hanya soal memindahkan symlink kembali. Untuk menentukan kembali ke mana, kita cukup mengurutkan daftar rilis dan mengambil yang berada tepat sebelum rilis saat ini:
<?php
function releaseSebelumnya(array $semuaRelease, string $saatIni): ?string
{
$urut = $semuaRelease;
sort($urut);
$posisi = array_search($saatIni, $urut, true);
if ($posisi === false || $posisi === 0) {
return null;
}
return $urut[$posisi - 1];
}
$semua = ["20260118103000", "20260115120000", "20260116090000"];
var_dump(releaseSebelumnya($semua, "20260118103000"));
var_dump(releaseSebelumnya($semua, "20260115120000"));Output yang Dihasilkan:
string(14) "20260116090000"
NULLKarena nama rilis berformat YYYYMMDDHHMMSS, urutan leksikal sama dengan urutan waktu, sehingga sort() cukup untuk mengurutkan riwayat. Bila rilis saat ini adalah yang terlama (20260115120000) atau tidak ditemukan, fungsi mengembalikan null — sinyal bahwa tidak ada tujuan rollback otomatis dan manusia harus memutuskan. Pada server nyata, daftar rilis didapat dari scandir() pada direktori releases/ setelah menyaring entri . dan ...
Skrip Deploy Sederhana #
Rangkaian di atas dirangkai menjadi satu skrip bash yang menjalankan tahap release lengkap. Skrip ini adalah blok display — pada proyek nyata ia berjalan di server melalui SSH dari pipeline CI Anda:
#!/usr/bin/env bash
set -euo pipefail
APP_DIR="/var/www/toko"
RELEASE_ID="$(date +%Y%m%d%H%M%S)"
RELEASE_DIR="${APP_DIR}/releases/${RELEASE_ID}"
RELEASE_SEBELUMNYA="$(basename "$(readlink "${APP_DIR}/current")")"
curl -sfL "https://ci.example.com/artifacts/toko-${RELEASE_ID}.tar.gz" -o /tmp/release.tar.gz
mkdir -p "${RELEASE_DIR}"
tar -xzf /tmp/release.tar.gz -C "${RELEASE_DIR}"
ln -sfn "releases/${RELEASE_ID}" "${APP_DIR}/.current-baru"
mv -T "${APP_DIR}/.current-baru" "${APP_DIR}/current"
if ! curl -sf "http://127.0.0.1:8080/healthz" > /dev/null; then
echo "health check gagal, rollback ke ${RELEASE_SEBELUMNYA}" >&2
ln -sfn "releases/${RELEASE_SEBELUMNYA}" "${APP_DIR}/.current-baru"
mv -T "${APP_DIR}/.current-baru" "${APP_DIR}/current"
exit 1
fi
echo "Rilis ${RELEASE_ID} aktif."Baris set -euo pipefail membuat skrip berhenti pada perintah pertama yang gagal, pada variable yang tidak terisi, atau pada pipeline yang putus di tengah — tanpa ini, kegagalan unduh bisa berlanjut ke ekstraksi arsip kosong. Urutan langkahnya meniru anatomi workflow dari awal hingga akhir. Sebelum menyentuh apa pun, skrip mencatat rilis yang sedang aktif ke RELEASE_SEBELUMNYA lewat readlink. Artefak yang sudah lulus test di CI diunduh dan diekstrak ke direktori rilis baru — bukan ke direktori aktif. Rilis kemudian diaktifkan lewat pasangan ln -sfn dan mv -T yang mereplikasi pola symlink sementara plus rename, dan tahap terakhir adalah verifikasi: health check dipanggil ke endpoint lokal.
Pemeriksaan Pasca-Deploy #
Tahap verify dimulai sebelum skrip mengaku selesai. Minimal ada tiga pemeriksaan yang layak dijalankan otomatis. Pertama, health check pada endpoint lokal — contoh skrip di atas memanggil /healthz dan menganggap kode HTTP non-2xx sebagai kegagalan. Kedua, smoke test satu-dua route penting (login, halaman utama) untuk memastikan routing dan dependensi hidup. Ketiga, pengamatan error rate beberapa menit pertama setelah rilis, karena sebagian bug hanya muncul di bawah trafik nyata.
Pemeriksaan ini hanya berguna bila hasilnya menentukan tindakan. Health check gagal harus memicu rollback otomatis seperti pada skrip di atas, bukan sekadar mencetak pesan. Desain endpoint /healthz itu sendiri — apa yang diperiksa, kode status yang dikembalikan, dan bagaimana load balancer membacanya — kita bahas khusus pada chapter Health Checks
.
Ringkasan #
Workflow deployment yang sehat memecah proses menjadi tahap bergerbang: build, test, package, release, verify, monitor. Pendekatan berbasis artefak menjaga apa yang dideploy identik dengan apa yang diuji, struktur releases/ plus symlink current membuat aktivasi rilis atomik, dan rollback cukup membalik arah symlink ke direktori rilis lama yang masih utuh. Satu tahap dalam workflow ini menuntut perhatian khusus karena menyangkut data, bukan sekadar kode: migrasi database. Urutan amannya — pola expand-contract, cadangan, dan pemulihan kegagalan — kita bahas pada chapter Database Deployment
.