Konfigurasi 12-factor

Konfigurasi dari lingkungan dan proses tanpa state.

Daftar isi

Prinsip twelve-factor app menyatakan bahwa konfigurasi yang berbeda antar lingkungan (development, staging, production) sebaiknya disimpan pada lingkungan, bukan pada kode. Kode yang sama dapat dijalankan pada beberapa lingkungan hanya dengan mengubah variabel lingkungan, sehingga build dapat dipromosikan dari staging ke production tanpa rekompilasi. Prinsip lain yang berkaitan adalah proses tanpa state, yaitu data persisten disimpan pada basis data atau layanan eksternal, bukan pada memori proses. Setelah membaca topik ini, pembaca dapat membaca konfigurasi dari process.env, memvalidasi variabel wajib, serta merancang proses yang dapat direplikasi tanpa state lokal.

Konfigurasi dari lingkungan#

Variabel lingkungan dibaca oleh Node melalui objek process.env, yang merupakan objek berisi pasangan kunci-nilai string. Setiap variabel yang diatur pada shell sebelum node dipanggil, atau disuntikkan oleh manajer proses, akan tersedia pada process.env. Praktik yang baik adalah mengelompokkan variabel menjadi wajib dan opsional, lalu memvalidasi keberadaan variabel wajib saat startup agar proses berhenti lebih dulu dengan pesan yang jelas alih-alih gagal di tengah permintaan.

js config.js
// tidak-dijalankan: dijalankan lewat blok bash berikutnya dengan variabel lingkungan
const REQUIRED = ['DATABASE_URL'];
const opsional = {
  PORT: { default: 3000, cast: Number },
  NODE_ENV: { default: 'development', cast: String },
  LOG_LEVEL: { default: 'info', cast: String },
  SESSION_SECRET: { default: '', cast: String }
};

const hilang = REQUIRED.filter((k) => !process.env[k]);
if (hilang.length) {
  console.error(`variabel lingkungan wajib hilang: ${hilang.join(', ')}`);
  process.exit(1);
}

const config = {};
for (const [k, meta] of Object.entries(opsional)) {
  const raw = process.env[k] ?? meta.default;
  config[k] = meta.cast === Number ? Number(raw) : String(raw);
}
config.DATABASE_URL = process.env.DATABASE_URL;

console.log(JSON.stringify(config, null, 2));
bash
# jalankan config.js dengan variabel lingkungan lengkap
DATABASE_URL='postgres://catatan:[email protected]:5432/catatan_suhu' \
PORT=8080 \
NODE_ENV=production \
LOG_LEVEL=info \
node config.js
text Keluaran
{
  "PORT": 8080,
  "NODE_ENV": "production",
  "LOG_LEVEL": "info",
  "SESSION_SECRET": "",
  "DATABASE_URL": "postgres://catatan:[email protected]:5432/catatan_suhu"
}

Konfigurasi di atas membagi variabel menjadi dua kelompok. Variabel DATABASE_URL ditandai wajib pada daftar REQUIRED, sehingga bila tidak diatur, proses berhenti dengan kode keluar 1 dan pesan yang menyebut nama variabel yang hilang. Variabel opsional seperti PORT dan LOG_LEVEL memiliki nilai default yang dipakai ketika variabel lingkungan tidak diatur, sehingga aplikasi tetap dapat berjalan di development tanpa konfigurasi eksplisit. Pemakaian operator ?? dan || memberikan perilaku berbeda terhadap string kosong: || menganggap string kosong sebagai falsy dan menggantinya dengan default, sedangkan ?? mempertahankan string kosong sebagai nilai yang sah. Pilih ?? bila string kosong adalah nilai yang valid, dan || bila string kosong harus diperlakukan sebagai tidak diatur.

Validasi variabel wajib saat startup mengikuti prinsip fail fast. Pesan error yang muncul saat startup lebih mudah ditelusuri daripada error undefined yang muncul di tengah permintaan. Pada production, pesan ini membantu operator memperbaiki konfigurasi sebelum container dianggap sehat oleh health check.

bash
# jalankan config.js tanpa DATABASE_URL
env -u DATABASE_URL PORT=8080 NODE_ENV=production LOG_LEVEL=info node config.js
text Keluaran
variabel lingkungan wajib hilang: DATABASE_URL

Perintah env -u DATABASE_URL menghapus variabel DATABASE_URL dari lingkungan sebelum menjalankan node, yang mensimulasikan kondisi di mana variabel wajib tidak diatur. Proses keluar dengan kode 1 dan pesan error yang menyebut nama variabel yang hilang, sehingga operator dapat langsung memperbaiki konfigurasi tanpa harus menebak penyebab kegagalan.

Tipe data dan validasi#

process.env selalu mengembalikan string, sehingga variabel numerik seperti PORT perlu dikonversi secara eksplisit. Pada config.js di atas, konversi dilakukan dengan field cast pada metadata opsional, yaitu Number untuk PORT dan String untuk variabel lain. Konversi eksplisit mencegah bug ketika nilai PORT dipakai pada server.listen(port) yang menerima angka, bukan string.

Untuk validasi yang lebih ketat, pustaka seperti zod atau envalid memungkinkan deklarasi skema variabel beserta tipe, default, dan pesan error yang ramah pembaca. Pustaka ini juga mendukung validasi format seperti URL dan enum, sehingga kesalahan konfigurasi terdeteksi lebih awal. Pada aplikasi kecil hingga menengah, konversi manual seperti pada config.js di atas sudah memadai, sementara aplikasi besar dengan banyak variabel biasanya lebih terbantu dengan pustaka validasi.

Proses tanpa state#

Prinsip twelve-factor menyatakan bahwa proses aplikasi sebaiknya tanpa state dan share-nothing. Data persisten seperti sesi pengguna, berkas yang diunggah, dan data transaksional disimpan pada basis data atau layanan eksternal, bukan pada memori proses atau sistem berkas lokal. Akibatnya, proses dapat direstart, diganti, atau ditambah tanpa kehilangan data, dan penyeimbang bebas dapat mengirim permintaan ke proses mana pun.

Penyimpanan sesi pada memori proses melanggar prinsip ini. Saat penyeimbang beban mengirim permintaan pengguna ke proses yang berbeda dari yang menyimpan sesinya, pengguna dianggap belum login. Solusinya adalah menyimpan sesi pada penyimpanan eksternal seperti Redis, yang diakses oleh seluruh proses melalui klien yang sama. Pola yang sama berlaku untuk rate limiter dan cache, yang sebaiknya menggunakan Redis atau layanan serupa alih-alih Map di memori proses.

Sistem berkas lokal dapat dipakai sebagai scratch space untuk berkas sementara, tetapi tidak boleh diandalkan untuk penyimpanan persisten. Pada container, sistem berkas lokal hilang saat container dihancurkan, sehingga berkas yang perlu bertahan harus disimpan pada volume yang dipasang atau layanan objek seperti S3. Aplikasi yang mengunggah berkas pengguna sebaiknya langsung menulis ke layanan objek, bukan ke sistem berkas lokal.

Kesalahan umum#

Kesalahan yang sering muncul adalah menyimpan konfigurasi pada berkas JSON yang dikomit ke kontrol versi, dengan cabang terpisah untuk staging dan production. Pendekatan ini menyebabkan kode berbeda antar lingkungan, sehingga build yang diuji di staging tidak persis sama dengan yang dijalankan di production. Perbaikannya adalah memindahkan konfigurasi ke variabel lingkungan, sehingga build tunggal dapat dipromosikan antar lingkungan hanya dengan mengubah variabel.

Kesalahan lain adalah mengandalkan memori proses untuk cache atau rate limiter. Pada deployment dengan beberapa proses atau beberapa container, setiap proses memiliki cache-nya sendiri, sehingga hit rate rendah dan batas rate limiter tidak konsisten. Perbaikannya adalah memindahkan cache dan rate limiter ke layanan eksternal seperti Redis, yang diakses bersama oleh seluruh proses.

Ringkasan#

  • Variabel lingkungan dibaca melalui process.env, yang selalu mengembalikan string. Variabel wajib divalidasi saat startup agar proses berhenti dengan pesan yang jelas bila konfigurasi tidak lengkap.
  • Variabel opsional diberi default dan dikonversi tipe secara eksplisit, karena server.listen dan banyak API lain mengharapkan tipe yang benar.
  • Rahasa disuntikkan melalui variabel lingkungan saat runtime, bukan dibakar ke citra atau dikomit ke kontrol versi. Berkas .env hanya untuk development lokal.
  • Proses tanpa state menyimpan data persisten pada basis data atau layanan eksternal, sehingga proses dapat direstart, diganti, atau ditambah tanpa kehilangan data dan tanpa bergantung pada node tertentu.

Referensi#