外部ステージへの AWS PrivateLink を使ったアウトバウンド接続を試してみた #SnowflakeDB

外部ステージへの AWS PrivateLink を使ったアウトバウンド接続を試してみた #SnowflakeDB

Clock Icon2025.02.09

はじめに

2025年1月のアップデートで Snowflake アカウントからのアウトバウンド プライベート接続時の外部ステージと外部ボリュームのサポートが一般提供となりました。

外部ステージについて本機能を試してみましたので、本記事で内容をまとめてみます。

https://docs.snowflake.com/en/release-notes/2025/9_01#outbound-private-connectivity-for-snowflake-features

アップデートの概要

Snowflake では以前より各クラウドサービスとのプライベート接続機能を提供していましたが、この機能は、インバウンド アクセスに対応していました。

2024年10月のアップデートで Snowflake アカウントからの送信接続(アウトバウンド接続)にも PrivateLink のサポートが拡張され、今回のアップデートで外部ステージ・外部ボリューム利用時にもこのオプションを利用できるようになりました。

これにより、COPY コマンドによるバルクロードや外部ステージへのアンロード時に PrivateLink を使用した通信が可能となります。
アウトバウンド プライベート接続については以下でも紹介しているので、あわせてご参照ください。

https://dev.classmethod.jp/articles/aws-privatelink-snowpark-external-network-access-try-snowflakedb/

前提条件

本機能の使用には Business Ciritical 以上のエディションが必要です。現時点ではトライアルアカウントでも利用できません。
以下の環境で検証しています。

  • Snowflake
    • Business Ciritical
    • クラウドリージョン:AWS_AP_NORTHEAST_1
  • AWS
    • Snowflake アカウントと同一のリージョンに S3 バケットを作成

検証手順

手順は以下に記載があるので、こちらに沿って進めました。上記のリージョンに S3 バケットは作成済みであるとします。

https://docs.snowflake.com/en/user-guide/data-load-aws-private#configure-external-stage-access

プライベートリンク エンドポイントをプロビジョニング

アウトバウンド プライベート接続のためには、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>"
                }
            }
        }
    ]
}

こちらのポリシー設定は以下の記事を参考とさせていただきました。

https://dev.classmethod.jp/articles/restricting-access-only-via-vpc-endpoint-in-s3/

IAMロールを作成

Snowflake から外部ステージにアクセスする際は、ストレージ統合オブジェクトを使用するロール引き受けが推奨されています。以下の手順に沿って Snowflake 側の IAM ユーザーが引き受けることになる IAM ロールを作成します。

https://docs.snowflake.com/ja/user-guide/data-load-s3-config-storage-integration

この際、以下の 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_ARNSTORAGE_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_ENDPOINTTRUEとしている場合、ステージのパラメータとしての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 などのユースケースをよりセキュアに構成したい、など組織によってはセキュリティ要件上求められる機能と思います。
こちらの内容が何かの参考になれば幸いです。

参考

https://dev.classmethod.jp/articles/amazon-s3-private-connectivity-on-premises-networks/

https://dev.classmethod.jp/articles/access-priority-vpcendpoint-natgateway/

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.