NaN/NaN/NaN

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, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#039;');
}

// 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キー管理・エラーハンドリング・基本監視)に絞り、トラフィックが増えるにつれてスロットリング・バッチ化・キャッシュを追加していくアプローチが現実的です。

各項目の詳細は以下の記事で深掘りしています。

まずはプレイグラウンドでAPIを試し、ドキュメントで仕様を確認したうえで、本記事のチェックリストを活用して本番環境を構築してください。ユースケース一覧では請求書・証明書・レポートなどの実装事例も紹介しています。

Powered by FUNBREW PDF