Lambda上でPuppeteer/Headless Chromeを動かすStarter Kitを作った

node.jswebawsproduct

PuppeteerでHeadless Chromeを動かすコードを Lambda上で動かすStarter Kitを作った。

puppeteer-lambda-starter-kit

Chromeの準備

Puppeteerのインストール時に落としてくるChromeをLambda上で動かそうとしても Lambdaにないshared libraryに依存しているため失敗する。

error while loading shared libraries: libpangocairo-1.0.so.0: cannot open shared object file: No such file or directory

Lambda上でHeadless Chromeを動かす例がないか調べたらserverless-chromeというのがあって、 Headless用の設定でChromeをビルドしていた。 ほかにはchromelessというのもあるが これはserverless-chromeに 依存している。 最小構成でPuppeteerを使いたかったので、今回はこれらを使わず一から作ることにした。

serverless-chromeにもビルドしたものが置いてあるが、少しバージョンが古いようだったので最新版でビルドした。 基本的には書いてある 通りやればうまくいく。他のプロセスとのshared memoryとして/dev/shmを使っているのを、/tmpに置き換える ようにしないと、実行時の page.goto() で Failed Provisional Load: ***, error_code: -12 になる。

ビルドしたheadless_shellには問題になった依存は含まれていないようだ。

$ ldd headless_shell 
	linux-vdso.so.1 =>  (0x00007ffcb6fed000)
	libpthread.so.0 => /lib64/libpthread.so.0 (0x00007f5f17dbe000)
	libdl.so.2 => /lib64/libdl.so.2 (0x00007f5f17bba000)
	librt.so.1 => /lib64/librt.so.1 (0x00007f5f179b1000)
	libnss3.so => /usr/lib64/libnss3.so (0x00007f5f17692000)
	libnssutil3.so => /usr/lib64/libnssutil3.so (0x00007f5f17466000)
	libsmime3.so => /usr/lib64/libsmime3.so (0x00007f5f1723e000)
	libnspr4.so => /lib64/libnspr4.so (0x00007f5f17001000)
	libexpat.so.1 => /lib64/libexpat.so.1 (0x00007f5f16dd8000)
	libfontconfig.so.1 => not found
	libfreetype.so.6 => /usr/lib64/libfreetype.so.6 (0x00007f5f16b3b000)
	libm.so.6 => /lib64/libm.so.6 (0x00007f5f16839000)
	libstdc++.so.6 => /usr/lib64/libstdc++.so.6 (0x00007f5f16533000)
	libgcc_s.so.1 => /lib64/libgcc_s.so.1 (0x00007f5f1631d000)
	libc.so.6 => /lib64/libc.so.6 (0x00007f5f15f5b000)
	/lib64/ld-linux-x86-64.so.2 (0x000055ba0af5e000)
	libplc4.so => /lib64/libplc4.so (0x00007f5f15d55000)
	libplds4.so => /lib64/libplds4.so (0x00007f5f15b51000)

Puppetterで落としてくる普通のChromeはLambdaの制限の50MBを超えていたが、 ビルドしたものはぎりぎり超えていないのでパッケージに含められるようになった。 PuppeteerのChromeは環境変数 PUPPETEER_SKIP_CHROMIUM_DOWNLOAD を設定することで含めないようにできる

他のパッケージのサイズによっては50MBを超えてしまうこともあるので、 パッケージに含めずS3からダウンロードできるようにもした。

いずれの場合も最終的な置き先はLambdaで唯一書き込める/tmpになる。 この領域は512MBまで使えるので展開してもまだ余裕がある。

Error: EROFS: read-only file system, open 'node_modules/puppeteer/.local-chromium'

ChromeのLaunch時のOption

いろいろ試した結果、最低限必要だったのはこのあたり。

exports.launchOptionForLambda = [
    // error when launch(); No usable sandbox! Update your kernel
    '--no-sandbox',
    // error when launch(); Failed to load libosmesa.so
    '--disable-gpu', 
    // freeze when newPage()
    '--single-process'
];

エラーは分かりづらいものが多く、ときにはエラーすら出ずに止まってしまうこともある。 デバッグの際はdumpioを有効にする。

const browser = await puppeteer.launch({
    ...
    dumpio: !!util.DEBUG,
});  

Babel

現在のLambdaのNodeのバージョンはv6.10.3。 node.greenによるとES2015は99%対応していて、ES2016もべき乗演算子(2 ** 3 = 8)以外は対応しているが、ES2017のasync/awaitは7.6からなので、8系に対応するまではbabelにかける必要がある。 ちなみにPuppeteerは6.4以降で動く

$ yarn add --dev babel-cli babel-preset-env

babel-preset-env .babelrcはこんな感じ。

$ cat .babelrc
{
  "presets": [
    ["env", {
      "targets": {
        "node": "6.10"
      }
    }]
  ]
}