スプレイピングを定期実行したい機会があったので、Serverlessを使ってLambdaにTypeScript/Puppeteerをデプロイしてみました。
地味に日本語資料が少なかったので残しておきます
LambdaでPuppeteerを実行するためには
google検索だと@serverless-chrome/lambdaが上位に出てくるのですが、1年以上更新がなく私の環境ではうまく動作せず諦めました(Nodeのバージョンが合わなかったなどあるかもしれません)。
対して、Sparticuz/chrome-aws-lambdaが提供するchromium/puppeteerを利用する方法が分かりやすくおすすめです。
exports.handler = async (event: any, context: any, callback: any) => {
const browser = await chromium.puppeteer.launch({
args: chromium.args,
defaultViewport: chromium.defaultViewport,
executablePath: await chromium.executablePath,
headless: chromium.headless,
ignoreHTTPSErrors: true,
});
const page = await browser.newPage();
// ...
この状態でビルドすると、ファイルサイズが50~60MB程度になりLambdaへのアップロード制限も楽々回避できますね
...
Deploying scrapper-lambda to stage dev (ap-northeast-1)
✔ Service deployed to stack scrapper-lambda-dev (178s)
functions:
hello: scrapper-lambda-dev-hello (57 MB)
...
ですが、このファイルサイズでアップロードすると結構時間かかります。僕の環境では3分ほどかかりました。
この容量のうちほとんどをSparticuz/chrome-aws-lambdaが提供するchromium/lambdaが占めています。そこで、Lambdaでのレイヤー機能を使うことで毎回これらをアップロードしなくて済むようにします
こちらのリポジトリ(shelfio/chrome-aws-lambda-layer)では、パブリックに上記のライブラリがレイヤーとして提供されています。
レイヤーのARNは2022/09/08時点で東京リージョンだったら arn:aws:lambda:ap-northeast-1:764866452798:layer:chrome-aws-lambda:31
ですね。
Serverlessの場合はfunctions.layers[]
にふくむだけで簡単に使えるので楽ですね。
functions:
hello:
handler: handler.handle
memorySize: 3008
timeout: 60
events:
- schedule: rate(1 hour)
layers:
- arn:aws:lambda:ap-northeast-1:764866452798:layer:chrome-aws-lambda:31 # here
こうすると、アップロードサイズが7MBほどになりそれに伴いアップロードする時間も1分ほどになりました。
...
✔ Service deployed to stack scrapper-lambda-dev (62s)
functions:
hello: scrapper-lambda-dev-hello (6.9 MB)
...
typescript
serverlessのテンプレートはJavascriptになっていたんですが、どうしてもTypeScriptがよくて書き換えました。compilerOptions
にはここら辺を設定すれば再現できると思います
- “module”: “commonjs”
- “esModuleInterop”: true
- “lib”: [“DOM”]
- Puppeteerで
evaluate()
する際にwindowにアクセスできないため必要です
- Puppeteerで
その他
普通にデプロイして実行しても、レスポンスが帰ってこず必ずタイムアウトしてしまいます。これはPuppeteerがイベントループをブロッキングしていて、Lambdaはデフォルトではイベントループが空っぽになってからレスポンスするためです。(Puppeteerを起動させるとChrome Debugger Protocolが動作するためなのかも?)
なので明示的にこれをoffにする必要があります
exports.handler = async (event: any, context: any, callback: any) => {
context.callbackWaitsForEmptyEventLoop = false
// ...
}