こんにちは、広野です。
サーバーレス 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 ロールの設定をカスタマイズすれば他の用途にも応用できると思いました。
本記事がお役に立てれば幸いです。
