【AWS S3】大容量ファイルアップロードの最適解:Presigned URL × Multipart Upload 実装ガイド

数GB単位の動画やログファイルをユーザーにアップロードさせる機能を実装する際、単純なフォーム送信やサーバー経由のストリーム転送を行っていませんか?そのアプローチは、サーバーのメモリ枯渇(OOM)や帯域幅の圧迫、そして504 Gateway Timeoutの温床となります。

AWS S3 Presigned URL(署名付きURL)とMultipart Uploadの組み合わせは、この問題を解決する「業界標準」のアーキテクチャです。サーバーを介さずにクライアントからS3へ直接、かつ分割してファイルを転送することで、スケーラビリティと信頼性を劇的に向上させます。

なぜ「サーバー経由」ではダメなのか?

アナロジー:引っ越し業者のトラックと倉庫の鍵

従来の「サーバー経由アップロード」は、あなたが引っ越しの荷物(ファイル)を一度「仲介業者(サーバー)」の小さな事務所に運び込み、業者がそれを改めて「巨大倉庫(S3)」へ運ぶようなものです。荷物が巨大だと、仲介業者の事務所はパンクし、他の客(リクエスト)をさばけなくなります。

Presigned URLは、倉庫の管理人が発行する「一時的な合鍵」です。これをあなた(クライアント)に渡すことで、あなたは仲介業者を通さず、直接倉庫の指定場所に荷物を搬入できます。

さらにMultipart Uploadは、巨大な家具を分解して運ぶ手法です。もし途中で一つの部品を落としても、その部品だけ運び直せば良く、最初からやり直す必要がありません。

技術的なメリットは以下の通りです:

  • サーバー負荷ゼロ: ファイルデータがアプリケーションサーバーを通過しないため、Node.jsのイベントループやメモリを消費しません。
  • コスト削減: EC2やLoad Balancerのデータ転送量を大幅に削減できます。
  • 耐障害性: ネットワークが不安定でも、失敗したパート(Chunk)のみを再送するだけで済みます。

実装:AWS SDK v3によるアーキテクチャ

ここでは、Node.js(Backend)と一般的なFrontend(React等)を想定した実装フローを解説します。AWS SDKは最新のv3を使用します。

Step 0: 必要なパッケージのインストール

npm install @aws-sdk/client-s3 @aws-sdk/s3-request-presigner

Step 1: Backend - アップロードの初期化とURL生成

クライアントから「ファイル名」と「パート数」を受け取り、S3に対してマルチパートアップロードを開始します。その後、各パートごとの署名付きURLを生成して返します。

import { 
  S3Client, 
  CreateMultipartUploadCommand, 
  UploadPartCommand,
  CompleteMultipartUploadCommand 
} from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";

const s3Client = new S3Client({ region: "ap-northeast-1" });
const BUCKET_NAME = "your-bucket-name";

// 1. アップロード初期化 & URL生成API
export const initiateUpload = async (fileName, totalParts) => {
  // A. マルチパートアップロードの開始を宣言
  const createCommand = new CreateMultipartUploadCommand({
    Bucket: BUCKET_NAME,
    Key: fileName,
    // 必要に応じてContentTypeなどを設定
  });
  const { UploadId } = await s3Client.send(createCommand);

  // B. 各パート用のPresigned URLを生成
  const partPromises = [];
  for (let i = 0; i < totalParts; i++) {
    const partNumber = i + 1;
    const command = new UploadPartCommand({
      Bucket: BUCKET_NAME,
      Key: fileName,
      UploadId,
      PartNumber: partNumber,
    });
    
    // URLの有効期限は短めに(例: 15分)
    partPromises.push(
      getSignedUrl(s3Client, command, { expiresIn: 900 }).then((url) => ({
        partNumber,
        url,
      }))
    );
  }

  const parts = await Promise.all(partPromises);
  return { uploadId: UploadId, parts };
};

Step 2: Frontend - 並列アップロードの実装

受け取ったURLに対してPUTリクエストを送ります。重要なのは、S3から返されるETagヘッダーを取得することです。

CORS設定の注意点: S3のバケット設定(CORS)で、ExposeHeadersETagを含めないと、ブラウザ側のJavaScriptからETagを読み取れず、アップロードを完了できません。

// Frontend: ファイルを分割してアップロード
const uploadFile = async (file, uploadData) => {
  const CHUNK_SIZE = 5 * 1024 * 1024; // 5MB (S3の最小制限)
  const uploadPromises = uploadData.parts.map(async (part) => {
    const start = (part.partNumber - 1) * CHUNK_SIZE;
    const end = Math.min(start + CHUNK_SIZE, file.size);
    const chunk = file.slice(start, end);

    const response = await fetch(part.url, {
      method: "PUT",
      body: chunk,
    });

    if (!response.ok) throw new Error("Upload failed");

    // ETagはダブルクォートで囲まれている場合があるため除去処理が必要な場合も
    const eTag = response.headers.get("ETag");
    return { PartNumber: part.partNumber, ETag: eTag };
  });

  // 並列実行(本番ではp-limit等で同時接続数を制御することを推奨)
  const partsInfo = await Promise.all(uploadPromises);
  
  // Backendへ完了通知を送る
  await completeUpload(uploadData.uploadId, partsInfo);
};

Step 3: Backend - アップロードの完了

全てのパートが揃ったら、S3に結合を指示します。

// 2. アップロード完了API
export const completeUpload = async (fileName, uploadId, parts) => {
  // partsは { PartNumber, ETag } の配列である必要があり、PartNumber順にソート推奨
  const sortedParts = parts.sort((a, b) => a.PartNumber - b.PartNumber);

  const command = new CompleteMultipartUploadCommand({
    Bucket: BUCKET_NAME,
    Key: fileName,
    UploadId: uploadId,
    MultipartUpload: {
      Parts: sortedParts,
    },
  });

  await s3Client.send(command);
  return { status: "success", location: `s3://${BUCKET_NAME}/${fileName}` };
};

コストトラップに注意: マルチパートアップロードが途中で中断された場合、アップロードされた「不完全なパート」はS3に残存し、ストレージ料金が発生し続けます。必ずS3のライフサイクルルールを設定し、「不完全なマルチパートアップロードを7日後に削除する」設定を有効にしてください。

Frequently Asked Questions

Q. ファイルサイズの制限はありますか?

A. はい。S3の仕様上、マルチパートアップロードの各パート(最後のパートを除く)は最低5MB以上である必要があります。ファイルサイズが5MB未満の場合は、マルチパートではなく通常のPutObjectCommand用の署名付きURLを使用する分岐処理を入れるのが一般的です。

Q. 署名付きURLのセキュリティリスクは?

A. 署名付きURLは、発行時に指定した操作(この場合はPUT)とリソースに対してのみ有効です。有効期限(expiresIn)を必要最小限(例: 15分〜1時間)に設定することでリスクを最小化できます。また、サーバー側で認証済みのユーザーに対してのみURLを発行することが前提です。

Q. アップロード中にネットワークが切れた場合は?

A. クライアント側で再試行ロジックを実装してください。失敗したパートだけを再度アップロードすれば良いため、ネットワーク帯域の無駄がありません。既存のUploadIdを使い回すことで、アップロードを再開(Resume)するUXも構築可能です。

Post a Comment