はじめに
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;
MONITOR と IMPORTED 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_user の network_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_user の network_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_user の network_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 でくっつけてくれています。これはありがたい。
時系列で追うと:
-
02:45:42 -
snowflake_userがSET NETWORK_POLICY(Step 2 で直接指定した時の正常な挙動) -
02:49:10 -
snowflake_userがUNSET NETWORK_POLICY(Step 3 でコメントアウトした結果、「定義にないから外す」と判断) -
02:49:12 -
snowflake_network_policy_attachmentがSET NETWORK_POLICY(「俺が付けるぞ」と上書き) -
02:52:48 -
snowflake_userがまたUNSET NETWORK_POLICY(「まだ付いてる、外す!」) -
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"}
======================================================================
ちなみに、カラーコードを入れたので、実際はこんな感じで色がつきます。
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で同様の調査ができますね。
参考になれば幸いです。
以上です。
