Pipeline TokoKita yang sama berjalan di tiga tempat: laptop engineer, server staging, dan cluster production dengan data nyata. Kalimat “di laptop saya jalan” tidak bernilai bila ketiganya berperilaku berbeda — versi pustaka, endpoint, dan nilai rahasia yang tidak terkelola menghasilkan bug yang hanya muncul saat pengguna sudah terdampak. Chapter Konfigurasi Environment di section Persiapan Lingkungan telah membahas cara membaca environment variable; chapter ini menyusun strategi lengkapnya: satu berkas konfigurasi per lingkungan, loader yang memvalidasi aturan mainnya, dan alur promosi yang menjaga ketiga lingkungan tetap sejajar.

Tiga Lingkungan dan Tujuannya #

LingkunganIsi dataPengaksesTujuan utama
developmentsampel kecil, boleh sintetisengineeriterasi cepat, eksperimen bebas
stagingsalinan production tanpa data pribaditim dan QAuji integrasi menyerupai produksi
productiondata nyataterbatas dan ter-auditmelayani pengguna

Satu basis kode melayani ketiganya; yang berbeda hanyalah konfigurasi — dan perubahan hanya boleh mengalir satu arah: development, staging, lalu production. Aturan ini menjaga eksperimen di development tidak pernah menyentuh data nyata, sekaligus memastikan apa yang lolos staging adalah apa yang berjalan di production. Setiap promosi juga membawa identitas commit dan tag image yang sama, sehingga pertanyaan “versi apa yang sedang berjalan di production” selalu bisa dijawab dari riwayat Git dalam hitungan detik.

Konfigurasi per Lingkungan #

Setiap lingkungan punya satu berkas YAML dengan kunci yang sama persis dan nilai yang berbeda. Versi development mengarah ke layanan lokal:

yaml
lingkungan: development
database_url: postgresql://budi:YOUR_DATABASE_PASSWORD@localhost:5432/tokokita_dev
s3_bucket: data-toko-dev
s3_endpoint: http://localhost:9000
log_level: DEBUG
batch_size: 1000

Versi production dibuat oleh engineer yang terburu-buru — dan memuat kesalahan yang akan kita tangkap: kata sandi tertulis langsung di dalam berkas:

yaml
lingkungan: production
database_url: postgresql://budi:[email protected]:5432/tokokita
s3_bucket: data-toko-prod
s3_endpoint: https://s3.ap-southeast-3.amazonaws.com
log_level: WARNING
batch_size: 20000

Kata sandi dalam berkas yang dikomit ke Git adalah insiden keamanan yang menunggu waktu — berapa lama sampai repositori bocor hanyalah pertanyaan kapan. Kebijakan yang kita terapkan: di production, kunci rahasia wajib berupa referensi env:NAMA_VAR, dan nilainya disuplai orchestrator dari secret manager saat runtime. Di development, penampung YOUR_DATABASE_PASSWORD sudah memadai karena database lokal tidak melindungi data nyata; penampong hanya menandai bentuk nilai yang diharapkan, bukan menyimpan rahasia sungguhan.

Loader dengan Validasi #

Loader berikut memuat berkas sesuai nama lingkungan sekaligus memvalidasi tiga aturan: nama lingkungan harus dikenal, semua kunci wajib harus ada, dan di production rahasia inline ditolak:

python
"""Loader konfigurasi per lingkungan dengan validasi ketat.

Aturan: nama lingkungan harus development/staging/production; semua kunci
wajib harus ada; di production kunci rahasia wajib referensi "env:NAMA_VAR"
dan nilai inline (kata sandi tertulis langsung) ditolak.
"""
import pathlib
import shutil

import yaml

AKAR = pathlib.Path(__file__).resolve().parent / "config"
LINGKUNGAN_VALID = {"development", "staging", "production"}
KUNCI_WAJIB = {"lingkungan", "database_url", "s3_bucket", "s3_endpoint",
               "log_level", "batch_size"}
KUNCI_RAHASIA = {"database_url"}

def sembunyikan(url: str) -> str:
    """Ganti bagian kredensial database_url dengan *** agar aman dicetak."""
    skema, sisanya = url.split("://", 1)
    if "@" in sisanya:
        _, host = sisanya.rsplit("@", 1)
        return f"{skema}://***:@{host}"
    return url

def muat_konfigurasi(nama: str, kredensial: dict | None = None):
    """Muat config_<nama>.yaml, validasi, lalu resolve referensi env di production."""
    if nama not in LINGKUNGAN_VALID:
        raise ValueError("lingkungan tidak dikenal: {!r}; pilihan valid: {}".format(
            nama, sorted(LINGKUNGAN_VALID)))
    berkas = AKAR / f"config_{nama}.yaml"
    cfg = yaml.safe_load(berkas.read_text())
    kurang = KUNCI_WAJIB - set(cfg)
    if kurang:
        raise ValueError("{}: kunci wajib hilang: {}".format(berkas.name, sorted(kurang)))
    if cfg["lingkungan"] != nama:
        raise ValueError("{}: kunci lingkungan {!r} tidak cocok dengan nama berkas".format(
            berkas.name, cfg["lingkungan"]))
    if nama == "production":
        sumber = kredensial if kredensial is not None else {}
        for kunci in KUNCI_RAHASIA:
            nilai = cfg[kunci]
            if isinstance(nilai, str) and nilai.startswith("env:"):
                var = nilai[len("env:"):]
                if var not in sumber:
                    raise ValueError(f"production: variable {var} belum diatur")
                cfg[kunci] = sumber[var]
            else:
                raise ValueError("production: kunci {!r} berisi nilai inline; "
                                 "rahasia wajib referensi 'env:NAMA_VAR'".format(kunci))
    return cfg

def ringkas(cfg: dict) -> str:
    return "bucket={} log={} batch={} db={}".format(
        cfg["s3_bucket"], cfg["log_level"], cfg["batch_size"],
        sembunyikan(cfg["database_url"]))

# Mulai dari kondisi awal: production masih menyimpan kata sandi inline.
shutil.copy(AKAR / "config_production_inline.yaml", AKAR / "config_production.yaml")

for nama in ["development", "staging", "production", "produksi"]:
    try:
        print("[OK]    {:<12}: {}".format(nama, ringkas(muat_konfigurasi(nama))))
    except ValueError as galat:
        print("[GAGAL] {:<12}: {}".format(nama, galat))

print()
print("--- perbaikan: config_production.yaml memakai referensi env:DATABASE_URL ---")
shutil.copy(AKAR / "config_production_env.yaml", AKAR / "config_production.yaml")
try:
    cfg = muat_konfigurasi(
        "production",
        kredensial={"DATABASE_URL": "postgresql://budi:YOUR_DATABASE_PASSWORD"
                                    "@prod-db.tokokita.internal:5432/tokokita"})
    print("[OK]    {:<12}: {}".format("production", ringkas(cfg)))
except ValueError as galat:
    print("[GAGAL] {:<12}: {}".format("production", galat))

Output yang Dihasilkan:

text
[OK]    development : bucket=data-toko-dev log=DEBUG batch=1000 db=postgresql://***:@localhost:5432/tokokita_dev
[OK]    staging     : bucket=data-toko-staging log=INFO batch=5000 db=postgresql://***:@staging-db.tokokita.internal:5432/tokokita_staging
[GAGAL] production  : production: kunci 'database_url' berisi nilai inline; rahasia wajib referensi 'env:NAMA_VAR'
[GAGAL] produksi    : lingkungan tidak dikenal: 'produksi'; pilihan valid: ['development', 'production', 'staging']

--- perbaikan: config_production.yaml memakai referensi env:DATABASE_URL ---
[OK]    production  : bucket=data-toko-prod log=WARNING batch=20000 db=postgresql://***:@prod-db.tokokita.internal:5432/tokokita

Mari kita telaah baris-baris kode di atas:

  1. Baris 13-16 (LINGKUNGAN_VALID s.d. KUNCI_RAHASIA): aturan main dinyatakan sebagai data — daftar lingkungan sah, kunci wajib, dan kunci yang dianggap rahasia.
  2. Baris 26-28 (if nama not in LINGKUNGAN_VALID): typo seperti produksi gagal cepat dengan daftar pilihan sah, bukan melanjutkan dengan konfigurasi kosong.
  3. Baris 33-38 (validasi kunci): berkas yang kehilangan kunci wajib atau berisi nilai lingkungan yang tidak cocok dengan namanya langsung ditolak.
  4. Baris 40-51 (kebijakan production): kunci rahasia hanya boleh referensi env:NAMA_VAR; referensi di-resolve dari environment variable proses, sedangkan nilai inline ditolak dengan pesan yang menjelaskan perbaikannya.
  5. Baris 59-65 (skenario): development dan staging muat normal; production versi inline ditolak; produksi ditolak karena tidak dikenal; setelah berkas diperbaiki, production muat dengan kredensial dari environment variable.

Parity dan Alur Promosi #

Agar staging benar-benar menjamin production, kedua lingkungan harus parity — sama dalam hal-hal yang menentukan perilaku. Dengan kata lain, hasil uji di staging hanya bermakna bila staging meniru production sedekat mungkin:

AspekHarus samaBoleh berbeda
Image containertag versi sama persis-
Skema databaseversi migrasi samaisi data
Struktur konfigurasiset kunci samanilai per lingkungan
Uji otomatisrangkaian uji samadata uji
Rahasiareferensi env atau vaultnilai dari secret manager tiap lingkungan

Alur promosinya berjalan satu arah dan selalu membawa artefak yang sama:

Pola satu image yang dipromosikan (bukan dibangun ulang per lingkungan) menghilangkan selisih build sebagai sumber bug; selisih yang tersisa hanyalah nilai konfigurasi yang memang seharusnya berbeda. Compose juga mendukung substitusi nilai dari environment variable, sebagaimana didokumentasikan di https://docs.docker.com/compose/environment-variables/ .

Dengan lingkungan yang terpisah dan tervalidasi, satu wilayah infrastruktur masih belum kita bahas: bagaimana layanan-layanan ini saling menemukan dan berbicara. Aturan jaringan, subnet, dan kebijakan akses menjadi pokok bahasan chapter Konfigurasi Networking.