Pertanyaan penting terakhir dari siklus deployment bukan “apakah rilis baru gagal” melainkan “seberapa cepat kita bisa pulih ketika gagal” — kegagalan rilis adalah kepastian statistik, bukan kemungkinan yang bisa dihapus. Tim yang matang mengukurnya sebagai MTTR (mean time to recovery): selang waktu dari gejala terdeteksi sampai versi stabil kembali melayani pengguna. Rollback yang butuh mencari-cari binary lama, mengingat konfigurasi, dan menebak langkah berikutnya mengubah insiden 10 menit menjadi downtime sejam. Chapter ini menyiapkan jalur pulang yang mekanis: versi lama yang tetap tersimpan, pengalihan lewat symlink atomik, strategi tingkat lanjut (blue-green dan canary), serta disiplin kompatibilitas database yang membuat rollback tetap mungkin.
Simpan Versi Lama, Alihkan lewat Symlink #
Pola deploy paling sederhana yang tetap bisa dipulihkan memakai tiga elemen di server: direktori releases/ berisi setiap versi yang pernah terpasang, symlink current menunjuk versi aktif, dan service (chapter Linux Service (systemd)
) menjalankan current/sapa-server. Rilis baru berarti menambah direktori dan menggeser penunjuk; rollback berarti menggeser penunjuk kembali — binary lama tidak pernah ditimpa:
/opt/sapa/
├── releases/
│ ├── v1.4.1/ ← versi stabil, disimpan
│ └── v1.4.2/ ← versi baru
└── current -> releases/v1.4.2Program rollback-demo.go pada bagian bawah halaman membangun struktur tersebut dan memainkan tiga kejadian: rilis normal, rollback, lalu roll forward:
package main
import (
"fmt"
"os"
"path/filepath"
"strings"
)
// pasang membuat direktori releases/<versi> berisi penanda versi.
func pasang(root, versi string) error {
dir := filepath.Join(root, "releases", versi)
if err := os.MkdirAll(dir, 0o755); err != nil {
return err
}
penanda := "berkas ini menandai versi " + versi + "\n"
return os.WriteFile(filepath.Join(dir, "VERSION"), []byte(penanda), 0o644)
}
// alihkan mengarahkan symlink current ke versi tertentu.
// Symlink sementara dibuat dulu, lalu diganti lewat rename yang bersifat atomik.
func alihkan(root, versi string) error {
target := filepath.Join("releases", versi) // relatif agar pindah folder tetap valid
link := filepath.Join(root, "current")
sementara := link + "-baru"
if err := os.Remove(sementara); err != nil && !os.IsNotExist(err) {
return err
}
if err := os.Symlink(target, sementara); err != nil {
return err
}
return os.Rename(sementara, link)
}
// versiAktif membaca versi yang ditunjuk symlink current.
func versiAktif(root string) (string, error) {
target, err := filepath.EvalSymlinks(filepath.Join(root, "current"))
if err != nil {
return "", err
}
return filepath.Base(target), nil
}
// lapor mencetak keadaan current beserta isi berkas VERSION di dalamnya.
func lapor(root, kejadian string) {
v, err := versiAktif(root)
if err != nil {
fmt.Println("gagal membaca current:", err)
os.Exit(1)
}
isi, err := os.ReadFile(filepath.Join(root, "current", "VERSION"))
if err != nil {
fmt.Println("gagal membaca VERSION:", err)
os.Exit(1)
}
fmt.Printf("%-20s current -> %s (VERSION: %s)\n", kejadian, v, strings.TrimSpace(string(isi)))
}
func main() {
root, err := os.MkdirTemp("", "sapa-rilis-*")
if err != nil {
fmt.Println("gagal membuat direktori sementara:", err)
os.Exit(1)
}
defer os.RemoveAll(root)
// Dua rilis terpasang berdampingan; versi lama tidak dihapus.
for _, v := range []string{"v1.4.1", "v1.4.2"} {
if err := pasang(root, v); err != nil {
fmt.Println("gagal memasang", v, ":", err)
os.Exit(1)
}
}
// Rilis normal: alihkan current ke versi terbaru.
if err := alihkan(root, "v1.4.2"); err != nil {
fmt.Println("gagal rilis:", err)
os.Exit(1)
}
lapor(root, "setelah rilis")
// Insiden: v1.4.2 bermasalah; rollback hanya mengarahkan ulang symlink.
if err := alihkan(root, "v1.4.1"); err != nil {
fmt.Println("gagal rollback:", err)
os.Exit(1)
}
lapor(root, "setelah rollback")
// Roll forward: perbaikan sudah dirilis ulang sebagai v1.4.2.
if err := alihkan(root, "v1.4.2"); err != nil {
fmt.Println("gagal roll forward:", err)
os.Exit(1)
}
lapor(root, "setelah roll forward")
}Output yang Dihasilkan:
setelah rilis current -> v1.4.2 (VERSION: berkas ini menandai versi v1.4.2)
setelah rollback current -> v1.4.1 (VERSION: berkas ini menandai versi v1.4.1)
setelah roll forward current -> v1.4.2 (VERSION: berkas ini menandai versi v1.4.2)Mari kita telaah bagian penting program di atas:
- Baris 14–22 (
pasang): setiap rilis mendapat direktori sendiri berisi berkasVERSION— di server sungguhan direktori ini berisi binary, template, dan aset dari chapter Release dan Versioning . - Baris 25–38 (
alihkan): kunci operasinya ada di baris 30–37 — symlink baru dibuat dengan nama sementara lalu diganti lewatos.Renameyang bersifat atomik di POSIX. Pembacacurrentmelihat seluruhnya versi lama atau versi baru, tidak pernah keadaan setengah terpasang. - Baris 41–47 (
versiAktif):filepath.EvalSymlinksmenelusuri symlink sampai direktori asli, danfilepath.Basememotong nama versinya — cara paling jujur membaca versi aktif. - Baris 50–64 (
lapor): membaca berkasVERSIONlewat jalurcurrent/VERSION, membuktikan bahwa pembacaan berkas ikut beralih bersama symlink. - Baris 97 dan 104 (rollback dan roll forward): keduanya memanggil fungsi
alihkanyang sama — jalur pulang dan jalur maju tidak berbeda mekanismenya, hanya arah penunjuknya.
Di server sungguhan, setelah alihkan selesai, satu perintah sudo systemctl restart sapa memuat binary versi baru — restart cepat yang aman karena service berhenti dengan graceful shutdown dari chapter sebelumnya.
Strategi Lanjutan: Blue-Green dan Canary #
Pengalihan symlink mengganti satu server. Platform dengan banyak instance punya dua pola lanjutan:
| Strategi | Mekanisme | Keunggulan | Biaya |
|---|---|---|---|
| Blue-green | Dua lingkungan penuh; load balancer bergeser serentak | Rollback = geser balik, hitungan detik | Kapasitas server dua kali lipat |
| Canary | Persentase kecil trafik ke versi baru, naik bertahap | Masalah terdeteksi dini dengan dampak kecil | Rilis berlangsung lama, butuh metrik |
| Rolling | Instance diganti satu per satu oleh orchestrator | Tanpa downtime, hemat sumber daya | Selama transisi dua versi hidup bersama |
Pilihan strategi menentukan alat yang dipakai (load balancer, Kubernetes, flag service mesh), tetapi fondasinya tetap sama: versi lama harus masih ada dan masih bisa menerima trafik dalam hitungan menit.
Checklist dan Latihan #
Rollback yang tidak pernah dilatih bukan rollback — itu naskah yang belum pernah dibaca. Simpan checklist ini bersama runbook deployment:
- Dua versi terakhir tetap ada di
releases/dengan checksum yang bisa dicocokkan. - Alur rollback ditulis sebagai urutan perintah yang bisa ditempel ke terminal (alihkan, restart, verifikasi).
- Metrik pemantauan (error rate, latensi, koneksi database) terlihat dari tempat yang sama dengan tombol rollback.
- Migration yang menyertai rilis sudah diperiksa kompatibilitas mundurnya.
- Kepada siapa keputusan rollback diambil — dan kriteria alarmnya — disepakati sebelum insiden, bukan saat insiden.
Section deployment ditutup dengan lingkaran yang lengkap: binary yang dibangun bersih dan reproducible, dikemas dan diberi versi, dijalankan sebagai service yang berhenti dengan tertib, dan — ketika semuanya tetap gagal — kembali ke versi stabil lewat jalur yang sudah teruji. Keahlian berikutnya yang melengkapi operasional ini adalah melihat ke dalam aplikasi yang berjalan: logging terstruktur, metrik, dan tracing di section Logging dan Observability .
Referensi #
- Dokumentasi package os
— referensi resmi
os.Symlink,os.Rename, danos.ReadFileyang menjadi mekanisme pengalihan. - Dokumentasi package path/filepath
— referensi resmi
filepath.EvalSymlinksdanfilepath.Join.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
rollback-demo.go
Unduh berkaspackage main
import (
"fmt"
"os"
"path/filepath"
"strings"
)
// pasang membuat direktori releases/<versi> berisi penanda versi.
func pasang(root, versi string) error {
dir := filepath.Join(root, "releases", versi)
if err := os.MkdirAll(dir, 0o755); err != nil {
return err
}
penanda := "berkas ini menandai versi " + versi + "\n"
return os.WriteFile(filepath.Join(dir, "VERSION"), []byte(penanda), 0o644)
}
// alihkan mengarahkan symlink current ke versi tertentu.
// Symlink sementara dibuat dulu, lalu diganti lewat rename yang bersifat atomik.
func alihkan(root, versi string) error {
target := filepath.Join("releases", versi) // relatif agar pindah folder tetap valid
link := filepath.Join(root, "current")
sementara := link + "-baru"
if err := os.Remove(sementara); err != nil && !os.IsNotExist(err) {
return err
}
if err := os.Symlink(target, sementara); err != nil {
return err
}
return os.Rename(sementara, link)
}
// versiAktif membaca versi yang ditunjuk symlink current.
func versiAktif(root string) (string, error) {
target, err := filepath.EvalSymlinks(filepath.Join(root, "current"))
if err != nil {
return "", err
}
return filepath.Base(target), nil
}
// lapor mencetak keadaan current beserta isi berkas VERSION di dalamnya.
func lapor(root, kejadian string) {
v, err := versiAktif(root)
if err != nil {
fmt.Println("gagal membaca current:", err)
os.Exit(1)
}
isi, err := os.ReadFile(filepath.Join(root, "current", "VERSION"))
if err != nil {
fmt.Println("gagal membaca VERSION:", err)
os.Exit(1)
}
fmt.Printf("%-20s current -> %s (VERSION: %s)\n", kejadian, v, strings.TrimSpace(string(isi)))
}
func main() {
root, err := os.MkdirTemp("", "sapa-rilis-*")
if err != nil {
fmt.Println("gagal membuat direktori sementara:", err)
os.Exit(1)
}
defer os.RemoveAll(root)
// Dua rilis terpasang berdampingan; versi lama tidak dihapus.
for _, v := range []string{"v1.4.1", "v1.4.2"} {
if err := pasang(root, v); err != nil {
fmt.Println("gagal memasang", v, ":", err)
os.Exit(1)
}
}
// Rilis normal: alihkan current ke versi terbaru.
if err := alihkan(root, "v1.4.2"); err != nil {
fmt.Println("gagal rilis:", err)
os.Exit(1)
}
lapor(root, "setelah rilis")
// Insiden: v1.4.2 bermasalah; rollback hanya mengarahkan ulang symlink.
if err := alihkan(root, "v1.4.1"); err != nil {
fmt.Println("gagal rollback:", err)
os.Exit(1)
}
lapor(root, "setelah rollback")
// Roll forward: perbaikan sudah dirilis ulang sebagai v1.4.2.
if err := alihkan(root, "v1.4.2"); err != nil {
fmt.Println("gagal roll forward:", err)
os.Exit(1)
}
lapor(root, "setelah roll forward")
}