YAML Environment Variable Substitution#
Overview#
APISIX supports injecting environment variables into YAML configuration files using the ${{VAR_NAME}} syntax. This mechanism is implemented in apisix/cli/file.lua and historically operates post-parse — the raw YAML is loaded first by lyaml, then the resulting Lua table is walked recursively to substitute and type-coerce variable values.
As of v3.17.0 (PR #13078, merged April 2026): Standalone mode (
apisix.yaml) now uses a pre-parse path viaresolve_conf_var_in_text(), which runs substitution on raw YAML text beforeyaml.load(). The post-parse path (resolve_conf_var()) remains in use forconfig.yamland other non-standalone paths.
Syntax#
| Pattern | Behavior |
|---|---|
${{MY_VAR}} | Substitutes the value of MY_VAR; fails if not set |
${{MY_VAR:=default}} | Substitutes MY_VAR; falls back to default if not set |
The ${{...}} delimiter was chosen to avoid conflicts with Nginx's own $var and ${var} variable syntax .
Implementation#
var_sub(val) — String-level substitution#
var_sub is the core helper. It applies a gsub pattern to replace all ${{...}} placeholders in a single string:
- Extracts the variable name and optional default via the
:=separator - Trims whitespace from the default value
- Returns
(new_val, var_used, err)— thevar_usedflag tells callers whether substitution occurred
resolve_conf_var(conf) — Table-level, post-parse walk#
resolve_conf_var recursively traverses the parsed config table:
- Substitutes env vars in both keys and values
- For string values, calls
var_sub; if a variable was used, applies type coercion :tonumber(new_val) ~= nil→ Lua number"true"/"false"→ Lua boolean- Otherwise → remains a string
- Exported as
_M.resolve_conf_varfor use by other modules
resolve_conf_var_in_text(text) — Raw-text, pre-parse substitution (standalone mode)#
Introduced in PR #13078, this function runs var_sub() on the raw YAML string before yaml.load() is called. Pre-parse substitution lets YAML's native type system determine the final type:
"${{VAR}}"(quoted in YAML) → always a string${{VAR}}(unquoted in YAML) → type inferred from the resolved value
This path is used for apisix.yaml in read_yaml_conf() and in apisix/core/config_yaml.lua for the runtime YAML config loader .
Reserved env var overrides#
A separate function replace_by_reserved_env_vars handles APISIX_DEPLOYMENT_ETCD_HOST, which accepts a JSON-encoded array and directly sets deployment.etcd.host. This path bypasses the ${{...}} mechanism entirely.
Type Inference Rules#
Post-parse path (config.yaml): Type coercion fires when var_used is true .
Large integer edge case: Values exceeding 15 digits risk precision loss when cast to Lua doubles (IEEE 754). PR #13078 added an exceeds_lua_precision() guard that skips tonumber() for pure-integer values longer than 15 digits, preserving them as strings. This fixes issue #12932 where values like 356002209726529540 were silently converted to scientific notation.
Pre-parse path (apisix.yaml): No Lua-side coercion needed — YAML's own parser determines the type based on quoting and content.
Call Chains#
config.yaml path :
read_yaml_conf()
→ yaml.load(user_conf_yaml) # parse raw YAML
→ resolve_conf_var(user_conf) # post-parse substitution + type coerce
→ merge_conf(default_conf, user_conf)
→ schema.validate(default_conf)
→ replace_by_reserved_env_vars() # APISIX_DEPLOYMENT_ETCD_HOST override
apisix.yaml / standalone mode (v3.17.0+) :
read_yaml_conf()
→ resolve_conf_var_in_text(raw_yaml) # pre-parse substitution on raw text
→ yaml.load(resolved_yaml) # YAML type inference
Known Issues#
Comment-line processing bug (v3.17.0 — issue #13685)#
The pre-parse substitution path has no awareness of YAML comment syntax. Any ${{VAR}} reference in a commented-out line (e.g., # ${{MY_ENV_VAR}}) is evaluated, causing a boot failure if the variable is not set :
failed to read local yaml config of apisix: failed to handle configuration: can't find environment variable MY_ENV_VAR
The existing is_empty_yaml_line helper (which matches '^%s*#') is only used to detect whether an entire config file is blank — it does not filter lines before the var_sub scan.
Workaround: Set the variable to any value, or remove the commented-out placeholder.
Affected version: v3.17.0 .
Key Source Files#
| File | Purpose |
|---|---|
apisix/cli/file.lua | Core implementation: var_sub, resolve_conf_var, read_yaml_conf |
apisix/core/config_yaml.lua | Runtime YAML config loader; uses resolve_conf_var_in_text post-3.17.0 |
Related Issues & PRs#
- PR #13078 — Introduces pre-parse substitution for standalone mode; fixes large-integer precision loss (issue #12932)
- PR #13503 — Release 3.17.0 (includes PR #13078 as a breaking change)
- Issue #13685 — Comment-line processing regression introduced in v3.17.0