Feature Flags & Gradual Rollouts — DevOps

Deploy ≠ Release Feature flag (atau feature toggle) memisahkan kapan kode di-deploy dari kapan user mendapat fitur. Kode bisa sudah ada di production, tapi…

Feature flag adalah saklar di dalam kode yang menentukan apakah sebuah fitur aktif, dan untuk siapa, tanpa deploy ulang. Bayangin instalasi listrik rumah baru. Tukang sudah memasang kabel dan lampu di semua kamar (itu deploy), tapi lampunya baru menyala saat saklarnya kamu tekan (itu release). Kamu bisa menyalakan satu kamar dulu untuk dicoba. Dan kalau ada korsleting, kamu turunkan MCB-nya: listrik mati seketika tanpa membongkar kabel (itu kill switch).

🧪 Tebakan cepat: fitur ekspor PDF di catatan-api dinyalakan untuk 25% user, dan sari kebagian. Besoknya kamu naikkan jadi 50%. Sari masih dapat fiturnya, atau diundi ulang dan bisa saja kehilangan fitur itu? Tebak dulu, jawabannya ada di bagian "Bucket: nomor tetap untuk tiap user".

Deploy bukan release

Di pelajaran CI/CD dan Deployment Strategies, kode baru catatan-api sampai ke web-1 lewat pipeline. Itu deploy: kodenya sudah ada di server. Release adalah saat user benar-benar bisa memakai fiturnya. Feature flag memisahkan dua momen itu.

Tim catatan-api bikin fitur baru: ekspor catatan jadi PDF. Kodenya di-deploy hari Senin, tapi disembunyikan di balik flag bernama ekspor-pdf:

app.get("/catatan/:id/pdf", (req, res) => {
  if (!evaluateFlag(flags["ekspor-pdf"], req.user)) {
    return res.status(404).json({ error: "Fitur belum tersedia" });
  }
  // ... bikin PDF dari catatan
});

Empat aturan, dicek berurutan

Satu flag di catatan-api bentuknya begini:

const flag = {
  enabled: true,        // saklar utama, alias kill switch
  percentage: 25,       // berapa persen user yang kebagian, 0 sampai 100
  allowlist: ["dewi"],  // selalu dapat: dewi tim internal yang menguji
  blocklist: ["bayu"],  // gak pernah dapat: PDF bayu sempat crash
};

Evaluator mengecek aturannya dari atas ke bawah, dan aturan pertama yang cocok langsung menentukan jawabannya:

  1. enabled false? Jawabannya false untuk semua orang, termasuk dewi. Ini kill switch.
  2. user.id ada di blocklist? false.
  3. user.id ada di allowlist? true.
  4. Persentase: 100 atau lebih berarti true, 0 atau kurang berarti false, selain itu bandingkan bucket user dengan persentase.
User Bucket Aturan yang memutuskan Hasil
dewi 55 3. allowlist true
bayu 3 2. blocklist (walau 3 < 25) false
sari 21 4. 21 < 25 true
budi 60 4. 60 tidak < 25 false

Urutan ini penting. Kalau allowlist dicek sebelum enabled, kill switch gak bisa mematikan fitur untuk dewi.

Bucket: nomor tetap untuk tiap user

Kenapa gak pakai Math.random() * 100 < 25 saja? Karena undiannya diulang di tiap request: sari melihat tombol PDF, refresh, tombolnya hilang, refresh lagi, muncul. Yang dibutuhkan adalah angka yang selalu sama untuk user yang sama. Caranya: hitung hash dari user.id, yaitu angka yang dihitung dari teks, dan teks yang sama selalu menghasilkan angka yang sama. Angka 0 sampai 99 itu disebut bucket.

Rumusnya: mulai dari hash = 0, lalu untuk tiap karakter, hash = (hash * 31 + kode karakter) % 100. Kode karakter diambil dengan charCodeAt(0). Untuk "sari":

Karakter Kode Hitungan Hash
s 115 (0 × 31 + 115) % 100 15
a 97 (15 × 31 + 97) % 100 = 562 % 100 62
r 114 (62 × 31 + 114) % 100 = 2036 % 100 36
i 105 (36 × 31 + 105) % 100 = 1221 % 100 21

Bucket sari 21. Dengan 25%, 21 < 25, jadi sari dapat. Naik ke 50%, 21 < 50, jadi sari tetap dapat. Bucket-nya gak berubah, yang berubah cuma batasnya. Itu jawaban tebakannya. Menaikkan persentase cuma menambah user baru (yang bucket-nya 25 sampai 49), gak pernah mencabut fitur dari yang sudah dapat.

Kenapa dikali 31 dan bukan dijumlah saja? Kalau dijumlah, "ab" dan "ba" dapat bucket yang sama. Dengan perkalian, urutan huruf ikut menentukan hasilnya.

Kode evaluator lengkap

function evaluateFlag(flag, user) {
  if (!flag.enabled) return false;                    // 1. kill switch
  if (flag.blocklist.includes(user.id)) return false; // 2. blocklist
  if (flag.allowlist.includes(user.id)) return true;  // 3. allowlist
  if (flag.percentage >= 100) return true;            // 4. persentase
  if (flag.percentage <= 0) return false;

  let hash = 0;
  for (const char of user.id) {
    hash = (hash * 31 + char.charCodeAt(0)) % 100;
  }
  return hash < flag.percentage;
}

const flag = { enabled: true, percentage: 25, allowlist: ["dewi"], blocklist: ["bayu"] };
console.log(evaluateFlag(flag, { id: "sari", plan: "free" })); // true
console.log(evaluateFlag(flag, { id: "budi", plan: "pro" }));  // false

Menaikkan rollout dengan aman

Rollout bertahap catatan-api: dewi dulu lewat allowlist, lalu 5%, 25%, 50%, 100%. Di tiap tahap, pantau error rate endpoint PDF. Dengan 20 user di Lab:

Tahap User yang dapat Error PDF
0% 1 (dewi, lewat allowlist) 0%
25% 5 (intan, tari, reza, sari, plus dewi) 0%
50% 10 (joko ikut masuk) 10%

Di 50%, PDF joko crash karena lampirannya besar, sama seperti bayu dulu, dan error rate langsung 10%. Set enabled: false, dan dalam hitungan detik gak ada lagi yang memanggil kode PDF, sementara tim mencari bug-nya dengan tenang.

Layanan flag sungguhan juga punya aturan target berdasarkan atribut, misalnya "user dengan plan pro selalu dapat" atau "cuma user di Indonesia". Konsepnya sama: aturan dicek berurutan, persentase jadi jaring terakhir.

Flag debt: membersihkan flag

Setelah ekspor PDF jalan 100% selama dua minggu tanpa masalah, flag ekspor-pdf sudah gak ada gunanya. Kalau dibiarkan, kode penuh if untuk flag yang selalu true, dan kombinasi flag yang harus dites membengkak: 10 flag berarti 2 pangkat 10 = 1.024 kombinasi on/off. Ini disebut flag debt, risiko utama feature flag yang gak dikelola. Hapus if-nya dan flag-nya, sama seperti membersihkan kode mati.

💡 Jenis flag

Beri flag sementara tanggal kedaluwarsa dan tiket pembersihan sejak hari dibuat.

⚠️ Jebakan yang sering ketemu

TL;DR: Feature flag memisahkan deploy (kode sampai di server) dari release (user bisa memakai fitur), jadi fitur bisa dinyalakan bertahap dan dimatikan dalam hitungan detik tanpa rollback. Evaluator mengecek berurutan: enabled false berarti false untuk semua (kill switch), lalu blocklist (false), allowlist (true), lalu persentase: >= 100 true, <= 0 false, selain itu bucket < percentage. Bucket dihitung dari hash user.id (hash = (hash * 31 + kode karakter) % 100), jadi user yang sama selalu dapat jawaban yang sama, dan menaikkan persentase gak mencabut fitur dari yang sudah dapat. Hapus flag sementara setelah 100% supaya gak jadi flag debt.

Yang akan kamu pelajari