オンプレの帳票処理の課題

オンプレ環境での帳票処理は、NASを中心に以下のような構成が多い。

オンプレの帳票フロー
【アップロード】
クライアント → Apache → Tomcat(マルチパートを受信)→ NAS に格納

【帳票生成】
リクエスト → Java(JasperReports / iText で生成)→ NASの /tmp に一時保存 → レスポンス

【ダウンロード】
クライアント → Apache → NASのファイルをストリームして返す

この構成の主な課題:

  • APサーバーがボトルネック:大きなファイルのアップロード・ダウンロードがTomcatのスレッドを占有する
  • NASの容量・可用性管理:物理ストレージの増設・バックアップ・障害対応が運用負荷になる
  • 大容量ファイルのタイムアウト:APサーバーを経由するためHTTPタイムアウトの影響を受けやすい
  • スケールアウト時の共有ストレージ問題:複数APサーバーがNASを共有するため、NASがSPOFになりやすい

presigned URL とは

S3の presigned URL(署名付きURL)は、一時的に有効なS3への直接アクセス権をURLに埋め込む仕組みだ。JavaアプリがS3 SDKで生成し、クライアントに返す。クライアントはそのURLを使ってAPサーバーを経由せずS3に直接アクセスできる。

presigned URL の生成(Java / AWS SDK v2)
S3Presigner presigner = S3Presigner.create();

// アップロード用 presigned URL(PUT)
PutObjectRequest putReq = PutObjectRequest.builder()
    .bucket("my-documents")
    .key("uploads/" + userId + "/" + fileId)
    .contentType("application/pdf")
    .build();

PresignedPutObjectRequest presignedPut = presigner.presignPutObject(
    PutObjectPresignRequest.builder()
        .signatureDuration(Duration.ofMinutes(15))  // 15分有効
        .putObjectRequest(putReq)
        .build()
);

String uploadUrl = presignedPut.url().toString();

// ダウンロード用 presigned URL(GET)
GetObjectRequest getReq = GetObjectRequest.builder()
    .bucket("my-documents")
    .key("reports/" + reportId + "/output.pdf")
    .build();

PresignedGetObjectRequest presignedGet = presigner.presignGetObject(
    GetObjectPresignRequest.builder()
        .signatureDuration(Duration.ofMinutes(30))  // 30分有効
        .getObjectRequest(getReq)
        .build()
);

String downloadUrl = presignedGet.url().toString();

アップロード設計(2パターン)

アップロードの2パターン
【パターンA:Java経由アップロード(シンプル構成)】

クライアント → ALB → Java → S3.putObject()

適用:小〜中規模ファイル(数MB以下)、認可チェックが複雑なケース

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

【パターンB:S3直接アップロード(大規模向け・推奨)】

① クライアント → Java:presigned PUT URL の発行を依頼
② Java → S3 SDK:presigned URL を生成してクライアントに返す
③ クライアント → S3:presigned URL を使って直接 PUT

APサーバーのメモリ・スレッド消費ゼロ
大容量ファイル(数百MB)でもタイムアウト不要
ECS コンテナの帯域を消費しない
観点パターンA(Java経由)パターンB(S3直接)
実装の簡易さシンプルフロントエンド側の実装が必要
APサーバー負荷高い(ファイルをメモリに展開)ゼロ
大容量ファイルタイムアウトリスクあり問題なし
認可チェックJava側で自由に実装可presigned URL発行前にJavaで実施
推奨規模数MB以下・社内ツール本番システム・大規模

帳票生成設計(同期 vs 非同期)

帳票生成の2パターン
【同期生成:即時応答(小規模帳票向け)】

リクエスト → Java(JasperReports で生成)→ S3 に格納
                                            → presigned URL をレスポンスで返す

適用:数秒以内で生成完了する単純な帳票

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

【非同期生成:大規模帳票・時間がかかる処理向け】

① リクエスト → Java → SQS に「帳票生成ジョブ」を登録
② Java → クライアントへ「受付完了(jobId)」を即時応答
③ (バックグラウンド)SQS → ECS Worker が受信
④ ECS Worker が帳票生成 → S3 に格納
⑤ 完了通知:SNS → メール or WebSocket → クライアント

適用:数十秒以上かかる大量データの帳票・複数帳票の一括生成

💡 非同期生成のメリット

HTTPレスポンスタイムアウト(通常60秒〜数分)の制約から解放される。帳票生成専用のECS Workerをスケールアウトすることで、リクエスト処理サーバーとワーカーを独立してスケールできる。SQSは帳票ジョブをキューで保持するため、Workerが落ちてもジョブが失われない。

ダウンロード設計(2パターン)

観点パターンA(Java経由)パターンB(presigned URL)
APサーバー負荷高い(ファイル全体を中継)ゼロ
認可チェックダウンロード毎にJavaで実施presigned URL発行前にJavaで実施
URLの有効期限なし(セッション次第)設定可(例:15分・30分)
ダウンロード速度APサーバー帯域依存S3の高スループット直接配信
大容量ファイルタイムアウトリスクあり問題なし(S3マルチパート)

S3バケット設計

S3バケット構成例
s3://myapp-documents/
    ├── uploads/
    │     └── {userId}/{yyyy-MM}/{fileId}/original.pdf
    │           ← ユーザーアップロードファイル
    │
    ├── reports/
    │     └── {jobId}/{timestamp}/output.pdf
    │           ← 生成済み帳票
    │
    └── temp/
          └── {uuid}/work.pdf
                ← 処理中の一時ファイル(Lifecycle: 1日で自動削除)

バケットポリシー:
  ・パブリックアクセス:完全にブロック
  ・ECSタスクロール(IAM)からのみ操作許可
  ・presigned URL経由のアクセスのみ外部に公開

ライフサイクルルール:
  ・temp/ : 1日後に自動削除
  ・reports/ : 90日後にGlacierへ移行
  ・uploads/ : 1年後に自動削除(要件に応じて調整)

対比表

処理オンプレAWS
アップロード受付Tomcatが受信 → NASに保存presigned PUT URL → S3直接PUT
帳票生成(同期)Java生成 → NAS一時保存 → レスポンスJava生成 → S3格納 → presigned URLを返却
帳票生成(非同期)独自キュー or バッチスケジューラSQS → ECS Worker → S3 → SNS通知
ダウンロードApache / Tomcatがストリーム返却S3 presigned GET URL(直接取得)
ストレージ管理NAS容量管理・増設・バックアップS3(容量無制限・99.999999999%耐久性)
一時ファイル削除cronジョブで定期削除S3 Lifecycle ルールで自動削除

ハマりポイント

⚠️ presigned URLの有効期限とCORS設定

フロントエンドからS3に直接PUTする場合、S3バケットにCORSポリシーを設定する必要がある。設定漏れがあると「ブロックされた」エラーになりがち。また有効期限(signatureDuration)は短すぎると大容量ファイルのアップロード中に期限切れが発生するため、ファイルサイズを考慮して設定すること。

⚠️ S3バケットはリージョン固定

presigned URLを生成するJavaコード内でリージョンを明示しないと、デフォルトリージョン(us-east-1)でURLが生成され、実際のバケットリージョンと一致せずアクセスエラーになる。S3Presigner.builder().region(Region.AP_NORTHEAST_1)のようにリージョンを明示すること。

次の記事では…

PART 05 では JavaからのDB接続設計を扱う。RDS Proxyが接続数爆発問題をどう解決するか、AuroraのWriter/Reader Endpoint分離の実装方法を解説する。