Konfigurasi & rahasia
Konfigurasi yang dibaca di tengah aplikasi adalah dependensi tersembunyi. Konfigurasi yang tidak divalidasi saat startup adalah insiden yang tertunda sampai permintaan pertama yang membutuhkannya.
Intisari
- Baca environment sekali, di
main, jadi satu struct bertipe β lalu oper lewat konstruktor. - Gagal saat startup kalau ada yang kurang. Container yang mati langsung jauh lebih baik daripada yang melayani setengah benar.
- Rahasia datang dari Secrets Manager / Parameter Store, disuntikkan sebagai environment oleh ECS β bukan dari berkas di image.
.envhanya untuk lokal, dan tidak pernah masuk git.- Jangan pernah mencatat seluruh konfigurasi ke log; beri tipe rahasia method
String()yang menyensor dirinya.
Satu struct, satu tempat
// internal/config/config.go
type Config struct {
Env string // production | staging | local
Addr string
DSN Rahasia
RedisURL string
JWTSecret Rahasia
LevelLog slog.Level
BatasWaktu time.Duration
MaxOpenConns int
}
func Muat() (Config, error) {
c := Config{
Env: wajib("APP_ENV"),
Addr: opsional("ADDR", ":8080"),
DSN: Rahasia(wajib("DATABASE_URL")),
RedisURL: wajib("REDIS_URL"),
JWTSecret: Rahasia(wajib("JWT_SECRET")),
LevelLog: level(opsional("LOG_LEVEL", "info")),
BatasWaktu: durasi("REQUEST_TIMEOUT", 10*time.Second),
MaxOpenConns: angka("DB_MAX_CONNS", 25),
}
if err := errors.Join(kesalahan...); err != nil {
return Config{}, fmt.Errorf("konfigurasi tidak lengkap: %w", err)
}
return c, c.periksa()
}
func (c Config) periksa() error {
if c.Env == "production" {
if len(c.JWTSecret) < 32 {
return errors.New("JWT_SECRET minimal 32 byte di produksi")
}
if c.LevelLog == slog.LevelDebug {
return errors.New("LOG_LEVEL=debug membocorkan data di produksi")
}
}
if c.MaxOpenConns < 1 {
return errors.New("DB_MAX_CONNS harus positif")
}
return nil
}
errors.Join mengumpulkan semua variabel yang kurang sekaligus. Tanpa itu,
kamu memperbaiki satu variabel, deploy, gagal lagi karena variabel kedua, deploy lagi⦠Satu pesan yang
menyebut semuanya menghemat siklus deploy yang panjang.
Tipe rahasia yang menyensor dirinya
type Rahasia string
// Menutupi nilainya di fmt.Println, %v, %s, dan slog.
func (r Rahasia) String() string { return "***" }
// slog memanggil ini untuk LogValuer.
func (r Rahasia) LogValue() slog.Value { return slog.StringValue("***") }
// Nilai aslinya harus diminta secara eksplisit.
func (r Rahasia) Buka() string { return string(r) }
log.Info("konfigurasi dimuat", "cfg", cfg)
// {"cfg":{"Env":"production","DSN":"***","JWTSecret":"***", ...}}
Mencatat seluruh struct konfigurasi saat startup adalah kebiasaan bagus β ia menghemat berjam-jam saat mendiagnosis "kenapa ia menunjuk database yang salah". Yang membuatnya berbahaya cuma satu hal: kredensial ikut tercetak, lalu tersimpan di CloudWatch selamanya, terbaca siapa pun yang punya akses log. Tipe di atas menyelesaikan keduanya sekaligus.
Dari mana nilainya datang
| Lingkungan | Sumber |
|---|---|
| Lokal | .env yang dimuat manual, atau direnv |
| CI | Secrets repositori, disuntikkan sebagai environment |
| ECS β nilai biasa | environment di task definition |
| ECS β rahasia | secrets yang menunjuk Secrets Manager / SSM (Fase 10) |
| Lambda | Environment variable, dienkripsi dengan KMS |
// Task definition ECS β kodemu tidak berubah sama sekali.
// Yang berbeda hanya dari mana nilainya datang.
"secrets": [
{ "name": "DATABASE_URL",
"valueFrom": "arn:aws:secretsmanager:ap-southeast-1:123:secret:prod/db-AbC123" },
{ "name": "JWT_SECRET",
"valueFrom": "arn:aws:secretsmanager:ap-southeast-1:123:secret:prod/jwt-XyZ789" }
]
Jangan memasang berkas .env ke dalam image. Image tersimpan di ECR, bisa ditarik
siapa pun yang punya izin baca, dan lapisannya bertahan selamanya β termasuk rahasia yang sudah kamu
ganti. Rahasia disuntikkan saat runtime, tidak pernah dibangun ke dalam artefak.
Rahasia yang berputar
// Kredensial database yang dirotasi Secrets Manager berarti kredensial
// yang dibaca saat startup bisa basi. Dua pilihan:
//
// 1. Muat ulang berkala (aplikasi berumur panjang)
// 2. Biarkan koneksi kedaluwarsa (ConnMaxLifetime) lalu ambil ulang
//
// Yang paling sederhana untuk ECS: rotasi memicu deployment baru,
// sehingga task baru membaca nilai baru dan task lama dihentikan rapi.
Yang bukan konfigurasi
| Hal | Sebaiknya |
|---|---|
| Daftar fitur yang aktif | Feature flag (Fase 11) β bisa diubah tanpa deploy |
| Teks yang dilihat pengguna | Berkas terjemahan, di-embed |
| Aturan bisnis (batas gratis ongkir) | Database, supaya bisa diubah tim bisnis |
| Skema, template, migrasi | //go:embed β ikut ke dalam binari |
Aturan pembedanya: konfigurasi adalah hal yang berbeda antar lingkungan. Kalau nilainya sama di staging dan produksi, ia bukan konfigurasi β ia konstanta, dan tempatnya di kode tempat ia bisa di-review.
Pustaka, atau tulis sendiri?
| Pendekatan | Kapan |
|---|---|
os.Getenv + fungsi bantu sendiri | Hampir selalu. ~60 baris, nol dependensi, errornya persis seperti yang kamu mau |
env berbasis struct tag | Kalau variabelnya puluhan dan seragam |
| Viper | Kalau memang butuh berkas YAML/TOML + flag + watch. Berat untuk aplikasi container |
Latihan: tulis config.Muat() yang gagal dengan pesan berisi semua
variabel yang kurang sekaligus. Tambahkan tipe Rahasia dan buktikan
slog.Info("cfg", "c", cfg) tidak membocorkan DSN. Terakhir, jalankan container tanpa
DATABASE_URL dan pastikan ia keluar dengan kode bukan-nol β itu yang membuat ECS berhenti
menggulirkan deploy yang rusak.
Rangkuman ini sengaja dipangkas ke bagian yang dipakai di roadmap. Buka sumber aslinya saat kamu butuh detail lengkap atau referensi parameter.