wkhtmltopdfを使用することになった背景
Puppeteerでは大容量ファイルに対応できなかった
私が開発しているLaravelのWebサービスでは書類をPDF生成するという機能があります。
今まではAWS Lambda上でPuppeteerを使用してPDF生成を行っていましたが、最近になってHTMLファイルの時点で65MBという巨大なファイルをPDF生成するケースが発生し始めてしまい、実行途中でブラウザのタイムアウトや切断が発生するようになってしまったため、緊急で他の方法に入れ替えなければならなくなりました。
他の選択肢の検討
最初に検討したのはLambdaを使用することを辞めて、barryvdh/laravel-dompdfをWorker上で動かす運用でしたが、大きいHTMLファイルの処理に非常に時間がかかるため断念しました。
wkhtmltopdfはパフォーマンスに優れているのと、Lambda関数上でPuppeteerを使用している部分を入れ替えるだけで済むので、最適な選択肢だと判断しました
※wkhtmltopdfは既に開発が終了しているので、近い将来にまた別の選択肢を考えなければなりません。詳しくは後述
実行時間の比較
10MB | 65MB | 実行環境 | 備考 | |
---|---|---|---|---|
Puppeteer | 30秒 | Puppeteerのエラーで異常終了 | AWS Lambda | メモリを2,048MBにしてもNG |
wkhtmltopdf | 10秒 | 60秒 | AWS Lambda | メモリが1,024MBだと異常終了するが、1,536MBなら安定した |
dompdf | 15分 | 30分経過して終わらないので中断 | Ubuntu (WSL2) | php artisan tinkerでテスト |
wkhtmltopdfは開発終了している
GitHubのリポジトリはアーカイブされており、今後更新されることはない状態。
wkhtmltopdfのダウンロードページにはAmazon Linux 2版が提供されていますが、Amazon Linux 2023版は提供されていません。
Lambda ランタイムではNode.js 18.xまでは実行環境がAmazon Linux 2ですが、Node.js 20.xからAmazon Linux 2023に変更されるため、Node.jsランタイムのバージョンアップを行うタイミングでwkhtmltopdf以外の選択肢を考える必要があります。
GitHub上のやり取りを見たところ、Alma Linux 9版を使えばAmazon Linux 2023でも動くという話もあるみたいですが、私は未検証です。
AWS Lambdaでwkhtmltopdfを使用するまでの手順
Lambdaレイヤーを作成する
wkhtmltopdfをダウンロードする
-
こちらにアクセスして、Amazon Linux 2 (lambda zip)をダウンロード
レイヤーを作成する
-
解凍フォルダ内のfontsフォルダに日本語フォントを設置する(今回はNoto Sans CJK-Regularのみ使用)
-
アップロードボタンからzipファイルをアップロードしてレイヤーを作成する
※10MB超えてもS3使わずに問題なく利用できている
※エラーになった場合はS3を使用してzipファイルをアップロードする
Lambda関数の作成
関数の作成
- AWS Lambdaのコンソール画面で右上部[関数の作成]を押下
関数にレイヤーを追加
-
これでwkhtmltopdfの実行ファイルが"/opt/bin/"に設置されたので、Lambda関数でこれを呼び出せばPDF生成が実行できる
コードの設定
- 実行コード例(※そのままコピペしても動きません)
const { exec } = require('child_process');
const fs = require("fs");
// その他必要なものをインポートする
exports.handler = async (event, context) => {
// 何らかの方法でPDF生成前のHTMLファイルをtmpディレクトリに設置しておく
const htmlFilePath = "PDF生成前のHTMLファイルのパス(tmpディレクトリ)";
const pdfFilePath = "生成したPDFファイル出力先のパス(tmpディレクトリ)";
return new Promise((resolve, reject) => {
exec(`/opt/bin/wkhtmltopdf ${htmlFilePath} ${pdfFilePath}`, {
env: {
...process.env,
LD_LIBRARY_PATH: '/opt/lib',
FONTCONFIG_PATH: '/opt/fonts'
}
}, (error, stdout, stderr) => {
if (error) {
return reject(error);
}
const output = fs.readFileSync(pdfFilePath);
// S3にアップする等、生成したPDFを使ってやりたいことをする
resolve({ data: "呼び出し元に返したい内容"});
});
});
}
メモリを1,536MBに増やす
日本語の文字化けが起きたら確認すること
- HTMLファイルに以下のタグがない場合は追加する
※忘れると日本語が謎の文字で表示されました
<meta charset="utf-8">
- レイヤーのfontsフォルダに日本語フォントファイルが入っていなければ追加する
※忘れると日本語が「□」で表示されました
※スタイルの中でフォントを指定しなくてもフォントファイルをfontsフォルダに設置するだけで日本語で表示されました