FirebaseのCloud FunctionsでPuppeteerを使おうとしたら無課金お断りだった件orz

# Goal
FirebaseのCloud FunctionsでPuppeteerを使ってWebの情報を収集する(￣▽￣)


# いきなり結論
Firebaseの無料プラン（Spark プラン）では**外部へのリクエストが発行できない**ため不可(´Д⊂ヽ

アウトバウンド ネットワーキングが**Google専用**になっている。
https://firebase.google.com/pricing/?hl=ja

Firebaseでの実行にこだわるなら有料プランへの移行は必須。
Firebaseでの実行にこだわる必要がない場合、各社のサーバーレスの無料プランが充実しているのでそちらのほうが有益。

ここから先はFirebase初心者の冒険の記録なので、読み物として興味のある方のみが対象ですm(_ _)m


# Firebase初心者の冒険録

## Puppeteerってそもそも何？

Chromeをコードで制御することのできるライブラリ。
用途の一つとして、WebAPIでデータが収集できない、いわゆる非構造化データを収集に利用できる。

## Puppeteerを動作させるコンピューティングの選定

選定するのに重視したポイント。
1. サーバーレスで極力管理しないこと。
2. 最悪Dockerイメージでの実行とし、その際Dockerイメージのメンテはないこと。

1のみで行く場合、PuppeteerはChromeを操作するため、サーバーにChromeがインストールされている必要があります。
おそらく各社のサーバーレスのサーバーには入っているとは思いますが、そこへアクセスできるかはまた別の話。
1がダメとなると、ChromeをインストールしたDockerイメージを作っておいて、それを実行させる方法が一つ。
AzureにはContainer Instancesという、Dockerイメージを渡してコマンドを実行させる製品がある。

ググってみると、GoogleのCloud FunctionsにChromeサポートとかｷﾀ――(ﾟ∀ﾟ)――!!
https://cloud.google.com/blog/products/gcp/introducing-headless-chrome-support-in-cloud-functions-and-app-engine
Cloud Functionsがサポートしているということは、恐らくFirebaseのCloud Functionsでも利用できる可能性が高い(*´▽｀*)
FirebaseのCloud Functions採用決定！！！


## 最初の関数を作成してデプロイする
公式のスタートガイドに則って作業する。
https://firebase.google.com/docs/functions/get-started
ドキュメントにはデフォルトはNode v6ですという記述を華麗にスルーして、後でPuppeteerがv8必須のため一汗書くのは内緒(;'∀')
更によく見ると結論書いてた(´-∀-｀；)
>Spark プランの Firebase プロジェクトは、Google API への発信リクエストのみを行うことができます。サードパーティの API へのリクエストはエラーで失敗します。プロジェクトのアップグレードの詳細については、料金をご覧ください。

```text:terminal
npm install -g firebase-tools
```

```text:terminal
firebase login
firebase init functions
```

進んでいくとコンプリート表示。

```text:terminal
+  Firebase initialization complete!
```

index.jsを見るとHello,Worldのサンプルコードが既にあるので、ここからはドキュメント無視して進める(;^_^A

```text:terminal
firebase deploy
```

Deploy complete!のメッセージとURLが表示されて、テンプレのFunctionsが実行されることを確認。


Puppeteerをインストールする。

```text:terminal
npm i puppeteer --save
```

すると140MB程度のChromiumがDLされる(;・∀・)
既にサーバーにはChromeがセットアップ済みのはずなのでChromiumはいらない。
このDLどうにかスキップできないかと思って公式を見たら丁寧に解説があった。
https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#puppeteer-vs-puppeteer-core
ということでcoreのほうに移行。

```text:terminal
npm uninstall puppeteer --save
npm i puppeteer-core --save
```

deployして実行するとサーバー側でエラー。

```text:Functionslog
Error: Chromium revision is not downloaded.
```

よくわからずにpuppeteerのほうに戻す(;^_^A
何度実行してもエラーになる。
今見るとググってればすぐなのにとすごく思う(;^ω^)

```text:Functionslog
Running as root without --no-sandbox is not supported.
```

エラー内容見ないくせに切り分けのため、ローカルでは実行可能かの判断のためにローカルで実行することに。

```text:terminal
firebase serve

i  functions: Preparing to emulate functions.
Warning: You're using Node.js v8.12.0 but Google Cloud Functions only supports v6.11.5.
+  functions: helloWorld: http://localhost:5000/qrcodemk/us-central1/helloWorld
info: User function triggered, starting execution
info: puppeteer succeded.
info: Execution took 5660 ms, user function completed successfully
Shutting down...
```

上手くいっているじゃないか・・・
ということはサーバー側の環境問題。
よく見るとonly supports v6.11.5とあるので、サーバーのNodeをV8対応すればいいのかと判明。
https://firebase.google.com/docs/functions/manage-functions?hl=ja#set_nodejs_version
もう公式がNodeV8をカレントにしているんだから、移行してほしいと思いつつpackage.jsonの最後尾にenginesパラメータを追加。

```package.json
{
  "name": "functions",
  "description": "Cloud Functions for Firebase",
  "scripts": {
    "serve": "firebase serve --only functions",
    "shell": "firebase functions:shell",
    "start": "npm run shell",
    "deploy": "firebase deploy --only functions",
    "logs": "firebase functions:log"
  },
  "dependencies": {
    "firebase-admin": "~6.0.0",
    "firebase-functions": "^2.1.0",
    "puppeteer": "^1.11.0"
  },
  "private": true,
  "engines": {
    "node": "8"
  }
}
```

これでも状況が変わらないため、エラーをようやく見てやるべきことがわかる・・・Google翻訳さん本当にお世話になりますw

```text:Functionslog
Running as root without --no-sandbox is not supported.
TROUBLESHOOTING: https://github.com/GoogleChrome/puppeteer/blob/master/docs/troubleshooting.md
```

Google翻訳先生

```
--no-sandboxなしでrootとして実行することはサポートされていません。
```

丁寧にもリンクの案内まであり、--no-sandboxは推奨しないとあるが、まずは動かしたいのでパラメータを追加してソース完成。
functionsのテンプレにPoppeteerのコードを埋め込んでasync/await入れた程度。

```javascript:index.js
const functions = require('firebase-functions');
const puppeteer = require('puppeteer');

// Create and Deploy Your First Cloud Functions
// https://firebase.google.com/docs/functions/write-firebase-functions

exports.helloWorld = functions.https.onRequest(async (request, response) => {

    await (async () => {
        const browser = await puppeteer.launch({
            args: ['--no-sandbox']
        });
        const page = await browser.newPage();
        await page.goto('https://www.google.com/');
        // await page.screenshot({path: 'example.png'});

        console.log("puppeteer succeded.");

        await browser.close();
    })();

    response.send("Hello new world from Firebase!");
});
```

これでもまだエラーを吐くのか・・・（#^ω^）ﾋﾟｷﾋﾟｷ

```text:Functionslog
Error: net::ERR_NAME_RESOLUTION_FAILED at https://www.google.com/
```

名前が解決できないとかありえないだろ・・・
そして、ようやくエラーの前の警告文にたどり着く。

```text:Functionslog
Billing account not configured. 
External network is not accessible and quotas are severely limited. 
Configure billing account to remove these restrictions
```

Google翻訳先生曰く。

```

請求先アカウントが設定されていません。
外部ネットワークにアクセスできず、クォータが大幅に制限されています。
これらの制限を削除するように請求先アカウントを設定します
```

Functionsの制限ページにはSparkの文言はなかったが、料金プランのほうでの制限にようやくたどり着くorz


# まとめ
FireabaseとGoogle CloudでFunctionsに違いはないと思ってたけど、無料枠の差も含めて以外にも差があった。
各クラウドベンダーにFunctionsあるけど、Puppeteerサポートを謳っているGoogle Cloud Functionsでリトライ予定。
長い長い遠回りだったけど、結果的にはCloud Functionsいろいろいじれてためになったかなと(;^_^A