AWS Lambda（Node.js 18.x）でwkhtmltopdfを使用するまでの流れ

# wkhtmltopdfを使用することになった背景
## Puppeteerでは大容量ファイルに対応できなかった
私が開発しているLaravelのWebサービスでは書類をPDF生成するという機能があります。
今まではAWS Lambda上でPuppeteerを使用してPDF生成を行っていましたが、最近になってHTMLファイルの時点で65MBという巨大なファイルをPDF生成するケースが発生し始めてしまい、実行途中でブラウザのタイムアウトや切断が発生するようになってしまったため、緊急で他の方法に入れ替えなければならなくなりました。

## 他の選択肢の検討
最初に検討したのはLambdaを使用することを辞めて、[barryvdh/laravel-dompdf](https://github.com/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のリポジトリ](https://github.com/wkhtmltopdf/wkhtmltopdf)はアーカイブされており、今後更新されることはない状態。
[wkhtmltopdfのダウンロードページ](https://wkhtmltopdf.org/downloads.html)にはAmazon Linux 2版が提供されていますが、Amazon Linux 2023版は提供されていません。
[Lambda ランタイム](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/lambda-runtimes.html)ではNode.js 18.xまでは実行環境がAmazon Linux 2ですが、Node.js 20.xからAmazon Linux 2023に変更されるため、Node.jsランタイムのバージョンアップを行うタイミングでwkhtmltopdf以外の選択肢を考える必要があります。
[GitHub上のやり取り](https://github.com/amazonlinux/amazon-linux-2023/issues/570)を見たところ、Alma Linux 9版を使えばAmazon Linux 2023でも動くという話もあるみたいですが、私は未検証です。

# AWS Lambdaでwkhtmltopdfを使用するまでの手順
## Lambdaレイヤーを作成する
### wkhtmltopdfをダウンロードする
- [こちら](https://wkhtmltopdf.org/downloads.html)にアクセスして、Amazon Linux 2 (lambda zip)をダウンロード
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280320/3cc6ef51-247d-bda1-9df5-03fae1a7fa8d.png)

### レイヤーを作成する
- ダウンロードしたzipファイルを解凍する
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280320/458be5c7-f023-dc85-5e88-8370732cde3e.png)

- 解凍フォルダ内のfontsフォルダに日本語フォントを設置する（今回はNoto Sans CJK-Regularのみ使用）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280320/cf4ab0e5-395e-ed3b-5e48-8f2d1e8d13cd.png)


- 解凍フォルダをもう一度zip圧縮する
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280320/d8e0a815-0cf0-0fca-ebc0-18bdd0295020.png)

- AWS Lambdaのコンソール画面でサイドバーの[レイヤー]→右上部[レイヤーの作成]を押下
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280320/809e26dd-1dac-1051-88cd-e2e4c5f9a9c7.png)

- アップロードボタンからzipファイルをアップロードしてレイヤーを作成する
※10MB超えてもS3使わずに問題なく利用できている
※エラーになった場合はS3を使用してzipファイルをアップロードする

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280320/8042154d-f10f-e988-aa85-a7c30700f08f.png)

## Lambda関数の作成
### 関数の作成
- AWS Lambdaのコンソール画面で右上部[関数の作成]を押下

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280320/f6ddbe8b-b57b-6fea-d8c8-d6100349c9a4.png)

- ランタイムをNode.js 18.xに設定して[関数の作成]を押下
※Lambda関数用のロールを作成している場合はここで設定
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280320/6ded63d4-956c-6964-cd24-e5fa5e3ee527.png)

### 関数にレイヤーを追加
- コードタブ最下部から[レイヤーの追加]を押下
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280320/51e16012-4296-e5a5-ee1c-72f12bb8897c.png)

- カスタムレイヤーを選択して、先ほど作成したレイヤーを追加
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280320/9a9e1fcb-959e-0659-7608-709354d5cef6.png)

- これでwkhtmltopdfの実行ファイルが"/opt/bin/"に設置されたので、Lambda関数でこれを呼び出せばPDF生成が実行できる

### コードの設定
- 実行コード例（※そのままコピペしても動きません）
```javascript
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に増やす
- 設定タブ→一般設定からメモリを1,536MBに設定
※メモリの設定によってLambda利用料金が倍増するので、ユースケースに合わせて設定する
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280320/889be08f-aee8-9cdd-1350-70e47d5a3af5.png)

# 日本語の文字化けが起きたら確認すること
- HTMLファイルに以下のタグがない場合は追加する
※忘れると日本語が謎の文字で表示されました
```html
<meta charset="utf-8">
```

- レイヤーのfontsフォルダに日本語フォントファイルが入っていなければ追加する
※忘れると日本語が「□」で表示されました
※スタイルの中でフォントを指定しなくてもフォントファイルをfontsフォルダに設置するだけで日本語で表示されました