めも

PDF帳票生成システム 設計書 (Serverless PDF Generation)

目次

1. コンセプト・基本方針

  • 疎結合: PDF生成ロジックを既存モノリスから切り離し、マイクロサービス(AWS Lambda)化する。
  • 保守性: PDFレイアウトとプログラムロジックを分離する。座標指定ではなく**AcroForm(フォームフィールド)**を採用し、デザイン変更によるコード修正を不要にする。
  • スケーラビリティ: サーバーレスアーキテクチャを採用し、突発的なアクセス増にも自動追従させる。
  • 日本語対応: Lambda Layerを活用し、日本語フォントを効率的に管理・ロードする。

2. 基本設計 (Basic Design)

2.1 アーキテクチャ構成

AWSサーバーレス構成を採用し、運用コストと管理工数を最小化する。

  • API Gateway: フロントエンドからのリクエスト受け付け(REST API)。
  • AWS Lambda (Node.js): PDF生成のコアロジック。
  • Amazon S3:
    • Input Bucket: 雛形(テンプレート)PDFの格納。
    • Output Bucket: 生成済みPDFの一時保管(ライフサイクルポリシーで自動削除)。
  • Lambda Layer: 日本語フォントファイル(.ttf)および共通ライブラリの格納。

2.2 処理フロー

  1. Request: クライアントが「テンプレートID」と「埋め込みデータ(JSON)」をPOST。
  2. Fetch: LambdaがS3からテンプレートPDFを取得。Layerからフォントをロード。
  3. Process: pdf-libを用いてAcroFormフィールドにデータをマッピング・描画。
  4. Flatten: フォームを平坦化(編集不可化)。
  5. Upload: 生成したPDFをS3へ保存。
  6. Response: クライアントへ署名付きURL(Presigned URL)を返却。

2.3 インフラ要件

項目設定値/仕様備考
RuntimeNode.js 20.x (TypeScript)
Memory512MB 〜 1024MBPDF操作はメモリ消費大のため余裕を持つ
Timeout10秒 〜 29秒API Gatewayの制限(29秒)に合わせる
LayerCustom Layeripaexg.ttf 等のフォント(約4-6MB)を配置
S3 Lifecycle1日〜3日で削除Outputバケットの肥大化防止

3. 詳細設計 (Detailed Design)

3.1 データ定義 (Interface)

Request Body (JSON)

JSON

{
  "templateKey": "invoice_template_v1.pdf",
  "data": {
    "date_field": "2026年1月16日",
    "name_field": "株式会社サンプル 御中",
    "amount_field": "¥100,000-"
  }
}
  • dataオブジェクトのキー名は、PDF側のフィールド名と完全一致させること。

Response Body (JSON)

JSON

{
  "status": "success",
  "downloadUrl": "https://s3.ap-northeast-1.amazonaws.com/..."
}

3.2 内部ロジック仕様 (Class/Module Structure)

A. PDF処理サービス (PdfService.ts)

  • ライブラリ: pdf-lib, @pdf-lib/fontkit
  • フォント読み込み戦略:
    • /opt/fonts/ (Lambda Layerのマウントパス) から fs.readFile で読み込む。
    • 初回実行時はグローバル変数にキャッシュし、ウォームスタート時の高速化を図る。
  • 埋め込み処理 (Mapping Logic):
    1. pdfDoc.getForm() でフォームコントローラーを取得。
    2. Requestの data キーをループ処理。
    3. form.getTextField(key) でフィールド特定。存在しない場合はWarnログを出してスキップ(エラーにはしない)。
    4. setText(value) で値セット。
    5. 重要: updateAppearances(customFont) を実行し、日本語フォントを適用してレンダリングを確定させる。
  • 仕上げ処理:
    • form.flatten() を実行し、AcroFormを入力不可のテキストパスへ変換する。

B. S3リポジトリ (S3Repository.ts)

  • getObject: テンプレート取得 (Stream or Buffer)。
  • putObject: 生成PDF保存。Content-Typeは application/pdf を指定。
  • getSignedUrl: getObject アクションに対する有効期限付きURL(例: 15分)を発行。

3.3 エラーハンドリング設計

ケースエラーコード対応方針
テンプレートが見つからない404 Not Foundクライアントへ「雛形設定エラー」を通知
フォント読み込み失敗500 Internal Errorクリティカルエラーとしてログ監視通知 (CloudWatch)
フィールド名不一致正常終了 (Warnログ)生成は続行するが、CloudWatch Logsに「Unused keys: […]」を出力し、テンプレート修正の気づきを与える
S3アップロード権限エラー500 Internal ErrorIAMロール設定ミス。即時検知が必要

3.4 セキュリティ設計

  • 最小権限の原則: Lambdaには、Inputバケットの GetObject と Outputバケットの PutObject のみを許可する。
  • Presigned URL: 公開設定(Public Access)はBlockし、署名付きURL経由でのみダウンロードさせる。

4. 運用・実装ガイドライン (Operation Manual)

開発者およびデザイナー(PDF作成者)向けのルール定義です。

4.1 テンプレートPDF作成ルール

  1. ツール: Adobe Acrobat Pro または LibreOffice Draw 等を使用する。
  2. フィールド配置: 可変にしたい箇所に「テキストフィールド」を配置する。
  3. 命名規則: 英小文字とスネークケースを使用する(例: user_name, issue_date)。日本語フィールド名は避ける(文字コードトラブル防止)。
  4. フォント設定: PDF作成ツール上でのフォント指定は無視される(プログラム側で強制上書きする)ため、デフォルトでOK。配置とサイズのみを調整する。

4.2 今後の拡張性 (Scalability consideration)

  • 大量生成バッチ対応:
    • 現状のAPI Gateway (同期) は単発生成用。
    • もし「1万人に一斉送信」などの要件が出た場合は、SQS + Lambda の非同期構成へ切り替えられるよう、コアロジック(PdfService)を独立させておく。

テックリードからの次の一手

この設計書をベースにすれば、メンバーへのタスク割り振りが明確になります。

  • インフラ担当: Terraform/CDKで S3, Lambda, Layer, IAM周りを構築。
  • バックエンド担当: PdfService.ts の実装と単体テスト(ローカルのPDFでテスト可能)。
  • フロント担当: ファイルアップロード不要、JSONを投げてURLを受け取るだけのシンプルな実装。

大量生成バッチ対応への調整案 (High Volume Batch Architecture)

1. 基本設計の変更点 (Architecture Update)

最大の変更は、API Gatewayでの即時応答を諦め、**「キューイング(SQS)」**を挟んだ非同期処理にすることです。

変更前(同期)

Client -> API Gateway -> Lambda -> S3 (Wait…) -> Client

  • 課題: 29秒以上の処理は不可。大量リクエストでLambdaの同時実行数上限(Account Limit)を食いつぶし、他システムに影響が出る。

変更後(非同期・ファンアウト構成)

  1. Job Entry: Client -> S3 (CSVアップロード) -> S3 Event -> Dispatcher Lambda
  2. Buffering: Dispatcher Lambda -> SQS (Standard Queue) -> (数千件のメッセージ)
  3. Processing: SQS -> Worker Lambda (並列数を制御して実行) -> Output S3

2. 詳細設計の調整ポイント (Detailed Adjustments)

A. エントリーポイントの設計 (Trigger)

1件ずつAPIを叩くのはネットワークオーバーヘッドが大きすぎるため、**「バルクデータ入稿」**方式に変更します。

  • 変更点:
    • API Gatewayではなく、S3へのCSV(またはJSONL)アップロードをトリガーにする。
    • CSVには「1行 = 1つのPDF生成に必要なデータ(名前、日付、ID等)」を記述。
  • Dispatcher Lambda (新規追加):
    • S3に置かれたCSVを読み込み、1行ずつ(または10件などの塊で)メッセージを作成し、SQSへ SendMessageBatch する役割。

B. 流量制御と並列数 (Concurrency Control)

ここが最も重要な「システムの安定性」に関わる部分です。

  • SQSの導入:
    • 大量の生成リクエストを一時的に受け止める(バッファリング)。
  • Worker Lambdaの同時実行数制限 (Reserved Concurrency):
    • 必須設定です。
    • PDF生成を行うLambda関数に対し、AWSコンソールで「予約された同時実行数」を設定します(例: 50)。
    • これを行わないと、1万件のキューが入った瞬間、Lambdaが数千個立ち上がろうとして、AWSアカウント全体の制限(デフォルト1000)に達し、他の本番サービス(API等)を巻き込んでダウンさせます。
    • 制限をかけることで、50並列で一定のペースを守りながら処理し続けます。

C. Worker Lambdaの実装調整 (Code Optimization)

単発実行と異なり、バッチ処理では**「ウォームスタート(再利用)」の効率**を最大化するコーディングが求められます。

TypeScript

// --- Global Scope (ハンドラ外) ---
// コンテナ再利用時にここがメモリに残るため、
// フォントとテンプレートのロードは「1回だけ」実行されるようにする
let cachedFontBytes: Uint8Array | null = null;
let cachedTemplateBytes: Uint8Array | null = null;

export const handler = async (event: SQSEvent) => {
  // 1. リソースのロード (未ロード時のみ実行)
  if (!cachedFontBytes || !cachedTemplateBytes) {
    [cachedFontBytes, cachedTemplateBytes] = await loadResources();
  }

  // 2. SQSはデフォルトで複数のメッセージ(Records)をまとめて渡してくる(Batch Size)
  const promises = event.Records.map(async (record) => {
    const data = JSON.parse(record.body);
    
    // 生成ロジックへ (メモリ上のキャッシュを渡す)
    return generatePdf(data, cachedFontBytes!, cachedTemplateBytes!);
  });

  // 並列実行
  await Promise.all(promises);
};
  • Batch Sizeの設定: SQSトリガーの設定で Batch Size を調整します(例: 10)。Lambda 1回の起動で10ファイル生成させ、初期化コストを1/10にします。

D. エラーハンドリング (Dead Letter Queue)

1万件中1件のデータ不備で、全体の処理を止めるわけにはいきません。

  • DLQ (Dead Letter Queue) の設置:
    • Worker Lambdaで処理に失敗したメッセージ(フォント破損、データ不正など)は、3回リトライした後に**DLQ(失敗用キュー)**へ流す設定にします。
    • 処理完了後、DLQに残ったメッセージだけを確認・修正して再流し込み(Redrive)できる運用フローを設計します。

E. 完了通知 (Completion Notification)

ユーザーは画面の前で待てないため、完了を知る仕組みが必要です。

  • 案: 生成したPDFをS3に保存するたびに、DynamoDBのステータスを更新する。あるいは、全件完了したか判定するのは難しいため、「エラーがあった場合のみ管理者へSNS/Email通知」などの簡易的な運用から始めるのが現実的です。

3. まとめ:ハイブリッドな構成案

テックリードとしての推奨は、**「コードベースは共通化し、入り口(Trigger)だけ分ける」**構成です。

  1. Core Logic (PdfService.ts): PDF生成の純粋な関数。API用とバッチ用で共有ライブラリとして利用。
  2. API Handler (api.ts): 画面からの単発即時プレビュー用。Core Logicを呼んで即レスポンス。
  3. Batch Worker (worker.ts): SQSトリガー用。Core Logicを呼び、ループ処理とエラーハンドリングを担当。

調整が必要なタスク一覧

  1. S3 Bucket: アップロード用バケットのEvent通知設定。
  2. SQS: Standard Queue と DLQ の作成。
  3. Lambda:
    • Dispatcher の実装。
    • Worker の実装(SQSトリガー対応、グローバルキャッシュ活用)。
    • Worker の**予約済み同時実行数(Reserved Concurrency)**設定。

この構成であれば、既存のモダンな設計思想を崩さず、エンタープライズレベルの大量処理にも耐えうる堅牢なシステムになります。

]

テックリードとして、非常に鋭い視点です。 「バッチA(PDF生成)の結果を待って、バッチB(通知、ZIP化、DB更新など)を実行する」という**依存関係(Workflow / Dependency)**が発生した瞬間、設計の難易度は一段階上がります。

単なるイベント駆動(S3 -> Lambda)だけでは、「1万件のPDF生成がすべて完了したタイミング」を検知するのが非常に難しいからです。

モダン開発における最適な解は、「オーケストレーションツール(AWS Step Functions)」の導入です。


推奨アーキテクチャ: AWS Step Functions (Distributed Map) 採用案

AWS Step Functionsには、大量データを並列処理し、その完了を待って次のステップへ進むための**「Distributed Map(分散マップ)」**という機能があります。これがまさにこの要件のために存在します。

1. 構成図イメージ

コード スニペット

graph TD
    Trigger[S3 CSV Upload / Schedule] --> SF[Step Functions State Machine]
    
    subgraph Step Functions Workflow
        SF --> |State 1| Pre[前処理: CSV解析 & S3パス取得]
        Pre --> |State 2| Map[Distributed Map (並列実行)]
        
        subgraph Map State (Fan-out)
            Map --> Worker[Lambda: PDF生成]
            Worker --> |Save| S3_PDF[S3 Output Bucket]
            Worker --> |Return| Result[生成結果(S3 Path/Status)]
        end
        
        Map --> |State 3: 完了待ち(Fan-in)| Agg[結果の集約 (S3へ出力)]
        Agg --> |State 4| BatchB[Lambda: 次のバッチ処理]
    end

    BatchB --> |Use| S3_PDF

2. この構成のメリット

  • 完了同期 (Sync): 「1万件すべてのLambda実行が終わるまで待つ」という制御をマネージドサービス側がやってくれます。自前でDBカウンターを実装する必要がありません。
  • データの引き継ぎ: バッチA(PDF生成)で作成されたS3のファイルパスリストを、自動的に集約してバッチBへの入力として渡せます。
  • エラー許容率の設定: 「1万件中、5件エラーが出ても、残りの成功分だけで次のバッチBに進む」といった柔軟な設定が可能です。

留意点とポイント(Tech Lead View)

この構成を採用する際、特に注意すべき設計ポイントを3つ挙げます。

① 「データの実体」ではなく「参照(パス)」を回す

バッチAからバッチBへデータを渡す際、生成したPDFのバイナリデータそのものや、巨大なJSON配列をStep Functionsのステート(メモリ)に乗せてはいけません(ペイロード制限 256KBに引っかかります)。

  • Bad: バッチAの結果(1万件分のファイルパスJSON)を次のステートに直接渡す。
  • Good:
    1. Distributed Mapの機能で、各Lambdaの結果を自動的にS3上の1つのJSONファイル(マニフェスト)に出力させる。
    2. バッチB(次のLambda)は、その「結果JSONファイルのS3パス」だけを受け取り、ストリーム処理で読み込んで実行する。

② 部分失敗(Partial Failure)のハンドリング

1万件処理して、1件だけフォントエラーで失敗した場合の振る舞いを決めておく必要があります。

  • Strict Mode: 1件でも失敗したらバッチBを実行せず、全体をFailedとしてアラートを飛ばす。
  • Tolerant Mode: エラー率○%以下なら許容し、成功したデータのみリスト化してバッチBに渡す。(請求書発行などはStrict、ログ解析などはTolerantなど、業務要件によります)
  • DLQ (Dead Letter Queue): 失敗した個別のレコードだけをSQSに逃がし、後でリカバリ可能にする。

③ 冪等性(Idempotency)の担保

Step Functionsはエラー時にリトライ機能を持っています。 バッチBが「全PDFを結合して1つのZIPにする」処理だったとして、途中でタイムアウトして再実行された場合、**「ZIPファイルが二重に生成されないか」「メールが2回飛ばないか」**を考慮したコードにする必要があります。


代替案:DynamoDB Atomic Counter(非推奨だが安価)

Step Functionsのコスト(状態遷移ごとの課金)を極限まで削りたい場合、または「単純なカウント」だけで良い場合は、以下の方法もありますが、実装工数とバグのリスクが高くなります。

  1. Dispatcher: 処理総数(例: 10,000)をDynamoDBに書き込む。
  2. Worker: PDF生成完了後、DynamoDBの数値を -1 する(Atomic Counter)。
  3. Check: 数値が 0 になった瞬間のWorkerが、トリガーとなって「バッチB」を起動する(EventBridge等を介して)。

※テックリードとしての判断: この方法は「0になった瞬間の判定(競合)」や「Lambdaが途中で落ちた時のズレ(ゾンビタスク)」の管理が非常に難しいため、運用負荷を考えると Step Functions (Distributed Map) の採用を強く推奨 します。

まとめ:提案する構成仕様

「Step Functions Distributed Map パターン」

  1. Trigger: 管理者がS3に「ターゲットリストCSV」をアップロード。
  2. Orchestrator: S3イベントでStep Functions起動。
  3. Phase 1 (Fan-out):Map State で最大数千並列でLambda(PDF生成)を実行。
    • 各Lambdaは生成したPDFのS3 Keyをreturnする。
  4. Phase 1 Result: Step Functionsが全実行結果を results.json としてS3に出力(自動)。
  5. Phase 2 (Next Batch): バッチB用Lambdaが起動。
    • Inputとして results.json のパスを受け取る。
    • 成功したPDFだけを対象に、次の処理(例: 印刷会社への送信、完了メール通知)を行う。

この構成であれば、ロジック設計(Lambdaの中身)に集中でき、状態管理という「難しい部分」をAWSにオフロードできます。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

コメント

コメントする

目次