LaravelからS3へファイルを保存する処理で500が出たとき、S3の権限だけを見直しても調査が終わりませんでした。AWS CLIやS3Clientで保存できることと、Laravelのディスクやメディア管理ライブラリを通した処理が成功することは、別に確認する必要がありました。
当時の記録には、ACLを受け付けないバケットへの送信、カスタムドライバーのURL生成、認証設定、設定キャッシュの確認が残っています。今回は、それぞれを一つの「S3エラー」にまとめず、失敗した操作を分けて追う流れを整理します。
対象は、LaravelのStorageや、Storageを使うライブラリでS3を扱っている人です。業務固有のコードやデータは使わず、調査用の小さな例へ置き換えています。
CLIで保存できても、画面の処理は成功しなかった
2025年10月の調査では、CLIやS3Client側の保存と、Laravel・メディア管理ライブラリ経由の保存を比べています。前者が通っていても、後者には失敗が残っていました。
ここで確認したかったのは、「S3へつながるか」だけではありません。同じ認証主体・バケット・操作を使っているか、Laravel側でどのディスクを選び、保存要求に何を付けているかを揃える必要がありました。
また、画面の一操作には、保存した後のURL生成やメタ情報の登録も含まれます。画面が500でも、保存要求そのものが失敗したとは限りません。オブジェクトの存在と、その後の処理を分けて調べます。
まず、どの操作で止まるかを小さくする
調査では、業務の画面を何度も操作するより、専用の検証環境・ディスクでStorageだけを動かした方が、確認対象を狭められます。次はTinkerに入力する調査例です。実データを使わず、自分で作った検証ファイルだけを対象にします。
use Illuminate\Support\Facades\Storage;
$disk = Storage::disk('s3'); // 実際に失敗するディスク名へ合わせる。
$path = 'article-check/upload.txt';
// 失敗時に例外を追えるよう、検証環境のdisk設定でthrowをtrueにする。
if (!$disk->put($path, 'sample')) {
throw new RuntimeException('Upload failed');
}
$stored = $disk->exists($path);
// 保存の確認と、URL生成の確認を同じ式にしない。
$url = $disk->url($path);
putまで通るのか、existsが通るのか、urlで止まるのかを別に見ます。URLや署名付きURL、例外全文は公開する調査ログへそのまま貼りません。読み書き・存在確認には異なる権限や要求が関わるため、ここでも一つの成功を別の操作へ広げません。
この例は既存の同名ファイルを上書きするので、専用バケットか一意の検証パスを用意して実行します。確認後の削除も、今回自分で作ったファイルに限定します。本番データで試す手順にはしません。
ACLのエラーは、権限不足とは分けて読む
当時の記録にはAccessControlListNotSupportedがありました。バケット側でACLが無効なのに、保存する側がACL付きで要求していないかを確認する場面です。
S3 Object Ownershipの公式資料では、Bucket owner enforcedではACLが無効となり、ACLを指定しない要求やbucket-owner-full-controlを指定する要求が受け付けられます。それ以外のACLを伴うアップロードは拒否されます。
「権限が足りないはず」と考えてIAMを広げても、要求とバケット設定の不整合は残ります。Storageのvisibility指定、利用ライブラリ、アダプターが実際に送る要求を調べる方が、原因に近づきます。
対応は、バケットの所有権設定とアップロード要求の整合を取ることです。ACLを外せるかは、使用中のアダプター・バージョンの実装まで確認します。設定へACL: nullを加えれば必ず送信されなくなる、といった未検証の回避策はここでは示しません。
カスタムドライバーが必要な場合も、保存だけでなく読み取り・削除・URL生成・署名付きURLまで、アプリが使う機能を洗い出してから作ります。
保存できるドライバーでも、URLを作れるとは限らない
もう一つの記録がThis driver does not support retrieving URLsです。これは、URL生成の機能までドライバーから提供されているかを調べる入口になります。
LaravelのFilesystemAdapter実装を見ると、url()は対応するメソッドやアダプターを探し、対応できなければこの例外を投げます。記事作成時に確認した12.xのコードであり、当時の実行バージョンそのものではありません。
Laravelでは、Flysystemのアダプターと、Storageとして使うLaravel側のラッパーも区別します。カスタムドライバーがS3へ書き込めても、標準S3ドライバーと同じURL関連機能を持つとは限りません。当時の記録では、カスタム実装の設定保持とURL生成も見直し対象でした。
なお、URLが文字列として返ることは、閲覧権限があることの証明にはなりません。非公開ファイルなら、認可済みの利用者へ一時URLを渡すなど、読み取り方法を別に設計します。
EC2の認証設定と、Laravelが読んだ設定を合わせる
認証をIAMロールに任せる場合は、アプリケーションから空文字のキーなどを明示的に渡し、意図した認証取得を妨げていないかも調べます。当時も認証設定が確認対象になっています。
一方で、環境ファイルだけを直しても、Laravelが古い設定を使っていれば結果は変わりません。次は値そのものを表示せず、選んだディスクとキー設定の有無を見る例です。
$config = config('filesystems.disks.s3');
[
'driver' => $config['driver'] ?? null,
'key_configured' => !empty($config['key']),
'secret_configured' => !empty($config['secret']),
'throw' => $config['throw'] ?? false,
];
対象ディスクが別なら、その名前へ合わせます。設定キャッシュの再生成やPHPプロセスの再起動は、実際の配備手順に沿って行います。Webからの処理とTinkerでは、実行ユーザーや環境も異なり得るので、Tinkerだけで完了とは判断しません。
修正後は、元の経路まで戻って確認する
確認を終える条件は、Storageだけの保存成功ではなく、もともと失敗した経路が正常に動くことです。
| 確認対象 | 見たい結果 |
|---|---|
| 認証と対象ディスク | 想定した主体・設定で動作する |
| 保存要求 | バケットの所有権設定と整合し、保存できる |
| URL生成 | アプリが使うURL機能で例外が出ない |
| 非公開ファイルの閲覧 | 認可に従って閲覧できる |
| メディア管理・画面操作 | 元の処理と後続の登録が成功する |
今回は公式資料とLaravelの公開ソースを照合しました。ここで示したPHP例をLaravel環境で再実行したものではなく、実S3への書き込みも行っていません。