SELECTSELECT

SELECT

Snowflake DCMプロジェクト:SQLだけで実現するInfrastructure as Code

このページはEnglish、Deutsch、Español、Français、Italiano、Portuguêsでもご覧いただけます。

By SELECTNov 12, 2022 min read

TLDR

  • DCMプロジェクトは、オブジェクトをコードとして管理するためのSnowflakeネイティブな宣言型の仕組みです。CREATE <object>の代わりにDEFINE <object>と書けば(例:DEFINE DATABASE)、何を変更すべきかはSnowflakeが判断してくれます。
  • 基本の流れは、定義を編集してプラン、そしてデプロイ。プランは読み取り専用の差分で、デプロイがそれを適用します。
  • DCMは、定義をやめたものを削除します。 1行消せば、データベースが1つ消えます。プランは必ず読みましょう。
  • manifest.ymlとJinjaを使えば、1セットのファイルからDEV、QA、PRODを構築できます。
  • DCMはテーブルやビューも管理_できます_が、すでにdbtを運用しているなら任せるべきではないと考えています。DCMにはプラットフォーム(ロール、データベース、ウェアハウス、権限付与)を、dbtにはデータベースの中身を任せましょう。
  • ロールとデータベースの設計は、何百ものdbtプロジェクトが踏襲してきたdbt LabsのSnowflakeセットアップガイドに基づいています。リポジトリではいくつかの改善を加え、コードに落とし込んでいます。
  • 本記事のベースとなっているGitHubリポジトリはこちら。ACCOUNTADMINに一切触れないGitHub ActionsによるCI/CD付きです。

ここに至るまでの経緯

Snowflakeをしばらく使っている方なら、アカウントのインフラをおそらく次の3つの方法のいずれかで管理してきたはずです。

  1. クリック運用(Click-ops)。 ACCOUNTADMINを持つ誰かが、ある火曜日にSnowsightでオブジェクトを作成。理由は誰も覚えていません。3年後には、怖くて誰も削除できなくなっています。
  2. マイグレーションスクリプト: V1__create_roles.sql、V2__grant_things.sql、V47__fix_the_grant_from_V2.sql。schemachangeのようなツールが順番に実行してくれます。動きはしますが、アカウントの「現在の状態」は47ファイルの総和であり、それを読み解くのは至難の業です。
  3. Terraform。 宣言型で強力です。ただしHCL、バージョン追従が必要なプロバイダー、そして保存・ロック・時には外科手術まで必要なステートファイルも付いてきます。多くのデータチームにとって、ウェアハウスを1つ作るためだけにまったく新しい専門分野を学ぶことになります。

DCMプロジェクトは、それに対するSnowflakeの答えです。Terraformの宣言型モデルを手に入れつつ、言語はSQL、ステートはSnowflakeの中に保持されます。

設計図:誰もがコピーしたgrant文

DCMの話に入る前に、先人の功績に触れておきましょう。dbt向けにSnowflakeをセットアップしたことがあるなら、dbt DiscourseにあるClaire CarrollのSetting up Snowflake — the exact grant statements we runを読んだことがあるのではないでしょうか。何百ものdbtプロジェクトが、この構成に従ってセットアップされてきました。私自身も数え切れないほど従ってきました。

この設計はシンプルで、だからこそ機能します。

  • 取り込んだデータを受けるrawデータベースと、モデリング済みデータ用のanalyticsデータベース。
  • rawに書き込むloaderロール、rawを読み取ってanalyticsを構築するtransformerロール、そしてanalyticsのみを読み取るreporterロール。
  • Future grantにより、新しいスキーマやテーブルは誰も手を動かすことなく読み取り可能に。

問題は設計そのものではありません。問題は、これがワークシートに一度貼り付けるだけの文のリストだということです。半年後には、アカウントがまだこの構成と一致しているのか誰にも分からず、投稿の最後にある「dev環境」への拡張は読者への宿題として残されたままです。

そこでスターターリポジトリでは、Claireの構成の形を保ちつつ、いくつかの点を変更しています。

Future grantをinherited grantに置き換える理由

あの表のgrantの行こそ最も重要な変更点なので、詳しく見てみましょう。

Future grantは長い間、正しい選択でした!Claireのセットアップを手間いらずにしているのはfuture grantです。しかし3つの問題があり、数年Snowflakeを運用した人なら少なくとも1つには遭遇しているはずです。

  1. 将来しかカバーしない。 future grantはすでに存在するテーブルには何の効果もないため、すべてのセットアップでgrant select on all tables文とセットになります。つまり権限ごとに2つの文、覚えておくべきことも2つです。
  2. スキーマレベルのfuture grantはデータベースレベルのものを上書きする。 誰かが1つのスキーマにfuture grantを追加すると、Snowflakeはそのスキーマに対するデータベースレベルのfuture grantを無視します。reporterロールはそのスキーマでいつの間にか新しいテーブルを参照できなくなり、エラーは一切出ません。
  3. ルールではなく、一度きりのコピーである。 future grantはオブジェクト作成時に各オブジェクトへ適用されます。後からfuture grantを取り消したり変更したりしても、すでに適用済みのオブジェクトは古い権限を保持し続けます。目に見えるgrantが、実際のアクセス権を表さなくなるのです。

Inherited grantは、コンテナ(アカウント、データベース、スキーマ)に対する1つの文で、その中の既存および将来のすべての該当オブジェクトに適用されます。

GRANT INHERITED SELECT ON ALL TABLES IN DATABASE {{ env }}_ANALYTICS
TO ROLE {{ env }}_ANALYST;

これだけで、future grant、on all grant、そしてそれぞれのスキーマ単位版をすべて置き換えられます。誰かがテーブルを読める理由を知りたいときは、SHOW GRANTSのIS_INHERITED列とINHERITED_FROM列が、該当するgrantを指し示してくれます。

DCMとの相性も自然です。宣言型ツールが求めるのは、ルールを1行で述べることです。「アナリストはANALYTICS内のすべてを読める」が、6行ではなく1行で済みます。

知っておくべきトレードオフもいくつかあります。

  • 例外は作れません。 特定のテーブルだけ権限を取り消して除外することはできません。Snowflakeのドキュメントでは、revokeは_成功したように見える_ものの、ロールはinherited grant経由でアクセスを保持し続けると警告されています。異なるアクセス制御が必要なテーブルは、別のスキーマやデータベースに置くべきです。
  • オブジェクトをコンテナに移動またはクローンすると、GRANT文を一切書かずに閲覧できるユーザーが変わります。 それこそが狙いなのですが、クローンの扱いには注意しましょう。
  • OWNERSHIPは継承できません。 所有権は通常のgrantのままです。
  • アカウントフラグが必要です。 FEATURE_RBAC_INHERITED_GRANTSはACCOUNTADMINのみが設定できます。詳細は後述のブートストラップで説明します。

SnowflakeのDCMプロジェクトとは?

次の2つから成ります。

  1. ファイルのフォルダ。 manifest.ymlと、DEFINE文が詰まった.sqlファイル群です。
  2. Snowflake内のDCMプロジェクトオブジェクト。 他のオブジェクトと同様にスキーマ内に存在し、デプロイ履歴を保持します。

私のスターターリポジトリのレイアウトはこちらです。

platform/
dcm/
manifest.yml
sources/definitions/
roles.sql
databases.sql
warehouses.sql
grants.sql
migrations/
000_bootstrap.sql
001_ci_deployer.sql

オブジェクトタイプごとに1ファイルというのは私の慣習であって、DCMのルールではありません。Snowflakeはsources/以下をすべて読み込むので、自分が分かりやすい形で整理して構いません。

CREATEではなくDEFINE

発想の転換のすべてはここにあります。マイグレーションスクリプトでは_手順_を書きます。

CREATE ROLE IF NOT EXISTS DEV_LOADER;
ALTER ROLE DEV_LOADER SET COMMENT = 'Owns DEV_RAW';

DCMでは_最終状態_を書きます。

DEFINE ROLE DEV_LOADER
COMMENT = 'Owns DEV_RAW. Used by ingestion tools.';

ロールが存在しなければDCMが作成します。コメントが異なる状態で存在していればDCMが変更します。すでに一致していればDCMは何もしません。IF NOT EXISTSを書くことは二度となく、先週書いたものを直すためのALTERを書くこともありません。

Grantも同じ仕組みです。grantをDEFINEするのではなく、ただ記述するだけです。

DEFINE DATABASE DEV_RAW
COMMENT = 'Landing zone. Schemas and tables are created by ingestion tools.';
GRANT OWNERSHIP ON DATABASE DEV_RAW TO ROLE DEV_LOADER;

プラン、そしてデプロイ

すべての変更は2つのステップを経ます。Snowflake CLIを使う場合は次のとおりです。

snow dcm plan --from platform/dcm --target DEV \
--connection my_account --role PLATFORM_DEPLOYER

プランはファイルとアカウントの実際の状態を比較し、何が作成・変更・削除されるのかを正確に示します。何も変更はしません。

[screenshot: plan output for adding a database]

問題なければデプロイします。

snow dcm deploy --from platform/dcm --target DEV --alias add_marketing_db \
--connection my_account --role PLATFORM_DEPLOYER

--aliasは省略可能ですが、ぜひ使ってください。aliasなしだと、デプロイ履歴は誰にも読めない自動生成名のリストになります。aliasがあれば、snow dcm list-deploymentsは変更履歴のように読めます:add_marketing_db、analyst_read_on_analytics、drop_legacy_loader。

これらはすべてSQLでも実行でき(EXECUTE DCM PROJECT ... PLAN)、Snowsight WorkspacesにはUIも用意されています。私はターミナル派なので、本記事ではCLIを使って進めます。

DCMは定義をやめたものを削除する

ここが要注意ポイントです。

以前デプロイしたDEFINEを削除すると、次回のデプロイでそのオブジェクトが削除されます。

これは宣言型ツールとして正しい振る舞いです。ファイルこそがアカウント_そのもの_なのですから。ただし、2つの習慣は絶対に欠かせません。

  1. デプロイ前に必ずプランを実行する。 プランに予期しないDROPがあるなら、それは定義が消えたということであって、DCMが気を利かせているわけではありません。
  2. PRでプランを読む。 その自動化の方法は後述します。

もう1つ。Snowflakeのドキュメントには、デプロイが失敗すると部分的に実行された状態が残る可能性があると明記されています。1つの大きなトランザクションではないのです。手作業でパッチを当てるのではなく、定義を修正してプラン→デプロイをやり直してください。

1セットのファイルからDEV、QA、PRODを

roles.sqlを3つコピーしたい人はいません。manifest.ymlでターゲットを定義し、各ターゲットが独自のプロジェクトオブジェクトを指し、独自のテンプレート変数を渡します。

manifest_version: 2
type: DCM_PROJECT
default_target: DEV
targets:
DEV:
account_identifier: ABCDEFG-XY12345
project_name: PLATFORM.DCM.DEV_PLATFORM_DCM
project_owner: PLATFORM_DEPLOYER
templating_config: DEV
# QA and PROD look the same
templating:
configurations:
DEV:

コードを展開

そして、すべての定義で{{ env }}をプレフィックスとして使います。

DEFINE DATABASE {{ env }}_RAW
COMMENT = 'Landing zone. Schemas and tables are created by ingestion tools.';
DEFINE DATABASE {{ env }}_ANALYTICS
COMMENT = 'Modelled data. Schemas and tables are created by transformation tools.';
GRANT OWNERSHIP ON DATABASE {{ env }}_RAW TO ROLE {{ env }}_LOADER;
GRANT OWNERSHIP ON DATABASE {{ env }}_ANALYTICS TO ROLE {{ env }}_TRANSFORMER;

--target DEVでデプロイすればDEV_RAWが、--target PRODならPROD_RAWができます。ファイルは同じです。

厄介なところ:アカウント全体のオブジェクト

特定の環境に属さないオブジェクトもあります。私のテンプレートでは、3つの環境すべてが1つのアカウント、1つのウェアハウス、そしてDEV_LOADER・QA_LOADER・PROD_LOADERを継承するLOADERのような「マスター」ロール群を共有しています。

条件なしでCOMPUTE_XSを定義すると、3つのターゲットすべてがそれを自分のものと主張し、各ターゲットのデプロイが奪い合いを起こします。そこでアカウント全体のオブジェクトは、Jinjaのifとループを使って、1つのターゲットだけで定義します。

{% if env == 'PROD' %}
{% set envs = ['DEV', 'QA', 'PROD'] %}
DEFINE WAREHOUSE COMPUTE_XS
WAREHOUSE_TYPE = 'ADAPTIVE'
MAX_QUERY_PERFORMANCE_LEVEL = XSMALL
QUERY_THROUGHPUT_MULTIPLIER = 2
COMMENT = 'Sole compute warehouse for loads and transforms.';
{% for e in envs %}
GRANT USAGE ON WAREHOUSE COMPUTE_XS TO ROLE {{ e }}_LOADER;
GRANT USAGE ON WAREHOUSE COMPUTE_XS TO ROLE {{ e }}_TRANSFORMER;
GRANT USAGE ON WAREHOUSE COMPUTE_XS TO ROLE {{ e }}_ANALYST;
{% endfor %}
{% endif %}

落とし穴があります。このPRODブロックはDEV_LOADERとQA_LOADERにgrantするため、DEVとQAはPRODより先にデプロイする必要があります。デプロイの順序が、プロセスとCIの一部になるのです。

環境ごとに別アカウントにすればこの問題は消えますが、今度は各アカウントにアカウント全体オブジェクトのコピーが必要になります。どちらのトレードオフを取るかは自分で選びましょう。

テンプレートで最終的に出来上がるロール構成はこちらです。

さらに、Deployerロールには環境固有のロールが直接付与されます。

データ取り込みツールは{env}_LOADERとして接続します。dbtは{env}_TRANSFORMERとして接続します。アナリストには{env}_ANALYSTが与えられ、ANALYTICSは読めますがRAWはまったく見えません。

dbtを使うなら、Analytics DBのテーブルとスキーマはdbtに任せる

DCMは幅広いオブジェクトタイプをサポートしています。データベース、スキーマ、テーブル、ビュー、動的テーブル、タスク、ステージ、関数、プロシージャ、マスキングポリシー、タグなどです。

では、なぜ私のテンプレートはロール、データベース、ウェアハウス、grantにしか使わないのでしょうか?

dbtを運用しているなら、テーブルとビューはすでにdbtが管理しているからです。リネージ、テスト、ドキュメント付きで。同じテーブルをDCMの定義にも入れると、2つのツールがどちらも自分の所有物だと思い込む状態になります。いずれ一方が、もう一方の作ったものを削除します。

そこで私は明確な線を引いています。

どのオブジェクトにも所有者は1つだけです。dbtを使っていない場合や、変換ツールが管理しないオブジェクト(ステージ、ファイルフォーマット、ネットワークルール)がある場合は、DCMが最適な置き場所になります。ルールは「オブジェクトごとに所有者は1つ」であって、「DCMはロールだけ」ではありません。

鶏が先か卵が先か:ブートストラップ

DCMには、デプロイに使うロールと、デプロイ先のプロジェクトオブジェクトが必要です。それらを作る何かが必要で、その何かにはACCOUNTADMINが必要です。

リポジトリ全体に適用している私のルールはこれです。

CI/CDは決してACCOUNTADMINを必要としてはならない。

そのため、ACCOUNTADMINが必要なものはすべて、人間が一度だけ実行する番号付きSQLファイルにまとめています。

USE ROLE ACCOUNTADMIN;
ALTER ACCOUNT SET FEATURE_RBAC_INHERITED_GRANTS = 'ENABLED';
CREATE ROLE IF NOT EXISTS PLATFORM_DEPLOYER
COMMENT = 'CI/CD deployment role for DCM.';
GRANT CREATE ROLE ON ACCOUNT TO ROLE PLATFORM_DEPLOYER;
GRANT CREATE DATABASE ON ACCOUNT TO ROLE PLATFORM_DEPLOYER;
GRANT CREATE WAREHOUSE ON ACCOUNT TO ROLE PLATFORM_DEPLOYER;
GRANT MANAGE GRANTS ON ACCOUNT TO ROLE PLATFORM_DEPLOYER;
GRANT ROLE PLATFORM_DEPLOYER TO ROLE SYSADMIN;
CREATE DATABASE IF NOT EXISTS PLATFORM;
CREATE SCHEMA IF NOT EXISTS PLATFORM.DCM;

コードを展開

(少し省略しています。完全なファイルはリポジトリにあります。)

それ以降はPLATFORM_DEPLOYERがすべてを行います。そして私が気に入っている習慣がこちらです。ローカルでも--role PLATFORM_DEPLOYERを渡すこと。 変更がこっそり追加の権限を必要としている場合、金曜午後のCIではなく、自分のラップトップで失敗してくれます。

私がハマった落とし穴(あなたがハマらないために)

DCMにおけるinherited grantはプレビュー扱いです。 Snowflakeのサポート対象DCMオブジェクト一覧では、inherited grantとコンテナレベルのMANAGE GRANTSがプレビュー機能とされています。私の環境では問題なく動いていますが、本番のアクセス制御を賭ける前にドキュメントを確認してください。

デプロイヤーを締め出さないこと。 DCMがDEV_RAWの所有権をDEV_LOADERに渡すと、PLATFORM_DEPLOYERは所有権を失います。次のデプロイで、DCMはアクセスできないものを管理できません。修正はロールごとに1行です。

GRANT ROLE {{ env }}_LOADER TO ROLE PLATFORM_DEPLOYER;
GRANT ROLE {{ env }}_TRANSFORMER TO ROLE PLATFORM_DEPLOYER;

マニフェストのaccount_identifierは環境変数を展開しません。 識別子を直接書き込んでください。これにより、間違ったアカウントへデプロイしようとしたときにDCMが警告してくれます。複数のクライアントアカウントを行き来するコンサルタントなら、ぜひ欲しい機能のはずです。

シークレットをテンプレート化しないこと。 Snowflakeは明言しています。テンプレート変数は認証情報のためのものではありません。

GitHub ActionsによるCI/CD

ここですべてがつながります。ワークフローは2つです。

プルリクエスト時:

  1. QAにプランとデプロイを実行。
  2. PRODのプランを実行し、その内容をPRコメントとして投稿。

mainへのマージ時:

  1. DEV、QA、PRODの順にプランとデプロイを実行。

PRコメントが私の一番のお気に入りです。レビュアーは「変更が安全なはず」と信じる必要はありません。マージをクリックする前に、DROPも含めて本番環境に何が起きるかを正確に確認できます。しかもジョブは新しいコメントを追加するのではなく前回のコメントを編集するので、10回プッシュしたPRでもプランコメントは1つだけです。

[screenshot: PROD plan posted as a PR comment]

CIは、キーペア認証を使うTYPE = SERVICEユーザーとして認証します。このユーザーが持つのはPLATFORM_DEPLOYERだけです。すべてのデプロイにはコミットに基づくalias(main_9fd3c1a)が付くため、Snowflake上のどのデプロイもマージまで直接たどることができます。

SnowsightでのDCMプロジェクト

Snowflake Workspacesには、DCMプロジェクトの管理・プラン・デプロイのための非常に便利なUIが用意されています。細かい話には立ち入らず、いくつかスクリーンショットを見てみましょう。

プランを実行すると、プランの内容を説明するタブが開きます。

ピッカーで環境を切り替えることができます。

デプロイの準備ができたら、Planのドロップダウンをクリックしてdeployを選択するだけです。

完了すると、画面上部中央にトーストが表示され、デプロイ完了を知らせてくれます。

Outputタブには、すべての実行のCLI出力が表示されます。

動的テーブルやタスクのDAGがあればリネージタブに表示されますが、このプロジェクトには含まれていません。

CLIでDCMをローカル実行する

個人的には、作業はすべてVisual Studio Codeで行っています。VS Codeでのデプロイの様子をざっと紹介します。dcmはsnow CLIコマンドのサブコマンドです。つまりsnow CLIがインストールされていれば、DCMはもう使えます!ここではsnow dcm planとsnow dcm deployを1枚のスクリーンショットにまとめて示しています。

これで、ローカルでファイルを編集し、dev環境にデプロイし、PRを開けばQAのプランとデプロイ、そして本番に対するプランが実行されます。開発者にとって非常に快適なワークフローです!

DCMと代替手段の比較

AWS、Snowflake、DNSを1つのTerraformリポジトリで管理しているなら、Terraformは今でも理にかなった選択です。チームのインフラが実質的にSnowflakeだけで、チームがすでにSQLに精通しているなら、DCMは私が見つけた中で最も摩擦の少ない方法です。

自分で試してみよう

ここで紹介した内容はすべてスターターリポジトリにまとめてあります:https://github.com/jeff-skoldberg-gmds/snowflake-dcm-starter。

定義ファイル、ブートストラップのマイグレーション、両方のGitHub Actionsワークフロー、そしてClaude Codeのスキルが含まれています。/project-setupを実行すれば、アカウント、マニフェスト、ブートストラップ、初回デプロイ、CIまで、1ステップずつチェックしながら案内してくれます。

空のingest/フォルダとdbt/フォルダも用意しています。DCMはあくまで土台にすぎないからです。データのロードと変換まで含めたSnowflakeプラットフォームのモノレポを構築すべきです。ロールとデータベースは、あなたのパイプラインを待っています。

さあ試してみて、SnowflakeのInfrastructure as Codeがまだ面倒に感じるかどうか、ぜひ教えてください!x`