Konfigurasi Aplikasi: Spring Profiles & Environment Variables
Setiap aplikasi yang "serius" akhirnya hidup di lebih dari satu tempat: laptop Anda untuk development, server staging untuk uji coba, dan server produksi untuk pengguna sungguhan. Masalahnya, ketiga tempat itu hampir pasti memakai URL database yang berbeda, kredensial yang berbeda, dan bahkan perilaku yang berbeda — misalnya log yang lebih detail di lokal dibanding di produksi.
Naluri pertama banyak developer adalah menulis semuanya langsung di kode: jdbc:mysql://localhost:3306/app lalu username root dan password root123. Di laptop Anda itu bekerja mulus. Sampai suatu hari kode itu di-push ke repository, seseorang membacanya, dan... selamat datang, database produksi Anda berhasil diakses orang lain. Artikel ini akan membahas cara yang benar: memisahkan konfigurasi dari kode menggunakan Spring Profiles dan environment variables, mengikuti aturan 12-Factor. Ini adalah artikel pendamping yang bagus untuk CI/CD dengan GitHub Actions — karena pipeline yang baik pun tetap membutuhkan konfigurasi yang benar.
Masalah: Kredensial & URL Berbeda di Tiap Lingkungan
Bandingkan tiga "lingkungan" (lingkungan di sini berarti tempat aplikasi dijalankan) berikut:
| Lingkungan | Database | Username | Password | Log level |
|---|---|---|---|---|
| Lokal (laptop Anda) | H2 in-memory | — | — | DEBUG |
| Staging | MySQL di cloud (sandbox) | staging_app | rahasia staging | INFO |
| Produksi | MySQL cluster terkelola | prod_app | rahasia produksi | WARN |
Jika setiap nilai di atas ditulis hardcode di kode, yang terjadi adalah:
- Secret bocor. Semua orang yang punya akses ke repository — termasuk history git — bisa membaca password produksi.
- Konfigurasi salah di produksi. Seorang developer menambah fitur yang butuh URL baru, lupa mengubahnya saat deploy, lalu aplikasi tersambung ke database staging di tengah jam sibuk.
- Kode penuh percabangan. Anda mulai menulis
if (isProduction) { ... } else { ... }di puluhan tempat. Kode menjadi sulit dibaca dan sulit diuji.
Prinsip inti: kode adalah sesuatu yang berubah jarang dan sama di mana-mana; konfigurasi adalah sesuatu yang berubah sering dan berbeda antar lingkungan. Keduanya harus dipisahkan.
Konfigurasi di Spring Boot
Spring Boot punya sistem externalized configuration yang sangat fleksibel: banyak "sumber" nilai yang saling menimpa berdasarkan urutan prioritas. Anda tidak perlu memilih satu saja — nilailah yang paling tinggi prioritasnya yang akan menang.
application.properties vs application.yml
Tempat paling dasar untuk menaruh konfigurasi adalah file application.properties (format key=value) atau application.yml (format YAML). Keduanya setara; YAML lebih mudah dibaca untuk struktur yang bertingkat.
# application.yml — konfigurasi BASE yang berlaku di semua lingkungan
server:
port: 8080
spring:
application:
name: inventory-service
app:
contact:
name: Tim Operasional
email: ops@example.com
File ini menjadi "default" — nilai yang dipakai bila tidak ada sumber lain yang lebih tinggi prioritasnya. Lalu ada application-{profile}.yml, yaitu konfigurasi spesifik untuk satu profile (kita bahas sebentar lagi). Jika kedua file mengatur key yang sama, nilai dari file spesifik profile-lah yang menang.
Urutan Prioritas Externalized Configuration
Spring Boot menimbang konfigurasi dari banyak tempat, dan yang lebih tinggi prioritasnya menimpa yang lebih rendah. Berikut ringkasannya dari dokumentasi resmi:
| Prioritas | Sumber | Contoh |
|---|---|---|
| Tertinggi | Command-line arguments | java -jar app.jar --spring.profiles.active=prod |
| ↑ | environment variables (OS level) | SPRING_PROFILES_ACTIVE=prod |
| ↑ | application-{profile}.yml | application-prod.yml |
| ↑ | application.yml (file base) | application.yml |
| Terendah | Default di dalam kode | @Value("${app.name:fallback}") |
Pola penting di tabel ini: semakin tinggi prioritas, semakin sedikit keterlibatan kode. environment variables lebih tinggi dari application.yml karena env var bisa diatur oleh platform deployment (Docker, Kubernetes, CI) tanpa menyentuh repository sama sekali — persis yang dicari oleh aturan 12-Factor. Command-line arguments menang karena itu yang paling "segar": diberikan langsung saat proses dimulai, tidak pernah ter-commit.
Spring Profiles
Apa itu Profile
Profile adalah sekadar grup konfigurasi bernama. Bayangkan sebuah file application.yml sebagai konfigurasi dasar, dan tiap application-{profile}.yml sebagai lapisan tambahan yang aktif hanya jika profile itu diaktifkan. Nama yang umum dipakai: dev, test, staging, prod.
Profile diaktifkan lewat tiga cara (urutan prioritasnya sesuai tabel di atas):
# 1. Lewat command-line saat menjalankan JAR
java -jar inventory-service.jar --spring.profiles.active=prod
# 2. Lewat environment variable (cara paling umum di Docker/CI)
SPRING_PROFILES_ACTIVE=prod java -jar inventory-service.jar
# 3. Di dalam application.yml (paling tidak fleksibel, hati-hati)
# spring.profiles.active: dev
Perhatikan penamaan env var: spring.profiles.active menjadi SPRING_PROFILES_ACTIVE (dot diganti underscore, semua huruf besar). Spring Boot memetakan nama key menjadi env var dengan aturan ini, jadi Anda tidak perlu menulis konfigurasi tambahan apa pun.
Contoh Lengkap: H2 vs MySQL
Sekarang mari lihat kasus paling umum. Di lingkungan dev kita ingin database ringan H2 in-memory yang diisi otomatis; di prod kita ingin MySQL sungguhan.
# application.yml — base
spring:
profiles:
active: dev # default bila tidak ada env var
jpa:
hibernate:
ddl-auto: validate
# application-dev.yml — aktif ketika profile "dev"
spring:
datasource:
url: jdbc:h2:mem:inventory;DB_CLOSE_DELAY=-1
driver-class-name: org.h2.Driver
username: sa
password:
jpa:
hibernate:
ddl-auto: create-drop
defer-datasource-initialization: true
h2:
console:
enabled: true
# application-prod.yml — aktif ketika profile "prod"
spring:
datasource:
url: jdbc:mysql://${DB_HOST}:3306/inventory
driver-class-name: com.mysql.cj.jdbc.Driver
username: ${DB_USER}
password: ${DB_PASSWORD}
jpa:
hibernate:
ddl-auto: validate
Satu JAR yang sama kini punya dua perilaku berbeda. Di dev, Spring Boot memuat H2, membuat tabel dari entity, dan mengaktifkan console H2 (jdbc:h2:mem:...). Di prod, ia memakai MySQL, dan nilai-nilai rahasia datang dari placeholder ${DB_HOST}, ${DB_USER}, ${DB_PASSWORD} — bukan dari kode. Perhatikan juga ddl-auto: validate di produksi: kita tidak pernah membiarkan aplikasi mengubah skema database produksi secara otomatis, melainkan hanya memvalidasi bahwa skema cocok.
@Profile untuk Bean Kondisional
Kadang perbedaan antar lingkungan bukan hanya soal nilai, tapi juga soal objek. Misalnya: di dev Anda ingin menampilkan dummy client pembayaran, di prod ingin memakai yang asli. Anotasi @Profile menghidupkan atau mematikan sebuah bean berdasarkan profile yang aktif.
// PembayaranClientDummy hanya dibuat saat profile "dev" aktif
@Profile("dev")
@Service
public class PembayaranClientDummy implements PembayaranClient {
@Override
public Invoice buatTagihan(Order order) {
// kembalikan invoice palsu tanpa memanggil API pembayaran
return new Invoice("DUMMY-" + order.getId(), order.getTotal());
}
}
// PembayaranClientAsli hanya dibuat saat profile "prod" atau "staging" aktif
@Profile({"prod", "staging"})
@Service
public class PembayaranClientAsli implements PembayaranClient {
@Override
public Invoice buatTagihan(Order order) {
// panggil API gateway pembayaran yang sebenarnya
}
}
Kedua bean mengimplementasi interface PembayaranClient yang sama. Selama hanya satu yang aktif di satu waktu, tidak terjadi konflik — Spring meng-inject implementasi yang tersedia, dan kode pemanggil tidak perlu tahu yang mana.
@ConfigurationProperties untuk Konfigurasi Terstruktur
Alih-alih menabur @Value di banyak tempat, konfigurasi yang berkelompok sebaiknya dibungkus dalam satu objek bertipe aman dengan @ConfigurationProperties. Perhatikan blok app.contact pada application.yml di atas — inilah cara membacanya:
// record secara otomatis menyediakan accessor name() dan email()
@ConfigurationProperties(prefix = "app.contact")
public record ContactProperties(String name, String email) {
}
// dipakai di mana saja
@Service
public class NotifikasiService {
private final ContactProperties contact;
public NotifikasiService(ContactProperties contact) {
this.contact = contact;
}
public String alamatBalasan() {
return "%s <%s>".formatted(contact.name(), contact.email());
}
}
Keuntungannya: salah ketik nama key langsung ketahuan saat aplikasi gagal mem-bind, dan semua properti terkait terkumpul rapi dalam satu kelas. Untuk aplikasi yang kompleks, ada mode validasi dengan anotasi Bean Validation (misalnya @NotBlank pada field email). Bila tim Anda masih memakai Java versi lama, bentuk klasik dengan getter & setter juga tetap didukung penuh.
Profile dalam Satu File: YAML Multi-Dokumen
Tidak suka file terpisah? YAML mendukung multi-dokumen menggunakan pemisah ---. Ini berguna untuk konfigurasi pendek, tapi untuk proyek besar sebaiknya tetap pisah per file.
spring:
config:
activate:
on-profile: dev
datasource:
url: jdbc:h2:mem:inventory
---
spring:
config:
activate:
on-profile: prod
datasource:
url: jdbc:mysql://${DB_HOST}:3306/inventory
Catatan sintaks: pada Spring Boot 2.4 ke atas, penanda profile dalam satu file ditulis
spring.config.activate.on-profile, bukanspring.profilesyang dulu (kini deprecated).
Environment Variables
Kenapa Env Var untuk Rahasia
application.yml yang berisi password produksi tetap akan masuk ke repository git — dan history git tidak pernah benar-benar hilang. environment variables memecahkan masalah ini karena nilainya diberikan di waktu jalan (runtime), bukan tersimpan di file.
| Rahasia umum | Env var yang lazim | Diisi di mana |
|---|---|---|
| URL database | DB_URL | Docker Compose, cloud secret store |
| Kredensial database | DB_USER, DB_PASSWORD | Docker Compose, cloud secret store |
| Kunci JWT | JWT_SECRET | CI secrets, secret store |
| Kunci API eksternal | API_KEY (misal STRIPE_API_KEY) | CI secrets, secret store |
| Lokasi file upload | UPLOAD_DIR | Platform deployment |
Pola ${VAR:default}
Spring Boot membaca env var lewat placeholder ${...}. Tambahkan nilai default setelah titik dua bila Anda ingin aplikasi tetap berjalan meski env var tidak di-set:
app:
name: ${APP_NAME:inventory-service}
allowed-origins: ${ALLOWED_ORIGINS:http://localhost:3000}
upload-dir: ${UPLOAD_DIR:./uploads}
Pola di atas berarti: pakai nilai env var jika ada, kalau tidak gunakan yang di dalam kurung. Waspada: default yang berupa rahasia adalah jebakan (lihat bagian Jebakan Umum).
.env Lokal dan .env.example
Untuk pengembangan lokal, .env adalah alat yang nyaman — dotenv memuat isi file ke env var sebelum aplikasi mulai. Dua aturan wajib:
.envjangan pernah di-commit ke git. File ini berisi rahasia lokal Anda..env.exampleharus di-commit sebagai template berisi kunci tanpa nilai asli, agar developer baru tahu variabel apa saja yang dibutuhkan.
# .env (LOKAL, TIDAK DI-COMMIT)
SPRING_PROFILES_ACTIVE=dev
DB_URL=jdbc:h2:mem:inventory;DB_CLOSE_DELAY=-1
JWT_SECRET=rahasia-lokal-jangan-diunggah
# .env.example (template yang DI-COMMIT)
SPRING_PROFILES_ACTIVE=dev
DB_URL=
JWT_SECRET=
# .gitignore
.env
*.env
!.env.example
| File | Dikomit? | Isi |
|---|---|---|
.env | Tidak | Nilai rahasia lokal yang asli |
.env.example | Ya | Template kosong, hanya nama variabel |
.gitignore | Ya | Aturan agar .env tidak ikut ter-push |
Aturan 12-Factor: "Config"
Prinsip Config pada 12-Factor berbunyi singkat dan tegas: konfigurasi disimpan dalam environment variables, terpisah dari kode. Implikasinya menarik — satu artefak build (satu JAR) yang sama harus berjalan identik di semua lingkungan, hanya dengan nilai env var yang berbeda.
| Praktik yang melanggar 12-Factor | Praktik yang sesuai 12-Factor |
|---|---|
| Menulis URL database di dalam kode Java | URL datang dari env var DB_URL |
Membuat file config/prod.yml yang ikut di-deploy | Nilai disuntik platform saat proses berjalan |
| Mengubah kode untuk tiap lingkungan | Kode sama; env var yang berbeda |
| Secret tercampur dalam kode | Secret tidak pernah menyentuh repository |
Alasan di balik aturan ini sangat praktis: karena artefaknya sama, apa yang lolos uji di staging persis seperti yang berjalan di produksi. Perbedaan satu-satunya adalah lingkungannya — bukan kodenya. Ini memangkas kelas bug "works on my machine, broke in production" secara drastis, dan membuat rollback hanya soal mengganti versi artefak, bukan mengganti kumpulan file konfigurasi.
Secret Management Lanjutan
.env lokal adalah titik awal yang baik, tetapi untuk produksi ada yang lebih kokoh: secret store khusus yang mengelola kredensial secara terpusat, ber-audit, dan bisa di-rotate (diganti) tanpa menurunkan aplikasi.
| Alat | Untuk apa | Kapan dipakai |
|---|---|---|
| HashiCorp Vault | Secret store self-hosted dengan kebijakan akses detail | Tim yang mengelola infrastruktur sendiri |
| Cloud Secret Manager (AWS, GCP, Azure) | Secret store terkelola dari penyedia cloud | Aplikasi sudah berjalan di cloud tersebut |
| CI Secrets (GitHub Actions, GitLab) | Menyimpan rahasia untuk pipeline build/deploy | Saat build & deploy otomatis |
| Docker secrets / Kubernetes Secrets | Menyuntik rahasia ke kontainer saat runtime | Aplikasi dikontainerkan |
Pola umumnya sama: nilai rahasia tidak lagi berada di file yang Anda commit, melainkan diambil oleh platform dan disuntikkan sebagai env var saat kontainer atau proses dimulai. Dari sudut pandang Spring Boot, tidak ada yang berubah — aplikasi tetap membaca ${DB_PASSWORD}; hanya sumbernya yang kini aman.
Jangan pernah
logsecret. Membuka debug log di produksi saat ada bug adalah naluri yang bagus, tapi aplikasi yang mencetak nilaiDataSource(yang berisi password) ke log bisa membocorkan secret ke sistem log yang mungkin juga milik pihak ketiga. Batasi log konfigurasi pada key-nya, bukan value-nya.
Studi Kasus: Satu JAR untuk Tiga Lingkungan
Mari kita satukan semuanya. Sebuah layanan inventory-service dibangun sekali menjadi satu JAR, lalu dijalankan di tiga lingkungan — tanpa mengubah kode sedikit pun.
# application.yml — hanya nilai yang SAMA di semua lingkungan
server:
port: ${PORT:8080}
spring:
jpa:
open-in-view: false
app:
contact:
name: Tim Operasional
email: ops@example.com
# application-dev.yml — lokal
spring:
datasource:
url: jdbc:h2:mem:inventory;DB_CLOSE_DELAY=-1
driver-class-name: org.h2.Driver
username: sa
jpa:
hibernate:
ddl-auto: create-drop
h2:
console:
enabled: true
logging:
level:
com.example.inventory: DEBUG
# application-staging.yml — uji coba di cloud, database sandbox
spring:
datasource:
url: jdbc:mysql://${DB_HOST}:3306/inventory_staging
username: ${DB_USER}
password: ${DB_PASSWORD}
jpa:
hibernate:
ddl-auto: validate
logging:
level:
com.example.inventory: INFO
# application-prod.yml — produksi, log lebih hemat
spring:
datasource:
url: jdbc:mysql://${DB_HOST}:3306/inventory
username: ${DB_USER}
password: ${DB_PASSWORD}
jpa:
hibernate:
ddl-auto: validate
logging:
level:
com.example.inventory: WARN
Cara menjalankannya:
# Lokal (default profile "dev" dari application.yml)
mvn spring-boot:run
# Staging
SPRING_PROFILES_ACTIVE=staging \
DB_HOST=staging-db.internal \
DB_USER=staging_app \
DB_PASSWORD=$(cat ~/.secrets/staging-db) \
java -jar target/inventory-service.jar
# Produksi (profile via command-line, nilai rahasia dari secret store)
java -jar target/inventory-service.jar \
--spring.profiles.active=prod \
--DB_PASSWORD=$(vault read -field=value secret/inventory/prod)
Perhatikan hasilnya: database berbeda (H2 vs MySQL inventory_staging vs MySQL inventory), log level berbeda (DEBUG → INFO → WARN), tetapi artefak JAR-nya sama. Rahasia tidak pernah ada di repository. Inilah yang dimaksud konfigurasi terpisah dari kode.
Jebakan Umum
Secret bocor lewat git history
Anda sadar ada .env ter-commit, lalu menghapusnya — tetapi riwayat git tetap menyimpannya. Sekali secret ter-push, anggap ia bocir dan segera rotate (ganti) di semua layanan yang menggunakannya. Jangan pernah berasumsi "tidak apa, repo ini privat".
Default value yang berbahaya
spring:
datasource:
password: ${DB_PASSWORD:root}
Bila DB_PASSWORD lupa di-set, aplikasi diam-diam memakai root — dan bisa jadi tersambung ke database produksi dengan password root. Rahasia tidak boleh punya default. Simpan placeholder tanpa default, atau berikan default yang jelas-jelas tidak valid agar aplikasi cepat gagal alih-alih gagal secara diam-diam:
spring:
datasource:
password: ${DB_PASSWORD}
Terlalu banyak konfigurasi dalam satu file
application.yml yang membengkak sampai ratusan baris dengan komentar # TODO: ganti di produksi adalah tanda bahaya. Pisahkan per lingkungan, dan pindahkan blok yang berulang ke konfigurasi bersama bila perlu.
Profile tidak di-set → salah lingkungan
Jika spring.profiles.active tidak pernah di-set dan tidak ada default yang jelas, aplikasi bisa berjalan dengan konfigurasi base yang keliru — misalnya memakai database lokal dev di produksi. Biasakan deploy produksi selalu menyebutkan profile secara eksplisit (melalui env var atau command-line), dan pertimbangkan fail-fast: jika env var rahasia tidak ada, biarkan aplikasi gagal mulai.
| Jebakan | Gejala | Pencegahan |
|---|---|---|
| Secret di git history | Kredensial terpakai orang tak dikenal | .gitignore sejak awal; rotate bila bocor |
| Default rahasia | Terhubung dengan kredensial lemah | Rahasia tanpa default |
| Satu file penuh semua lingkungan | Edit produksi berisiko merusak dev | Pisah application-{profile}.yml |
| Profile tidak di-set | Berjalan di lingkungan yang salah | Set profile secara eksplisit saat deploy |
Ringkasan
Konfigurasi yang baik adalah konfigurasi yang terpisah dari kode. Spring Boot memberi fondasi yang rapi: file application.yml sebagai default, application-{profile}.yml sebagai lapisan per lingkungan, dan sistem prioritas yang membuat environment variables serta command-line arguments menang atas file. Aktifkan profile lewat SPRING_PROFILES_ACTIVE atau --spring.profiles.active=..., gunakan @Profile untuk bean kondisional dan @ConfigurationProperties untuk konfigurasi terstruktur. Simpan rahasia sebagai env var yang disuntikkan platform, bukan di repository — mengikuti aturan 12-Factor Config yang menjamin satu artefak build berjalan di semua lingkungan hanya dengan env yang berbeda. Terakhir, waspadai jebakan: secret yang bocor lewat git history, default rahasia yang berbahaya, dan profile yang tidak pernah di-set.
Lanjut membaca
- Dokumentasi Externalized Configuration Spring Boot
- Properties & Configuration di Spring Boot
- Prinsip Config pada 12-Factor App
- Profil pada Spring Boot
- Spring Boot Properties & Configuration Metadata
- CI/CD dengan GitHub Actions — di sinilah env var dan secrets dipakai untuk men-deploy dengan aman
- Dockerizing Java Applications — bagaimana env var disuntikkan ke dalam kontainer saat aplikasi dijalankan