I tried CI/CD for dbt Projects on Snowflake with GitHub Actions + OIDC authentication

I tried CI/CD for dbt Projects on Snowflake with GitHub Actions + OIDC authentication

I will show you how to build a secure CI/CD pipeline for dbt projects on Snowflake without long-lived secrets by combining GitHub Actions OIDC tokens with Snowflake Workload Identity Federation.
2026.07.23

This page has been translated by machine translation. View original

This is Kawabata.

With dbt Projects on Snowflake, you can now run dbt projects directly on Snowflake, and there are many cases where you'd want to incorporate CI/CD into your development workflow. A key concern in such cases is how to authenticate from GitHub Actions to Snowflake. The approach of storing passwords or PATs (Programmatic Access Tokens) in GitHub Secrets comes with the operational burden of managing and rotating long-lived secrets.

In this article, we'll use GitHub Actions OIDC tokens and Snowflake's Workload Identity Federation to build a CI that runs dbt build in the dev environment on PRs and a CD that deploys to production via snow dbt deploy on merges to main — all without long-lived secrets.

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

[Update]
I was selected as a finalist in the "RISING COMMUNITY LEADER OF THE YEAR" category (APJ region) of the Snowflake Community Awards.
Please see the link below for details.

https://dev.classmethod.jp/articles/snowflake-community-awards-finalist-activities-review/

Overview of the Architecture

CI/CD for dbt Projects on Snowflake

With dbt Projects on Snowflake, dbt projects are deployed as schema-level DBT PROJECT objects and executed on Snowflake. The official documentation describes the following CI/CD configuration:

  • CI: For each Pull Request, deploy a DBT PROJECT object for testing and run dbt build against the dev environment
  • CD: After merging to the main branch, update the production DBT PROJECT object using snow dbt deploy

snow dbt deploy performs versioned deployments, stacking a new version with each deploy. Since execution history is also retained, you can track when each version was executed.

Authentication via OIDC (Workload Identity Federation)

Using Snowflake's Workload Identity Federation, you can authenticate to Snowflake using short-lived OIDC tokens issued by GitHub Actions. Only the account identifier needs to be stored in GitHub Secrets — no passwords, PATs, or key pairs are required.

The mechanism works by registering the ISSUER (token issuer) and SUBJECT (token's sub claim) as WORKLOAD_IDENTITY for a TYPE = SERVICE user, allowing Snowflake to verify GitHub's OIDC tokens.

https://docs.snowflake.com/en/user-guide/workload-identity-federation

sub claim and Service User Design

The sub claim in GitHub Actions OIDC tokens changes format depending on the triggering event.

Event sub claim format
pull_request repo:<org>/<repo>:pull_request
push (branch) repo:<org>/<repo>:ref:refs/heads/<branch>
Job with environment specified repo:<org>/<repo>:environment:<name>

Snowflake service users require an exact match between the SUBJECT and the sub claim. The official tutorial specifies environment: prod for both CI/CD jobs to unify the sub claim and uses a single service user.

Since we're going with a simple configuration without GitHub Environments, we'll create two service users: one for CI (pull_request) and one for CD (push to main). Separating the authentication principals also makes it easier to implement privilege separation, such as giving the CI user access only to dev and the CD user access only to prod.

Note that for simplicity in this article, both users are granted a common role (dbt_cicd_role), which means the CI user can also access prod in this configuration. If you want to implement privilege separation, split the roles into dbt_ci_role (dev and raw only) / dbt_cd_role (prod and raw only), grant each to the respective service user, and set the role in the corresponding target in profiles.yml.

Prerequisites

  • Snowflake account (ACCOUNTADMIN-level privileges required for creating service users)
  • A repository with GitHub Actions enabled
  • Snowflake CLI 3.11.0 or later (required for OIDC authentication; installed via snowflakedb/snowflake-actions@v3)
  • dbt project: This article uses jaffle-shop (a sample project by dbt Labs)
  • dbt version: dbt Core 1.11.11 specified
  • This article assumes the default branch is main. If you use a different branch name such as master, update the branches in the workflows and the SUBJECT of the CD service user accordingly.

Prerequisites Setup

Creating the Snowflake Environment

Create the database, dev/prod schemas, warehouse, and a role for CI/CD. The official tutorial uses ACCOUNTADMIN directly, but here we grant minimal privileges to a dedicated role.

Creating the Snowflake Environment
USE ROLE ACCOUNTADMIN;

CREATE DATABASE IF NOT EXISTS jaffle_shop_db;
CREATE SCHEMA IF NOT EXISTS jaffle_shop_db.dev;
CREATE SCHEMA IF NOT EXISTS jaffle_shop_db.prod;
CREATE SCHEMA IF NOT EXISTS jaffle_shop_db.raw;   -- For source data (seeds)

CREATE WAREHOUSE IF NOT EXISTS jaffle_shop_wh
  WAREHOUSE_SIZE = XSMALL
  AUTO_SUSPEND = 60
  AUTO_RESUME = TRUE
  INITIALLY_SUSPENDED = TRUE;

CREATE ROLE IF NOT EXISTS dbt_cicd_role;
GRANT USAGE ON DATABASE jaffle_shop_db TO ROLE dbt_cicd_role;
GRANT USAGE ON WAREHOUSE jaffle_shop_wh TO ROLE dbt_cicd_role;
GRANT ALL ON SCHEMA jaffle_shop_db.dev  TO ROLE dbt_cicd_role;
GRANT ALL ON SCHEMA jaffle_shop_db.prod TO ROLE dbt_cicd_role;
GRANT ALL ON SCHEMA jaffle_shop_db.raw  TO ROLE dbt_cicd_role;

GRANT CREATE DBT PROJECT ON SCHEMA jaffle_shop_db.dev  TO ROLE dbt_cicd_role;
GRANT CREATE DBT PROJECT ON SCHEMA jaffle_shop_db.prod TO ROLE dbt_cicd_role;

GRANT CREATE SCHEMA ON DATABASE jaffle_shop_db TO ROLE dbt_cicd_role;

CREATE DBT PROJECT is the privilege required for snow dbt deploy. Also, since dbt checks for the existence of the target schema and creates it (CREATE IF NOT EXISTS) at runtime, even if the schema is pre-created, dbt build will fail with a permissions error if the CREATE SCHEMA privilege on the database is missing (this is an error I actually encountered during testing). Details on access control can be found in the following documentation.

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

Adjusting the dbt Project (jaffle-shop)

In dbt Projects on Snowflake, a profiles.yml at the project root is required in addition to dbt_project.yml. Define two targets: dev and prod.

profiles.yml
jaffle_shop:
  target: dev
  outputs:
    dev:
      type: snowflake
      account: '_'   # Placeholder. Not used since execution happens inside Snowflake
      user: '_'      # Same as above
      role: DBT_CICD_ROLE
      database: JAFFLE_SHOP_DB
      schema: DEV
      warehouse: JAFFLE_SHOP_WH
      threads: 8
    prod:
      type: snowflake
      account: '_'
      user: '_'
      role: DBT_CICD_ROLE
      database: JAFFLE_SHOP_DB
      schema: PROD
      warehouse: JAFFLE_SHOP_WH
      threads: 8

Note: Since execution happens inside Snowflake, placeholder strings are fine for account and user. No password is required either. However, type / database / schema / role / warehouse are all required. Furthermore, snow dbt deploy validates the contents of profiles.yml against the actual environment before deploying — if role or warehouse does not exist or is not accessible by the connecting user, you'll get an error like "Role 'XXX' does not exist or is not accessible." (all targets are validated). Make sure these match the actual object names in your environment exactly.

Creating OIDC Service Users

Create two users: one for CI and one for CD. Configure the SUBJECT to exactly match the sub claim for each respective event.

Creating OIDC Service Users
USE ROLE ACCOUNTADMIN;

-- For CI (pull_request event)
CREATE USER IF NOT EXISTS svc_gha_dbt_ci
  TYPE = SERVICE
  WORKLOAD_IDENTITY = (
    TYPE = OIDC
    ISSUER = 'https://token.actions.githubusercontent.com'
    SUBJECT = 'repo:<your-org>/<your-dbt-repo>:pull_request'
  )
  DEFAULT_ROLE = dbt_cicd_role
  DEFAULT_WAREHOUSE = jaffle_shop_wh
  COMMENT = 'GitHub Actions CI (pull_request) service user';

-- For CD (push to main event)
CREATE USER IF NOT EXISTS svc_gha_dbt_cd
  TYPE = SERVICE
  WORKLOAD_IDENTITY = (
    TYPE = OIDC
    ISSUER = 'https://token.actions.githubusercontent.com'
    SUBJECT = 'repo:<your-org>/<your-dbt-repo>:ref:refs/heads/main'
  )
  DEFAULT_ROLE = dbt_cicd_role
  DEFAULT_WAREHOUSE = jaffle_shop_wh
  COMMENT = 'GitHub Actions CD (push to main) service user';

GRANT ROLE dbt_cicd_role TO USER svc_gha_dbt_ci;
GRANT ROLE dbt_cicd_role TO USER svc_gha_dbt_cd;

Note: The SUBJECT must exactly match the sub claim. Replace <your-org>/<your-dbt-repo> with your repository's actual path. The match must be exact, including case.

I verified that WORKLOAD_IDENTITY (TYPE=OIDC) is registered correctly with the ISSUER/SUBJECT.

2026-07-23_14h27_53

GitHub Repository Settings

Open Settings, select Secrets and variables → Actions, click New repository secret, and register just one secret.

  • SNOWFLAKE_ACCOUNT: Account identifier (in <orgname>-<account_name> format)

That's the only secret you need to register. The fact that no passwords or PATs are required is a major advantage of OIDC authentication.

2026-07-23_14h51_45

Creating the CI Workflow

Create .github/workflows/ci.yml. Triggered by a PR, it deploys a test DBT PROJECT object to the dev schema and runs dbt build with the dev target.

name: CI - dbt build on PR
run-name: PR by ${{ github.actor }}
on:
  pull_request:
    types: [opened, synchronize, reopened, ready_for_review]
    branches: [main]

concurrency:
  group: ci-dev
  cancel-in-progress: false

permissions:
  contents: read
  id-token: write   # Required for issuing OIDC tokens

jobs:
  dbt-ci:
    runs-on: ubuntu-latest
    env:
      SNOWFLAKE_CLI_FEATURES_ENABLE_DBT: true
      SNOWFLAKE_ACCOUNT: ${{ secrets.SNOWFLAKE_ACCOUNT }}
      SNOWFLAKE_ROLE: DBT_CICD_ROLE
      SNOWFLAKE_WAREHOUSE: JAFFLE_SHOP_WH
      SNOWFLAKE_DATABASE: JAFFLE_SHOP_DB
      SNOWFLAKE_SCHEMA: DEV
    steps:
      - uses: actions/checkout@v4

      - name: Install dbt and resolve dependencies
        run: |
          pip install 'dbt-core==1.11.*'
          dbt deps

      - name: Install Snowflake CLI
        uses: snowflakedb/snowflake-actions@v3
        with:
          use-oidc: true

      - name: Test connection
        run: snow connection test -x

      - name: Deploy tester dbt project object
        run: snow dbt deploy ci_jaffle_shop --source . --dbt-version 1.11.11 -x

      - name: Build and test on dev
        run: snow dbt execute -x ci_jaffle_shop build --target dev

Key points:

  • id-token: write in permissions is required for issuing OIDC tokens
  • Passing use-oidc: true to snowflakedb/snowflake-actions@v3 automatically sets the authentication environment variables (SNOWFLAKE_AUTHENTICATOR=WORKLOAD_IDENTITY, etc.)
  • The environment variable SNOWFLAKE_CLI_FEATURES_ENABLE_DBT: true is required to use snow dbt commands
  • -x is the temporary connection option, which connects based on environment variables without a config file
  • Explicitly specifying SNOWFLAKE_ROLE / SNOWFLAKE_WAREHOUSE ensures the connection context is set without relying on the service user's default role settings (if unspecified and the default role doesn't take effect, you'll get a "Could not use database" error)
  • concurrency serializes CI runs. All PRs share the same ci_jaffle_shop object, and concurrent EXECUTE DBT PROJECT on the same DBT PROJECT object is not supported.

Creating the CD Workflow

Create .github/workflows/deploy.yml. Triggered by a push to main, it deploys the production DBT PROJECT object to the prod schema, then runs build with the prod target.

name: CD - deploy dbt project on merge
run-name: Deploy by ${{ github.actor }}
on:
  push:
    branches: [main]

concurrency:
  group: cd-prod
  cancel-in-progress: false

permissions:
  contents: read
  id-token: write

jobs:
  dbt-deploy:
    runs-on: ubuntu-latest
    env:
      SNOWFLAKE_CLI_FEATURES_ENABLE_DBT: true
      SNOWFLAKE_ACCOUNT: ${{ secrets.SNOWFLAKE_ACCOUNT }}
      SNOWFLAKE_ROLE: DBT_CICD_ROLE
      SNOWFLAKE_WAREHOUSE: JAFFLE_SHOP_WH
      SNOWFLAKE_DATABASE: JAFFLE_SHOP_DB
      SNOWFLAKE_SCHEMA: PROD
    steps:
      - uses: actions/checkout@v4

      - name: Install dbt and resolve dependencies
        run: |
          pip install 'dbt-core==1.11.*'
          dbt deps

      - name: Install Snowflake CLI
        uses: snowflakedb/snowflake-actions@v3
        with:
          use-oidc: true

      - name: Test connection
        run: snow connection test -x

      - name: Deploy production dbt project object
        run: snow dbt deploy jaffle_shop --source . --default-target prod --dbt-version 1.11.11 -x

      - name: Build models on prod
        run: snow dbt execute -x jaffle_shop build --target prod

      - name: List dbt project objects
        run: snow dbt list -x

In CD, --default-target prod is specified to set the default target of the DBT PROJECT object to prod. I initially tested a deploy-only configuration, but deploy alone only registers a new version of the DBT PROJECT object without updating the tables in the prod schema, so I added a build step to reflect data changes on merge. For production use with many models, you may also consider separating the build into a scheduled Snowflake Task execution, with CD handling only the deploy.

concurrency is also used to serialize CD runs. Since concurrent EXECUTE DBT PROJECT on the same DBT PROJECT object is not supported, cancel-in-progress: false causes subsequent runs to wait rather than be cancelled in case of back-to-back merges.

Testing It Out

Verifying OIDC Authentication

When you push the workflow to main, the CD pipeline starts. First, confirm that OIDC authentication succeeds via the snow connection test -x output.

2026-07-23_18h32_39

CI: Tests Run on PR Creation

When you make a minor change to a model on a feature branch and open a PR, the CI workflow starts.
In this case, I changed some column names in customers.sql.

2026-07-23_18h27_21

Create the PR.
2026-07-23_18h30_29

In CI, the test object ci_jaffle_shop is deployed to the dev schema and dbt build is run with the dev target.

2026-07-23_18h36_19

At this point, you can confirm that the changes are reflected in the dev schema while the prod schema remains unchanged.

2026-07-23_18h34_43

CD: Production Object Updated on Merge

When the PR is merged, the CD workflow starts, the production object is deployed as a new version, and the subsequent build step updates the tables and views in the prod schema as well.

2026-07-23_18h36_51

2026-07-23_18h41_56

I confirmed that the changes were also reflected in the prod schema.

2026-07-23_18h43_54

Confirming CI Blocks on Test Failure

When you open a PR with a change that intentionally causes a test failure (such as a not_null violation), the CI fails. Combined with branch protection rules, this allows you to block merging of changes that don't pass tests.

For this test, I added the following to the end of customers.sql:

-- test: Intentionally duplicate customer_id to cause the unique test to fail
select * from joined

union all

(select * from joined limit 1)

2026-07-23_19h01_17

Limitations and Notes

  • snow dbt deploy --force behaves as CREATE OR REPLACE DBT PROJECT, which destroys existing versions and execution history. It is not recommended for use in a normal CD pipeline.
  • DBT PROJECT objects cannot be executed in Serverless Tasks; a user-managed warehouse is required.
  • Concurrent EXECUTE DBT PROJECT on the same DBT PROJECT object is not supported.
  • dbt Cloud projects are not supported (dbt Core / dbt Fusion only).
  • If your account uses network policies, you'll need to add the Snowflake-managed network rule SNOWFLAKE.NETWORK_SECURITY.GITHUBACTIONS_GLOBAL to the allowlist for connections from GitHub-hosted runners.
  • This article uses two service users for simplicity, but for production use, it is recommended to configure a GitHub Environment (environment: prod) with required reviewers and set the SUBJECT to repo:<org>/<repo>:environment:prod. This creates a safer CD pipeline where deploy jobs don't run until they pass the approval gate.
  • The sub claim specification for GitHub OIDC tokens is subject to change and extension, so always check the official GitHub documentation when implementing.

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

Closing Thoughts

Using GitHub Actions OIDC tokens and Workload Identity Federation, we were able to build a CI/CD pipeline for dbt Projects on Snowflake with only the account identifier stored in GitHub Secrets.

I hope this article is helpful to someone!


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の詳細を見る

Share this article