Aplikasi yang sama berjalan di tiga tempat: laptop Budi, server staging, dan produksi. Yang membedakan ketiganya bukan kodenya — melainkan alamat database, level logging, dan kredensial. Bila perbedaan ini ditulis di dalam kode, setiap rilis butuh cabang khusus dan secret tercecer di repository. Prinsip yang menyelesaikan masalah ini datang dari metodologi 12-factor, dan chapter ini mempraktikkannya di Python: dari pola paling sederhana sampai validasi otomatis.

Prinsip 12-Factor: Konfigurasi di Environment #

Aturan ketujuh metodologi 12-factor berbunyi singkat: simpan konfigurasi di environment. Kode berisi satu set nilai bawaan yang aman; setiap environment — dev, staging, produksi — menginjeksi nilainya sendiri lewat environment variable. Hasilnya, satu berkas .pyz atau image Docker yang sama dapat dideploy ke mana saja tanpa perubahan sepatah kode pun.

EnvironmentTOKO_DEBUGTOKO_LOG_LEVELSumber nilai
devtrueDEBUG.env lokal
stagingfalseINFOkonfigurasi platform
produksifalseWARNINGsecret manager

os.environ dan .env Lokal #

Pembacaan nilainya memakai dictionary os.environ. Untuk pengembangan lokal, package python-dotenv memuat berkas .env ke environment saat aplikasi start:

python
import os

from dotenv import load_dotenv

# Simulasi variabel yang sudah ada di environment saat proses berjalan
os.environ["TOKO_DEBUG"] = "false"

load_dotenv("contoh.env")

print(f"TOKO_DEBUG = {os.environ['TOKO_DEBUG']!r}")
print(f"TOKO_NAME = {os.environ['TOKO_NAME']!r}")
print(f"TOKO_DATABASE_URL = {os.environ['TOKO_DATABASE_URL']!r}")

Berkas contoh.env berisi tiga baris: TOKO_NAME=toko-buku, TOKO_DEBUG=true, dan TOKO_DATABASE_URL=postgresql://[email protected]/toko.

Output yang Dihasilkan:

text
TOKO_DEBUG = 'false'
TOKO_NAME = 'toko-buku'
TOKO_DATABASE_URL = 'postgresql://[email protected]/toko'

Perhatikan kejutan pertama pada output: nilai TOKO_DEBUG tetap 'false' meski .env berisi true. Fungsi load_dotenv() sengaja tidak menimpa variabel yang sudah ada di environment — environment asli selalu menang. Perilaku ini menguntungkan di produksi (nilai platform tidak tertimpa berkas lokal), tetapi menjebak saat debugging bila kita lupa sebuah variabel sudah terset.

Validasi dengan pydantic-settings #

Pembacaan mentah meninggalkan pekerjaan konversi dan validasi ke programmer. Package pydantic-settings menyusun konfigurasi sebagai class bertipe, lengkap dengan konversi otomatis:

python
import os

os.environ["APP_NAME"] = "toko-buku"
os.environ["APP_DEBUG"] = "false"
os.environ["APP_MAX_ITEM_PER_HALAMAN"] = "25"

from pydantic_settings import BaseSettings, SettingsConfigDict


class Settings(BaseSettings):
    model_config = SettingsConfigDict(env_prefix="APP_")

    name: str
    debug: bool
    max_item_per_halaman: int


settings = Settings()
print(f"name = {settings.name!r}")
print(f"debug = {settings.debug!r}")
print(f"max_item_per_halaman = {settings.max_item_per_halaman!r}")

Output yang Dihasilkan:

text
name = 'toko-buku'
debug = False
max_item_per_halaman = 25

Mari kita telaah bagian kuncinya:

  1. env_prefix="APP_": class hanya membaca variabel berawalan APP_ — variabel milik aplikasi lain di server yang sama tidak saling menabrak.
  2. debug: bool dan max_item_per_halaman: int: string environment "false" dan "25" otomatis dikonversi ke bool dan int dengan aturan pydantic; bila nilainya tidak bisa dikonversi, error validasi muncul saat pembuatan object.
  3. settings = Settings(): seluruh konfigurasi kini hidup sebagai satu object — aplikasi membacanya sebagai settings.debug, bukan mengulang os.environ di sepuluh tempat.

Gagal Cepat saat Startup #

Kesalahan konfigurasi yang baru terdeteksi pada permintaan pertama di tengah malam adalah mimpi buruk operasional. Pola fail fast memvalidasi semua kebutuhan di saat aplikasi dinyalakan:

python
import os
import sys
from dataclasses import dataclass


@dataclass(frozen=True)
class Config:
    database_url: str
    secret_key: str
    log_level: str


def load_config(env: dict[str, str]) -> Config:
    wajib = ["DATABASE_URL", "APP_SECRET_KEY"]
    hilang = [kunci for kunci in wajib if not env.get(kunci)]
    if hilang:
        raise SystemExit(
            "Konfigurasi tidak lengkap, aplikasi berhenti: "
            + ", ".join(hilang)
        )

    return Config(
        database_url=env["DATABASE_URL"],
        secret_key=env["APP_SECRET_KEY"],
        log_level=env.get("LOG_LEVEL", "INFO"),
    )


# Skenario 1: environment lengkap
config = load_config(
    {
        "DATABASE_URL": "postgresql://[email protected]/toko",
        "APP_SECRET_KEY": "YOUR_API_TOKEN",
    }
)
print(f"log_level default = {config.log_level!r}")

# Skenario 2: environment kosong
try:
    load_config({})
except SystemExit as e:
    print(f"SystemExit: {e}")

Output yang Dihasilkan:

text
log_level default = 'INFO'
SystemExit: Konfigurasi tidak lengkap, aplikasi berhenti: DATABASE_URL, APP_SECRET_KEY

Exception SystemExit dipilih karena pesannya tercetak langsung oleh interpreter dan proses berhenti dengan exit code 1 — sinyal yang dibaca orchestrator seperti Docker dan systemd untuk mencatat startup gagal. Skenario pertama juga memperlihatkan kebijakan nilai default: LOG_LEVEL yang kosong jatuh ke "INFO" yang aman.

Nilai Default dan Secret Manager #

Kebijakan nilai default perlu dipilih sadar. Nilai yang tidak kritis (level log, batas halaman, flag debug) pantas punya default aman agar dev tidak mengetik apa-apa. Nilai kritis — terutama secret — justru tidak boleh punya default sama sekali: ketiadaannya harus memicu kegagalan startup, bukan diam-diam memakai password kosong. Untuk penyimpanannya, layanan secret manager menjadi sumber kebenaran di produksi:

LayananEkosistem
AWS Secrets ManagerAmazon Web Services
Google Cloud Secret ManagerGoogle Cloud
Azure Key VaultMicrosoft Azure
HashiCorp Vaultlintas cloud, self-hosted

Ragam opsi dan pola pemakaiannya sudah dibahas pada Chapter Mengelola Secrets; pola aksesnya konsisten — aplikasi mengambil secret saat startup, lalu berperilaku sama seperti membaca environment biasa.

Konfigurasi kini aman di luar kode. Berikutnya: mengirim aplikasi yang sudah terkonfigurasi ini ke internet.