S3 へユーザー認証付きでファイルをアップロードする Amplify のサンプルアプリを試してみた

勉強のためにちょうど欲していた内容のサンプルアプリを見つけたのでやってみました。
2023.04.23

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

Web ブラウザからユーザー認証付きで S3 へファイルをアップロードする方法を考えていました。Amplify を使ったサンプルアプリケーションを見つけたので試しにやってみて学んだ記録です。Amplify でサンプルアプリのデプロイと、Web ブラウザから S3 へファイルをアップロード方法を確認した内容を紹介しています。

画像引用: 外部ユーザが安全かつ直接的に Amazon S3 へファイルをアップロードできるようにする方法 | Amazon Web Services ブログ

Amplify で S3 アップロード作成

AWS ブログで紹介されていた Amplify を利用したサンプルアプリケーションのデプロイを試してみます。

サンプルアプリの GitHub リポジトリはこちらでした。

aws-samples/s3uploader-ui

サンプルアプリのデプロイでエラーになる箇所があります。基本は AWS ブログで紹介されている手順通りでほぼほぼ問題ありません。エラーになる箇所のワークアラウンドを記載してあります。それ以外の箇所は簡単な手順のみ残しています。

Amplify設定

東京リージョン指定に修正しました。

cat <<END > ~/.aws/config
[default]
region=ap-northeast-1
END

Amplify のバージョンは現時点の最新版11.0.5で進めていきます。

$ npm install -g @aws-amplify/cli
$ amplify version
11.0.5

最新版の確認は以下のリンクからご確認ください。

Releases · aws-amplify/amplify-cli

Web アプリケーションのビルド・デプロイ

プロジェクト初期化します。AWS ブログで指定のあった所定のパラメータ通りに進めました。

$ amplify init

お目当ての Cognito 認証を追加します。Cognito と連携した認証機能を簡単に追加できるのは Amplify の利点ですよね。

$ amplify add auth

Authentication - Getting started - JavaScript - AWS Amplify Docs

データアップロード用の S3 バケットを作成します。S3 バケットを作成できるコマンドの存在を知りました。

$ amplify add storage

Storage - Getting started - JavaScript - AWS Amplify Docs

静的コンテンツを保存用の S3 バケットを作成します。静的コンテンツを配信することになるため CloudFront + S3 の標準的な構成です。

$ amplify hosting add

アプリケーションの初期化に必要なすべてのモジュールと依存関係をインストールします。

$ npm install

パブリックに公開するためにデプロイします。

$ amplify push
$ amplify publish

amplify publishを実行すると「S3 バケット ACL の許可がない」とエラーになりました。

Publish started for S3AndCloudFront
✖ Error has occurred during file upload.
? The bucket does not allow ACLs

S3 へファイルのアップロードのサンプル Web アプリケーションがデプロイできませんでした。

ワークアラウンド

Amplify の方の Issue に本件の報告があがっていました。ワークアラウンドも懇切丁寧にキャプチャ付きで説明してくれています。

"The bucket does not allow ACLs" on amplify publish · Issue #12503 · aws-amplify/amplify-cli

静的コンテンツが保存する S3 バケットの ACL を有効に戻し、オブジェクトライターにチェックを入れました。

アップロードしたファイルを保存する S3 バケットもありますので設定を変更する対象のバケットをよく確認しましょう。

改めてamplify publishしたところエラーは解消され正常にデプロイが完了しました。

ファイルをアップロードしてみる

CloudFornt の URL へアクセスしてログインユーザーを作成します。

登録したメールアドレスに認証用のコードが届くので入力したらユーザー作成完了です。

ローカル PC に保存してあるファイルを選択してみます。

画像ファイルを選択してみました。アップロードしてみます。

ファイルサイズが小さかったのですぐにアップロードが完了してしまいました。

3GB のファイルをアップロードしてみました。さすがに転送には時間がかかります。

Chrome のデベロッパーツールを開いてみると、S3 バケットへ直接マルチパートアップロードしている様子を観察できました。

インターネット環境にもよりますが 3GB のファイルも無事アップロード完了しました。

データ保存用の S3 バケットを確認してみるとユーザー毎のパス(Key)配下にアップロードしたファイルが保存されていました。

以上、ユーザー認証付きで S3 へファイル保存できる Web アプリケーションのサンプルを試してみたでした。

S3 へのアップロード方法を学ぶ

サンプルアプリを動かしてみて S3 へファイルをどのようにアップロードしているのか学びたかったのがはじめた動機でした。ここからが個人的に確認したかった内容です。

現在 Amplify を利用してわかっていること

  • S3 バケットには Cognito で認証したユーザー毎にパス(Key)があり、ユーザー個別にファイルが保存される
  • クライアント(Web ブラウザ)から S3 バケットへ直接ファイルをアップロードしている

クライアントからのアップロード処理

Amplify でデプロイしたコードを確認してみるとStorage.put(...)で選択したファイルをアップロードしていることがわかりました。

App.js language=抜粋

    const handleUpload = () => {
        if (uploadList.length === 0) {
            setVisibleAlert(true);
        } else {
            console.log('Uploading files to S3');
            let i, progressBar = [], uploadCompleted = [];
            for (i = 0; i < uploadList.length; i++) {
                // If the user has removed some items from the Upload list, we need to correctly reference the file
                const id = uploadList[i].id;
                progressBar.push(progressBarFactory(fileList[id]));
                setHistoryCount(historyCount + 1);
                uploadCompleted.push(Storage.put(fileList[id].name, fileList[id], {
                        progressCallback: progressBar[i],
                        level: "protected"
                    }).then(result => {
                        // Trying to remove items from the upload list as they complete. Maybe not work correctly
                        // setUploadList(uploadList.filter(item => item.label !== result.key));
                        console.log(`Completed the upload of ${result.key}`);
                    })
                );
            }
            // When you finish the loop, all items should be removed from the upload list
            Promise.all(uploadCompleted)
                .then(() => setUploadList([]));
        }
    }

Storage.putとは?と調べてみると Amplify の Storage ライブラリが提供している put メソッドでした。

Storage - Upload files - React Native - AWS Amplify Docs

ドキュメントを確認すると S3 へファイルをアップロードするだけなら簡単に実装できることを知りました。アップロードするファイルの保存の種類(レベル)にPublic, Protected, Privateがあったりとドキュメントを確認する良い機会となりました。ちなみにサンプルアプリではProtectedレベルが使われています。また、レジューム機能が有効もあり、今回のテストの様に数 GB ある大きなファイル転送時にはありがたいです。

クライアントが S3 バケットへアクセスする権限

Cognito のユーザープールでユーザー認証したあと、Cognito の ID プールから AWS の一時クレデンシャルキーを得ています。

CloudTrail のイベントログにはユーザー認証時にInitiateAuthが記録され、その後にAssumeRoleWitheWebidentityで IAM ロールへ AssumeRole し権限を引き受けるていることが記録されています。

イベントログ抜粋

    "resources": [
        {
            "accountId": "123456789012",
            "type": "AWS::IAM::Role",
            "ARN": "arn:aws:iam::123456789012:role/amplify-s3uploaderui-dev-92822-authRole"
        }
    ],

AssumeRoleWithWebIdentityで IAM ロールの権限を引き受けると引き受けるすべてのユーザーに対して同じ権限が有効になるとのことです。

Once a user has been included in a trust relationship, when your application calls AssumeRoleWithWebIdentity (perhaps via an AWSCognitoCredentialsProvider), they will receive the permissions attached to that role. These permissions will be effective across all users that assume that role. If you want to partition your users’ access, you can do so via policy variables. Be careful when using your users’ identity IDs in your access policies, particularly for unauthenticated identities, as these may change if the user chooses to login.

Understanding Amazon Cognito Authentication Part 3: Roles and Policies | Front-End Web & Mobile

IAM ロールを確認します。特定の S3 バケットのパスへPutObjectのみ許可されたポリシーを確認できました。

サンプルアプリはProtectedレベルが使われていたためProtectedに該当するポリシーを確認したものが下記です。

Protected_policy_df49e90b

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": [
                "s3:PutObject"
            ],
            "Resource": [
                "arn:aws:s3:::amplify-s3upload-ohmura92822-dev/protected/${cognito-identity.amazonaws.com:sub}/*"
            ],
            "Effect": "Allow"
        }
    ]
}

プレースホルダー${cognito-identity.amazonaws.com:sub}が気になるので確認してみます。

先ほど引用したAssumeRoleWithWebIdentityの文章でも触れられていましたがすべてのユーザーで同じ権限が渡ってしまうため、ユーザー毎のフォルダ(パス)毎にPutObjectを許可するために有効な方法が上記のプレースホルダー${cognito-identity.amazonaws.com:sub}の利用とのことでした。

You can very easily give a user a specific prefix “folder” in an S3 bucket by mapping this prefix to the ${cognito-identity.amazonaws.com:sub} variable:

Understanding Amazon Cognito Authentication Part 3: Roles and Policies | Front-End Web & Mobile

公式ドキュメントのリンクも参考に載せておきます。詳細確認先のリンク(Understanding Amazon Cognito Authentication Part 3: Roles and Policies on the AWS Mobile Blog)は先ほどから引用しているリンクです。

You can further limit the permissions for a given identity ID by using policy variables where possible. For example, using ${cognito-identity.amazonaws.com:sub}. For more information, see Understanding Amazon Cognito Authentication Part 3: Roles and Policies on the AWS Mobile Blog.

IAM roles - Amazon Cognito

まとめ

Cognito でユーザー認証し、Web ブラウザから S3 へファイルアップロードするとき、AssumeRole して IAM ロールの権限を引き受けて、Web ブラウザから S3 へ直接アクセスを行っていた。しかし、IAM ロールはユーザー個別のの権限を引き受けられないため S3 へのアクセス管理が問題になる。

そこで、ユーザー毎の権限管理するために IAM ポリシーにプレースホルダー${cognito-identity.amazonaws.com:sub}を利用している。ユーザー毎に S3 バケットにアクセスできるパス(Key)を指定することで、S3 バケットにはユーザー毎のフォルダがあるイメージで利用し上手くユーザー毎に S3 の権限管理を実現していた。

おわりに

S3 へのファイルアップロードを検討していて久々に Amplify を使ってみました。ユーザー認証付きの SPA を作成するなら Amplify を利用すると楽と記憶していたのですが相変わらず楽でした。

S3 へのファイルアップロードは署名付き URL を API Gateway 経由で Lambda から発行してクライアントに返そうかと考えていたのですけど、Cognito からの AssumeRole とプレースホルダー${cognito-identity.amazonaws.com:sub}のポリシー制御でも同じことをできることを知り学びがありました。

バックエンドも検討している部分があり、Amplify に特別詳しくないため、バックエンドへのつなぎ込みはこちらのブログで紹介されている内容もいいなと思いつつも、久々に使った Amplify が便利だったのでもう少し機能を把握しておこうと思います。触らないとだいぶ忘れていました。

参考