💸

Firebase で無料枠を超えたら Firestore を自動的に無効にする

2022/11/30に公開

個人開発ではあまり課金したくないですよね
ただし、Firebase の場合は、Functions を使えば Blaze プラン(従量課金)への移行が必須で、Firestore を使えば割とすぐに無料枠を超過します

もし、アプリケーションを公開してると F5/Command+R 連打される危険性もあります
寝てる時にされたら終了です 😇

実際私は以前、無料枠を超過したことがあって、26 円払いました

今は円安なので、26 円じゃあ済まされないですね

ちなみにこの時はローカルで開発してて、Firestore 叩きまくってたら読み取り 5 万件/日を超えてました
料金プランは以下を参考
https://firebase.google.com/pricing?hl=ja

本題ですが、今回作った「Firestore を無効にするプログラム」は以下の流れで動きます

  1. GCP の予算アラートが発動
  2. Pub/Sub トピックに接続
    • 予算アラート内の設定で指定可能
  3. Pub/Sub トリガーから実行される Cloud Functions 内で Firestore を無効化する

以下注意点です

  • この方法では課金を 0 円にすることはできません
    • 予算アラート発動から Firestore を無効化するまで、使われた分は課金されます
  • 無効化されると Firestore へのリクエストはすべてエラーになるので、アプリケーションを公開している場合は注意してください

また、Firestore の無効化に関して実際に行う作業は、Google App Engine を無効化することです
GAE アプリを無効化することで、それに紐づく Firestore へのアクセスが無効になるという流れです

手動で GAE アプリを無効化する場合は以下から行えます
https://console.cloud.google.com/appengine/settings

こちらに書かれている通り、無効化しても Firestore へのリクエストは停止されますが、データはなくなりません(保存済みのデータの課金も無料枠を超えていれば発生します)

予算アラートの作成

GCP コンソールのお支払い -> 予算とアラート -> 予算を作成から作成できます
とりあえず私は 1 円でも使われたらアラート飛ばす設定にしています

最後に通知の種類を選べるので、ここで Pub/Sub トピックと紐付けてください
もし、トピックがない場合でもこの画面から作成できます

Cloud Functions for Firebase の作成

必要なパッケージをインストールします

npm i firebase-functions googleapis

環境変数をセットします

firebase functions:config:set pubsub.topic="[先程作成したPub/Subトピック名]"

以下が関数の実装(TypeScript)です
実装例は Python ですが以下にあります
https://cloud.google.com/appengine/docs/managing-costs#disable_your_app_programmatically

index.ts
import * as functions from 'firebase-functions'
import { google } from 'googleapis'

export const disableApp = functions.pubsub
  .topic((functions.config() as { pubsub: { topic: string } }).pubsub.topic)
  .onPublish(async m => {
    const data = JSON.parse(Buffer.from(m.data, 'base64').toString()) as { costAmount: number; budgetAmount: number }
    if (data.costAmount <= data.budgetAmount) {
      console.info(`No action necessary. (Current cost: ${data.costAmount})`)
      return null
    }

    const auth = new google.auth.GoogleAuth({
      scopes: ['https://www.googleapis.com/auth/cloud-platform']
    })
    const authClient = await auth.getClient()
    const projectId = await auth.getProjectId()

    await google.appengine('v1').apps.patch({
      auth: authClient,
      appsId: projectId,
      updateMask: 'serving_status',
      requestBody: { servingStatus: 'USER_DISABLED' }
    })
    console.info(`App ${projectId} disabled`)

    return null
  })

デプロイします

firebase deploy --only functions

動作確認

gcloud を使って、作成した Pub/Sub トピックに publish します
costAmount と budgetAmount 以外はたぶん適当な値でオッケーです

gcloud pubsub topics publish [先程作成したPub/Subトピック名] --message '{
    "budgetDisplayName": "aaaaa",
    "alertThresholdExceeded": 1.0,
    "costAmount": 100.01,
    "costIntervalStart": "2019-01-01T00:00:00Z",
    "budgetAmount": 100.00,
    "budgetAmountType": "SPECIFIED_AMOUNT",
    "currencyCode": "USD"
}'

再度、GAE の設定画面を確認し、以下の画像のように無効化されていれば成功です

再開する場合は、「アプリケーションを有効にする」ボタンを押せばオッケーです
私の環境の場合は有効化後に数秒で使えるようになっていました

また、無効化後に Firebase JS SDK 経由で Firestore を参照すると以下のエラーになります

Could not reach Cloud Firestore backend. Connection failed 1 times. Most recent error: FirebaseError: [code=permission-denied]: The project was disabled or deleted.
This typically indicates that your device does not have a healthy Internet connection at the moment. The client will operate in offline mode until it is able to successfully connect to the backend.

参考

https://cloud.google.com/appengine/docs/managing-costs

Discussion