こんにちは、広野です。
サーバーレス WEB アプリ (SPA) から、ファイルをバックエンドにアップロードして処理したいときがあります。AWS 環境であれば AWS Amplify と連携させた Amazon S3 バケット「AWS Amplify Storage」を使用すると、アプリとセキュアに連動したストレージを簡単に作ることができます。
簡単に、と言いましたが、それは AWS Amplify CLI を使用すればの話。私は AWS Amplify CLI ではプロビジョニングできない環境や設定を一元管理・プロビジョニングしたいので、以下のマニュアルセットアップ手順に従うことになります。
しかし、複雑なIAM ロールの設定や、Amazon S3 / Amazon Cognito / AWS Amplify の相互連携があり、設定は手作業ではやってられません。ということで、私はAWS CloudFormation でプロビジョニングするテンプレートを作って、要件に応じてカスタマイズしています。
今回は AWS Amplify Storage をセットアップする AWS CloudFormation テンプレートのサンプルを紹介します。
やりたいこと
- AWS Amplify Console を使用した SPA (ここでは React アプリ) から AWS Amplify Storage (Amazon S3) にファイルをアップロードしたい。
- SPA の認証システムは Amazon Cognito ユーザプールを使用する。
- SPA にログインしたユーザのみ、AWS Amplify Storage の特定フォルダ (ここでは private) にファイルをアップロード可とする。
実現方法
- ユーザはアプリ画面経由で Amazon Cognito ユーザプールから認証を受ける。(ログインのこと)
- Amazon Cognito フェデレーティッドアイデンティティによりユーザは Amazon S3 にアクセス可能な IAM ロールを割り当てられる。
- ユーザはアプリ画面経由で Amazon S3 バケットにファイルをアップロードする。(アプリ画面やアップロード後の処理は割愛)
AWS CloudFormation テンプレート
- 以下のリソースをプロビジョニングする。
- Amazon Cognito
- AWS CodeCommit
- Amazon S3
- AWS Amplify Console
- Amazon Cognito ユーザープールはメールアドレスでセルフサインアップする形式、TOTP MFA あり。
- Amazon Cognito フェデレーティッドアイデンティティにより、Amazon Cognito ユーザープールにより認証されたユーザ、認証されていないユーザに分けて IAM ユーザ とそれに紐づけられた IAM ロールを割り当てる。IAM ロール内には指定の Amazon S3 バケットへのアクセスポリシーが定義されている。※ Amplify Storage の公式ドキュメント通り
- AWS CodeCommit は AWS Amplify をプロビジョニングするために必要なため、とりあえずプロビジョニング。サンプルソースコードはなし。
- AWS Amplify にはここでプロビジョニングされた Amazon Cognito および Amazon S3 のリソース情報が環境変数として渡される。環境変数は SPA アプリ内で利用できる。(後述)
AWSTemplateFormatVersion: 2010-09-09 Description: The CloudFormation template that creates sample resources for Amplify Storage. # ------------------------------------------------------------# # Input Parameters # ------------------------------------------------------------# Parameters: SystemName: Type: String Description: The system name. Default: example MaxLength: 10 MinLength: 1 CognitoAdminEmail: Type: String Description: Cognito Admin e-mail address. (e.g. xxx@xxx.xx) Default: xxxxx@example.xxx MaxLength: 100 MinLength: 5 Resources: # ------------------------------------------------------------# # Cognito IdP Roles (IAM) # ------------------------------------------------------------# # Cognito 認証を受けたユーザ用の IAM Role CognitoIdPAuthRole: Type: AWS::IAM::Role Properties: RoleName: !Sub example-CognitoIdPAuthRole-${SystemName} Description: This role allows Cognito authenticated users to access AWS resources. AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Principal: Federated: cognito-identity.amazonaws.com Action: "sts:AssumeRoleWithWebIdentity" Condition: StringEquals: "cognito-identity.amazonaws.com:aud": !Ref IdPool "ForAnyValue:StringLike": "cognito-identity.amazonaws.com:amr": authenticated Policies: - PolicyName: !Sub example-CognitoIdPAuthRolePolicy-${SystemName} PolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Action: - "mobileanalytics:PutEvents" - "cognito-sync:*" - "cognito-identity:*" Resource: "*" # Amplify Storage (S3) アクセス用 IAM Role 権限 - Action: - "s3:GetObject" - "s3:PutObject" - "s3:DeleteObject" Resource: - !Sub "arn:aws:s3:::example-${SystemName}-amplifystorage/public/*" - !Join - "" - - !Sub "arn:aws:s3:::example-${SystemName}-amplifystorage/protected/" - "${cognito-identity.amazonaws.com:sub}/*" - !Join - "" - - !Sub "arn:aws:s3:::example-${SystemName}-amplifystorage/private/" - "${cognito-identity.amazonaws.com:sub}/*" Effect: Allow - Action: - "s3:PutObject" Resource: - !Sub "arn:aws:s3:::example-${SystemName}-amplifystorage/uploads/*" Effect: Allow - Action: - "s3:GetObject" Resource: - !Sub "arn:aws:s3:::example-${SystemName}-amplifystorage/protected/*" Effect: Allow - Condition: StringLike: "s3:prefix": - "public/" - "public/*" - "protected/" - "protected/*" - "private/${cognito-identity.amazonaws.com:sub}/" - "private/${cognito-identity.amazonaws.com:sub}/*" Action: - "s3:ListBucket" Resource: - !Sub "arn:aws:s3:::example-${SystemName}-amplifystorage" Effect: Allow # Cognito 認証を受けていないユーザ用の IAM Role CognitoIdPUnauthRole: Type: AWS::IAM::Role Properties: RoleName: !Sub example-CognitoIdPUnauthRole-${SystemName} Description: This role allows Cognito unauthenticated users to access AWS resources. AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Principal: Federated: cognito-identity.amazonaws.com Action: "sts:AssumeRoleWithWebIdentity" Condition: StringEquals: "cognito-identity.amazonaws.com:aud": !Ref IdPool "ForAnyValue:StringLike": "cognito-identity.amazonaws.com:amr": unauthenticated Policies: - PolicyName: !Sub example-CognitoIdPUnauthRolePolicy-${SystemName} PolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Action: - "mobileanalytics:PutEvents" - "cognito-sync:*" Resource: "*" # Amplify Storage (S3) アクセス用 IAM Role 権限 - Action: - "s3:GetObject" - "s3:PutObject" - "s3:DeleteObject" Resource: - !Sub "arn:aws:s3:::example-${SystemName}-amplifystorage/public/*" Effect: Allow - Action: - "s3:PutObject" Resource: - !Sub "arn:aws:s3:::example-${SystemName}-amplifystorage/uploads/*" Effect: Allow - Action: - "s3:GetObject" Resource: - !Sub "arn:aws:s3:::example-${SystemName}-amplifystorage/protected/*" Effect: Allow - Condition: StringLike: "s3:prefix": - "public/" - "public/*" - "protected/" - "protected/*" Action: - "s3:ListBucket" Resource: - !Sub "arn:aws:s3:::example-${SystemName}-amplifystorage" Effect: Allow # ------------------------------------------------------------# # Cognito # ------------------------------------------------------------# # Cognito ユーザープール UserPool: Type: AWS::Cognito::UserPool Properties: UserPoolName: !Sub example-${SystemName} MfaConfiguration: "ON" EnabledMfas: - SOFTWARE_TOKEN_MFA Policies: PasswordPolicy: MinimumLength: 8 RequireUppercase: true RequireLowercase: true RequireNumbers: true RequireSymbols: false TemporaryPasswordValidityDays: 180 AccountRecoverySetting: RecoveryMechanisms: - Name: verified_email Priority: 1 AdminCreateUserConfig: AllowAdminCreateUserOnly: false AutoVerifiedAttributes: - email DeviceConfiguration: ChallengeRequiredOnNewDevice: false DeviceOnlyRememberedOnUserPrompt: false EmailConfiguration: EmailSendingAccount: DEVELOPER From: !Sub ${CognitoAdminEmail} ReplyToEmailAddress: !Sub ${CognitoAdminEmail} SourceArn: !Sub arn:aws:ses:${AWS::Region}:${AWS::AccountId}:identity/${CognitoAdminEmail} EmailVerificationMessage: !Sub "example-${SystemName} Verification code: {####}" EmailVerificationSubject: !Sub "example-${SystemName} Verification code" UsernameAttributes: - email UsernameConfiguration: CaseSensitive: false UserPoolAddOns: AdvancedSecurityMode: "OFF" UserPoolTags: Cost: !Sub example-${SystemName} UserPoolClient: Type: AWS::Cognito::UserPoolClient Properties: UserPoolId: !Ref UserPool ClientName: !Sub example-${SystemName}-appclient GenerateSecret: false RefreshTokenValidity: 3 AccessTokenValidity: 6 IdTokenValidity: 6 ExplicitAuthFlows: - ALLOW_USER_SRP_AUTH - ALLOW_REFRESH_TOKEN_AUTH PreventUserExistenceErrors: ENABLED SupportedIdentityProviders: - COGNITO CallbackURLs: - https://example.xxx/index.html LogoutURLs: - https://example.xxx/index.html DefaultRedirectURI: https://example.xxx/index.html AllowedOAuthFlows: - implicit AllowedOAuthFlowsUserPoolClient: true AllowedOAuthScopes: - email - openid # Cognito フェデレーティッドアイデンティティ IdPool: Type: AWS::Cognito::IdentityPool Properties: IdentityPoolName: !Sub example-${SystemName} AllowClassicFlow: false AllowUnauthenticatedIdentities: false CognitoIdentityProviders: - ClientId: !Ref UserPoolClient ProviderName: !GetAtt UserPool.ProviderName ServerSideTokenCheck: true IdPoolRoleAttachment: Type: AWS::Cognito::IdentityPoolRoleAttachment Properties: IdentityPoolId: !Ref IdPool Roles: authenticated: !GetAtt CognitoIdPAuthRole.Arn unauthenticated: !GetAtt CognitoIdPUnauthRole.Arn # ------------------------------------------------------------# # CodeCommit Repository # ------------------------------------------------------------# CodeCommitRepo: Type: AWS::CodeCommit::Repository Properties: RepositoryName: !Sub example-${SystemName} RepositoryDescription: !Sub Source Code for example-${SystemName} Tags: - Key: Cost Value: !Sub example-${SystemName} # ------------------------------------------------------------# # S3 # ------------------------------------------------------------# S3BucketAmplifyStorage: Type: AWS::S3::Bucket Properties: BucketName: !Sub example-${SystemName}-amplifystorage LifecycleConfiguration: Rules: - Id: AutoDelete Status: Enabled ExpirationInDays: 30 PublicAccessBlockConfiguration: BlockPublicAcls: true BlockPublicPolicy: true IgnorePublicAcls: true RestrictPublicBuckets: true CorsConfiguration: CorsRules: - AllowedHeaders: - "*" AllowedMethods: - "GET" - "HEAD" - "PUT" - "POST" - "DELETE" AllowedOrigins: - !GetAtt AmplifyConsole.DefaultDomain ExposedHeaders: - x-amz-server-side-encryption - x-amz-request-id - x-amz-id-2 - ETag MaxAge: 3000 Tags: - Key: Cost Value: !Sub example-${SystemName} DependsOn: - AmplifyConsole # ------------------------------------------------------------# # Amplify Role (IAM) # ------------------------------------------------------------# AmplifyRole: Type: AWS::IAM::Role Properties: RoleName: !Sub example-AmplifyRole-${SystemName} Description: This role allows Amplify to pull source codes from CodeCommit. AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - amplify.amazonaws.com Action: - sts:AssumeRole Policies: - PolicyName: !Sub example-AmplifyPolicy-${SystemName} PolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Resource: - !Sub "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/amplify/*" Action: - "logs:CreateLogGroup" - "logs:CreateLogStream" - "logs:PutLogEvents" - Effect: Allow Resource: - !GetAtt CodeCommitRepo.Arn Action: - "codecommit:GitPull" DependsOn: - CodeCommitRepo # ------------------------------------------------------------# # Amplify Console # ------------------------------------------------------------# AmplifyConsole: Type: AWS::Amplify::App Properties: Name: !Sub example-${SystemName} Description: !Sub Web App environment for example-${SystemName} Repository: !GetAtt CodeCommitRepo.CloneUrlHttp AutoBranchCreationConfig: EnableAutoBranchCreation: false EnableAutoBuild: true EnablePerformanceMode: false EnableBranchAutoDeletion: false BuildSpec: |- version: 1 frontend: phases: preBuild: commands: - npm ci build: commands: - npm run build - echo "REACT_APP_REGION=$REACT_APP_REGION" >> .env - echo "REACT_APP_USERPOOLID=$REACT_APP_USERPOOLID" >> .env - echo "REACT_APP_USERPOOLWEBCLIENTID=$REACT_APP_USERPOOLWEBCLIENTID" >> .env - echo "REACT_APP_IDPOOLID=$REACT_APP_IDPOOLID" >> .env - echo "REACT_APP_AMPLIFYSTORAGE=$REACT_APP_AMPLIFYSTORAGE" >> .env artifacts: baseDirectory: build files: - '**/*' cache: paths: - node_modules/**/* CustomRules: - Source: /<*> Status: 404-200 Target: /index.html - Source: Status: 200 Target: /index.html # アプリに渡す環境変数 EnvironmentVariables: - Name: REACT_APP_REGION Value: !Ref AWS::Region - Name: REACT_APP_USERPOOLID Value: !Ref UserPool - Name: REACT_APP_USERPOOLWEBCLIENTID Value: !Ref UserPoolClient - Name: REACT_APP_IDPOOLID Value: !Ref IdPool - Name: REACT_APP_AMPLIFYSTORAGE Value: !Sub example-${SystemName}-amplifystorage IAMServiceRole: !GetAtt AmplifyRole.Arn Tags: - Key: Cost Value: !Sub example-${SystemName} DependsOn: - AmplifyRole AmplifyBranchProd: Type: AWS::Amplify::Branch Properties: AppId: !GetAtt AmplifyConsole.AppId BranchName: master Description: production EnableAutoBuild: true EnablePerformanceMode: false DependsOn: - AmplifyConsole
SPA コード内の設定
SPA (ここでは React) 側で Amplify Storage と連携するために、コード内に以下の設定追記が必要。設定パラメータは AWS CloudFormation で定義した環境変数(process.env.REACT_APP_XXXXX の部分)を利用する。
- App.js 内
import Amplify from 'aws-amplify'; //Amplify Cognito, S3 連携設定 Amplify.configure({ Auth: { region: process.env.REACT_APP_REGION, userPoolId: process.env.REACT_APP_USERPOOLID, userPoolWebClientId: process.env.REACT_APP_USERPOOLWEBCLIENTID, identityPoolId: process.env.REACT_APP_IDPOOLID }, Storage: { AWSS3: { bucket: process.env.REACT_APP_AMPLIFYSTORAGE, region: process.env.REACT_APP_REGION } } });
上記設定ができている前提であれば、SPA 内任意のコンポーネントで Amplify Storage にアクセスするコードが機能するようになる。以下はサンプル。
import { Storage } from 'aws-amplify'; //AmplifyStorageにJSONデータを転送 const putFile = async () => { try { await Storage.put( "data.json", //S3に送信したデータファイルに付けるオブジェクト名 jsonData, //S3に送信したいJSONオブジェクトのデータ { level: "private", //private権限のフォルダに格納する contentType: "application/json", progressCallback(progress) { setProgress(progress.loaded/progress.total); } } ); } catch (error) { alert("File uploading error occurred: " + error); } };
まとめ
いかがでしたでしょうか?
基本的には Amplify Storage は AWS の公式ドキュメント通りに設定すればよいのですが、一度 AWS CloudFormation でテンプレート化しておけば後が楽なのでそうしました。ある程度権限分けしたフォルダ設計になっているので、ユーザごとに、グループごと等にアクセス制御を分けた使い道にフル活用できそうです。また、IAM ロールの設定をカスタマイズすれば他の用途にも応用できると思いました。
本記事がお役に立てれば幸いです。