This article was contributed by Will Webberley
Will is a computer scientist and is enthused by nearly all aspects of the technology domain. He is specifically interested in mobile and social computing and is currently a researcher in this area at Cardiff University.
Node.js での S3 へのファイルの直接アップロード
最終更新日 2022年03月09日(水)
Web アプリケーションには多くの場合、ユーザーが画像、ムービー、アーカイブなどのファイルをアップロードできるようにする機能が必要になります。 Amazon S3 は、これらのファイルのための一般的で信頼性の高いストレージオプションです。
この記事では、S3 のオリジン間リソース共有 (CORS) サポートを利用して、Web アプリケーションを経由せずにファイルを S3 に直接アップロードする Node.js アプリケーションを作成する方法の例を示します。以下の例では、リクエスト処理を容易にするために Express Web フレームワークを使用していますが、どの Node.js アプリケーションでも手順はほとんど同じです。
S3 への直接アップロード
この記事で説明するコードの完全な例は、この GitHub リポジトリから入手して直接使用できます。
直接アップロードの主な利点は、アプリケーションの dyno への負荷が大幅に削減されることです。ファイルの受信と S3 への転送にアプリ側のプロセスを使用すると、dyno が不必要に拘束され、並列 Web リクエストへの dyno の応答効率が低下する可能性があります。
アプリケーションでは、クライアント側とアプリ側の JavaScript を使用してリクエストに署名します。実際のアップロードは非同期で実行されるため、アップロードの完了後にアプリケーションのフローを処理する方法は開発者が決定できます (たとえば、アップロードが成功したら、完全にページを更新する代わりにユーザーを別のページにリダイレクトできます)。
直接アップロードを実行するために必要な各種の手順を完了するためのガイドとして、また、より広範囲のユースケースにこのアプリケーションを関連付けるために、単純なアカウント編集シナリオの例を使用しています。このシナリオについての詳細は、後で説明します。
概要
S3 は、それぞれにグローバル固有名が付いており、個々のファイル (オブジェクトと呼ばれる) やディレクトリを保存できる一連のバケットで構成されています。
ファイルを S3 にアップロードするには、ユーザー名とパスワードの役割を果たすアクセスキー ID とシークレットアクセスキーが必要です。アップロードを成功させるには、ターゲットバケットに対する十分なアクセス権限がアクセスキーアカウントに必要です。
これについての詳細、バケットの作成、および認証キーの処理については、S3 関連の記事を参照してください。
一般に、この記事で説明する方法では、以下の簡単な手順に従います。
- アップロードするファイルをユーザーが Web ブラウザで選択します。
- ユーザーのブラウザから、Heroku 上の Web アプリケーションにリクエストが行われます。これにより、アップロードリクエストへの署名に使用される一時的な署名が生成されます。
- 一時的に署名されたリクエストが JSON 形式でブラウザに返されます。
- 次に、Node.js アプリケーションによって提供された署名済みリクエストを使用して、ブラウザから Amazon S3 に直接、ファイルがアップロードされます。
このガイドには、完全なシステムを構築するために、クライアント側およびアプリ側のコードを実装する方法についての情報が含まれています。ガイドに従って完成した、最低限の機能を持つシステムを使用して、ユーザーはファイルを S3 にアップロードできます。ただし通常は、システムのセキュリティを強化したり、特定の用途に合わせてカスタマイズしたりするために、さらに機能を追加する価値があります。これについては、ガイド内の別の箇所で説明します。
後で説明するように、サーバーでの署名生成には AWS の公式 SDK が使用されます。この SDK の機能についての詳細は、AWS のドキュメントを参照してください。
前提条件
- Heroku CLI がインストールされていること。
- Node.js がインストールされていること。
- 現在のプロジェクトのための Heroku アプリケーションが作成されていること。
- AWS S3 バケットが作成されていること。
- バケットへの書き込みアクセス権を持つ AWS 認証キーがあること。
初期設定
S3 の設定
ここでは、ターゲットの S3 バケットの権限プロパティの一部を編集して、最終的なリクエストがバケットに書き込むための十分な権限を持つようにする必要があります。Web ブラウザで、AWS コンソールにサインインして [S3] セクションを選択します。適切なバケットを選択し、Permissions
(アクセス許可) タブをクリックします。このページにはいくつかのオプションが提供されています (パブリックアクセスのブロック、アクセス制御リスト、バケットポリシー、CORS 設定など)。
最初に、"Block all public access" (すべてのパブリックアクセスをブロックする) をオフにします。特に “Block public access to buckets and objects granted through new access control lists” (新しいアクセス制御リスト経由で許可されたバケットおよびオブジェクトへのパブリックアクセスをブロックする) および “Block public access to buckets and objects granted through any access control lists” (任意のアクセス制御リスト経由で許可されたバケットおよびオブジェクトへのパブリックアクセスをブロックする) を、このプロジェクトのためにオフにします。バケットをこの方法で設定すると、署名済みの URL がなくてもそのコンテンツを読み取ることができますが、これは本番環境で実行中のサービスには適さないことがあります。
次に、バケットの CORS (オリジン間リソース共有) を設定する必要があります。これにより、アプリケーションは S3 バケット内のコンテンツにアクセスできるようになります。各ルールで、バケットへのアクセスが許可されるアクセス元ドメインのセットを指定し、それらのドメインから許可されるメソッドおよびヘッダーも指定する必要があります。
「Properties」 (プロパティ) タブと CORS 設定エディタの場所Permissions
tab and CORS configuration editor](https://devcenter3.assets.heroku.com/article-images/1606246183-Screenshot-2020-11-24-at-19.21.01.png)
アプリケーションでこれが機能するよう、「Edit」 (編集) をクリックして、バケットの CORS 設定に次の JSON を入力します。
[
{
"AllowedHeaders": [
"*"
],
"AllowedMethods": [
"GET",
"HEAD",
"POST",
"PUT"
],
"AllowedOrigins": [
"*"
],
"ExposeHeaders": []
}
]
「Save changes」 (変更の保存) をクリックしてエディタを閉じます。
これにより、任意のドメインにバケットへのアクセスを許可し、リクエストに含まれるヘッダーを制限しないことを S3 に指示します。通常、テストではこの設定でも問題ありません。デプロイするときは、特定のドメインからのリクエストしか受け付けないように ‘AllowedOrigin’ を変更する必要があります。
このアプリケーション専用の S3 資格情報を使用する場合、AWS アカウントページでさらにキーを生成できます。これにより、このキーのセットが実行できるリクエストのセットを具体的に指定できるため、セキュリティがさらに強化されます。これが望ましい場合は、S3 バケットの 「Edit bucket policy」 (バケットポリシーの編集) オプションで IAM ユーザーを設定する必要があります。AWS の Web ページには、この実行方法を詳細に説明しているさまざまなガイドが掲載されています。
アプリの設定
アプリをまだ設定していない場合、この段階で設定すると便利です。まず、ローカルマシン上の適当な場所にディレクトリを作成します。
$ mkdir NodeDirectUploader
NodeDirectUploader/
のサブディレクトリをさらに 2 つ作成し、それぞれに HTML ページとサポートファイルを格納します。
$ cd NodeDirectUploader
$ mkdir views
$ mkdir public
Node のパッケージマネージャー npm
はデフォルトで Node と共にインストールされ、アプリに必要なパッケージのインストールと更新の処理に使用できます。これを開始するには、アプリのディレクトリのルートで Node の対話型パッケージ設定ツールを実行します。
$ npm init
このツールで、名前、説明、ライセンス、バージョン管理など、アプリに関するいくつかの質問に答えると、package.json
という名前のファイルがアプリのルートに作成されます。回答に基づいて、アプリに関する情報がこのファイルに保存されます。このファイルの内容は開発途中で自由に編集できます。
同じファイルを使用して、アプリの依存関係を容易に宣言できます。これにより、アプリのデプロイと共有が容易になります。これを行うには、package.json
を編集して "dependencies"
JSON オブジェクトを追加し、以下のパッケージ依存関係を含めます。
{
"name": "NodeDirectUploader",
"version": "0.0.1",
...
"dependencies": {
"aws-sdk": "2.x",
"ejs": "2.x",
"express": "4.x"
}
}
その後、npm
を使用してこれらの依存関係をインストールできます。
$ npm install
これらのパッケージの使用は後で明らかになり、この方法でインストールしておけば、アプリが拡張された場合にアプリごとの依存関係を管理しやすくなります。
Heroku の設定
アップロードリクエストに署名するために、アプリケーションが AWS の資格情報にアクセスする必要がある場合、その資格情報は Heroku で環境設定として追加する必要があります。
$ heroku config:set AWS_ACCESS_KEY_ID=xxx AWS_SECRET_ACCESS_KEY=yyy
Adding config vars and restarting app... done, v21
AWS_ACCESS_KEY_ID => xxx
AWS_SECRET_ACCESS_KEY => yyy
AWS のアクセス資格情報に加えて、ターゲットの S3 バケットの名前を次のように設定します。
$ heroku config:set S3_BUCKET=zzz
Adding config vars and restarting app... done, v21
S3_BUCKET => zzz
セキュリティ上の理由から、設定ファイルではなく環境設定を使用することをお勧めします。パスワードやアクセスキーをアプリケーションのコードまたは設定ファイルに直接記述することは避けてください。詳細については、「設定と環境設定」の記事を参照してください。
アプリのローカル環境変数を設定すると、ローカルでのアプリの実行とテストに役立ちます。詳細については、Heroku Local の記事の「Set up your local environment variables」(ローカル環境変数の設定) セクションを参照してください。ローカルでのアプリの起動については、この記事の後半で説明します。
.env
ファイルはローカルテストにしか使用しないため、必ず .gitignore
に追加してください。
直接アップロード
この記事の目的は、S3 への直接アップロードを実行するために必要なプロセスと手順を、簡単なプロフィール編集シナリオを使用して説明することです。この例では、許可されたユーザーが、アップロードするアバター画像を選択し、アカウントに保存される基本情報をいくつか入力します。
このシナリオでは、次の手順が実行されます。
- ユーザーに、自分のアバターとしてアップロードする画像を選択したり、ユーザー名と自分の名前を入力したりするための要素が含まれた Web ページが表示されます。
- ユーザーが選択した画像のプレビューを要素に保持する必要があります。デフォルトでは、アップロードする画像が選択されていない場合、代わりにデフォルトのアバター画像が使用されます (このシナリオでは、画像のアップロードはユーザーにとって事実上、任意です)。
- ユーザーがアップロードされる画像を選択すると、この記事で前に説明したプロセスによって、S3 へのアップロードが自動かつ非同期に処理されます。アップロードが正常に完了すると、選択した画像で画像プレビューが更新されます。
- その後、ユーザーは残りの情報の入力に進むことができます。
- ユーザーが 「Submit」 (送信) ボタンをクリックすると、ユーザー名、アップロードされた画像の名前と URL が Node アプリケーションに送信され、確認と保存が行われます。ユーザーが画像をアップロードしなかった場合、デフォルトのアバター画像の URL が代わりに送信されます。
クライアント側のコードの設定
クライアント側で実装を完了するために必要なサードパーティのコードはありません。
ファイル選択を処理し、Node アプリケーションからリクエストと署名を取得し、最後にアップロードリクエストを実行するための HTML および JavaScript をここで作成できます。
最初に、account.html
という名前のファイルをアプリケーションの views/
ディレクトリに作成し、head
やその他の必要な HTML タグのデータをアプリケーションに合わせて適切に設定します。この HTML ファイルの本文には、ファイル入力と、アップロードの最新の進捗状況を反映する要素を含めます。これに加えて、ユーザーが自分のユーザー名とフルネームを入力するためのフォームと、選択されたアバター画像の URL を保持する非表示の input
要素を作成します。
完成した HTML ファイルを確認するには、コンパニオンリポジトリ内の該当するコードを参照してください。
<input type="file" id="file-input">
<p id="status">Please select a file</p>
<img id="preview" src="/images/default.png">
<form method="POST" action="/save-details">
<input type="hidden" id="avatar-url" name="avatar-url" value="/images/default.png">
<input type="text" name="username" placeholder="Username"><br>
<input type="text" name="full-name" placeholder="Full name"><br><br>
<input type="submit" value="Update profile">
</form>
#preview
要素には当初、(新しい画像が選択されない場合にユーザーのアバターになる) デフォルトのアバター画像が保持され、#avatar-url
入力はユーザーが選択したアバター画像の現在の URL を保持します。これらは両方とも、ユーザーが新しいアバターを選択したら、以下で説明する JavaScript によって更新されます。
したがって、ユーザーが最後に 「Submit」 (送信) ボタンをクリックすると、アバターの URL とユーザーのユーザー名およびフルネームが指定のエンドポイントに送信され、サーバー側での処理に回されます。
クライアント側のコードでは、次の 2 つの処理を行います。
- 画像を S3 に PUT するために使用できるアプリから署名済みリクエストを取得する
- 署名済みリクエストを使用して実際に画像を S3 に PUT する
非同期 HTTP リクエストを実行するために、JavaScript の XMLHttpRequest
オブジェクトを作成および使用できます。
これを行うには、まず <script>
ブロックを作成します。次に、ドキュメントが読み込まれたらファイル入力の変更をリッスンしてアップロードプロセスを開始するコードを記述します。
(() => {
document.getElementById("file-input").onchange = () => {
const files = document.getElementById('file-input').files;
const file = files[0];
if(file == null){
return alert('No file selected.');
}
getSignedRequest(file);
};
})();
このコードでは、アップロードされるファイルオブジェクト自体も決定します。ファイルオブジェクトが正しく選択されている場合、ファイルに対する署名済み PUT リクエストを取得するための関数の呼び出しに進みます。次に、ファイルオブジェクトを受け入れてそのオブジェクトに対する適切な署名済みリクエストをアプリから取得する関数を記述します。
function getSignedRequest(file){
const xhr = new XMLHttpRequest();
xhr.open('GET', `/sign-s3?file-name=${file.name}&file-type=${file.type}`);
xhr.onreadystatechange = () => {
if(xhr.readyState === 4){
if(xhr.status === 200){
const response = JSON.parse(xhr.responseText);
uploadFile(file, response.signedRequest, response.url);
}
else{
alert('Could not get signed URL.');
}
}
};
xhr.send();
}
アップロードするファイルの名前 (file.name
) や MIME タイプ (file.type
) に特殊文字 (スペースなど) が含まれている場合は、まず特殊文字をエンコードする必要があります (例: encodeURIComponent(file.name)
)。
この記事の後半で説明するように、署名済みリクエストの作成にはファイルの名前と MIME タイプが必要であるため、上記の関数はファイルの名前と MIME タイプを GET リクエストにパラメータとして渡します。署名済みリクエストの取得に成功した場合、この関数は、実際のファイルをアップロードするための関数の呼び出しに進みます。
function uploadFile(file, signedRequest, url){
const xhr = new XMLHttpRequest();
xhr.open('PUT', signedRequest);
xhr.onreadystatechange = () => {
if(xhr.readyState === 4){
if(xhr.status === 200){
document.getElementById('preview').src = url;
document.getElementById('avatar-url').value = url;
}
else{
alert('Could not upload file.');
}
}
};
xhr.send(file);
}
この関数は、アップロードするファイル、署名済みリクエスト、アバター画像の最終的な取得 URL を表す生成済み URL を受け取ります。最後の 2 つの引数は、アプリからの応答の一部として返されます。S3 へのリクエストが成功した場合、この関数はプレビュー要素を新しいアバター画像に更新し、非表示の入力に URL を格納して、アプリでの保存のために送信できるようにします。
これで、ユーザーがフォームの残り項目を入力完了して 「Submit」 (送信) をクリックすると、名前、ユーザー名、アバター画像のすべてを同じエンドポイントに送信できます。
システムの実装後、意図したとおりにページが機能しない場合、console.log()
を使用して、onreadystatechange
関数で発生したすべてのエラーを記録し、ブラウザのエラーコンソールを使用して問題の診断を試みることを検討してください。
アプリケーションの形式が Web ベースかデバイスベースかを問わず、アプリケーション内でアクティビティが長引いた場合はユーザーに通知し、変更に関する最新の情報を表示することが推奨されます。したがって、ファイルを選択してからアップロードが完了するまでの間に、読み込みインジケータが表示される可能性があります。 このような情報がないと、ユーザーはページがクラッシュしたことを疑い、ページを更新しようとしたり、アップロードプロセスを中断したりする可能性があります。
アプリ側の Node コードの設定
このセクションでは、アップロードリクエストへの署名に使用できる一時的な署名を生成するための Node.js の使用について説明します。この一時的な署名では、AWS 認証資格情報 (アクセスキーとシークレットキー) を署名の基礎として使用しますが、ユーザーはこの情報に直接アクセスできません。署名の有効期限が切れた後、同じ署名を使用したアップロードリクエストは失敗します。
完成した Node ファイルを確認するには、コンパニオンリポジトリ内の該当するコードを参照してください。
まず、アプリケーションディレクトリのルートにメインのアプリケーションファイル app.js
を作成し、スケルトンアプリケーションを適切に設定します。
const express = require('express');
const aws = require('aws-sdk');
const app = express();
app.set('views', './views');
app.use(express.static('./public'));
app.engine('html', require('ejs').renderFile);
app.listen(process.env.PORT || 3000);
const S3_BUCKET = process.env.S3_BUCKET;
一部のシナリオでは、Number(process.env.PORT)
を使用して、環境の PORT
変数が数値であることを確認することが必要な場合があります。
npm
を使用してインストールするパッケージは、上書きの形でアプリケーションにインポートされます。次に Express アプリが設定され、最後にバケット名が環境から読み込まれます。
ここで、AWS リージョンを設定する必要があります。そのためには、インポートされた aws
オブジェクトを更新します。次に例を示します。
aws.config.region = 'eu-west-1';
必ず、ターゲットのバケットが存在するリージョンを使用してください。必要な場合は、このページを使用してリージョンを確認してください。
次に、さまざまな URL に対してリクエストが行われたときに正しい情報をユーザーのブラウザに返す役割を担うビューを、同じファイル内に作成する必要があります。app.js
ファイル内で、/account
へのリクエストに対応し、account.html
ページを返すビューを定義します。ユーザーはこのページに含まれるフォームで入力を完了します。
app.get('/account', (req, res) => res.render('account.html'));
ここで、同じ JavaScript ファイル内に、クライアント側の JavaScript が画像をアップロードするために使用できる署名を生成して返すためのビューを作成します。これは、S3 へのアップロードを試みる前にクライアントが実行する最初のリクエストです。このビューの応答は /sign-s3
へのリクエストです。
app.get('/sign-s3', (req, res) => {
const s3 = new aws.S3();
const fileName = req.query['file-name'];
const fileType = req.query['file-type'];
const s3Params = {
Bucket: S3_BUCKET,
Key: fileName,
Expires: 60,
ContentType: fileType,
ACL: 'public-read'
};
s3.getSignedUrl('putObject', s3Params, (err, data) => {
if(err){
console.log(err);
return res.end();
}
const returnData = {
signedRequest: data,
url: `https://${S3_BUCKET}.s3.amazonaws.com/${fileName}`
};
res.write(JSON.stringify(returnData));
res.end();
});
});
このコードでは、S3 への PUT リクエストを実行するためにブラウザで使用できる署名済み URL を、aws-sdk
モジュールを使用して作成します。さらに、アップロードされるオブジェクトの予想 URL は、S3 バケット名とオブジェクト名の組み合わせとして生成されます。この URL と署名済みリクエストが JSON 形式でブラウザに返されます。
Expires
パラメータは、署名済み URL が有効になる秒数を記述します。大きいファイルのアップロード時など、状況によっては、署名済み URL の有効時間を延ばすために値を大きくする方が適切な場合があります。
s3
オブジェクトを初期化すると、以前に環境に設定された AWS_ACCESS_KEY_ID
変数と AWS_SECRET_ACCESS_KEY
変数が自動的に読み込まれます。
すでにファイルに付けられている名前を使用する代わりに、別のカスタマイズされた名前をオブジェクトに割り当てることができます。これは、S3 バケットでの意図しない上書きを防止するために役立ちます。この名前は、たとえば、ユーザーのアカウントの ID に関連付けることができます。そうしない場合、スペースやその他の問題がある文字が含まれる場合に備えて、名前を適切に引用するための何らかの方法を提供する必要があります。さらに、この段階で、特定の種類のファイルへのアクセスを制限するために、アップロードされたファイルのチェックを提供できます。たとえば、.png
ファイル以外は処理の続行を許可しないという単純なチェックを実装できます。
最後に、ユーザーがアバターをアップロードし、フォームに入力し、「Submit」 (送信) をクリックした後にアカウント情報を受信するためのビューを app.js
内に作成します。
app.post('/save-details', (req, res) => {
// TODO: Read POSTed form data and do something useful
});
この関数は現在、単なるスタブです。送信されたプロフィール情報をアプリで読み取って保存し、ユーザーのアカウント詳細の残りの項目と正しく関連付けるためには、この関数を完成させる必要があります。
アプリの実行
これで、S3 への直接アップロードを実行するための準備がすべて整いました。アップロードをテストするには、すべての変更を保存し、heroku local
を使用してアプリケーションを起動します。
これを成功させるには、Procfile が必要になります。詳細については、「Heroku スターターガイド (Node.js)」を参照してください。アプリケーションをローカルで実行する前に、自分のマシンの環境変数を正しく設定することも忘れないでください。
$ heroku local
15:44:36 web.1 | started with pid 12417
Ctrl+C
を押すとプロンプトに戻ります。アプリケーションで 500
エラー (またはその他のサーバー関連の問題) が発生した場合、デバッグモードでサーバーを起動し、ターミナルエミュレータで出力を確認して問題の解決に役立ててください。
$ DEBUG=express:* node app.js
まとめ
この記事では、Node.js を使用してブラウザから Amazon S3 に直接アップロードし、アップロードリクエストに一時的に署名する方法について説明しました。このガイドおよびコンパニオンコードでは Express フレームワークを主に扱っていますが、アイデア自体は他の Node アプリケーションにも簡単に応用できます。