Ponz Dev Log

ゆるくてマイペースな開発日記

ブラウザからREST APIでAmazon S3にファイルをアップロードするときに気をつけること

書籍『AWSによるサーバーレスアーキテクチャ』(翔泳社)を読み進めているときにS3のファイルアップロード関連でつまづいたところが多々あったので、気をつけるべきところとして手順と一緒にまとめます。

AWSに関わる動的な処理を全てJavaScriptで実装することを選択した場合、AWS Lambda上ではAWS SDK for JavaScript一択ではありますが、ブラウザの場合は生のJavaScriptで記述することも可能です。

今回はブラウザ側で実行されるS3関連のコードをSDKを使わずREST APIで扱った時のメモです。 (書籍に従って生JSでゴリゴリ書きましたけど、潔くSDKを使えばこういったところで躓かなかったのではと思わずにはいられない。)

www.shoeisha.co.jp

やること

  • 署名付きURLを発行する。
  • バケットのアクセス権限を設定する。
  • REST API経由でリクエストを投げる。

署名付きURLを発行する

AWS S3 署名バージョン4でリクエストする

  • ファイル名(オブジェクトのキー)、バケット名、有効期限を使って署名付きのURLを発行します。このとき、署名バージョン4でリクエストします。
    • V4以外は受け付けません。明示的に指定した方がいいかも。
    • ここはLambda + API Gatewayで実装すると扱いやすい。

AWS regions created before January 30, 2014 will continue to support the previous protocol, Signature Version 2. Any new regions after January 30, 2014 will support only Signature Version 4 and therefore all requests to those regions must be made with Signature Version 4

https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/API/sig-v4-authenticating-requests.html

以下、コード例。

"use strict";

const AWS = require("aws-sdk");
const S3 = new AWS.S3({
    signatureVersion: 'v4'
});
const crypto = require("crypto");

function getSignedUrl(fileName) {
    const prefix = crypto.randomBytes(20).toString("hex");
    const params = {
        Bucket: process.env.UPLOAD_BUCKET,
        Key: prefix + "/" + fileName,
        Expires: 600
    };
    return S3.getSignedUrl("putObject", params);
}

module.exports = {
    getSignedUrl
};
  • オペレーション名を一緒に付与すること

    • オペレーションを指定するときは、putObjectとキャメルケースで指定すること。PutObjectと先頭大文字で指定するとエラーで弾かれる。
    • 公式ドキュメントには上記は明記されていないが、Sampleにはちゃっかりキャメルケースで指定されている。
  • ここで返却される署名付きURLは仮想ホスト形式で返されます。

    • http://{bucket-name}.s3.amazonaws.com
    • http://{bucket-name}.s3-aws-region.amazonaws.com

バケットのアクセス権限を設定する

バケットポリシー

まずはパブリックアクセス権限が付いていたら外します。 今回は署名付きURLでダウンロード/アップロードを行うため、リクエストの度にリクエスト者に権限が付与される仕組みにするためです。

バケットを選択 -> アクセス権限 -> バケットポリシーを開いてドキュメントを削除する。バケット一覧で該当のバケットに"非公開"と記載されていればOK。

常に最小限の権限を付与することが推奨されているため、不用意にパブリックアクセスを許可することは回避した方が良いです。

CORS設定

AWSとは別ドメインからアクセスすることになるので、どこから/どんなオペレーションが許可されるかバケットに設定する必要があります。 バケットを選択 -> アクセス権限 -> CORSの設定から以下のように設定を追加します。これだけ。

<?xml version="1.0" encoding="UTF-8"?>
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
    <CORSRule>
        <AllowedOrigin>*</AllowedOrigin>
        <AllowedMethod>POST</AllowedMethod>
        <AllowedMethod>PUT</AllowedMethod>
        <MaxAgeSeconds>3000</MaxAgeSeconds>
        <AllowedHeader>*</AllowedHeader>
    </CORSRule>
</CORSConfiguration>

あと地味なTipsとしては、1行目の<?xml...の記載は設定のエディタに書かなくても勝手に追加されます。

REST API経由でリクエストを投げる

PUTリクエストで投げる。

オブジェクトの新規作成をするんだからPOSTリクエストじゃね?と直感的に思いつきますが、PUTリクエスで投げます。POSTメソッドでリクエストを投げてもAccess Deniedと403が寂しく返ってくるだけです。 思い返してみれば、オペレーション名はPutObjectだもんね。

おそらく同一キーのオブジェクトに対しては上書きもできることから、POSTメソッドではなくPUTメソッドのリクエストにしているのではと推測します。 --> REST API Reference: PUT Object

Content-Typeはファイルのものを指定する

画像や動画をHTMLから送信する場合の定石だとContent-Type: multipart/form-dataでデータをバックエンドに送信しますが、S3へのアップロードに関しては単一ファイルの場合はmultipartリクエストを行うのは間違いです。 ファイル本体のContent-Typeを指定しましょう。

例えば、MP4の動画をアップロードする場合は、Content-Type: video/mp4といった具合です。

"use strict";

/**
 * ファイルをS3バケットにアップロードする。
 * @param {string} url アップロード先S3バケットの署名付きURL
 */
function upload(url) {
    // MP4の動画を添付したと想定。
    const uploadBtn = document.getElementById("uploadButton");
    const file = uploadButton.files[0];

    // S3バケットにアップロード実行。
    $.ajax({
        url: url,
        type: "PUT",
        data: file,
        processData: false,
        contentType: "video/mp4"
    })
    .done(response => {
        console.log(response);
        alert("Upload Finished!");
    })
    .fail(err => {
        console.log(err);
        alert("Failed to upload...");
    });
}

実はこれもちゃっかり公式ドキュメントに記載されています。日本語版にはないですけどね。 以下のExample2にJPEG画像の例が参考になります。 https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/API/RESTObjectPUT.html?shortFooter=true#RESTObjectPUT-responses-examples


ブラウザに含めるJSファイルのサイズを気にしてSDKを入れられない場合などに参考にしていただければと思います。

以上。