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:
| Variabel | Contoh nilai | Peran |
|---|---|---|
APP_ENV | production | Nama lingkungan; membedakan perilaku log dan fail-fast |
APP_PORT | 8080 | Port HTTP yang di-binding aplikasi |
APP_DATABASE_URL | postgres://... | Alamat koneksi database (berisi secret) |
APP_LOG_LEVEL | info | Ambang level log terstruktur |
APP_SHUTDOWN_TIMEOUT | 10s | Batas 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:
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:
APP_ENV=production APP_PORT=8080 \
APP_DATABASE_URL="postgres://sapa:[email protected]:5432/sapa" \
go run env-demo.goOutput yang Dihasilkan:
konfigurasi aktif:
APP_ENV = production
APP_PORT = 8080
APP_DATABASE_URL = terisi (60 karakter)
variabel APP_ terdeteksi: APP_DATABASE_URL, APP_ENV, APP_PORTMari kita telaah baris demi baris program di atas:
- Baris 12 (
var variabelWajib = ...): mendeklarasikan daftar variabel wajib sebagai satu-satunya tempat referensi — menambah variabel baru cukup satu baris di sini. - Baris 15–20 (loop pemeriksaan):
os.Getenvmengembalikan string kosong baik untuk variabel yang kosong maupun yang tidak ada; keduanya dianggap cacat, pesan galat ditulis ke standard error lewatfmt.Fprintf, laluos.Exit(1)menghentikan program dengan status gagal. - Baris 25–27 (ringkasan): mencetak konfigurasi non-rahasia; isi
APP_DATABASE_URLsengaja tidak dicetak — panjangnya saja yang ditampilkan. - Baris 30–37 (
os.Environ+strings.Cut): memindai seluruh environment proses, memotong setiap entri pada tanda=untuk mengambil namanya, dan menyaring yang berprefixAPP_. - Baris 38 (
slices.Sort): mengurutkan nama variabel karena urutanos.Environtidak dijamin — tanpa pengurutan, output berubah-ubah antar mesin. - 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:
go build -o env-demo env-demo.go
./env-demo; echo "exit=$?"Output yang Dihasilkan:
variabel wajib kosong: APP_PORT
exit=1Pesan 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/envpada chapter sebelumnya membaca berkasKEY=nilaisaat 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_URLsebaiknya 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:
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| Berkas | Permission | Pemilik | Alasan |
|---|---|---|---|
/opt/sapa/sapa-server | 0755 | root | Binary bisa dieksekusi semua orang, hanya root yang menimpanya |
/etc/sapa/env | 0640 | root:sapa | Secret hanya terbaca oleh grup sapa — bukan user lain |
/opt/sapa | 0755 | root | Aplikasi 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:
[Service]
MemoryMax=512M
CPUQuota=100%
LimitNOFILE=8192| Direktif | Efek |
|---|---|
MemoryMax=512M | Proses dihentikan systemd saat melampaui batas — sebelum kernel OOM-killer ikut campur |
CPUQuota=100% | Membatasi pemakaian CPU (100% = satu penuh inti) |
LimitNOFILE=8192 | Batas 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 #
- Dokumentasi package os
— referensi resmi
os.Getenv,os.Environ, danos.Exit. - Dokumentasi package fmt
— pola
fmt.Fprintfkeos.Stderruntuk pesan galat.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
env-demo.go
Unduh berkaspackage 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, ", "))
}