Learning Platform
Глоссарий Troubleshooting
Урок 19.04 · 30 мин
Начальный
env-filesgitignoredirenvsecrets-managervaultairflowdbtdoppler

.env, secrets backends, direnv — где хранить секреты правильно

В предыдущих уроках мы выяснили: секрет в Git — катастрофа, prevention дешевле cleanup. Логичный вопрос: а где хранить ключи и пароли, если не в репо?

Ответ зависит от среды и зрелости команды. Самый простой подход — .env файл локально, и .gitignore. Следующий уровень — direnv для auto-load. Production-уровень — централизованный secrets backend (AWS Secrets Manager, HashiCorp Vault, doppler, 1Password CLI). Каждый — со своими плюсами и DE-специфичными нюансами.

В этом уроке разберём по уровням, от минимума до production-grade, с конкретными примерами для Airflow и dbt.


Уровень 0: hardcode в коде (плохо)

# dags/etl.py
postgres_password = "SuperSecret123"  # ← НИКОГДА
aws_key = "AKIAIOSFODNN7..."           # ← НИКОГДА

Понятно почему плохо: секрет в commit, gitleaks ловит, история навсегда. Это не вариант.


Уровень 1: .env файл + .gitignore

Классика для локальной разработки. .env — файл в корне проекта с переменными:

# .env (НЕ КОММИТИТЬ!)
AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
POSTGRES_PASSWORD=local_dev_password
SNOWFLAKE_USER=dbt_local
SNOWFLAKE_PASSWORD=local_password_123
SLACK_WEBHOOK=https://hooks.slack.com/services/T0/B0/xyz

В коде загружаем:

import os
from dotenv import load_dotenv

load_dotenv()  # читает .env из текущей директории

aws_key = os.environ["AWS_ACCESS_KEY_ID"]
postgres_pwd = os.environ["POSTGRES_PASSWORD"]

.gitignore:

# Secrets — никогда не коммитим
.env
.env.local
.env.*.local
*.pem
*.p8
*.key

# IDE-shadow конфиги, которые могут содержать секреты
.vscode/launch.json
.idea/workspace.xml

# Direnv state
.direnv/
.envrc.cache

.env.example — что коммитим

Чтобы новый член команды знал, какие переменные нужны, не зная значений — .env.example (или .env.template):

# .env.example (КОММИТИМ)
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
POSTGRES_PASSWORD=
SNOWFLAKE_USER=
SNOWFLAKE_PASSWORD=
SLACK_WEBHOOK=

Junior клонирует репо:

$ git clone repo && cd repo
$ cp .env.example .env
$ # редактирует .env, вставляет свои dev-credentials
TIP

В .env.example иногда полезно положить тестовые / dummy значения, которые работают для unit-тестов: POSTGRES_PASSWORD=test. Это даёт «нулевой setup» — клонировал, сразу пускаются tests против локального docker-postgres с тем же паролем.

Проверка: .env не в commit

После clone и редактирования .env:

$ git status
On branch main
nothing to commit, working tree clean

Если .env в .gitignore, Git его не видит. Если видишь его в git status — значит, gitignore сломан, проверь правила.

WARNING

.gitignore работает только для untracked файлов. Если ты однажды закоммитил .env (даже случайно), добавление его в .gitignore не уберёт его из истории. Сначала git rm --cached .env, потом изменение нужно закоммитить, и только потом gitignore начнёт работать. И в истории старая версия останется — см. урок 03 (cleanup).


Уровень 2: direnv — auto-load переменных

direnv — shell-расширение, которое автоматически загружает .envrc файл, когда ты cd-аешь в директорию, и выгружает когда уходишь. Удобство — не нужно вручную делать source .env или думать о виртуальном окружении.

Установка:

# macOS
$ brew install direnv

# Linux
$ sudo apt install direnv  # или curl-script

# Подключаем к shell (для zsh)
$ echo 'eval "$(direnv hook zsh)"' >> ~/.zshrc
$ source ~/.zshrc

Использование:

$ cd ~/projects/my-de-project

# Создаём .envrc
$ cat > .envrc << 'EOF'
# Загрузить .env как переменные среды
dotenv

# Активировать venv
source .venv/bin/activate

# Установить AWS_PROFILE для этого проекта
export AWS_PROFILE=de-team-prod

# Set PYTHONPATH для проекта
export PYTHONPATH="$PWD/src:$PYTHONPATH"
EOF

# direnv требует явного разрешения для каждого .envrc — защита от malicious
$ direnv allow
direnv: loading ~/projects/my-de-project/.envrc
direnv: export +AWS_PROFILE +PYTHONPATH ...

# Теперь автоматически:
$ echo $AWS_PROFILE
de-team-prod

$ cd ~  # ушли из директории
direnv: unloading
$ echo $AWS_PROFILE
# (пусто)

Что коммитим, что нет

# .envrc структура — коммитим
# .envrc — содержит логику (dotenv, source, etc), не сами секреты
# .env — содержит секреты, не коммитим
.env
.direnv/

.envrc коммитим — он не содержит секретов, только инструкции откуда их загрузить. .env всё ещё в gitignore.

TIP

direnv хорошо комбинируется с команд pyenv и nodenv. В .envrc можно use python 3.13, и при cd автоматически активируется правильная версия Python. Для DE — особенно полезно, когда работаешь с разными версиями PySpark / Airflow.

Почему direnv allow — критично

.envrc — это shell-script, который выполняется. Если кто-то закоммитил malicious .envrc (rm -rf $HOME), он бы выполнился при cd. Поэтому direnv требует явное разрешение для каждого .envrc файла, и переразрешение, если файл изменился. Это защита от supply-chain атаки.


Уровень 3: централизованный secrets backend

.env хорош для локальной разработки. Для production — нет:

  • Если у команды 10 инженеров — 10 копий .env с разными паролями. Кто-то отстаёт по обновлениям.
  • Если меняется production пароль — нужно обновить везде вручную.
  • .env файл на CI runner — кто его положил туда? Кто видит его в logs?
  • Compliance: PCI-DSS требует логировать кто и когда доставал secret.

Решение — централизованный secrets backend.

AWS Secrets Manager

Стандарт для AWS-проектов. Секрет хранится в AWS, доступ — через IAM (роль, не ключ).

import boto3
import json

def get_secret(secret_name: str) -> dict:
    client = boto3.client("secretsmanager", region_name="us-east-1")
    response = client.get_secret_value(SecretId=secret_name)
    return json.loads(response["SecretString"])

# В DAG / скрипте
db = get_secret("prod/postgres/dbt_runner")
postgres_pwd = db["password"]

Плюсы:

  • Никаких .env на production.
  • Rotation: AWS может автоматически менять пароль раз в N дней.
  • Audit: CloudTrail логирует каждый GetSecretValue — кто, когда, откуда IP.
  • IAM: разные роли видят разные секреты (prod/* — только prod, dev/* — dev).

Минусы:

  • Стоит денег (0.40/секрет/месяц+0.40 / секрет / месяц + 0.05 / 10k requests).
  • Vendor lock-in.

HashiCorp Vault

Open-source, multi-cloud. Запускается локально / в Kubernetes.

$ vault kv get -mount=secret prod/postgres
====== Data ======
Key         Value
---         -----
host        prod-db.internal
user        dbt_runner
password    ***

Через API:

import hvac

client = hvac.Client(url="https://vault.company.local:8200", token=os.environ["VAULT_TOKEN"])
secret = client.secrets.kv.read_secret_version(path="prod/postgres", mount_point="secret")
postgres_pwd = secret["data"]["data"]["password"]

Плюсы:

  • Бесплатный (open source).
  • Поддерживает dynamic secrets (auto-generate DB credentials per session — выдан, использован, отозван).
  • Multi-cloud, on-prem.

Минусы:

  • Сложнее в setup и operations (kuber-стек).

doppler

SaaS, простая UI. Хорошо для маленьких команд.

$ doppler setup
$ doppler run -- python dag.py
# Все секреты из Doppler автоматически в env-vars

Плюсы:

  • Простой setup (5 минут).
  • Хорошая UI, audit log.
  • Бесплатный тariff для команд до 5 человек.

Минусы:

  • Vendor lock-in.
  • Платно для production team scale.

1Password CLI (op)

Для маленьких команд / соло-developer. Использует общий 1Password vault.

$ eval $(op signin)
$ export AWS_KEY=$(op read "op://Engineering/aws-prod/access_key")

Хорошо, когда команда уже использует 1Password для паролей — не нужен отдельный tool.


DE-специфика: Airflow Connections и Variables

Airflow имеет два механизма хранения secret-like values:

  1. Connections — для подключения к базам/API (host + login + password + extras JSON).
  2. Variables — для конфигов (например, S3 bucket name).

И три способа хранить их:

(a) В Airflow UI / metadata DB (плохо)

Junior часто видит этот туториал: «зайди в Admin -> Connections, добавь postgres_default с паролем». Это работает, но:

  • Пароль лежит в Airflow metadata DB (Postgres).
  • Кто имеет доступ к Airflow UI — видит пароль (через Edit, в зависимости от Airflow версии).
  • Backup metadata DB — содержит пароли.
  • Невозможно rotate централизованно.

OK для очень маленьких setup-ов, не для production.

(b) Environment variables (лучше)

Airflow умеет читать Connection из env-var с префиксом AIRFLOW_CONN_:

$ export AIRFLOW_CONN_POSTGRES_DEFAULT="postgres://dbt_runner:[email protected]/main"

В DAG:

from airflow.providers.postgres.hooks.postgres import PostgresHook

hook = PostgresHook(postgres_conn_id="postgres_default")
# Airflow прочитает из AIRFLOW_CONN_POSTGRES_DEFAULT

Плюсы: пароль не в metadata DB. Env-var задаётся при старте контейнера из secret-источника (Kubernetes Secret, ECS Task Definition).

Минусы: пароль всё ещё «на диске» в виде env, видим ps -e environ. Лучше — secrets backend.

(c) Secrets backend (production)

Airflow поддерживает Secrets Backend для Connections и Variables:

# airflow.cfg
[secrets]
backend = airflow.providers.amazon.aws.secrets.secrets_manager.SecretsManagerBackend
backend_kwargs = {"connections_prefix": "airflow/connections", "variables_prefix": "airflow/variables", "profile_name": null}

В AWS Secrets Manager создаём:

airflow/connections/postgres_default = "postgres://dbt_runner:***@prod-db.internal/main"
airflow/variables/s3_bucket = "company-de-prod"

В DAG ничего не меняем:

hook = PostgresHook(postgres_conn_id="postgres_default")
bucket = Variable.get("s3_bucket")

Airflow сам пойдёт в Secrets Manager -> достанет -> закеширует. Junior пишет тот же код, что и в (a), но безопасность production-grade. Это правильный путь.

Airflow Connection resolution с Secrets Backend
PostgresHook(postgres_conn_id="postgres_default")DAG задаёт connection_id
Airflow resolver chainAirflow ищет в порядке: env -> secrets backend -> metadata DB
env vars1. Env var AIRFLOW_CONN_POSTGRES_DEFAULT
secrets backend2. Configured backend (Secrets Manager / Vault)
metadata DB3. Fallback: airflow.connections таблица
Resolved connectionConnection возвращается из первого источника, где найдено

DE-специфика: dbt profiles.yml

profiles.yml содержит credentials для подключения к warehouse. Никогда не коммитим. Но как тогда CI/CD?

env_var() — основной механизм

# profiles.yml
my_project:
  outputs:
    prod:
      type: snowflake
      account: "{{ env_var('SNOWFLAKE_ACCOUNT') }}"
      user: "{{ env_var('SNOWFLAKE_USER') }}"
      password: "{{ env_var('SNOWFLAKE_PASSWORD') }}"
      database: analytics
      warehouse: transforming
      schema: prod
  target: prod

env_var() — Jinja-функция dbt, читает из переменных среды. Подставляется в runtime, не сохраняется на диск.

CI/CD передаёт env-vars из secrets backend:

# GitHub Actions
- name: Run dbt
  env:
    SNOWFLAKE_PASSWORD: ${{ secrets.SNOWFLAKE_PASSWORD }}
  run: dbt build

Локально — из .env через direnv или dotenv:

$ source .env
$ dbt build

env_var с дефолтом

password: "{{ env_var('SNOWFLAKE_PASSWORD', 'default_for_local_dev') }}"

Если env-var не задана — берётся default. Удобно для dev: новый член команды клонировал, не настроил env — сразу видит fallback.

Key-pair authentication

В 2026 многие DE-команды переходят с password auth на key-pair (Snowflake / Snowpipe):

my_project:
  outputs:
    prod:
      type: snowflake
      account: "{{ env_var('SNOWFLAKE_ACCOUNT') }}"
      user: "{{ env_var('SNOWFLAKE_USER') }}"
      private_key_path: "{{ env_var('SNOWFLAKE_KEY_PATH') }}"
      private_key_passphrase: "{{ env_var('SNOWFLAKE_KEY_PASSPHRASE') }}"

.p8 файл — секрет, никогда не коммитим. В CI — генерируется на лету из secret-store:

- name: Setup Snowflake key
  run: |
    echo "${{ secrets.SNOWFLAKE_PRIVATE_KEY }}" > /tmp/key.p8
    chmod 600 /tmp/key.p8
  env:
    SNOWFLAKE_KEY_PATH: /tmp/key.p8

Универсальный setup для нового DE-проекта

Recipe, который подходит на 80% случаев:

1. .gitignore (стандартный для DE-проекта):

# Secrets
.env
.env.local
*.pem
*.p8
*.key
profiles.yml
dbt_project_secrets.yml

# Direnv
.direnv/

# IDE
.vscode/launch.json
.idea/

# Python
__pycache__/
.venv/
*.pyc
*.egg-info/

# Data files
*.parquet
*.duckdb
*.sqlite

# OS
.DS_Store

2. .env.example:

# Snowflake credentials (получи у тимлида)
SNOWFLAKE_ACCOUNT=
SNOWFLAKE_USER=
SNOWFLAKE_PASSWORD=

# AWS (для S3 staging)
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_REGION=us-east-1

# Slack для алертов (получи webhook у devops)
SLACK_WEBHOOK_URL=

3. .envrc (direnv):

dotenv
source .venv/bin/activate
export PYTHONPATH="$PWD/src:$PYTHONPATH"

# Защита: предупреждаем если .env пустой
if [ ! -f .env ] || ! grep -q "SNOWFLAKE_USER=." .env; then
    echo "WARNING: .env is missing or incomplete. Copy from .env.example."
fi

4. .pre-commit-config.yaml (из урока 02):

repos:
  - repo: https://github.com/gitleaks/gitleaks
    rev: v8.21.2
    hooks:
      - id: gitleaks

5. README onboarding section:

## Setup

1. Clone the repo
2. `cp .env.example .env` and fill in values (ask tech lead for production credentials)
3. `direnv allow` (or `source .env` if no direnv)
4. `uv sync` to install dependencies
5. `pre-commit install` to enable secret scanning hook

NEVER commit `.env`, `*.p8`, or `profiles.yml`. The pre-commit hook will block it.

Это — production-ready минимум для junior DE на день 1.


Попробуй сам

Создадим mini-DE-проект с правильным setup:

$ mkdir my-de-project && cd my-de-project
$ git init

# .gitignore
$ cat > .gitignore << 'EOF'
.env
.env.local
*.p8
.direnv/
__pycache__/
EOF

# .env.example (коммитим)
$ cat > .env.example << 'EOF'
POSTGRES_HOST=
POSTGRES_USER=
POSTGRES_PASSWORD=
EOF

# Свой .env (НЕ коммитим — gitignore)
$ cp .env.example .env
$ echo "POSTGRES_HOST=localhost" >> .env
$ echo "POSTGRES_USER=dev" >> .env
$ echo "POSTGRES_PASSWORD=dev-password" >> .env

$ git add .
$ git status
# .env НЕ должен быть в списке

$ git commit -m "Initial DE project structure"

# Создаём DAG, использующий env-vars
$ mkdir dags
$ cat > dags/etl.py << 'EOF'
import os
from dotenv import load_dotenv

load_dotenv()

POSTGRES_HOST = os.environ["POSTGRES_HOST"]
POSTGRES_USER = os.environ["POSTGRES_USER"]
POSTGRES_PASSWORD = os.environ["POSTGRES_PASSWORD"]

# Используем переменные, не hardcode
print(f"Connecting to {POSTGRES_USER}@{POSTGRES_HOST}")
EOF

$ python dags/etl.py
# Должен напечатать "Connecting to dev@localhost"

# Проверяем gitleaks
$ gitleaks detect --source . --verbose
# Должен сказать "no leaks found"

Killer takeaway

Hierarchy безопасности secrets для DE: (1) .env в .gitignore + .env.example коммитим — минимум для всех; (2) direnv — auto-load и удобство; (3) централизованный secrets backend — production. В Airflow используй secrets backend (AWS Secrets Manager / Vault), не Connections в UI с hardcoded паролями. В dbt — env_var() в profiles.yml, никогда не commit реальный profiles.yml. В CI — secrets через GitHub Secrets -> env-vars -> процесс. Главный принцип: секреты не на диске Git. Каждый шаг вверх по уровню увеличивает safety и удобство rotate-операций.

dbt: profiles.yml, секреты и правильный .gitignore
Проверка знанийKnowledge check
ОтветAnswer

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

Результат: 0 из 0
Прикладной
Вопрос 1 из 5. В DE-проекте Airflow какой подход для хранения secrets ЛУЧШЕ для production?

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

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

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

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