GAミント至上主義

Web Monomaniacal Developer.

Firestoreでコレクションだけ持ってるドキュメントを一覧で取得できなくてハマったらFirestoreの構造を実感できた

Firebase Admin SDKでデータを抜き出そうとしたら取れなくてハマった。
結論としてはコレクションのみを持つドキュメントは一覧を取得することができないのが分かった。

Firebase Consoleだと見れるけどどうやってるかわからないが、よく見ると注意書きがあることに後で気づく。

f:id:uyamazak:20180808111905p:plain

全体のデータ構造は下記のようになっている

/userdata/{uId}/backups/{backupId}

あえて図にすると下記のような感じ。

  • がコレクション、#がドキュメント
-userdata
  #yamada
    -backups
      #backup1(データ入り)
      #backup2
      #backup3
  #suzuki
    -backups
      #backup1
      #backup2
      #backup3
var cRef = db.collection('userdata')
cRef.get()
    .then(snap =>{
      console.log(snap.size)
    })
# 0

これはyamadaとsuzukiが欲しいがとれない

しかしbackupsまで入れば取れる

cRef.doc('yamada')
    .collection('backups')
    .get()
    .then(snap =>{
      console.log(snap.size)
    })
# 3

最初ルートは取れないかと思ったけど関係はなく、いろいろ試した末の違いとしては、ドキュメントにデータがあるかどうか。
userdata/yamadaのドキュメントにすでにあるbackupsのほかにフィールドを追加すれば取得することができた。

Firestoreのドキュメントはコレクションとフィールド(Key, Valueのデータ)の両方を持つことができる、ファイルシステムで言うディレクトリみたいなものなのでややこしい。

Firestoreを大きなJSONと考えるとこの問題は理解できないけど、Firestoreはファイルシステムや、JSON専用のKVSのようなものだと考え、途中のパスは単なるパスであり、jsの配列ような実体はないと考えると納得がいった。

そう体感してから下記ドキュメントを読むとそれはそうだなと思えた。
Cloud Firestore データモデル  |  Firebase

コレクションを「作成」したり「削除」したりする必要はありません。コレクション内の最初のドキュメントを作成すると、コレクションはすでに存在しています。コレクション内のすべてのドキュメントを削除すると、コレクションは存在しなくなります。
警告: ドキュメントを削除しても、そのサブコレクションは削除されません。

サブコレクションが関連付けられているドキュメントを削除しても、そのサブコレクションは削除されません。その後もサブコレクションには、リファレンスによるアクセスが可能です。たとえば、db.collection('coll').doc('doc') によって参照されるドキュメントは存在しなくなったにもかかわらず、db.collection('coll').doc('doc').collection('subcoll').doc('subdoc') によって参照されるドキュメントは存在する場合があります。ドキュメントを削除するときにサブコレクション内のドキュメントも削除する場合は、コレクションを削除するで説明されているように、手動で削除する必要があります。

Vue.jsとFirebaseで本番環境、ステージング環境をなるべく節約して構築する

yagish履歴書は、システム1人、デザイン1人、イラスト1人、計3人の小さなチームで作っていて、開発は非常に身軽なので、本番公開時もローカルの開発環境+Firebase本番環境だけだった。

rirekisho.yagish.jp

でもさすがに大きな変更を本番でやるのは怖いので、手間と費用をなるべく抑えつつ、Firebaseを使ったステージング環境を構築することにした。

開発環境はVue CLI 3で構築。

cli.vuejs.org

環境変数

これは3ファイルで分ける

.env.production
.env.development
.env.staging

ステージング環境について公式ドキュメントに書いてあるので、参考にする。
cli.vuejs.org

.env.staging

NODE_ENV=production
VUE_APP_TITLE=My App (staging)

ただNODE_ENVを上書きしてしまうので、ステージングの時に切り替えたい部分でNODE_ENV === 'staging'を判定に使うことができないため、VUE_APP_MODEという変数を追加して使うことにした。
あんまりきれいじゃない。
.env.staging

VUE_APP_MODE=staging

ビルド時に書き出しディレクトリを変えて、Dockerコンテナは一つで済ませる

開発環境はDockerコンテナで行っているが、これを本番、ステージングなどで分けてしまうと面倒くささが圧倒的に増すのでやりたくない。
開発サーバーの実行、ステージング用ビルド、本番用ビルドもすべて一つのコンテナで行う。

まず、buildコマンドを明示的にbuild-staging、build-productionでそれぞれ--modeをつけて分ける。

package.json

{
  "name": "yagish",
  "version": "1.1.0",
  "private": true,
  "scripts": {
    "serve": "vue-cli-service serve",
    "build-staging": "vue-cli-service build --mode staging",
    "build-production": "vue-cli-service build --mode production",
    // etc...
  },
// etc...

これだけだとビルドしたファイルが同じdistディレクトリに入ってしまい、間違えてステージングのファイルを本番にデプロイなんてことも起きそうなので分ける。

書き出し先ディレクトリはvue.config.jsのoutputDirで行う。
上記で書いた環境変数を使ってディレクトリを変える関数を作って切り替えた。

vue.config.js

const getOutputDir = () => {
  if (process.env.VUE_APP_MODE === "production") {
    return 'dist-production'
  } else if (process.env.VUE_APP_MODE === "staging") {
    return 'dist-staging'
  }
}
module.exports = {
  outputDir: getOutputDir(),
  // etc...

この設定でステージングはdist-stagingディレクトリに、
本番はdist-productionに書き出されるようになった。
distディレクトリは間違わないよう消しておいた

Firebaseにデプロイ

次にFirebase Hostingでデプロイ先プロジェクトを分ける設定。
もちろん事前に本番用、ステージング用でプロジェクトを作っておく。

公式にあった
Firebase CLI リファレンス  |  Firebase

firebase use --add

でステージング、本番用の設定を追加しておく

firebae.jsonに書くpublicはdeploy時、-pオプションで上書きできるのでこれで分ける。

最終的にステージングへのビルド、デプロイは下記2つで完了する

yarn build-staging
firebase deploy --project your-staging-project-name -p dist-staging

本番はこんな感じ

yarn build-production
firebase deploy --project your-production-project-name -p dist-production

これを実行中の開発用Dockerコンテナでexecするためシェルスクリプト化した。

staging_deploy.sh

sudo docker exec -it \
    $(sudo docker ps -a -q --filter="name=your-container-name") \
    yarn build-staging

sudo docker exec -it \
    $(sudo docker ps -a -q --filter="name=your-container-name") \
    firebase deploy --project your-staging-project-name  -p dist-staging
date

本番用はビルドでこけたらデプロイしないように止める必要がありそう。

API用のGKEとロードバランサーは本番と同じのを使う

GCPHTTPSロードバランサーは最低2000円/月ぐらいしてしまうので、分けるのは避けたい。
またドメインを分けるとSSL証明書とかの手間も増えるのでドメインも同じにしたい。

今回はHTTPSロードバランサーの設定でアクセスしてきたパスでバックエンドを分けた。

/api → 本番用バックエンド
/api-staging → ステージングバックエンド

というように振り分けるようにして、APIではどちらのパスも同じ処理へルーティングして同じコンテナでどちらでも動くようにした。
Vue側は環境変数で切り分ける。

まだ使ったことないけどIngressを使えばもっと簡単にできそうなので調査中。

CORSの設定

許可ドメインの設定はFirebaseや、GoogleAPIキー、Cloud StorageとかDjangoとか使ってるサービスごとにやるので何気にめんどう。
chromeのdeveloper consoleのエラーを見ながら設定しておく。

Google Cloud FunctionsでNode.js 8が使えるようになってる件

Python3.7に引き続き。
これで偉大なるasync/awaitが使えるようになった。
id:takuya_minami373がasync/awaitが無くて困ってPythonで書き直すか、と思ってたところのナイスタイミングで気づいてくれた。
f:id:uyamazak:20180731115545p:plain
おそらくLTSだし、これで気兼ねなくCloud Functionsをjsで書いていけそう。