tl;dr
- Githubにまとめた https://github.com/celeron1ghz/lambda-layer-puppeteer
- fontにread permissionに注意
概要
この前のre:InventでLambda Layerが発表されました。
共通ライブラリを一つにまとめるという使い方もありますが、Lambdaの容量制限である50Mを超える何かを詰めるのに使うのは誰もが考える話だと思います。そうですHeadless Chrome。
とうことでやってました。結果としては簡単だった。
Headless ChromeのLayerを作る
serverlessを使います。いつの間にかlayerも作れるようになってました。
結果としては、Layerがない時代からheadless chromeを動かすような serverless-chrome というプロジェクトがLambda上で動かすバイナリを提供してくれているので、設定ファイルを書いてコマンド打つだけだった。セキュリティが気になる人は頑張ってバイナリをビルドしよう。
service: puppeteer
provider:
name: aws
runtime: nodejs8.10
stage: dev
region: ap-northeast-1
package:
exclude:
- node_modules/@serverless-chrome/lambda/dist/**
layers:
puppeteer:
path: node_modules/@serverless-chrome/lambda/dist
name: puppeteer
description: chrome-headless binary
欲しいのはバイナリだけなのでLayerではバイナリのあるディレクトリだけを指定。
そしてデプロイの設定のzipには含まれないようにglobalの設定では除外。
そして
yarn @serverless-chrome/lambda
sls deploy
Layer側はこれで終わり。めっちゃ簡単やんけ。
無駄にfunction作られちゃうのかなと思ったりしたんですが、functionの設定を記載せずにlayerの設定だけ書けばちゃんとlayerだけ作られるようになってました。かしこい。
あとは使う側でLayerの設定と、Layerは/opt/
にマウントされるという点を考慮すればおk。
functions:
main:
handler: handler.main
timeout: 60
memoriSize: 3008
layers:
- Fn::Join: [ ":", [ "arn:aws:lambda", { Ref: AWS::Region }, { Ref: AWS::AccountId }, "layer:puppeteer:3" ] ]
module.exports.main = async (event, context) => {
try {
const browser = await puppeteer.launch({
headless: true,
executablePath: '/opt/headless-chromium',
args: ['--no-sandbox', '--disable-gpu', '--single-process'],
});
const page = await browser.newPage();
await page.goto("https://google.co.jp", { waitUntil: ['domcontentloaded', 'networkidle0'] });
const ret = await page.screenshot();
} catch(e) {
console.log("Error happen:", e);
}
return { message: 'OK' };
};
これだけで動く!
FontファイルのLayerも作る
こういうヘッドレスブラウザではフォントが入っていなくてscreenshotが文字化けします。
どうせなので合わせて対応しました。
対応としては、下記のようにフォントファイル+フォントキャッシュ+設定ファイルをすれば化けなくなります。実際に必要なファイル等も下記を参照。
上記でも直接lambdaで動かしていますが、lambdaのdockerイメージもあることなのでdockerでやってみました。
フォントファイルを置いて、下記のhandlerを追加して
const exec = require('child_process').execSync;
module.exports.fontcache = async (event, context) => {
process.env.HOME = process.env.LAMBDA_TASK_ROOT;
const fontdir = `${process.env.HOME}/.fonts`;
const tempdir = "/tmp/cache/fontconfig/";
console.log(exec(`fc-cache -v ${fontdir}`).toString());
//console.log(exec(`ls ${process.env.HOME}`).toString());
//console.log(exec(`ls -la ${tempdir}`).toString());
console.log(exec(`cp ${tempdir}\* /tmp/result`).toString());
return { message: 'OK' };
};
あとはコマンド実行!!
ここでcacheが生成されなくてめっちゃ悩んでたんですが、GoogleからダウンロードしたNotoSansの.ttc
のパーミッションが0640
だったので見えてないというだけだったやつでした。皆さんもお気を付けください。
mkdir -p .fontconfig
chmod 0777 .fontconfig
docker run \
-v "$PWD:/var/task" \
-v "$PWD/.fontconfig:/tmp/result:rw" \
-it lambci/lambda:nodejs8.10 \
handler.fontcache
chmod 0755 .fontconfig
コマンド一発でできるようになって良いですね。
あとはデプロイ。
sls deploy
簡単ですね。dockerの環境分離は素晴らしい。
使う側も少し変更する必要があるけどごく些細な変更ですみます。
functions:
main:
handler: handler.main
timeout: 60
memoriSize: 3008
layers:
- Fn::Join: [ ":", [ "arn:aws:lambda", { Ref: AWS::Region }, { Ref: AWS::AccountId }, "layer:puppeteer:3" ] ]
- Fn::Join: [ ":", [ "arn:aws:lambda", { Ref: AWS::Region }, { Ref: AWS::AccountId }, "layer:japanese_font:1" ] ]
module.exports.main = async (event, context) => {
process.env.HOME = "/opt/"; //ADD THIS LINE
};
三行程度の追加で使えるようになる&デプロイ時のzipの容量が少なくなる!
いいことづくめ!!