When running dbt in CI, the two biggest pitfalls are leaking secrets and inconsistent environment switching. This article shows the minimal setup for safely switching between dev, staging, and prod using dbt's built-in features (env_var, profiles.yml, target) and a typical CI secret store.
We also organise the topics that appear frequently on the Analytics Engineer exam. Pay close attention to env_var defaults, Jinja in profiles.yml, target.name usage, CI log masking, and per-environment schema switching.
dbt can read environment variables through Jinja. The form env_var('KEY', 'default') safely pulls in values injected by CI or set locally. Omit the default and any missing variable raises a compile error, preventing accidents.
Connection settings live in profiles.yml, and target switches the destination between dev, stg, prod, and so on. Models can reference target.name or target.schema. The golden rule: secrets (passwords, tokens, connection strings) must be injected as environment variables from a CI or dbt Cloud secret store, never written into the repo.
Basic env_var usage (model Jinja and dbt_project.yml)
-- models/stg_orders.sql
{{
config(
schema=env_var('DBT_SCHEMA', target.schema) # CIで上書き、未設定ならtarget.schema
)
}}
select *
from {{ source('raw', 'orders') }}
-- dbt_project.yml(抜粋)
name: my_dbt
version: 1.0.0
config-version: 2
models:
+materialized: view
vars:
environment: "{{ env_var('DBT_ENVIRONMENT', 'dev') }}"The standard practice is to absorb per-environment differences in profiles.yml. Make the target overridable via env_var, and append an environment suffix to schema and database names. This keeps CI and local switching symmetric.
Centralise schema-name branching with the generate_schema_name macro or config(schema=...). Switching via configuration rather than if-statements in SQL is more maintainable and aligns with model exam answers.
Snowflake example: externalising environment and secrets in profiles.yml
my_profile:
target: "{{ env_var('DBT_TARGET_NAME', 'dev') }}"
outputs:
dev:
type: snowflake
account: "{{ env_var('SNOWFLAKE_ACCOUNT') }}"
user: "{{ env_var('SNOWFLAKE_USER') }}"
password: "{{ env_var('SNOWFLAKE_PASSWORD') }}"
role: "{{ env_var('SNOWFLAKE_ROLE', 'DEV_ROLE') }}"
warehouse: "{{ env_var('SNOWFLAKE_WAREHOUSE', 'DEV_WH') }}"
database: "{{ env_var('SNOWFLAKE_DATABASE', 'ANALYTICS_DEV') }}"
schema: "{{ env_var('DBT_SCHEMA', 'dbt_dev') }}"
ci:
type: snowflake
account: "{{ env_var('SNOWFLAKE_ACCOUNT') }}"
user: "{{ env_var('SNOWFLAKE_USER') }}"
password: "{{ env_var('SNOWFLAKE_PASSWORD') }}"
role: "{{ env_var('SNOWFLAKE_ROLE', 'CI_ROLE') }}"
warehouse: "{{ env_var('SNOWFLAKE_WAREHOUSE', 'CI_WH') }}"
database: "{{ env_var('SNOWFLAKE_DATABASE', 'ANALYTICS_CI') }}"
schema: "{{ env_var('DBT_SCHEMA', 'dbt_ci') }}"
prod:
type: snowflake
account: "{{ env_var('SNOWFLAKE_ACCOUNT') }}"
user: "{{ env_var('SNOWFLAKE_USER') }}"
password: "{{ env_var('SNOWFLAKE_PASSWORD') }}"
role: "{{ env_var('SNOWFLAKE_ROLE', 'PROD_ROLE') }}"
warehouse: "{{ env_var('SNOWFLAKE_WAREHOUSE', 'PROD_WH') }}"
database: "{{ env_var('SNOWFLAKE_DATABASE', 'ANALYTICS') }}"
schema: "{{ env_var('DBT_SCHEMA', 'dbt') }}"Secret stores built into CI platforms (GitHub Actions Secrets, GitLab CI/CD Variables, CircleCI Contexts, and so on) inject values as environment variables and automatically mask them in logs. dbt simply reads them via env_var, which keeps things simple and safe.
In large-scale or highly regulated environments, teams typically pair CI with a dedicated store like HashiCorp Vault, having CI fetch a short-lived token and inject it. Either way, the rules are the same: never write secrets in the repo or profiles.yml, follow least privilege, and keep them out of logs.
| Method | Injection | Masking / Access Control | Best Fit |
|---|---|---|---|
| GitHub Actions Secrets | env: KEY: ${{ secrets.KEY }} | Auto-masking; environments & environment protection | Small/medium and general-purpose CI |
| GitLab CI/CD Variables | variables/Masked/Protected | Masking, protected branches, environment scope | When you need strict per-environment control |
| dbt Cloud (env variables) | Set per environment/job -> env_var in Jinja | Encrypted storage; managed in the UI | When you want centralised management in dbt Cloud |
| HashiCorp Vault (combined) | CI fetches short-lived token via OIDC etc. -> injects into env | Fine-grained policy and audit | Highly regulated or zero-trust requirements |
Safe injection on GitHub Actions (bundled into env)
env:
DBT_TARGET_NAME: ci
DBT_ENVIRONMENT: ci
SNOWFLAKE_ACCOUNT: ${{ secrets.SNOWFLAKE_ACCOUNT }}
SNOWFLAKE_USER: ${{ secrets.SNOWFLAKE_USER }}
SNOWFLAKE_PASSWORD: ${{ secrets.SNOWFLAKE_PASSWORD }}
SNOWFLAKE_ROLE: ${{ secrets.SNOWFLAKE_ROLE }}
SNOWFLAKE_WAREHOUSE: ${{ secrets.SNOWFLAKE_WAREHOUSE }}
SNOWFLAKE_DATABASE: ${{ secrets.SNOWFLAKE_DATABASE }}The minimal setup is a serial chain of dependency resolution -> compile/validate -> run -> test. Pin target to ci and use CI-only resources for the schema/database. Logs are masked and SQL contains no embedded secrets.
Below is an example of running dbt-snowflake on GitHub Actions. The same idea applies to any CI's YAML.
Secret injection and execution flow in CI
GitHub Actions: dbt CI workflow (excerpt)
name: dbt-ci
on:
pull_request:
branches: [ main ]
jobs:
run-dbt:
runs-on: ubuntu-latest
env:
DBT_TARGET_NAME: ci
DBT_ENVIRONMENT: ci
SNOWFLAKE_ACCOUNT: ${{ secrets.SNOWFLAKE_ACCOUNT }}
SNOWFLAKE_USER: ${{ secrets.SNOWFLAKE_USER }}
SNOWFLAKE_PASSWORD: ${{ secrets.SNOWFLAKE_PASSWORD }}
SNOWFLAKE_ROLE: ${{ secrets.SNOWFLAKE_ROLE }}
SNOWFLAKE_WAREHOUSE: ${{ secrets.SNOWFLAKE_WAREHOUSE }}
SNOWFLAKE_DATABASE: ${{ secrets.SNOWFLAKE_DATABASE }}
DBT_SCHEMA: dbt_ci_${{ github.actor }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install dbt adapter
run: |
python -m pip install --upgrade pip
pip install dbt-snowflake
- name: Verify deps and connection
run: |
dbt deps
dbt debug --target ci --no-write-json
- name: Compile
run: dbt compile --target ci --warn-error
- name: Run and test
run: |
dbt run --target ci --fail-fast --select state:modified+
dbt test --target ci --fail-fastMasking can fail when CI scripts use set -x or echo to print environment variables. The rule: do not reference, log, or display secrets. dbt's own logs print things like connection test results, so enable CI-side masking and minimise debug verbosity.
If a required environment variable is missing in Jinja, raise a compile error early to halt execution. This prevents accidents like running against the wrong target or with anonymous credentials.
Practical snippets for keeping secrets out of logs
# シェル: コマンドエコー抑止
set +x
# Jinja: 必須環境変数の検査(任意のmacroやmodel冒頭で)
{% if not env_var('SNOWFLAKE_PASSWORD', none) %}
{{ exceptions.raise_compiler_error('Missing SNOWFLAKE_PASSWORD') }}
{% endif %}
# dbt実行時は必要最小限の出力
# 例: 失敗時にのみ詳細を参照する運用
dbt run --target ci --fail-fast
# 必要なら --log-level warn などで情報量を絞るThe Analytics Engineer exam frequently tests separation of concerns between environment variables and configuration. The keys are: keep environment-specific branching out of SQL by switching via profiles.yml and target, inject secrets from a CI or dbt Cloud secure store, and understand env_var defaults.
Operational judgment questions also come up, such as whether the same pipeline can swap configuration from dev to prod, and whether secrets leak into logs or metadata.
Example commands for switching environments
# CIターゲットで実行
DBT_TARGET_NAME=ci DBT_ENVIRONMENT=ci dbt run --target ci
# ローカル開発(デフォルトのdevにフォールバック)
dbt run # profiles.ymlのtarget: dev が使われることを想定
# 明示的にprodを指定(本番は保護ブランチ/承認必須が望ましい)
DBT_TARGET_NAME=prod dbt test --target prodAnalytics Engineer
問題 1
You are running a dbt Core project that connects to Snowflake in CI. You want to keep the password out of the repo and switch schemas per branch. Which combination is most appropriate?
正解: A
The standard, safe approach is to inject secrets as environment variables from a CI secret store and reference them in profiles.yml via env_var. Externalise the schema via DBT_SCHEMA and pin target to ci, switching by configuration. Storing passwords in vars, branching in SQL, or hand-editing files are all anti-patterns.
What is the difference between env_var and vars? Which should hold secrets?
env_var pulls OS environment variables into Jinja, while vars are dbt's user-defined variables. Secrets should never live in vars. The standard pattern is: CI or dbt Cloud secret store -> environment variable -> env_var reference.
How do you use environment variables in dbt Cloud?
Register environment variables under Environment or Job settings, then reference them with env_var('KEY') in Jinja from models, dbt_project.yml, or profiles.yml. Values are managed in the UI and never live in the repo.
How can you detect missing environment variables and fail safely?
Omit the default argument to env_var('KEY') so any missing variable triggers a compile error. You can also inspect the value in Jinja and call exceptions.raise_compiler_error to halt explicitly.
Practice with certification-focused question sets
無料で問題を解いてみるNicheeLab Editorial Team
NicheeLab editorial team focused on data engineering and cloud certification learning. Content is structured around practical study needs and official exam domains.
dbt Models: SQL-Defined Transformation Units (2026)
Model fundamentals — SELECT-based definitions, naming, refs,...
dbt Analytics Engineering Exam: Complete Guide (2026)
Pass the AE Certification — scope, weighting, sample questio...
dbt Cloud vs dbt Core: Feature & Cost Comparison (2026)
Honest comparison of dbt Cloud vs. dbt Core — IDE, scheduler...
dbt Project Structure: models/seeds/macros Layout (2026)
Recommended dbt project layout — models, seeds, macros, snap...
dbt_project.yml Explained: Every Config (2026)
Every dbt_project.yml setting that matters — paths, vars, ma...