ヘッドレスChromeをAWS Lambda上のPuppeteerから操作してみた

AWS Lambda上でPuppeteerからヘッドレスChromeを操作してみよう!
2020.04.27

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

ブラウザテストやスクレイピングのためにPuppeteerからヘッドレスChromeを操作させたい時があります。

AWS Lambda上でPuppeteerを動作さるために、ナイーブにデプロイパッケージを作成すると、Lambdaのサイズ上限に引っかかってしまいます。

GitHub - alixaxel/chrome-aws-lambda を利用し、Lambdaのサイズ制限を回避する方法を紹介します。

AWS Lambdaのパッケージサイズ上限とChrome単体のサイズ

AWS Lambda のデプロイパッケージには以下のサイズ制限があります。

  • 50 MB (zip 圧縮済み、直接アップロード)
  • 250 MB (解凍、レイヤーを含む)

Puppeteer パッケージのサイズを確認すると、Puppeteer に同梱さているChrome単体で250MBもあり、Lambdaパッケージのサイズを超過することがわかります。

$ npm i puppeteer
$ du -s --block-size=MB *
377MB	node_modules
1MB	package-lock.json
$ ls -s --block-size=MB \
  node_modules/puppeteer/.local-chromium/linux-737027/chrome-linux/chrome
250MB chrome

サイズ上限を回避するために、AWS Lambda や Google Cloud Functions 向けにスリムダウンした Chrome を提供するのが、今回紹介する GitHub - alixaxel/chrome-aws-lambda です。

chrome-aws-lambda をLambda Layerとして登録

chrome-aws-lambda はデプロイパッケージに含めることもLambda Layerとして登録することも可能です。

今回は、再利用しやすいように Lambda Layerとして登録します。

Lambda Layer 用のパッケージを作成

今回は chrome-aws-lambda が提供している最新バージョンである 2.1.1 を利用します。

$ wget https://github.com/alixaxel/chrome-aws-lambda/archive/v2.1.1.tar.gz
$ tar xf v2.1.1.tar.gz
$ cd chrome-aws-lambda-2.1.1
$ make chrome_aws_lambda.zip
$ ls -s --block-size=MB chrome_aws_lambda.zip
45MB chrome_aws_lambda.zip

この Lambda Layer には Chrome と puppeteer-core やその他パッケージが含まれており、サイズはわずか45MB です。

Lambda Layer として登録

$ aws lambda publish-layer-version \
  --layer-name puppetteer \
  --description "puppeteer 2.1.1" \
  --license-info "MIT" \
  --zip-file fileb://chrome_aws_lambda.zip \
  --compatible-runtimes nodejs10.x nodejs12.x

{
    "LayerVersionArn": "arn:aws:lambda:eu-central-1:123:layer:puppetteer:1",
    "Description": "puppeteer 2.1.1",
    "CreatedDate": "2020-04-27T11:07:50.579+0000",
    "LayerArn": "arn:aws:lambda:eu-central-1:123:layer:puppetteer",
    ...
    "Version": 1,
    "CompatibleRuntimes": [
        "nodejs10.x",
        "nodejs12.x"
    ],
    "LicenseInfo": "MIT"
}

Puppeteer 関数を作成

Node.js ランタイムのLambda関数を作成します。

Chrome から URL を開き、ページタイトルを出力するだけのプログラムを用意します。

const chromium = require('chrome-aws-lambda');

exports.handler = async(event, context) => {

    const browser = await chromium.puppeteer.launch({
        args: chromium.args,
        defaultViewport: chromium.defaultViewport,
        executablePath: await chromium.executablePath,
        headless: chromium.headless,
    });

    let page = await browser.newPage();

    await page.goto('https://dev.classmethod.jp/');

    const result = await page.title();
    await browser.close();
    return result
};

Puppeteer 向けには

const puppeteer = require('puppeteer');

const chromium = require('chrome-aws-lambda');
const puppeteer = chromium.puppeteer

に読み替えてスクリプティングします。

また、puppeteer.launchのオプションはexecutablePath/tmp/chromium を指すなど、 AWS Lambda 環境向けに設定されています。 公式ドキュメントのオプションをおまじないとして指定しましょう。

Lambda LayerとLambda関数の紐付け

事前に登録した Lambda Layerを Lambda 関数に紐付けます。

Lambda 関数のメモリーを調整

chrome-aws-lambda の公式ドキュメントでは、Lambda 関数に割り当てる最低メモリーを 512 MB とし、推奨は 1600 MB となっています。

You should allocate at least 512 MB of RAM to your Lambda, however 1600 MB (or more) is recommended.

Lambda 関数のワークロードにあわせて適切なメモリーを割り当ててください。

Lambda関数を実行

最後に Lambda 関数をテスト実行します。 ページタイトルを取得できれば成功です。

テスト実行では最大で 545 MB のメモリーを消費しました。

Brotli でサイズ圧縮

どうやって Chrome の実行ファイルを削減しているのでしょうか?

肝は LZ77アルゴリズムをベースにした圧縮アルゴリズム Brotli で圧縮されていることです。

chrome-aws-lambda の公式ドキュメント によると、圧縮レベルを最大にすることで74.58%圧縮されたとあります(130.62MiB→33.21MiB)

なお chrome-aws-lambda に同梱されている Chrome バイナリのサイズは41MBでした。

$ ls -s --block-size=MB chromium.br
41MB chromium.br

その他にもいろいろなテクニックが使われているようで、興味のある方は Chrome バイナリの作成に利用されている Ansible Playbook をご確認ください。

最後に

GitHub - alixaxel/chrome-aws-lambda を利用すると、AWS Lambda から簡単に Puppeteer 経由でヘッドレス Chrome を操作できます。

最近 GA になった Amazon CloudWatch Synthetics も AWS Lambda 関数内で類似の処理をしています。

自動テスト、スクレイピング、定形作業などで、AWS Lambda とヘッドレス Chrome を組み合わせて作業を効率化しましょう!

参考