Aplikasi yang sama biasanya berjalan dengan konfigurasi berbeda di laptop dan di server: port lain, database lain, level log lain. Praktik yang mapan (dipopulerkan metodologi twelve-factor) adalah menyimpan seluruh konfigurasi itu sebagai environment variable — binary yang sama bisa dipindah antar lingkungan tanpa dikompilasi ulang. Namun keleluasaan ini punya sisi rawan: variabel yang lupa terisi baru ketahuan saat request pertama gagal, secret yang tertinggal di berkas .env bisa ikut ter-commit, dan aplikasi tanpa batas sumber daya bisa melumpuhkan seluruh server. Chapter ini menata ketiganya: konvensi variabel, kegagalan yang cepat dan terang, lalu disiplin user serta batas sumber daya di production.

Konvensi Penamaan Variabel #

Seluruh variabel milik aplikasi kita diberi prefix APP_. Prefix memisahkan konfigurasi milik aplikasi dari variabel milik sistem (seperti PATH dan HOME) atau variabel runtime Go seperti GOMAXPROCS — daftar variabel di server tetap mudah dibaca meskipun banyak aplikasi berbagi satu mesin. Konvensi penamaan yang umum:

VariabelContoh nilaiPeran
APP_ENVproductionNama lingkungan; membedakan perilaku log dan fail-fast
APP_PORT8080Port HTTP yang di-binding aplikasi
APP_DATABASE_URLpostgres://...Alamat koneksi database (berisi secret)
APP_LOG_LEVELinfoAmbang level log terstruktur
APP_SHUTDOWN_TIMEOUT10sBatas waktu graceful shutdown

Fail-Fast di Awal Program #

Aplikasi production sebaiknya menolak menyala ketika konfigurasinya cacat — lebih baik gagal saat deploy daripada gagal di depan pengguna. Program env-demo.go pada bagian bawah halaman memeriksa variabel wajib sebelum melakukan pekerjaan apa pun:

go
package main

import (
	"fmt"
	"os"
	"slices"
	"strings"
)

// variabelWajib berisi environment variable yang wajib terisi saat produksi.
var variabelWajib = []string{"APP_PORT", "APP_ENV", "APP_DATABASE_URL"}

func main() {
	// Fail-fast: hentikan program bila ada variabel wajib yang kosong.
	for _, nama := range variabelWajib {
		if os.Getenv(nama) == "" {
			fmt.Fprintf(os.Stderr, "variabel wajib kosong: %s\n", nama)
			os.Exit(1)
		}
	}

	fmt.Println("konfigurasi aktif:")
	fmt.Println("  APP_ENV          =", os.Getenv("APP_ENV"))
	fmt.Println("  APP_PORT         =", os.Getenv("APP_PORT"))
	fmt.Printf("  APP_DATABASE_URL = terisi (%d karakter)\n", len(os.Getenv("APP_DATABASE_URL")))

	// Kumpulkan seluruh variabel berprefix APP_ lalu urutkan supaya stabil.
	var ketemu []string
	for _, kv := range os.Environ() {
		nama, _, _ := strings.Cut(kv, "=")
		if strings.HasPrefix(nama, "APP_") {
			ketemu = append(ketemu, nama)
		}
	}
	slices.Sort(ketemu)
	fmt.Println("variabel APP_ terdeteksi:", strings.Join(ketemu, ", "))
}

Jalankan dengan variabel lengkap:

bash
APP_ENV=production APP_PORT=8080 \
APP_DATABASE_URL="postgres://sapa:[email protected]:5432/sapa" \
go run env-demo.go

Output yang Dihasilkan:

text
konfigurasi aktif:
  APP_ENV          = production
  APP_PORT         = 8080
  APP_DATABASE_URL = terisi (60 karakter)
variabel APP_ terdeteksi: APP_DATABASE_URL, APP_ENV, APP_PORT

Mari kita telaah baris demi baris program di atas:

  1. Baris 12 (var variabelWajib = ...): mendeklarasikan daftar variabel wajib sebagai satu-satunya tempat referensi — menambah variabel baru cukup satu baris di sini.
  2. Baris 15–20 (loop pemeriksaan): os.Getenv mengembalikan string kosong baik untuk variabel yang kosong maupun yang tidak ada; keduanya dianggap cacat, pesan galat ditulis ke standard error lewat fmt.Fprintf, lalu os.Exit(1) menghentikan program dengan status gagal.
  3. Baris 25–27 (ringkasan): mencetak konfigurasi non-rahasia; isi APP_DATABASE_URL sengaja tidak dicetak — panjangnya saja yang ditampilkan.
  4. Baris 30–37 (os.Environ + strings.Cut): memindai seluruh environment proses, memotong setiap entri pada tanda = untuk mengambil namanya, dan menyaring yang berprefix APP_.
  5. Baris 38 (slices.Sort): mengurutkan nama variabel karena urutan os.Environ tidak dijamin — tanpa pengurutan, output berubah-ubah antar mesin.
  6. Baris 39 (strings.Join): mencetak daftar hasil dengan pemisah koma sehingga satu baris rapi.

Status exit yang benar penting agar systemd dan CI tahu kegagalan konfigurasi. Kita buktikan dengan binary hasil build — variabel sengaja tidak diset:

bash
go build -o env-demo env-demo.go
./env-demo; echo "exit=$?"

Output yang Dihasilkan:

text
variabel wajib kosong: APP_PORT
exit=1

Pesan galat muncul di standard error dan status keluar program adalah 1 — systemd akan menandai start sebagai gagal, bukan menganggapnya sukses lalu bingung aplikasi tidak merespons.

.env Hanyalah untuk Development #

Berkas .env nyaman di development karena otomatis dimuat alat lokal, tetapi ia bukan mekanisme production. Tiga alasan utamanya: berkas mudah ter-commit ke repository, tidak punya mekanisme rotasi, dan isinya terbaca oleh siapa pun yang bisa membaca direktori kerja. Di server, tempat variabel tetap berupa environment variable — tetapi sumbernya berbeda:

  • systemd: direktif EnvironmentFile=/etc/sapa/env pada chapter sebelumnya membaca berkas KEY=nilai saat memulai layanan; berkas ini diadministrasi root dan tidak pernah masuk repository.
  • Docker/Kubernetes: variabel didefinisikan pada definisi container — dibahas di dua chapter berikutnya.
  • Secret manager: nilai sensitif seperti APP_DATABASE_URL sebaiknya disimpan di layanan khusus (HashiCorp Vault, AWS Secrets Manager, Google Cloud Secret Manager) lalu di-inject sebagai variabel saat aplikasi start. Rotasi dan audit akses menjadi bawaan platformnya.

User Khusus dan Permission Berkas #

Aplikasi tidak boleh berjalan sebagai root: proses yang disusupi mendapat seluruh hak sistem. Langkah standarnya membuat user sistem tanpa kemampuan login, lalu membatasi kepemilikan berkas:

bash
sudo useradd --system --no-create-home --shell /usr/sbin/nologin sapa
sudo install -m 0755 sapa-server /opt/sapa/sapa-server
sudo install -m 0640 -o root -g sapa /etc/sapa/env /etc/sapa/env
BerkasPermissionPemilikAlasan
/opt/sapa/sapa-server0755rootBinary bisa dieksekusi semua orang, hanya root yang menimpanya
/etc/sapa/env0640root:sapaSecret hanya terbaca oleh grup sapa — bukan user lain
/opt/sapa0755rootAplikasi tidak bisa menulis binary-nya sendiri

Membatasi Sumber Daya #

Satu layanan yang bocor memori tidak boleh menjatuhkan tetangganya di server yang sama. systemd membatasi sumber daya dari sisi manajer proses, tanpa mengubah kode aplikasi:

ini
[Service]
MemoryMax=512M
CPUQuota=100%
LimitNOFILE=8192
DirektifEfek
MemoryMax=512MProses dihentikan systemd saat melampaui batas — sebelum kernel OOM-killer ikut campur
CPUQuota=100%Membatasi pemakaian CPU (100% = satu penuh inti)
LimitNOFILE=8192Batas jumlah berkas/koneksi yang terbuka serentak — koneksi database dan klien HTTP menghabiskan kuota ini

Direktif LimitNOFILE sering dilupakan: server dengan banyak koneksi TCP bisa menyentuh batas bawaan (kerap 1024) padahal memori masih lega, dan gejalanya berupa error too many open files yang jauh dari kode kita. Environment yang rapi kini lengkap — berikutnya kita kemas binary dan konfigurasinya menjadi sesuatu yang bisa dicopy-dimana-mana: container image.

Referensi #

Berkas Sumber

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

env-demo.go

Unduh berkas
go
package main

import (
	"fmt"
	"os"
	"slices"
	"strings"
)

// variabelWajib berisi environment variable yang wajib terisi saat produksi.
var variabelWajib = []string{"APP_PORT", "APP_ENV", "APP_DATABASE_URL"}

func main() {
	// Fail-fast: hentikan program bila ada variabel wajib yang kosong.
	for _, nama := range variabelWajib {
		if os.Getenv(nama) == "" {
			fmt.Fprintf(os.Stderr, "variabel wajib kosong: %s\n", nama)
			os.Exit(1)
		}
	}

	fmt.Println("konfigurasi aktif:")
	fmt.Println("  APP_ENV          =", os.Getenv("APP_ENV"))
	fmt.Println("  APP_PORT         =", os.Getenv("APP_PORT"))
	fmt.Printf("  APP_DATABASE_URL = terisi (%d karakter)\n", len(os.Getenv("APP_DATABASE_URL")))

	// Kumpulkan seluruh variabel berprefix APP_ lalu urutkan supaya stabil.
	var ketemu []string
	for _, kv := range os.Environ() {
		nama, _, _ := strings.Cut(kv, "=")
		if strings.HasPrefix(nama, "APP_") {
			ketemu = append(ketemu, nama)
		}
	}
	slices.Sort(ketemu)
	fmt.Println("variabel APP_ terdeteksi:", strings.Join(ketemu, ", "))
}