Dua chapter sebelumnya sudah menyiapkan dasarnya: Konfigurasi Environment di section Persiapan Lingkungan memperkenalkan os.environ dan berkas .env, sementara Environment Development dan Production menyusun pemisahan dev/staging/production beserta alur promosinya. Bab ini mengangkat persoalannya ke level deployment: artifact dari chapter Deployment Pipeline bersifat immutable, jadi satu-satunya bagian yang boleh berubah antar lingkungan adalah konfigurasi. Praktik twelve-factor menyebutnya config — kode menerima seluruh pengaturan dari luar, dan pipeline wajib memvalidasi pengaturan tersebut sebelum memproses satu baris data pun.

Prinsip Twelve-Factor: Konfigurasi Berlapis #

Prinsip konfigurasi twelve-factor menyimpan konfigurasi di environment, bukan di dalam kode. Untuk pipeline yang dideploy, kita pecah menjadi tiga lapis yang urutan prioritasnya naik:

LapisanIsiSiapa yang mengubah
defaults.yamlnilai bawaan aman untuk semua kunciengineer, direview di Git
config_<env>.yamlselisih per lingkungan: host, log levelengineer, direview di Git
environment variable TOKO_*nilai per deployment dan seluruh rahasiaorchestrator atau secret manager

Konfigurasi sebagai kode berarti setiap perubahan lapisan pertama dan kedua melewati pull request: ter-review, terdokumentasi, dan bisa di-revert. Lapisan ketiga sengaja tidak masuk Git — rahasia seperti kata sandi database hanya disuntikkan sebagai environment variable saat runtime, praktik yang dibahas tuntas pada chapter Pengelolaan Secrets. Konsekuensi lapisan ini perlu penjaga: kunci salah ketik harus ditolak, bukan diterima diam-diam.

Loader dengan Validasi Ketat #

Lapisan dasar mendefinisikan seluruh kunci yang dikenal beserta nilai bawaannya:

yaml
pipeline_nama: etl_pesanan
batch_size: 1000
retries: 3
log_level: INFO
db_host: localhost
db_port: 5432
db_password: ""

Overlay per lingkungan hanya menimpa sebagian kunci — versi staging misalnya cukup mengubah db_host dan menyerakan sisanya ke nilai bawaan:

yaml
db_host: staging-db.tokokita.internal
log_level: INFO

Loader berikut membaca ketiga lapisan, menolak kunci yang tidak dikenal, dan memaksa rahasia datang dari environment variable:

python
"""Loader konfigurasi 12-factor: defaults.yaml < overlay env < OS env var.

Lapisan (urutan naik): defaults.yaml, config_<env>.yaml, environment variable
berprefiks TOKO_. Validasi ketat: kunci tak dikenal ditolak, rahasia wajib
datang dari env var, tipe angka dipaksa, nilai dicek terhadap aturan main.
"""
import difflib
import os
from pathlib import Path

import yaml

AKAR = Path(__file__).parent
PREFIKS = "TOKO_"
RAHASIA = {"db_password"}
TIPE_ANGKA = {"batch_size": int, "retries": int, "db_port": int}
LOG_LEVEL = {"DEBUG", "INFO", "WARNING", "ERROR"}


def muat_config(overlay: Path) -> tuple[dict, dict]:
    """Kembalikan (config final, sumber tiap kunci) atau raise ValueError."""
    cfg = yaml.safe_load((AKAR / "defaults.yaml").read_text())
    asal = {kunci: "defaults.yaml" for kunci in cfg}
    dikenal = set(cfg)

    env_var = {k[len(PREFIKS):].lower(): v for k, v in os.environ.items()
               if k.startswith(PREFIKS)}
    lapisan = [(overlay.name, yaml.safe_load(overlay.read_text())),
               (f"env var {PREFIKS}*", env_var)]

    for sumber, isi in lapisan:
        for kunci, nilai in isi.items():
            if kunci not in dikenal:
                mirip = difflib.get_close_matches(kunci, sorted(dikenal), n=1)
                petunjuk = f" (maksudnya '{mirip[0]}'?)" if mirip else ""
                raise ValueError(f"{sumber}: kunci tidak dikenal "
                                 f"'{kunci}'{petunjuk}")
            if kunci in RAHASIA and not sumber.startswith("env var"):
                raise ValueError(f"{sumber}: kunci rahasia '{kunci}' tidak "
                                 "boleh di berkas; suplai lewat env var "
                                 f"{PREFIKS}{kunci.upper()}")
            cfg[kunci] = nilai
            asal[kunci] = sumber

    for kunci in RAHASIA:
        if not cfg[kunci]:
            raise ValueError(f"kunci wajib '{kunci}' kosong; suplai lewat "
                             f"env var {PREFIKS}{kunci.upper()}")

    for kunci, tipe in TIPE_ANGKA.items():
        try:
            cfg[kunci] = tipe(cfg[kunci])
        except (TypeError, ValueError):
            raise ValueError(f"kunci '{kunci}' bernilai {cfg[kunci]!r} — "
                             f"wajib bilangan bulat") from None

    if cfg["log_level"] not in LOG_LEVEL:
        raise ValueError(f"log_level {cfg['log_level']!r} tidak sah; "
                         f"pilihan: {sorted(LOG_LEVEL)}")
    if cfg["batch_size"] <= 0:
        raise ValueError(f"batch_size harus > 0; diterima {cfg['batch_size']}")
    return cfg, asal

Mari kita telaah bagian-bagian kuncinya:

  1. Kamus asal: setiap kunci mencatat lapisan terakhir yang menimpanya, sehingga keluaran bisa menunjukkan nilai mana yang berasal dari berkas dan mana dari environment variable — bukti layering yang bisa diperiksa.
  2. kunci not in dikenal: defaults.yaml menjadi daftar kunci sah; kunci apa pun di luar daftar langsung ditolak, dan difflib menyarankan kunci terdekat sehingga salah ketik batch_sise gagal dengan pesan yang menunjuk perbaikannya.
  3. Pemeriksaan RAHASIA: rahasia yang datang dari berkas ditolak sekalipun nilainya benar, dan rahasia yang kosong gagal — production tanpa TOKO_DB_PASSWORD tidak boleh jalan.
  4. TIPE_ANGKA + LOG_LEVEL: environment variable selalu berupa string, jadi int() memaksa tipe; nilai di luar aturan main (log level asing, batch_size nol) ditolak dengan pesan yang menyebut pilihan sah.

Skenario demo dijalankan lewat helper jalankan() yang mengatur ulang variabel TOKO_* per skenario, memanggil muat_config(), lalu mencetak status OK atau GAGAL; blok berikut adalah daftar skenarionya:

python
if __name__ == "__main__":
    # berkas typo untuk skenario 2: satu kunci salah ketik
    (AKAR / "config_typo.yaml").write_text("batch_sise: 20000\n")
    sandi = {"TOKO_DB_PASSWORD": "YOUR_DATABASE_PASSWORD"}

    jalankan("staging lengkap (overlay + env var)",
             AKAR / "config_staging.yaml",
             {"TOKO_BATCH_SIZE": "20000", **sandi})
    print()
    jalankan("overlay salah ketik 'batch_sise'",
             AKAR / "config_typo.yaml", sandi)
    jalankan("tipe salah: TOKO_RETRIES=tiga",
             AKAR / "config_staging.yaml", {"TOKO_RETRIES": "tiga", **sandi})
    jalankan("production tanpa TOKO_DB_PASSWORD",
             AKAR / "config_production.yaml", {})
    jalankan("production dengan secret dari env var",
             AKAR / "config_production.yaml", sandi)

Output yang Dihasilkan:

text
[OK]    staging lengkap (overlay + env var)
        db_host      = staging-db.tokokita.internal  (config_staging.yaml)
        batch_size   = 20000  (env var TOKO_*)
        log_level    = INFO  (config_staging.yaml)
        db_password  = ***  (env var TOKO_*)

[GAGAL] overlay salah ketik 'batch_sise': config_typo.yaml: kunci tidak dikenal 'batch_sise' (maksudnya 'batch_size'?)
[GAGAL] tipe salah: TOKO_RETRIES=tiga: kunci 'retries' bernilai 'tiga' — wajib bilangan bulat
[GAGAL] production tanpa TOKO_DB_PASSWORD: kunci wajib 'db_password' kosong; suplai lewat env var TOKO_DB_PASSWORD
[OK]    production dengan secret dari env var
        db_host      = prod-db.tokokita.internal  (config_production.yaml)
        batch_size   = 1000  (defaults.yaml)
        log_level    = WARNING  (config_production.yaml)
        db_password  = ***  (env var TOKO_*)

Baca baris-baris keluaran sebagai cerita layering: batch_size berakhir di env var karena lapisan ketiga menang, db_host di overlay, dan kunci yang tidak disentuh tetap membawa nilai defaults.yaml. Empat skenario gagal-sukses menunjukkan bahwa setiap jenis kesalahan konfigurasi — salah ketik, tipe salah, rahasia hilang — berhenti di startup dengan pesan yang menyebut perbaikannya.

Fail-Fast di Startup, Bukan di Tengah Run #

Semua validasi di atas sengaja dijalankan sebagai langkah pertama pipeline, sebelum koneksi database dibuka. Konfigurasi yang salah terdeteksi pada detik ke-0 dengan exit code bukan nol; tanpa fail-fast, kesalahan yang sama muncul sebagai kegagalan di tengah run setelah berjam-jam pemrosesan, atau lebih buruk: run sukses dengan nilai yang keliru.

Dengan konfigurasi yang tervalidasi, semua bahan deployment sudah lengkap: artifact immutable dari chapter sebelumnya plus konfigurasi per lingkungan yang aman. Langkah berikutnya menyatukan semuanya menjadi jalur otomatis dari commit sampai production — materi chapter Continuous Integration dan Delivery.