LoginSignup
13
9

More than 5 years have passed since last update.

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

Last updated at Posted at 2019-01-02

tl;dr

概要

この前のre:InventでLambda Layerが発表されました。

共通ライブラリを一つにまとめるという使い方もありますが、Lambdaの容量制限である50Mを超える何かを詰めるのに使うのは誰もが考える話だと思います。そうですHeadless Chrome。

とうことでやってました。結果としては簡単だった。

Headless ChromeのLayerを作る

serverlessを使います。いつの間にかlayerも作れるようになってました。

結果としては、Layerがない時代からheadless chromeを動かすような 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が文字化けします。
どうせなので合わせて対応しました。

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

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

いいことづくめ!!

13
9
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
9