LambdaでHeadless Chrome(puppeteer)をlayerに分離した上で動かす


## 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](https://github.com/adieuadieu/serverless-chrome) というプロジェクトがLambda上で動かすバイナリを提供してくれているので、設定ファイルを書いてコマンド打つだけだった。セキュリティが気になる人は頑張ってバイナリをビルドしよう。


```serverless.yml
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が文字化けします。
どうせなので合わせて対応しました。

対応としては、下記のようにフォントファイル＋フォントキャッシュ＋設定ファイルをすれば化けなくなります。実際に必要なファイル等も下記を参照。

https://qiita.com/komeda-shinji/items/e049edd1389579059c53

上記でも直接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の容量が少なくなる！

いいことづくめ！！