S3のPresigned URLによるファイルアップロード実装でハマったことまとめ
S3にPresigned URLでファイルアップロードを実装する中で遭遇した問題を、段階ごとに整理しました。
同じような状況で悩んでいる人の参考になればと思います。
1. はじめに: Node.js Deprecation Warning
最初に以下のような警告が表示されました。
[DEP0060] util._extend is deprecated
これはNode.jsのDeprecation Warningです。
- 単なる警告
- 機能には影響なし
-
Object.assign()への置き換えが推奨されている
つまり、この警告は今回のアップロード失敗とは直接関係ありませんでした。
実際の問題とは無関係
2. CORSエラー発生
次に、ブラウザからS3へ直接リクエストした際にCORSエラーが発生しました。
No 'Access-Control-Allow-Origin'
原因
ブラウザからS3へ直接リクエストする場合、S3バケット側にCORS設定が必要です。
今回の原因は、S3バケットにCORS設定が無かったため、ブラウザによってリクエストがブロックされていたことでした。
解決方法
S3バケットの以下の画面からCORSを設定します。
S3 → Permissions → CORS
設定例:
[
{
"AllowedOrigins": ["http://localhost:3000"],
"AllowedMethods": ["GET", "PUT", "POST", "HEAD"],
"AllowedHeaders": ["*"],
"ExposeHeaders": ["ETag"]
}
]
これでブラウザからS3へのリクエストがCORSでブロックされなくなります。
3. CORS解決後もPUTリクエストが失敗
CORS設定を追加したことで、CORSエラー自体は解消されました。
しかし、その後もファイルアップロードのPUTリクエストは失敗しました。
ここで問題はCORSではなく、別の原因に移ります。
4. AccessDeniedエラー
PUTリクエストのレスポンスを見ると、以下のようなエラーが返っていました。
<Error>
<Code>AccessDenied</Code>
<Message>
User: arn:aws:iam::...:user/my-user is not authorized to perform: s3:PutObject
</Message>
</Error>
原因
エラーメッセージの通り、IAMユーザー my-user に s3:PutObject 権限がありませんでした。
現在のバケットポリシーでは、以下のように s3:GetObject のみが許可されていました。
"Action": "s3:GetObject"
つまり、読み取りは可能ですが、アップロードはできない状態でした。
解決方法
IAM Userに s3:PutObject 権限を追加します。
例:
{
"Effect": "Allow",
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::バケット名/uploads/*"
}
これにより、対象パス配下へのアップロードが可能になります。
5. Presigned URLの動作方式
ここで重要なのが、Presigned URLの権限の考え方です。
Presigned URLは、URLを生成した主体の権限で動作します。
つまり、以下のようになります。
Presigned URLを生成したAWS主体 = 権限の基準
フロントエンドからPUTリクエストを送っているユーザーではなく、Presigned URLを生成したIAM UserやIAM Roleの権限が使われます。
そのため、フロントエンド側のユーザーが誰かは本質的には関係ありません。
6. 「なぜ my-user なのか?」問題
実装としてはRoleベースで動かしているつもりでした。
しかし、エラーメッセージには以下のように my-user が表示されていました。
arn:aws:iam::ACCOUNT_ID:user/my-user
原因
ローカル環境で実装したあと、masterにマージすればECS上ではRoleベースで動作する構成でした。
しかし今回の検証時は、ローカル環境で動かしているにもかかわらず、ECSのTask Roleが適用されているものだと思い込んでいました。
そのため、Presigned URLを生成しているAWS主体がECS Roleではなく、ローカルに設定されているIAM Userであることに気づくまで少しハマりました。
この過程で新しく分かったことは、AWS SDKがcredentialを自動的に解決する順番です。
AWS SDKは一般的に、以下のような順番でcredentialを選択します。
- 環境変数
- ローカルのcredentialsファイル(
~/.aws/credentials) AWS_PROFILE- IAM Role
つまり、ローカル環境にIAM Userのcredentialが設定されている場合、ECSのRoleではなく、そのローカルcredentialが使われることがあります。
AWS SDKはcredentialを自動的に解決します。
その際、一般的には以下のような順番でcredentialが選択されます。
- 環境変数
- ローカルcredentialsファイル
AWS_PROFILE- IAM Role
ローカル環境に ~/.aws/credentials が存在し、そこに my-user のcredentialが設定されている場合、SDKはそのIAM Userを使用します。
そのため、Roleベースで実装しているつもりでも、ローカルではIAM Userのcredentialが優先されていました。
7. ECS Roleとの関係
ECS上で正常にTask Roleが使われている場合、呼び出し元のARNは以下のようになります。
arn:aws:sts::ACCOUNT_ID:assumed-role/ROLE_NAME/SESSION
一方、ローカル環境では以下のようになっていました。
arn:aws:iam::ACCOUNT_ID:user/my-user
理由
ECSとローカルではcredentialの取得元が異なります。
ECS → Task Roleが自動適用される
ローカル → ローカルのIAM User credentialが使用される
つまり、ECSではRoleが使われますが、ローカルでは ~/.aws/credentials や環境変数に設定されたIAM Userが使われます。
8. 最終的な原因整理
今回の最終的な原因は以下の流れでした。
ローカル環境
↓
AWS SDKが my-user credential を使用
↓
my-user に s3:PutObject 権限がない
↓
Presigned URLもPutObject権限を持たない
↓
S3へのPUTリクエストでAccessDeniedが発生
つまり、問題はPresigned URLそのものではなく、Presigned URLを生成しているAWS主体の権限でした。
9. 解決方法
方法1: ローカルIAM Userに権限を追加する
一番簡単な方法は、ローカルで使われているIAM Userに s3:PutObject 権限を追加することです。
{
"Effect": "Allow",
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::バケット名/uploads/*"
}
ローカル開発だけであれば、この方法が最も手早いです。
方法2: Roleベースでテストする
本番環境と同じRoleで動作確認したい場合は、aws sts assume-role を使用します。
または、AWS CLIのprofileを設定して、特定のRoleを使うようにします。
例:
aws sts assume-role \
--role-arn arn:aws:iam::ACCOUNT_ID:role/ROLE_NAME \
--role-session-name local-test-session
Roleを使ってcredentialを発行し、そのcredentialを環境変数に設定してからアプリケーションを起動します。
方法3: 環境ごとにcredentialを分離する
ローカル環境とECS環境で、credentialの使い方を明確に分ける方法です。
ローカル → IAM User または assume-role
ECS → Task Role
このように環境ごとに利用するAWS主体を明確にしておくと、原因調査がしやすくなります。
10. デバッグのコツ
AWS関連の権限エラーが出た場合は、まず「今どのAWS主体で実行されているか」を確認するのが重要です。
AWS CLIでは以下のコマンドで確認できます。
aws sts get-caller-identity
コードから確認する場合は、以下のように GetCallerIdentityCommand を使います。
import { STSClient, GetCallerIdentityCommand } from "@aws-sdk/client-sts";
const client = new STSClient({});
const res = await client.send(new GetCallerIdentityCommand({}));
console.log(res);
出力例:
{
"UserId": "XXXXXXXXXXXXXXXXXXXXX",
"Account": "123456789012",
"Arn": "arn:aws:iam::123456789012:user/my-user"
}
この Arn を見れば、現在どのIAM UserまたはIAM Roleで実行されているかが分かります。
結論
今回の問題の本質は、次の一つでした。
今どのAWS主体でリクエストしているのか
S3 Presigned URLまわりで問題が起きた場合は、以下の3つを切り分けると原因を追いやすくなります。
CORS → ブラウザとS3の通信設定の問題
AccessDenied → IAM権限の問題
Role vs User → 実行環境とcredential解決の問題
Presigned URLは便利ですが、実際には「URLを生成したAWS主体の権限」に強く依存します。
そのため、アップロードが失敗した場合は、まずCORSだけでなく、Presigned URLを生成しているIAM UserやIAM Roleに s3:PutObject 権限があるかを確認することが重要です。