Aplikasi modern berjalan di banyak environment: development lokal, staging, production, dan kadang multiple region. Konfigurasi yang berbeda per environment tidak boleh hardcode di kode — itu melanggar prinsip 12-factor app. Solusinya adalah environment variables: nilai yang dioper ke proses oleh OS atau container runtime. Environment variables cocok untuk secrets (password, API key) karena tidak masuk ke repository dan dapat diganti tanpa rebuild. Pada chapter ini kita akan mendemokan pembacaan env vars dengan System.getenv(), normalisasi nama dari SNAKE_CASE ke dot.notation, validasi wajib fail-fast, serta praktik terbaik untuk Docker dan Kubernetes.
Membaca Environment Variables #
System.getenv("KEY") mengembalikan String? — null bila tidak ada. Env vars selalu String, sehingga konversi tipe (Int, Boolean) harus dilakukan manual. Untuk aplikasi Kotlin, bungkus akses dengan EnvReader yang menangani default, parsing, dan masking secret:
// environment_variables.kt — Demo env vars: baca, normalisasi, dan validasi
import java.util.Properties
import java.io.StringReader
// === Env var normalizer: APP_DB_URL -> app.db.url ===
fun normalizeEnvKey(envKey: String): String {
return envKey.lowercase().replace('_', '.')
}
// === Typed env reader ===
class EnvReader(private val env: Map<String, String>) {
fun getString(key: String, default: String? = null): String {
return env[key] ?: default ?: error("Env var $key tidak ditemukan")
}
fun getStringOrNull(key: String): String? = env[key]
fun getInt(key: String, default: Int? = null): Int {
val raw: String = env[key] ?: default?.toString() ?: error("Env var $key tidak ditemukan")
return raw.toIntOrNull() ?: error("Env var $key bukan integer: $raw")
}
fun getBoolean(key: String, default: Boolean = false): Boolean {
val raw = env[key] ?: return default
return raw.lowercase() in listOf("true", "1", "yes", "on")
}
fun requireAll(keys: List<String>): Map<String, String> {
val missing = keys.filter { it !in env }
if (missing.isNotEmpty()) {
error("Env vars wajib tidak ditemukan: ${missing.joinToString()}")
}
return keys.associateWith { env[it]!! }
}
}
// === Tipe data konfigurasi dari env ===
data class EnvConfig(
val port: Int,
val dbUrl: String,
val dbUser: String,
val dbPassword: String,
val jwtSecret: String,
val featureX: Boolean,
val logLevel: String
)
fun main() {
println("=== Environment Variables ===")
println()
println("Sumber konfigurasi 12-factor app; secrets di env vars.")
println()
// === 1. Baca env vars (simulasi untuk demo deterministik) ===
println("=== 1. Environment Variables (simulasi) ===")
val mockEnv = mapOf(
"APP_PORT" to "8080",
"APP_DB_URL" to "jdbc:postgresql://db.example.com/app",
"APP_DB_USER" to "app_user",
"APP_DB_PASSWORD" to "YOUR_DB_PASSWORD",
"APP_JWT_SECRET" to "YOUR_JWT_SECRET",
"APP_FEATURE_X" to "true",
"APP_LOG_LEVEL" to "INFO"
)
mockEnv.forEach { (k, v) ->
val display = if (k.contains("PASSWORD") || k.contains("SECRET")) "********" else v
println(" " + k.padEnd(22) + " = " + display)
}
println()
// === 2. Normalisasi kunci ===
println("=== 2. Normalisasi APP_DB_URL -> app.db.url ===")
mockEnv.keys.forEach { envKey ->
val normalized = normalizeEnvKey(envKey)
println(" " + envKey.padEnd(22) + " -> " + normalized)
}
println()
// === 3. Baca dengan EnvReader ===
println("=== 3. EnvReader (typed) ===")
val reader = EnvReader(mockEnv)
println(" APP_PORT: ${reader.getInt("APP_PORT")}")
println(" APP_DB_URL: ${reader.getString("APP_DB_URL")}")
println(" APP_FEATURE_X: ${reader.getBoolean("APP_FEATURE_X")}")
println(" APP_TIMEOUT (default 3000): ${reader.getInt("APP_TIMEOUT", 3000)}")
println()
// === 4. Validasi wajib ===
println("=== 4. Validasi Env Vars Wajib ===")
val required = listOf("APP_DB_URL", "APP_DB_USER", "APP_DB_PASSWORD", "APP_JWT_SECRET")
val wajib = reader.requireAll(required)
println(" Semua wajib hadir: ${wajib.size} vars")
println()
// === 5. Bangun EnvConfig ===
println("=== 5. EnvConfig (typed) ===")
val config = EnvConfig(
port = reader.getInt("APP_PORT"),
dbUrl = reader.getString("APP_DB_URL"),
dbUser = reader.getString("APP_DB_USER"),
dbPassword = reader.getString("APP_DB_PASSWORD"),
jwtSecret = reader.getString("APP_JWT_SECRET"),
featureX = reader.getBoolean("APP_FEATURE_X"),
logLevel = reader.getString("APP_LOG_LEVEL", "INFO")
)
println(" port = ${config.port}")
println(" dbUrl = ${config.dbUrl}")
println(" dbUser = ${config.dbUser}")
println(" dbPassword = ******** (disembunyikan)")
println(" jwtSecret = ******** (disembunyikan)")
println(" featureX = ${config.featureX}")
println(" logLevel = ${config.logLevel}")
println()
// === 6. Anti-pattern ===
println("=== 6. Anti-pattern (hindari) ===")
val antiPatterns = listOf(
"1. Hardcode password di kode -> gunakan env var",
"2. Commit .env ke git -> tambahkan ke .gitignore",
"3. Log env var sensitif -> mask sebelum log",
"4. Baca env var di function -> cache di startup",
"5. Pola dot.notation di env -> SNAKE_CASE; normalisasi saat load",
"6. Tidak ada validasi -> fail-fast pada startup"
)
antiPatterns.forEach { println(" " + it) }
println()
// === 7. Aturan praktis ===
println("=== 7. Aturan Praktis ===")
val rules = listOf(
"1. 12-factor app: konfigurasi di env, bukan di kode",
"2. Prefix konsisten (APP_*) untuk hindari tabrakan",
"3. Secrets (password, API key) WAJIB via env var",
"4. .env file untuk lokal dev; JANGAN commit ke repo",
"5. Docker -e / --env-file untuk inject ke container",
"6. Kubernetes ConfigMap + Secret untuk produksi",
"7. Rotasi secret periodik; baca ulang tanpa restart bila perlu"
)
rules.forEach { println(" " + it) }
}Output yang Dihasilkan:
=== Environment Variables ===
Sumber konfigurasi 12-factor app; secrets di env vars.
=== 1. Environment Variables (simulasi) ===
APP_PORT = 8080
APP_DB_URL = jdbc:postgresql://db.example.com/app
APP_DB_USER = app_user
APP_DB_PASSWORD = ********
APP_JWT_SECRET = ********
APP_FEATURE_X = true
APP_LOG_LEVEL = INFO
=== 2. Normalisasi APP_DB_URL -> app.db.url ===
APP_PORT -> app.port
APP_DB_URL -> app.db.url
APP_DB_USER -> app.db.user
APP_DB_PASSWORD -> app.db.password
APP_JWT_SECRET -> app.jwt.secret
APP_FEATURE_X -> app.feature.x
APP_LOG_LEVEL -> app.log.level
=== 3. EnvReader (typed) ===
APP_PORT: 8080
APP_DB_URL: jdbc:postgresql://db.example.com/app
APP_FEATURE_X: true
APP_TIMEOUT (default 3000): 3000
=== 4. Validasi Env Vars Wajib ===
Semua wajib hadir: 4 vars
=== 5. EnvConfig (typed) ===
port = 8080
dbUrl = jdbc:postgresql://db.example.com/app
dbUser = app_user
dbPassword = ******** (disembunyikan)
jwtSecret = ******** (disembunyikan)
featureX = true
logLevel = INFO
=== 6. Anti-pattern (hindari) ===
1. Hardcode password di kode -> gunakan env var
2. Commit .env ke git -> tambahkan ke .gitignore
3. Log env var sensitif -> mask sebelum log
4. Baca env var di function -> cache di startup
5. Pola dot.notation di env -> SNAKE_CASE; normalisasi saat load
6. Tidak ada validasi -> fail-fast pada startup
=== 7. Aturan Praktis ===
1. 12-factor app: konfigurasi di env, bukan di kode
2. Prefix konsisten (APP_*) untuk hindari tabrakan
3. Secrets (password, API key) WAJIB via env var
4. .env file untuk lokal dev; JANGAN commit ke repo
5. Docker -e / --env-file untuk inject ke container
6. Kubernetes ConfigMap + Secret untuk produksi
7. Rotasi secret periodik; baca ulang tanpa restart bila perluMari kita bedah demo di atas:
- Env Vars Simulasi: Demo memakai
mockEnvmap agar output deterministik. Di production, ganti denganSystem.getenv()yang mengembalikanMap<String, String>dari OS. - Normalisasi:
APP_DB_URLditransformasi menjadiapp.db.url. Ini memudahkan integrasi dengan file config berformat HOCON/YAML yang memakai dot notation. FunctionnormalizeEnvKeysederhana: lowercase + ganti_dengan.. - EnvReader typed: membungkus akses
env[key]dengan parsing tipe.getInt("APP_PORT")mengembalikanInt,getBooleanmenerima"true","1","yes","on"sebagai nilai true. Default di-support untuk semua tipe. - Validasi Wajib:
requireAll(listOf(...))memeriksa bahwa semua env var wajib hadir sebelum startup. Bila ada yang hilang,error(...)melempar exception dengan pesan jelas — fail-fast lebih baik daripada runtime error tidak jelasus setelah 10 menit. - EnvConfig typed: hasil akhir dipetakan ke
data class EnvConfig. Secret (dbPassword,jwtSecret) disimpan tetapi saat dicetak, demo memask dengan"********". Implementasi nyata sebaiknya tidak mengimplementasikantoString()default — gunakantoString()custom yang memask secret.
System.getenv() vs System.getProperty() #
| Aspek | System.getenv() | System.getProperty() |
|---|---|---|
| Sumber | OS environment | JVM args (-D) |
| Setelah startup | Tidak bisa diubah (immutable) | Bisa diubah via System.setProperty() |
| Konvensi | SCREAMING_SNAKE_CASE | dot.notation |
| Cocok untuk | Secrets, 12-factor config | Override development |
| Akses | System.getenv("KEY") | System.getProperty("key") |
Env vars lebih sesuai untuk deployment karena tidak bisa diubah setelah startup — sifat penting untuk audit dan reproducibility. System properties berguna untuk debugging ad-hoc (-Ddebug=true).
Docker dan Kubernetes #
Docker mengoper env vars via flag -e (single) atau --env-file (bulk):
docker run -e APP_PORT=8080 \
-e APP_DB_PASSWORD=$APP_DB_PASSWORD \
-p 8080:8080 \
my-app:latest
# atau via file (tidak commit .env ke repo)
docker run --env-file .env.production my-app:latestKubernetes memisahkan konfigurasi non-sensitif (ConfigMap) dan sensitif (Secret):
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
spec:
template:
spec:
containers:
- name: app
image: my-app:latest
envFrom:
- configMapRef:
name: app-config
- secretRef:
name: app-secretAnti-pattern yang Harus Dihindari #
- Hardcode password di kode:
val password = "s3cr3t"— siapa pun yang baca repository tahu password. - Commit
.envke git: tambahkan.env*ke.gitignore. Gunakan.env.examplesebagai template. - Log env var sensitif:
println("Connecting with $password")bocor ke log aggregator. Selalu mask. - Baca env var tiap function call: baca sekali di startup, simpan di object/factory.
- Tidak validasi: aplikasi startup OK tetapi crash 10 menit kemudian karena
APP_PORTtidak ada. Fail-fast lebih baik.
Referensi Lanjutan #
- Spring Boot — dokumentasi Spring Boot untuk konsumsi environment variable dan relaxed binding
Selanjutnya #
Setelah konfigurasi terkelola, langkah berikutnya adalah observability — logging terstruktur, trace ID, dan korelasi antar service. Materi tersebut dibahas pada chapter Logging dan Observability.