SnowflakeからIcebergテーブルをS3に書き出したい
今回は、Snowflake-managedではない、S3にデータの実態をおく形でicebergテーブルを作成し、その際の手順を共有します。
記事3行概要
- Snowflakeで自前S3にIcebergを書き出すには、「外部ステージ」ではなく外部ボリューム(External Volume)という別の仕組みを使うよ!
- AWS側でIAMポリシーとIAMロールを用意して、Snowflake側で外部ボリュームを作り、信頼ポリシーを繋ぎ直す、という往復作業が必要だよ!
- 繋がってしまえば、書き出し自体は
CREATE ICEBERG TABLE ... AS SELECTの一発で、ファイル(Parquet+メタデータ)が自分のS3に並ぶよ!
外部ステージと外部ボリュームの違い
S3からcopy intoでsnowflakeにテーブルファイルを書き込む際、外部ステージを使うことがありますが、今回は外部ボリュームという別のものを使用します。
外部ステージ
名前付き外部ステージは、スキーマで作成されたデータベースオブジェクトです。このオブジェクトは、クラウドストレージのファイルへの URL、クラウドストレージアカウントへのアクセスに使用される設定、およびステージングされたファイルの形式を説明するオプションなどの便利な設定を格納します。https://docs.snowflake.com/ja/user-guide/data-load-overview#label-data-load-overview-external-stages
外部ボリューム
外部ボリュームは名前付きのアカウントレベルのSnowflakeオブジェクトで、SnowflakeをIcebergテーブル用の外部クラウドストレージに接続するために使用します。https://docs.snowflake.com/ja/user-guide/tables-iceberg-storage
外部ボリュームはiceberg専用であり、アカウントレベルだが、外部ステージはiceberg専用ではなく、スキーマレベル、と言った違いがあるようです。
いまいちすっきりしませんが、今回は外部ボリュームを使用します。
やること(全体像)
手順は大きく9ステップです。手順書はこれ
AWSとSnowflakeを行ったり来たりします。
- AWS: S3バケットを作成する
- AWS: S3にアクセスできるIAMポリシーを作る
- AWS: そのポリシーを付けたIAMロールを作る(External IDを設定)
- Snowflake: 外部ボリュームを作る(
CREATE EXTERNAL VOLUME) - Snowflake: 外部ボリュームに割り当てられたSnowflake側のIAMユーザーARNを取得する(
DESC EXTERNAL VOLUME) - AWS: IAMロールの信頼ポリシーを、5で取得した値で更新する
- Snowflake: 接続できるか確認する(
SYSTEM$VERIFY_EXTERNAL_VOLUME) - Snowflake: Icebergテーブルを書き出す(
CREATE ICEBERG TABLE ... AS SELECT) - S3: 書き出されたファイル(
data/とmetadata/)を確認する
「ロールを作る → Snowflakeに見せる → Snowflakeが教えてくれたユーザーを信頼するようロールを直す」という往復が一見ややこしいですが、要はお互いを指し合う設定を作っているだけです。
先に押さえる前提
- 同一リージョンであること。 Snowflakeをカタログにする場合、クロスクラウド/クロスリージョンは未対応です。S3バケットは、Snowflakeアカウントと同じリージョンに置いてください。
-
バケット名にドット(
.)を含めない。 Snowflakeはバケット名にドットを含むS3外部ボリュームをサポートしません(my.s3.bucketのような名前はNG)。 -
書き込み許可(
ALLOW_WRITES = TRUE)が必須。 Snowflakeをカタログにするicebergテーブルは、外部ボリュームが書き込み可能でないと作れません。 -
権限。 外部ボリュームの作成にはアカウントレベルの
CREATE EXTERNAL VOLUME、テーブル作成にはスキーマに対するCREATE ICEBERG TABLEが必要です。
手順
Step 1.S3バケットを作成
S3バケットを作成します。設定は以下の通りで、名前だけわかりやすいように、iceberg-testのprefixをつけるようにします。
設定は以下の通り
設定項目一覧
- バケットタイプ
- 汎用
- バケット名前空間
- アカウントのリージョナル名前空間
- バケット名
- iceberg-test + アカウントリージョナル名前空間の接尾辞
- オブジェクト所有者
- ACL無効
- バケットのブロックパブリックアクセス設定
- パブリックアクセスを全てブロック
- バケットのバージョニング
- 無効
- オプション
- なし
- デフォルトの暗号化
- Amazon S3マネージドキーを使用したサーバー側の暗号化(SSE-S3)
- バケットキー
- 有効にする
- オブジェクトロック
- 無効にする
Step 2. IAMポリシーを作る(AWS)
AWSマネジメントコンソールで IAM → ポリシー → 「ポリシーの作成」と進み、JSONで以下を貼ります。<my_bucket> は自分のバケット名に置き換えてください。
IAMポリシーに貼り付けるJSON
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:PutObject",
"s3:GetObject",
"s3:GetObjectVersion",
"s3:DeleteObject",
"s3:DeleteObjectVersion"
],
"Resource": "arn:aws:s3:::<my_bucket>/*"
},
{
"Effect": "Allow",
"Action": [
"s3:ListBucket",
"s3:GetBucketLocation"
],
"Resource": "arn:aws:s3:::<my_bucket>",
"Condition": {
"StringLike": {
"s3:prefix": ["*"]
}
}
}
]
}
書き出し(Snowflakeカタログ)が目的なので、s3:PutObject や s3:DeleteObject などの書き込み系アクションを残しておくのがポイントです。読み取り専用にすると後で書き出しに失敗します。
Step 3. IAMロールを作る(AWS)
IAM → ロール → 「ロールを作成」より、ロール作成後、ロール自体のARN(Amazon リソースネーム)を記録してください。
- 信頼されたエンティティ: AWSアカウント → このアカウント を選ぶ(信頼ポリシーはStep 6で繋ぎ直すので、いったんこれでOK)
-
「外部IDを要求する」にチェックを入れ、任意の外部ID(例:
iceberg_table_external_id)を入力する - Step 2で作ったポリシーをアタッチ
- ロール名を付けて作成
- 作成後、ロールの ARN(
arn:aws:iam::123456789012:role/...)を控えておく
Step 3.5. SSE-KMS 暗号化に必要な権限を IAM ロールに付与する(今回は不要)
今回は、S3のバケット暗号化を、デフォルトの暗号化(Amazon S3マネージドキーを使用したサーバー側の暗号化(SSE-S3))にしているため、対応は不要ですが、KMSキーを使う場合は、IAMロールに、キーに対する復号権限が必要であり、そのセットアップが必要となります。
S3の暗号化方式に注意してください。
確認する場合は、S3→バケット名→プロパティ→デフォルトの暗号化から確認可能です。

Step 4. 外部ボリュームを作る(Snowflake)
ACCOUNTADMIN(または CREATE EXTERNAL VOLUME を持つロール)で実行します。
CREATE OR REPLACE EXTERNAL VOLUME iceberg_s3_vol
STORAGE_LOCATIONS =
(
(
NAME = 'my-s3-ap-northeast-1'
-- アカウント内部で一意であればOK。ユーザーが自由に決める
STORAGE_PROVIDER = 'S3'
STORAGE_BASE_URL = 's3://<my_bucket>/'
STORAGE_AWS_ROLE_ARN = '<Step3で控えたロールARN>'
STORAGE_AWS_EXTERNAL_ID = 'iceberg_table_external_id' -- Step3で入れた外部ID
)
)
ALLOW_WRITES = TRUE; -- Snowflakeカタログのicebergテーブルには必須
STORAGE_AWS_EXTERNAL_ID を自分で指定しておくと、CREATE OR REPLACE で作り直しても外部IDが変わらず、信頼ポリシーを貼り直す手間が減ります。(指定しないとSnowflakeが毎回新しいIDを振るので注意)
Step 5. Snowflake側のIAMユーザーARNを取得する(Snowflake)
外部ボリュームには、Snowflakeアカウント用のIAMユーザーが割り当てられます。DESC でその値を確認します。
DESC EXTERNAL VOLUME iceberg_s3_vol;
この2行目のproperty_nameには、以下のようなjsonが入っているので、ここから、以下の2つを控えます。
-
STORAGE_AWS_IAM_USER_ARN(例:arn:aws:iam::123456789001:user/abc1-b-self1234) -
STORAGE_AWS_EXTERNAL_ID(Step 3で自分で指定した値になっているはず)
property_nameのjson
{
"NAME”:”作成した外部ボリューム名”,
"STORAGE_PROVIDER":"S3",
”STORAGE_BASE_URL”:"s3://<my_bucket>/",
”STORAGE_ALLOWED_LOCATIONS":["s3://<my_bucket>/*”],
”STORAGE_AWS_ROLE_ARN”:”<step3のrole arn>,
”STORAGE_AWS_IAM_USER_ARN”:”<なんかのarn>,
”STORAGE_AWS_EXTERNAL_ID”:”<指定した外部ID>,
”ENCRYPTION_TYPE”:”暗号化タイプ”,
”ENCRYPTION_KMS_KEY_ID":""
}
Step 6. ロールの信頼ポリシーを更新する(AWS)
IAM → ロール → Step 3で作ったロール → 信頼関係 タブ → 「信頼ポリシーを編集」。
内容を以下に置き換えます。<STORAGE_AWS_IAM_USER_ARN> と <STORAGE_AWS_EXTERNAL_ID> は、Step 5で控えた値です。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "<snowflakeのSTORAGE_AWS_IAM_USER_ARN>"
},
"Action": "sts:AssumeRole",
"Condition": {
"StringEquals": {
"sts:ExternalId": "<snowflakeのSTORAGE_AWS_EXTERNAL_ID>"
}
}
}
]
}
これで「Snowflakeのこのユーザーが、この外部IDを添えてきたらロールを引き受けてよい」という関係になります。
Step 7. 接続確認(Snowflake)
設定が噛み合っているかを、専用の関数で確認します。
SELECT SYSTEM$VERIFY_EXTERNAL_VOLUME('<作成した外部ボリューム名>');
成功すればOK。
Step 8. Icebergテーブルを書き出す(Snowflake)
ここまで来れば、あとはCTASで一発です。題材は前回も使った大福帳 BI_OBT_LINEORDER_LIMITED(50万レコード)をそのまま使います。
CREATE OR REPLACE ICEBERG TABLE BI_OBT_LINEORDER_LIMITED_ICE_S3
CATALOG = 'SNOWFLAKE' -- カタログはSnowflake管理のまま
EXTERNAL_VOLUME = 'iceberg_s3_vol' -- 置き場所だけ自前のS3
BASE_LOCATION = 'bi_obt_lineorder_limited_ice_s3' -- バケット内のパス
ICEBERG_VERSION = 3 -- icebergテーブルのバージョン
AS SELECT * FROM BI_OBT_LINEORDER_LIMITED; -- AS SELECT は必ず文末
-- 6秒程度で書き出し
行数とDDLを確認しておきます。
SELECT COUNT(*) FROM BI_OBT_LINEORDER_LIMITED_ICE_S3;
-- 500000 になっていればOK
SELECT GET_DDL('ICEBERG_TABLE', 'BI_OBT_LINEORDER_LIMITED_ICE_S3');
-- EXTERNAL_VOLUME = 'ICEBERG_S3_VOL' が効いているのを確認
DDLの中身
create or replace ICEBERG TABLE BI_OBT_LINEORDER_LIMITED_ICE_V3_S3 (
LO_ORDERKEY NUMBER(38,0),
LO_LINENUMBER NUMBER(38,0),
LO_CUSTKEY NUMBER(38,0),
LO_PARTKEY NUMBER(38,0),
LO_SUPPKEY NUMBER(38,0),
LO_ORDERDATEKEY NUMBER(38,0),
LO_QUANTITY NUMBER(12,2),
LO_EXTENDEDPRICE NUMBER(12,2),
LO_DISCOUNT NUMBER(12,2),
LO_REVENUE NUMBER(25,4),
LO_TAX NUMBER(12,2),
LO_RETURNFLAG VARCHAR(134217728),
LO_LINESTATUS VARCHAR(134217728),
LO_SHIPMODE VARCHAR(134217728),
C_NAME VARCHAR(134217728),
C_MKTSEGMENT VARCHAR(134217728),
C_NATION VARCHAR(134217728),
C_REGION VARCHAR(134217728),
P_NAME VARCHAR(134217728),
P_MFGR VARCHAR(134217728),
P_BRAND VARCHAR(134217728),
P_TYPE VARCHAR(134217728),
P_SIZE NUMBER(38,0),
S_NAME VARCHAR(134217728),
S_NATION VARCHAR(134217728),
S_REGION VARCHAR(134217728),
D_YEAR NUMBER(4,0),
D_YEARMONTHNUM NUMBER(38,0),
D_DAYOFWEEK VARCHAR(134217728)
)
EXTERNAL_VOLUME = '<作成した外部ボリューム名>’
ICEBERG_VERSION = 3
CATALOG = 'SNOWFLAKE'
BASE_LOCATION = 'bi_obt_lineorder_limited_ice_v3_s3/';
Step 9. S3を見にいく
s3://<my_bucket>/bi_obt_lineorder_limited_ice_s3/ の配下に、data/(Parquet)と metadata/(Icebergのメタデータ)が出来ていました。
結論
自前S3への書き出しは、「外部ボリュームを作る」「AWSロールとSnowflakeユーザーを相互に指し合う」という設定さえ通れば、テーブルの書き出し自体はSnowflake管理のときと同じくCTAS一発です。山場はSQLよりも、AWS側のIAM周りの往復にあります。
記事3行概要(再掲)
- Snowflakeで自前S3にIcebergを書き出すには、「外部ステージ」ではなく外部ボリューム(External Volume)という別の仕組みを使うよ!
- AWS側でIAMポリシーとIAMロールを用意して、Snowflake側で外部ボリュームを作り、信頼ポリシーを繋ぎ直す、という往復作業が必要だよ!
- 繋がってしまえば、書き出し自体は
CREATE ICEBERG TABLE ... AS SELECTの一発で、ファイル(Parquet+メタデータ)が自分のS3に並ぶよ!
参考情報
- Configure an external volume for Amazon S3 - Snowflake Documentation
- CREATE EXTERNAL VOLUME - Snowflake Documentation
- SYSTEM$VERIFY_EXTERNAL_VOLUME - Snowflake Documentation
- CREATE ICEBERG TABLE (Snowflake as the catalog) - Snowflake Documentation
- Storage for Apache Iceberg™ tables - Snowflake Documentation
- データ読み込みの概要(外部ステージ) - Snowflake Documentation






