外部ステージへの AWS PrivateLink を使ったアウトバウンド接続を試してみた #SnowflakeDB
はじめに
2025年1月のアップデートで Snowflake アカウントからのアウトバウンド プライベート接続時の外部ステージと外部ボリュームのサポートが一般提供となりました。
外部ステージについて本機能を試してみましたので、本記事で内容をまとめてみます。
アップデートの概要
Snowflake では以前より各クラウドサービスとのプライベート接続機能を提供していましたが、この機能は、インバウンド アクセスに対応していました。
2024年10月のアップデートで Snowflake アカウントからの送信接続(アウトバウンド接続)にも PrivateLink のサポートが拡張され、今回のアップデートで外部ステージ・外部ボリューム利用時にもこのオプションを利用できるようになりました。
これにより、COPY コマンドによるバルクロードや外部ステージへのアンロード時に PrivateLink を使用した通信が可能となります。
アウトバウンド プライベート接続については以下でも紹介しているので、あわせてご参照ください。
前提条件
本機能の使用には Business Ciritical 以上のエディションが必要です。現時点ではトライアルアカウントでも利用できません。
以下の環境で検証しています。
- Snowflake
- Business Ciritical
- クラウドリージョン:AWS_AP_NORTHEAST_1
- AWS
- Snowflake アカウントと同一のリージョンに S3 バケットを作成
検証手順
手順は以下に記載があるので、こちらに沿って進めました。上記のリージョンに S3 バケットは作成済みであるとします。
プライベートリンク エンドポイントをプロビジョニング
アウトバウンド プライベート接続のためには、Snowflake 側でプライベートリンク エンドポイントをプロビジョニングする必要があります。
USE ROLE ACCOUNTADMIN;
SELECT SYSTEM$PROVISION_PRIVATELINK_ENDPOINT(
'com.amazonaws.ap-northeast-1.s3',
'*.s3.ap-northeast-1.amazonaws.com');
エンドポイントが利用可能になるまでしばらく待機後 SYSTEM$GET_PRIVATELINK_ENDPOINTS_INFO を実行し、プロビジョニングされたエンドポイントの ID(snowflake_endpoint_name
の値)を取得します。
SELECT SYSTEM$GET_PRIVATELINK_ENDPOINTS_INFO();
バケットポリシーを作成
今回はプライベート接続経由でのみアクセスできることを検証したかったので、以下のバケットポリシーを設定しました。
マネジメントコンソールからのアクセス用にリクエスト元の IAM ロール Arn も設定しています。
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "Access-to-specific-VPCE-only",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::<バケット名>",
"arn:aws:s3:::<バケット名>/*"
],
"Condition": {
"StringNotEquals": {
"aws:SourceVpce": "<snowflake_endpoint_nameの値>",
"aws:PrincipalArn": "<リクエスト元のIAMロールArn>"
}
}
}
]
}
こちらのポリシー設定は以下の記事を参考とさせていただきました。
IAMロールを作成
Snowflake から外部ステージにアクセスする際は、ストレージ統合オブジェクトを使用するロール引き受けが推奨されています。以下の手順に沿って Snowflake 側の IAM ユーザーが引き受けることになる IAM ロールを作成します。
この際、以下の IAM ポリシーを関連付けました。
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VisualEditor0",
"Effect": "Allow",
"Action": [
"s3:ListBucket",
"s3:GetObject",
"s3:GetBucketLocation",
"s3:GetObjectVersion",
"s3:DeleteObject",
"s3:PutObject"
],
"Resource": [
"arn:aws:s3:::<バケット名>",
"arn:aws:s3:::<バケット名>/*"
]
}
]
}
ストレージ統合を作成
IAM ロールを作成後、Snowflake 側でストレージ統合を作成します。STORAGE_AWS_ROLE_ARN
には上記の手順で作成したポリシーを関連付けた IAM ロールの Arn を指定します。
ポイントとして、プライベート接続を使用するためにUSE_PRIVATELINK_ENDPOINT
オプションをTRUE
としてオブジェクトを作成します。
--ストレージ統合を作成
CREATE OR REPLACE STORAGE INTEGRATION outbound_private_link_int
TYPE = EXTERNAL_STAGE
STORAGE_PROVIDER = 'S3'
STORAGE_AWS_ROLE_ARN = '<IAMロールのArn>'
STORAGE_ALLOWED_LOCATIONS = ('s3://<バケット名>/')
USE_PRIVATELINK_ENDPOINT = TRUE
ENABLED = TRUE;
ストレージ統合を作成後、以下を実行しSTORAGE_AWS_IAM_USER_ARN
とSTORAGE_AWS_EXTERNAL_ID
の値を取得し、AWS 側で IAM ロールの信頼関係を更新します。
DESC INTEGRATION outbound_private_link_int;
信頼関係
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "<STORAGE_AWS_IAM_USER_ARN>"
},
"Action": "sts:AssumeRole",
"Condition": {
"StringEquals": {
"sts:ExternalId": "<STORAGE_AWS_EXTERNAL_ID>"
}
}
}
]
}
ステージを作成
さいごに任意のスキーマ内に外部ステージを作成します。ストレージ統合オブジェクトでUSE_PRIVATELINK_ENDPOINT
をTRUE
としている場合、ステージのパラメータとしてのUSE_PRIVATELINK_ENDPOINT
にもこの設定が引き継がれます。
--file formatを作成
CREATE OR REPLACE FILE FORMAT my_csv_format
TYPE = CSV
FIELD_DELIMITER = ','
SKIP_HEADER = 1
EMPTY_FIELD_AS_NULL = true
COMPRESSION = AUTO;
--外部ステージを作成
CREATE OR REPLACE STAGE my_storage_private_stage
URL = 's3://<バケット名>/'
STORAGE_INTEGRATION = outbound_private_link_int
FILE_FORMAT = my_csv_format;
;
パラメータを確認
DESC STAGE my_storage_private_stage;
>SELECT * FROM table(result_scan(-1)) WHERE "property" like '%PRIVATELINK%';
+-----------------+--------------------------+---------------+----------------+------------------+
| parent_property | property | property_type | property_value | property_default |
|-----------------+--------------------------+---------------+----------------+------------------|
| PRIVATELINK | USE_PRIVATELINK_ENDPOINT | Boolean | true | false |
+-----------------+--------------------------+---------------+----------------+------------------+
1 Row(s) produced. Time Elapsed: 0.691s
バケットにファイルフォーマットに対応した CSV ファイルを配置し、Snowflake で以下を実行します。
>list @my_storage_private_stage;
+---------------------------------------------------+------+----------------------------------+------------------------------+
| name | size | md5 | last_modified |
|---------------------------------------------------+------+----------------------------------+------------------------------|
| s3://yasuhara-sf-privatelink-test/sample_data.csv | 271 | e57095e21f57eb080a7bdb8244b9b768 | Sun, 9 Feb 2025 02:43:21 GMT |
+---------------------------------------------------+------+----------------------------------+------------------------------+
>SELECT t.$1,t.$2 FROM @my_storage_private_stage t;
+----+--------------+
| $1 | $2 |
|----+--------------|
| 1 | 0.121164735 |
| 2 | 0.417581506 |
| 3 | 0.205080272 |
| 4 | 1.689011529 |
| 5 | -1.811709749 |
| 6 | -0.152483105 |
| 7 | -0.976142577 |
| 8 | -0.125357886 |
| 9 | -0.089125088 |
| 10 | -0.00069313 |
+----+--------------+
10 Row(s) produced. Time Elapsed: 0.487s
問題なく参照できています。
続けて COPY コマンドによるバルクロードを試してみます。
--スキーマ検出用のファイルフォーマットを作成
CREATE OR REPLACE FILE FORMAT my_csv_format_infer
TYPE = CSV
FIELD_DELIMITER = ','
PARSE_HEADER = TRUE
EMPTY_FIELD_AS_NULL = true
COMPRESSION = AUTO;
--スキーマ検出機能によりテーブルを定義
CREATE OR REPLACE TABLE mytable
USING TEMPLATE (
SELECT ARRAY_AGG(OBJECT_CONSTRUCT(*))
FROM TABLE(
INFER_SCHEMA(
LOCATION=>'@my_storage_private_stage/',
FILE_FORMAT=>'my_csv_format_infer'
)
)
);
--テーブル定義を確認
DESC TABLE mytable;
--バルクロードを実行
>COPY INTO mytable FROM @my_storage_private_stage/
FILE_FORMAT=(FORMAT_NAME = 'my_csv_format_infer')
MATCH_BY_COLUMN_NAME = CASE_INSENSITIVE ;
+---------------------------------------------------+--------+-------------+-------------+-------------+-------------+-------------+------------------+-----------------------+-------------------------+
| file | status | rows_parsed | rows_loaded | error_limit | errors_seen | first_error | first_error_line | first_error_character | first_error_column_name |
|---------------------------------------------------+--------+-------------+-------------+-------------+-------------+-------------+------------------+-----------------------+-------------------------|
| s3://<バケット名>/sample_data.csv | LOADED | 10 | 10 | 1 | 0 | NULL | NULL | NULL | NULL |
+---------------------------------------------------+--------+-------------+-------------+-------------+-------------+-------------+------------------+-----------------------+-------------------------+
1 Row(s) produced. Time Elapsed: 0.936s
>SELECT * FROM mytable;
+----+--------------+-----------+
| ID | Value | Date |
|----+--------------+-----------|
| 1 | 0.121164735 | 2024/4/20 |
| 2 | 0.417581506 | 2024/4/20 |
| 3 | 0.205080272 | 2024/4/20 |
| 4 | 1.689011529 | 2024/4/20 |
| 5 | -1.811709749 | 2024/4/20 |
| 6 | -0.152483105 | 2024/4/20 |
| 7 | -0.976142577 | 2024/4/20 |
| 8 | -0.125357886 | 2024/4/20 |
| 9 | -0.089125088 | 2024/4/20 |
| 10 | -0.000693130 | 2024/4/20 |
+----+--------------+-----------+
10 Row(s) produced. Time Elapsed: 0.706s
こちらも問題なさそうです。
ストレージ統合オブジェクトの設定を変更
続けて、ストレージ統合オブジェクトの設定を変更し、プライベートエンドポイントを使用しない場合、アクセスが拒否されるか確認してみます。
--統合オブジェクトの設定を変更
ALTER STORAGE INTEGRATION outbound_private_link_int
SET USE_PRIVATELINK_ENDPOINT = FALSE;
ステージの設定を確認するとこちらにも自動的に変更が反映されているようでした。
DESC STAGE MY_STORAGE_PRIVATE_STAGE;
>SELECT * FROM table(result_scan(-1)) WHERE "property" like '%PRIVATELINK%';
+-----------------+--------------------------+---------------+----------------+------------------+
| parent_property | property | property_type | property_value | property_default |
|-----------------+--------------------------+---------------+----------------+------------------|
| PRIVATELINK | USE_PRIVATELINK_ENDPOINT | Boolean | false | false |
+-----------------+--------------------------+---------------+----------------+------------------+
1 Row(s) produced. Time Elapsed: 0.762s
この状態で再度上記と同じクエリを実行すると以下のようにアクセスが拒否されます。以下ではアンロードを試していますが COPY コマンドも同じようにエラーとなります。
>list @my_storage_private_stage;
091003 (22000): Failure using stage area. Cause: [User: <ロール> is not authorized to perform: s3:ListBucket on resource: "arn:aws:s3:::<バケット名>" with an explicit deny in a resource-based policy (Status Code: 403; Error Code: AccessDenied)]
>SELECT t.$1,t.$2 FROM @my_storage_private_stage t;
091003 (22000): Failure using stage area. Cause: [User: <ロール> is not authorized to perform: s3:ListBucket on resource: "arn:aws:s3:::<バケット名>" with an explicit deny in a resource-based policy (Status Code: 403; Error Code: AccessDenied)]
--アンロード
>COPY INTO @my_storage_private_stage/unload/mytable.csv FROM mytable
FILE_FORMAT = (FORMAT_NAME ='my_csv_format' COMPRESSION='NONE')
HEADER = TRUE
SINGLE = TRUE ;
100089 (42501): Failed to access remote file: access denied. Please check your credentials
再度プライベートエンドポイントを使用する設定に変更します。
ALTER STORAGE INTEGRATION outbound_private_link_int
SET USE_PRIVATELINK_ENDPOINT = TRUE;
こうすることで、プライベートエンドポイントを経由し、アンロードなど各種コマンドが実行できるようになります。
>COPY INTO @my_storage_private_stage/unload/mytable.csv FROM mytable
FILE_FORMAT = (FORMAT_NAME ='my_csv_format' COMPRESSION='NONE')
HEADER = TRUE
SINGLE = TRUE ;
+---------------+-------------+--------------+
| rows_unloaded | input_bytes | output_bytes |
|---------------+-------------+--------------|
| 10 | 261 | 261 |
+---------------+-------------+--------------+
1 Row(s) produced. Time Elapsed: 1.855s
>list @my_storage_private_stage;
+------------------------------------------------------+------+----------------------------------+------------------------------+
| name | size | md5 | last_modified |
|------------------------------------------------------+------+----------------------------------+------------------------------|
| s3://<バケット名>/sample_data.csv | 271 | e57095e21f57eb080a7bdb8244b9b768 | Sun, 9 Feb 2025 02:43:21 GMT |
| s3://<バケット名>/unload/mytable.csv | 261 | cb6485a3c83e2d4d6a449eb48becc9b2 | Sun, 9 Feb 2025 05:12:08 GMT |
+------------------------------------------------------+------+----------------------------------+------------------------------+
2 Row(s) produced. Time Elapsed: 1.665s
さいごに
Snowflake アカウントからのアウトバウンド プライベート接続を外部ステージに対して試してみました。アンロード時もプライベートエンドポイントを経由することになるので、データロードやリバース ETL などのユースケースをよりセキュアに構成したい、など組織によってはセキュリティ要件上求められる機能と思います。
こちらの内容が何かの参考になれば幸いです。
参考