Skip to content

Commit 15d58e1

Browse files
committed
Add support for Snowflake key pair and OAuth auth
This update introduces support for Snowflake key pair (JWT) and OAuth authentication in Airflow DAGs, dbt profiles, and environment setup scripts. The .env.example and profiles.yml files now document and support multiple authentication modes. Airflow DAGs and credential checks are updated to accept password, key pair, or OAuth token. Docker and local environment scripts are improved to handle secrets securely and remap key paths for local development. Minor fixes and clarifications are also included in Makefile, SQL, and test configs.
1 parent 5f3090f commit 15d58e1

16 files changed

Lines changed: 136 additions & 52 deletions

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,7 @@ dbt_packages/
4141
# Great Expectations heavy artifacts
4242
great_expectations/uncommitted/validations/**
4343
great_expectations/uncommitted/data_docs/**
44+
45+
# Local secrets (e.g. Snowflake key pair)
46+
secrets/*
47+
!secrets/README.md

Makefile

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -25,43 +25,43 @@ help: ## 显示可用命令
2525
# 环境初始化 / 本地 dbt
2626
# ---------------------------
2727
.PHONY: env
28-
env: ## 一键初始化本地环境(source init_env.sh
28+
env: ## 初始化本地 dbt 环境(注意:不会持久化到你的当前终端
2929
@test -x "$(PROJECT_ROOT)/init_env.sh" || { echo "❌ 找不到 init_env.sh 或无执行权限"; exit 1; }
30-
@source "$(PROJECT_ROOT)/init_env.sh"
30+
@bash -lc 'source "$(PROJECT_ROOT)/init_env.sh"; echo ""; echo "提示:如需让 venv/环境变量留在当前终端,请手动运行:source ./init_env.sh"'
3131

3232
.PHONY: dbt-debug
3333
dbt-debug: env ## dbt debug 自检
34-
@cd "$(PROJECT_ROOT)/data_pipeline" && dbt debug
34+
@bash -lc 'source "$(PROJECT_ROOT)/init_env.sh" >/dev/null 2>&1 || true; cd "$(PROJECT_ROOT)/data_pipeline" && dbt debug'
3535

3636
.PHONY: dbt-parse
3737
dbt-parse: env ## dbt parse(解析项目)
38-
@cd "$(PROJECT_ROOT)/data_pipeline" && dbt parse
38+
@bash -lc 'source "$(PROJECT_ROOT)/init_env.sh" >/dev/null 2>&1 || true; cd "$(PROJECT_ROOT)/data_pipeline" && dbt parse'
3939

4040
.PHONY: dbt-ls
4141
dbt-ls: env ## 列出所有模型
42-
@cd "$(PROJECT_ROOT)/data_pipeline" && dbt ls
42+
@bash -lc 'source "$(PROJECT_ROOT)/init_env.sh" >/dev/null 2>&1 || true; cd "$(PROJECT_ROOT)/data_pipeline" && dbt ls'
4343

4444
.PHONY: dbt-ls-bronze dbt-ls-silver dbt-ls-gold
4545
dbt-ls-bronze: env ## 列出 bronze 模型
46-
@cd "$(PROJECT_ROOT)/data_pipeline" && dbt ls --select 'path:models/bronze'
46+
@bash -lc 'source "$(PROJECT_ROOT)/init_env.sh" >/dev/null 2>&1 || true; cd "$(PROJECT_ROOT)/data_pipeline" && dbt ls --select "path:models/bronze"'
4747
dbt-ls-silver: env ## 列出 silver 模型
48-
@cd "$(PROJECT_ROOT)/data_pipeline" && dbt ls --select 'path:models/silver'
48+
@bash -lc 'source "$(PROJECT_ROOT)/init_env.sh" >/dev/null 2>&1 || true; cd "$(PROJECT_ROOT)/data_pipeline" && dbt ls --select "path:models/silver"'
4949
dbt-ls-gold: env ## 列出 gold 模型
50-
@cd "$(PROJECT_ROOT)/data_pipeline" && dbt ls --select 'path:models/gold'
50+
@bash -lc 'source "$(PROJECT_ROOT)/init_env.sh" >/dev/null 2>&1 || true; cd "$(PROJECT_ROOT)/data_pipeline" && dbt ls --select "path:models/gold"'
5151

5252
.PHONY: dbt-run-bronze dbt-run-silver dbt-run-gold dbt-build
5353
dbt-run-bronze: env ## 仅跑 bronze
54-
@cd "$(PROJECT_ROOT)/data_pipeline" && dbt run --select 'path:models/bronze'
54+
@bash -lc 'source "$(PROJECT_ROOT)/init_env.sh" >/dev/null 2>&1 || true; cd "$(PROJECT_ROOT)/data_pipeline" && dbt run --select "path:models/bronze"'
5555
dbt-run-silver: env ## 仅跑 silver
56-
@cd "$(PROJECT_ROOT)/data_pipeline" && dbt run --select 'path:models/silver'
56+
@bash -lc 'source "$(PROJECT_ROOT)/init_env.sh" >/dev/null 2>&1 || true; cd "$(PROJECT_ROOT)/data_pipeline" && dbt run --select "path:models/silver"'
5757
dbt-run-gold: env ## 仅跑 gold
58-
@cd "$(PROJECT_ROOT)/data_pipeline" && dbt run --select 'path:models/gold'
58+
@bash -lc 'source "$(PROJECT_ROOT)/init_env.sh" >/dev/null 2>&1 || true; cd "$(PROJECT_ROOT)/data_pipeline" && dbt run --select "path:models/gold"'
5959
dbt-build: env ## 全量构建(含测试)
60-
@cd "$(PROJECT_ROOT)/data_pipeline" && dbt build
60+
@bash -lc 'source "$(PROJECT_ROOT)/init_env.sh" >/dev/null 2>&1 || true; cd "$(PROJECT_ROOT)/data_pipeline" && dbt build'
6161

6262
.PHONY: dbt-docs
6363
dbt-docs: env ## 生成 + 本地预览文档(前台)
64-
@cd "$(PROJECT_ROOT)/data_pipeline" && dbt docs generate && dbt docs serve
64+
@bash -lc 'source "$(PROJECT_ROOT)/init_env.sh" >/dev/null 2>&1 || true; cd "$(PROJECT_ROOT)/data_pipeline" && dbt docs generate && dbt docs serve'
6565

6666
# ---------------------------
6767
# Docker / Airflow 管理

airflow/.env.example

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,25 @@
77
# @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
88
###
99
# Snowflake for dbt CLI (example values)
10-
SNOWFLAKE_ACCOUNT=ywatlqy-hx58385
11-
SNOWFLAKE_USER=whyang
12-
SNOWFLAKE_PASSWORD=Yang890522890522
10+
# NOTE: Do NOT put real credentials in this file; copy to airflow/.env locally.
11+
SNOWFLAKE_ACCOUNT=your_account
12+
SNOWFLAKE_USER=your_user
13+
SNOWFLAKE_PASSWORD=your_password
1314
SNOWFLAKE_ROLE=dbt_role
14-
SNOWFLAKE_WAREHOUSE=dbt_wh
15-
SNOWFLAKE_DATABASE=dbt_db
16-
SNOWFLAKE_SCHEMA=dbt_schema
15+
SNOWFLAKE_WAREHOUSE=DBT_WH
16+
SNOWFLAKE_DATABASE=DBT_DB
17+
SNOWFLAKE_SCHEMA=DBT_SCHEMA
1718
DBT_TARGET=dev
1819

20+
# If your Snowflake account enforces MFA, username/password auth may fail for
21+
# programmatic tools (dbt / Airflow). In that case, prefer key pair auth:
22+
# DBT_TARGET=dev_keypair
23+
# SNOWFLAKE_PRIVATE_KEY_PATH=/opt/airflow/secrets/snowflake_rsa_key.p8
24+
# SNOWFLAKE_PRIVATE_KEY_PASSPHRASE=your_passphrase # optional
25+
1926
# Optional: Airflow alerts (email on task failure)
2027
# Configure SMTP in Airflow if you want emails delivered.
21-
ALERT_EMAIL=i.whyang@gmail.com
28+
ALERT_EMAIL=you@example.com
2229

2330
# Optional: Airflow secrets (recommended for non-local use)
2431
# Generate with: python -c 'import secrets,base64; print(base64.b64encode(secrets.token_bytes(32)).decode())'
@@ -27,10 +34,10 @@ AIRFLOW__WEBSERVER__SECRET_KEY=
2734

2835
# Optional: Minimal SMTP example (Gmail via STARTTLS)
2936
# NOTE: For Gmail, use an app password, not your main password.
30-
AIRFLOW__SMTP__SMTP_HOST=smtp.gmail.com
31-
AIRFLOW__SMTP__SMTP_PORT=587
32-
AIRFLOW__SMTP__SMTP_STARTTLS=True
33-
AIRFLOW__SMTP__SMTP_SSL=False
34-
AIRFLOW__SMTP__SMTP_USER=whyang
35-
AIRFLOW__SMTP__SMTP_PASSWORD=fsnopcddwsuebfqn
36-
AIRFLOW__SMTP__SMTP_MAIL_FROM=s.wyang@live.com
37+
# AIRFLOW__SMTP__SMTP_HOST=smtp.gmail.com
38+
# AIRFLOW__SMTP__SMTP_PORT=587
39+
# AIRFLOW__SMTP__SMTP_STARTTLS=True
40+
# AIRFLOW__SMTP__SMTP_SSL=False
41+
# AIRFLOW__SMTP__SMTP_USER=you@gmail.com
42+
# AIRFLOW__SMTP__SMTP_PASSWORD=APP_PASSWORD_HERE
43+
# AIRFLOW__SMTP__SMTP_MAIL_FROM=you@gmail.com

airflow/dags/dbt_daily.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ def g(k):
2424
# 统一的 ENV(两条 DAG 完全一致)
2525
env_vars = {
2626
"DBT_PROFILES_DIR": "/opt/airflow/dbt",
27+
"DBT_TARGET": g("DBT_TARGET") or "dev",
2728
# Traceability: pass query tag to Snowflake via dbt profile
2829
# Format: dag:task:run_id:queue:ts:try:run_type
2930
"DBT_QUERY_TAG": (
@@ -39,6 +40,9 @@ def g(k):
3940
"SNOWFLAKE_ACCOUNT": g("SNOWFLAKE_ACCOUNT"),
4041
"SNOWFLAKE_USER": g("SNOWFLAKE_USER"),
4142
"SNOWFLAKE_PASSWORD": g("SNOWFLAKE_PASSWORD"),
43+
"SNOWFLAKE_PRIVATE_KEY_PATH": g("SNOWFLAKE_PRIVATE_KEY_PATH"),
44+
"SNOWFLAKE_PRIVATE_KEY_PASSPHRASE": g("SNOWFLAKE_PRIVATE_KEY_PASSPHRASE"),
45+
"SNOWFLAKE_OAUTH_TOKEN": g("SNOWFLAKE_OAUTH_TOKEN"),
4246
"SNOWFLAKE_ROLE": g("SNOWFLAKE_ROLE"),
4347
"SNOWFLAKE_DATABASE": g("SNOWFLAKE_DATABASE"),
4448
"SNOWFLAKE_WAREHOUSE": g("SNOWFLAKE_WAREHOUSE"),

airflow/dags/dbt_layered_pipeline.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
# 通常与项目根同路径
2626
DBT_PROFILES_DIR = "/opt/airflow/dbt"
2727
# 可用 .env 或容器 ENV 指定
28-
DBT_TARGET = os.environ.get("DBT_TARGET", "prod")
28+
DBT_TARGET = os.environ.get("DBT_TARGET", "dev")
2929
# 使用 run+test 拆分以体现“质量闸门”
3030
USE_DBT_BUILD = False
3131
# 每日 03:00 UTC 触发
@@ -70,6 +70,9 @@ def _g(k: str, default: str = "") -> str:
7070
"SNOWFLAKE_ACCOUNT": _g("SNOWFLAKE_ACCOUNT"),
7171
"SNOWFLAKE_USER": _g("SNOWFLAKE_USER"),
7272
"SNOWFLAKE_PASSWORD": _g("SNOWFLAKE_PASSWORD"),
73+
"SNOWFLAKE_PRIVATE_KEY_PATH": _g("SNOWFLAKE_PRIVATE_KEY_PATH"),
74+
"SNOWFLAKE_PRIVATE_KEY_PASSPHRASE": _g("SNOWFLAKE_PRIVATE_KEY_PASSPHRASE"),
75+
"SNOWFLAKE_OAUTH_TOKEN": _g("SNOWFLAKE_OAUTH_TOKEN"),
7376
"SNOWFLAKE_ROLE": _g("SNOWFLAKE_ROLE"),
7477
"SNOWFLAKE_DATABASE": _g("SNOWFLAKE_DATABASE"),
7578
"SNOWFLAKE_WAREHOUSE": _g("SNOWFLAKE_WAREHOUSE"),

airflow/dags/dbt_pipeline_dag.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ def g(k):
1616
# 与 dbt_daily 完全一致的 ENV
1717
env_vars = {
1818
"DBT_PROFILES_DIR": "/opt/airflow/dbt",
19+
"DBT_TARGET": g("DBT_TARGET") or "dev",
1920
# dag:task:run_id:queue:ts:try:run_type
2021
"DBT_QUERY_TAG": (
2122
"{{ dag.dag_id }}:"
@@ -30,6 +31,9 @@ def g(k):
3031
"SNOWFLAKE_ACCOUNT": g("SNOWFLAKE_ACCOUNT"),
3132
"SNOWFLAKE_USER": g("SNOWFLAKE_USER"),
3233
"SNOWFLAKE_PASSWORD": g("SNOWFLAKE_PASSWORD"),
34+
"SNOWFLAKE_PRIVATE_KEY_PATH": g("SNOWFLAKE_PRIVATE_KEY_PATH"),
35+
"SNOWFLAKE_PRIVATE_KEY_PASSPHRASE": g("SNOWFLAKE_PRIVATE_KEY_PASSPHRASE"),
36+
"SNOWFLAKE_OAUTH_TOKEN": g("SNOWFLAKE_OAUTH_TOKEN"),
3337
"SNOWFLAKE_ROLE": g("SNOWFLAKE_ROLE"),
3438
"SNOWFLAKE_DATABASE": g("SNOWFLAKE_DATABASE"),
3539
"SNOWFLAKE_WAREHOUSE": g("SNOWFLAKE_WAREHOUSE"),

airflow/dags/lib/creds.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,9 @@ def ok(k: str) -> bool:
1515
"sample",
1616
}
1717

18-
required = ("SNOWFLAKE_ACCOUNT", "SNOWFLAKE_USER", "SNOWFLAKE_PASSWORD")
19-
return all(ok(k) for k in required)
18+
# Accept password auth OR key pair auth OR oauth token auth.
19+
base = ("SNOWFLAKE_ACCOUNT", "SNOWFLAKE_USER")
20+
password_ok = ok("SNOWFLAKE_PASSWORD")
21+
keypair_ok = ok("SNOWFLAKE_PRIVATE_KEY_PATH") or ok("SNOWFLAKE_PRIVATE_KEY")
22+
oauth_ok = ok("SNOWFLAKE_OAUTH_TOKEN")
23+
return all(ok(k) for k in base) and (password_ok or keypair_ok or oauth_ok)

airflow/dags/serving/dbt_gold_consumer.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,18 @@ def _g(k: str, default: str = "") -> str:
2020

2121
ENV_VARS = {
2222
"DBT_PROFILES_DIR": "/opt/airflow/dbt",
23+
"DBT_TARGET": _g("DBT_TARGET", "dev"),
2324
"SNOWFLAKE_ACCOUNT": _g("SNOWFLAKE_ACCOUNT"),
2425
"SNOWFLAKE_USER": _g("SNOWFLAKE_USER"),
2526
"SNOWFLAKE_PASSWORD": _g("SNOWFLAKE_PASSWORD"),
27+
"SNOWFLAKE_PRIVATE_KEY_PATH": _g("SNOWFLAKE_PRIVATE_KEY_PATH"),
28+
"SNOWFLAKE_PRIVATE_KEY_PASSPHRASE": _g("SNOWFLAKE_PRIVATE_KEY_PASSPHRASE"),
29+
"SNOWFLAKE_OAUTH_TOKEN": _g("SNOWFLAKE_OAUTH_TOKEN"),
2630
"SNOWFLAKE_ROLE": _g("SNOWFLAKE_ROLE"),
2731
"SNOWFLAKE_DATABASE": _g("SNOWFLAKE_DATABASE"),
2832
"SNOWFLAKE_WAREHOUSE": _g("SNOWFLAKE_WAREHOUSE"),
2933
"SNOWFLAKE_SCHEMA": _g("SNOWFLAKE_SCHEMA"),
34+
"PATH": _g("PATH", "/home/airflow/.local/bin:/usr/local/bin:/usr/bin:/bin"),
3035
}
3136

3237

airflow/docker-compose.yml

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
version: "3.8"
22

3+
# NOTE:
4+
# - This file is a legacy single-folder compose (SQLite + SequentialExecutor).
5+
# - The main stack lives at repo root: `docker-compose.yml` (Postgres + LocalExecutor).
6+
# - Do not hardcode secrets here; use env vars or an env file.
7+
38
services:
49
airflow-init:
510
build: .
@@ -10,9 +15,8 @@ services:
1015
environment:
1116
AIRFLOW__CORE__EXECUTOR: SequentialExecutor
1217
AIRFLOW__DATABASE__SQL_ALCHEMY_CONN: sqlite:////opt/airflow/airflow.db
13-
AIRFLOW__WEBSERVER__SECRET_KEY: simple-secret
14-
AIRFLOW__CORE__FERNET_KEY: TN6KCTBau36eJ3SLKN08_K_u5_sNQs-kDKUEdSuSq4o=
15-
AIRFLOW__WEBSERVER__SECRET_KEY: 8b748b37f196ab9e7801c5f907ba129e32ff071b53064c960082346da52ebf1f6391b166d255d77ab54583ac15618e8a2f445b05637b3add2140242aca2a8559
18+
AIRFLOW__CORE__FERNET_KEY: ${AIRFLOW__CORE__FERNET_KEY:-}
19+
AIRFLOW__WEBSERVER__SECRET_KEY: ${AIRFLOW__WEBSERVER__SECRET_KEY:-dev-change-me}
1620

1721
DBT_PROFILES_DIR: /opt/airflow/dbt
1822
volumes:
@@ -32,7 +36,7 @@ services:
3236
environment:
3337
AIRFLOW__CORE__EXECUTOR: SequentialExecutor
3438
AIRFLOW__DATABASE__SQL_ALCHEMY_CONN: sqlite:////opt/airflow/airflow.db
35-
AIRFLOW__WEBSERVER__SECRET_KEY: simple-secret
39+
AIRFLOW__WEBSERVER__SECRET_KEY: ${AIRFLOW__WEBSERVER__SECRET_KEY:-dev-change-me}
3640
DBT_PROFILES_DIR: /opt/airflow/dbt # <<<
3741
volumes:
3842
- airflow_home:/opt/airflow
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{% macro discounted_amount(extended_price, discount) -%}
22
({{ extended_price }} * (1 - {{ discount }}))
3-
{%- endmacro %}dbt run -s int_order_items_summary
3+
{%- endmacro %}

0 commit comments

Comments
 (0)