Deploy pertama biasanya terasa mudah: salin beberapa berkas, restart service, selesai dalam lima menit. Masalah baru terasa pada deploy kedua puluh — satu langkah terlewat, satu berkas tercampur karena disalin saat aplikasi masih berjalan, dan tiga server diam-diam menjalankan tiga versi berbeda. Chapter ini merancang alur deployment yang dijalankan oleh mesin, bukan oleh ingatan manusia: build, transfer, activate, verify, dan rollback. Semuanya kita praktikkan dengan skrip yang menjalankan pipeline sungguhan di sandbox, memakai aplikasi contoh app-toko dan strategi rilis berbasis symlink dari Chapter Symbolic Links dan Hard Links .

Kenapa Deploy Manual Selalu Berujung Kecelakaan #

Deployment manual bergantung pada dua hal yang rapuh: ingatan admin dan konsistensi tangan. Ingatan terhadap lima belas langkah akan goyah pukul sebelas malam, dan salin-menempel manual menghasilkan server snowflake — server yang tampak sama tetapi berperilaku berbeda. Tabel berikut merangkum kecelakaan yang paling sering terjadi:

Kecelakaan deploy manualAkibat di lapangan
berkas disalin langsung ke folder aplikasi yang sedang jalanpengguna menerima campuran dua versi selama proses salin
migrasi database terlupa dijalankanaplikasi baru membaca kolom yang belum dibuat
urutan langkah berbeda antar admindua server “identik” berperilaku berbeda
tidak ada catatan versi yang terpasangsaat insiden, tidak ada yang tahu kode mana yang berjalan
rollback dilakukan sambil panikkesalahan baru ditumpuk di atas kesalahan lama

Karena itu deployment yang sehat punya tiga sifat: berulang (hasil sama siapa pun yang menjalankan), cepat dibatalkan (menit, bukan jam), dan berjejak (setiap rilis bisa ditunjuk foldernya). Ketiganya kita bangun bertahap di chapter ini.

Enam Tahap Pipeline Deployment #

Pipeline adalah urutan tahap yang sama setiap kali deploy dijalankan, tanpa tergantung siapa yang memencet tombolnya:

TahapIsi pekerjaanKalau tahap ini gagal
buildhasilkan artifact dari kode sumbertidak ada yang dikirim, server lama tetap aman
testuji artifact (unit, smoke)versi cacat tertahan sebelum menyentuh server
uploadkirim artifact ke server, belum diaktifkanserver tidak berubah sama sekali
activatealihkan traffic ke versi baruinilah satu-satunya momen berisiko
verifyhealth check pasca-activatememicu rollback
rollbackkembali ke versi sebelumnyatersedia sebagai jaring pengaman

Ada satu prinsip penting di tabel itu: artifact yang diuji adalah artifact yang dipasang. Kita membangun sekali, lalu memindahkan berkas yang sama ke semua server — bukan membangun ulang di tiap server, karena hasil build bisa berbeda antar mesin. Tahap activate sengaja dipisah dari upload supaya ada jeda yang aman: artifact sudah utuh di server sebelum pengguna dialihkan.

Tiga Strategi Aktivasi Rilis #

Activate adalah momen paling kritis — detik ketika pengguna berpindah dari versi lama ke versi baru. Ada tiga strategi utama:

StrategiCara kerjaKebutuhanKecepatan rollback
blue-greendua environment utuh; load balancer memilih yang aktifserver dua kali lipatsecepat switch balik
rollingserver diganti satu per satu dari dalam clustercluster dengan beberapa serverpelan: mundur satu-satu
symlink releasetiap versi punya folder; symlink current dipindahsatu server sajasecepat membalik symlink

Blue-green paling boros tetapi paling aman: environment biru tetap melayani selama hijau disiapkan, dan berpindah kembali hanya satu konfigurasi load balancer. Rolling hemat karena tidak ada server menganggur, cocok untuk cluster web yang banyak. Symlink release — pola yang dipopulerkan Capistrano di dunia Ruby — paling sederhana dan menjadi fokus latihan kita, karena prinsipnya bisa diterapkan di satu server tanpa perangkat tambahan.

Strategi ini menyimpan setiap versi sebagai folder bernomor di bawah releases/, lalu satu symlink bernama current menunjuk versi yang sedang aktif:

text
app-toko/
├── releases/
│   ├── 1/
│   │   └── app.txt
│   └── 2/
│       └── app.txt
└── current -> releases/2

Web server atau systemd service hanya pernah membaca lewat current — tidak pernah menyebut nomor versi secara langsung. Kita bangun strukturnya langkah demi langkah:

bash
mkdir -p app-toko/releases/1
printf 'app-toko versi 1\n' > app-toko/releases/1/app.txt
ln -sfn releases/1 app-toko/current
readlink app-toko/current
cat app-toko/current/app.txt

Output yang Dihasilkan:

text
releases/1
app-toko versi 1

Mari kita bedah baris demi baris:

  1. Baris 1 (mkdir -p app-toko/releases/1): folder rilis pertama dibuat. Rilis berikutnya tinggal menambah folder releases/2, releases/3, dan seterusnya — isi rilis lama tidak pernah disentuh.
  2. Baris 2 (printf ... > app-toko/releases/1/app.txt): menulis “artifact” versi 1. Di pipeline nyata, berkas ini adalah hasil build yang sudah lulus tahap test — bukan salinan acak dari laptop developer.
  3. Baris 3 (ln -sfn releases/1 app-toko/current): opsi -s membuat symlink, -f menimpa bila current sudah ada, dan -n memperlakukan current sebagai berkas biasa — tanpa -n, ln bisa salah sasaran dengan menulis ke dalam folder tujuan symlink lama. Target ditulis relatif terhadap lokasi current, bukan direktori kerja kita.
  4. Baris 4–5 (readlink dan cat): tahap verify — symlink harus menunjuk releases/1, dan membaca lewat current harus menghasilkan versi yang benar sebelum kita percaya rilis ini aktif.

Satu Skrip untuk Satu Siklus Penuh #

Bundle chapter ini membawa skrip deploy-app.sh yang merangkai tahap build → upload → activate → verify dalam satu perintah, plus subperintah rollback untuk jaring pengamannya. Kita jalankan satu siklus lengkap: dua rilis sukses, lalu simulasi health check versi 2 yang gagal:

bash
bash deploy-app.sh 1
bash deploy-app.sh 2
echo "versi aktif : $(cat current/app.txt)"
echo "--- health check versi 2 gagal; rollback ---"
bash deploy-app.sh rollback
echo "versi aktif : $(cat current/app.txt)"

Output yang Dihasilkan:

text
deploy versi 1: OK (current -> releases/1)
deploy versi 2: OK (current -> releases/2)
versi aktif : app-toko versi 2
--- health check versi 2 gagal; rollback ---
rollback: current -> releases/1
versi aktif : app-toko versi 1

Skripnya layak dibedah karena polanya bisa dipindahkan ke pipeline nyata:

  1. deploy(): empat tahap pipeline berurutan — printf membangun artifact, cp mengunggahnya ke releases/$versi, ln -sfn mengaktifkan, dan blok if memverifikasi. Kalau verify gagal, skrip exit 1 — pipeline berhenti, tidak melanjutkan dengan asumsi baik-baiknya.
  2. rollback(): readlink current mengetahui versi yang aktif, lalu ls releases | sort -n | awk memilih nomor rilis terbesar yang lebih kecil dari versi sekarang. Ekspansi ${sekarang#"$DIR"/} membuang prefiks releases/ sehingga tersisa angkanya.
  3. Dispatch argumen: argumen rollback masuk ke fungsi rollback, argumen kosong mencetak pemakaian, dan lainnya dianggap nomor versi.
  4. Di pipeline nyata (GitLab CI, GitHub Actions, Jenkins): tahap yang sama dipicu oleh git tag, artifact dikirim dengan rsync, verify memanggil health check sungguhan — materi Chapter Uptime dan Health Checks .

Setelah beberapa rilis, folder releases/ menjadi jejak audit yang bisa diperiksa siapa saja:

bash
bash deploy-app.sh 1
bash deploy-app.sh 2
bash deploy-app.sh 3
bash deploy-app.sh rollback
echo "== isi folder releases (jejak setiap rilis) =="
ls -R releases
echo "current menunjuk ke: $(readlink current)"

Output yang Dihasilkan:

text
deploy versi 1: OK (current -> releases/1)
deploy versi 2: OK (current -> releases/2)
deploy versi 3: OK (current -> releases/3)
rollback: current -> releases/2
== isi folder releases (jejak setiap rilis) ==
releases:
1
2
3

releases/1:
app.txt

releases/2:
app.txt

releases/3:
app.txt
current menunjuk ke: releases/2

Perhatikan hasilnya: tiga rilis terpasang lengkap, current sudah dibalik ke releases/2 oleh rollback, dan folder versi 1 tetap utuh — inilah yang membuat rollback di strategi ini nyaris instan. Di produksi, rilis yang lebih tua dari N terakhir biasanya dihapus otomatis agar disk tidak membengkak, tetapi beberapa rilis terakhir selalu disimpan sebagai jaring pengaman.

Migrasi Database dan Batas Rollback #

Symlink membalikkan kode dalam satu detik, tetapi migrasi database tidak mundur sendiri: kolom yang sudah ditambah dan data yang sudah ditulis tetap ada. Karena itu urutannya diberlakukan: migrasi dijalankan sebelum activate, dan skema dirancang backward-compatible — rilis N menambahkan kolom tanpa memakainya, rilis N+1 yang mulai menulis, dan penghapusan kolom menunggu rilis N+2. Pola expand-contract ini menjaga rollback selalu mungkin.

Dengan pipeline yang berulang, rollback yang cepat, dan jejak rilis yang rapi, perubahan kode sudah menjadi urusan menit. Pertanyaan berikutnya adalah bagaimana kita tahu bahwa rilis baru bermasalah sebelum pengguna membanjiri kotak masuk — jawabannya ada di log yang terkumpul dari semua server, yang kita bangun di Chapter Log Aggregation .

Referensi lanjut: halaman manual ln untuk opsi -s, -f, -n, serta readlink untuk membaca target symlink.

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

deploy-app.sh

Unduh berkas
bash
#!/usr/bin/env bash
# deploy-app.sh — simulasi pipeline deployment berbasis symlink release.
# Pemakaian: bash deploy-app.sh <versi>   → rilis versi baru
#            bash deploy-app.sh rollback → kembali ke versi sebelumnya
APP=app-toko
DIR=releases

deploy() {
    versi="$1"
    # 1. build: hasilkan artifact dari "kode sumber"
    printf '%s versi %s\n' "$APP" "$versi" > artifact.txt
    # 2. upload: taruh artifact di folder release baru
    mkdir -p "$DIR/$versi"
    cp artifact.txt "$DIR/$versi/app.txt"
    # 3. activate: alihkan symlink current ke rilis baru
    ln -sfn "$DIR/$versi" current
    # 4. verify: pastikan current melayankan versi yang benar
    if [ "$(cat current/app.txt)" = "$APP versi $versi" ]; then
        echo "deploy versi $versi: OK (current -> $DIR/$versi)"
    else
        echo "deploy versi $versi: GAGAL"
        exit 1
    fi
}

rollback() {
    sekarang=$(readlink current)
    versi_sekarang=${sekarang#"$DIR"/}
    sebelumnya=$(ls "$DIR" | sort -n | awk -v v="$versi_sekarang" \
        '($1+0) < (v+0) {akhir=$1} END {print akhir}')
    if [ -z "$sebelumnya" ]; then
        echo "rollback: tidak ada versi lebih lama"
        exit 1
    fi
    ln -sfn "$DIR/$sebelumnya" current
    echo "rollback: current -> $DIR/$sebelumnya"
}

if [ "${1:-}" = "rollback" ]; then
    rollback
elif [ -z "${1:-}" ]; then
    echo "pemakaian: bash deploy-app.sh <versi>|rollback" >&2
    exit 0
else
    deploy "$1"
fi