FirebaseのSparkプラン(無料枠)でRealtimeDBの自動バックアップを構築する
個人開発のアプリでRealtimeDBのバックアップを無料でやりたい
Firebase RealtimeDBのBlazeプラン(有料)には自動バックアップ機能があります。 でもBlazeプランは青天井なので個人開発ではなるべく触りたくありません。お金持ちならいいけど。
RealtimeDBのドキュメントには、以下のような記述があります。
自動バックアップ | Firebase Realtime Database | Firebase
また、実際にfirebaseのコンソールにも同様の記述があります。
というわけで、無料枠からはみ出ない範囲でRealtimeDBの自動バックアップを手動で構築してみることにします。
で、一番最初に思いついたのは以下のようなワークフローです。
Firebase cloud functionsにスケジューリングされたfunctionを実装
↓
このfunctionでRealtimeDBを丸ごと引っ張ってくる
↓
Firebase cloud storageにjsonなりzipなりで格納
Firebase cloud functionsは単体でスケジューラを作成できない
調べてみたところcloud functions単体にはcronに相当する機能がないようです。 (AWS Lambdaにはcronがあるのに...)
Rate または Cron を使用したスケジュール式 - AWS Lambda
Google App Engineを使おう
今回はGoogle Cloud PlatformのGoogle App Engineにスケジューラを作成してfunctionsを定期的に実行することにします。
ちなみにApp Engineにも無料枠/有料枠があり、大量のバックアップを実行したい場合は素直にFirebaseのBlazeプランでやった方がいいと思います。 (この記事ではあくまで個人開発の小規模データベースのバックアップを目的としています)
無料枠の上限ですが、1日あたり28インスタンス時間と定められています。
インスタンス時間とはなんぞや
単純にインスタンスが稼働している時間のことのようです。 1日あたり28インスタンス時間ということは、複数のインスタンスを稼働させない限りは無料枠から足は出ることは無いと思います。 ちなみに皆さんご存知の通りGCPは負荷に応じてよしなにスケーリングしてくれるので、App Engine上にブログやサービスを構築しており、それらが一時的にバズったりするとインスタンスがモリモリ生成されて一瞬で無料枠を使い果たすので注意が必要です。
今回のケースはcronスケジューラなのでインスタンス数が増えるものでもないので特に問題はない(たぶん)です。 今のところこのインスタンス時間をはみ出た事はありませんが、今後この無料枠が変更されることもあり得るので、実際に利用する場合はApp Engineの利用規約や料金表を確認するのが安全です。
また、スケーリングを禁止(最大インスタンス数を1で固定)する設定もできるようです。
参考:
GAE でうっかり発生していた課金を無くして無料運用に戻した話 – OTCHY.NET
Google App Engineでスケジュールジョブを作成する
Precondition
ジョブを作成する前に
Firebase toolsとGoogle Cloud SDKを使用するので、あらかじめ構築しておく必要があります。
それぞれ、手順は以下の通りです。
Firebase CLI リファレンス | Firebase
Quickstart for macOS | Cloud SDK Documentation | Google Cloud
また、あらかじめ関連付けが完了しているGoogle Cloud PlatformプロジェクトとFirebaseプロジェクトを用意しておいてください。
はじめに: 最初の関数の記述とデプロイ | Firebase
App Engineにcronジョブをデプロイ
githubにApp Engineでcloud functionsをスケジューリングするテンプレートプロジェクトがあるのでこちらを使用します。
$ git clone https://github.com/firebase/functions-cron $ cd functions-cron
このプロジェクトは以下のようなディレクトリ構成となっています。
. ├── LICENSE ├── appengine │ ├── app.yaml │ ├── cron.yaml │ ├── lib │ ├── main.py │ ├── main_test.py │ ├── pubsub_utils.py │ ├── requirements.txt │ └── setup.cfg ├── firebase-debug.log ├── firebase.json ├── functions │ ├── index.js │ ├── package-lock.json │ └── package.json ├── package-lock.json └── readme.md
フレームワークにApp Engineで利用されているwebapp2を使ってる...とか色々あるんですが、 ひとまずApp Engine用のディレクトリとcloud functions用の2つのディレクトリがあることだけ分かれば大丈夫です。
今回触る必要があるファイルは、
. ├── appengine │ ├── app.yaml │ ├── cron.yaml │ ├── requirements.txt ├── functions │ ├── index.js
この辺だけです。
次にApp Engineにジョブをデプロイします。
$ gcloud config set <yourprojectname> $ cd appengine/ $ pip install -t lib -r requirements.txt $ gcloud app create $ gcloud app deploy app.yaml \cron.yaml
ちなみにfunctions-cron/appengine/cron.yaml
には以下のような記述があります。
cron: - description: Push a "tick" onto pubsub every hour url: /publish/hourly_tick schedule: every 1 hours - description: Push a "tick" onto pubsub every day url: /publish/daily_tick schedule: every 24 hours - description: Push a "tick" onto pubsub every week url: /publish/weekly_tick schedule: every saturday 00:00
どうやらここでスケジューリングの設定ができる模様。
schedule:
の部分の記述を変えれば"金曜日の夜だけ実行"みたいなのもできます。
詳しい記述方法は公式のドキュメントに書かれているので読んでおくことをオススメします。 https://cloud.google.com/appengine/docs/standard/python/config/cronref?hl=ja#schedule_format
gcloud
コマンドでApp Engineにデプロイします。
$ gcloud app deploy app.yaml \cron.yaml
実行すると
Services to deploy: descriptor: [/Users/username/sandbox/google/functions-cron/appengine/app.yaml] source: [/Users/username/sandbox/google/functions-cron/appengine] target project: [yourprojectname] target service: [default] target version: [version number] target url: [https://yourprojectname.appspot.com] Configurations to update: descriptor: [/Users/username/sandbox/google/functions-cron/appengine/cron.yaml] type: [cron jobs] target project: [yourprojectname] Do you want to continue (Y/n)? #Yでデプロイ開始
という表示が出るので、Yで続行。
Beginning deployment of service [default]... Some files were skipped. Pass `--verbosity=info` to see which ones. You may also view the gcloud log file, found at [/Users/username/.config/gcloud/logs/YYYY.MM.DD/********.log]. ╔════════════════════════════════════════════════════════════╗ ╠═ Uploading 0 files to Google Cloud Storage ═╣ ╚════════════════════════════════════════════════════════════╝ File upload done. Updating service [default]...done. Setting traffic split for service [default]...done. Deployed service [default] to [https://yourprojectname.appspot.com] Updating config [cron]...done. Cron jobs have been updated. Visit the Cloud Platform Console Task Queues page to view your queues and cron jobs. https://console.cloud.google.com/appengine/taskqueues/cron?project=yourprojectname You can stream logs from the command line by running: $ gcloud app logs tail -s default To view your application in the web browser run: $ gcloud app browse
以下のページに進むと、デプロイされたジョブを確認することができます。
- /publish/daily-tick
- /publish/hourly-tick
- /publish/weekly-tick
の3つのジョブが確認できますが、まだこれらのジョブに紐づけられたfunctionが存在しないため、 "今すぐ実行"をクリックしてもエラーが発生してしまいます。
cloud functionsのデプロイ
では次にcloud functionsのfunctionをデプロイします。
cloud functionsの関数を記述するためにはfunctions/index.js
を編集します。
初期状態では以下のような文字列をログに出力するだけの関数が用意されています。
var functions = require('firebase-functions'); exports.hourly_job = functions.pubsub.topic('hourly-tick').onPublish((event) => { console.log("This job is ran every hour!") });
ひとまずこの状態でデプロイしてみることにします。
$ firebase deploy --only functions --project <yourprojectname>
=== Deploying to 'yourprojectname'... i deploying functions i functions: ensuring necessary APIs are enabled... ✔ functions: all necessary APIs are enabled i functions: preparing functions directory for uploading... i functions: packaged functions (XX.XX KB) for uploading ✔ functions: functions folder uploaded successfully i functions: updating function hourly_job... ✔ functions[hourly_job]: Successful update operation. ✔ Deploy complete! Project Console: https://console.firebase.google.com/project/yourprojectname/overview
デプロイが完了すると、Firebase consoleからデプロイしたプロジェクトを選択->Functions->ダッシュボードにてデプロイしたfunction一覧が表示されます。
新しいfunctionの実装
必要なモジュールをインストールします。
$ npm i --save moment-timezone @google-could/storage firebase-functions firebase-admin #firebase CLIを初めて使う場合は以下のコマンドが必要 $ npm i -g firebase-tools $ firebase login #Googleアカウントのログイン要求があるのでログイン情報を入力
Firebase toolsの初期化等は以下のページを参照するのが良いと思います。
Firebase CLI リファレンス | Firebase
const moment = require('moment-timezone'); //Timezoneを日本に設定 moment.tz.setDefault("Asia/Tokyo"); const gcs = require('@google-cloud/storage'); const functions = require('firebase-functions'); const admin = require('firebase-admin'); admin.initializeApp(functions.config().firebase) const database = admin.database(); const storage = admin.storage(); //hourly_tickイベントをsubscribe exports.hourly_job = functions.pubsub.topic('hourly_tick').onPublish((event) => { const ref = database.ref(); const bucket = storage.bucket(); const dirname = 'Backup/' + moment().format('YYYY_MM_DD'); const filename = moment().format('YYYY_MM_DD_HH:mm:ss') + '.json'; const file = bucket.file(dirname + '/' + filename); const stream = file.createWriteStream({ gzip: true }); ref.once('value').then((snapshot) => { stream.on('error', (err) => { console.error(err); }); stream.on('finish', (err) => { console.log('successfully finished.'); }); //JSON.stringifyでjson stringに変換してからstreamに渡す stream.end(JSON.stringify(snapshot.val())); }).catch((err) => { console.error(err); }); //何らかのpromiseもしくはvalueをreturnしない場合、functionsのlogでエラーが出てしまう return 0; });
細かい点ですが、Firebase clound functionsはサーバのリージョンがデフォルトで米国なのでmoment.jsでは米国の現地時間が出力されてしまいます。
そのため、moment-timezoneでタイムゾーンをmoment.tz.setDefault("Asia/Tokyo");
としてセットしています。
リージョンを日本にしてプロジェクト生成していた場合は普通のmoment.jsで問題ないです。
補足: タイムゾーンの設定はプロジェクトの設定で変えられるかも?と思ってましたがどうやら最初に決めたリージョンからは動かせない模様。
ちなみになんでこんなこと書いてるかというと、自分のApp Engineのプロジェクトのリージョンをうっかりus-centralで作ってしまったためです。悲しいなあ。
それはともかく、変更が完了したので再度functionsをデプロイします。
$ firebase deploy --only functions --project <YourProjectName>
App Engine->タスクキュー->cronジョブのタブに行き、/publish/hourly_tickジョブを実行します。
Firebase cloud storageに、バックアップファイルが格納されていることを確認できれば、バックアップのスケジューリングが正常に完了しています。
バックアップが正常に作成されていない場合は、以下のコマンドでfunctionsのログを確認できます。
$ firebase functions:log --project <yourprojectname>
あとは、App Engine上のcronジョブによって1時間ごとに自動でバックアップが作成されます。
おわりに
いろんなPassやらBaaSやらを組み合わせれば小さなWebサービスならほぼ無料で運用できるかも。