← Semua pembelajaran / Go Nol β†’ Enterprise
Fase 5 Β· Arsitektur Aplikasi

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.

Sumber asli pkg.go.dev Resmi Rangkuman ~6 menit baca

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.
  • .env hanya 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

LingkunganSumber
Lokal.env yang dimuat manual, atau direnv
CISecrets repositori, disuntikkan sebagai environment
ECS β€” nilai biasaenvironment di task definition
ECS β€” rahasiasecrets yang menunjuk Secrets Manager / SSM (Fase 10)
LambdaEnvironment 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

HalSebaiknya
Daftar fitur yang aktifFeature flag (Fase 11) β€” bisa diubah tanpa deploy
Teks yang dilihat penggunaBerkas 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?

PendekatanKapan
os.Getenv + fungsi bantu sendiriHampir selalu. ~60 baris, nol dependensi, errornya persis seperti yang kamu mau
env berbasis struct tagKalau variabelnya puluhan dan seragam
ViperKalau 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.