【Rails × Next.js】S3 Presigned URLの仕組みと実装方法

はじめに
こんにちは、株式会社TOKOSのナオキです!
本記事では、Presigned URLとは何かをわかりやすく解説し、Rails × Next.js環境でクライアントからS3へ直接ファイルをアップロードする方法を紹介します。
対象読者
- Rails × Next.jsを使っている方
- S3のPresigned URLの仕組みを理解したい方
- S3へのファイルアップロード機能を実装したい方
この記事で扱う内容
- Presigned URLの仕組みの解説
- AWSの事前準備(IAMユーザー・S3バケットの作成)
- Rails APIでPresigned URLを発行する実装
- Next.jsからS3へ直接ファイルをアップロードする実装
S3のPresigned URLとは?
S3に保存されたファイル(オブジェクト)は、基本的に非公開です。
アクセス権限を持つ所有者のみが閲覧(・更新)できます。
Presigned URLは、アクセス権限を持たないユーザーに対して一定期間だけファイルのダウンロードやアップロードを許可するURLを発行する仕組みです。
このURLを知っているユーザーはAWSアカウントやIAM権限が無くても、設定された有効期限内であればS3のファイルに対して閲覧やアップロードができるようになります。
署名付き URL を使用したオブジェクトの共有 - Amazon Simple Storage Service
オブジェクトのダウンロード用に署名付き URL を作成して、オブジェクトを別のユーザーと共有できるようオブジェクトを設定する方法について説明します。
docs.aws.amazon.comなぜPresigned URLを使うのか?
Presigned URLを使わない場合
Presigned URLを使わずにS3へファイルをアップロードする場合、クライアント → サーバー → S3という経路になります。
このとき、クライアントからサーバーへ画像などのファイルを送る方法としては、一般的に以下の2つがあります。
- バイナリ送信(
multipart/form-data) - Base64エンコード
しかし、これらの方法にはそれぞれ以下のようなデメリットがあります。
バイナリ送信(multipart/form-data)
- JSONデータとファイルを同時に送る場合、
multipart/form-dataの構築が複雑になりがち
Base64エンコード
- バイナリデータをBase64に変換する際、CPUへ負荷がかかる
- Base64に変換することでデータサイズが約33%増加する
また、いずれの方法でもサーバーがファイルデータを中継するため、ファイルサイズが大きいほどサーバーへの負荷も増大します。
バイナリ送信・Base64エンコードについて調べた際に以下の記事が参考になったので、気になった方はご覧ください。
JPG画像をBase64で送るとサイズが33%増えるが、Gzip圧縮すれば「ほぼ元通り」になるという話 - Qiita
当たり前なのではというはてぶが多かったので、画像のファイルフォーマット毎の差異もまとめてみました。 Web APIでJPG画像を送信する際、「バイナリ送信(multipart/form-data)で送るか」「Base64エンコードしてJSONに埋め込むか」で迷うこと...
qiita.comPresigned URLを使う場合
一方、Presigned URLを使うとクライアントからS3へ直接ファイルをアップロードでき、サーバーはPresigned URLの発行のみを行ないます。
これによりサーバーを経由する必要がなくなり、上記のようなデメリットが解消されます。
シーケンス図を見比べると、Presigned URLを使うほうがリクエストの流れが複雑に見えるかもしれません。
しかし、サーバーの負荷においてもっともネックになるのはファイルデータの受け取り・送信処理です。
Presigned URLを使えばその処理がサーバーから不要になるため、サーバーの負荷を軽減できます。
Presigned URLを使うメリット
- シンプルなファイル送信:画像などのバイナリデータをそのまま送信できるため、
multipart/form-dataの構築やBase64エンコードが不要になる - サーバー負荷の軽減:ファイル送信処理にてサーバーを経由する必要がなくなるため、サーバーの負荷を軽減できる
- セキュリティの確保:許可されたユーザーにのみアクセス権限をあたえ、一定時間後にURLが期限切れになる
- スケーラビリティ: サーバーがファイルデータを処理しないため、同時アップロード数が増えてもサーバーへの負荷を最小限に抑えることができる
Presigned URLの生成に必要な情報
Presigned URLを生成するには、以下の4つの情報が必要です。
| 項目 | 説明 |
|---|---|
| バケット名 | どのS3バケットに対してアクセスを許可するか |
| オブジェクトキー | 対象のファイルパス(例:products/images/sample.jpg) |
| HTTPメソッド | 操作の種類を指定する(下記参照) |
| 有効期限 | URLがいつまで有効かを指定する |
HTTPメソッドによって、Presigned URLで行える操作が異なります。
| HTTPメソッド | 操作 | 用途 |
|---|---|---|
| GET | ダウンロード | S3上のファイルを取得する |
| PUT | アップロード | S3にファイルを保存する |
| HEAD | メタデータの確認 | ファイル情報を確認する |
オブジェクトキーの指定方法はHTTPメソッドによって少し異なります。
- GET/HEAD:S3上に存在する既存ファイルのパスを指定
- PUT:S3のどこにどんな名前で保存するかを指定(ファイルが存在しなくてもOK)
Presigned URLを作成できるユーザー
Presigned URLは誰でも作れるわけではありません。有効なセキュリティ認証情報を持つユーザーである必要があります。
認証情報の種類によって、生成できるPresigned URLの有効期限の上限が異なります。
| 認証情報の種類 | 有効期限の上限 |
|---|---|
| IAMユーザー | 最大7日間 |
| IAMロールの認証情報 | ロールセッションの有効期限まで |
| EC2インスタンスのIAMロール認証情報 | ロール認証情報の期間(通常6時間) |
| AWS Security Token Service認証情報 | 一時的な認証情報の期間中のみ |
有効期限の上限(ツール別)
Presigned URLの有効期限は、使用するツールによっても上限が異なります。
| ツール | 説明 | 有効期限の上限 |
|---|---|---|
| AWSコンソール | ブラウザの管理画面(GUI) | 1分〜12時間 |
| AWS CLI | ターミナルでコマンド入力 | 最大7日間 |
| AWS SDK | コードに組み込んで使用 | 最大7日間 |
今回の実装ではRailsからAWS SDK(aws-sdk-s3 gem) を使用してPresigned URLを生成するため、最大7日間の有効期限を設定できます。
オススメの有効期限
AWSの推奨する有効期限はありませんが、用途に応じて以下のようなポイントを踏まえて決めるのがいいと思われます。
GET:メール等で相手に渡すことが多く、開くタイミングをこちらでは決めにくいのでやや長めに取りがちです。Presigned URLが漏洩した場合にファイルの読み取りができてしまいます。そのファイルの機密性によって有効期限を決めるのがいいと思われます。機密性が低いファイルであれば長めに設定しても問題ありません。
PUT:Presigned URLを発行してすぐ使用する場面が一般的なので短めにしがちです。Presigned URLが漏洩した場合に第三者が意図しないファイルをS3バケットに書き込めてしまうため、セキュリティ観点からも短めにしたほうがいいです。
目安はGETなら15分〜1時間、PUTなら5〜15分程度です。
AWSの事前準備
1. S3バケットの作成
S3コンソールから「バケットを作成」ボタンをクリックします。
今回はバケット名を「test-bucket-20260322」とし、デフォルトの設定で作成しました。
2. CORSの設定
S3コンソールから対象のS3バケットを選択し、「アクセス許可」タブをクリックします。
下部に「Cross-Origin Resource Sharing (CORS)」項目があるので、「編集」ボタンをクリックし、下記のJSON形式で設定を追加します。
[
{
"AllowedHeaders": ["*"],
"AllowedMethods": ["GET", "PUT"],
"AllowedOrigins": ["http://localhost:3000"]
}
]AllowedHeaders:リクエストに含めることを許可するHTTPヘッダー(例:Content-Type,Authorizationなど)AllowedMethods:許可するHTTPメソッド(例:GET,PUTなど)AllowedOrigins:リクエスト元として許可するオリジンExposeHeaders:参照を許可するレスポンスヘッダー(例:ETag,x-amz-meta-custom-headerなど)
3. IAMポリシーの作成
IAMコンソールから「ポリシー」を選択し、「ポリシーを作成」ボタンをクリックします。
JSONタブを選択し、下記のJSON形式でポリシーを作成します。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["s3:PutObject", "s3:GetObject"],
"Resource": "arn:aws:s3:::test-bucket-20260322/*"
}
]
}Version:ポリシーのバージョン、最新が2012-10-17で推奨されているバージョンStatement:ポリシーのステートメント、複数のステートメントを記述できるEffect:ポリシーの効果(AllowまたはDeny)、Allowは許可、Denyは拒否Action:許可するアクション(例:s3:PutObject,s3:GetObjectなど)Resource:許可するリソース(例:arn:aws:s3:::test-bucket-20260322/\*など)
このポリシーは、test-bucket-20260322バケットに対して、PUTメソッドとGETメソッドを許可するポリシーになります。
4. IAMユーザーの作成
IAMコンソールのサイドバーから「ユーザー」を選択し、「ユーザーを追加」ボタンをクリックします。
今回は名前を「test-user」とし、デフォルトの設定で作成しました。
5. IAMユーザーにポリシーの追加
IAMユーザーの作成が完了したら、3で作成したポリシーを追加します。
対象のIAMユーザーを選択し、「許可ポリシー」項目の「許可を追加」ボタンをクリックし、先ほど作成したポリシーを選択します。
6. IAMユーザーのアクセスキー・シークレットキーの取得
最後にアクセスキー・シークレットキーを取得します。
対象のIAMユーザーを選択し、「セキュリティ認証情報」タブをクリックし、「アクセスキーを作成」ボタンをクリックします。
以下の項目では「AWSの外部で実行されるアプリケーション」を選択します。
後はデフォルトの設定で作成します。
作成後は以下のようにアクセスキーとシークレットキーが表示されます。
これでAWSの事前準備は完了です。
RailsでPresigned URLを発行する
1. aws-sdk-s3 gemの追加
RailsにはAWSのサービスを操作する機能が標準では備わっていません。
そのため、AWSが提供しているRuby向けのSDKであるaws-sdk-s3 gemを追加する必要があります。
このgemを追加することでAws::S3::ClientやAws::S3::Presignerなどのクラスが使えるようになり、RailsからS3の操作やPresigned URLの生成が可能になります。
gem 'aws-sdk-s3'2. 環境変数の設定
AWSの事前準備で取得したアクセスキーとシークレットキー、リージョン、バケット名を環境変数に設定します。
AWS_ACCESS_KEY=取得したアクセスキー
AWS_SECRET_ACCESS_KEY=取得したシークレットキー
AWS_REGION=ap-northeast-1 # 東京リージョン
AWS_S3_BUCKET=test-bucket-202603223. S3クライアントの初期化
config/initializers/配下にファイルを作成すると、Railsアプリケーションの起動時に自動で読み込まれます。
ここでAWSの認証情報を設定しておくことで、アプリケーション全体でAws::S3::Clientなどのクラスを使用する際に、毎回認証情報を渡す必要がなくなります。
Aws.config.update(
region: ENV.fetch("AWS_REGION"),
credentials: Aws::Credentials.new(
ENV.fetch("AWS_ACCESS_KEY"),
ENV.fetch("AWS_SECRET_ACCESS_KEY")
)
)Aws.config.update:AWS SDK全体の設定を更新するメソッドregion:AWSリソースが存在するリージョンを指定(ap-northeast-1は東京リージョン)credentials:Aws::Credentialsにアクセスキーとシークレットキーを渡してAWSへの認証情報を設定
4. Presigned URLを発行するAPIの作成
Presigned URLを生成するサービスクラスとコントローラーを作成します。
class PresignedUrlService
def initialize
@client = Aws::S3::Client.new
@signer = Aws::S3::Presigner.new(client: @client)
@bucket = ENV.fetch("AWS_S3_BUCKET")
end
def generate_presigned_url(filename, content_type)
object_key = "uploads/#{SecureRandom.uuid}/#{filename}"
url = @signer.presigned_url(
:put_object,
bucket: @bucket,
key: object_key,
content_type:,
expires_in: 600 # 有効期限:10分
)
{ url:, object_key: }
end
endSecureRandom.uuidでファイル名にUUIDを付与することで、同じファイル名によるS3上のファイルの上書きを防いでいます。
module Api
class PresignedUrlsController < ApplicationController
def create
service = PresignedUrlService.new
result = service.generate_presigned_url(
params[:filename],
params[:content_type]
)
render json: result
end
end
endnamespace :api do
resources :presigned_urls, only: [:create]
end5. 動作確認
Railsサーバーをポート3001で起動し、curlでAPIを叩いてPresigned URLが返ってくることを確認します。
rails server -p 3001curl -X POST http://localhost:3001/api/presigned_urls \
-H "Content-Type: application/json" \
-d '{"filename": "test.jpg", "content_type": "image/jpeg"}'以下のようなレスポンスが返ってくれば成功です。
{
"url": "https://test-bucket-20260322.s3.ap-northeast-1.amazonaws.com/uploads/550e8400-e29b.../test.jpg?X-Amz-Algorithm=...",
"object_key": "uploads/550e8400-e29b.../test.jpg"
}返却されたurlは、S3オブジェクトのURL末尾に?X-Amz-Algorithm=...のような署名用のクエリパラメータが付与されたものです。
署名を除いた、オブジェクト自体のURLの形は次のようになります。
https://{バケット名}.s3.{リージョン}.amazonaws.com/{オブジェクトキー}
本記事の例では、次のようになります。
https://test-bucket-20260322.s3.ap-northeast-1.amazonaws.com/uploads/.../test.jpg
S3バケットはデフォルトで非公開のため、署名のないこのURLをブラウザで開いてもファイルは取得できません。
Next.jsからS3へ直接ファイルをアップロードする
1. 環境変数の設定
Rails APIのURLを環境変数に設定します。
API_BASE_URL=http://localhost:30012. アップロード処理の作成
Rails APIからPresigned URLを取得し、そのURLを使ってS3に直接ファイルをアップロードする関数を作成します。
const API_BASE_URL = process.env.API_BASE_URL
export const uploadToS3 = async (file: File) => {
try {
// 1. Rails APIからPresigned URLを取得
const presignedResponse = await fetch(`${API_BASE_URL}/api/presigned_urls`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
filename: file.name,
content_type: file.type,
}),
})
if (!presignedResponse.ok) {
throw new Error("Presigned URLの取得に失敗しました")
}
const { url, object_key } = await presignedResponse.json()
// 2. Presigned URLを使ってS3に直接アップロード
const uploadResponse = await fetch(url, {
method: "PUT",
headers: {
"Content-Type": file.type,
},
body: file,
})
if (!uploadResponse.ok) {
throw new Error("S3へのアップロードに失敗しました")
}
return object_key
} catch (error) {
console.error(error)
throw new Error("予期しないエラーが発生しました")
}
}ポイントは2つのリクエストに分かれている点です。
- Rails APIへのリクエスト:Presigned URLを取得する
- S3へのリクエスト:取得したPresigned URLに対してファイルを直接アップロードする
3. アップロードフォームの作成
ファイル選択とアップロードを行うコンポーネントを作成します。
"use client"
import { useState } from "react"
import { uploadToS3 } from "@/lib/uploadToS3"
export function FileUploadForm() {
const [file, setFile] = useState<File | null>(null)
const [uploading, setUploading] = useState(false)
const [message, setMessage] = useState("")
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
if (!file) return
setUploading(true)
setMessage("")
try {
const objectKey = await uploadToS3(file)
setMessage(`アップロード成功!(${objectKey})`)
} catch (error) {
setMessage(error instanceof Error ? error.message : "エラーが発生しました")
} finally {
setUploading(false)
}
}
return (
<form onSubmit={handleSubmit}>
<input
type="file"
accept="image/jpeg, image/png, image/webp"
onChange={(e) => setFile(e.target.files?.[0] ?? null)}
/>
<button type="submit" disabled={!file || uploading}>
{uploading ? "アップロード中..." : "アップロード"}
</button>
{message && <p>{message}</p>}
</form>
)
}4. 動作確認
Railsサーバー(ポート3001)とNext.jsの開発サーバー(ポート3000)をそれぞれ起動し、ファイルを選択してアップロードボタンをクリックします。
# Rails(ポート3001)
rails server -p 3001
# Next.js(ポート3000)
npm run dev「アップロード成功!」と表示されれば、クライアントからS3へ直接ファイルがアップロードされています。
S3コンソールから対象のバケットを確認し、uploads/ディレクトリ内にファイルが保存されていることを確認してみましょう。
まとめ
本記事では、Presigned URLの仕組みからRails × Next.jsでの実装方法まで解説しました。
Presigned URLを活用することで、サーバーを経由せずにクライアントからS3へ直接ファイルをアップロードでき、サーバーの負荷軽減が期待できます。
今回はアップロードを実装しましたが、同様の仕組みでダウンロード用のPresigned URLも発行できます。ぜひ活用してみてください。
