Firestore Local Emulator のデータを擬似的に永続化する
Firestore Local Emulator にimport/export機能が追加されたらしいので、それを利用して擬似的に永続化しました。
Dockerを落とすたびにリセットされるデータ
最近、業務でFirestoreを使用した開発をしています。ローカルではエミュレータを起動し、それに接続しています。このエミュレータには個人的に不便なことがあり、データをメモリ上に保存しています。ローカルの開発にはDockerを使用しており、エミュレータもDocker上で動かしています。そのため、Dockerを落とすたびにデータが全消去されてしまいます。
何とかしたいなーと思いつつもいい方法が見つからず放置していましたが、少し前にこちらの記事を見つけ、最近import/export機能が実装されたことを知りました。

この機能を使えばデータを擬似的に永続化できるのでは?と思い、試してみました。
gcloudコマンドでの起動に対応していない?
上の記事ではfirebaseコマンドでエミュレータを起動していますが、自分の業務ではGolangで書かれたサーバーサイドから接続する関係でgcloudコマンドで起動していました。ところが、ドキュメントを見るとimportやexportの記述が見つかりませんでした。

gcloudコマンドでは対応していないのかなと思いつつ、以下のfirebaseコマンドのソースを見てみると、
- export:エミュレータに対してPOSTリクエストする
- import:exportしたファイルを起動時にパラメータで渡す
という処理になっていました。
これならシェルスクリプトで無理やり実行できるのでは?と思いやってみました。
できたもの
以下の構成で進めます。
.
├── docker-compose.yml
├── firestore
│ ├── Dockerfile
│ ├── firestore_export <- exportファイルの保存先
│ └── start.sh <- Docker起動時の実行スクリプト
└── go
└── src
└── main.go <- アプリケーション| version: "3.7" | |
| services: | |
| firestore: | |
| build: | |
| context: ./firestore | |
| environment: | |
| - PROJECT_ID=test-project-id | |
| volumes: | |
| - ./firestore:/firestore | |
| command: /bin/bash /firestore/start.sh | |
| go: | |
| image: golang:1.14 | |
| tty: true | |
| environment: | |
| - PROJECT_ID=test-project-id | |
| - FIRESTORE_EMULATOR_HOST=firestore:8812 | |
| volumes: | |
| - ./go/src:/go/src | |
| working_dir: /go/src |
| FROM google/cloud-sdk:alpine | |
| RUN apk add --update --no-cache openjdk8 \ | |
| && gcloud components update \ | |
| && gcloud components install cloud-firestore-emulator beta --quiet |
| #!/bin/bash | |
| export_path="/firestore/firestore_export/firestore_export.overall_export_metadata" | |
| start_command="/google-cloud-sdk/platform/cloud-firestore-emulator/cloud_firestore_emulator start --host=0.0.0.0 --port=8812" | |
| if [ -e $export_path ]; then | |
| $start_command --seed_from_export=$export_path & | |
| else | |
| $start_command & | |
| fi | |
| while true | |
| do | |
| curl -XPOST -H "Content-Type: application/json" -d "{\"database\": \"projects/$PROJECT_ID/databases/(default)\", \"export_directory\": \"/firestore\", \"export_name\": \"firestore_export\"}" http://0.0.0.0:8812/emulator/v1/projects/$PROJECT_ID:export | |
| sleep 5 | |
| done |
Docker起動時に実行するstart.sh では以下のことをしています。
- exportファイルの存在確認をし、あれば引数にセットする
- gcloudコマンドではなく、gcloudコマンドで使われている(と思われる)実行ファイルを直接実行する
- 5秒毎にデータをexportし、マウントしたディレクトリに保存
これによってexportしたデータは5秒毎にローカルに保存され、Dockerを落としてもデータをある程度維持できます。5秒毎にcurlを叩くのでローカル環境への負荷が少し気になりましたが、1日つけっぱなしでも特に重くなったりはしなかったので多分大丈夫だと思います。
試しに以下のファイルを用意し、
main.go実行 → Firestoreコンテナの再起動 → main.go実行
としてみました。
| package main | |
| import ( | |
| "context" | |
| "fmt" | |
| "log" | |
| "os" | |
| "strconv" | |
| "cloud.google.com/go/firestore" | |
| ) | |
| func main() { | |
| ctx := context.Background() | |
| client, err := firestore.NewClient(ctx, os.Getenv("PROJECT_ID")) | |
| if err != nil { | |
| log.Fatal(err) | |
| } | |
| create(ctx, client) | |
| print(ctx, client) | |
| } | |
| func create(ctx context.Context, client *firestore.Client) { | |
| ref, _, err := client.Collection("companies").Add(ctx, map[string]interface{}{ | |
| "name": "test_company", | |
| }) | |
| if err != nil { | |
| log.Fatal(err) | |
| } | |
| for i := 0; i < 5; i++ { | |
| _, _, err := client.Collection("users").Add(ctx, map[string]interface{}{ | |
| "name": "test_" + strconv.Itoa(i), | |
| "company_id": ref.ID, | |
| }) | |
| if err != nil { | |
| log.Fatal(err) | |
| } | |
| } | |
| } | |
| func print(ctx context.Context, client *firestore.Client) { | |
| companies, err := client.Collection("companies").Documents(ctx).GetAll() | |
| if err != nil { | |
| log.Fatal(err) | |
| } | |
| for _, company := range companies { | |
| fmt.Println(company.Data()) | |
| } | |
| users, err := client.Collection("users").Documents(ctx).GetAll() | |
| if err != nil { | |
| log.Fatal(err) | |
| } | |
| for _, user := range users { | |
| fmt.Println(user.Data()) | |
| } | |
| } |
root@bde17f5d8780:/go/src# go run main.go
map[name:test_company]
map[company_id:EEG48ivVXqwjOgHDY8lJ7IiO9bRekcClGJ6Y0ljh9co name:test_4]
map[company_id:EEG48ivVXqwjOgHDY8lJ7IiO9bRekcClGJ6Y0ljh9co name:test_1]
map[company_id:EEG48ivVXqwjOgHDY8lJ7IiO9bRekcClGJ6Y0ljh9co name:test_0]
map[company_id:EEG48ivVXqwjOgHDY8lJ7IiO9bRekcClGJ6Y0ljh9co name:test_3]
map[company_id:EEG48ivVXqwjOgHDY8lJ7IiO9bRekcClGJ6Y0ljh9co name:test_2]
--- Firestore再起動 ---
root@bde17f5d8780:/go/src# go run main.go
map[name:test_company]
map[name:test_company]
map[company_id:VZu8kWTjTd_36LaSJKxqqCTulZl9SXsahUAKvzvqIjw name:test_2]
map[company_id:VZu8kWTjTd_36LaSJKxqqCTulZl9SXsahUAKvzvqIjw name:test_0]
map[company_id:EEG48ivVXqwjOgHDY8lJ7IiO9bRekcClGJ6Y0ljh9co name:test_4]
map[company_id:EEG48ivVXqwjOgHDY8lJ7IiO9bRekcClGJ6Y0ljh9co name:test_1]
map[company_id:VZu8kWTjTd_36LaSJKxqqCTulZl9SXsahUAKvzvqIjw name:test_4]
map[company_id:VZu8kWTjTd_36LaSJKxqqCTulZl9SXsahUAKvzvqIjw name:test_1]
map[company_id:VZu8kWTjTd_36LaSJKxqqCTulZl9SXsahUAKvzvqIjw name:test_3]
map[company_id:EEG48ivVXqwjOgHDY8lJ7IiO9bRekcClGJ6Y0ljh9co name:test_0]
map[company_id:EEG48ivVXqwjOgHDY8lJ7IiO9bRekcClGJ6Y0ljh9co name:test_3]
map[company_id:EEG48ivVXqwjOgHDY8lJ7IiO9bRekcClGJ6Y0ljh9co name:test_2]2回目に実行した時に1回目で保存したレコードも取得できているので、無事に永続化できました。
データを消したいときは、コンテナを止めてfirestore/firestore_export フォルダを削除してから起動し直せばOKです。
注意点
この方法でimportしたデータは、全てのProjectIDに対して初期データとして扱われるようです。
上の例ではtest-project-id というProjectIDでexportしていますが、importするとtest-project-id-1 でも、test-project-id-2 でも、どんなProjectIDを指定しても同じ初期データが入った状態になります。
ランダムなProjectIDを使ってUTを書いている場合は気をつけましょう。
自分は2時間ハマりました。

