この記事は株式会社カオナビ Advent Calendar 2025の(シリーズ3)の記事です。
はじめに
カオナビで労務・勤怠グループに所属している荒木です。
弊社で開発しているサービスでは、請求書や帳票などのPDF生成機能があります。今回、PDF生成基盤の見直しを行うことになり、代替ツールとして Gotenberg を検討することになりました。
移行にあたり、現行システムで必要な以下の機能要件が実現できるか検証してみました:
- HTML → PDF変換
- 複数PDFの結合
- Webhookによる非同期処理
- PDFオーバーレイ
Gotenbergとは
Gotenberg は、Docker上で動作するステートレスなPDF生成APIです。
主な特徴:
- Chromiumを使ったHTML→PDF変換
- LibreOfficeを使ったOfficeドキュメント→PDF変換
- PDFの結合・分割・メタデータ操作
- Webhook対応で非同期処理が可能
なぜGotenbergを使うのか
PDF生成をアプリケーションに直接組み込む場合、 Chromium/Puppeteer/wkhtmltopdf などの依存関係が必要になります。これには以下の課題があります:
- 依存関係の管理が複雑: Chromiumのバージョン管理、フォントのインストールなど
- リソース消費が大きい: PDF生成時のメモリ・CPU負荷がアプリケーションに影響
- スケールしにくい: PDF生成だけをスケールアウトできない
Gotenbergを使うと、簡単にPDF生成機能を独立したマイクロサービスとして切り出す事ができます!
- デプロイが簡単: 公式Docker imageをそのままECS/EKS/Cloud Runなどにデプロイ可能
- 依存関係の分離: アプリケーションからChromium等の依存を排除
- 独立したスケーリング: PDF生成の負荷に応じてGotenbergだけをスケールアウト
- 言語非依存: HTTP APIなのでどの言語からでも利用可能
環境構築
Dockerfile(日本語フォント対応)
FROM gotenberg/gotenberg:8
USER root
# 日本語フォント (IPA) をインストール
RUN apt-get update && \
apt-get install -y --no-install-recommends \
fonts-ipafont-gothic \
fonts-ipafont-mincho && \
rm -rf /var/lib/apt/lists/*
USER gotenberg
docker-compose.yml
services:
gotenberg:
build: .
ports:
- "3000:3000"
command:
- "gotenberg"
- "--api-timeout=60s"
- "--chromium-auto-start=true"
extra_hosts:
- "host.docker.internal:host-gateway"
# 非同期処理のS3エミュレーション用
localstack:
image: localstack/localstack:latest
ports:
- "4566:4566"
environment:
- SERVICES=s3
起動
docker-compose up -d --build
1. HTML → PDF変換
A4サイズ、マージン15mm、背景印刷ONという実務でよく使われるような基本的な設定で変換を試してみます。
HTML → PDF変換を試す
# リクエスト
curl --request POST http://localhost:3000/forms/chromium/convert/html \
--form "files=@templates/sample.html;filename=index.html" \
--form paperWidth=8.27 \
--form paperHeight=11.7 \
--form marginTop=0.59 \
--form marginBottom=0.59 \
--form marginLeft=0.59 \
--form marginRight=0.59 \
--form printBackground=true \
-o output/converted.pdf -w "HTTP %{http_code}, %{size_download} bytes, %{time_total}s\n"
# 結果
HTTP 200, 264415 bytes, 0.190s
約260KBのPDFが0.19秒で生成されました!
2. PDF結合
複数のPDFを1つにまとめます。すでにPDFファイルがある前提で進めます。
PDF結合を試す
# 結合前のpdfのサイズを確認
ls -la templates/*.pdf
-rw-r--r-- 1 user staff 65258 12 22 12:48 templates/01-page1.pdf
-rw-r--r-- 1 user staff 220253 12 22 12:48 templates/02-page2.pdf
# リクエスト
curl -s --request POST http://localhost:3000/forms/pdfengines/merge \
--form files=@templates/01-page1.pdf \
--form files=@templates/02-page2.pdf \
-o output/merged.pdf -w "HTTP %{http_code}, %{size_download} bytes\n"
# 結果
HTTP 200, 285860 bytes
2つのPDFが結合されました。(65KB + 220KB = 285KB)
3. Webhookによる非同期処理
大量のPDFを生成する場合、同期処理ではタイムアウト等の問題があります。
GotenbergのWebhook機能を使えば、生成完了後に指定のURLへ結果を送信できます。
Webhookの2つの方式
WebhookでPDFを受け取る方法は主に2つあります:
| 方式 | 概要 | メリット | デメリット |
|---|---|---|---|
| Callback + API方式 | 自前のAPIエンドポイントでPDFを受け取り、そこからS3にアップロード | 受け取り時に追加処理が可能 | アプリサーバーのメモリを消費 |
| Presigned URL方式 | S3のPresigned URLを直接指定し、GotenbergからS3へ直接アップロード | アプリサーバーを経由しない | 受け取り時の追加処理ができない |
今回はPresigned URL方式を試してみます。大容量PDFでもアプリサーバーのメモリを消費せず効率的です。
仕組み
┌─────────┐ ①リクエスト ┌───────────┐
│ Server │ ─────────────────▶ │ Gotenberg │
└─────────┘ (即座に204返却) └───────────┘
│
②PDF変換完了後
▼
┌─────────────┐
│ S3 (PUT) │
│ Presigned │
│ URL │
└─────────────┘
Presigned URL方式を試す
まずはLocalStack S3の準備を行います。
# 環境変数
export AWS_ACCESS_KEY_ID=test
export AWS_SECRET_ACCESS_KEY=test
export AWS_DEFAULT_REGION=us-east-1
# バケット作成
aws --endpoint-url=http://localhost:4566 s3 mb s3://gotenberg-output
# Presigned URL生成(Docker内部からアクセス可能なURL)
PRESIGNED_URL=$(aws --endpoint-url=http://localhost:4566 \
s3 presign s3://gotenberg-output/async-result.pdf --expires-in 3600)
# Docker内部からアクセスできるようlocalhostをlocalstackに置換
DOCKER_URL=$(echo "$PRESIGNED_URL" | sed 's/localhost:4566/localstack:4566/')
webhookを使用してリクエスト
curl --request POST http://localhost:3000/forms/chromium/convert/html \
--form "files=@templates/sample.html;filename=index.html" \
--form paperWidth=8.27 \
--form paperHeight=11.7 \
--form printBackground=true \
--form "webhookUrl=$DOCKER_URL" \
--form webhookMethod=PUT
レスポンスは即座に 204 No Content が返り、PDF生成完了後にS3へ直接アップロードされます。
# S3に保存されたか確認
aws --endpoint-url=http://localhost:4566 s3 ls s3://gotenberg-output/
2024-12-22 12:35:41 264340 async-result.pdf
S3にアップロードされている事を確認できました!
4. PDFオーバーレイ
帳票のPDFテンプレート(枠線やロゴ)に、動的なデータ(名前や金額)を重ねたいケースがありましたが、Gotenberg v8のAPIを調査した結果、2つのPDFを重ね合わせる機能は提供されていませんでした。
回避策:pdfcpuライブラリ
Goの pdfcpu ライブラリを使って実現しました。
手順
- 背景PDF生成: 枠線・ラベル・ロゴなどの固定要素
-
前面PDF生成: 動的な値(
omitBackground=trueで透明背景) - 合成: pdfcpuで重ね合わせ
Goでの実装例(pdfcpuライブラリ)
import (
"github.com/pdfcpu/pdfcpu/pkg/api"
"github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model"
"github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types"
)
func overlayPDF(backgroundPath, foregroundPath, outputPath string) error {
// 背景PDFをベースにコピー
os.WriteFile(outputPath, backgroundPDF, 0644)
// 前面PDFをWatermarkとして設定
wm, err := api.PDFWatermark(
foregroundPath,
"scale:1.0, position:c, rotation:0",
true, // onTop: 前面に配置
false, // update
types.POINTS,
)
if err != nil {
return err
}
// オーバーレイを適用
return api.AddWatermarksFile(
outputPath,
"", // 出力先(空なら上書き)
nil, // ページ指定(nilで全ページ)
wm,
model.NewDefaultConfiguration(),
)
}
この方法で、テンプレートPDFに動的データを重ね合わせることができました。
まとめ
GotenbergはシンプルなAPIでHTML→PDF変換ができ、Webhook機能で大量生成にも対応できます。ただし、オーバーレイのような高度な機能には別ツールとの組み合わせが必要でした。