Подстановка env-переменных
Значения в YAML-конфиге могут ссылаться на переменные окружения через синтаксис ${...}. Интерполяция выполняется при загрузке файла, до слияния слоёв и до типизированного доступа.
Синтаксис
${VAR} → значение env-переменной VAR; если не задана — ConfigError
${VAR:-default} → значение VAR, или literal "default" если VAR не задана / пустая
Примеры:
database:
host: "${DB_HOST:-localhost}"
port: "${DB_PORT:-5432}"
password: "${DB_PASSWORD}"
pool_size: "${DB_POOL_SIZE:-20}" # всё ещё строка, число приводится к типу позже
cache:
url: "${REDIS_URL:-redis://localhost:6379/0}"
Правила семантики
-
Время интерполяции — при загрузке файла (не при каждом чтении). Меняется env-переменная после
Config.load()→ конфиг не обновляется автоматически; для hot-reload нуженConfigSource.watch. -
Строковый тип после интерполяции —
${NUM}возвращает строку. Type coercion (getInt,getBool) происходит при чтении, не при интерполяции:database:pool_size: "${DB_POOL_SIZE:-20}" # строка "20" после интерполяции- Python
- TypeScript
- Go
config.getInt("database.pool_size") # 20 — приводится к типу на летуconfig.getString("database.pool_size") # "20" — остаётся строкойconfig.getInt("database.pool_size"); // 20config.getString("database.pool_size"); // "20"cfg.GetInt("database.pool_size") // 20cfg.GetString("database.pool_size") // "20" -
Правила приведения типов (строгие):
int:^-?\d+$.bool:true|false|yes|no|1|0(case-insensitive).float: стандартный парсер чисел с плавающей точкой.- Если строка не соответствует типу →
ConfigError(type_mismatch).
-
Default-значения — literal string — вложенные
${...}в default-части не интерполируются. Это оставляет парсер простым:database:# ОК:host: "${DB_HOST:-localhost}"# НЕ работает (default literal):password: "${DB_PASSWORD:-${FALLBACK_PASSWORD}}" # default будет "${FALLBACK_PASSWORD}" как строкаЕсли нужен двухэтапный резервный вариант — используйте внешний скрипт / shell-exec, или логику в приложении.
-
Escape literal
$—$$→$. Полезно, когда литерально нужен символ$:billing:price_prefix: "$$" # literal "$"
Отсутствующая env без default
database:
password: "${DB_PASSWORD}" # без default
Если DB_PASSWORD не задана в окружении, Config.load выбрасывает ConfigError:
ConfigError(
path="database.password",
reason="env_unresolved",
details="Environment variable DB_PASSWORD is not set and no default provided",
)
Приложение получает явную ошибку на старте — пароль не подставляется пустой строкой, что привело бы к рантайм-проблемам («authentication failed»).
Где писать env-переменные
Локальная разработка: .env-файл, подхватываемый инструментом (direnv / dotenv):
DB_HOST=localhost
DB_PASSWORD=local-dev-pw
REDIS_URL=redis://localhost:6379/0
DAGSTACK_ENV=
Docker / production: через env: в docker-compose.yml / kubernetes/deployment.yaml.
CI: через secrets-mechanism CI-системы (GitHub Actions Secrets / Gitea Secrets).
dagstack/config не читает .env-файлы сам — это ответственность того, кто запускает процесс (Python python-dotenv, Node dotenv, direnv, и т.п.). К моменту запуска приложения нужные переменные уже в os.environ / process.env.
Подстановка в разных слоях
Env-интерполяция применяется к каждому файловому слою независимо. Результат: разные слои могут ссылаться на разные env-переменные, merge работает на уже интерполированных значениях.
database:
password: "${DB_PASSWORD}"
database:
password: "${DB_PASSWORD_PRODUCTION}" # отдельная env-переменная в prod
Для DAGSTACK_ENV=production:
- Загружается base →
${DB_PASSWORD}→"local-pw"из окружения. - Загружается production →
${DB_PASSWORD_PRODUCTION}→"prod-pw". - Merge: значение production побеждает →
password: "prod-pw".
Типичные анти-паттерны
# ✗ Слишком много магии — приложение зависит от специфических env:
feature_flags:
new_ui_enabled: "${NEW_UI_ENABLED:-true}" # зачем env-переопределение для каждого флага?
# ✓ Лучше — базовое значение в YAML, переопределение через app-config.${ENV}.yaml:
feature_flags:
new_ui_enabled: true
# ✗ Секрет с default-значением-заглушкой:
database:
password: "${DB_PASSWORD:-admin123}" # в prod без env — попадает default (опасно!)
# ✓ Без default — явная ошибка на старте если забыли env:
database:
password: "${DB_PASSWORD}"
# ✗ Default содержит env-переменную (не будет интерполирован):
database:
url: "${DATABASE_URL:-postgresql://user:${DB_PASSWORD}@localhost/dev}"
# ✓ Использовать отдельную env или логику в приложении:
database:
url: "${DATABASE_URL}"
# Сборка url'а из частей — в коде приложения, не в YAML.
См. также
- Секреты — env как основной канал передачи секретов.
- Слои конфигурации — env-подстановка в каждом слое.
- ADR-0001 §2 Env interpolation syntax — нормативный контракт.