Snowflake のマスキングポリシーと集計ポリシーを併用してSnowflake CoCoの利用時にPII をマスキングしつつ集計できるか試してみた

Snowflake のマスキングポリシーと集計ポリシーを併用してSnowflake CoCoの利用時にPII をマスキングしつつ集計できるか試してみた

前回までの記事で検証したマスキングポリシーと集計ポリシーを、同時に活用したいというニーズに応えるため、`SYSTEM$GET_TAG_ON_CURRENT_TABLE`による条件分岐マスキングと条件付き集計ポリシーの併用構成を検証しました。dbt Projects on Snowflakeでの安全な運用方法もあわせてご紹介します。
2026.07.08

かわばたです。

https://dev.classmethod.jp/articles/snowflake-is-agent-activated-coco-pii-masking/

IS_AGENT_ACTIVATED を使ったエージェント利用時のマスキングを

https://dev.classmethod.jp/articles/snowflake-aggregation-policy/

で集計ポリシー単体の動作をそれぞれ検証しました。

この2つを同時に使用する場合、単純に組み合わせると集計関数がマスク後の値に対して動作するため、意味のある集計結果が返らないという問題があります。さらに、dbt でテーブルを再作成(CREATE OR REPLACE)すると、テーブルに直接付与したタグや集計ポリシーが消えるという運用上の課題もあります。

この記事では、SYSTEM$GET_TAG_ON_CURRENT_TABLE による条件分岐マスキングと条件付き集計ポリシーで併用の問題を解決し、dbt Projects on Snowflake の post-hook で安全に運用する構成を検証しました。

https://docs.snowflake.com/en/sql-reference/functions/system_get_tag_on_current_table

https://docs.snowflake.com/en/user-guide/tag-based-masking-policies

https://docs.snowflake.com/en/user-guide/aggregation-policies

背景・課題

AI エージェント(CoCoなど)がデータにアクセスする際、以下の2つの保護を同時に実現したいケースがあります。

  1. エージェント経由では PII をマスクしたい(前々回の記事で検証したマスキングポリシー + IS_AGENT_ACTIVATED
  2. エージェントには集計結果だけ見せ、明細は見せたくない(前回の記事で検証した集計ポリシー)

しかし、そのまま両方を適用すると以下の問題が発生します。

  • 集計関数はマスク後の値に対して動作するSUM(amount)AVG(salary) がマスク後の値(NULL や固定文字列)に対して計算されるため、集計結果が無意味になる
  • dbt の標準的なマテリアライゼーション(table)は CREATE OR REPLACE でテーブルを再作成するため、テーブルに直接付与した集計ポリシーやタグが消える

これらを解決し、安全側に倒れる(fail-safe)構成を作ることがゴールです。

技術的アプローチ

解決策は以下の3つを組み合わせます。

  1. タグベースマスキング + SYSTEM$GET_TAG_ON_CURRENT_TABLE による条件分岐: 集計ポリシー適用済みテーブルではマスクを解除し、集計ポリシー側でデータを保護する
  2. 条件付き集計ポリシー: IS_AGENT_ACTIVATED で非エージェント実行コンテキストは無制約、エージェントのみ集計を強制する
  3. dbt の post-hook: CREATE OR REPLACE 後にタグと集計ポリシーをセットで再適用する

SYSTEM$GET_TAG_ON_CURRENT_TABLE とは

SYSTEM$GET_TAG_ON_CURRENT_TABLE は、マスキングポリシーや行アクセスポリシーの条件式内でのみ使用できるシステム関数です。現在のテーブルに付与されたタグの値を参照できます。

-- マスキングポリシーの条件式内で使用する例
SYSTEM$GET_TAG_ON_CURRENT_TABLE('db.schema.tag_name')

本記事では、「集計ポリシー適用済み」を示す例外タグが付いたテーブルではマスクを解除し、タグがないテーブルではマスクを適用する、という条件分岐に使います。

https://docs.snowflake.com/en/sql-reference/functions/system_get_tag_on_current_table

条件付き集計ポリシーの設計

集計ポリシーの CASE 式で IS_AGENT_ACTIVATED を条件に使い、非エージェント実行コンテキストは無制約、エージェントセッションのみ集計を強制する設計にします。

CREATE AGGREGATION POLICY agent_only_agg_policy
  AS () RETURNS AGGREGATION_CONSTRAINT ->
  CASE
    WHEN SYS_CONTEXT('SNOWFLAKE$CURRENT', 'IS_AGENT_ACTIVATED')::BOOLEAN = TRUE
    THEN AGGREGATION_CONSTRAINT(MIN_GROUP_SIZE => 5)
    ELSE NO_AGGREGATION_CONSTRAINT()
  END;

これにより、人間ユーザーは従来どおり明細データを参照でき、エージェントだけが集計制約を受けます。

COPY TAGS ではなく post-hook を推奨する理由

dbt でテーブルを再作成する際、COPY TAGS で例外タグを保持する方法もあります。しかし、本構成では post-hook 方式を推奨します。

観点 COPY TAGS post-hook
例外タグ 保持される 再適用される
集計ポリシー 保持されないCOPY POLICIES は存在しない) 再適用される
失敗時の挙動 fail-open(マスク解除だけ残り、集計制約なし) fail-safe(対象データ型列がマスクされた状態に戻る)
dbt-snowflake 対応 バージョンにより copy_tags config が利用可能な場合あり。ただし集計ポリシーは対象外 +post-hook で設定可能

COPY TAGS を使うと「例外タグ(マスク解除)は生き残るが、集計ポリシーは消える」という危険な状態が発生し得ます。post-hook なら両方が一緒に消えて STRING / NUMBER 型列がマスクされた状態に戻るため、安全側に倒れます。

制限事項

  • タグベースマスキングポリシー・集計ポリシーともに Enterprise Edition 以上が必要です
  • 集計ポリシーで許可される集計関数は AVG / COUNT / COUNT(DISTINCT) / HLL / SUM のみです。MIN / MAX / MEDIAN / ウィンドウ関数 / GROUP BY ROLLUP・CUBE 等は使用できません
  • エージェントが SELECT * LIMIT 10 のような明細クエリを生成した場合、集計ポリシーによりエラーになります。エージェントがリカバリ(集計形式に書き直し)するかは実機検証が必要です
  • 集計ポリシーは GROUP BY キー列の実値をそのまま返します。PII を GROUP BY キーに使われた場合、MIN_GROUP_SIZE 以上のグループであれば実値がエージェントに見えます。PII 列を GROUP BY キーに使わせたくない場合は、ビュー設計やカラムレベルマスキングの併用を検討してください
  • 1つのタグに紐づけられるマスキングポリシーは「データ型ごとに1つ」です。利用するデータ型分のポリシーを用意する必要があります
  • SYSTEM$GET_TAG_ON_CURRENT_TABLE で参照するタグ名は、名前解決の曖昧さや実行コンテキストの差異を避けるため、完全修飾名(db.schema.tag)で指定することを推奨します

前提条件

  • Snowflake: 本検証はトライアルアカウントのEnterprise版で実施
  • 必要な権限: ACCOUNTADMIN ロール(検証用)
  • dbt Projects on Snowflake: 利用可能な環境であること(セットアップ手順は公式ドキュメントを参照)

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

事前準備

検証用データベース・スキーマの作成

マスキングポリシーと集計ポリシーの併用を検証するための環境を作成します。ガバナンス用オブジェクト(タグ・ポリシー)は governance スキーマに、データ用テーブルは public スキーマに配置します。

検証用データベース・スキーマ・テーブルの作成
USE ROLE ACCOUNTADMIN;

CREATE DATABASE IF NOT EXISTS masking_agg_test;
CREATE SCHEMA IF NOT EXISTS masking_agg_test.governance;
CREATE SCHEMA IF NOT EXISTS masking_agg_test.public;

検証用テーブルの作成

売上データのテーブルを作成します。PII(顧客名・メールアドレス)と集計対象の数値(金額)を含むデータです。

USE SCHEMA masking_agg_test.public;

CREATE OR REPLACE TABLE sales (
    sale_id INT COMMENT '売上ID',
    region VARCHAR COMMENT '地域',
    product VARCHAR COMMENT '商品名',
    amount NUMBER(10, 2) COMMENT '売上金額',
    customer_name VARCHAR COMMENT '顧客名',
    email VARCHAR COMMENT 'メールアドレス',
    sale_date DATE COMMENT '売上日'
);
-- 検証用データの投入(25レコード)
-- 地域別: Tokyo 8件、Osaka 7件、Nagoya 5件、Fukuoka 3件、Sapporo 2件
-- Fukuoka(3件)とSapporo(2件)は MIN_GROUP_SIZE=5 で remainder group(剰余グループ)に入ることを確認するため意図的に少件数
INSERT INTO sales VALUES
    (1, 'Tokyo', 'Laptop', 120000, '田中太郎', 'tanaka@example.com', '2026-06-01'),
    (2, 'Tokyo', 'Mouse', 3500, '佐藤花子', 'sato@example.com', '2026-06-02'),
    (3, 'Tokyo', 'Keyboard', 8000, '鈴木一郎', 'suzuki@example.com', '2026-06-03'),
    (4, 'Tokyo', 'Monitor', 45000, '高橋美咲', 'takahashi@example.com', '2026-06-04'),
    (5, 'Tokyo', 'Headset', 12000, '伊藤健太', 'ito@example.com', '2026-06-05'),
    (6, 'Tokyo', 'Laptop', 135000, '渡辺直美', 'watanabe@example.com', '2026-06-06'),
    (7, 'Tokyo', 'Tablet', 55000, '山本学', 'yamamoto@example.com', '2026-06-07'),
    (8, 'Tokyo', 'Mouse', 4200, '中村陽子', 'nakamura@example.com', '2026-06-08'),
    (9, 'Osaka', 'Laptop', 118000, '小林誠', 'kobayashi@example.com', '2026-06-09'),
    (10, 'Osaka', 'Keyboard', 7500, '加藤裕二', 'kato@example.com', '2026-06-10'),
    (11, 'Osaka', 'Monitor', 42000, '吉田真理', 'yoshida@example.com', '2026-06-11'),
    (12, 'Osaka', 'Headset', 15000, '松本大輔', 'matsumoto@example.com', '2026-06-12'),
    (13, 'Osaka', 'Tablet', 48000, '井上さくら', 'inoue@example.com', '2026-06-13'),
    (14, 'Osaka', 'Laptop', 125000, '木村拓也', 'kimura@example.com', '2026-06-14'),
    (15, 'Osaka', 'Mouse', 3800, '林美穂', 'hayashi@example.com', '2026-06-15'),
    (16, 'Nagoya', 'Laptop', 110000, '清水翔太', 'shimizu@example.com', '2026-06-16'),
    (17, 'Nagoya', 'Monitor', 38000, '斎藤恵', 'saito@example.com', '2026-06-17'),
    (18, 'Nagoya', 'Keyboard', 9200, '前田健一', 'maeda@example.com', '2026-06-18'),
    (19, 'Nagoya', 'Headset', 11000, '藤井優子', 'fujii@example.com', '2026-06-19'),
    (20, 'Nagoya', 'Tablet', 52000, '岡田龍一', 'okada@example.com', '2026-06-20'),
    (21, 'Fukuoka', 'Laptop', 128000, '石川由美', 'ishikawa@example.com', '2026-06-21'),
    (22, 'Fukuoka', 'Mouse', 4500, '三浦健二', 'miura@example.com', '2026-06-22'),
    (23, 'Fukuoka', 'Monitor', 41000, '西田あかり', 'nishida@example.com', '2026-06-23'),
    (24, 'Sapporo', 'Laptop', 115000, '上田浩司', 'ueda@example.com', '2026-06-24'),
    (25, 'Sapporo', 'Keyboard', 8500, '河野真由', 'kawano@example.com', '2026-06-25');

2026-07-08_09h08_04

テーブルの内容を SELECT して、25件のデータが正しく入っていることを確認しておきます。

SELECT * FROM sales ORDER BY sale_id;

ガバナンス用オブジェクトの作成

タグ、マスキングポリシー、集計ポリシーを governance スキーマに作成します。

ガバナンス用オブジェクトの作成

まず、タグを2つ作成します。

USE SCHEMA masking_agg_test.governance;

-- データベース単位でマスキングを適用するためのタグ
CREATE TAG IF NOT EXISTS agent_pii_mask
  COMMENT = 'データベースレベルで付与し、タグ継承でエージェント向けマスキングを適用する';

-- 集計ポリシー適用済みテーブルに付与する例外タグ
CREATE TAG IF NOT EXISTS agg_policy_applied
  COMMENT = '集計ポリシー適用済みのテーブルに付与し、マスキングを解除する';

次に、SYSTEM$GET_TAG_ON_CURRENT_TABLE で条件分岐するマスキングポリシーを作成します。STRING 型と NUMBER 型の2つを用意します。

-- STRING 型用マスキングポリシー
CREATE OR REPLACE MASKING POLICY mask_string_for_agent
  AS (val STRING) RETURNS STRING ->
  CASE
    -- 集計ポリシー適用済みテーブルではマスクしない(集計ポリシー側で保護)
    WHEN SYSTEM$GET_TAG_ON_CURRENT_TABLE('masking_agg_test.governance.agg_policy_applied') IS NOT NULL
      THEN val
    -- エージェントセッションのみマスクする
    WHEN SYS_CONTEXT('SNOWFLAKE$CURRENT', 'IS_AGENT_ACTIVATED')::BOOLEAN = TRUE
      THEN '***MASKED***'
    ELSE val
  END;
-- NUMBER 型用マスキングポリシー
CREATE OR REPLACE MASKING POLICY mask_number_for_agent
  AS (val NUMBER) RETURNS NUMBER ->
  CASE
    -- 集計ポリシー適用済みテーブルではマスクしない(集計ポリシー側で保護)
    WHEN SYSTEM$GET_TAG_ON_CURRENT_TABLE('masking_agg_test.governance.agg_policy_applied') IS NOT NULL
      THEN val
    -- エージェントセッションのみマスクする
    WHEN SYS_CONTEXT('SNOWFLAKE$CURRENT', 'IS_AGENT_ACTIVATED')::BOOLEAN = TRUE
      THEN NULL
    ELSE val
  END;

条件付き集計ポリシーを作成します。非エージェント実行コンテキスト(人間ユーザー、dbt 等)は無制約、エージェントのみ集計を強制します。

-- 条件付き集計ポリシー(エージェントのみ集計強制)
CREATE OR REPLACE AGGREGATION POLICY agent_only_agg_policy
  AS () RETURNS AGGREGATION_CONSTRAINT ->
  CASE
    WHEN SYS_CONTEXT('SNOWFLAKE$CURRENT', 'IS_AGENT_ACTIVATED')::BOOLEAN = TRUE
    THEN AGGREGATION_CONSTRAINT(MIN_GROUP_SIZE => 5)
    ELSE NO_AGGREGATION_CONSTRAINT()
  END;

タグベースマスキングの適用(データベースレベル)

マスキングポリシーをタグに紐づけ、データベースレベルでタグを付与します。タグの継承により、データベース配下の全テーブルの全カラムに自動的にマスキングが適用されます。

タグベースマスキングの適用
-- タグにデータ型ごとのマスキングポリシーを紐づけ
ALTER TAG masking_agg_test.governance.agent_pii_mask
  SET MASKING POLICY masking_agg_test.governance.mask_string_for_agent;

ALTER TAG masking_agg_test.governance.agent_pii_mask
  SET MASKING POLICY masking_agg_test.governance.mask_number_for_agent;
-- データベースレベルでタグを付与(配下の全テーブル・全カラムに継承される)
ALTER DATABASE masking_agg_test
  SET TAG masking_agg_test.governance.agent_pii_mask = 'on';

2026-07-08_09h11_31

これで、masking_agg_test データベース配下の全テーブルで、エージェント経由のアクセス時に STRING 型カラムは '***MASKED***'、NUMBER 型カラムは NULL に置換されるようになりました。

検証用ロールの作成

検証用ロールを作成し、必要な権限を付与します。

検証用ロールの作成
USE ROLE ACCOUNTADMIN;

CREATE ROLE IF NOT EXISTS MASKING_AGG_TEST_ROLE;

GRANT USAGE ON DATABASE masking_agg_test TO ROLE MASKING_AGG_TEST_ROLE;
GRANT USAGE ON SCHEMA masking_agg_test.public TO ROLE MASKING_AGG_TEST_ROLE;
GRANT SELECT ON ALL TABLES IN SCHEMA masking_agg_test.public TO ROLE MASKING_AGG_TEST_ROLE;
-- dbt で作成されるテーブルにも SELECT 権限が自動付与されるようにする
GRANT SELECT ON FUTURE TABLES IN SCHEMA masking_agg_test.public TO ROLE MASKING_AGG_TEST_ROLE;
GRANT USAGE ON WAREHOUSE <your_warehouse_name> TO ROLE MASKING_AGG_TEST_ROLE;

GRANT ROLE MASKING_AGG_TEST_ROLE TO USER <your_username>;

:::

dbt 実行ロールへの権限付与

dbt Projects on Snowflake でモデルを実行するロールには、post-hook で集計ポリシーとタグを適用するための権限が必要です。dbt の実行ロールはモデルテーブルの OWNERSHIP を持つため、各オブジェクトへの APPLY 権限を付与します。

USE ROLE ACCOUNTADMIN;

-- post-hook で SET AGGREGATION POLICY を実行するために必要
GRANT APPLY ON AGGREGATION POLICY masking_agg_test.governance.agent_only_agg_policy
  TO ROLE <your_dbt_role>;

-- post-hook で SET TAG を実行するために必要
GRANT APPLY ON TAG masking_agg_test.governance.agg_policy_applied
  TO ROLE <your_dbt_role>;

-- governance スキーマ内のオブジェクトを参照するために必要
GRANT USAGE ON DATABASE masking_agg_test TO ROLE <your_dbt_role>;
GRANT USAGE ON SCHEMA masking_agg_test.governance TO ROLE <your_dbt_role>;
GRANT USAGE ON SCHEMA masking_agg_test.public TO ROLE <your_dbt_role>;

-- モデル作成に必要
GRANT CREATE TABLE ON SCHEMA masking_agg_test.public TO ROLE <your_dbt_role>;

試してみた

マスキングのみの状態でクエリを実行する

まず、集計ポリシーを適用する前の状態で、マスキングの動作を確認します。

通常の SQL クエリ(マスクされない)

MASKING_AGG_TEST_ROLE に切り替えて、Snowsight のワークシートから直接 SQL を実行します。

USE ROLE MASKING_AGG_TEST_ROLE;

SELECT sale_id, region, product, amount, customer_name, email
FROM masking_agg_test.public.sales
ORDER BY sale_id
LIMIT 5;

2026-07-08_09h31_19

すべてのカラムが生データのまま表示されればOKです。エージェント経由ではないため、IS_AGENT_ACTIVATEDFALSE を返し、マスキングポリシーの ELSE 節で元の値がそのまま返ります。

Cortex Code(Snowsight)経由(マスクされる)

同じ MASKING_AGG_TEST_ROLE のまま、Snowsight 上の Cortex Code から同じテーブルを問い合わせます。

Cortex Code のロール設定が MASKING_AGG_TEST_ROLE になっていることを確認してください。

Cortex Code に「masking_agg_test.public.sales テーブルから売上ID、地域、商品名、金額、顧客名、メールアドレスを5件取得してください」のように依頼します。

2026-07-08_09h40_49

STRING 型カラム(regionproductcustomer_nameemail)が ***MASKED***、NUMBER 型カラム(sale_idamount)が NULL と表示されればOKです。この状態では STRING / NUMBER 型の全列がマスクされるため、集計クエリを実行しても意味のある結果は返りません。

集計ポリシーと例外タグをセットで適用し、エージェント経由で集計クエリを実行する

ACCOUNTADMIN に切り替え、sales テーブルに集計ポリシーと例外タグをセットで適用します。

USE ROLE ACCOUNTADMIN;

-- 集計ポリシーの適用
ALTER TABLE masking_agg_test.public.sales
  SET AGGREGATION POLICY masking_agg_test.governance.agent_only_agg_policy;

-- 例外タグの付与(マスキングポリシーの条件分岐で参照される)
ALTER TABLE masking_agg_test.public.sales
  SET TAG masking_agg_test.governance.agg_policy_applied = 'true';

通常の SQL(人間ユーザー → 無制約)

まず、人間ユーザーとして動作を確認します。条件付き集計ポリシーにより、人間ユーザーは NO_AGGREGATION_CONSTRAINT() が適用されるため、明細も集計も自由に実行できます。

USE ROLE MASKING_AGG_TEST_ROLE;

-- 明細クエリ(人間ユーザーなので制約なし)
SELECT sale_id, region, product, amount, customer_name, email
FROM masking_agg_test.public.sales
ORDER BY sale_id
LIMIT 5;

2026-07-08_10h44_35

すべてのカラムが生データのまま表示されれば問題ありません。例外タグが付与されているためマスキングは解除され、かつ条件付き集計ポリシーにより人間ユーザーは集計制約を受けません。

Cortex Code で集計クエリ(マスク解除 + 実値ベースの集計結果)

Cortex Code(Snowsight)から集計クエリを実行します。

Cortex Code に「masking_agg_test.public.sales テーブルから地域別の売上件数と合計金額を集計してください」のように依頼します。

2026-07-08_10h46_33

以下の結果になれば問題ありません。

  • Tokyo(8件)、Osaka(7件)、Nagoya(5件): MIN_GROUP_SIZE=5 以上なので、地域名と集計値がそのまま表示される
  • Fukuoka(3件)と Sapporo(2件): MIN_GROUP_SIZE=5 未満なので、remainder group(剰余グループ、regionNULL)にまとめられる

例外タグによりマスキングが解除されているため、実データに基づく意味のある集計結果が返ります。マスク状態だった amount の合計・件数が正しい値で集計されている点がポイントです。

Cortex Code で明細クエリ(集計ポリシーによりエラー)

Cortex Code に「masking_agg_test.public.sales テーブルの全レコードを表示してください」のように依頼します。

2026-07-08_10h53_28

集計ポリシーにより、集計関数を含まないクエリはエラーになります。エージェント経由では明細データにアクセスできないことが確認できます。

dbt Projects on Snowflake でモデルを作成し、post-hook なしで実行する

ここから dbt Projects on Snowflake での運用を検証します。sales テーブルと同じデータを dbt モデルとして作成し、CREATE OR REPLACE によるテーブル再作成時のタグ・ポリシーの挙動を確認します。

dbt プロジェクト内に以下のモデルファイルを作成します。

-- models/sales_mart.sql
{{ config(
    materialized='table',
    database='masking_agg_test',
    schema='public'
) }}

SELECT
    sale_id,
    region,
    product,
    amount,
    customer_name,
    email,
    sale_date
FROM {{ source('public', 'sales') }}

ソース定義も作成します。

# models/sources.yml
version: 2

sources:
  - name: public
    database: masking_agg_test
    schema: public
    tables:
      - name: sales

post-hook なしの状態で dbt run を実行します。

2026-07-08_11h26_11

dbt run が成功した後、sales_mart テーブルのタグと集計ポリシーの状態を確認します。

USE ROLE ACCOUNTADMIN;

-- 例外タグの確認
SELECT SYSTEM$GET_TAG('masking_agg_test.governance.agg_policy_applied', 'masking_agg_test.public.sales_mart', 'TABLE');

2026-07-08_11h27_48

例外タグが NULL(付与されていない)であることを確認します。CREATE OR REPLACE によりテーブルが再作成され、直接付与したタグが消えています。

-- 集計ポリシーの確認
SELECT policy_name, policy_kind
FROM TABLE(
  masking_agg_test.information_schema.policy_references(
    ref_entity_name => 'masking_agg_test.public.sales_mart',
    ref_entity_domain => 'TABLE'
  )
)
WHERE policy_kind = 'AGGREGATION_POLICY';

2026-07-08_11h30_24

集計ポリシーも適用されていない(結果が0行)ことを確認します。

一方、データベースレベルのタグベースマスキングは維持されています。タグの継承はデータベース側の設定に基づくため、再作成されたテーブルも「新規に追加されたテーブル」として自動的にマスキング対象になります。

-- マスキングポリシーの確認(データベースレベルタグの継承で適用されている)
SELECT policy_name, policy_kind
FROM TABLE(
  masking_agg_test.information_schema.policy_references(
    ref_entity_name => 'masking_agg_test.public.sales_mart',
    ref_entity_domain => 'TABLE'
  )
)
WHERE policy_kind = 'MASKING_POLICY';

2026-07-08_13h31_54

マスキングポリシーが維持されていることを確認できます。つまり、post-hook なしで dbt run した場合、テーブルは「STRING / NUMBER 型列がマスクされ、集計制約なし」の状態に戻ります。エージェントから見ると対象データ型の列がマスクされた状態なので、データは保護されています。

post-hook を設定して dbt run を再実行する

モデルの設定に post-hook を追加し、集計ポリシーと例外タグを dbt run 後に自動で再適用させます。

-- models/sales_mart.sql
{{ config(
    materialized='table',
    database='masking_agg_test',
    schema='public',
    post_hook=[
      "ALTER TABLE {{ this }} SET AGGREGATION POLICY masking_agg_test.governance.agent_only_agg_policy FORCE",
      "ALTER TABLE {{ this }} SET TAG masking_agg_test.governance.agg_policy_applied = 'true'"
    ]
) }}

SELECT
    sale_id,
    region,
    product,
    amount,
    customer_name,
    email,
    sale_date
FROM {{ source('public', 'sales') }}

dbt run を再実行します。

実行後、タグと集計ポリシーの状態を再確認します。

USE ROLE ACCOUNTADMIN;

-- 例外タグの確認
SELECT SYSTEM$GET_TAG('masking_agg_test.governance.agg_policy_applied', 'masking_agg_test.public.sales_mart', 'TABLE');

2026-07-08_13h39_17

例外タグが 'true' で返れば問題ありません。

-- 集計ポリシーの確認
SELECT policy_name, policy_kind
FROM TABLE(
  masking_agg_test.information_schema.policy_references(
    ref_entity_name => 'masking_agg_test.public.sales_mart',
    ref_entity_domain => 'TABLE'
  )
)
WHERE policy_kind = 'AGGREGATION_POLICY';

2026-07-08_13h39_49
集計ポリシーも再適用されていることを確認します。

Cortex Code(Snowsight)で sales_mart テーブルに対して集計クエリを実行し、同様に実値ベースの集計結果が返ることを確認します。

2026-07-08_13h41_20

post-hook により、dbt run のたびに集計ポリシーと例外タグがセットで再適用され、エージェントからの集計クエリが正常に動作することが確認できました。

(補足)incremental モデルと --full-refresh 時の挙動

incremental マテリアライゼーションの場合、通常の dbt run ではテーブルが再作成されないため、タグと集計ポリシーは維持されます。ただし、--full-refresh オプションを指定すると CREATE OR REPLACE が実行されるため、前項目と同様にタグ・ポリシーが消えます。

incremental モデルでも post-hook を設定しておけば、--full-refresh 時に自動で再適用されます。通常実行時にも post-hook は実行されますが、SET TAG は値の上書きで冪等に動作し、SET AGGREGATION POLICYFORCE パラメータにより既存ポリシーを原子的に置き換えるため、2回目以降の実行でもエラーにはなりません。

最後に

マスキングポリシーと集計ポリシーの併用構成を検証し、dbt Projects on Snowflake での運用方法を確認しました。

ポイントは以下のとおりです。

  • SYSTEM$GET_TAG_ON_CURRENT_TABLE による条件分岐で、集計ポリシー適用済みテーブルのマスクを解除することで、マスク後の値で集計される問題を回避
  • IS_AGENT_ACTIVATED による条件付き集計ポリシーで、非エージェント実行コンテキスト(人間ユーザー、dbt 等)は明細を参照でき、エージェントのみ集計を強制する制御を実現
  • dbt の post-hook で集計ポリシーと例外タグをセットで再適用することで、CREATE OR REPLACE 後も安全に運用できる構成を確認
  • post-hook の設定漏れや失敗時には**少なくとも STRING / NUMBER 型列がマスクされた状態に戻る(fail-safe)**ため、情報漏えい方向の事故にならない

この記事が何かの参考になれば幸いです!


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

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

Snowflakeの詳細を見る

この記事をシェアする

関連記事