仕事で、webと全く同じ見た目でPDFを出力するという業務要件に遭遇したので、Serverless+Lambda+Puppeteer+Typescriptで戦ってみた。いつか戦う運命だと感じていた
つまづいた9のことを残す。
1. @serverless-chrome/lambda
は利用できない
serverlessを利用するので、 「serverless chrome pdf」でぐぐると、Serverless公式のexampleとしてPuppeteerを利用したサンプルプロジェクトが出てきた
https://www.serverless.com/examples/aws-node-puppeteer
https://github.com/serverless/examples/tree/master/aws-node-puppeteer
公式のexampleだと思って試してみたが、Lambdaにデプロイした後invokeすると
ERROR Error occured in serverless-plugin-chrome wrapper when trying to ensure Chrome for hello() handler. { flags: [] } Error: Unable to start Chrome.
とエラーが出た。調べるとライブラリの@serverless-chrome/lambda
に一部不具合があり利用できないようだった。
2. PuppeteerでPDF出力を行う場合はheadless: true
でなければならない
chrome-aws-lambda
を利用すべきだと先ほどのissueで言われたので、chrome-aws-lambda
を使っていそうなデモライブラリを見つけてきた
試してみたけどいい感じに動いたので page.pdf()
を呼び出すもエラーになった
UnhandledPromiseRejectionWarning: Error: Protocol error (Page.printToPDF): PrintToPDF is not implemented
原因はヘッドレスモードではなかったため。ヘッドレスモードでしかPDF出力できなかった。
3. PDF出力時の見た目をブラウズ時と一致させる場合、page.emulateMedia("screen")
が必要
https://pptr.dev/#?product=Puppeteer&version=v3.1.0&show=api-pageemulatemediatype
4. chrome-aws-lambda
とPuppeteer
のバージョンはできるだけ最新に
ローカルでいい感じに動いたのでデプロイしたが、lambdaで改めて実行するとまたエラー
Cannot find module 'iltorb'\nRequire stack:\n- /var/task/node_modules/chrome-aws-lambda/source/index.js\n- /var/task/functions/pdf.js\n- /var/runtime/UserFunction.js\n- /var/runtime/index.js","code":"MODULE_NOT_FOUND","requireStack":["/var/task/node_modules/chrome-aws-lambda/source/index.js
Node.jsのバージョンが10.15に上がった段階で変更が入ったように読める。
書かれてる通り yarn add iltorb
してみたがダメ。
https://github.com/alixaxel/chrome-aws-lambda/issues/55#issuecomment-568113363
chrome-aws-lambda
のREADME読んでねぇと思ったので読んでたらPuppeteerのバージョンが全然違うことに気づいた。1.19.0
ってどんなけ前のやつ使ってたんや…
Puppeteer関連とchrome-aws-lambdaのバージョンを上げてみたらすっきり解決
- puppeteer-core
- 1.19.0 → 3.1.0
- puppeteer
- 1.19.0 → 3.1.0
- chrome-aws-lambda
- 1.19.0 → 3.1.1
5. Puppeteerで日本語を表示するならフォントを読み込む
日本語が文字化けして豆腐になっていた…😇
この方法を発見したがややこしそうなのでパスした
chrome-aws-lambdaのREADMEにfontに関する説明があったので、すすめられた通りraw.githack.com
を介してNoto-Sans-CJK-JP
のttfファイルを取得して設定した
import chromium from "chrome-aws-lambda";
const FONT_PATH = process.env.FONT_PATH;
// ...
const init = async (): Promise<Page> => {
const readFontProcess = chromium.font(FONT_PATH);
// ...
await readFontProcess;
// ...
}
fontはこちらを利用した。
6. lambdaを通すと リクエストヘッダーのkeyが小文字になる
X-LAMBDA-ACCESS-KEY
というヘッダーを設定してリクエストを送ると、lambda内でヘッダーを取得した際には キーがx-lambda-access-key
となってしまう
もしかするとmiddy
が変換処理をしている可能性がある.要調査
7. lambdaにデプロイした場合のみ、bodyがbase64でエンコードされている
ローカルでの実行時には、json形式のリクエストボディを取得する際
const bodyObject = JSON.parse(event.body);
で取得できたが、Lambdaでの実行時には base64でエンコードされていた。isBase64Encoded
をもとにデコードするか判断することにした
let body = event.body;
if (!body) {
throw new Error("body can't loaded.");
}
if (event.isBase64Encoded) {
const buff = Buffer.from(body, "base64");
body = buff.toString("ascii");
}
const bodyObject = JSON.parse(body);
8. Puppeteerのサイトアクセス時のヘッダーを加工するにはpage.setRequestInterception(true)
とpage.on("request", _)
を利用する
今回の場合、特定のwebサーバーにlambdaで印刷する専用の画面を用意し、そこにPuppeteerがアクセスしてPDFを出力する。
となるとlambda以外からのリクエストは全て弾く必要があるので、リクエストヘッダーに独自トークンをつける必要が出てきた
page.setRequestInterceptionを利用すると以下のように実装することができた
await page.setRequestInterception(true).then(() => {
page.on("request", (request) => {
const headers = request.headers();
headers["X-HOGEHOGE-TOKEN"] = accessToken;
request.continue({
headers,
});
});
})
9. middy
とTypescriptの相性は少し悪い?
const handler: APIGatewayProxyHandler = async (
event
) => {
// ....
}
と書くとmiddyにhandlerを登録する時、型が不一致になる
middy公式の方法を試しても変わらず。
If you are using TypeScript, you will also want to make sure that you have installed the @types/aws-lambda peer-dependency:
npm install --save-dev @types/aws-lambda
結局、このエラーはAPIGatewayProxyHandler
型を利用せずに、APIGatewayProxyEvent
型とAPIGatewayProxyResult
型を利用することで回避できた
const handler = async (
event: APIGatewayProxyEvent
): Promise<APIGatewayProxyResult> => {
// ...
}
所感
コードはこちらに
- 最近コード書く機会がないので楽しかった
- 微妙にしか知らないスタック同士を組み合わせたものを作る時、学ぶことがとてつもなく多い
- 初心者がそういう動きをするとどのスタックで詰まっているのかわからず死ぬと思った
- いくつか対応できてない
- コールドスタート対策
- エラーハンドリング
- middyの
httpErrorHandler
を有効活用できていない
- middyの
- ビジネスロジック分離
- awsがBestPracticeとして載せてる
middy
初めて使ってみた👀