はじめに
Laravel Vapor(AWS Lambda環境)でファイルアップロード機能を構築する際、
Controllerでファイルを受け取る通常の構成ではすぐにアップロード制限に引っかかってしまいます。
今回は、Vapor環境で直面したアップロード制限の課題と、それを解決するために採用した
「S3直接アップロード(Presigned URL)」の実装方法について解説します。
問題:Vapor(Lambda)のアップロード制限が厳しい
初期の実装では、一般的な「フロントエンド → Laravel (Controller) → S3」というフローを採用していました。
しかしVapor(=AWS Lambda)にはいくつか制約があります。
- リクエストサイズ制限(API Gateway経由)
- 実行時間制限
- メモリ制約
その結果、
- 少し大きいファイルでアップロード失敗
- タイムアウト発生
- 安定しない
といった問題が発生しました。
解決策:S3へ直接アップロードする
ブラウザから直接S3へアップロードする構成に切り替えました。

1. Laravelで presigned URL(署名付きURL)を発行
2. フロントからそのURLへ直接PUT
3. アップロード後にサーバーへ通知
これでサーバーを経由しないアップロードになります。
実装例
① サーバー側:presigned URLを発行
use Illuminate\Support\Facades\Storage;
public function generateUploadUrl()
{
$path = 'uploads/' . uniqid();
$url = Storage::disk('s3')->temporaryUrl( $path, now()->addMinutes(5), [ 'ContentType' => 'application/octet-stream', ] );
return response()->json([ 'url' => $url, 'path' => $path, ]);
}
② フロント側:直接アップロード
async function upload(file) {
const res = await fetch('/api/upload-url')
const { url, path } = await res.json()
await fetch(url, {
method: 'PUT',
headers: {
'Content-Type': file.type,
},
body: file,
})
// アップロード完了後に通知
await fetch('/api/upload-complete', {
method: 'POST',
body: JSON.stringify({ path }),
headers: {
'Content-Type': 'application/json',
},
})
}
運用上の注意点と「ハマりどころ」
CORS設定
S3側でCORS設定が必要です。
[
{
"AllowedHeaders": ["*"],
"AllowedMethods": ["PUT"],
"AllowedOrigins": ["*"],
"ExposeHeaders": []
}
]
Content-Typeを合わせる
presigned URL発行時とアップロード時のContent-Typeが違うと失敗します。
完了通知前の離脱対策(ゴミ掃除)
この構成では、アップロードはS3に直接行われるため、
- アップロードは成功
- しかしLaravelへの完了通知が送られない
といったケースが発生する可能性があります。
この場合、S3上に未使用ファイルが残り続けます。
そのため実際には`uploads/tmp/` のような一時領域にアップロードし、
S3のLifecycleルールで一定時間後に自動削除するなどの対応が必要です。
まとめ
Vapor環境でファイルアップロードを扱う場合は、Laravel経由ではなく、S3直接アップロードにしておくのが無難です。