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:
| Lapisan | Isi | Siapa yang mengubah |
|---|---|---|
defaults.yaml | nilai bawaan aman untuk semua kunci | engineer, direview di Git |
config_<env>.yaml | selisih per lingkungan: host, log level | engineer, direview di Git |
environment variable TOKO_* | nilai per deployment dan seluruh rahasia | orchestrator 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:
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:
db_host: staging-db.tokokita.internal
log_level: INFOLoader berikut membaca ketiga lapisan, menolak kunci yang tidak dikenal, dan memaksa rahasia datang dari environment variable:
"""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, asalMari kita telaah bagian-bagian kuncinya:
- 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. kunci not in dikenal:defaults.yamlmenjadi daftar kunci sah; kunci apa pun di luar daftar langsung ditolak, dandifflibmenyarankan kunci terdekat sehingga salah ketikbatch_sisegagal dengan pesan yang menunjuk perbaikannya.- Pemeriksaan
RAHASIA: rahasia yang datang dari berkas ditolak sekalipun nilainya benar, dan rahasia yang kosong gagal — production tanpaTOKO_DB_PASSWORDtidak boleh jalan. TIPE_ANGKA+LOG_LEVEL: environment variable selalu berupa string, jadiint()memaksa tipe; nilai di luar aturan main (log level asing,batch_sizenol) 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:
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:
[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.