Tampilan
Kenapa penting
Aplikasi yang sama berjalan di laptop, CI, staging, dan production — masing-masing dengan database, URL payment gateway, dan kredensial yang berbeda. Kalau perbedaan itu ditanam di kode atau di image, kamu akan membuat image per environment, lupa mengganti satu nilai, atau lebih buruk: API key production nangkring di GitHub. Insiden keamanan paling umum di backend bukan peretasan canggih, melainkan secret yang bocor lewat Git, image, atau log. Bab ini membahas cara memisahkan konfigurasi dari kode, memperlakukan secret dengan disiplin, dan membuat aplikasi menolak start bila konfigurasinya salah.
Konsep inti
12-factor config: satu image, banyak environment
Metodologi The Twelve-Factor App membagi isi aplikasi menjadi dua: kode (sama di semua environment) dan konfigurasi (berbeda per environment). Tesnya sederhana: bisakah source code ini dibuat open source sekarang tanpa membocorkan kredensial? Kalau tidak, ada konfigurasi yang salah tempat.
| Masuk konfigurasi (eksternal) | Tetap di kode / application.yml |
|---|---|
| URL & kredensial database, Redis, broker | Nama endpoint, routing internal |
| API key payment gateway, SMTP | Default yang aman (timeout, ukuran pool) |
| Hostname layanan lain | Wiring bean, aturan bisnis |
| Level log per environment | Format pesan, mapping error |
Hasilnya: image yang sama yang lulus test di staging adalah image yang naik ke production. Yang berbeda hanya nilai yang disuntikkan saat container start.
Sumber konfigurasi: env var, file, atau config server
Spring Boot membaca konfigurasi dari banyak sumber dengan urutan prioritas. Yang paling sering dipakai di production:
| Sumber | Cocok untuk | Catatan |
|---|---|---|
| Environment variable | Nilai non-rahasia & secret sederhana | Mudah, tapi terlihat di docker inspect dan bisa ikut tercetak di dump |
| File (mount volume/secret) | Secret, sertifikat | Dibaca lewat spring.config.import=configtree:; tidak muncul di env |
| Config server / vault | Banyak layanan, rotasi terpusat, audit akses | Tambah komponen yang harus selalu hidup |
Env var mengalahkan nilai di application.yml, jadi YAML cukup berisi default yang aman. Spring memetakan nama env var dengan relaxed binding: PAYMENT_GATEWAY_BASE_URL mengisi payment.gateway.base-url.
yaml
# application.yml — default aman, tanpa secret
payment:
gateway:
base-url: https://sandbox.pay.example
timeout: 5s
# api-key sengaja TIDAK ditulis: wajib datang dari env var atau file secretUntuk file, Kubernetes dan Docker me-mount secret sebagai file — satu file per nilai. Spring Boot bisa membaca seluruh folder itu sebagai properti dengan configtree: nama file menjadi nama properti, isi file menjadi nilainya.
yaml
spring:
config:
import: optional:configtree:/run/secrets/Dengan file /run/secrets/payment.gateway.api-key, properti payment.gateway.api-key terisi. Awalan optional: membuat aplikasi tetap bisa start di laptop yang tidak punya folder itu.
Secret: jangan di Git, image, atau log
Secret adalah konfigurasi yang kalau bocor menimbulkan kerugian: password database, API key, private key JWT, token webhook. Tiga tempat yang paling sering membocorkannya:
- Git — file
.envatauapplication-prod.ymlter-commit. History Git abadi; menghapus file di commit berikutnya tidak menolong. - Image —
COPY .envatauENV DB_PASSWORD=...di Dockerfile. Siapa pun yang bisadocker pullbisa membaca layer image. - Log — mencetak objek konfigurasi, header
Authorization, atau seluruh request body.
Untuk development lokal, simpan nilai di file .env yang di-ignore Git, dan commit .env.example berisi nama variabel tanpa nilai asli. Docker Compose membaca .env secara otomatis untuk substitusi ${...}.
bash
# .gitignore
.env
# .env.example — di-commit, tanpa nilai asli
PAYMENT_GATEWAY_API_KEY=
DB_PASSWORD=Pastikan juga .env tercantum di .dockerignore supaya tidak ikut terkirim ke build context.
Docker secrets dan secret manager
Docker Compose punya fitur secrets yang me-mount nilai sebagai file di /run/secrets/<nama>, bukan env var:
yaml
# compose.yaml — potongan
services:
app:
image: shop-api:1.4.0
environment:
SPRING_CONFIG_IMPORT: optional:configtree:/run/secrets/
secrets:
- payment.gateway.api-key
secrets:
payment.gateway.api-key:
file: ./secrets/payment-api-key.txt # file lokal, di-ignore GitDi production yang lebih serius, secret disimpan di secret manager: HashiCorp Vault, AWS Secrets Manager, GCP Secret Manager, atau Azure Key Vault. Keunggulannya: akses berbasis identitas (layanan A hanya boleh membaca secret A), audit log siapa membaca apa, enkripsi at rest, dan rotasi terjadwal. Integrasinya bisa lewat library (misalnya Spring Cloud Vault) atau lewat platform — Kubernetes menyinkronkan secret dari vault menjadi file yang di-mount ke pod, sehingga aplikasimu tetap cukup membaca file.
Rotasi secret
Rencanakan rotasi sejak awal: secret akan bocor suatu hari, atau karyawan yang tahu akan keluar. Pola aman: buat secret baru → deploy aplikasi agar memakai secret baru (atau dukung dua secret sekaligus sementara, misalnya dua signing key JWT) → cabut secret lama. Kalau rotasi butuh downtime, kamu tidak akan melakukannya saat darurat.
Validasi konfigurasi saat startup
Typo nama env var adalah bug klasik: aplikasi start dengan nilai null, lalu gagal saat request pertama. Solusinya adalah fail fast — bind konfigurasi ke objek bertipe dan validasi saat startup. Prinsipnya bisa dilihat dengan Java murni:
java
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
public class ConfigCheck {
record PaymentConfig(String baseUrl, String apiKey, int timeoutSeconds) {
@Override
public String toString() {
return "PaymentConfig[baseUrl=" + baseUrl + ", apiKey=****, timeoutSeconds=" + timeoutSeconds + "]";
}
}
static PaymentConfig load(Map<String, String> env) {
List<String> errors = new ArrayList<>();
String baseUrl = env.get("PAYMENT_BASE_URL");
String apiKey = env.get("PAYMENT_API_KEY");
String timeout = env.getOrDefault("PAYMENT_TIMEOUT_SECONDS", "5");
if (baseUrl == null || !baseUrl.startsWith("https://")) {
errors.add("PAYMENT_BASE_URL wajib diisi dan harus https");
}
if (apiKey == null || apiKey.isBlank()) {
errors.add("PAYMENT_API_KEY wajib diisi");
}
int seconds = Integer.parseInt(timeout);
if (seconds < 1 || seconds > 30) {
errors.add("PAYMENT_TIMEOUT_SECONDS harus 1-30");
}
if (!errors.isEmpty()) {
throw new IllegalStateException("Konfigurasi tidak valid: " + errors);
}
return new PaymentConfig(baseUrl, apiKey, seconds);
}
public static void main(String[] args) {
var ok = Map.of("PAYMENT_BASE_URL", "https://api.pay.example",
"PAYMENT_API_KEY", "sk_live_123456");
System.out.println(load(ok));
var broken = Map.of("PAYMENT_BASE_URL", "http://api.pay.example",
"PAYMENT_TIMEOUT_SECONDS", "60");
try {
load(broken);
} catch (IllegalStateException e) {
System.out.println(e.getMessage());
}
}
}text
PaymentConfig[baseUrl=https://api.pay.example, apiKey=****, timeoutSeconds=5]
Konfigurasi tidak valid: [PAYMENT_BASE_URL wajib diisi dan harus https, PAYMENT_API_KEY wajib diisi, PAYMENT_TIMEOUT_SECONDS harus 1-30]Perhatikan dua hal: semua kesalahan dikumpulkan sekaligus (bukan satu per satu per deploy), dan toString sengaja menyamarkan API key. Di Spring Boot, pekerjaan ini dilakukan @ConfigurationProperties + @Validated dengan anotasi Bean Validation (butuh spring-boot-starter-validation) — kamu akan melihatnya di contoh berikutnya.
Feature flag sekilas
Tidak semua perubahan perilaku perlu deploy. Feature flag adalah konfigurasi yang menyalakan/mematikan fitur saat runtime — misalnya mengaktifkan metode bayar baru untuk 5% pengguna, atau mematikan rekomendasi produk saat layanan itu bermasalah (kill switch). Versi paling sederhana adalah properti boolean; versi matang memakai layanan flag (misalnya Unleash atau OpenFeature + provider) yang bisa diubah tanpa restart. Jaga agar flag punya pemilik dan tanggal kedaluwarsa — flag yang dibiarkan bertahun-tahun menjadi cabang if yang tak pernah dites.
Contoh di dunia backend
Layanan checkout memanggil payment gateway. Konfigurasinya di-bind ke record bertipe dan divalidasi saat startup:
java
// Potongan — Spring Boot 4.1, butuh spring-boot-starter-validation
package id.jalurjava.shop.payment;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import java.net.URI;
import java.time.Duration;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.bind.DefaultValue;
import org.springframework.validation.annotation.Validated;
@Validated
@ConfigurationProperties(prefix = "payment.gateway")
public record PaymentGatewayProperties(
@NotNull URI baseUrl,
@NotBlank String apiKey,
@DefaultValue("5s") Duration timeout,
@DefaultValue("false") boolean qrisEnabled) {
@Override
public String toString() {
return "PaymentGatewayProperties[baseUrl=" + baseUrl + ", apiKey=****, timeout=" + timeout
+ ", qrisEnabled=" + qrisEnabled + "]";
}
}java
// Potongan — daftarkan semua @ConfigurationProperties di package aplikasi
@SpringBootApplication
@ConfigurationPropertiesScan
public class ShopApplication {
public static void main(String[] args) {
SpringApplication.run(ShopApplication.class, args);
}
}java
// Potongan — service memakai properti bertipe, bukan @Value tersebar
@Service
public class PaymentService {
private final PaymentGatewayProperties props;
public PaymentService(PaymentGatewayProperties props) {
this.props = props;
}
public List<String> availableMethods() {
return props.qrisEnabled() ? List.of("VA", "CARD", "QRIS") : List.of("VA", "CARD");
}
}Di production, PAYMENT_GATEWAY_BASE_URL datang dari env var, API key dari file /run/secrets/payment.gateway.api-key lewat configtree, dan PAYMENT_GATEWAY_QRIS_ENABLED=true berfungsi sebagai feature flag sederhana. Kalau API key lupa di-mount, aplikasi menolak start dengan pesan yang menyebut nama properti dan alasan validasinya (must not be blank) — rollout berhenti sebelum ada pelanggan yang terdampak.
Pertanyaan interview
1. Kenapa konfigurasi sebaiknya disuntikkan saat runtime, bukan di-build ke dalam image?
Agar satu artefak yang sama bisa dipromosikan dari staging ke production. Kalau konfigurasi ikut di-build, setiap environment butuh image berbeda, sehingga yang dites bukan yang di-deploy, dan secret ikut tersimpan di layer image yang bisa dibaca siapa pun yang punya akses registry.
2. Apa beda menyimpan secret di environment variable dan di file yang di-mount?
Env var sederhana dan didukung semua platform, tapi terlihat lewat docker inspect, diwariskan ke child process, dan kadang ikut tercetak di crash dump atau endpoint diagnostik. File yang di-mount (Docker/Kubernetes secrets) tidak muncul di daftar env, bisa diberi permission ketat, dan bisa diperbarui platform saat rotasi. Untuk secret sensitif, file atau secret manager lebih disukai.
3. Bagaimana kamu memastikan aplikasi tidak berjalan dengan konfigurasi yang salah?
Bind konfigurasi ke class atau record bertipe dengan @ConfigurationProperties, beri @Validated dan constraint seperti @NotBlank, @NotNull, @Min. Validasi berjalan saat context dibuat, jadi aplikasi gagal start dan deployment otomatis berhenti — jauh lebih aman daripada error di request pertama.
Jebakan umum
- Menghapus secret dari Git lalu menganggap masalah selesai — secret yang pernah ter-commit wajib dirotasi.
ENV DB_PASSWORD=...atauCOPY .envdi Dockerfile — secret ikut tersimpan permanen di layer image.- Mencetak objek konfigurasi atau record ke log tanpa menyamarkan field rahasia —
toStringbawaan record mencetak semuanya. - Memberi default palsu (
api-key: dummy) untuk properti wajib — error pindah dari startup ke request pertama. @Value("${...}")tersebar di puluhan class — sulit dilacak dan tidak tervalidasi; kelompokkan ke@ConfigurationProperties.- Feature flag tanpa pemilik dan tanggal kedaluwarsa — menumpuk menjadi kode mati yang tak pernah dites.
Ringkasan
- 12-factor: kode sama di semua environment; konfigurasi disuntikkan saat runtime sehingga satu image dipromosikan apa adanya.
- Env var untuk nilai biasa, file (
configtree, Docker/Kubernetes secrets) atau secret manager untuk secret. - Secret tidak boleh masuk Git, image, maupun log; pakai
.envlokal yang di-ignore dan.env.examplesebagai dokumentasi. - Rencanakan rotasi secret sejak awal agar bisa dilakukan tanpa downtime saat darurat.
@ConfigurationProperties+@Validatedmembuat konfigurasi bertipe dan aplikasi fail fast bila nilai salah.- Feature flag mengubah perilaku tanpa deploy, tapi perlu disiplin pemilik dan masa berlaku.