Cara Pakai Environment Variables di YAML
Salah satu kebutuhan paling umum saat menulis file konfigurasi YAML adalah menyisipkan nilai yang berbeda-beda tergantung environment — misalnya URL database yang beda antara development, staging, dan production, atau API key yang tidak boleh di-hardcode langsung di file yang di-commit ke Git. Di sinilah environment variable berperan. Tapi ada satu hal penting yang perlu dipahami dulu: YAML sebagai spesifikasi murni tidak punya fitur interpolasi environment variable bawaan. Kemampuan ini datang dari tool yang membaca file YAML tersebut, bukan dari YAML itu sendiri.
Kenapa YAML Sendiri Tidak Punya Fitur Ini?
YAML adalah format data serialization murni — tugasnya cuma merepresentasikan struktur data (map, list, scalar), bukan bahasa template atau bahasa pemrograman. Jadi kalau kamu menulis ${DB_HOST} di file YAML lalu di-parse pakai library YAML polos (seperti PyYAML tanpa modifikasi), hasilnya cuma string literal "${DB_HOST}", bukan nilai environment variable yang sesungguhnya. Interpolasi itu harus dilakukan oleh tool di atas YAML — baik lewat library tambahan, preprocessing, atau fitur bawaan tool spesifik seperti Docker Compose. Memahami batasan ini penting supaya kamu tidak menghabiskan waktu debugging "kenapa variabel saya tidak ke-replace", padahal memang bukan tanggung jawab YAML itu sendiri untuk melakukannya.
Docker Compose: Interpolasi Bawaan
Docker Compose adalah salah satu tool yang punya dukungan interpolasi environment variable bawaan, langsung di file docker-compose.yml:
services:
web:
image: myapp:${APP_VERSION}
environment:
- DATABASE_URL=postgres://user:pass@${DB_HOST}:${DB_PORT}/mydb
ports:
- "${HOST_PORT}:3000"
Nilai APP_VERSION, DB_HOST, DB_PORT, dan HOST_PORT diambil dari environment shell saat menjalankan docker compose up, atau dari file .env yang ditaruh sejajar dengan docker-compose.yml. Docker Compose juga mendukung default value kalau variabel tidak diset:
image: myapp:${APP_VERSION:-latest}
Sintaks :- berarti "pakai nilai default latest kalau APP_VERSION tidak diset atau kosong". Ada juga - tanpa titik dua yang cuma cek apakah variabel di-set (bukan kosong), perbedaannya halus tapi penting untuk edge case string kosong.
GitHub Actions: Konteks env dan secrets
GitHub Actions punya sintaks sendiri untuk mengakses environment variable dan secrets, memakai ekspresi ${{ }}:
jobs:
deploy:
runs-on: ubuntu-latest
env:
NODE_ENV: production
steps:
- name: Deploy
run: ./deploy.sh
env:
API_KEY: ${{ secrets.API_KEY }}
REGION: ${{ vars.DEPLOY_REGION }}
Perhatikan bedanya dengan Docker Compose: GitHub Actions memakai ${{ }} (dua kurung kurawal), bukan ${ }. Ini contoh kenapa penting membaca dokumentasi tool spesifik — sintaks interpolasi environment variable tidak universal, tergantung tool apa yang memproses file YAML tersebut.
Kubernetes: Environment Variable di Dalam Container
Berbeda dengan dua contoh sebelumnya, Kubernetes manifest biasanya tidak melakukan interpolasi pada file YAML-nya sendiri — sebaliknya, YAML dipakai untuk mendefinisikan environment variable yang nanti di-inject ke dalam container saat runtime:
apiVersion: v1
kind: Pod
metadata:
name: myapp
spec:
containers:
- name: myapp
image: myapp:latest
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: db-secret
key: url
- name: APP_ENV
value: "production"
Kalau kamu butuh nilai dinamis di file manifest Kubernetes itu sendiri (bukan di dalam container), biasanya dipakai tool tambahan seperti Helm (template {{ .Values.dbHost }}) atau Kustomize, bukan environment variable shell langsung.
Interpolasi Manual dengan envsubst
Untuk tool yang tidak punya dukungan interpolasi bawaan, pendekatan umum adalah menulis file YAML dengan placeholder, lalu memprosesnya lewat command line sebelum dipakai:
# template.yaml
database:
host: ${DB_HOST}
port: ${DB_PORT}
export DB_HOST=localhost
export DB_PORT=5432
envsubst < template.yaml > config.yaml
envsubst adalah utility bawaan Linux (paket gettext) yang menggantikan semua ${VAR} dengan nilai environment variable yang sesuai, menghasilkan file YAML baru yang sudah "matang" tanpa placeholder.
Interpolasi di Python dengan PyYAML + os.environ
Kalau kamu memproses YAML lewat kode Python dan butuh interpolasi sendiri, pola umum adalah custom constructor:
import yaml, os, re
pattern = re.compile(r'\$\{([^}^{]+)\}')
def env_constructor(loader, node):
value = loader.construct_scalar(node)
return pattern.sub(lambda m: os.environ.get(m.group(1), ''), value)
yaml.add_implicit_resolver('!env', pattern)
yaml.add_constructor('!env', env_constructor)
config = yaml.load(open('config.yaml'), Loader=yaml.FullLoader)
Pendekatan ini memberi kontrol penuh, termasuk menentukan behavior kalau variabel tidak ditemukan (error keras, string kosong, atau default value).
Strategi Multi-Environment: Satu File vs Banyak File
Ada dua pendekatan umum untuk mengelola konfigurasi berbeda per environment. Pendekatan pertama, satu file YAML dengan section per environment (development, staging, production), lalu aplikasi membaca section yang relevan berdasarkan environment variable seperti APP_ENV. Pendekatan kedua, file terpisah per environment (config.development.yaml, config.production.yaml) yang di-load sesuai kebutuhan. Pendekatan kedua biasanya lebih mudah dibaca dan lebih aman karena file production bisa dipisahkan aksesnya secara lebih ketat, tapi pendekatan pertama lebih mudah dibandingkan perbedaan antar environment karena semuanya ada dalam satu file yang sama.
Kombinasi keduanya juga umum: file base berisi konfigurasi default yang dipakai bersama, lalu file override per environment yang di-merge saat runtime memakai tool seperti Helm values atau library konfigurasi seperti config di Node.js yang mendukung layering otomatis berdasarkan NODE_ENV. Apapun pendekatan yang dipilih, pastikan tim sepakat satu pola dan menuliskannya di README project supaya developer baru tidak bingung harus mengubah konfigurasi di file mana untuk environment tertentu.
Praktik Keamanan yang Perlu Diperhatikan
- Jangan pernah commit nilai sensitif langsung ke YAML — selalu pakai environment variable atau secret manager (Vault, AWS Secrets Manager, Kubernetes Secrets) untuk API key, password, dan token.
- Sertakan file
.env.example— file contoh berisi nama variabel tanpa nilai asli, supaya developer lain tahu variabel apa saja yang dibutuhkan tanpa perlu membocorkan nilai production. - Pastikan
.envmasuk.gitignore— kesalahan umum adalah lupa menambahkan file environment ke gitignore sehingga tanpa sengaja ter-commit ke repository publik. - Validasi environment variable wajib ada saat startup — aplikasi sebaiknya gagal cepat (fail fast) dengan pesan error jelas kalau variabel penting tidak diset, daripada berjalan dengan nilai kosong yang menyebabkan bug aneh di kemudian hari. Pola umum adalah membuat fungsi
requireEnv(name)yang melempar exception langsung di awal aplikasi kalau variabel wajib tidak ditemukan, sehingga masalah konfigurasi terdeteksi dalam hitungan detik saat deploy, bukan berjam-jam kemudian saat fitur tertentu baru diakses user.
How to Use Environment Variables in YAML
One of the most common needs when writing YAML config files is inserting values that differ per environment — like a database URL that differs between development, staging, and production, or an API key that shouldn't be hardcoded into a file committed to Git. That's where environment variables come in. But there's one important thing to understand first: YAML as a pure spec has no built-in environment variable interpolation feature. That capability comes from the tool reading the YAML file, not from YAML itself.
Why Doesn't YAML Have This Natively?
YAML is purely a data serialization format — its job is only to represent data structures (maps, lists, scalars), not to be a templating or programming language. So if you write ${DB_HOST} in a YAML file and parse it with a plain YAML library (like PyYAML unmodified), the result is just the literal string "${DB_HOST}", not an actual environment variable value. Interpolation has to be handled by a layer on top of YAML — either an extra library, preprocessing, or a specific tool's built-in feature like Docker Compose.
Docker Compose: Built-in Interpolation
Docker Compose is one tool with built-in environment variable interpolation, directly in the docker-compose.yml file:
services:
web:
image: myapp:${APP_VERSION}
environment:
- DATABASE_URL=postgres://user:pass@${DB_HOST}:${DB_PORT}/mydb
ports:
- "${HOST_PORT}:3000"
The values for APP_VERSION, DB_HOST, DB_PORT, and HOST_PORT are pulled from the shell environment when running docker compose up, or from a .env file placed next to docker-compose.yml. Docker Compose also supports default values when a variable isn't set:
image: myapp:${APP_VERSION:-latest}
The :- syntax means "use the default value latest if APP_VERSION is unset or empty." There's also a bare - without the colon that only checks whether the variable is set (not whether it's empty) — a subtle but important difference for empty-string edge cases.
GitHub Actions: The env and secrets Contexts
GitHub Actions has its own syntax for accessing environment variables and secrets, using ${{ }} expressions:
jobs:
deploy:
runs-on: ubuntu-latest
env:
NODE_ENV: production
steps:
- name: Deploy
run: ./deploy.sh
env:
API_KEY: ${{ secrets.API_KEY }}
REGION: ${{ vars.DEPLOY_REGION }}
Notice the difference from Docker Compose: GitHub Actions uses ${{ }} (double curly braces), not ${ }. This illustrates why it's important to read the specific tool's documentation — environment variable interpolation syntax isn't universal; it depends on which tool is processing the YAML file.
Kubernetes: Environment Variables Inside a Container
Unlike the two examples above, Kubernetes manifests typically don't interpolate the YAML file itself — instead, YAML is used to define environment variables that get injected into the container at runtime:
apiVersion: v1
kind: Pod
metadata:
name: myapp
spec:
containers:
- name: myapp
image: myapp:latest
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: db-secret
key: url
- name: APP_ENV
value: "production"
If you need dynamic values in the manifest file itself (not inside the container), an extra tool like Helm (with {{ .Values.dbHost }} templates) or Kustomize is typically used, rather than direct shell environment variables.
Manual Interpolation with envsubst
For tools without built-in interpolation, a common approach is writing a YAML file with placeholders and processing it via the command line before use:
# template.yaml
database:
host: ${DB_HOST}
port: ${DB_PORT}
export DB_HOST=localhost
export DB_PORT=5432
envsubst < template.yaml > config.yaml
envsubst is a built-in Linux utility (part of the gettext package) that replaces every ${VAR} with the matching environment variable's value, producing a new, "resolved" YAML file with no placeholders left.
Interpolation in Python with PyYAML + os.environ
If you're processing YAML through Python code and need custom interpolation, a common pattern is a custom constructor:
import yaml, os, re
pattern = re.compile(r'\$\{([^}^{]+)\}')
def env_constructor(loader, node):
value = loader.construct_scalar(node)
return pattern.sub(lambda m: os.environ.get(m.group(1), ''), value)
yaml.add_implicit_resolver('!env', pattern)
yaml.add_constructor('!env', env_constructor)
config = yaml.load(open('config.yaml'), Loader=yaml.FullLoader)
This approach gives you full control, including deciding what happens if a variable isn't found (hard error, empty string, or a default value). You can extend this pattern further to support default value syntax like ${VAR:default} by adjusting the regular expression and constructor logic, mirroring how Docker Compose's built-in interpolation behaves.
Multi-Environment Strategy: One File vs Multiple Files
There are two common approaches for managing different configuration per environment. The first: one YAML file with a section per environment (development, staging, production), with the application reading the relevant section based on an environment variable like APP_ENV. The second: separate files per environment (config.development.yaml, config.production.yaml) loaded as needed. The second approach is usually easier to read and safer since the production file's access can be restricted more tightly, but the first approach makes it easier to compare differences between environments since everything lives in one file.
A combination of both is also common: a base file holds shared default configuration, then per-environment override files get merged at runtime using a tool like Helm values or a configuration library like config in Node.js, which supports automatic layering based on NODE_ENV.
Security Practices Worth Keeping in Mind
- Never commit sensitive values directly into YAML — always use environment variables or a secret manager (Vault, AWS Secrets Manager, Kubernetes Secrets) for API keys, passwords, and tokens.
- Include a
.env.examplefile — a sample file with variable names but no real values, so other developers know which variables are required without leaking production values. - Make sure
.envis in.gitignore— a common mistake is forgetting to add the environment file to gitignore, causing it to accidentally get committed to a public repository. - Validate required environment variables at startup — an application should fail fast with a clear error message if a critical variable is missing, rather than running with an empty value that causes weird bugs down the line.