爆速でSnowflakeの外部ステージ(S3)連携を設定するためのCloudFormationテンプレートを作ってみた
こんにちは!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ポリシーがインラインポリシーになるのですが、今回はこれは目をつむりました。(公式ドキュメントの手順だと、個別のポリシーとして作成していますね)
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 @ootaka_devio_external_stage;
まとめ
以上、爆速でSnowflakeの外部ステージ(S3)連携を設定するためのCloudFormationテンプレートを作ってみました。
個人的にAWS上でポチポチIAMポリシーとIAMロールを作成するのが面倒だったのですが、CloudFormationテンプレートを利用することで、だいぶこの作業が楽になりました。
どなたかのお役に立てば幸いです。それでは!