[新機能]dbt Projects on Snowflake で Slim CI / Defer to Production が一般提供になりました
はじめに
2026年9月のアップデートで、dbt Projects on Snowflake の Slim CI と Defer to Production が一般提供となりました。
こちらの機能を試してみた内容を本記事でまとめます。
アップデートの概要
本機能については以下に記載があります。
Slim CI / defer / state について
はじめに dbt 自体の Slim CI の仕組みを整理しておきます。
Slim CI とは「変更があったモデルとその下流だけを実際にビルド・テストし、それ以外のモデルへの参照は本番の実体をそのまま使う」ことで、プロジェクトの規模が大きくなっても CI の実行時間や計算コストを抑えられる仕組みです。
具体的には、dbt の実行のたびに生成されるアーティファクト(プロジェクト構造・実行結果を記録したメタデータファイル一式)のうち、過去の実行で生成されたmanifest.jsonを読み込み、現在のプロジェクトとの差分を取ることでこれを実現します。実際には、以下のように--select state:modified+と--defer・--stateという dbt のコマンドオプションを組み合わせることで実行する、dbt がもともと備えている仕組みです。
dbt build --select state:modified+ --defer --state ./prod-artifacts/
state(manifest.json)- プロジェクトの静的構造(ノード定義・SQL・依存関係)のスナップショットです。
--stateオプションで読み込み、現在のプロジェクトとの比較基準にします。実体は、過去のdbt build実行で生成されたtarget/manifest.jsonを指します
- プロジェクトの静的構造(ノード定義・SQL・依存関係)のスナップショットです。
state:modified+--stateの内容と現在のプロジェクトを比較し、変更されたノードとその下流だけを選択するセレクタです
--defer- 選択されなかったモデルの
ref()を、--stateで渡した manifest に記録済みのリレーション(多くの場合は本番)に解決します
- 選択されなかったモデルの
これまでは、本番のmanifest.json/run_results.jsonをどのように次の CI 実行に渡すかが課題でした(S3 へのアップロード など)。
従来の dbt project object は、デプロイのたびに不変(immutable)の番号付きバージョン(VERSION$1など)が積み上がる方式でしたが、直近の動作変更(BCR-2362)によりliveという単一の可変(mutable)バージョンに統一されました。この動作変更は2026_06バンドルに含まれており、Slim CI / Defer to Production を使うにはこのバンドルが有効になっている必要があります。
単一の可変バージョンになったことで、dbt project object 自身が直近の実行結果を保持できるようになり、SYSTEM$DBT_GET_LAST_SUCCESSFUL_RUN_TARGET関数で本番の直近成功実行を直接取得できるようになりました。これにより、state の受け渡しを外部の仕組みに頼らず dbt project object 自体が担えるようになった、というのが今回のアップデートの特徴です。
なお、この関数が取得できるのは直近 7 日以内に成功した実行に限られる点には注意が必要です。本番の dbt project object が 7 日以上成功実行されていない場合、Slim CI 側で defer 先の state を取得できません。
これにより、具体的には以下が可能になっています。
- Slim CI
- 本番の直近実行アーティファクト(
manifest.json/run_results.json)をインポートし、state:modified+セレクタで変更されたモデルとその下流だけをビルドする
- 本番の直近実行アーティファクト(
- Defer to Production
--deferにより、ビルド対象に含まれない上流モデルの参照を本番の既存リレーションに解決する
- 失敗回復・並行実行
dbt retryによるエラーリソースの再実行に加え、同一の project object を複製することなく並行実行できるようにもなりました
並列実行については、以下の記事で紹介されていますので、こちらをご参照ください。
また、公式によるチュートリアルも公開されています。
試してみる
前提条件
以下の環境を使用しています。
- BCR バンドル: 2026_06(有効化済み)
- Snowflake CLI version: 3.27.0
- サンプルデータ: dbt 公式の jaffle shop チュートリアルを使用
- CI/CD: GitHub Actions + OIDC(Workload Identity Federation)
検証環境
今回の検証では、以下のようにデータベースを構成しました。
| データベース | 役割 | 主なオブジェクト |
|---|---|---|
raw |
生データ(環境に紐付かない共有 DB) | jaffle_shop/stripeスキーマ、customers/orders/paymentテーブル |
prd_db |
本番(prd)の dbt 出力先 |
ここでは固定のdbt_outputスキーマ |
dev_db |
開発(dev)の dbt 出力先。CI 実行時は PR ごとの検証用スキーマの作成先も兼ねる |
開発者ごとのdbt_<username>スキーマ、PR ごとのpr_<N>スキーマ |
slim_ci_demo |
dbt project object の管理用 | dbt_objectsスキーマ(本番用のdbt_slimci_demo、CI 用のci_pr_<N>) |
ウェアハウス・ロールもprd/dev/ciの env ごとに${env_name}_dbt_whs・${env_name}_dbt_roleという命名で分けています。生データ(raw)だけは env に紐付けず、どの env からも同じデータを参照する構成にしました。
まず、生データ用のrawデータベースを準備します。
CREATE DATABASE IF NOT EXISTS raw;
CREATE SCHEMA IF NOT EXISTS raw.jaffle_shop;
CREATE SCHEMA IF NOT EXISTS raw.stripe;
続けて、rawデータベースにサンプルデータを追加します。
raw テーブル作成・COPY INTO の SQL
CREATE TABLE IF NOT EXISTS raw.jaffle_shop.customers (
id INTEGER,
first_name VARCHAR,
last_name VARCHAR
);
COPY INTO raw.jaffle_shop.customers (id, first_name, last_name)
FROM 's3://dbt-tutorial-public/jaffle_shop_customers.csv'
FILE_FORMAT = (
TYPE = 'CSV',
FIELD_DELIMITER = ',',
SKIP_HEADER = 1
);
CREATE TABLE IF NOT EXISTS raw.jaffle_shop.orders (
id INTEGER,
user_id INTEGER,
order_date DATE,
status VARCHAR,
_etl_loaded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
COPY INTO raw.jaffle_shop.orders (id, user_id, order_date, status)
FROM 's3://dbt-tutorial-public/jaffle_shop_orders.csv'
FILE_FORMAT = (
TYPE = 'CSV',
FIELD_DELIMITER = ',',
SKIP_HEADER = 1
);
CREATE TABLE IF NOT EXISTS raw.stripe.payment (
id INTEGER,
orderid INTEGER,
paymentmethod VARCHAR,
status VARCHAR,
amount INTEGER,
created DATE,
_batched_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
COPY INTO raw.stripe.payment (id, orderid, paymentmethod, status, amount, created)
FROM 's3://dbt-tutorial-public/stripe_payments.csv'
FILE_FORMAT = (
TYPE = 'CSV',
FIELD_DELIMITER = ',',
SKIP_HEADER = 1
);
続けて、dbt project object を管理するためのslim_ci_demoデータベースも作成しておきます。本番用(dbt_slimci_demo)・CI 用(ci_pr_<PR番号>)のいずれの project object も、この下のdbt_objectsスキーマに作成します。
CREATE DATABASE IF NOT EXISTS slim_ci_demo;
CREATE SCHEMA IF NOT EXISTS slim_ci_demo.dbt_objects;
続けて、env_nameごとにウェアハウス・データベース・ロールを作成します。生データ(raw)だけは env に紐付けず、共通で参照します。prd/dev/ciで dbt 出力先の持ち方が異なるため、環境別に記載します。
prd(本番。固定の dbt 出力スキーマを 1 つだけ持つ)
USE ROLE ACCOUNTADMIN;
SET env_name = 'prd';
SET db_name = concat($env_name,'_db');
SET wh_name = concat($env_name,'_dbt_whs');
SET role_name = concat($env_name,'_dbt_role');
SET dbt_schema_name = concat($db_name,'.dbt_output');
CREATE WAREHOUSE IF NOT EXISTS identifier($wh_name)
WITH
WAREHOUSE_SIZE = XSMALL
AUTO_SUSPEND = 60
AUTO_RESUME = TRUE
INITIALLY_SUSPENDED = TRUE;
CREATE DATABASE IF NOT EXISTS identifier($db_name);
CREATE SCHEMA IF NOT EXISTS identifier($dbt_schema_name);
USE ROLE SECURITYADMIN;
CREATE ROLE IF NOT EXISTS identifier($role_name);
GRANT ROLE identifier($role_name) TO ROLE SYSADMIN;
GRANT USAGE, OPERATE ON WAREHOUSE identifier($wh_name) TO ROLE identifier($role_name);
GRANT USAGE ON DATABASE raw TO ROLE identifier($role_name);
GRANT USAGE ON SCHEMA raw.jaffle_shop TO ROLE identifier($role_name);
GRANT USAGE ON SCHEMA raw.stripe TO ROLE identifier($role_name);
GRANT SELECT ON ALL TABLES IN SCHEMA raw.jaffle_shop TO ROLE identifier($role_name);
GRANT SELECT ON FUTURE TABLES IN SCHEMA raw.jaffle_shop TO ROLE identifier($role_name);
GRANT SELECT ON ALL VIEWS IN SCHEMA raw.jaffle_shop TO ROLE identifier($role_name);
GRANT SELECT ON FUTURE VIEWS IN SCHEMA raw.jaffle_shop TO ROLE identifier($role_name);
GRANT SELECT ON ALL TABLES IN SCHEMA raw.stripe TO ROLE identifier($role_name);
GRANT SELECT ON FUTURE TABLES IN SCHEMA raw.stripe TO ROLE identifier($role_name);
GRANT SELECT ON ALL VIEWS IN SCHEMA raw.stripe TO ROLE identifier($role_name);
GRANT SELECT ON FUTURE VIEWS IN SCHEMA raw.stripe TO ROLE identifier($role_name);
-- 固定の dbt 出力スキーマへの書き込み権限
GRANT USAGE ON DATABASE identifier($db_name) TO ROLE identifier($role_name);
GRANT USAGE ON SCHEMA identifier($dbt_schema_name) TO ROLE identifier($role_name);
GRANT CREATE TABLE ON SCHEMA identifier($dbt_schema_name) TO ROLE identifier($role_name);
GRANT CREATE VIEW ON SCHEMA identifier($dbt_schema_name) TO ROLE identifier($role_name);
dev(開発。固定スキーマは作らず、開発者ごとに自分のスキーマを作成できるようにする)
USE ROLE ACCOUNTADMIN;
SET env_name = 'dev';
SET db_name = concat($env_name,'_db');
SET wh_name = concat($env_name,'_dbt_whs');
SET role_name = concat($env_name,'_dbt_role');
CREATE WAREHOUSE IF NOT EXISTS identifier($wh_name)
WITH
WAREHOUSE_SIZE = XSMALL
AUTO_SUSPEND = 60
AUTO_RESUME = TRUE
INITIALLY_SUSPENDED = TRUE;
-- スキーマは開発者ごとに CREATE SCHEMA で作らせるため、ここでは DB のみ作成
CREATE DATABASE IF NOT EXISTS identifier($db_name);
USE ROLE SECURITYADMIN;
CREATE ROLE IF NOT EXISTS identifier($role_name);
GRANT ROLE identifier($role_name) TO ROLE SYSADMIN;
GRANT USAGE, OPERATE ON WAREHOUSE identifier($wh_name) TO ROLE identifier($role_name);
GRANT USAGE ON DATABASE raw TO ROLE identifier($role_name);
GRANT USAGE ON SCHEMA raw.jaffle_shop TO ROLE identifier($role_name);
GRANT USAGE ON SCHEMA raw.stripe TO ROLE identifier($role_name);
GRANT SELECT ON ALL TABLES IN SCHEMA raw.jaffle_shop TO ROLE identifier($role_name);
GRANT SELECT ON FUTURE TABLES IN SCHEMA raw.jaffle_shop TO ROLE identifier($role_name);
GRANT SELECT ON ALL VIEWS IN SCHEMA raw.jaffle_shop TO ROLE identifier($role_name);
GRANT SELECT ON FUTURE VIEWS IN SCHEMA raw.jaffle_shop TO ROLE identifier($role_name);
GRANT SELECT ON ALL TABLES IN SCHEMA raw.stripe TO ROLE identifier($role_name);
GRANT SELECT ON FUTURE TABLES IN SCHEMA raw.stripe TO ROLE identifier($role_name);
GRANT SELECT ON ALL VIEWS IN SCHEMA raw.stripe TO ROLE identifier($role_name);
GRANT SELECT ON FUTURE VIEWS IN SCHEMA raw.stripe TO ROLE identifier($role_name);
-- 固定スキーマではなく、dev_db 配下に開発者ごとのスキーマを自由に作成できるようにする
GRANT USAGE ON DATABASE identifier($db_name) TO ROLE identifier($role_name);
GRANT CREATE SCHEMA ON DATABASE identifier($db_name) TO ROLE identifier($role_name);
ciのロールには本番(prd_db.dbt_output)への読み取り権限も持たせています。これは、Slim CI の--deferがビルド対象に含めなかったモデルのref()を本番のリレーションへ直接解決するため、実行時にその本番リレーションをSELECTできる権限がciロール自身に必要になるためです。
ci(CI/CD 自動実行専用。固定 DB は持たず、PR ごとに dev_db 配下に PR 専用スキーマを作成する)
USE ROLE ACCOUNTADMIN;
SET env_name = 'ci';
SET wh_name = concat($env_name,'_dbt_whs');
SET role_name = concat($env_name,'_dbt_role');
CREATE WAREHOUSE IF NOT EXISTS identifier($wh_name)
WITH
WAREHOUSE_SIZE = XSMALL
AUTO_SUSPEND = 60
AUTO_RESUME = TRUE
INITIALLY_SUSPENDED = TRUE;
USE ROLE SECURITYADMIN;
CREATE ROLE IF NOT EXISTS identifier($role_name);
GRANT ROLE identifier($role_name) TO ROLE SYSADMIN;
GRANT USAGE, OPERATE ON WAREHOUSE identifier($wh_name) TO ROLE identifier($role_name);
GRANT USAGE ON DATABASE raw TO ROLE identifier($role_name);
GRANT USAGE ON SCHEMA raw.jaffle_shop TO ROLE identifier($role_name);
GRANT USAGE ON SCHEMA raw.stripe TO ROLE identifier($role_name);
GRANT SELECT ON ALL TABLES IN SCHEMA raw.jaffle_shop TO ROLE identifier($role_name);
GRANT SELECT ON FUTURE TABLES IN SCHEMA raw.jaffle_shop TO ROLE identifier($role_name);
GRANT SELECT ON ALL VIEWS IN SCHEMA raw.jaffle_shop TO ROLE identifier($role_name);
GRANT SELECT ON FUTURE VIEWS IN SCHEMA raw.jaffle_shop TO ROLE identifier($role_name);
GRANT SELECT ON ALL TABLES IN SCHEMA raw.stripe TO ROLE identifier($role_name);
GRANT SELECT ON FUTURE TABLES IN SCHEMA raw.stripe TO ROLE identifier($role_name);
GRANT SELECT ON ALL VIEWS IN SCHEMA raw.stripe TO ROLE identifier($role_name);
GRANT SELECT ON FUTURE VIEWS IN SCHEMA raw.stripe TO ROLE identifier($role_name);
-- --defer で未選択モデルの ref() が prd_db.dbt_output を直接参照するための読み取り権限
GRANT USAGE ON DATABASE prd_db TO ROLE identifier($role_name);
GRANT USAGE ON SCHEMA prd_db.dbt_output TO ROLE identifier($role_name);
GRANT SELECT ON ALL TABLES IN SCHEMA prd_db.dbt_output TO ROLE identifier($role_name);
GRANT SELECT ON FUTURE TABLES IN SCHEMA prd_db.dbt_output TO ROLE identifier($role_name);
GRANT SELECT ON ALL VIEWS IN SCHEMA prd_db.dbt_output TO ROLE identifier($role_name);
GRANT SELECT ON FUTURE VIEWS IN SCHEMA prd_db.dbt_output TO ROLE identifier($role_name);
-- dev_db 配下に PR ごとのスキーマを作成できるようにする(CREATE DATABASE ON ACCOUNT より狭い権限で済む)
GRANT USAGE ON DATABASE dev_db TO ROLE identifier($role_name);
GRANT CREATE SCHEMA ON DATABASE dev_db TO ROLE identifier($role_name);
Workspace で dbt project object の作成
Snowflake Workspace から GitHub リポジトリと連携した dbt project を作成します。まず OAuth 用の API Integration を作成しておきます。
USE ROLE ACCOUNTADMIN;
CREATE OR REPLACE API INTEGRATION oauth2_api_integration
API_PROVIDER = git_https_api
API_ALLOWED_PREFIXES = (
'https://github.com/<アカウント名>/'
)
API_USER_AUTHENTICATION = (TYPE = SNOWFLAKE_GITHUB_APP)
ENABLED = TRUE;
API 統合作成後、dbt project を作成します。

env.yml を追加
続けて、env.ymlを追加します。env.ymlを使うと、環境ごとの変数(データベース名・スキーマ名)を SQL 式で切り替えられます。開発者ごとのスキーマ切り替えにはCURRENT_USER()を使いました。
env_config:
default_environment: dev
environments:
- name: dev
env:
DBT_TARGET_DATABASE: dev_db
DBT_TARGET_SCHEMA: "{{ select 'dbt_' || UPPER(CURRENT_USER()) }}"
- name: prd
env:
DBT_TARGET_DATABASE: prd_db
DBT_TARGET_SCHEMA: dbt_output
profiles.yml を追加
続けて、profiles.ymlを追加します。profiles.yml側はenv_var()で参照します。
slim_cicd_demo:
target: dev
outputs:
dev:
type: snowflake
role: DEV_DBT_ROLE
warehouse: DEV_DBT_WHS
database: "{{ env_var('DBT_TARGET_DATABASE') }}"
schema: "{{ env_var('DBT_TARGET_SCHEMA') }}"
threads: 8
prd:
type: snowflake
role: PRD_DBT_ROLE
warehouse: PRD_DBT_WHS
database: "{{ env_var('DBT_TARGET_DATABASE') }}"
schema: "{{ env_var('DBT_TARGET_SCHEMA') }}"
threads: 8
ci:
type: snowflake
role: CI_DBT_ROLE
warehouse: CI_DBT_WHS
database: "{{ env_var('DBT_TARGET_DATABASE') }}"
schema: "{{ env_var('DBT_TARGET_SCHEMA') }}"
threads: 8
基本的な開発を行う
dbt project object の作成ができたので、続けてmodels/配下に通常の dbt 開発と同様にソース定義とモデルファイルを追加しました。
まずソースをsources.ymlで定義します。
sources.yml
version: 2
sources:
- name: jaffle_shop
description: This is a replica of the Postgres database used by our app
database: raw
schema: jaffle_shop
tables:
- name: customers
description: One record per customer.
- name: orders
description: One record per order. Includes cancelled and deleted orders.
- name: stripe
description: Stripe payment data used by our app
database: raw
schema: stripe
tables:
- name: payment
description: One record per payment. Includes payment method and status.
続けて、STG モデルを 2 つと、それらを参照するcustomersモデルを追加します。
stg_customers.sql / stg_orders.sql / customers.sql
select
id as customer_id,
first_name,
last_name,
first_name || ' ' || last_name as full_name
from {{ source('jaffle_shop', 'customers') }}
select
id as order_id,
user_id as customer_id,
order_date,
status
from {{ source('jaffle_shop', 'orders') }}
with customers as (
select * from {{ ref('stg_customers') }}
),
orders as (
select * from {{ ref('stg_orders') }}
),
customer_orders as (
select
customer_id,
min(order_date) as first_order_date,
max(order_date) as most_recent_order_date,
count(order_id) as number_of_orders
from orders
group by 1
),
final as (
select
customers.customer_id,
customers.first_name,
customers.last_name,
customer_orders.first_order_date,
customer_orders.most_recent_order_date,
coalesce(customer_orders.number_of_orders, 0) as number_of_orders
from customers
left join customer_orders using (customer_id)
)
select * from final
開発環境でdbt runを実行し、追加したモデルが指定のスキーマ(ここでは開発環境のユーザースキーマ)に作成されることを確認します。

CI/CD 用の準備(サービスユーザー・CD パイプライン)
このあとの手順でSYSTEM$DBT_GET_LAST_SUCCESSFUL_RUN_TARGETで「本番の直近成功実行」を参照するには、あらかじめ本番(prd)側で一度は成功実行が記録されている必要があります。そこで先に、main ブランチへの push 時に本番へデプロイ・ビルドする CD パイプラインを組んでおきます。PR 時に動く CI 用のサービスユーザー・ワークフローは、後ほど作成します。
まず、prd_dbt_roleにslim_ci_demoデータベース・dbt_objectsスキーマへのUSAGEと、その配下へ dbt project object を作成する権限を付与します。CD パイプラインはこのロールで本番用の project object を新規作成・デプロイするため、これらの権限がないとsnow dbt deployが失敗します。
USE ROLE SECURITYADMIN;
GRANT USAGE ON DATABASE slim_ci_demo TO ROLE prd_dbt_role;
GRANT USAGE ON SCHEMA slim_ci_demo.dbt_objects TO ROLE prd_dbt_role;
GRANT CREATE DBT PROJECT ON SCHEMA slim_ci_demo.dbt_objects TO ROLE prd_dbt_role;
続けて、GitHub Actions の OIDC トークンを受け取れる CD 専用のサービスユーザーを作成します。パスワードや PAT を発行せず、Workload Identity Federation でトークンをそのまま検証させます。
CREATE USER IF NOT EXISTS svc_gha_dbt_cd
TYPE = SERVICE
WORKLOAD_IDENTITY = (
TYPE = OIDC
ISSUER = 'https://token.actions.githubusercontent.com'
SUBJECT = 'repo:<owner>@<owner の数値 ID>/<repo>@<repo の数値 ID>:ref:refs/heads/main'
)
DEFAULT_ROLE = prd_dbt_role
DEFAULT_WAREHOUSE = prd_dbt_whs
COMMENT = 'GitHub Actions CD (push to main) service user';
GRANT ROLE prd_dbt_role TO USER svc_gha_dbt_cd;
SUBJECTは OIDC トークンのsubclaim と大文字小文字含めて完全一致させる必要があります。
OIDC による連携は以下の記事が参考になると思います。
GitHub 側は Repository Secret に接続先アカウントのみを登録します。OIDC 認証のみを使うため、パスワードや PAT は不要です。
| Name | Value |
|---|---|
SNOWFLAKE_ACCOUNT |
<組織名>-<アカウント名> |
続けて、main への push をトリガーに本番の dbt project object をデプロイ・ビルドするdeploy.ymlを追加します。
name: CD - deploy dbt project on merge
on:
push:
branches: [main]
concurrency:
group: cd-prod
cancel-in-progress: false
permissions:
contents: read
id-token: write # OIDC トークン発行に必須
jobs:
dbt-deploy:
runs-on: ubuntu-latest
env:
SNOWFLAKE_CLI_FEATURES_ENABLE_DBT: true
SNOWFLAKE_ACCOUNT: ${{ secrets.SNOWFLAKE_ACCOUNT }}
SNOWFLAKE_ROLE: PRD_DBT_ROLE
SNOWFLAKE_WAREHOUSE: PRD_DBT_WHS
SNOWFLAKE_DATABASE: SLIM_CI_DEMO
SNOWFLAKE_SCHEMA: DBT_OBJECTS
steps:
- uses: actions/checkout@v4
- name: Install Snowflake CLI
uses: snowflakedb/snowflake-actions@v3
with:
use-oidc: true # OIDC認証を有効化
- name: Test connection
run: snow connection test -x
- name: Deploy production dbt project object
# auto-compile(デフォルトTrue)は env.yml の default_environment(dev)でコンパイルしようとするため、
# --default-target(dbt側)と対になる --default-env(env.yml側)も明示して prd に固定する。
run: snow dbt deploy slim_ci_demo.dbt_objects.dbt_slimci_demo --source ./slim_cicd_demo --default-target prd --default-env prd -x
- name: Build models on prd
# --target(dbtのprofiles.yml出力選択)と --env(env.ymlの環境変数選択)は別軸。
# --target prd だけでは env.yml の default_environment(dev)が使われてしまうため --env prd を明示する。
run: snow dbt execute -x --env prd slim_ci_demo.dbt_objects.dbt_slimci_demo build --target prd
ポイントは以下です。
--target(dbt のprofiles.ymlの出力先選択)と--env(env.ymlの環境変数選択)は別軸のオプションでdeploy時は--default-target/--default-env、execute時は--target/--envという対応関係になりますsnow dbt deployはデフォルトで--auto-compileが有効なため、--default-target prdだけを指定してもコンパイル自体はenv.ymlのdefault_environment(今回はdev)で行われてしまいます。そのため--default-env prdも明示して揃える必要があります- 同様に
snow dbt executeでも、--target prdだけではenv.ymlのdefault_environmentが使われてしまうため、--env prdを明示しています - このワークフローでは
--selectや--state/--deferを使用していません。そのため、Slim CI のような差分ビルドではなく、prdの全モデルを毎回フルビルドする通常の CD です
このワークフローを main にマージすることで、本番の dbt project object に最初の成功実行が記録され、Slim CI 実行時に defer 先として参照できるようになります。実際に main へマージし、GitHub Actions 上でこの CD が成功することを確認します。

Slim CI 実行の準備(ci ロールへの権限付与)
Slim CI 側で使うci_dbt_role/ci_dbt_whs自体はすでに作成済みですが、実際に Slim CI を実行するにはさらに権限が必要です。slim_ci_demoデータベース・dbt_objectsスキーマへのUSAGE、その配下へ PR 専用の dbt project object を作成する権限、そしてSYSTEM$DBT_GET_LAST_SUCCESSFUL_RUN_TARGETを呼び出すために本番の dbt project object に対して必要なMONITOR権限です。
USE ROLE SECURITYADMIN;
GRANT USAGE ON DATABASE slim_ci_demo TO ROLE ci_dbt_role;
GRANT USAGE ON SCHEMA slim_ci_demo.dbt_objects TO ROLE ci_dbt_role;
GRANT CREATE DBT PROJECT ON SCHEMA slim_ci_demo.dbt_objects TO ROLE ci_dbt_role;
GRANT MONITOR ON DBT PROJECT slim_ci_demo.dbt_objects.dbt_slimci_demo TO ROLE ci_dbt_role;
GitHub Actions + OIDC で CI/CD パイプラインを組む
CD 側と同様に、PR 時(CI)用の OIDC サービスユーザーを作成します。CD とはイベント種別(pull_request)が異なるだけです。
USE ROLE ACCOUNTADMIN;
CREATE USER IF NOT EXISTS svc_gha_dbt_ci
TYPE = SERVICE
WORKLOAD_IDENTITY = (
TYPE = OIDC
ISSUER = 'https://token.actions.githubusercontent.com'
SUBJECT = 'repo:<owner>@<owner の数値 ID>/<repo>@<repo の数値 ID>:pull_request'
)
DEFAULT_ROLE = ci_dbt_role
DEFAULT_WAREHOUSE = ci_dbt_whs;
USE ROLE SECURITYADMIN;
GRANT ROLE ci_dbt_role TO USER svc_gha_dbt_ci;
SUBJECTの形式は CD 用サービスユーザーと同様に、組織・リポジトリの数値 ID が付与された形式です(前述の「CI/CD 用の準備」参照)。イベント種別の部分だけがref:refs/heads/mainではなくpull_requestになります。
CI 用のワークフローでは、PR ごとに開発用データベース配下へ専用スキーマを作成し、そこへ変更モデルと下流だけをビルドします。
name: CI - dbt Slim CI build on PR
on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review, closed]
branches: [main]
concurrency:
group: ci-pr-${{ github.event.pull_request.number }}
cancel-in-progress: true
permissions:
contents: read
id-token: write # OIDC トークン発行に必須
jobs:
dbt-ci:
if: github.event.action != 'closed'
runs-on: ubuntu-latest
env:
SNOWFLAKE_CLI_FEATURES_ENABLE_DBT: true
SNOWFLAKE_ACCOUNT: ${{ secrets.SNOWFLAKE_ACCOUNT }}
SNOWFLAKE_ROLE: CI_DBT_ROLE
SNOWFLAKE_WAREHOUSE: CI_DBT_WHS
SNOWFLAKE_DATABASE: SLIM_CI_DEMO
SNOWFLAKE_SCHEMA: DBT_OBJECTS
PR_NUMBER: ${{ github.event.pull_request.number }}
steps:
- uses: actions/checkout@v4
- name: Install Snowflake CLI
uses: snowflakedb/snowflake-actions@v3
with:
use-oidc: true # OIDC認証を有効化
- name: Test connection
run: snow connection test -x
- name: Create PR-scoped schema under dev_db
run: snow sql -x -q "CREATE SCHEMA IF NOT EXISTS dev_db.pr_${PR_NUMBER};"
- name: Deploy tester dbt project object for this PR
# auto-compile はデフォルト有効だが env.yml に ci 環境が無いため --no-auto-compile で回避
run: snow dbt deploy slim_ci_demo.dbt_objects.ci_pr_${PR_NUMBER} --source ./slim_cicd_demo --default-target ci --no-auto-compile -x
- name: Build changed models + downstream (Slim CI, deferred to prd)
# --env-vars で DBT_TARGET_DATABASE/SCHEMA を PR 専用スキーマに直接上書き(env.yml の環境選択は使わない)
# --import で本番の直近成功実行アーティファクトを取り込み、--state/--defer/--select state:modified+ で Slim CI を実行
run: |
snow dbt execute -x \
--env-vars "{\"DBT_TARGET_DATABASE\": \"dev_db\", \"DBT_TARGET_SCHEMA\": \"pr_${PR_NUMBER}\"}" \
--import "SYSTEM\$DBT_GET_LAST_SUCCESSFUL_RUN_TARGET('slim_ci_demo.dbt_objects.dbt_slimci_demo') AS 'state'" \
slim_ci_demo.dbt_objects.ci_pr_${PR_NUMBER} \
build --target ci --state ./imports/state --defer --select state:modified+
cleanup:
if: github.event.action == 'closed'
runs-on: ubuntu-latest
env:
SNOWFLAKE_CLI_FEATURES_ENABLE_DBT: true
SNOWFLAKE_ACCOUNT: ${{ secrets.SNOWFLAKE_ACCOUNT }}
SNOWFLAKE_ROLE: CI_DBT_ROLE
SNOWFLAKE_WAREHOUSE: CI_DBT_WHS
PR_NUMBER: ${{ github.event.pull_request.number }}
steps:
- name: Install Snowflake CLI
uses: snowflakedb/snowflake-actions@v3
with:
use-oidc: true
- name: Drop PR schema and tester project object
run: |
snow sql -x -q "DROP SCHEMA IF EXISTS dev_db.pr_${PR_NUMBER};"
snow sql -x -q "DROP DBT PROJECT IF EXISTS slim_ci_demo.dbt_objects.ci_pr_${PR_NUMBER};"
ポイントは以下です。
dbt-ciジョブは PR の作成・更新時に、cleanupジョブは PR のクローズ時(マージ・クローズ問わずclosed)にそれぞれ動きます- PR 専用スキーマ(
dev_db.pr_<PR番号>)と PR 専用の dbt project object(ci_pr_<PR番号>)は、PR ごとに作成・削除します snow dbt deployはデフォルトで--auto-compileが有効なため、デプロイ時にenv.ymlのdefault_environment(今回はdev)でコンパイルを試みます。--default-target ciを指定しても、env.ymlにci環境がそもそも存在しないため--default-env ciで揃えることもできません。そこで--no-auto-compileでデプロイ時のコンパイル自体を丸ごとスキップし、project object の登録のみを行っています。実際のコンパイル・変数解決は次のsnow dbt execute側(--env-vars)で行います- 実際にビルドする
snow dbt executeでは、--envではなく--env-varsでDBT_TARGET_DATABASE/DBT_TARGET_SCHEMAを PR 専用スキーマへ直接上書きしています。PR 番号は GitHub Actions 側でしか分からずenv.ymlの SQL 式では表現できないため、env.yml側にci環境をあえて定義していません --importで本番の直近成功実行を取り込み、--state/--defer/--select state:modified+で Slim CI を実行しています
PR での動作確認
実際に PR を作成し、2 パターンで Slim CI の挙動を確認しました。
PR #1: stg_paymentsの追加(新規モデル)
Stripe のソース(raw.stripe.payment。ソース定義自体はsources.ymlに含めています)を参照するstg_paymentsモデルを新規追加する PR です。既存のモデルには依存していない、新規モデルだけのシンプルなケースです。
select
id as payment_id,
orderid as order_id,
paymentmethod as payment_method,
status,
amount,
created
from {{ source('stripe', 'payment') }}
state:modified+により、新規追加したstg_paymentsだけが選択されてビルドされ、既存のstg_customers/stg_orders/customersはいずれも選択されないことを確認できました。


このタイミングでは PR はまだオープンなので、ci.ymlのcleanupジョブはスキップされています。

その後 PR をマージすると、今度はcleanupジョブが実行され、PR 専用のスキーマ・project object が削除されます。

PR #2: stg_customersの変更(既存モデル変更+下流伝播)
続けて、既存モデルを変更した場合に「変更モデル+その下流だけがビルドされる」ことを確認します。stg_customers.sqlにfull_name列を追加しました。
select
id as customer_id,
first_name,
last_name,
first_name || ' ' || last_name as full_name
from {{ source('jaffle_shop', 'customers') }}
customersモデルはstg_customersとstg_ordersの両方をref()しているため、stg_customersを変更するとcustomersは下流としてビルド対象に入り、無関係なstg_orders/stg_paymentsは選択されないはずです。実際のビルドログでも、想定どおりstg_customersとcustomersの 2 モデルだけが選択されました。
13:54:34 Concurrency: 8 threads (target='ci')
13:54:36 1 of 2 START sql view model pr_2.stg_customers ................................. [RUN]
13:54:37 1 of 2 OK created sql view model pr_2.stg_customers ............................ [SUCCESS 1 in 1.04s]
13:54:37 2 of 2 START sql view model pr_2.customers ..................................... [RUN]
13:54:38 2 of 2 OK created sql view model pr_2.customers ................................ [SUCCESS 1 in 1.20s]
DAG:

さらに、customersのコンパイル後の SQLを確認すると、選択されたstg_customersは PR 専用スキーマを、選択されなかったstg_ordersは--deferにより本番のリレーションを直接参照するという状態が、同じクエリの中に混在していることが確認できました。

マージ時の動作確認(CD・クリーンアップ)
PR をマージすると、main ブランチへの push による CD(フルビルド)と、PR クローズに伴うクリーンアップ(PR 専用スキーマ・PR 用 project object の削除)が並行して動作します。


CD は Slim CI ではなく、想定どおりフルビルド:

さいごに
dbt Projects on Snowflake の Slim CI / Defer to Production を試してみました。本番アーティファクトをネイティブに扱え、GitHub Actions との連携もしやすいと思いました。
こちらの内容がどなたかの参考になれば幸いです。







