2021-7-16

個人開発の振り返り(バックエンド編)

雑な技術メモ

個人開発で React+Firebase でそこそこの規模のものを作ったので振り返りをする。

  • Cloud Functions
  • Firestore
  • Authentication
  • Firebase その他
  • Express + TypeScript

Cloud Functions

Hosting を経由すると CDN にレスポンスのキャッシュを置ける

今回これをめちゃくちゃ活用した。

Functions + Firestore はめちゃくちゃスケールしやすいのでパフォーマンスに関して問題が出るというようなことは起きにくいだろうけど、Firestore は DB とデータのやり取りをする際に課金されるので、何も対策しないとめちゃくちゃ課金されて金銭的な意味で死ぬ。Hosting を経由するようにするとキャッシュされている間はそもそも Functions にすら到達しないので懐に優しい。

また、Hosting は Fastly を採用しているみたいで、インスタントパージが可能。パージしたい URL に対して PURGE メソッドでリクエストを送ればそれだけでパージされる。(これはこれで大丈夫かという感じはする)

ただ、Firebase のドキュメントには載っていないやり方なので、仕事で採用するのは控えておいたほうが無難。

注意として、Hosting 経由で Functions にアクセスする場合、リージョンを us-central1 にする必要がある。

https://firebase.google.com/docs/hosting/functions?hl=ja

Firestore

つらみ

  • LIKE が使えないので全文検索が firestore 単体で出来ない
  • ページングがめんどくせぇ
  • 「読み込み〜回で〜円」っていう計算なので、過剰に読み込み回数を意識してしまう。結果、パフォーマンスを過剰に意識した可読性の悪いコードが生み出されやすい
  • クエリが貧弱
    • 複合クエリで array 系が一回しか使えなかったり、きつい
  • クライアントアプリが無い、あるかもしれないが見当たらない

Firestore のエミュレータテストで Unable to detect a Project Id in the current environment.って怒られる

CI でテスト動かそうとしたら怒られた。とりあえず initializeApp 時に initializeApp({projectId: 'dummy-id'})って仮の ID 指定するようにしたら動いた。(でも他のプロジェクトだと ID 指定なしで普通に動いたので謎

↓ が関連しているのかもしれない(けどめんどくさいので読んでいない)

https://github.com/firebase/firebase-tools/issues/2602

Firestore のバックアップ

https://firebase.google.com/docs/firestore/manage-data/export-import?hl=ja

基本ここに書いている

https://firebase.google.com/docs/firestore/solutions/schedule-export?hl=ja

CloudFunctions による自動化はここ

注意点は、

  • エクスポート/インポート共に、ドキュメントの数だけ読み込み・書き込みが発生する。なので、Firestore のデータが多いとそれだけで大量に課金される点注意
  • CloudStorage のバケットは、Firestore のリージョンを同じじゃないといけない

Firestore のエラー

https://firebase.google.com/docs/reference/js/firebase.firestore.FirestoreError?hl=ja

error.code でエラーの種類が取得できる

エラーコードは数字で、gRPC に則っているみたい

https://github.com/grpc/grpc/blob/master/doc/statuscodes.md

Firestore で where 対象と orderBy 対象が異なる時、index の作成が必要

https://qiita.com/zaburo/items/1905499ceab8c1420593

問題がある際はエラーが出る。下記記事に書いている通り、エラーにはられている URL にアクセスすれば自動でインデックス作成してくれるので、それで作成した上で出力出来る json を保存しておくのが吉

https://qiita.com/HorikawaTokiya/items/68235c99e2f335a36f3a

Authentication

Authentication で Google 認証すると、他プロバイダーの情報を上書きする

https://qiita.com/katsu-o/items/728812f5d7f682e80fc3#4-google-認証で-sign-in-した場合に既存アカウントを上書きする場合がある

xxx@gmail.com が Google 認証以外でユーザーアカウントとして登録されている状態で、xxx@gmail.com を Google 認証で signInWithRedirect(provider_google) 等としてサインインした場合、xxx@gmail.com は Google 認証のユーザーアカウントとして上書きされてしまいます(この時、それまで登録されていた Google 認証以外の認証方法はクリアされてしまいます)。

げろげろ、って感じ。「1 つのメールアドレスにつき複数のアカウント」を出来ないように設定でしている場合、例えば「Twitter ログイン => ログアウト => Google ログイン」をすると Twitter で再度ログインしようとした際にエラーになってログインできない(Google だとログイン出来る)。

ユーザーからしたら意味不明すぎるので、これはかなり注意しないといけない。

Cloud Storage

CloudStorage は一般公開しないと CDN にキャッシュされない

https://cloud.google.com/storage/docs/static-website?hl=ja#tip-dynamic

考えてみれば当然だが、上の公式に書かれているように一般公開しないと CDN にキャッシュされない。

バケットの一般公開の仕方は

https://www.kabanoki.net/2363/

なので、デフォルトで用意されているバケットのみでなんとかしようとするんじゃなく、少なくとも一般公開用のバケットは用意したほうがいい。

ローカルに CloudStorage のファイル一式をダウンロードする

GCP は GUI でのフォルダダウンロード機能を提供してないのでコマンドを打つ必要がある。

https://hub.docker.com/r/google/cloud-sdk

docker pull google/cloud-sdk:latest
docker run -ti google/cloud-sdk:latest gcloud version
docker run -ti --name gcloud-config google/cloud-sdk gcloud auth login
docker run -v /Users/ryou/work/ryouEin/cloud_sdk_docker/files:/home/files --rm -ti --volumes-from gcloud-config google/cloud-sdk:latest bash

# bash内
gsutil cp gs://example-bucket-name/cloudfunctions.googleapis.com/cloud-functions/2021/07/01/00:00:00_00:59:59_S0.json /home/files/hoge.json

みたいな感じで、Docker 使ってやるのが一番楽だと思う。

CloudStorage のキャッシュに関して

async upload(filePath: string, dataUri: string): Promise<void> {
    const { data } = parseDataUri(dataUri)
    const buffer = base64StringToBuffer(data)
    const storageFile = storage()
      .bucket(POST_IMAGES_CLOUD_STORAGE_BUCKET_NAME)
      .file(filePath)
    await storageFile.save(buffer)
    await storageFile.setMetadata({
      cacheControl: 'private,max-age=7200',
    })
  }

こんな感じで setMetadata でキャッシュの設定ができる

public は CDN とローカル両方にキャッシュする、private はローカルにしかキャッシュしない

s-maxage で CDN のキャッシュ時間をコントロール出来るみたいだが…どうも CDN のキャッシュ時間はベストエフォートみたい

https://qiita.com/sinmetal/items/37c105a098174fb6bf77

Firebase その他

ログに関して

https://www.apps-gcp.com/stackdriver-logging-export-gcs/

  • ログは何もしないと 30 日で消滅してしまうらしい
  • なので、シンク設定をしてどっかに保存しておく必要がある
  • CloudStorage への保存の場合、一時間ごとに保存されるっぽい
    • 指定したバケットに、日付単位でディレクトリが切られ、「04:00:00_04:59:59_S0.json」ってな感じのファイルで保存される
    • 専用のバケットを作ったほうが良さそう

CloudScheduler のタスクは、コンソールから「今すぐ実行」できる

https://firebase.google.com/docs/firestore/solutions/schedule-export?hl=ja#test_your_job_and_cloud_function

テストで動かしたい際に便利。

TypeScript + Express

余剰プロパティに注意

TypeScript は余剰プロパティがあってもコンパイルエラーが出ない。これは割とありがたいことも多いんだけど、HTTP レスポンスとして本来外部に出てはいけない物が漏出してしまう原因になることが予想される。

HTTP レスポンスは最終段階で Zod とかを使って余剰プロパティが無いかもチェックする必要がある。

その他

スプシでマスタデータ管理

マスタデータを管理する際にスプシが便利なことも多い。

https://qiita.com/takatama/items/7aa1097aac453fff1d53

この記事のやりかたで、いけるので参考に

API のバージョニングに関して

https://qiita.com/seya/items/f98dede65844dd2c3dcb

API 更新した際に、ユーザーがサイト開きっぱなしだとフロントは古いがバックエンドは新しいみたいな状況になってしまう。

そうなると異常動作をしてしまうので、バージョニングした上で異なるバージョンでアクセスした際には再読込させるような仕組みが必要だよという話。

今回、/api/v0.0.1/~みたいにバージョニングして、バックエンドの API のバージョンと異なるバージョンがリクエストされた際にリロードを促すようにした。