env_var(): чтение environment variables и секреты
В прошлом уроке мы изучили var() — параметры проекта. Теперь — env_var(), который читает значения из окружения операционной системы. Это другой инструмент с другой задачей: var — для конфигурации, версионированной в git; env_var — для секретов (пароли, API-ключи) и runtime-окружения (среда, dev/prod), которые не должны попадать в репозиторий.
Базовый синтаксис
{{ env_var('NAME_OF_VAR') }}
Возвращает значение переменной окружения NAME_OF_VAR. Если переменная не установлена — исключение при компиляции. То есть в отличие от var(), env_var без default обязателен.
С default
{{ env_var('NAME_OF_VAR', 'default_value') }}
Если NAME_OF_VAR не установлена — берётся 'default_value'. Аналог var('x', default).
Где можно использовать
Везде, где работает Jinja:
- В моделях.
- В macros.
- В
dbt_project.yml. - В
profiles.yml(главный use case). - В YAML-конфигурациях (
_models.yml,_sources.yml).
Use case #1: профили без хардкода паролей
Это главное применение env_var. Допустим, ваш profiles.yml для Postgres:
# profiles.yml
jaffle_shop:
target: dev
outputs:
dev:
type: postgres
host: localhost
user: dbt_user
password: super_secret_pass_123 # <-- В git!! Катастрофа.
port: 5432
dbname: jaffle_shop
schema: dbt_dev
Если этот файл попадёт в git — пароль утёк всему миру. Решение: вынести в env var.
# profiles.yml
jaffle_shop:
target: dev
outputs:
dev:
type: postgres
host: "{{ env_var('DBT_POSTGRES_HOST', 'localhost') }}"
user: "{{ env_var('DBT_POSTGRES_USER') }}"
password: "{{ env_var('DBT_POSTGRES_PASS') }}"
port: "{{ env_var('DBT_POSTGRES_PORT', '5432') | int }}"
dbname: "{{ env_var('DBT_POSTGRES_DB', 'jaffle_shop') }}"
schema: "{{ env_var('DBT_POSTGRES_SCHEMA', 'dbt_dev') }}"
Теперь профиль в git без секретов, а пользователь устанавливает переменные перед запуском:
$ export DBT_POSTGRES_USER='alice'
$ export DBT_POSTGRES_PASS='secret_pass_from_vault'
$ dbt run
В CI/CD — через секреты pipeline (GitHub Actions secrets, GitLab CI variables, AWS Secrets Manager).
Заметьте | int после env_var('DBT_POSTGRES_PORT', '5432'). Env vars всегда строки. Если ожидается число (port, threads), приводите явно: | int. Без этого можно получить port: '5432' — string, что warehouse-driver не примет.
Use case #2: DuckDB path через env
Если используете DuckDB локально:
# profiles.yml
jaffle_shop:
target: dev
outputs:
dev:
type: duckdb
path: "{{ env_var('DBT_DUCKDB_PATH', 'dev.duckdb') }}"
threads: "{{ env_var('DBT_DUCKDB_THREADS', '4') | int }}"
schema: "{{ env_var('DBT_DUCKDB_SCHEMA', 'main') }}"
Это даёт гибкость:
# Локально — обычный файл
$ dbt run
# В CI — temp файл, чтобы не пересекаться
$ DBT_DUCKDB_PATH=/tmp/ci_test_$RANDOM.duckdb dbt run --select +my_model
Use case #3: dev vs prod через env
Многие команды держат один профиль и переключают target через env var:
# profiles.yml
jaffle_shop:
target: "{{ env_var('DBT_TARGET', 'dev') }}"
outputs:
dev:
type: duckdb
path: dev.duckdb
prod:
type: postgres
host: prod.example.com
user: "{{ env_var('DBT_PROD_USER') }}"
password: "{{ env_var('DBT_PROD_PASS') }}"
...
# Запуск в dev (по умолчанию)
$ dbt run
# Запуск в prod
$ DBT_TARGET=prod dbt run
# Или явно через CLI
$ dbt run --target prod
Обе формы работают: --target имеет более высокий приоритет, чем DBT_TARGET.
Use case #4: tenant ID для multi-tenant
Если ваш dbt-проект обслуживает несколько клиентов с одной кодовой базой:
-- В модели:
select *
from {{ source('jaffle', 'orders') }}
where tenant_id = '{{ env_var('TENANT_ID') }}'
Запуск под каждого:
$ TENANT_ID=acme dbt run --target prod
$ TENANT_ID=corp dbt run --target prod
$ TENANT_ID=startup dbt run --target prod
Это даёт разделение данных через одну модель. Бывает практичнее, чем плодить отдельные модели или базы.
var vs env_var: когда что
Главное правило: secret? -> env_var. Конфиг проекта? -> var.
var не работает в profiles.yml — там доступен только env_var. Это сознательное ограничение dbt: профиль не часть проекта, var тут не имеет смысла.
env_var возвращает строку всегда
{% set port = env_var('DB_PORT', '5432') %}
{# port — это string '5432', не int 5432 #}
{# Если нужно сравнить как число #}
{% if port | int > 5000 %}
-- prod port
{% endif %}
{# Если нужно подставить в SQL — обычно неважно, но в boolean — важно #}
{% set is_prod = env_var('IS_PROD', 'false') %}
{# is_prod = 'false' — это STRING, не false! #}
{# Поэтому: #}
{% if is_prod == 'true' %}
...
{% endif %}
Часто junior пишет:
{% if env_var('IS_PROD', 'false') %}
-- prod branch
{% endif %}
И удивляется, что 'false' (строка) truthy в Python — поэтому ветка всегда выполняется. Правильно:
{% if env_var('IS_PROD', 'false') == 'true' %}
Или явная convert:
{% set is_prod = (env_var('IS_PROD', 'false') | lower == 'true') %}
env_var с типизацией: распространённые конверсии
{# String -> Int #}
{% set port = env_var('PORT', '5432') | int %}
{# String -> Bool #}
{% set debug = (env_var('DEBUG', 'false') | lower == 'true') %}
{# String -> List (через split) #}
{% set tags = env_var('TAGS', 'tag1,tag2').split(',') %}
{# String -> Date (через Python datetime) #}
{% set start = modules.datetime.datetime.strptime(env_var('START_DATE', '2026-01-01'), '%Y-%m-%d') %}
.env файл для local dev
Если у вас много env vars для разработки, держите их в .env файле:
# .env (НЕ коммитить!)
DBT_DUCKDB_PATH=/Users/me/projects/jaffle_shop.duckdb
DBT_DUCKDB_THREADS=8
DBT_TARGET=dev
TENANT_ID=test_tenant
И в .gitignore:
.env
.env.local
*.duckdb
Загрузка перед dbt:
$ export $(grep -v '^#' .env | xargs)
$ dbt run
Или используйте direnv / dotenv-cli для автоматической загрузки в shell session.
Также можно держать .env.example в git — без секретов, как шаблон для новых разработчиков:
# .env.example
DBT_POSTGRES_USER=
DBT_POSTGRES_PASS=
DBT_POSTGRES_HOST=
Безопасность: что НЕ делать
1. НИКОГДА не коммить env vars значения.
# Bad: значение в git
password: "{{ env_var('DBT_PASS', 'real_password_here') }}" # default — это secret!
# Good: default только для non-secret
password: "{{ env_var('DBT_PASS') }}" # упадёт, если не задано — это OK для секрета
Default подходит для конфигурации (port, threads), не для секретов.
2. НИКОГДА не логировать env vars.
{# Bad — может попасть в логи CI #}
{% do log("Password: " ~ env_var('DBT_PASS'), info=True) %}
3. Защита через secrets manager в CI.
В GitHub Actions:
- name: Run dbt
env:
DBT_POSTGRES_USER: ${{ secrets.DBT_POSTGRES_USER }}
DBT_POSTGRES_PASS: ${{ secrets.DBT_POSTGRES_PASS }}
run: dbt run
В GitLab CI:
job_dbt:
variables:
DBT_POSTGRES_PASS: $DBT_POSTGRES_PASS # из CI Settings
script:
- dbt run
Никогда не хардкодьте секрет ни в yml-файле, ни в env.
Тонкость: env_var на parse phase
env_var работает на parse phase (env vars доступны без warehouse). Так что в отличие от run_query, можно использовать без {% if execute %}:
{# OK даже на parse #}
{% set tenant = env_var('TENANT_ID') %}
select * from {{ ref('orders') }} where tenant_id = '{{ tenant }}'
Распространённые ошибки
1. Забыли default для конфиг-параметра.
{# При отсутствии env — упадёт #}
threads: "{{ env_var('DBT_THREADS') | int }}"
{# Лучше: разумный default #}
threads: "{{ env_var('DBT_THREADS', '4') | int }}"
2. Сравнение env_var(...) == True с типом string.
{# Bad: 'true' это string, никогда не равен boolean True #}
{% if env_var('IS_PROD', 'false') == True %}
{# Good #}
{% if env_var('IS_PROD', 'false') == 'true' %}
{# Or #}
{% if env_var('IS_PROD', 'false') | lower == 'true' %}
3. env_var без | int для числа.
{# Может вызвать ошибку при passing в driver, который ждёт int #}
port: "{{ env_var('PORT', '5432') }}"
{# Правильно #}
port: "{{ env_var('PORT', '5432') | int }}"
4. var в profiles.yml вместо env_var.
{# profiles.yml — var НЕ работает #}
password: "{{ var('db_pass') }}" # упадёт
{# Правильно #}
password: "{{ env_var('DBT_DB_PASS') }}"
Попробуй сам
Настройте свой profiles.yml, чтобы все настройки DuckDB шли из env:
# ~/.dbt/profiles.yml
jaffle_shop:
target: "{{ env_var('DBT_TARGET', 'dev') }}"
outputs:
dev:
type: duckdb
path: "{{ env_var('DBT_DUCKDB_PATH', 'dev.duckdb') }}"
threads: "{{ env_var('DBT_DUCKDB_THREADS', '4') | int }}"
schema: "{{ env_var('DBT_DUCKDB_SCHEMA', 'main') }}"
Создайте .env.example:
DBT_TARGET=dev
DBT_DUCKDB_PATH=/path/to/dev.duckdb
DBT_DUCKDB_THREADS=4
DBT_DUCKDB_SCHEMA=main
Запустите без env vars: dbt debug — должно работать на defaults.
Затем экспортируйте свои значения:
$ export DBT_DUCKDB_PATH=/tmp/my_jaffle.duckdb
$ dbt debug
$ dbt run
Проверьте, что dev.duckdb не создаётся (вместо него — /tmp/my_jaffle.duckdb).
Итоги
env_var('NAME')— читает переменную окружения. Без default — обязательна (упадёт при отсутствии).env_var('NAME', 'default')— fallback. Использовать для конфига, НЕ для секретов.- env_var ВСЕГДА возвращает string. Для числа —
| int, для bool — сравнение со'true'. - Главное применение: профили
profiles.yml(где var не доступен), tenant ID, runtime-окружение. var(для конфига проекта) vsenv_var(для секретов и окружения).- Безопасность: НИКОГДА секрет в default. НИКОГДА не логировать значение env_var-секретов. В CI/CD — через secrets manager.
В следующем (последнем) уроке модуля и курса junior — практический паттерн dev vs prod через target + var, который ставит всё изученное в одно место.