Cloud Functions for Firebase (2nd gen)でPuppeteerを動かす 2024冬

この記事はGoodpatch Advent Calendar 2024の9日目の記事になります。

エンジニアのyossyこと古家です。普段はオンラインホワイトボードサービス Strapを開発しています。

Strap(ストラップ) | 日本製オンラインホワイトボード

Strapのとある機能の開発で、Cloud Functions上でPuppeteerを動作させる必要がありました。 少しWebを検索すると色々載っていたのでできそうじゃんと軽い気持ちでやってみたところ、それなりにハマったのでここに最新記録を残したいと思います。

免責として、現時点ではまだ本番運用は行なっていないソリューションです。 記載内容に未知の問題が存在する可能性があることをご了承ください 🙏

最初にネタバレをしておくと、この記事はStack Overflowの解答をベースにしていますが、より分かりやすく解説できればと思います。

linux - How to use Puppeteer in 2nd gen Cloud Functions? - Stack Overflow

Step0. 素直にやってみる

StrapではNode.jsランタイムでCloud Functionsを利用しています。 まずはPuppeteerをインストールします。

yarn add puppeteer

何か関数を実装します(雑ですみません・・)

import puppeteer from 'puppeteer';

export const hogehoge = async () => {
  const browser = await puppeteer.launch()
};

デプロイして実行すると以下のようなエラーが発生することがWebコンソールから確認できます。悲報。

Error: Could not find Chrome (ver. 131.0.6778.85). This can occur if either
 1. you did not perform an installation before running the script (e.g. `npx puppeteer browsers install chrome`) or
 2. your cache path is incorrectly configured (which is: /workspace/.cache/puppeteer).
For (2), check out our guide on configuring puppeteer at https://pptr.dev/guides/configuration.

Step1. PuppeteerがインストールするChromiumのパスを変更する

Puppeteerはyarn install実行時に$HOME/.cache/puppeteerにChromiumをインストールしますが、このデフォルトのインストールパスではFunctions環境ではインストールできません。

公式のトラブルシュートを参考に.puppeteerrc.cjsファイルを作成し、functionsディレクトリ直下のキャッシュディレクトリ(今回は.cache/puppeteer) にChromiumが保存されるようにします。

Troubleshooting | Puppeteer

// .puppeteerrc.cjs

const {join} = require('path');

/**
 * @type {import("puppeteer").Configuration}
 */
module.exports = {
  cacheDirectory: join(__dirname, '.cache', 'puppeteer'),
};

この状態で yarn install を実行すると、指定したディレクトリにChromiumがインストールされます。

注意点として、公式ドキュメントではnode_modules配下のディレクトリを指定していますが、その方法ではPuppeteerが参照できないため、ディレクトリ直下を指定する必要がありました。*1

Step2. Functionsのデプロイ対象からキャッシュディレクトリを除外する

functionsディレクトリ直下に保存するようにしたため、このままでは.cacheディレクトリもデプロイ対象となります。 Chromiumを含めたままでは、Cloud Functionsの容量制限100MBに抵触してデプロイができません。

Quotas and limits  |  Cloud Functions for Firebase

そのため、firebase.jsonのfunctions.ignoreにキャッシュディレクトリを追加します。

なお、Firebase CLI(firebase-tools)の2024.12.6現在の実装では、node_modulesなどはデフォルトで除外されているため、それらの設定は変更せずに追加します。

// firebase.json

{
  "functions": {
    "ignore": ["node_modules", ".git", ".cache"],
    ....
}

Step3. Functionのビルド時にChromiumがインストールされるようにする

Puppeteerのインストールパスを変更すると、Chromiumは自動インストールされません。*2 Puppeteerはモジュール内にChromiumのインストールスクリプトを持っているため、これを手動で実行します。

Cloud FunctionsはCloud Run上に構築されており、コンテナイメージのパッケージ化プロセス中に任意の処理を実行できる仕組みが用意されているため、これを利用します。

Node.js アプリケーションの構築  |  Buildpacks  |  Google Cloud

ドキュメントでは、環境変数GOOGLE_NODE_RUN_SCRIPTSを使用する方法と、package.jsonのscriptsにgcp-buildを設定する方法の2つが提供されています。

gcloud CLIを利用する場合は両方の方法が使えますが、今回はFirebase CLIを使用する必要があり、その場合は環境変数による方法を利用できないため、gcp-buildを利用します。*3

// functions/package.json

{
  "name": "functions",
  "scripts": {
     "gcp-build": "node node_modules/puppeteer/install.mjs"
  }
}

この状態で関数をデプロイすると、パッケージ化の過程で指定したキャッシュディレクトリにChromiumが自動的にインストールされます。

Step.4 任意のFunctionのデプロイでだけ実行したい

gcp-buildでインストールを実行すると、Puppeteerを利用しない関数の場合もChromiumのダウンロードが都度実行されるため、デプロイ時間の増大が懸念されます。

この問題の解消のために、ビルド環境変数でビルド時にターゲットとなっている関数名を取得できるのでこれを利用して処理を分岐させます。*4

Configure Cloud Run and Cloud Run functions services  |  Buildpacks  |  Google Cloud

// gcp-build.sh

#!/bin/bash

if [ "$GOOGLE_FUNCTION_TARGET" == "functionname" ]; then
  node node_modules/puppeteer/install.mjs
fi
// functions/package.json

{
  "name": "functions",
  "scripts": {
     "gcp-build": "/bin/bash gcp-build.sh"
  }
}

これで任意の関数のデプロイでだけ追加の処理を追加することができました。

その他

Puppeteerを実行するにはそれなりにメモリが必要です。安定稼働には最低でも1GiBの設定は必要になるはずです。 また実行時のメモリ消費を抑えるため、用途が許容するなら画像やフォントの読み込みをスキップするなどの工夫が必要かもしれません。

お約束: エンジニア採用中です!

2024.12.9現在、Strapチームではバックエンドエンジニアの採用を行なっています。 まずはカジュアル面談からお気軽に!お待ちしております!

hrmos.co

その他、Goodpatchではデザイン好きなエンジニアの仲間を募集しています。 少しでもご興味を持たれた方は、ぜひ一度カジュアルにお話ししましょう!

*1:PuppeteerにはChromiumのexecutablePathを設定するオプションも存在しますが、こちらでインストールパスを指定しても正常に参照できませんでした。

*2:https://pptr.dev/guides/configuration#changing-download-options

*3:Firebase CLIには、現在のところビルド環境に環境変数を設定するオプションがありません。実装を確認したところ、GOOGLE_NODE_RUN_SCRIPTS は常に空文字が設定される実装になっていました。https://github.com/firebase/firebase-tools/blob/2690983187b4bf2022ea0ba32882884d1fda77ac/src/gcp/cloudfunctionsv2.ts#L330

*4:Cloud Functionsにはmonorepo内の複数のコードベースを単一環境にデプロイする仕組みも提供されています。今回は対応を見送りましたが、プロジェクトが対応可能であればこちらのソリューションが最もスマートかもしれません。https://firebase.google.com/docs/functions/organize-functions?gen=2nd