02. config dan env: bagaimana konfigurasi masuk

Milik tim (kerangka bawaan). File asli: internal/config/config.go dan internal/core/env.go. Kamu tidak menulisnya, tetapi store dan polling memakai nilai-nilainya.


Kenapa ada

Ketentuan Implementasi no. 6: kredensial dan konfigurasi wajib dibaca dari environment variable, tidak boleh tertulis di kode atau ter-commit. Environment variable adalah pengaturan yang diberikan dari luar program saat ia dijalankan. Di proyek ini, docker-compose.yml mengisinya dari file .env yang tidak masuk git.

Kenapa dibutuhkan

Tanpa ini, kunci rahasia (BMKG_API_KEY, PVMBG_TOKEN, password Mongo) akan tertulis di kode dan ikut ter-commit. Spek mengurangi nilai untuk itu. Selain itu, nilai seperti interval polling bisa diubah tanpa mengubah kode.


Bagian 1: Config, semua pengaturan di satu tempat

Dikutip dari internal/config/config.go, baris 11 sampai 31

type Config struct {
	ServiceName string
	LogLevel    string
	Port        int
 
	// Upstream sources. Each has its own credential; they must never be swapped.
	BMKGBaseURL     string
	BMKGAPIKey      string
	PVMBGBaseURL    string
	PVMBGToken      string
	PollInterval    time.Duration
	UpstreamTimeout time.Duration
 
	// Canonical Store (owned exclusively by this service)
	MongoURI string
	MongoDB  string
 
	// Message broker
	KafkaBootstrapServers string
	KafkaHazardTopic      string
}

Satu struct menampung semua pengaturan. Bagian lain tidak memanggil os.Getenv sendiri-sendiri. Yang dipakai tugasmu:

FieldDipakai olehContoh nilai
BMKGBaseURL, BMKGAPIKeyklien BMKGhttp://bmkg-mock:8081, kunci rahasia
PVMBGBaseURL, PVMBGTokenklien PVMBGhttp://pvmbg-mock:8082, token rahasia
PollIntervalingest.Run3 detik
UpstreamTimeoutcore.HTTPClient5 detik
MongoURI, MongoDBstore.Newalamat Mongo + nama database bnpb

Nama host seperti bmkg-mock dan mongo bukan alamat internet. Itu nama service di docker-compose, yang diterjemahkan Docker menjadi alamat container di jaringan internalnya.


Bagian 2: Load, membaca dari environment

Dikutip dari internal/config/config.go, baris 33 sampai 54

func Load() (Config, error) {
	var e core.Env
	c := Config{
		ServiceName: e.String("SERVICE_NAME", "bnpb-aggregator"),
		LogLevel:    e.String("LOG_LEVEL", "INFO"),
		Port:        e.Int("PORT", 8000),
 
		BMKGBaseURL:     e.String("BMKG_BASE_URL", "http://bmkg-mock:8081"),
		BMKGAPIKey:      e.Required("BMKG_API_KEY"),
		PVMBGBaseURL:    e.String("PVMBG_BASE_URL", "http://pvmbg-mock:8082"),
		PVMBGToken:      e.Required("PVMBG_TOKEN"),
		PollInterval:    e.Seconds("POLL_INTERVAL_SECONDS", 3*time.Second),
		UpstreamTimeout: e.Millis("UPSTREAM_TIMEOUT_MS", 5000*time.Millisecond),
 
		MongoURI: e.Required("MONGO_URI"),
		MongoDB:  e.String("MONGO_DB", "bnpb"),
 
		KafkaBootstrapServers: e.String("KAFKA_BOOTSTRAP_SERVERS", "kafka:9092"),
		KafkaHazardTopic:      e.String("KAFKA_HAZARD_TOPIC", "hazard-events"),
	}
	return c, e.Err()
}
  • e.String("SERVICE_NAME", "bnpb-aggregator"): ambil nilainya, atau pakai nilai bawaan kalau tidak diisi.
  • e.Required("BMKG_API_KEY"): wajib. Tidak ada nilai bawaan, supaya tidak ada rahasia yang tertanam di kode. Kalau kosong, service menolak start.
  • e.Seconds("POLL_INTERVAL_SECONDS", 3*time.Second): membaca angka detik (boleh pecahan) dan mengubahnya menjadi time.Duration, tipe durasi Go yang langsung bisa dipakai untuk ticker dan timeout.
  • Di akhir, return c, e.Err(): semua masalah yang terkumpul, atau nil kalau konfigurasi valid.

Bagian 3: Env, membaca sambil mengumpulkan error

Dikutip dari internal/core/env.go, baris 22 sampai 28

func (e *Env) Required(key string) string {
	v := os.Getenv(key)
	if v == "" {
		e.errs = append(e.errs, fmt.Errorf("%s is required", key))
	}
	return v
}

Dikutip dari internal/core/env.go, baris 44 sampai 55

func (e *Env) Seconds(key string, def time.Duration) time.Duration {
	v, ok := os.LookupEnv(key)
	if !ok || v == "" {
		return def
	}
	f, err := strconv.ParseFloat(v, 64)
	if err != nil || f <= 0 {
		e.errs = append(e.errs, fmt.Errorf("%s must be a positive number of seconds, got %q", key, v))
		return def
	}
	return time.Duration(f * float64(time.Second))
}

Dikutip dari internal/core/env.go, baris 74 sampai 74

func (e *Env) Err() error { return errors.Join(e.errs...) }

Yang membuat Env berguna: setiap method mencatat error ke daftar e.errs alih-alih langsung berhenti. Err() menggabungkan semuanya (errors.Join). Hasilnya, kalau tiga variabel wajib kosong, kamu mendapat satu pesan yang menyebut ketiganya, bukan harus menjalankan ulang tiga kali untuk menemukannya satu per satu. (Pelajaran 2.)


Kalau kamu menambah variabel baru

Aturan repo: tambahkan di tiga tempat.

  1. internal/config/config.go (dibaca),
  2. .env.example (didokumentasikan dengan nilai contoh),
  3. docker-compose.yml (diteruskan ke container service itu).

Kalau salah satu terlewat, service gagal start di Docker, atau reviewer tidak tahu variabel itu ada.

Cara menjelaskan dalam 30 detik

Semua pengaturan dan rahasia dibaca dari environment variable lewat config.Load, tidak pernah tertulis di kode. Variabel wajib yang kosong dilaporkan sekaligus lalu service menolak start. Isinya diberikan docker-compose.yml dari file .env yang tidak di-commit.

Latihan

  1. Variabel apa yang akan kamu ubah untuk membuat polling tiap 5 detik? Apakah perlu mengubah kode?
  2. Kenapa BMKG_API_KEY memakai Required dan bukan String dengan nilai bawaan?
  3. Apa gunanya Env mengumpulkan error, dibanding berhenti di error pertama?