Learning Platform
Глоссарий Troubleshooting
Урок 13.02 · 18 мин
Начальный
dbtJinjaenv_varsecretsconfiguration

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).

WARNING

Заметьте | 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.

NOTE

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).

Проверка знанийKnowledge check
В чём принципиальная разница между var() и env_var()? Приведи пример, для которого нужен var(), и пример, где нужен env_var().
ОтветAnswer
var() — параметры ПРОЕКТА. Хранятся в git (через dbt_project.yml), могут быть переопределены на CLI через --vars. Подходят для бизнес-конфигурации, версионированной с кодом. env_var() — переменные ОКРУЖЕНИЯ. Берутся из ОС, не из проекта. Не хранятся в git. Подходят для секретов и runtime-конфигурации, которая отличается между окружениями (dev/prod). Пример для var(): - start_date: '2020-01-01' — дата отсечения staging-моделей. Должна быть видна команде, версионироваться, может переопределяться на CLI для backfill (--vars '{start_date: 2019-01-01}'). - excluded_statuses: ['cancelled', 'fraud'] — бизнес-правило. Часть кодовой базы. - lookback_days: 365 — конфигурация incremental-моделей. Открытая часть проекта. Пример для env_var(): - DBT_POSTGRES_PASS — пароль к warehouse. КАТЕГОРИЧЕСКИ нельзя в git. - DBT_TARGET — какое окружение используем (dev/prod). Зависит от того, КТО запускает, не часть проекта. - TENANT_ID — для multi-tenant пайплайнов. Меняется между запусками для разных клиентов. Главное правило: secret или runtime-окружение — env_var. Бизнес-параметр, который должен версионироваться — var. В profiles.yml работает только env_var (var там не доступен сознательно).
Проверка знанийKnowledge check
В коде модели: `{% if env_var('ENABLE_DEBUG', 'false') %}``{% do log('debug', info=true) %}``{% endif %}`. Будет ли лог напечатан, если переменная не установлена?
ОтветAnswer
Да, лог будет напечатан — и это БАГ. Объяснение: env_var() ВСЕГДА возвращает строку. Если ENABLE_DEBUG не установлена, возвращается default 'false' — но это СТРОКА 'false', не boolean false. В Jinja (как и Python) непустая строка — truthy. 'false', 'no', '0', '' — это string, и все они проходят как truthy в ''{% if %}'' (кроме пустой строки '', которая falsy). Тест проверки: ''{% if 'false' %}''YES''{% endif %}'' -> выведет YES. Потому что 'false' — непустая строка. Чтобы исправить, нужно явное сравнение: ''{% if env_var('ENABLE_DEBUG', 'false') == 'true' %}'' ''{% do log('debug', info=true) %}'' ''{% endif %}'' Или case-insensitive: ''{% if env_var('ENABLE_DEBUG', 'false') | lower in ['true', '1', 'yes'] %}'' ... ''{% endif %}'' Best practice: для boolean env_var всегда сравнивать с конкретной строкой. Никогда не полагаться на truthy/falsy от env_var. Документировать в README: 'установите ENABLE_DEBUG=true для включения debug-логирования'.

Итоги

  • env_var('NAME') — читает переменную окружения. Без default — обязательна (упадёт при отсутствии).
  • env_var('NAME', 'default') — fallback. Использовать для конфига, НЕ для секретов.
  • env_var ВСЕГДА возвращает string. Для числа — | int, для bool — сравнение со 'true'.
  • Главное применение: профили profiles.yml (где var не доступен), tenant ID, runtime-окружение.
  • var (для конфига проекта) vs env_var (для секретов и окружения).
  • Безопасность: НИКОГДА секрет в default. НИКОГДА не логировать значение env_var-секретов. В CI/CD — через secrets manager.
env_var для секретов в multi-environment

В следующем (последнем) уроке модуля и курса junior — практический паттерн dev vs prod через target + var, который ставит всё изученное в одно место.

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 7. В чём принципиальное различие между var() и env_var()?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 3