JSONYAMify

Home / Blog / YAML Environment Variables

Cara Pakai Environment Variables di YAML

Oleh Andi Putra Ogie · Update: Juli 2026 · 9 menit baca

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

💡 Sebelum commit atau deploy, cek dulu apakah file YAML kamu benar-benar valid setelah placeholder environment variable diganti. Gunakan JSONYAMify untuk memvalidasi hasil akhir YAML kamu sebelum dipakai di production.
🔧 Validasi YAML Kamu di JSONYAMify

How to Use Environment Variables in YAML

By Andi Putra Ogie · Updated: July 2026 · 9 min read

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

💡 Before committing or deploying, double-check that your YAML is actually valid after environment variable placeholders are resolved. Use JSONYAMify to validate the final resolved YAML before it goes to production.
🔧 Validate Your YAML on JSONYAMify