サーバーレスアーキテクチャでPDFを生成したいとき、Lambda関数にChromiumをバンドルするのは現実的ではありません。デプロイパッケージの容量制限、コールドスタートの遅延、メモリ消費の問題が発生します。
PDF生成をAPIに委任すれば、Lambda関数は軽量なHTTPリクエストを送るだけで済みます。この記事では、FUNBREW PDFのAPIをAWS Lambdaから呼び出してサーバーレスにPDFを生成する方法を、Node.jsとPythonのコード例付きで解説します。
API自体の基本的な使い方はクイックスタートガイドを参照してください。
サーバーレスでPDF APIを使うメリット
Lambda + 自前PDF生成の問題点
| 課題 | 詳細 |
|---|---|
| デプロイサイズ | Chromiumバンドルで約130MB。Lambdaの250MB制限に迫る |
| コールドスタート | 重いパッケージほど初回起動が遅い(5〜10秒以上) |
| メモリ消費 | Chromiumの起動だけで500MB以上必要 |
| 並行実行 | 同時実行数の制限にすぐ到達する |
PDF API委任のメリット
| メリット | 詳細 |
|---|---|
| 軽量デプロイ | HTTPクライアントだけで数KB |
| 高速起動 | コールドスタートが100ms以下に |
| 低メモリ | 128MBでも十分動作 |
| 無制限スケール | API側が並列処理を担当 |
| コスト削減 | メモリ割り当てを最小限にできる |
準備
APIキーの管理
Lambda関数でAPIキーを安全に管理するには、AWS Secrets Managerまたは環境変数の暗号化を使います。
# AWS CLIでSecrets Managerにキーを保存
aws secretsmanager create-secret \
--name funbrew-pdf-api-key \
--secret-string "sk-your-api-key"
APIキーのセキュリティについてはセキュリティガイドで詳しく解説しています。
IAMロールの設定
Lambda関数には以下の権限が必要です。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"secretsmanager:GetSecretValue"
],
"Resource": "arn:aws:secretsmanager:ap-northeast-1:*:secret:funbrew-pdf-api-key-*"
},
{
"Effect": "Allow",
"Action": [
"s3:PutObject"
],
"Resource": "arn:aws:s3:::your-pdf-bucket/*"
}
]
}
Node.jsでのLambda関数実装
基本的なPDF生成
const { SecretsManagerClient, GetSecretValueCommand } = require('@aws-sdk/client-secrets-manager');
const { S3Client, PutObjectCommand } = require('@aws-sdk/client-s3');
const secretsManager = new SecretsManagerClient({ region: 'ap-northeast-1' });
const s3 = new S3Client({ region: 'ap-northeast-1' });
let cachedApiKey = null;
async function getApiKey() {
if (cachedApiKey) return cachedApiKey;
const command = new GetSecretValueCommand({ SecretId: 'funbrew-pdf-api-key' });
const response = await secretsManager.send(command);
cachedApiKey = response.SecretString;
return cachedApiKey;
}
exports.handler = async (event) => {
const apiKey = await getApiKey();
// PDF生成APIを呼び出し
const response = await fetch('https://api.pdf.funbrew.cloud/v1/pdf/from-html', {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
html: event.html || '<h1>Hello from Lambda</h1>',
engine: 'quality',
format: 'A4',
}),
});
if (!response.ok) {
const errorBody = await response.text();
throw new Error(`PDF API error: ${response.status} - ${errorBody}`);
}
const pdfBuffer = Buffer.from(await response.arrayBuffer());
// S3にアップロード
const key = `pdfs/${Date.now()}.pdf`;
await s3.send(new PutObjectCommand({
Bucket: process.env.PDF_BUCKET,
Key: key,
Body: pdfBuffer,
ContentType: 'application/pdf',
}));
return {
statusCode: 200,
body: JSON.stringify({
message: 'PDF generated and uploaded',
s3Key: key,
size: pdfBuffer.length,
}),
};
};
テンプレートを使ったPDF生成
HTMLテンプレートとデータを分離すると、Lambda関数の汎用性が上がります。テンプレートの設計についてはテンプレートエンジンガイドを参照してください。
exports.handler = async (event) => {
const { templateId, data } = event;
const apiKey = await getApiKey();
// テンプレートに動的データを埋め込む
const html = buildHtml(templateId, data);
const response = await fetch('https://api.pdf.funbrew.cloud/v1/pdf/from-html', {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ html, engine: 'quality', format: 'A4' }),
});
if (!response.ok) {
throw new Error(`PDF API error: ${response.status}`);
}
// 以降、S3アップロードなどの処理
// ...
};
function buildHtml(templateId, data) {
const templates = {
invoice: (d) => `
<html>
<head><style>
body { font-family: 'Noto Sans JP', sans-serif; padding: 40px; }
.header { display: flex; justify-content: space-between; }
table { width: 100%; border-collapse: collapse; margin-top: 20px; }
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
</style></head>
<body>
<div class="header">
<h1>請求書</h1>
<p>No. ${d.invoiceNumber}</p>
</div>
<p>発行日: ${d.date}</p>
<p>宛先: ${d.customerName}</p>
<table>
<tr><th>品目</th><th>数量</th><th>単価</th><th>金額</th></tr>
${d.items.map(i => `<tr><td>${i.name}</td><td>${i.qty}</td><td>${i.price}</td><td>${i.qty * i.price}</td></tr>`).join('')}
</table>
</body>
</html>
`,
};
return templates[templateId](data);
}
請求書PDFの自動化については請求書PDF自動化ガイドも参考になります。
PythonでのLambda関数実装
import json
import os
import time
import boto3
import urllib.request
secrets_client = boto3.client('secretsmanager', region_name='ap-northeast-1')
s3_client = boto3.client('s3', region_name='ap-northeast-1')
cached_api_key = None
def get_api_key():
global cached_api_key
if cached_api_key:
return cached_api_key
response = secrets_client.get_secret_value(SecretId='funbrew-pdf-api-key')
cached_api_key = response['SecretString']
return cached_api_key
def handler(event, context):
api_key = get_api_key()
html = event.get('html', '<h1>Hello from Lambda</h1>')
# PDF生成APIを呼び出し
payload = json.dumps({
'html': html,
'engine': 'quality',
'format': 'A4',
}).encode('utf-8')
req = urllib.request.Request(
'https://api.pdf.funbrew.cloud/v1/pdf/from-html',
data=payload,
headers={
'Authorization': f'Bearer {api_key}',
'Content-Type': 'application/json',
},
method='POST',
)
try:
with urllib.request.urlopen(req, timeout=30) as resp:
pdf_bytes = resp.read()
except urllib.error.HTTPError as e:
error_body = e.read().decode('utf-8')
raise Exception(f'PDF API error: {e.code} - {error_body}')
# S3にアップロード
bucket = os.environ['PDF_BUCKET']
key = f'pdfs/{int(time.time() * 1000)}.pdf'
s3_client.put_object(
Bucket=bucket,
Key=key,
Body=pdf_bytes,
ContentType='application/pdf',
)
return {
'statusCode': 200,
'body': json.dumps({
'message': 'PDF generated and uploaded',
's3Key': key,
'size': len(pdf_bytes),
}),
}
Pythonの場合、標準ライブラリのurllibだけで実装できるため、外部パッケージのインストールが不要です。Lambda Layerを用意する必要もありません。
API Gateway + Lambda構成
REST APIとしてPDF生成を公開するには、API GatewayとLambdaを組み合わせます。
アーキテクチャ
クライアント → API Gateway → Lambda → FUNBREW PDF API
↓
S3(PDF保存)
↓
CloudFront(配信)
SAMテンプレート
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Globals:
Function:
Timeout: 60
MemorySize: 256
Runtime: nodejs20.x
Environment:
Variables:
PDF_BUCKET: !Ref PdfBucket
Resources:
PdfGeneratorFunction:
Type: AWS::Serverless::Function
Properties:
Handler: index.handler
CodeUri: src/
Events:
GeneratePdf:
Type: Api
Properties:
Path: /generate-pdf
Method: post
Policies:
- S3CrudPolicy:
BucketName: !Ref PdfBucket
- AWSSecretsManagerGetSecretValuePolicy:
SecretArn: !Sub arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:funbrew-pdf-api-key-*
PdfBucket:
Type: AWS::S3::Bucket
Properties:
LifecycleConfiguration:
Rules:
- Id: DeleteOldPdfs
Status: Enabled
ExpirationInDays: 30
Outputs:
ApiEndpoint:
Value: !Sub https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/generate-pdf
コールドスタート対策
Lambdaのコールドスタートを最小限に抑えるためのテクニックを紹介します。
1. Provisioned Concurrency
頻繁に呼ばれる関数には、Provisioned Concurrencyを設定してコールドスタートを完全に排除します。
# SAMテンプレートに追加
PdfGeneratorFunction:
Properties:
ProvisionedConcurrencyConfig:
ProvisionedConcurrentExecutions: 5
2. 初期化処理をハンドラ外に
AWS SDKクライアントやAPIキーのキャッシュは、ハンドラ関数の外側で初期化します。上のコード例ではcachedApiKey変数がこのパターンを実装しています。Lambda実行環境が再利用されるとき、これらの変数は保持されます。
3. 軽量なデプロイパッケージ
PDF APIを使う最大のメリットがここにあります。Chromiumをバンドルする必要がないため、パッケージサイズを数KB〜数MBに抑えられます。
# Node.jsの場合、AWS SDK v3は個別パッケージをインストール
npm install @aws-sdk/client-s3 @aws-sdk/client-secrets-manager
# Pythonの場合、追加パッケージ不要(boto3はLambdaランタイムに含まれる)
4. SnapStart(Java)
Java Lambdaを使う場合、SnapStartを有効化するとコールドスタートを大幅に短縮できます。
タイムアウト対策
Lambdaのタイムアウト上限は15分ですが、API Gatewayを通す場合は29秒の制限があります。
同期処理の場合
単一のPDF生成は通常1〜5秒で完了するため、API Gateway経由でも問題ありません。
// Lambda関数のタイムアウトを60秒に設定(余裕を持たせる)
// API Gatewayのタイムアウトは29秒
非同期処理が必要なケース
大量のPDF生成や複雑なテンプレートの場合は、非同期パターンを使います。
クライアント → API Gateway → Lambda(ジョブ登録)
↓
SQS / EventBridge
↓
Lambda(PDF生成)→ S3
↓
Webhook通知 → クライアント
const { SQSClient, SendMessageCommand } = require('@aws-sdk/client-sqs');
const sqs = new SQSClient({ region: 'ap-northeast-1' });
// ジョブ登録Lambda
exports.enqueueHandler = async (event) => {
const body = JSON.parse(event.body);
const jobId = `job-${Date.now()}`;
await sqs.send(new SendMessageCommand({
QueueUrl: process.env.PDF_QUEUE_URL,
MessageBody: JSON.stringify({
jobId,
html: body.html,
webhookUrl: body.webhookUrl,
}),
}));
return {
statusCode: 202,
body: JSON.stringify({ jobId, status: 'queued' }),
};
};
// PDF生成Lambda(SQSトリガー)
exports.processHandler = async (event) => {
for (const record of event.Records) {
const { jobId, html, webhookUrl } = JSON.parse(record.body);
// PDF生成
const pdfBuffer = await generatePdf(html);
// S3にアップロード
const s3Key = `pdfs/${jobId}.pdf`;
await uploadToS3(s3Key, pdfBuffer);
// Webhook通知
if (webhookUrl) {
await fetch(webhookUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
event: 'pdf.generated',
jobId,
s3Key,
size: pdfBuffer.length,
}),
});
}
}
};
Webhookの詳しい実装方法はWebhook連携ガイドを参照してください。
S3連携:生成したPDFの管理
署名付きURLでの配信
生成したPDFをユーザーに配信する場合、S3の署名付きURLを使うと認証付きの一時URLを発行できます。
const { GetObjectCommand } = require('@aws-sdk/client-s3');
const { getSignedUrl } = require('@aws-sdk/s3-request-presigner');
async function getDownloadUrl(s3Key) {
const command = new GetObjectCommand({
Bucket: process.env.PDF_BUCKET,
Key: s3Key,
});
// 1時間有効な署名付きURL
return await getSignedUrl(s3, command, { expiresIn: 3600 });
}
ライフサイクルルール
不要になったPDFを自動削除するライフサイクルルールを設定しておきましょう。SAMテンプレートの例では30日後に自動削除しています。
トラブルシューティング
よくあるエラーと対処法
| エラー | 原因 | 対処 |
|---|---|---|
Task timed out |
Lambda/API Gatewayのタイムアウト超過 | タイムアウト値の見直し、非同期処理への切り替え |
ECONNRESET |
API呼び出し中のネットワークエラー | リトライロジックの実装 |
AccessDeniedException |
IAMロールの権限不足 | S3・Secrets Managerのポリシーを確認 |
413 Payload Too Large |
HTMLが大きすぎる | HTMLの最適化、外部CSS/画像の使用 |
エラーハンドリングの詳細はエラーハンドリングガイドを参照してください。
リトライ戦略
Lambda関数にリトライロジックを組み込みましょう。
async function callPdfApiWithRetry(html, maxRetries = 3) {
const apiKey = await getApiKey();
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const response = await fetch('https://api.pdf.funbrew.cloud/v1/pdf/from-html', {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ html, engine: 'quality', format: 'A4' }),
signal: AbortSignal.timeout(30000),
});
if (response.ok) {
return Buffer.from(await response.arrayBuffer());
}
// 4xxエラーはリトライしない
if (response.status >= 400 && response.status < 500) {
throw new Error(`Client error: ${response.status}`);
}
} catch (error) {
if (attempt === maxRetries) throw error;
// 指数バックオフ
await new Promise(r => setTimeout(r, 1000 * Math.pow(2, attempt - 1)));
}
}
}
CloudWatch Logsでのデバッグ
構造化ログを出力すると、CloudWatch Logs Insightsでの検索が容易になります。
console.log(JSON.stringify({
level: 'info',
event: 'pdf_generation',
status: 'success',
duration_ms: endTime - startTime,
pdf_size: pdfBuffer.length,
s3_key: key,
}));
本番運用のチェックリスト
- APIキーをSecrets Managerまたは暗号化環境変数で管理している
- Lambda関数のタイムアウトを適切に設定している(60秒推奨)
- リトライロジックを実装している
- S3バケットにライフサイクルルールを設定している
- CloudWatch Alarmsでエラー率を監視している
- DLQ(Dead Letter Queue)を設定している
本番環境の詳しい設定は本番環境ガイドも参考にしてください。
まとめ
AWS LambdaとPDF APIの組み合わせは、サーバーレスPDF生成の最適解です。Chromiumをバンドルする必要がなく、デプロイパッケージは軽量、コールドスタートは高速、メモリ使用量も最小限で済みます。
- 同期処理: API Gateway + Lambdaで単純なPDF生成
- 非同期処理: SQS + Lambda + Webhookで大量のPDF生成
- 配信: S3 + CloudFront + 署名付きURLで安全に配信
まずは無料アカウントを作成して、PlaygroundでAPI動作を確認してみてください。
関連リンク
- FUNBREW PDF APIドキュメント — エンドポイント仕様とオプション全覧
- 言語別クイックスタート — Node.js/Python/PHPの基本コード例
- PDF APIエラーハンドリング — リトライ戦略と指数バックオフの実装
- Webhook連携ガイド — 非同期処理完了後のPush通知設定
- PDF API本番運用チェックリスト — 本番環境のセキュリティ・モニタリング設定
- PDF APIセキュリティガイド — APIキー管理とアクセス制御
- PDF一括生成(バッチ処理)ガイド — 大量PDF生成のスループット最適化
- Docker・Kubernetes構成ガイド — コンテナ環境でのPDF API活用
- GitHub Actions CI/CD連携 — デプロイパイプラインでのPDF生成
- 請求書PDF自動化ガイド — 請求書生成の実装パターン
- PDFテンプレートエンジンガイド — 動的テンプレートの設計
- ユースケース一覧 — Lambda活用が多い業務シナリオ