この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
こんにちは!DA(データアナリティクス)事業本部 サービスソリューション部の大高です。
みなさん、Snowflakeの外部ステージ(S3)、使っていますか?私は使っています。
SnowflakeからS3にアクセスするためには、以下のドキュメントやエントリに記載のとおり、「ストレージ統合」を作成して、SnowflakeとAWSをいったりきたりしながら設定をする必要があるわけですが、正直なところちょっと大変です。
弊社プロダクトのデータ分析基盤「カスタマーストーリー アナリティクス」においては、この設定の自動化に対応しましたが、プロダクトを利用しない場合にはやらざるを得ないので少し困っていました。
そこで、今回はこれを解消するべく、CloudFormationテンプレートを作成してみたのでご紹介します。
前提
「外部ステージに利用するS3バケットは、AWS上に作成済みであること」を前提とします。
やってみた
では、さっそくやってみましょう。
大きな流れとしては、以下の流れになります。
- (Snowflake) 仮の「ストレージ統合」を作成する
- (AWS) CloudFormationを利用してサクッと設定を実施する
- (Snowflake) 「ストレージ統合」を修正する
- (Snowflake) 「外部ステージ」を作成する
公式ドキュメントにおける「ステップ1, 2, 5」をCloudFormationを利用してやってしまおう、という発想です。
(Snowflake) 仮の「ストレージ統合」を作成する
まずは仮の「ストレージ統合」を以下のSQLで作成します。
仮の「ストレージ統合」作成クエリ
CREATE OR REPLACE STORAGE INTEGRATION <integration_name>
TYPE = EXTERNAL_STAGE
STORAGE_PROVIDER = S3
ENABLED = TRUE
STORAGE_AWS_ROLE_ARN = 'arn:aws:iam::0:role/'
STORAGE_ALLOWED_LOCATIONS = ('s3://')
;
<integration_name>
はootaka_devio_interation
など、任意の名前に変更してください。
ここでの一番のポイントは、STORAGE_AWS_ROLE_ARN
の値です。公式手順では先にAWS上でIAM Roleを作成してから、ここに値の設定を行うのですが、そうするとAWS側といったりきたりしないといけないので、ダミーの値を設定することで手数を減らしています。
「ストレージ統合」を作成したら、DESC INTEGRATION
コマンドを実行してSTORAGE_AWS_IAM_USER_ARN
とSTORAGE_AWS_EXTERNAL_ID
の値を手に入れます。この値は後でCloudFormationのパラメータとして利用します。
「ストレージ統合」情報取得クエリ
DESC INTEGRATION <integration_name>;
(AWS) CloudFormationを利用してサクッと設定を実施する
STORAGE_AWS_IAM_USER_ARN
とSTORAGE_AWS_EXTERNAL_ID
の値が手に入ったら、AWSの管理コンソールでCloudFormationの画面を開きます。
「スタックの作成」から、下記のテンプレートをファイル保存したものなどを指定してください。なお、このテンプレートではIAMポリシーがインラインポリシーになるのですが、今回はこれは目をつむりました。(公式ドキュメントの手順だと、個別のポリシーとして作成していますね)
setup-external-stage-settings.yml
AWSTemplateFormatVersion: "2010-09-09"
Description: "Snowflake - S3 Secure Access Configuration"
Parameters:
StorageAwsIamUserArn:
Type: String
Description: "[Required] STORAGE_AWS_IAM_USER_ARN value obtained by Snowflake's DESC INTEGRATION command"
# "[必須]SnowflakeのDESC INTEGRATIONコマンドで取得したSTORAGE_AWS_IAM_USER_ARNの値"
StorageAwsExternalId:
Type: String
Description: "[Required] STORAGE_AWS_IAM_EXTERNAL_ID value obtained by Snowflake's DESC INTEGRATION command"
# "[必須]SnowflakeのDESC INTEGRATIONコマンドで取得したSTORAGE_AWS_IAM_EXTERNAL_IDの値"
S3BucketName:
Type: String
Description: "[Required] S3 bucket name to be set on the external stage"
# "[必須]外部ステージに設定するS3バケット名"
S3PathPrefix:
Type: String
Description: "[Optional] S3 path prefix to set on the external stage (ex: snowflake/load/ )"
# "[任意]外部ステージに設定するS3のパスプレフィックス (例: snowflake/load/ )"
ReadOnlyBucket:
Type: String
Description: "[Required] Whether it is a read-only bucket (yes / no)"
# "[必須]読み取り専用バケットか否か(yes/no)"
AllowedValues:
- "yes"
- "no"
Default: "no"
IAMPolicyName:
Type: String
Description: "[Required] IAM policy name for Snowflake. Alphanumeric characters and + =,. @ -_ Can be used. Maximum 128 characters."
# "[必須]Snowflake用IAMポリシー名。英数字と「 +=,.@-_ 」が利用可能。最大 128 文字。"
MaxLength: 128
IAMRoleName:
Type: String
Description: "[Required] IAM role name for Snowflake. Alphanumeric characters and + =,. @ -_ Can be used. Maximum 64 characters."
# "[必須]Snowflake用IAMロール名。英数字と「 +=,.@-_ 」が利用可能。最大 64 文字。"
MaxLength: 64
IAMRoleDescription:
Type: String
Description: "[Optional] IAM role description for Snowflake. Alphanumeric characters and + =,. @ -_ Can be used. Maximum 1000 characters."
# "[任意]Snowflake用IAMロール説明。英数字と「 +=,.@-_ 」が利用可能。最大 1000 文字。"
MaxLength: 1000
Metadata:
AWS::CloudFormation::Interface:
ParameterGroups:
-
Label:
default: "Snowflake Information"
Parameters:
- StorageAwsIamUserArn
- StorageAwsExternalId
-
Label:
default: "S3 Bucket Information"
Parameters:
- S3BucketName
- S3PathPrefix
- ReadOnlyBucket
-
Label:
default: "IAM Information"
Parameters:
- IAMPolicyName
- IAMRoleName
- IAMRoleDescription
Conditions:
IsS3PathPrefixEmpty: !Equals [!Ref "S3PathPrefix", ""]
IsReadOnlyBucket: !Equals [!Ref "ReadOnlyBucket", "yes"]
IsWritableBucket: !Equals [!Ref "ReadOnlyBucket", "no"]
Resources:
SnowflakeReadOnlyIAMPolicy:
Condition: IsReadOnlyBucket
Type: AWS::IAM::Policy
Properties:
PolicyName: !Ref IAMPolicyName
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- s3:GetObject
- s3:GetObjectVersion
Resource: !If
- IsS3PathPrefixEmpty
- !Sub arn:aws:s3:::${S3BucketName}/*
- !Sub arn:aws:s3:::${S3BucketName}/${S3PathPrefix}*
- Effect: Allow
Action:
- s3:ListBucket
- s3:GetBucketLocation
Resource: !Sub arn:aws:s3:::${S3BucketName}
Condition:
StringLike:
s3:prefix: !If
- IsS3PathPrefixEmpty
- '*'
- !Sub ${S3PathPrefix}*
Roles:
- !Ref SnowflakeIAMRole
SnowflakeWritableIAMPolicy:
Condition: IsWritableBucket
Type: AWS::IAM::Policy
Properties:
PolicyName: !Ref IAMPolicyName
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- s3:PutObject
- s3:GetObject
- s3:GetObjectVersion
- s3:DeleteObject
- s3:DeleteObjectVersion
Resource: !If
- IsS3PathPrefixEmpty
- !Sub arn:aws:s3:::${S3BucketName}/*
- !Sub arn:aws:s3:::${S3BucketName}/${S3PathPrefix}*
- Effect: Allow
Action:
- s3:ListBucket
- s3:GetBucketLocation
Resource: !Sub arn:aws:s3:::${S3BucketName}
Condition:
StringLike:
s3:prefix: !If
- IsS3PathPrefixEmpty
- '*'
- !Sub ${S3PathPrefix}*
Roles:
- !Ref SnowflakeIAMRole
SnowflakeIAMRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Ref IAMRoleName
Description: !Ref IAMRoleDescription
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
-
Effect: Allow
Principal:
AWS:
- !Ref StorageAwsIamUserArn
Action: sts:AssumeRole
Condition:
StringEquals:
sts:ExternalId: !Ref StorageAwsExternalId
Outputs:
StorageAwsRoleArn:
Description: "STORAGE_AWS_ROLE_ARN"
Value: !GetAtt SnowflakeIAMRole.Arn
StorageAwsRoleArnAlterCommand:
Description: "STORAGE_AWS_ROLE_ARN Alter Command"
Value: !Join
- ""
- - "ALTER INTEGRATION <integration_name> SET STORAGE_AWS_ROLE_ARN = '"
- !GetAtt SnowflakeIAMRole.Arn
- "';"
StorageAllowedLocations:
Description: "STORAGE_ALLOWED_LOCATIONS"
Value: !If
- IsS3PathPrefixEmpty
- !Sub "('s3://${S3BucketName}/')"
- !Sub "('s3://${S3BucketName}/${S3PathPrefix}')"
StorageAllowedLocationsAlterCommand:
Description: "STORAGE_ALLOWED_LOCATIONS Alter Command"
Value: !Join
- ""
- - "ALTER INTEGRATION <integration_name> SET STORAGE_ALLOWED_LOCATIONS = "
- !If
- IsS3PathPrefixEmpty
- !Sub "('s3://${S3BucketName}/')"
- !Sub "('s3://${S3BucketName}/${S3PathPrefix}')"
- ";"
それぞれパラメータの概要に記載がある通り、必要な値を設定して構築を行います。構築は数分で終わると思います。
構築が終わったら、CloudFormationスタックの「出力」タブに以下のように表示される「キー」のStorageAllowedLocationsAlterCommand
とStorageAwsRoleArnAlterCommand
の値をコピーします。
出力例
# StorageAllowedLocationsAlterCommandの値
ALTER INTEGRATION <integration_name> SET STORAGE_ALLOWED_LOCATIONS = ('s3://foobar/sample/');
# StorageAwsRoleArnAlterCommandの値
ALTER INTEGRATION <integration_name> SET STORAGE_AWS_ROLE_ARN = 'arn:aws:iam::123456789012:role/foobar-role';
(Snowflake) 「ストレージ統合」を修正する
次に、Snowflakeに戻って「ストレージ統合」を修正します。先程CloudFormationスタックの「出力」から取得したクエリを貼り付けて実行します。この時、<integration_name>
は最初に作成した自分の「ストレージ統合」の名前に置き換えます。
「ストレージ統合」修正クエリ例
ALTER INTEGRATION ootaka_devio_interation SET STORAGE_ALLOWED_LOCATIONS = ('s3://foobar/sample/');
ALTER INTEGRATION ootaka_devio_interation SET STORAGE_AWS_ROLE_ARN = 'arn:aws:iam::123456789012:role/foobar-role';
(Snowflake) 「外部ステージ」を作成する
これで「ストレージ統合」はできたので、最後に「外部ステージ」を自分の作成したい設定に応じて作成すれば完了です。
以下は、適宜データベースとスキーマを選択してから作成した外部ステージの作成クエリ例です。
「外部ステージ」作成クエリ例
CREATE OR REPLACE STAGE ootaka_devio_external_stage
STORAGE_INTEGRATION = ootaka_devio_interation
URL = 's3://foobar/sample/'
;
外部ステージが作成できたら、LIST
コマンドでS3バケットの中身が参照できているか確認して完了です。
LISTコマンド例
LIST @ootaka_devio_external_stage;
まとめ
以上、爆速でSnowflakeの外部ステージ(S3)連携を設定するためのCloudFormationテンプレートを作ってみました。
個人的にAWS上でポチポチIAMポリシーとIAMロールを作成するのが面倒だったのですが、CloudFormationテンプレートを利用することで、だいぶこの作業が楽になりました。
どなたかのお役に立てば幸いです。それでは!