AWS Amplify Storage を AWS CloudFormation でマニュアルセットアップする

こんにちは、広野です。

サーバーレス WEB アプリ (SPA) から、ファイルをバックエンドにアップロードして処理したいときがあります。AWS 環境であれば AWS Amplify と連携させた Amazon S3 バケット「AWS Amplify Storage」を使用すると、アプリとセキュアに連動したストレージを簡単に作ることができます。

簡単に、と言いましたが、それは AWS Amplify CLI を使用すればの話。私は AWS Amplify CLI ではプロビジョニングできない環境や設定を一元管理・プロビジョニングしたいので、以下のマニュアルセットアップ手順に従うことになります。

https://docs.amplify.aws/lib/storage/getting-started/q/platform/js/#manual-setup-import-storage-bucket

しかし、複雑な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) にファイルをアップロード可とする。

実現方法

  1. ユーザはアプリ画面経由で Amazon Cognito ユーザプールから認証を受ける。(ログインのこと)
  2. Amazon Cognito フェデレーティッドアイデンティティによりユーザは Amazon S3 にアクセス可能な IAM ロールを割り当てられる。
  3. ユーザはアプリ画面経由で 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 ロールの設定をカスタマイズすれば他の用途にも応用できると思いました。

本記事がお役に立てれば幸いです。

著者について
広野 祐司

AWSサーバーレスアーキテクチャを駆使して社内クラウド人材育成アプリや教育コンテンツをつくっています。ReactでSPAを書き始めたら、快適すぎて他の開発言語には戻れなくなりました。AWSサーバーレスやReactの仲間を増やしたいです。
取得資格:AWS認定は7つ、ITサービスマネージャ、ITIL v3 Expert、等
2020, 2021 APN AWS Top Engineers 受賞
2022 AWS Partner Ambassador 受賞
好きなAWSサービス:AWS Amplify / Amazon Cognito / AWS Step Functions / AWS CloudFormation

広野 祐司をフォローする
クラウドに強いによるエンジニアブログです。
SCSKは専門性と豊富な実績を活かしたクラウドサービス USiZE(ユーサイズ)を提供しています。
USiZEサービスサイトでは、お客様のDX推進をワンストップで支援するサービスの詳細や導入事例を紹介しています。
AWSサーバレスアーキテクチャ
TechHarmony
タイトルとURLをコピーしました