Cloud Firestoreをサーバーから更新しようとしたら手間取ったのでまとめておく

# 概要

Cloud Firestoreをサーバーから更新しようとしたんですが、ドキュメントの入り口間違えたのとGCPのIAMに軽く混乱したのでその辺まとめておきます。


# 読者対象

- 将来的にFunctionsやGCP以外のサーバーからCloud Firestoreを操作しようとしている方。


# Road to 解決

## 半端に理解しているほど質の悪いものはない

クライアントサイドでは浅く利用済み。
書き込みのセキュリティを外していたので、書き込みはサーバーからのみ受け付けるように変更を決意。
公式ドキュメントのデータを保護するの概要を見てみる。
https://firebase.google.com/docs/firestore/security/overview?hl=ja
>サーバー クライアント ライブラリの場合は、Cloud Identity and Access Management（IAM）を使用して、データベースへのアクセスを管理します。IAM を使用して Java、Python、Node.js、Go のクライアント ライブラリのデータを保護する方法を説明します。

たったこれだけ・・・
しばらく固まってしまったがどうやら左のツリーから、スタートガイドに行けということを理解する。

https://firebase.google.com/docs/firestore/security/get-started?hl=ja

スタートガイドにはサーバー的な項目がないと思っていたら、注意書きが…

>注: サーバー クライアント ライブラリはすべての Cloud Firestore セキュリティ ルールをバイパスし、代わりに Google アプリケーションのデフォルト認証情報を使って認証を行います。REST API または RPC API 用のサーバー クライアント ライブラリを使用している場合は、Cloud Firestore 用の Cloud Identity and Access Management を設定してください。

デフォルト認証情報のリンク先がGCPの認証の話なのだが、はっきり言ってここにリンクした意味が分からないorz
https://cloud.google.com/docs/authentication/production?hl=ja

つんだのでググってみると、FirebaseAdminSDKで難なく書き込みしている方を散見できた。
ということは、私はどうやら入り口を間違ったとわかり、Firestoreのドキュメントを最初から読んでみることに。

上から2番目のスタートガイドにばっちり書いてますがなorz
https://firebase.google.com/docs/firestore/quickstart?hl=ja


## 後はすんなりと思ったがGCPの大草原にいきなり放り出される

ドキュメントに従ってFirebase Admin SDKをインストール・・・OK!

以下の手順に従い適切な認証情報を利用して、Cloud Firestoreを初期化します・・・OK！

>各自のサーバー（またはその他の Node.js 環境）で Firebase Admin SDK を使用するには、サービス アカウントを使用します。Cloud Platform Console で [IAM と管理] > [サービス アカウント] にアクセスします。新しい秘密鍵を生成し、JSON ファイルを保存します。次に、このファイルを使用して SDK を初期化します。

( ﾟДﾟ)はぁ？
いきなり投げやりになったのは気のせいだろうか？

「サービスアカウント」の部分はGCPドキュメントへのリンクになっており、どうもアカウント作れよということと理解。
https://cloud.google.com/compute/docs/access/create-enable-service-accounts-for-instances?hl=ja

firebaseが自動で作ったアカウントがいくつか出てくるので、ユニークな名前にしたほうがよさげ。
特にサービスアカウントの説明は、時間がたっても自分が作ったとわかるような説明が望ましいはず。
![firestoreAdmin1.png](https://qiita-image-store.s3.amazonaws.com/0/310578/f8037069-f505-a516-f1ac-f44871b078ae.png)


このアカウントへ権限を付与するが、GCPの中にFirebaseが潜んでいるため結構迷う。
分散しているのか、FirebaseのPrefixでまとまっているのか・・・正解としては、Firebaseの大項目一つにまとめられている。
私は最初「Cloud Filestore」という初見殺しにつかまって、1時間は無駄にしたm9(^Д^)プギャー
![firestoreAdmin2.png](https://qiita-image-store.s3.amazonaws.com/0/310578/0f2c939a-40a8-1202-a4bc-43219502d32d.png)

最後にオプションとなっているが、キーをもらうために作っているのでここでキーを作成しておく。
JSONファイルがDLされるが、これが漏れると大変なので、発行したなら厳重管理すること。
![firestoreAdmin3.png](https://qiita-image-store.s3.amazonaws.com/0/310578/b6ed4115-c83f-e393-6949-2eb88567baae.png)

あとはFirebaseの公式ドキュメントにある通り、そのJSONを読み込んでやればOK。
ただし、権限付与、もしくは変更してすぐに反映された旨のメッセージが表示されるが、実際にFireStoreに書き込みできるようになるには1分程度の遅延があった。
時間がたっても書き込みできない場合は、キーの再作成すれば解消されるはず。
それでも解消されない場合は、権限の見直しを。


## おまけ　セキュリティルールも勘違いした

すべて許可するでFirestoreを作成するとこうなっている。
私はallow readでreadが無条件に解放されていて、writeは条件を書くと誤って解釈してしまい・・・

```text
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read, write: if true;
    }
  }
}
```

クライアントからの書き込みはすべて拒否するため```if false```に変更
するとクライアントからデータが全く読めない事態にΣ(￣ロ￣lll)ガーン

```text
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read, write: if false;
    }
  }
}
```

公式ドキュメントのスタートアップの次に個別設定の例が記述されているが、allowは,で区切ったすべてにかかっており、ifもそれらすべての条件に対応する。
つまり、readとwriteの条件が異なるのであれば、別々に記述するのが正解。
以下が読み取り専用で書き込みが一切不可の条件。

```text
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read: if true;
      allow write: if false;
    }
  }
}
```

設定後はシミュレータが用意されているので、少なくともあれで確認したほうがいい。
本番環境ではテストコード作ってしっかり対応すべきだが。

念のため言っとくと、このセキュリティ設定はクライアントSDK(RESTでもいいが)からの書き込みに対応するものでしかない。
AdminSDK(RESTでもいいが)を使った場合には、これらをバイパスするので問題なく書き込める。(IAMで操作権限付与していることが前提)
もし不正な書き込みがあった場合は、以下の可能性があるということになる。

- セキュリティルールを変更する前に書き込まれた。
- セキュリティルールの適用範囲を誤っていた。(matchの部分)
- AdminSDKで別の担当が書き込んでいた。（おいっ）
- AdminSDKで使用する鍵が漏洩した(ﾟДﾟ;)
- クラウドベンダーのバグでGUI操作時に自動的にごみデータが書き込まれた。（Firebaseじゃないけど体験談(;^_^A）



# まとめ

中途半端に知っていただけに、遠回りした挙句GCPにもてあそばれた話でした。
GCPとの連携が進んでできることが進んでいくのはいいけど、Firebaseのお手軽感からいきなりGCPに投げ出されるのはつらかった・・・。
一枚ドキュメントはさんだほうがよくないか？で締めようと思ったら、”各自のサーバーで初期化する”場合は面倒で、Functionsからならこんな面倒なことしなくていいのか・・・orz
Functionsからコールせずに愚痴ってすみませんでしてた(ノД`)・゜・。