[新機能]dbt Projects on Snowflake で Slim CI / Defer to Production が一般提供になりました

[新機能]dbt Projects on Snowflake で Slim CI / Defer to Production が一般提供になりました

2026年9月のアップデートで、dbt Projects on Snowflake の Slim CI と Defer to Production が一般提供となりました。本番アーティファクトをネイティブに扱える新機能を試してみた内容をまとめます。
2026.09.12

はじめに

2026年9月のアップデートで、dbt Projects on Snowflake の Slim CI と Defer to Production が一般提供となりました。

https://docs.snowflake.com/en/release-notes/2026/other/2026-09-10-dbt-artifacts-slim-ci-defer-to-production-ga

こちらの機能を試してみた内容を本記事でまとめます。

アップデートの概要

本機能については以下に記載があります。

https://docs.snowflake.com/en/user-guide/data-engineering/dbt-projects-on-snowflake-slim-ci-defer-to-prod

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/

https://docs.getdbt.com/reference/node-selection/state-selection

https://docs.getdbt.com/reference/node-selection/defer

  • statemanifest.json
    • プロジェクトの静的構造(ノード定義・SQL・依存関係)のスナップショットです。--stateオプションで読み込み、現在のプロジェクトとの比較基準にします。実体は、過去のdbt build実行で生成されたtarget/manifest.jsonを指します
  • 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 を使うにはこのバンドルが有効になっている必要があります。

https://docs.snowflake.com/en/release-notes/bcr-bundles/2026_06/bcr-2362

単一の可変バージョンになったことで、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 を複製することなく並行実行できるようにもなりました

並列実行については、以下の記事で紹介されていますので、こちらをご参照ください。

https://dev.classmethod.jp/articles/dbt-projects-on-snowflake-run-one-project-concurrently/

また、公式によるチュートリアルも公開されています。

https://docs.snowflake.com/en/user-guide/tutorials/dbt-projects-on-snowflake-advanced-ci-cd-tutorial#introduction

試してみる

前提条件

以下の環境を使用しています。

  • 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 を作成します。

2026-09-12_00h07_29

env.yml を追加

続けて、env.ymlを追加します。env.ymlを使うと、環境ごとの変数(データベース名・スキーマ名)を SQL 式で切り替えられます。開発者ごとのスキーマ切り替えにはCURRENT_USER()を使いました。

env.yml
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

https://docs.snowflake.com/en/user-guide/data-engineering/dbt-projects-on-snowflake-environment-variables

https://dev.classmethod.jp/articles/dbt-projects-on-snowflake-with-sql-environment-variables/

profiles.yml を追加

続けて、profiles.ymlを追加します。profiles.yml側はenv_var()で参照します。

profiles.yml
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
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
stg_customers.sql
select
    id as customer_id,
    first_name,
    last_name,
    first_name || ' ' || last_name as full_name

from {{ source('jaffle_shop', 'customers') }}
stg_orders.sql
select
    id as order_id,
    user_id as customer_id,
    order_date,
    status

from {{ source('jaffle_shop', 'orders') }}
customers.sql
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を実行し、追加したモデルが指定のスキーマ(ここでは開発環境のユーザースキーマ)に作成されることを確認します。

2026-09-12_00h19_29

CI/CD 用の準備(サービスユーザー・CD パイプライン)

このあとの手順でSYSTEM$DBT_GET_LAST_SUCCESSFUL_RUN_TARGETで「本番の直近成功実行」を参照するには、あらかじめ本番(prd)側で一度は成功実行が記録されている必要があります。そこで先に、main ブランチへの push 時に本番へデプロイ・ビルドする CD パイプラインを組んでおきます。PR 時に動く CI 用のサービスユーザー・ワークフローは、後ほど作成します。

まず、prd_dbt_roleslim_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 による連携は以下の記事が参考になると思います。

https://dev.classmethod.jp/articles/try-dbt-projects-on-snowflake-github-actions-oidc-cicd/

GitHub 側は Repository Secret に接続先アカウントのみを登録します。OIDC 認証のみを使うため、パスワードや PAT は不要です。

Name Value
SNOWFLAKE_ACCOUNT <組織名>-<アカウント名>

続けて、main への push をトリガーに本番の dbt project object をデプロイ・ビルドするdeploy.ymlを追加します。

.github/workflows/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の出力先選択)と--envenv.ymlの環境変数選択)は別軸のオプションでdeploy時は--default-target/--default-envexecute時は--target/--envという対応関係になります
  • snow dbt deployはデフォルトで--auto-compileが有効なため、--default-target prdだけを指定してもコンパイル自体はenv.ymldefault_environment(今回はdev)で行われてしまいます。そのため--default-env prdも明示して揃える必要があります
  • 同様にsnow dbt executeでも、--target prdだけではenv.ymldefault_environmentが使われてしまうため、--env prdを明示しています
  • このワークフローでは--select--state/--deferを使用していません。そのため、Slim CI のような差分ビルドではなく、prdの全モデルを毎回フルビルドする通常の CD です

https://docs.snowflake.com/en/user-guide/data-engineering/dbt-projects-on-snowflake-environment-variables#call-stored-procedures-from-envyml

このワークフローを main にマージすることで、本番の dbt project object に最初の成功実行が記録され、Slim CI 実行時に defer 先として参照できるようになります。実際に main へマージし、GitHub Actions 上でこの CD が成功することを確認します。

2026-09-12_00h43_39

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 ごとに開発用データベース配下へ専用スキーマを作成し、そこへ変更モデルと下流だけをビルドします。

.github/workflows/ci.yml
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.ymldefault_environment(今回はdev)でコンパイルを試みます。--default-target ciを指定しても、env.ymlci環境がそもそも存在しないため--default-env ciで揃えることもできません。そこで--no-auto-compileでデプロイ時のコンパイル自体を丸ごとスキップし、project object の登録のみを行っています。実際のコンパイル・変数解決は次のsnow dbt execute側(--env-vars)で行います
  • 実際にビルドするsnow dbt executeでは、--envではなく--env-varsDBT_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 です。既存のモデルには依存していない、新規モデルだけのシンプルなケースです。

stg_payments.sql
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はいずれも選択されないことを確認できました。

2026-09-12_01h20_02

2026-09-12_01h28_05

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

2026-09-12_01h20_50

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

2026-09-12_01h22_24

PR #2: stg_customersの変更(既存モデル変更+下流伝播)

続けて、既存モデルを変更した場合に「変更モデル+その下流だけがビルドされる」ことを確認します。stg_customers.sqlfull_name列を追加しました。

stg_customers.sql
select
    id as customer_id,
    first_name,
    last_name,
    first_name || ' ' || last_name as full_name

from {{ source('jaffle_shop', 'customers') }}

customersモデルはstg_customersstg_ordersの両方をref()しているため、stg_customersを変更するとcustomersは下流としてビルド対象に入り、無関係なstg_orders/stg_paymentsは選択されないはずです。実際のビルドログでも、想定どおりstg_customerscustomersの 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:

2026-09-12_01h29_29

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

2026-09-12_01h29_46

マージ時の動作確認(CD・クリーンアップ)

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

2026-09-12_01h32_28

2026-09-12_01h33_15

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

2026-09-12_01h36_22

さいごに

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


Snowflake World Tour Tokyo 2026に参加しませんか?

Snowflakeの国内最大級イベント「Snowflake World Tour Tokyo」が2026年9月10日(木)・11日(金)にグランドプリンスホテル新高輪にて開催されます。
最新のAI・データ活用事例やライブデモを体感できる無料イベントです。

Snowflake World Tour Tokyoイベントに参加する


Snowflake Community Awards ファイナリストに選出されました

DevelopersIO で Snowflake 記事を執筆している かわばた が、Snowflake Community Awards「RISING COMMUNITY LEADER OF THE YEAR」部門・APJ枠のファイナリストに選ばれました。
最終選考の30%はコミュニティ投票です。記事がお役に立っていたようでしたら、9月15日(火)までにぜひ一票お願いします。フォームの「(4 of 6) RISING COMMUNITY LEADER OF THE YEAR」で Tomohiro Kawabata | Classmethod, Japan を選択、2分ほどで完了します。

投票フォームを開く


Snowflakeの導入支援はクラスメソッドに!

クラスメソッドでは Snowflake の導入を支援しております。
製品の詳細や支援の内容についてお気軽にお問い合わせください。

Snowflakeの詳細を見る

この記事をシェアする

関連記事