1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Terraform × Snowflake のネットワークポリシーで known after apply の罠にハマった話

1
Last updated at Posted at 2026-03-30

はじめに

Terraform の known after apply、結果どうなる?が気になりますよね。

Terraformあるあるですが、セキュリティ設定でこれが出ると話が変わります。
今回気になったのは、Snowflake のユーザーにアタッチするネットワークポリシーです。

Snowflakeのネットワークポリシーは、要は「このIPからしかアクセスさせない」というアクセス制御の設定です。

これが known after apply になると、plan の時点で「ポリシーが外れるかどうか」が分からない。外れたら普通にセキュリティホールじゃん、と。

applyしないとわからない、じゃなくて事前に知りたいじゃないですか。
plan で正しい apply 予測値が表示されてほしいんです。
でも出ないんですよね...

というわけで検証してみました。

環境セットアップ

トライアルアカウントで検証しました。

ローカルから terraform plan / apply ができる認証設定は済んでいる前提です。

そのため、ここでは認証情報設定は割愛します。

Snowflake 側の権限

ネットワークポリシーの管理と、後半で使うクエリ履歴の監視に必要な権限をまとめて付与します。

USE ROLE ACCOUNTADMIN;

-- ネットワークポリシー管理用のロールを作成
CREATE ROLE NETWORK_POLICY_ADMIN;
GRANT CREATE NETWORK POLICY ON ACCOUNT TO ROLE NETWORK_POLICY_ADMIN;
GRANT CREATE USER ON ACCOUNT TO ROLE NETWORK_POLICY_ADMIN;

-- ロールの付与と継承
GRANT ROLE NETWORK_POLICY_ADMIN TO USER YOUR_USER;
GRANT ROLE NETWORK_POLICY_ADMIN TO ROLE SYSADMIN;

-- クエリ履歴の監視権限(原因調査で使う)
GRANT MONITOR ON ACCOUNT TO ROLE NETWORK_POLICY_ADMIN;
GRANT IMPORTED PRIVILEGES ON DATABASE SNOWFLAKE TO ROLE NETWORK_POLICY_ADMIN;

MONITORIMPORTED PRIVILEGES は、後半の「Terraform が裏で何の SQL を投げたか」を調査するために必要です。
ネットワークポリシーの検証だけならなくても動きますが、原因調査まで一気にやりたいので最初から入れておきます。

Terraform プロバイダー設定

snowflake_network_policy_attachment リソースはプレビュー機能のため、明示的に有効化が必要です。

パスワード認証は検証でも使いたくないので、今だったらサクッとPAT(Personal Access Token)を払い出して使うか、キーペアにしておきましょう。

ここではキーペアを使う例です。

provider "snowflake" {
  organization_name = var.organization_name
  account_name      = var.account_name
  user              = var.user
  private_key_path  = var.private_key_path
  role              = var.role

  preview_features_enabled = ["snowflake_network_policy_attachment_resource"]
}

Step 1:ユーザを作る(ノーアタッチ状態)

まずはベースラインです。
ユーザーとポリシーを作るだけで、紐付けは一切行いません。

resource "snowflake_network_policy" "test_policy" {
  name            = "TEST_POLICY"
  allowed_ip_list = ["192.168.0.1/32"] # 適当な値でよい
}

resource "snowflake_user" "test_user" {
  name       = "TEST_USER"
  login_name = "TEST_USER"
  password   = "TestPass123!#"

  # ここには何も書かない(ノーアタッチ状態)
}

apply して、実体を確認します。

terraform apply -auto-approve

snow sql -q "SHOW PARAMETERS LIKE 'NETWORK_POLICY' IN USER TEST_USER;"
+---------------------------------------------------------------------------------------------------+
| key            | value | default | level | description                                   | type   |
|----------------+-------+---------+-------+-----------------------------------------------+--------|
| NETWORK_POLICY |       |         |       | Network policy assigned for the given target. | STRING |
+---------------------------------------------------------------------------------------------------+

value が空です。
何も付いていない、正しい初期状態が確認できました。

Step 2:パターンA - ユーザーリソース内で直接指定する(正解)

snowflake_usernetwork_policy 属性に直接書く方法です。

resource "snowflake_network_policy" "test_policy" {
  name            = "TEST_POLICY"
  allowed_ip_list = ["192.168.0.1/32"]
}

resource "snowflake_user" "test_user" {
  name           = "TEST_USER"
  login_name     = "TEST_USER"
  network_policy = snowflake_network_policy.test_policy.name
}

余談 : plan の差分地獄

network_policy を 1 行追加しただけなのに、plan を実行するとこうなります。

  ~ resource "snowflake_user" "test_user" {
        id                                            = "TEST_USER"
        name                                          = "TEST_USER"
      + network_policy                                = "TEST_POLICY"
      ~ parameters                                    = [
          - {
              - abort_detached_query  = [...]
              - autocommit            = [...]
              - binary_input_format   = [...]
              - binary_output_format  = [...]
              - client_memory_limit   = [...]
              ...(以下、数十個のデフォルトパラメータが延々と続く)...
            },
        ] -> (known after apply)
        # (77 unchanged attributes hidden)
    }

plan長い!
そんな変えてないのに!!!

ってなりますね。

これは Snowflake プロバイダーの仕様で、NETWORK_POLICY 含む諸々パラメータは、全部「ユーザーに紐づくパラメータリスト」の中の一員です。

network_policy を指定した瞬間、Terraform は「このユーザーのパラメータリストを管理するぞ」と認識して、同居している数多のデフォルト値を全部読み直そうとします。

結果として parameters: [...] -> (known after apply) という大量ログが流れ出します。

見た目はアレですが、Read Only属性だし、我々で何ともできない部分なので、受け入れましょう。

これ、毎回出るたびに、気になりますよね。
ふーん、ぐらいで流したい...
でも気になるんだよなぁ...(ブツブツ)

apply と安定性の確認

terraform apply -auto-approve

からの

terraform plan

そして

No changes. Your infrastructure matches the configuration.

差分なし。OK。

snow sql -q "SHOW PARAMETERS LIKE 'NETWORK_POLICY' IN USER TEST_USER;"
+-------------------------------------------------------------------------------------------------------------+
| key            | value       | default | level | description                                   | type   |
|----------------+-------------+---------+-------+-----------------------------------------------+--------|
| NETWORK_POLICY | TEST_POLICY |         | USER  | Network policy assigned for the given target. | STRING |
+-------------------------------------------------------------------------------------------------------------+

ポリシーが付いていて、plan も安定。
これが理想の挙動です。

Step 3:パターンB - attachment リソースで分離管理する(罠)

ユーザー管理とポリシーのアタッチを分離して管理したい、という発想自体はアリだと思ってます。
責任分離の観点でこういう構成にしたくなることはあります。

Step 2 の安定状態から、snowflake_usernetwork_policy をコメントアウトし、代わりに snowflake_network_policy_attachment を追加します。

resource "snowflake_network_policy" "test_policy" {
  name            = "TEST_POLICY"
  allowed_ip_list = ["192.168.0.1/32"]
}

resource "snowflake_user" "test_user" {
  name       = "TEST_USER"
  login_name = "TEST_USER"
  password   = "TestPass123!#"

  # network_policy をコメントアウト(attachmentに移管)
  # network_policy = snowflake_network_policy.test_policy.name
}

resource "snowflake_network_policy_attachment" "test_attach" {
  network_policy_name = snowflake_network_policy.test_policy.name
  set_for_account     = false
  users               = [snowflake_user.test_user.name]
}

1回目の apply:一見問題なさそうに見える

plan を見ると、snowflake_usernetwork_policy"TEST_POLICY" -> (known after apply) になります。

ここで既に「嫌な予感」がしますが、apply は成功します。

terraform apply -auto-approve

からの

snow sql -q "SHOW PARAMETERS LIKE 'NETWORK_POLICY' IN USER TEST_USER;"

さて...どうなる?

| NETWORK_POLICY | TEST_POLICY |         | USER  |

おお。いいですね。

ちゃんとアタッチされています。
ここまでは問題なし。

apply 直後の plan

ここからおかしくなるんです...

terraform plan
  ~ resource "snowflake_user" "test_user" {
        name                                          = "TEST_USER"
      ~ network_policy                                = "TEST_POLICY" -> (known after apply)
    }

Plan: 0 to add, 1 to change, 0 to destroy.

を??

「のうん あふたー あぷらい」だと?

apply したばかりなのに、なんか差分が出ます。

2回目の apply:ポリシーが外れる

terraform apply -auto-approve

「のうん あふたー あぷらい」だったが、結果どうなるの?と。

snow sql -q "SHOW PARAMETERS LIKE 'NETWORK_POLICY' IN USER TEST_USER;"

ドキドキ...

+---------------------------------------------------------------------------------------------------+
| key            | value | default | level | description                                   | type   |
|----------------+-------+---------+-------+-----------------------------------------------+--------|
| NETWORK_POLICY |       |         |       | Network policy assigned for the given target. | STRING |
+---------------------------------------------------------------------------------------------------+

えー!!

ポリシーが外れてます。

3回目の apply

再度実行してみると...

terraform plan
  ~ resource "snowflake_user" "test_user" {
        name                                          = "TEST_USER"
      ~ network_policy                                = "TEST_POLICY" -> (known after apply)
    }

Plan: 0 to add, 1 to change, 0 to destroy.

known after apply が永遠に消えない。

terraform apply -auto-approve
snow sql -q "SHOW PARAMETERS LIKE 'NETWORK_POLICY' IN USER TEST_USER;"

でどうなるの?

| NETWORK_POLICY | TEST_POLICY |         | USER  |

ついてるやないかーい!

……怖いですね。

apply を繰り返すたびに、アタッチ → 外れる → アタッチ → 外れる、のシーソーゲームですよこりゃ。

どっちに転ぶかは apply してみないと分からない。
plan の known after apply 「付くかもしれないし外れるかもしれない」と言っているだけで、何の保証もありません。

ちなみに、ここでもう一度ユーザーリソースの方で直接ネットワークポリシーをアタッチする記述で「on/off」するとどう見えるか?を見てみます。

resource "snowflake_user" "test_user" {
  name         = "GEM_TEST_USER"
  login_name   = "GEM_TEST_USER"

  # ① ここをコメントアウト。
  # network_policy = snowflake_network_policy.test_policy.name
}

これのplanは...

  # snowflake_user.test_user will be updated in-place
  ~ resource "snowflake_user" "test_user" {
        id                                            = "GEM_TEST_USER"
        name                                          = "GEM_TEST_USER"
      ~ network_policy                                = "GEM_TEST_POLICY" -> (known after apply)
        # (78 unchanged attributes hidden)
    }

なんですねぇ。

今の仕様だとどっちを使っても「適用するまでわからない」となります。

ですが、snowflake_userを使うと安定する(冪等性担保) -> (known after apply) は「どっちかわからない」というよりは「(新規ユーザー向けには)アタッチされる」「(既存ユーザー向けには)外れる可能性があるかも」という判断をしやすくなる、と言った感じですかね。

現時点では、plan結果を信じきれないので、何かしらチェック機構が必要かと思います。

Query History で犯人を特定する

Terraform が裏でどんな SQL を投げているのか、Snowflake の Query History から抜き出します。

snow sql --format json -q "
SELECT
    TO_CHAR(START_TIME, 'YYYY-MM-DD HH24:MI:SS.FF3') AS START_TIME,
    QUERY_TEXT
FROM TABLE(SNOWFLAKE.INFORMATION_SCHEMA.QUERY_HISTORY(RESULT_LIMIT => 10000))
WHERE QUERY_TEXT ILIKE '%ALTER USER%TEST_USER%'
  AND QUERY_TEXT ILIKE '%NETWORK_POLICY%'
ORDER BY START_TIME DESC
LIMIT 10;
"

出てきたのがこれです。

| 02:55:26.400 | ALTER USER "TEST_USER" SET NETWORK_POLICY = "TEST_POLICY"
|              | --terraform_provider_usage_tracking
|              | {"resource":"snowflake_network_policy_attachment","operation":"update"}
|
| 02:52:48.885 | ALTER USER "TEST_USER" UNSET NETWORK_POLICY
|              | --terraform_provider_usage_tracking
|              | {"resource":"snowflake_user","operation":"update"}
|
| 02:49:12.342 | ALTER USER "TEST_USER" SET NETWORK_POLICY = "TEST_POLICY"
|              | --terraform_provider_usage_tracking
|              | {"resource":"snowflake_network_policy_attachment","operation":"create"}
|
| 02:49:10.523 | ALTER USER "TEST_USER" UNSET NETWORK_POLICY
|              | --terraform_provider_usage_tracking
|              | {"resource":"snowflake_user","operation":"update"}
|
| 02:45:42.711 | ALTER USER "TEST_USER" SET NETWORK_POLICY = "TEST_POLICY"
|              | --terraform_provider_usage_tracking
|              | {"resource":"snowflake_user","operation":"update"}

注目すべきは --terraform_provider_usage_tracking の部分です。
Snowflake プロバイダーが、SQL の末尾に「どのリソースがこの SQL を発行したか」を JSON でくっつけてくれています。これはありがたい。

時系列で追うと:

  1. 02:45:42 - snowflake_userSET NETWORK_POLICY(Step 2 で直接指定した時の正常な挙動)
  2. 02:49:10 - snowflake_userUNSET NETWORK_POLICY(Step 3 でコメントアウトした結果、「定義にないから外す」と判断)
  3. 02:49:12 - snowflake_network_policy_attachmentSET NETWORK_POLICY(「俺が付けるぞ」と上書き)
  4. 02:52:48 - snowflake_user がまた UNSET NETWORK_POLICY(「まだ付いてる、外す!」)
  5. 02:55:26 - snowflake_network_policy_attachment がまた SET NETWORK_POLICY(「また外れてる、付ける!」)

なんだこりゃ...

なぜこうなるのか

ログを見た限り、おそらくこういう構造ではないかと思います。

snowflake_user リソースは、HCL の定義に network_policy が書かれていない場合、「定義にないなら外れているべき」と判断して UNSET NETWORK_POLICY を発行しているようです。

一方、snowflake_network_policy_attachment リソースは「自分の定義通りにアタッチする」と判断して SET NETWORK_POLICY を発行します。

2つのリソースが同じパラメータを操作しようとしていて、後に実行された方が勝つ、いわゆる後勝ちという状態になっているんじゃないかと。

Terraform はリソース単体の SQL 成功を見て Apply complete を返すだけなので、最終的な実体がどちらになったかは関知しません。

プロバイダーの実装を読んだわけではないので断言はできませんが、Query History のログからはそう推測しました。

apply 後の実体確認を自動化する

ここまでの検証は毎回手動で snow sql を叩いていましたが、これを自動化します。

ローカルの時刻と Snowflake の時刻はズレがあるので、Snowflake 側の時刻を JST で取得してマーカーにします。
apply 前にマーカーを取得し、apply 後にそのマーカー以降の ALTER USER を引っ張ることで、「今の apply で何が起きたか」だけをピンポイントで特定できます。

verify_apply.shという名前でシェルを作ってみます。

#!/bin/bash

# --- 1. JST基準時刻の同期 ---
START_MARKER=$(snow sql -c trial01 --format json -q "ALTER SESSION SET TIMEZONE = 'Asia/Tokyo'; SELECT CURRENT_TIMESTAMP()::STRING;" | jq -r '.[1][0]."CURRENT_TIMESTAMP()::STRING"')

if [ -z "$START_MARKER" ] || [ "$START_MARKER" == "null" ]; then
    echo "🚨 Error: 時刻同期に失敗しました。"
    exit 1
fi

echo "======================================================================"
echo "🕒 監査開始時刻 (JST): $START_MARKER"
echo "======================================================================"

# --- 2. Terraform Apply 実行 (ログ全量表示) ---
echo ""
echo "--- [STEP 1] Terraform Apply 実行 ---"
terraform apply -auto-approve

# --- 3. 履歴の取得と解析 ---
echo ""
echo "--- [STEP 2] 実行履歴の解析 (JST) ---"

RAW_HISTORY=$(snow sql -c trial01 --format json -q "
ALTER SESSION SET TIMEZONE = 'Asia/Tokyo';
SELECT
    TO_CHAR(START_TIME, 'HH24:MI:SS.FF3') AS TIME_JST,
    QUERY_TEXT
FROM TABLE(SNOWFLAKE.INFORMATION_SCHEMA.QUERY_HISTORY(RESULT_LIMIT => 100))
WHERE START_TIME >= '$START_MARKER'::TIMESTAMP_LTZ
  AND QUERY_TEXT ILIKE 'ALTER USER %'
  AND QUERY_TEXT NOT ILIKE '%QUERY_HISTORY%'
ORDER BY START_TIME ASC;
")

TARGET_USERS=$(echo "$RAW_HISTORY" | jq -r '.[1][]? | .QUERY_TEXT' | sed -E 's/ALTER USER "?([^" ]+)"?.*/\1/' | sort | uniq)

if [ -z "$TARGET_USERS" ]; then
    echo "⚠️  情報: この実行で対象となったユーザー操作は見つかりませんでした。"
    exit 0
fi

# --- 4. ユーザーごとの詳細レポート ---
for USER in $TARGET_USERS; do
    echo ""
    echo "----------------------------------------------------------------------"
    echo "👤 対象ユーザー: $USER"

    # 現時点のパラメータ状態を取得
    PARAM_SHOW=$(snow sql -c trial01 --format json -q "SHOW PARAMETERS LIKE 'NETWORK_POLICY' IN USER $USER;")
    CURRENT_VAL=$(echo "$PARAM_SHOW" | jq -r '.[0].value // empty')

    # --- 判定と警告 ---
    if [ -z "$CURRENT_VAL" ] || [ "$CURRENT_VAL" == "null" ]; then
        echo -e "\033[0;31m🚨 最終ステータス: [ ERROR / DETACHED ]\033[0m"
        echo "   ⚠️  警告: ネットワークポリシーが未設定(空)です。セキュリティリスクがあります。"
    else
        echo -e "\033[0;32m✅ 最終ステータス: [ OK / ATTACHED ]\033[0m"
        echo "   値: $CURRENT_VAL"
    fi

    echo ""
    echo "📝 実行されたSQL履歴 (JST時刻順):"
    # UNSETが含まれる場合は赤字で表示
    echo "$RAW_HISTORY" | jq -r ".[1][] | select(.QUERY_TEXT | contains(\"$USER\")) | \"[\\(.TIME_JST)] \\(.QUERY_TEXT)\"" | while read -r log; do
        if [[ $log == *"UNSET"* ]]; then
            echo -e "\033[0;31m$log\033[0m"
        else
            echo "$log"
        fi
    done
done

echo "======================================================================"

実行してみる

パターンBの構成のまま、このスクリプトを2回連続で実行します。

1回目の実行:ポリシーが外れるパターン

監査開始時刻 (JST): 2026-03-29 12:43:40...
--- [STEP 1] Terraform Apply 実行 ---
Apply complete! Resources: 0 added, 1 changed, 0 destroyed.
--- [STEP 2] 実行履歴の解析 (JST) ---
----------------------------------------------------------------------
対象ユーザー: TEST_USER
最終ステータス: [ ERROR / DETACHED ]
  警告: ネットワークポリシーが未設定です。セキュリティリスクがあります。

実行されたSQL履歴 (JST時刻順):
[12:43:41.523] ALTER USER "TEST_USER" UNSET NETWORK_POLICY --terraform_provider_usage_tracking {"resource":"snowflake_user","operation":"update"}
======================================================================

ちなみに、カラーコードを入れたので、実際はこんな感じで色がつきます。

image.png

Apply complete! なのに ERROR / DETACHED です。
犯人は "resource":"snowflake_user" による UNSET

2回目の実行:ポリシーが付くパターン

監査開始時刻 (JST): 2026-03-29 12:45:12...
--- [STEP 1] Terraform Apply 実行 ---
Apply complete! Resources: 0 added, 1 changed, 0 destroyed.
--- [STEP 2] 実行履歴の解析 (JST) ---
----------------------------------------------------------------------
対象ユーザー: TEST_USER
最終ステータス: [ OK / ATTACHED ] (TEST_POLICY)

実行されたSQL履歴 (JST時刻順):
[12:45:13.400] ALTER USER "TEST_USER" SET NETWORK_POLICY = "TEST_POLICY" --terraform_provider_usage_tracking {"resource":"snowflake_network_policy_attachment","operation":"update"}
======================================================================

同じコードなのに、apply のたびに結果が変わる。
Terraform の Apply complete だけ見ていたら、絶対に気づけません。

ポイント

ざっくりこんな感じ。

  • 最新のTerraform Snowflake Providerでは、snowflake_user リソース内で network_policy を直接指定するのが正解
  • 一方で、現時点では snowflake_network_policy_attachment リソースで分離管理すると危険
  • 「ユーザー定義」と「ネットワークポリシーの適用」を分けて管理したいケースもあるが、それだと「apply 成功なのにポリシーが外れる」状態が発生する
  • Snowflake の Query History を見ると、Terraform が裏で投げている SQL に犯人の名前が書いてある

おわりに

とりあえず、ユーザー個別のネットワークポリシーについては snowflake_user 内で network_policy を直接指定してください、ですかね。

Apply complete! は「各リソースの SQL が成功した」という意味であって、「意図した最終状態になった」ことの保証ではない、と。

セキュリティ設定については、apply 後に実体を確認する仕組みを入れておくことをおすすめします。

Terraform で「なんか勝手に設定が戻る」みたいな怪奇現象が起きた時は、Query History を引いてみると犯人が分かります。

これはSnowflakeの話でしたが、AWSだとCloudTrailで同様の調査ができますね。

参考になれば幸いです。

以上です。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?