目的
はじめまして、AWS初学者の宮崎です。
これまではC#エンジニアとして実装中心の開発に携わっていたためクラウドには触れる機会がありませんでしたが、最近AWSを使う機会が増えてきたことをきっかけに学習を始めました。
現在、少しずつAWSを学習していますが、テキストベースで学習するよりも実際にハンズオン形式で手を動かしたほうが楽しい(理解も深まる)ので、基本的なAWSサービスを用いてセキュアなアップロードを試してみたいと思います。
それでは早速いってみましょう!
構成図

全体的な流れとしては下記になります。
- フロントエンドから「API Gateway」へPOSTリクエストを送信する
- 「API Gateway」から「Lambda」を呼び出す
- 「Lambda」が「S3」の署名付きURLを発行してフロントエンドにレスポンスを返す
- 返ってきた署名付きURLにファイルをアップロードする
署名付きURL経由でアップロードすることの利点
- セキュリティ(認証・認可の委譲)
- クライアントに直接AWS資格情報を渡す必要がない
→代わりに一時的な「署名付きURL」を発行することで限定的な権限でS3へアクセス可能
※今回の場合だとPutObjectの権限のみ付与する - URLには有効期限が設定可能で期限切れ後はS3へアップロードできない
- クライアントに直接AWS資格情報を渡す必要がない
- サーバーの負荷軽減
- ファイルアップロードを自前サーバー経由で受け取った場合、
クライアント → アプリサーバー → S3という流れになり、これだとファイルサイズが大きかったり、高トラフィックになったりするとサーバーの負担が大きくなってしまいます。
そのためアップロードの流れをクライアント→S3 という風に直接行うことでサーバーの負荷が軽減されます。
- ファイルアップロードを自前サーバー経由で受け取った場合、
各AWSサービスの設定および実装
0.事前準備
まずは「API Gateway」「Lambda」「S3」の各種設定を行っていきます。
【API Gateway】
AWSコンソール > API Gateway > API > APIを作成
・APIタイプを選択で「HTTP API」タイプを構築
各APIタイプごとの違いはタイプ選択時の画面内に記載されていますが、それでも不明の場合は公式ドキュメントで確認します。
今回はHTTP APIでよさそう。
REST API と HTTP API は、いずれも RESTful API 製品です。REST API は HTTP API よりも多くの機能をサポートしていますが、HTTP API は低価格で提供できるように最小限の機能で設計されています。API キー、クライアントごとのスロットリング、リクエストの検証、AWS WAF の統合、プライベート API エンドポイントなどの機能が必要な場合は、REST API を選択します。
https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/http-api-vs-rest.html

ステップ1:APIを設定
・API名に「FileUploadAPI」を入力
・IPアドレスのタイプに「IPv4」を選択
・統合は一旦未設定 → Lambda作成後に追加するため
・「次へ」を選択
ステップ2:ルートを設定
・ルートを設定は一旦保留 → Lambda作成後に追加するため
・「次へ」を選択
ステップ:ステージを定義
・ステージ名は「$default」のまま
・「次へ」を選択

・ルートの作成を選択
・「POST」を選択
※デフォルトは「ANY」ですが今回はPOSTリクエストのみを想定しているため変更
・「/upload-url」を入力
→API Gateway > API 画面で作成したAPIが表示されていれば作成完了です!
【Lambda】
AWSコンソール > Lambda > 関数 > 関数の作成

・一から作成
・関数名「CreateUploadUrl」を入力
・ランタイム「Python 3.13」を選択
・アーキテクチャ「x86_64」を選択
・デフォルトの実行ロールの変更で「基本的なLambdaアクセス権限で新しいロールを作成」を選択
・その他の構成はすべて未チェック
・「関数の作成」ボタンを押下
→Lambda > 関数画面で作成した関数が表示されていればOKです!
ちなみにLambda作成時に新しくロールも作成したので確認してみましょう。

【S3】
AWSコンソール > Amazon S3 > バケット > バケットを作成
・バケットタイプ:汎用
・バケット名:「photoup-sample」
・オブジェクト所有者:ACL無効
・パブリックアクセス:すべてブロック
・バージョニング:無効
・タグ:未設定
・デフォルトの暗号化:SSE-S3
・バケットキー:無効(SSE-S3の場合は関係ないため)
・オブジェクトロック:無効
・「バケットを作成」を選択
→まだオブジェクトはアップロードしていないので下記のままでOKです!

一旦AWSの各種サービスを作成したので、あとは下記の流れに沿って、AWSサービス間の統合や実装を進めていきます。
1.フロントエンドから「API Gateway」へPOSTリクエストを送信する
2.「API Gateway」から「Lambda」を呼び出す
3.「Lambda」が「S3」の署名付きURLを発行してフロントエンドにレスポンスを返す
4.返ってきた署名付きURLにファイルをアップロードする
1.フロントエンドから「API Gateway」へPOSTリクエスト
まずはローカルの開発用サーバーである「vite」「TypeScript」「React」を用いてAPI GatewayへPOSTリクエストを送信できるようにします。

import { useState } from 'react';
import type { PresignReq, PresignRes } from './api';
import { getPresignedUrlForFile, uploadToS3 } from './api';
export default function Uploader() {
const [msg, setMsg] = useState('');
async function handleFile(file: File) {
try {
setMsg('署名URLを取得中...');
const req: PresignReq = {
key: `images/${Date.now()}-${file.name}`,
contentType: file.type,
};
const { url } = await getPresignedUrlForFile(req); // 署名付きURLの取得
} catch (e: any) {
setMsg('エラー: ' + e.message);
}
}.tsxファイルの一部。
一旦オブジェクトのキーが「images/日付-ファイル名」となるように記述しているのと、最終的に「url」には返ってきた署名付きURLが入る想定です。
export type PresignReq = { key: string; contentType: string };
export type PresignRes = { url: string };
export async function getPresignedUrlForFile(
req: PresignReq
): Promise<PresignRes> {
// ${import.meta.env.VITE_API_BASE}は下記URLがセットされている
// https://<APIのID>.execute-api.<リージョンコード>.amazonaws.com
const res = await fetch(`${import.meta.env.VITE_API_BASE}/upload-url`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(req),
});
if (!res.ok) throw new Error('presign failed');
return res.json();
}getPresignedUrlForFile関数の実装部分。
署名付きURLを取得するためにメソッドをPOST、ヘッダーやボディを設定しています。
※APIのIDやリージョンコードは間違えないようにしましょう
2.「API Gateway」から「Lambda」を呼び出す
現時点では API GatewayとLambdaをそれぞれ作成しただけなので、次はそれぞれを紐づけます。(統合をアタッチ)
※統合・・・ルートがリクエストを受信したときに呼び出すバックエンドリソース
・API Gateway > API > ルート – FileUploadAPI(ルートID)> 統合
→「統合を作成してアタッチする」を選択

・統合ターゲットに「Lambda関数」を選択
・AWSリージョンには「0.事前準備」で作成したLambdaのリージョンを指定
・Lambda関数には「0.事前準備」で作成したLambda関数を設定
・タイムアウトはデフォルト値の「30000ミリ秒」を設定
・ペイロード形式のバージョンもデフォルト値の「2.0」を設定
・アクセス許可を呼び出すはチェック
・「作成」ボタンを押下
API Gateway > API > ルート – FileUploadAPI(ルートID)> 統合
→統合がアタッチされていたらOKです!

3.「Lambda」が「S3」の署名付きURLを発行してフロントにレスポンスを返す
Lambda作成時には下記のようになっているので「S3」の署名付きURLを発行。そしてレスポンスとして返すような処理を実装していきましょう。

[(前略)]
s3 = boto3.client(
"s3",
region_name=REGION_NAME,
config=Config(signature_version="s3v4",
s3={"addressing_style": "virtual"})
)
def _json_response(status: int, body: Dict[str, Any],headers: Dict[str, str] | None =None) -> Dict[str, Any]:
return {
"statusCode": status,
"headers": {"Content-Type": "application/json", **(headers or {})},
"body": json.dumps(body),
}
def lambda_handler(event, context):
method = (
event.get("httpMethod")
or (event.get("requestContext", {}).get("http", {}).get("method"))
or "GET"
).upper()
if method != "POST":
return _json_response(405, {"message": "Method Not Allowed"}, {"Allow": "POST"})
try:
body_raw = event.get("body") or "{}"
payload = json.loads(body_raw)
content_type: str = payload.get("contentType") or "application/octet-stream"
expires: int = DEFAULT_EXPIRES
key: Optional[str] = payload.get("key") # キー名決定
# PUT用 presigned URL
params = {
"Bucket": BUCKET,
"Key": key,
"ContentType": content_type,
}
url = s3.generate_presigned_url(
ClientMethod="put_object",
Params=params,
ExpiresIn=expires,
HttpMethod="PUT",
)
return _json_response(200, {
"url": url, # 署名付きURL
"key": key,
"expiresIn": expires,
"headers": {"Content-Type": content_type},
})AWS SDK for Python(boto3)のライブラリを用いて実装しています。
4.返ってきた署名付きURLにファイルをアップロードする
「(前略)」
async function handleFile(file: File) {
try {
setMsg('署名URLを取得中...');
const req: PresignReq = {
key: `images/${Date.now()}-${file.name}`,
contentType: file.type,
};
const { url } = await getPresignedUrlForFile(req); // 署名付きURLの取得
// 下記、S3アップロード用に追記
setMsg('アップロード中...');
await uploadToS3(url, file); // 署名付きURLへPUT
setMsg('完了! ');
} catch (e: any) {
setMsg('エラー: ' + e.message);
}
}先ほどの.tsxファイルです。
返ってきた署名付きURLを引数して渡し、S3アップロード用の関数を呼び出している記述を追記しました。
export async function uploadToS3(url: string, file: File): Promise<void> {
const res = await fetch(url, {
method: 'PUT',
headers: { 'Content-Type': file.type },
body: file,
});uploadToS3関数の実装部分になります。
受け取った署名付きURLとファイルを受け取り、そのままfetchでPUTリクエストを送信しています。
以上でAWSサービスの各設定や実装部分が完了したので、最後に実際にファイルをアップロードして確認してみたいと思います。


AWSコンソールからでもオブジェクトがアップロードされていることを確認できました。
つまづいたところ
【CORS(クロスオリジンリソース共有)の設定】
実際に.jpgファイルをアップロードしようとすると下記エラーになりました。

開発者ツールで確認すると、異なるオリジン間のアクセスを制限するWebブラウザのセキュリティに弾かれたのが原因みたいでした。Access to fetch at 'https://<ルートID>.execute-api.ap-northeast-1.amazonaws.com/upload-url' from origin 'http://localhost:5173' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.
上記のエラーメッセージに含まれているCORSとは
オリジン間リソース共有 (Cross-Origin Resource Sharing, CORS) は、 HTTP ヘッダーベースの仕組みを使用して、あるオリジンで動作しているウェブアプリケーションに、異なるオリジンにある選択されたリソースへのアクセス権を与えるようブラウザーに指示するための仕組みです。
MDNから引用 https://developer.mozilla.org/ja/docs/Web/HTTP/Guides/CORS
今回の場合だと、異なるオリジン(プロトコル+ドメイン+ポート番号)からのアクセスを許可するためにAWS側からクライアントに返す際のヘッダーに「Access-Control-Allow-Origin」を含めればよさそうですね。
ただ、API GatewayからでもCORSの設定が可能なので今回は画面から設定を行いたいと思います。
またS3のCORS設定も同様に必要なため、併せて設定を確認したいと思います。
1.API GatewayのCORS設定
API Gateway > API > FileUploadAPI(ルートID)> CORS
→CORSの設定画面から各項目を下記のように設定する。

2.S3のCORS設定
Amazon S3 > バケット > バケット名 > アクセス許可タグ
→CORSの設定を下記のようにしました。

所感
今回はあくまでAPI Gateway,Lambda,署名付きURLの理解を深めることを目的としていたため、より実践的な実装やログ監視などは後回しにしましたが、やりたかったことが実現できた瞬間の気持ちよさはやはり別格ですね。
これからも気になるAWSサービスやユースケースがあれば手を動かしながら試してみたいと思います。
それではまた!


