Configuration Manager #

Setiap aplikasi membutuhkan konfigurasi — URL database, API key, jumlah worker thread, timeout, feature flag. Yang membedakan aplikasi yang mudah di-operate dari yang menjadi mimpi buruk adalah bagaimana konfigurasi itu dikelola. Konfigurasi yang tersebar di berbagai tempat, yang berbeda antara developer satu dengan yang lain, yang mengandung secret yang di-commit ke git, atau yang tidak ada mekanisme untuk mengubahnya tanpa deploy ulang adalah tanda-tanda sistem yang rapuh.

Configuration Manager — dalam konteks ini bukan merujuk ke tool spesifik, tapi ke praktik dan pola untuk mengelola konfigurasi aplikasi secara sistematis — adalah fondasi dari operabilitas sistem. Ketika konfigurasi dikelola dengan benar, mengubah behavior aplikasi di production bisa dilakukan tanpa deploy, secret aman tersimpan di vault bukan di git, dan onboarding developer baru tidak membutuhkan ritual “minta konfigurasi ke senior”.

Masalah dengan Konfigurasi yang Tidak Dikelola #

Pola masalah konfigurasi yang paling umum:

  1. Hardcode di source code:
     DATABASE_URL = "postgres://admin:password123@localhost/mydb"
     → Password di git history selamanya
     → Harus ubah kode untuk ganti konfigurasi

  2. Konfigurasi berbeda antar developer:
     Developer A pakai port 5432, Developer B pakai 5433
     → "Works on my machine" yang tidak berkaitan dengan aplikasi
     → Bug yang sulit direproduksi

  3. Secret di .env yang di-commit:
     # .env — JANGAN COMMIT FILE INI
     API_KEY=sk-prod-xxxxxxxxxxxx  ← tapi sudah ada di git history
     → Git history menyimpan semua versi file, termasuk yang lama

  4. Konfigurasi yang berbeda di production vs staging:
     → Bug yang hanya muncul di production
     → Tidak ada yang tahu konfigurasi production sebenarnya apa

  5. Tidak ada validasi konfigurasi:
     Aplikasi start tanpa DB_URL → crash dengan error yang membingungkan
     → Harusnya: "DB_URL is required" saat startup

  6. Secret rotation yang tidak bisa dilakukan tanpa downtime:
     Ganti API key → update .env di semua server → restart semua instance
     → Proses manual yang rawan kesalahan
flowchart TD
    A[Konfigurasi Aplikasi] --> B{Jenisnya apa?}
    B --> C["Non-secret\nURL publik, timeout, feature flag"]
    B --> D["Secret\nPassword, API key, private key"]

    C --> E["Environment Variable\natau Config File"]
    D --> F["Secret Manager\nVault, AWS Secrets Manager"]

    E --> G["Version control\n.env.example tanpa nilai"]
    F --> H["Tidak pernah di-commit\nDi-inject saat runtime"]

    G --> I[Tersedia untuk semua developer]
    H --> J[Hanya proses yang berhak yang bisa akses]

Twelve-Factor App: Config #

The Twelve-Factor App methodology mendefinisikan satu prinsip yang paling fundamental untuk konfigurasi: simpan konfigurasi di environment, bukan di kode. Segala sesuatu yang mungkin berbeda antara deployment (dev, staging, production) harus ada di environment variable, bukan hardcode.

package main

import (
	"fmt"
	"log"
	"os"
	"strconv"
	"strings"
)

// ANTI-PATTERN: konfigurasi di kode
type BadConfig struct {
	DatabaseURL string
	RedisURL    string
	SecretKey   string
	Debug       bool
	MaxWorkers  int
}

// BENAR: konfigurasi dari environment
type Config struct {
	DatabaseURL string
	RedisURL    string
	SecretKey   string
	Debug       bool
	MaxWorkers  int
	LogLevel    string
}

// Load dan validasi semua konfigurasi dari environment
func fromEnv() (*Config, error) {
	var errors []string

	require := func(name string) string {
		value := os.Getenv(name)
		if value == "" {
			errors = append(errors, fmt.Sprintf("Required environment variable '%s' is not set", name))
		}
		return value
	}
	optional := func(name, def string) string {
		value := os.Getenv(name)
		if value == "" {
			return def
		}
		return value
	}

	maxWorkers, _ := strconv.Atoi(optional("MAX_WORKERS", "4"))
	config := &Config{
		DatabaseURL: require("DATABASE_URL"),
		RedisURL:    optional("REDIS_URL", "redis://localhost:6379"),
		SecretKey:   require("SECRET_KEY"),
		Debug:       strings.ToLower(optional("DEBUG", "false")) == "true",
		MaxWorkers:  maxWorkers,
		LogLevel:    strings.ToUpper(optional("LOG_LEVEL", "INFO")),
	}

	// Fail fast: jika ada yang missing, crash saat startup dengan pesan jelas
	if len(errors) > 0 {
		return nil, fmt.Errorf("incomplete configuration:\n  - %s", strings.Join(errors, "\n  - "))
	}

	return config, nil
}

// Load sekali saat startup
func main() {
	config, err := fromEnv()
	if err != nil {
		log.Fatal(err)
	}
	_ = config
}

Environment Variable: Panduan Praktis #

# .env.example — commit file ini ke git (tanpa nilai sensitif)
# Ini adalah dokumentasi konfigurasi yang dibutuhkan aplikasi

# Database (REQUIRED)
DATABASE_URL=postgres://user:password@host:5432/dbname

# Redis (REQUIRED untuk caching dan session)
REDIS_URL=redis://localhost:6379

# Security (REQUIRED)
SECRET_KEY=generate-with-openssl-rand-hex-32

# Application settings
DEBUG=false
LOG_LEVEL=INFO
MAX_WORKERS=4
PORT=8000

# External services (REQUIRED untuk fitur pembayaran)
PAYMENT_GATEWAY_URL=https://api.payment-gateway.com
PAYMENT_API_KEY=your-api-key-here

# Optional: Email service
SMTP_HOST=smtp.mailtrap.io
SMTP_PORT=587
SMTP_USER=
SMTP_PASS=
# .gitignore — PASTIKAN .env ada di sini
.env
.env.local
.env.*.local
*.secret

# .env — TIDAK di-commit ke git
# File ini ada di setiap developer machine dan server
# Dibuat dari .env.example dan diisi dengan nilai aktual

DATABASE_URL=postgres://myuser:actualpassword@localhost/mydb_dev
REDIS_URL=redis://localhost:6379
SECRET_KEY=a8f5f167f44f4964e6c998dee827110c
DEBUG=true
LOG_LEVEL=DEBUG
// Go — menggunakan godotenv
func loadEnv() {
	// Load .env file jika ada (untuk local development)
	// Di production, environment variable sudah ada dari sistem
	_ = godotenv.Load()
}

Konfigurasi Per Environment #

Strategi konfigurasi per environment:

  Pendekatan: Overlay / inheritance
  Nilai default di kode (paling rendah prioritasnya)
  ↓
  Config file (environment-specific, bukan secret)
  ↓
  Environment variable (tertinggi prioritasnya, override semua)

  Dev:        .env lokal masing-masing developer
  Staging:    environment variable dari secret manager (otomatis inject)
  Production: environment variable dari secret manager (otomatis inject)

  Tidak ada config file yang di-commit yang mengandung nilai aktual
package main

import (
	"encoding/json"
	"os"
	"strings"
)

// Implementasi layered config sederhana
// Konfigurasi dengan prioritas:
// 1. Environment variable (tertinggi)
// 2. config.{environment}.json
// 3. config.default.json (terendah)
type LayeredConfig struct {
	config map[string]interface{}
}

func NewLayeredConfig() *LayeredConfig {
	env := os.Getenv("APP_ENV")
	if env == "" {
		env = "development"
	}
	c := &LayeredConfig{config: map[string]interface{}{}}
	c.loadFile("config.default.json")
	c.loadFile("config." + env + ".json")
	c.loadEnvVars()
	return c
}

func (c *LayeredConfig) loadFile(filename string) {
	data, err := os.ReadFile(filename)
	if err != nil {
		return
	}
	var parsed map[string]interface{}
	if json.Unmarshal(data, &parsed) == nil {
		mergeMaps(c.config, parsed)
	}
}

func (c *LayeredConfig) loadEnvVars() {
	// Override dengan environment variable yang relevan
	mappings := map[string]string{
		"DATABASE_URL": "database.url",
		"REDIS_URL":    "redis.url",
		"LOG_LEVEL":    "logging.level",
		"MAX_WORKERS":  "server.max_workers",
	}
	for envKey, configKey := range mappings {
		if value := os.Getenv(envKey); value != "" {
			setNested(c.config, strings.Split(configKey, "."), value)
		}
	}
}

// Get dengan dot notation: get("database.url")
func (c *LayeredConfig) get(key string) interface{} {
	keys := strings.Split(key, ".")
	var d interface{} = c.config
	for _, k := range keys {
		m, ok := d.(map[string]interface{})
		if !ok {
			return nil
		}
		d = m[k]
	}
	return d
}

func mergeMaps(dst, src map[string]interface{}) {
	for k, v := range src {
		if vm, ok := v.(map[string]interface{}); ok {
			if dm, ok := dst[k].(map[string]interface{}); ok {
				mergeMaps(dm, vm)
				continue
			}
		}
		dst[k] = v
	}
}

func setNested(m map[string]interface{}, keys []string, value string) {
	for _, k := range keys[:len(keys)-1] {
		if _, ok := m[k].(map[string]interface{}); !ok {
			m[k] = map[string]interface{}{}
		}
		m = m[k].(map[string]interface{})
	}
	m[keys[len(keys)-1]] = value
}

Secret Management #

Secret — password, API key, private key — tidak boleh pernah ada di source code atau config file yang di-commit. Gunakan dedicated secret manager.

HashiCorp Vault #

package main

import (
	"os"

	vault "github.com/hashicorp/vault/api"
)

// getVaultClient mengembalikan client Vault yang ter-autentikasi via AppRole
func getVaultClient() (*vault.Client, error) {
	client, err := vault.NewClient(vault.DefaultConfig())
	if err != nil {
		return nil, err
	}
	client.SetAddress(os.Getenv("VAULT_ADDR"))

	// Login AppRole
	secret, err := client.Logical().Write("auth/approle/login", map[string]interface{}{
		"role_id":   os.Getenv("VAULT_ROLE_ID"),
		"secret_id": os.Getenv("VAULT_SECRET_ID"),
	})
	if err != nil {
		return nil, err
	}
	client.SetToken(secret.Auth.ClientToken)
	return client, nil
}

// getDatabaseCredentials mengembalikan dynamic secret — kredensial baru tiap kali, dengan TTL terbatas
func getDatabaseCredentials() (map[string]interface{}, error) {
	client, err := getVaultClient()
	if err != nil {
		return nil, err
	}
	secret, err := client.Logical().Read("database/creds/myapp-role")
	if err != nil {
		return nil, err
	}
	return map[string]interface{}{
		"username": secret.Data["username"],
		"password": secret.Data["password"],
		"lease_id": secret.LeaseID,
	}, nil
}

// getStaticSecret mengembalikan secret statis dari Vault KV store (v2)
func getStaticSecret(path string) (map[string]interface{}, error) {
	client, err := getVaultClient()
	if err != nil {
		return nil, err
	}
	secret, err := client.KVv2("secret").Get(nil, path)
	if err != nil {
		return nil, err
	}
	return secret.Data, nil
}

AWS Secrets Manager #

package main

import (
	"context"
	"encoding/json"
	"os"

	"github.com/aws/aws-sdk-go-v2/aws"
	"github.com/aws/aws-sdk-go-v2/config"
	"github.com/aws/aws-sdk-go-v2/service/secretsmanager"
)

// getSecret mengembalikan secret dari AWS Secrets Manager.
// Di-cache per proses — refresh dengan buat instance baru atau restart.
func getSecret(secretName string) (map[string]string, error) {
	cfg, err := config.LoadDefaultConfig(context.Background(),
		config.WithRegion(envOr("AWS_DEFAULT_REGION", "ap-southeast-1")))
	if err != nil {
		return nil, err
	}
	client := secretsmanager.NewFromConfig(cfg)

	output, err := client.GetSecretValue(context.Background(), &secretsmanager.GetSecretValueInput{
		SecretId: aws.String(secretName),
	})
	if err != nil {
		return nil, err
	}

	var parsed map[string]string
	if err := json.Unmarshal([]byte(*output.SecretString), &parsed); err != nil {
		return nil, err
	}
	return parsed, nil
}

// Penggunaan
func getDatabaseConfig() (map[string]string, error) {
	env := os.Getenv("APP_ENV")
	return getSecret("myapp/" + env + "/database")
	// Returns: {"host": "...", "port": "5432", "username": "...", "password": "..."}
}

func envOr(key, fallback string) string {
	if v := os.Getenv(key); v != "" {
		return v
	}
	return fallback
}
AWS Secrets Manager mendukung rotation otomatis — password database bisa di-rotate setiap 30 hari tanpa downtime. Pastikan aplikasi tidak meng-cache credential terlalu lama (gunakan TTL cache < 1 jam) agar mendapat nilai terbaru setelah rotation.

Feature Flag #

Feature flag memisahkan deployment dari release — kode bisa di-deploy kapan saja, fitur diaktifkan secara terpisah. Berguna untuk canary release, A/B testing, dan kill switch.

package main

import (
	"context"
	"encoding/json"
	"time"

	"github.com/redis/go-redis/v9"
)

var redisClient = redis.NewClient(&redis.Options{
	Addr: "localhost:6379",
})

const cacheTTL = 60 * time.Second

// isEnabled mengecek flag dengan rollout percentage dan whitelist
func isEnabled(ctx context.Context, flagName string, userID int64) bool {
	flag := getFlag(ctx, flagName)
	if flag == nil || flag["enabled"] != true {
		return false
	}

	// Rollout percentage: aktif untuk X% user
	rolloutPct := 100
	if v, ok := flag["rollout_percentage"].(float64); ok {
		rolloutPct = int(v)
	}

	// User whitelist selalu aktif
	if userID > 0 && containsID(flag["user_whitelist"], userID) {
		return true
	}

	// Deterministic bucket: user yang sama selalu dapat hasil yang sama
	if userID > 0 && rolloutPct < 100 {
		return userID%100 < int64(rolloutPct)
	}

	return rolloutPct == 100
}

// getFlag membaca dari cache, fallback ke database
func getFlag(ctx context.Context, flagName string) map[string]interface{} {
	cacheKey := "feature_flag:" + flagName
	cached, err := redisClient.Get(ctx, cacheKey).Result()
	if err == nil {
		var flag map[string]interface{}
		if json.Unmarshal([]byte(cached), &flag) == nil {
			return flag
		}
	}

	// Ambil dari database jika tidak ada di cache
	flag := queryFlagFromDB(flagName)
	if flag == nil {
		return nil
	}

	flagData := map[string]interface{}{
		"enabled":            flag.enabled,
		"rollout_percentage": flag.rolloutPercentage,
		"user_whitelist":     flag.userWhitelist,
	}
	encoded, _ := json.Marshal(flagData)
	redisClient.Set(ctx, cacheKey, encoded, cacheTTL)
	return flagData
}

// Guard ala middleware untuk handler (ekuivalen dekorator Python)
func featureFlag(flagName string, next func(userID int64)) func(userID int64) {
	return func(userID int64) {
		if !isEnabled(context.Background(), flagName, userID) {
			writeJSON(map[string]string{"error": "Feature not available"}, 404)
			return
		}
		next(userID)
	}
}

// Penggunaan
// http.HandleFunc("/api/new-checkout", featureFlag("new_checkout_flow", handleNewCheckout))

// Di dalam fungsi biasa
func getRecommendations(userID int64) []string {
	if isEnabled(context.Background(), "ml_recommendations", userID) {
		return mlService.GetRecommendations(userID)
	}
	return getPopularProducts() // fallback
}

func containsID(list interface{}, userID int64) bool {
	items, ok := list.([]interface{})
	if !ok {
		return false
	}
	for _, item := range items {
		if id, ok := item.(float64); ok && int64(id) == userID {
			return true
		}
	}
	return false
}

Validasi Konfigurasi saat Startup #

Fail fast saat startup mencegah aplikasi berjalan dengan konfigurasi yang salah dan crash di tengah jalan dengan error yang membingungkan.

package main

import (
	"fmt"
	"net/url"
	"os"
	"strconv"
)

// Validasi semua konfigurasi saat startup
type ConfigValidator struct {
	errors   []string
	warnings []string
}

func (v *ConfigValidator) require(name string) *ConfigValidator {
	if os.Getenv(name) == "" {
		v.errors = append(v.errors, fmt.Sprintf("MISSING: %s is required", name))
	}
	return v
}

func (v *ConfigValidator) requireURL(name string) *ConfigValidator {
	value := os.Getenv(name)
	if value == "" {
		v.errors = append(v.errors, fmt.Sprintf("MISSING: %s is required", name))
		return v
	}
	parsed, err := url.Parse(value)
	if err != nil || parsed.Scheme == "" || parsed.Host == "" {
		v.errors = append(v.errors, fmt.Sprintf("INVALID_URL: %s = '%s'", name, value))
	}
	return v
}

func (v *ConfigValidator) requireInt(name string, minVal, maxVal *int) *ConfigValidator {
	value := os.Getenv(name)
	if value == "" {
		v.errors = append(v.errors, fmt.Sprintf("MISSING: %s is required", name))
		return v
	}
	intVal, err := strconv.Atoi(value)
	if err != nil {
		v.errors = append(v.errors, fmt.Sprintf("NOT_INTEGER: %s = '%s'", name, value))
		return v
	}
	if minVal != nil && intVal < *minVal {
		v.errors = append(v.errors, fmt.Sprintf("INVALID: %s = %d (min: %d)", name, intVal, *minVal))
	}
	if maxVal != nil && intVal > *maxVal {
		v.errors = append(v.errors, fmt.Sprintf("INVALID: %s = %d (max: %d)", name, intVal, *maxVal))
	}
	return v
}

func (v *ConfigValidator) warnIfDefault(name, dangerousValue string) *ConfigValidator {
	if os.Getenv(name) == dangerousValue {
		v.warnings = append(v.warnings,
			fmt.Sprintf("WARNING: %s is still using an unsafe default value", name))
	}
	return v
}

func (v *ConfigValidator) validate() {
	for _, warning := range v.warnings {
		fmt.Printf("⚠️  %s\n", warning)
	}
	if len(v.errors) > 0 {
		fmt.Printf("\n❌ Invalid configuration:\n\n")
		for _, e := range v.errors {
			fmt.Printf("  ✗ %s\n", e)
		}
		fmt.Printf("\nSee .env.example for guidance.\n")
		os.Exit(1)
	}
	fmt.Println("✓ Configuration valid")
}

func newConfigValidator() *ConfigValidator { return &ConfigValidator{} }
func intPtr(v int) *int                    { return &v }

// Panggil di awal aplikasi, sebelum apapun
func validateConfig() {
	newConfigValidator().
		requireURL("DATABASE_URL").
		requireURL("REDIS_URL").
		require("SECRET_KEY").
		requireInt("MAX_WORKERS", intPtr(1), intPtr(64)).
		requireInt("PORT", intPtr(1024), intPtr(65535)).
		warnIfDefault("SECRET_KEY", "changeme").
		warnIfDefault("SECRET_KEY", "your-secret-key-here").
		validate()
}

Anti-Pattern yang Harus Dihindari #

package main

// ✗ Anti-pattern 1: secret di source code
const apiKey = "sk-..."
const databasePassword = "supersecretpassword"
// ✓ Solusi: environment variable + secrets manager

// ✗ Anti-pattern 2: .env di-commit ke git
// .gitignore tidak mengandung .env
// ✓ Solusi: .env di .gitignore DARI AWAL, .env.example yang di-commit

// ✗ Anti-pattern 3: tidak ada validasi konfigurasi
// Aplikasi start tanpa DATABASE_URL → crash saat ada request
// ✓ Solusi: validasi dan fail fast saat startup dengan pesan jelas

// ✗ Anti-pattern 4: .env.example tidak up-to-date
// Production punya 10 env var yang tidak ada di .env.example
// Developer baru debugging 2 jam
// ✓ Solusi: .env.example selalu di-update bersamaan dengan perubahan konfigurasi

// ✗ Anti-pattern 5: feature flag tanpa mekanisme cleanup
// Flag yang sudah 100% rollout sejak 6 bulan tidak pernah dihapus
// Kode lama dan kondisional yang tidak perlu terus ada
// ✓ Solusi: feature flag punya expiry date, cleanup secara rutin

// ✗ Anti-pattern 6: cache secret terlalu lama
// Secret diambil saat startup, di-cache selamanya
// Secret rotation di Vault tidak berefek
// ✓ Solusi: cache secret dengan TTL pendek (< 1 jam)

Checklist Configuration Manager #

STRUKTUR KONFIGURASI:
  □ .env.example ada di repository dengan semua variable yang diperlukan
  □ .env ada di .gitignore (dan tidak pernah ter-commit)
  □ Semua variable terdokumentasi (nama, tipe, deskripsi, contoh nilai)
  □ Ada pemisahan jelas antara non-secret dan secret

SECRET MANAGEMENT:
  □ Tidak ada hardcoded secret di source code
  □ Tidak ada secret di config file yang di-commit ke git
  □ Secret diambil dari secret manager (Vault, AWS Secrets Manager, dll)
  □ Secret rotation bisa dilakukan tanpa downtime
  □ Audit log untuk akses ke secret tersedia

VALIDASI:
  □ Konfigurasi divalidasi saat startup aplikasi (fail fast)
  □ Pesan error yang jelas jika konfigurasi missing atau invalid
  □ Nilai default yang tidak aman menghasilkan warning

PER ENVIRONMENT:
  □ Konfigurasi production tidak bisa "bocor" ke development
  □ Ada mekanisme yang jelas untuk mengubah konfigurasi di setiap environment
  □ Konfigurasi staging semirip mungkin dengan production

FEATURE FLAG:
  □ Feature flag ada untuk fitur baru yang berisiko
  □ Ada mekanisme rollout percentage dan user whitelist
  □ Feature flag lama yang sudah 100% rollout secara reguler di-cleanup
  □ Ada kill switch untuk fitur yang bermasalah

OPERASIONAL:
  □ Mengubah konfigurasi tidak memerlukan deploy (kecuali yang struktural)
  □ Perubahan konfigurasi ter-audit (siapa mengubah apa kapan)
  □ Rollback konfigurasi bisa dilakukan dengan cepat

Ringkasan #

  • Konfigurasi di environment, bukan di kode — prinsip twelve-factor yang paling mendasar. Segala sesuatu yang berbeda antar deployment harus ada di environment variable, tidak hardcode.
  • .env.example adalah kontrak, .env adalah implementasi — .env.example di-commit ke git sebagai dokumentasi apa yang diperlukan. .env tidak pernah di-commit — berisi nilai aktual yang spesifik per environment.
  • Secret butuh perlakuan lebih dari environment variable biasa — gunakan secret manager (Vault, AWS Secrets Manager) untuk password, API key, dan private key. Secret manager menyediakan audit log, rotation otomatis, dan akses yang dikontrol.
  • Validasi konfigurasi saat startup, bukan saat digunakan — fail fast dengan pesan error yang jelas jauh lebih baik dari crash di tengah jalan dengan error yang membingungkan.
  • Feature flag memisahkan deployment dari release — kode bisa di-deploy kapan saja, fitur diaktifkan secara terpisah. Ini memungkinkan canary release, A/B testing, dan kill switch tanpa rollback kode.
  • Cache secret dengan TTL yang pendek — secret yang di-cache selamanya tidak bisa di-rotate. Ambil fresh secara reguler atau gunakan mekanisme rotation otomatis dari secret manager.
  • Buat .env.example selalu up-to-date — developer baru yang tidak mendapat semua konfigurasi yang diperlukan akan debugging berjam-jam. .env.example yang lengkap menghemat waktu semua orang.
  • Feature flag punya lifecycle — flag yang dibuat untuk rollout bertahap harus dihapus setelah 100% rollout. Flag yang menumpuk adalah technical debt yang memperlambat tim.
  • Konfigurasi production harus bisa berubah tanpa deploy — timeout, jumlah worker, rate limit threshold yang bisa diubah via environment variable tanpa perlu CI/CD cycle penuh.
  • Semua akses ke secret harus ter-audit — siapa yang mengakses secret mana kapan harus tercatat. Ini critical untuk compliance dan incident investigation.

← Sebelumnya: Infrastructure as Code   Berikutnya: Serverless →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact