PDF APIの統合に成功したあと、本当の課題は本番運用で安定させることです。開発環境では問題なく動いていたAPIが、トラフィックスパイクやバッチ処理のタイミングでタイムアウトを起こしたり、想定外のコストが月末に発覚したりするケースは珍しくありません。
この記事は、FUNBREW PDFをはじめとするPDF APIを本番環境に投入・維持する際に押さえるべき項目を8つのカテゴリに整理したチェックリストです。エラーハンドリング・セキュリティ・バッチ処理の3記事で個別に詳しく解説している内容をここで統合し、チームがデプロイ前に確認すべき全体像を示します。
HTML→PDF変換の基礎はすでに理解している前提で進めます。PDF APIを初めて触る場合は言語別クイックスタートから始めてください。
1. APIキー管理とローテーション戦略
APIキーは本番環境への「鍵」です。一度漏洩すると不正利用によるコスト発生やデータ流出に直結します。
環境ごとにキーを分離する
# 本番環境
FUNBREW_PDF_API_KEY=sk-prod-xxxxxxxxxxxxxxxxxxxx
# ステージング環境
FUNBREW_PDF_API_KEY=sk-stg-xxxxxxxxxxxxxxxxxxxx
# ローカル開発環境
FUNBREW_PDF_API_KEY=sk-dev-xxxxxxxxxxxxxxxxxxxx
本番・ステージング・開発で同じキーを使い回すと、開発者のローカルマシンから本番クォータが消費されたり、誤ったデータで本番APIを叩いてしまうリスクがあります。
シークレット管理ツールへの移行
.envファイルによる管理は手軽ですが、チームが拡大したら専用ツールを検討してください。
| ツール | 適した場面 |
|---|---|
| AWS Secrets Manager | AWS上で動作するアプリケーション |
| HashiCorp Vault | マルチクラウド・オンプレ混在環境 |
| Doppler | 小〜中規模チームのシークレット集中管理 |
| GitHub Actions Secrets | CI/CDパイプライン限定での利用 |
ローテーションの自動化
import boto3
import os
def rotate_pdf_api_key():
"""AWS Secrets ManagerでAPIキーをローテーションする例"""
client = boto3.client('secretsmanager', region_name='ap-northeast-1')
# 新しいキーをダッシュボードAPIで取得(仮実装)
new_key = provision_new_api_key()
# Secrets Managerを更新
client.put_secret_value(
SecretId='funbrew-pdf-api-key-prod',
SecretString=new_key,
)
# 古いキーを無効化(移行期間を設けて安全に切り替え)
print("APIキーローテーション完了")
# 90日ごとに実行(CloudWatch Eventsで自動化)
rotate_pdf_api_key()
90日ごとのローテーションが目安です。ローテーション後は旧キーを即時無効化せず、数時間のオーバーラップ期間を設けることで、デプロイ中のリクエスト失敗を防げます。
APIキーの詳細なセキュリティ対策はセキュリティガイドで網羅しています。
チェックリスト
- 本番・ステージング・開発でAPIキーを分離している
-
.envをGitにコミットしていない(.gitignore設定済み) - フロントエンドのコードにAPIキーを含んでいない
- 90日ごとのローテーションスケジュールが決まっている
- キー漏洩時の無効化手順がドキュメント化されている
2. レート制限の把握とスロットリング設計
PDF生成APIには通常、プランごとに分あたり・日あたりのリクエスト上限が設定されています。この上限を超えると429 Too Many Requestsが返り、処理が止まります。
現在のレート制限を確認する
# APIレスポンスヘッダーでレート制限状況を確認
curl -s -I -X POST "https://pdf.funbrew.cloud/api/v1/pdf/generate" \
-H "X-API-Key: $FUNBREW_PDF_API_KEY" \
-H "Content-Type: application/json" \
-d '{"html": "<h1>test</h1>"}' | grep -i "x-rate"
# レスポンス例:
# X-RateLimit-Limit: 100
# X-RateLimit-Remaining: 87
# X-RateLimit-Reset: 1743465600
アプリケーション側でのスロットリング実装
API側のレート制限に「当たる前に」自分でリクエストを制御します。
// トークンバケットによるスロットリング(Node.js)
class RateLimiter {
constructor(requestsPerMinute) {
this.tokens = requestsPerMinute;
this.maxTokens = requestsPerMinute;
this.refillRate = requestsPerMinute / 60; // 1秒あたりの補充量
this.lastRefill = Date.now();
}
async acquire() {
this._refill();
if (this.tokens < 1) {
// トークン補充まで待機
const waitMs = (1 - this.tokens) / this.refillRate * 1000;
await new Promise(resolve => setTimeout(resolve, waitMs));
this._refill();
}
this.tokens -= 1;
}
_refill() {
const now = Date.now();
const elapsed = (now - this.lastRefill) / 1000;
this.tokens = Math.min(
this.maxTokens,
this.tokens + elapsed * this.refillRate
);
this.lastRefill = now;
}
}
// 1分間に最大80リクエスト(上限100の80%で運用)
const limiter = new RateLimiter(80);
async function generatePdf(html) {
await limiter.acquire();
// PDF生成APIを呼び出す
}
上限の**80%**を目安に自社側でスロットリングすると、突発的なトラフィックスパイクに対してバッファを確保できます。
プランごとのレート制限と料金についてはPDF API料金比較で詳しく解説しています。
チェックリスト
- 使用プランのレート制限(分・日)を把握している
- アプリケーション側でスロットリングを実装している
- ピーク時のリクエスト数を想定し、プランが対応できることを確認した
-
X-RateLimit-Remainingヘッダーを監視している
3. エラーハンドリングとリトライ設計
本番環境では、一時的な障害を前提とした設計が必須です。ネットワーク瞬断・API側のメンテナンス・レンダリングエラーなど、様々な理由でリクエストが失敗します。
エラー分類とリトライ判断
RETRYABLE_STATUS_CODES = {408, 429, 500, 502, 503, 504}
NON_RETRYABLE_STATUS_CODES = {400, 401, 403, 404}
def should_retry(status_code: int) -> bool:
"""リトライすべきエラーかどうかを判定する"""
return status_code in RETRYABLE_STATUS_CODES
| ステータスコード | 意味 | リトライ | 対応 |
|---|---|---|---|
| 400 | 不正なリクエスト | しない | HTMLやオプションを修正 |
| 401 / 403 | 認証エラー | しない | APIキーを確認・再発行 |
| 408 | タイムアウト | する | 指数バックオフ |
| 429 | レート制限 | する | Retry-Afterヘッダーの値を待機 |
| 500 / 502 / 503 | サーバーエラー | する | 指数バックオフ |
指数バックオフの実装
interface RetryConfig {
maxRetries: number;
initialDelayMs: number;
maxDelayMs: number;
backoffMultiplier: number;
}
const DEFAULT_RETRY_CONFIG: RetryConfig = {
maxRetries: 5,
initialDelayMs: 1000,
maxDelayMs: 60000,
backoffMultiplier: 2,
};
async function generatePdfWithRetry(
html: string,
apiKey: string,
config: RetryConfig = DEFAULT_RETRY_CONFIG
): Promise<Buffer> {
let delay = config.initialDelayMs;
for (let attempt = 0; attempt <= config.maxRetries; attempt++) {
const response = await fetch('https://pdf.funbrew.cloud/api/v1/pdf/generate', {
method: 'POST',
headers: {
'X-API-Key': apiKey,
'Content-Type': 'application/json',
},
body: JSON.stringify({ html }),
signal: AbortSignal.timeout(120_000),
});
if (response.ok) {
return Buffer.from(await response.arrayBuffer());
}
const isRetryable = [408, 429, 500, 502, 503, 504].includes(response.status);
if (!isRetryable || attempt === config.maxRetries) {
throw new Error(`PDF generation failed: HTTP ${response.status}`);
}
// Retry-Afterヘッダーがあればその値を優先
const retryAfter = response.headers.get('retry-after');
const waitMs = retryAfter
? parseFloat(retryAfter) * 1000
: Math.min(delay + Math.random() * 1000, config.maxDelayMs);
console.warn(`Retry ${attempt + 1}/${config.maxRetries}: waiting ${(waitMs / 1000).toFixed(1)}s`);
await new Promise(resolve => setTimeout(resolve, waitMs));
delay = Math.min(delay * config.backoffMultiplier, config.maxDelayMs);
}
throw new Error('Max retries exceeded');
}
リトライロジックの完全実装(curl・Python・Node.js・PHP)はエラーハンドリング完全ガイドで確認してください。
チェックリスト
- リトライ可能なエラーとそうでないエラーを区別している
- 指数バックオフ(ジッター付き)を実装している
- 最大リトライ回数と最大待機時間に上限を設けている
- リトライ上限到達時にアラートを発火させている
- リクエストIDをログに記録してトレーサビリティを確保している
4. 監視・アラート設計
問題を「ユーザーから報告される前に気づく」ための監視体制を構築します。
監視すべき主要メトリクス
| メトリクス | 警告閾値の目安 | クリティカル閾値の目安 |
|---|---|---|
| PDF生成失敗率 | > 1% | > 5% |
| p50レスポンスタイム | > 5秒 | > 15秒 |
| p99レスポンスタイム | > 30秒 | > 60秒 |
| 1分あたりのリトライ率 | > 10% | > 30% |
RateLimit-Remaining残量 |
< 30% | < 10% |
Datadogへのメトリクス送信例
import time
import functools
from datadog import statsd
def track_pdf_generation(func):
"""PDF生成のメトリクスを自動収集するデコレータ"""
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
tags = ['service:pdf-generator', 'env:production']
try:
result = func(*args, **kwargs)
duration_ms = (time.perf_counter() - start) * 1000
statsd.histogram('pdf.generation.duration_ms', duration_ms, tags=tags)
statsd.increment('pdf.generation.success', tags=tags)
return result
except Exception as e:
duration_ms = (time.perf_counter() - start) * 1000
error_tags = tags + [f'error_type:{type(e).__name__}']
statsd.histogram('pdf.generation.duration_ms', duration_ms, tags=error_tags)
statsd.increment('pdf.generation.failure', tags=error_tags)
raise
return wrapper
@track_pdf_generation
def generate_invoice_pdf(customer_data):
"""請求書PDFを生成する(メトリクス収集付き)"""
import requests
response = requests.post(
'https://pdf.funbrew.cloud/api/v1/pdf/generate',
headers={'X-API-Key': os.environ['FUNBREW_PDF_API_KEY']},
json={'html': build_invoice_html(customer_data)},
timeout=120,
)
response.raise_for_status()
return response.content
Webhook通知との組み合わせ
Webhook連携を設定すると、PDF生成の完了・失敗をサーバー側からプッシュ通知で受け取れます。ポーリングをなくしてシステムをシンプルに保てます。
// Webhook通知ペイロード例
{
"event": "pdf.generation.failed",
"job_id": "job_abc123",
"timestamp": "2026-04-01T12:00:00Z",
"error": {
"code": "RENDER_TIMEOUT",
"message": "Rendering exceeded 60 seconds",
"html_size_bytes": 245120
}
}
PagerDutyアラートの設定例
# アラートルール例(Prometheus Alertmanager形式)
groups:
- name: pdf-api
rules:
- alert: PdfGenerationHighFailureRate
expr: |
rate(pdf_generation_failure_total[5m]) /
rate(pdf_generation_total[5m]) > 0.05
for: 2m
labels:
severity: critical
annotations:
summary: "PDF生成失敗率が5%を超えています"
description: "過去5分間の失敗率: {{ $value | humanizePercentage }}"
- alert: PdfGenerationHighLatency
expr: histogram_quantile(0.99, pdf_generation_duration_ms_bucket) > 30000
for: 5m
labels:
severity: warning
annotations:
summary: "PDF生成のp99レイテンシが30秒を超えています"
チェックリスト
- 生成失敗率をリアルタイムで監視している
- レスポンスタイム(p50・p99)をトラッキングしている
- レート制限の残量に対してアラートを設定している
- クリティカルエラー発生時のオンコール通知が設定されている
- Webhookまたはポーリングで非同期ジョブの完了を検知している
5. コスト最適化
API利用料金は生成リクエスト数に比例します。無駄な生成を減らし、バッチをまとめることで大幅なコスト削減が可能です。
戦略1: キャッシュで重複生成を防ぐ
同じ入力から同じPDFが生成される場合(静的な約款・利用規約など)はキャッシュが効果的です。
import hashlib
import redis
import requests
import os
class PdfCache:
def __init__(self, redis_client, ttl_seconds=86400):
self.redis = redis_client
self.ttl = ttl_seconds # デフォルト24時間
def get_cache_key(self, html: str, options: dict) -> str:
"""HTMLとオプションからキャッシュキーを生成"""
content = f"{html}{str(sorted(options.items()))}"
return f"pdf_cache:{hashlib.sha256(content.encode()).hexdigest()}"
def generate_with_cache(self, html: str, options: dict = None) -> bytes:
options = options or {}
key = self.get_cache_key(html, options)
# キャッシュヒット
cached = self.redis.get(key)
if cached:
return cached
# キャッシュミス → API呼び出し
response = requests.post(
'https://pdf.funbrew.cloud/api/v1/pdf/generate',
headers={'X-API-Key': os.environ['FUNBREW_PDF_API_KEY']},
json={'html': html, 'options': options},
timeout=120,
)
response.raise_for_status()
pdf_bytes = response.content
self.redis.setex(key, self.ttl, pdf_bytes)
return pdf_bytes
# 使用例
cache = PdfCache(redis.Redis(host='localhost', port=6379))
pdf = cache.generate_with_cache(
html='<h1>利用規約</h1><p>...</p>',
options={'format': 'A4'}
)
戦略2: バッチ処理で複数PDFをまとめる
複数のPDFを1リクエストで生成すると、APIコール数を削減できます。詳しくはPDF一括生成ガイドを参照してください。
# バッチリクエスト: 1回のAPIコールで3つのPDFを生成
curl -X POST "https://pdf.funbrew.cloud/api/v1/pdf/generate" \
-H "X-API-Key: $FUNBREW_PDF_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"batch": [
{
"html": "<h1>請求書 #001</h1>",
"filename": "invoice-001.pdf",
"options": { "format": "A4" }
},
{
"html": "<h1>請求書 #002</h1>",
"filename": "invoice-002.pdf",
"options": { "format": "A4" }
},
{
"html": "<h1>請求書 #003</h1>",
"filename": "invoice-003.pdf",
"options": { "format": "A4" }
}
]
}'
戦略3: 不要な再生成を防ぐ
データが変更されたときだけPDFを再生成するパターンです。
from datetime import datetime
class PdfGenerationRecord:
"""PDF生成履歴を管理してデータ変更時のみ再生成する"""
def generate_if_outdated(
self,
record_id: str,
html: str,
data_updated_at: datetime,
) -> bytes:
"""データの更新日時が前回生成日時より新しい場合のみ再生成"""
last_generated = self._get_last_generated(record_id)
if last_generated and last_generated >= data_updated_at:
# データが変更されていないのでキャッシュを返す
return self._get_cached_pdf(record_id)
# 再生成が必要
pdf_bytes = self._call_pdf_api(html)
self._store(record_id, pdf_bytes, generated_at=datetime.utcnow())
return pdf_bytes
コスト試算
| 月間PDF生成数 | 最適化なし | キャッシュ30%適用 | バッチ化50%削減 |
|---|---|---|---|
| 10,000件 | 基準 | -3,000リクエスト | -5,000リクエスト |
| 100,000件 | 基準 | -30,000リクエスト | -50,000リクエスト |
料金プランの詳細はPDF API料金比較で確認できます。
チェックリスト
- 同一内容のPDFに対してキャッシュを実装している
- 複数PDFの生成をバッチリクエストにまとめている
- データ変更がない場合の不要な再生成を防いでいる
- 月間リクエスト数を定期的に確認してプランが適切かを見直している
6. スケーリング設計(キュー・非同期処理)
同期的なPDF生成(リクエスト→レスポンス待機)は、レスポンスタイムが長くなるとユーザー体験を損ないます。高負荷・大量生成シーンでは非同期パターンが適切です。
同期 vs 非同期の使い分け
| シナリオ | 推奨パターン | 理由 |
|---|---|---|
| ユーザーが「ダウンロード」ボタンを押す | 同期(最大15秒) | 即時フィードバックが必要 |
| 月次請求書バッチ(1,000件) | 非同期 + キュー | 処理時間が長く、完了を待てない |
| レポートの定期生成 | 非同期 + スケジューラ | バックグラウンドで完結する |
| 大量の証明書発行 | 非同期 + バッチ | APIコール数を最小化したい |
Redisキューを使った非同期パターン(Node.js + BullMQ)
import { Queue, Worker } from 'bullmq';
import { Redis } from 'ioredis';
const connection = new Redis({ host: 'localhost', port: 6379 });
// キューの定義
const pdfQueue = new Queue('pdf-generation', { connection });
// ジョブをエンキューする(APIエンドポイントから呼ぶ)
export async function enqueuePdfGeneration(jobData) {
const job = await pdfQueue.add('generate', jobData, {
attempts: 5, // 最大5回リトライ
backoff: {
type: 'exponential',
delay: 1000, // 初回1秒、以降倍増
},
removeOnComplete: { count: 1000 },
removeOnFail: { count: 500 },
});
return { jobId: job.id };
}
// ワーカー(スケールアウト可能)
const worker = new Worker(
'pdf-generation',
async (job) => {
const { html, options, webhookUrl } = job.data;
const response = await fetch('https://pdf.funbrew.cloud/api/v1/pdf/generate', {
method: 'POST',
headers: {
'X-API-Key': process.env.FUNBREW_PDF_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({ html, options }),
signal: AbortSignal.timeout(120_000),
});
if (!response.ok) {
throw new Error(`API error: HTTP ${response.status}`);
}
const pdfBuffer = Buffer.from(await response.arrayBuffer());
// S3などに保存してURLを通知する
const downloadUrl = await uploadToStorage(pdfBuffer);
if (webhookUrl) {
await notifyWebhook(webhookUrl, { downloadUrl, jobId: job.id });
}
return { downloadUrl };
},
{
connection,
concurrency: 10, // 同時処理数(APIのレート制限に合わせて調整)
}
);
worker.on('failed', (job, err) => {
console.error(`Job ${job?.id} failed:`, err.message);
// アラート送信(PagerDuty, Slackなど)
});
スケールアウト戦略
# Kubernetes HPA(Horizontal Pod Autoscaler)の設定例
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: pdf-worker-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: pdf-worker
minReplicas: 2
maxReplicas: 20
metrics:
- type: External
external:
metric:
name: bullmq_queue_size
selector:
matchLabels:
queue: pdf-generation
target:
type: AverageValue
averageValue: "50" # ワーカー1台あたり最大50ジョブ
ワーカーをスケールアウトしても、APIのレート制限は変わりません。ワーカー数 × 同時処理数がレート制限を超えないようにconcurrencyを調整してください。
チェックリスト
- バッチ・大量生成ジョブはキューで非同期処理している
- ワーカーの並行数がAPIのレート制限を超えていない
- キューの深さ(滞留ジョブ数)を監視している
- ジョブ失敗時のDead Letter Queueを設定している
- ワーカーの自動スケーリング(HPA等)が機能している
7. セキュリティチェック
本番投入前に必ず確認すべきセキュリティ項目です。セキュリティ完全ガイドで詳しく解説していますが、ここでは要点のみ列挙します。
入力バリデーション
// ユーザー入力をHTMLに埋め込む前に必ずエスケープ
function escapeHtml(str) {
return String(str)
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
// HTMLサイズの上限チェック(例: 1MB)
const MAX_HTML_SIZE_BYTES = 1_048_576;
function validateHtmlInput(html) {
if (Buffer.byteLength(html, 'utf8') > MAX_HTML_SIZE_BYTES) {
throw new Error('HTML is too large. Maximum size is 1MB.');
}
}
本番環境のセキュリティチェックリスト
| カテゴリ | チェック項目 | 優先度 |
|---|---|---|
| 認証 | APIキーを環境変数で管理している | 必須 |
| 認証 | 本番・ステージング・開発でキーを分離している | 必須 |
| 通信 | HTTPS (TLS 1.2+) での通信のみ | 必須 |
| 入力 | ユーザー入力のHTMLエスケープを実装している | 必須 |
| 入力 | HTMLサイズの上限チェックを実装している | 推奨 |
| アクセス | サーバーIPアドレス制限を設定している | 推奨 |
| アクセス | フロントエンドからAPIを直接呼んでいない | 必須 |
| データ | 生成PDFの自動削除ポリシーを確認した | 推奨 |
| 監査 | API呼び出しのログを記録している | 推奨 |
セキュリティの全項目はPDFセキュリティガイドで確認してください。
8. デプロイメントチェックリスト(まとめ)
本番リリース前に以下をすべて確認します。このリストをPRテンプレートやリリースチェックリストに組み込んでください。
事前準備
- 本番用APIキーを取得済み(ダッシュボードから発行)
- APIキーをシークレット管理ツール or 環境変数に設定済み
-
.envファイルがGitに含まれていないことを確認 - ステージング環境でE2Eテストが通過している
エラーハンドリング
- タイムアウト(120秒以上)を設定している
- 指数バックオフ付きリトライを実装している
- リトライ不可エラーは即座にアラートを発火している
- エラーログにリクエストID・ステータスコード・試行回数を含めている
パフォーマンス・スケーリング
- バッチ処理ジョブはキューで非同期処理している
- アプリケーション側でスロットリングを実装している
- ワーカー並行数がAPIレート制限に収まっている
- PDFキャッシュを適切な範囲で実装している
監視・アラート
- 生成失敗率のアラートを設定している(閾値: 5%)
- レスポンスタイムのアラートを設定している(p99 > 30秒)
- レート制限残量の監視を設定している
- キュー深さの監視を設定している
- ダッシュボードから月間利用量を確認できる状態になっている
セキュリティ
- フロントエンドのコードにAPIキーが含まれていない
- ユーザー入力をHTMLに埋め込む際のエスケープを実装している
- HTTPS(TLS 1.2+)のみで通信している
- IPアドレス制限を設定している(本番サーバーのみ許可)
コスト管理
- 月間リクエスト数の見積もりが現在のプラン上限以内
- 不要な重複生成を防ぐキャッシュ・更新チェックを実装している
- 月次のコスト確認フローが決まっている
まとめ
PDF APIの本番運用は、「動く」から「安定して動き続ける」に引き上げる作業です。本記事で取り上げた8項目を一度に全部実装する必要はありません。リリース初期は最低限の項目(APIキー管理・エラーハンドリング・基本監視)に絞り、トラフィックが増えるにつれてスロットリング・バッチ化・キャッシュを追加していくアプローチが現実的です。
各項目の詳細は以下の記事で深掘りしています。
- エラーハンドリング: PDF APIのエラーハンドリング完全ガイド
- セキュリティ: PDF APIのセキュリティ対策ガイド
- バッチ処理: PDF一括生成ガイド
- Webhook連携: PDF API Webhook連携ガイド
- 料金比較: PDF API料金比較 2026年版
まずはプレイグラウンドでAPIを試し、ドキュメントで仕様を確認したうえで、本記事のチェックリストを活用して本番環境を構築してください。ユースケース一覧では請求書・証明書・レポートなどの実装事例も紹介しています。