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

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


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が文字化けします。

どうせなので合わせて対応しました。

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

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の容量が少なくなる!

いいことづくめ!!

コメント

このブログの人気の投稿

投稿時間:2021-06-17 05:05:34 RSSフィード2021-06-17 05:00 分まとめ(1274件)

投稿時間:2021-06-20 02:06:12 RSSフィード2021-06-20 02:00 分まとめ(3871件)

投稿時間:2020-12-01 09:41:49 RSSフィード2020-12-01 09:00 分まとめ(69件)