GolangとFirestoreの開発環境をDockerで作る

# 概要
Firestoreのエミュレータ構築は **[gcloud CLIを使う方法](https://cloud.google.com/firestore/docs/emulator?hl=ja)** と **[Firebase CLIを使う方法](https://firebase.google.com/docs/emulator-suite/connect_and_prototype?hl=ja)** があります。

どちらを使ってもよいのですがFirebase CLIを使う場合は、ブラウザ上でFirestoreのUIが使えるため、開発効率が上がりそうです。
ということでFirebase CLIでの構築をしていきます。

## こんな人向け
- CLIをローカル環境に入れたくない(Dockerを使いたい)
- FirestoreにGoで読み書きをしたい
- デプロイは特に考えず、お手軽にFirestoreの動作確認ができる環境を作りたい

# 環境構築
## Dockerfileの設定
### Firebaseエミュレータコンテナ用
>Emulator Suite をインストールする前に、以下が必要です。
> - Node.js バージョン 16.0 以降
> - Java JDK バージョン 11 以降

公式ドキュメントによると、Firebaseエミュレータを使うには [NodeとJavaが必要](https://firebase.google.com/docs/emulator-suite/install_and_configure?hl=ja#install_the_local_emulator_suite) とのことなので、nodeコンテナにJavaをインストールしました。
ついでにnpmでFirebase CLIをインストールしておきます。

```Dockerfile:firebase/Dockerfile
FROM node:20.10.0-slim

RUN apt-get -y update && apt-get -y install default-jdk
RUN npm install -g firebase-tools
```

### Goコンテナ用
Goのアプリケーション用のコンテナです。
1行なので、わざわざDockerfileにすることもないですが、開発を進めていくと色んなパッケージをイメージに入れておきたくなるのでDockerfileにしてます。
個人の好みで必要なパッケージをインストールしましょう。

```Dockerfile:app/Dockerfile
FROM golang:1.21
```


## Firebaseの設定
エミュレータを動かすのに必要なファイルは下記の2つです。

- `.firebaserc`
- `firebase.json`

まずは`.firebaserc`です。
エミュレータ用なのでプロジェクト名は何でもOKです。
```json:firebase/.firebaserc
{
  "projects": {
    "default": "test-project"
  }
}
```

次に`firebase.json`ですが、Dockerで使用する場合は`"host": "0.0.0.0"`を指定しないとdocker-composeでポートフォワードしてもブラウザで確認できないようです。
公式ドキュメントのこの辺の記載が該当しそうです。
>デフォルトでは、エミュレータは localhost からのリクエストにのみ応答します。つまり、ホストされているコンテンツには同じパソコンのウェブブラウザからアクセスできますが、ネットワーク上の他のデバイスからはアクセスできません。

https://firebase.google.com/docs/emulator-suite/use_hosting?hl=ja#emulators-no-local-host

```json:firebase/firebase.json
{
  "emulators": {
    "singleProjectMode": false,
    "ui": {
      "host": "0.0.0.0",
      "port": 4000
    },
    "firestore": {
      "host": "0.0.0.0",
      "port": 8080
    }
  }
}
```

これらのファイルは`firebase login`をして`firebase init`によるプロジェクト初期化設定を進めていくと自動で作成されます。
今回はFirestoreのエミュレータだけの構成なので事前に作ってバインドしますが、別のサービス(`Authentication`, `Cloud Functions`など)を使ったり複雑な構成にする場合は、事前に作るの大変なので自動作成するのが良いかと思います。

また`firebase init`による初期化時に「Firestoreを使用する」選択をした場合は、下記のファイルも自動で作成されます。
- `firestore.indexes.json`
- `firestore.rules`

今回はデプロイは特に考えず、開発環境だけ作りたいので `firestore.indexes.json`, `firestore.rules`は不要です。

## docker-composeの設定
Firestoreエミュレータに接続するのに、接続する側は`FIRESTORE_EMULATOR_HOST`を環境変数に設定しておく必要があるので忘れず設定しておきます([公式ドキュメント](https://firebase.google.com/docs/emulator-suite/connect_firestore?hl=ja#admin_sdks))。

```yaml:docker-compose.yaml
version: '3.9'

services:
  app:
    tty: true
    build:
      context: .
      dockerfile: ./app/Dockerfile
    environment:
      FIRESTORE_EMULATOR_HOST: "firebase:8080"
    volumes:
      - ./:/workspace
    working_dir: /workspace

  firebase:
    build:
      context: .
      dockerfile: ./firebase/Dockerfile
    ports:
      - 4000:4000
    volumes:
      - ./firebase/.firebaserc:/firebase/.firebaserc
      - ./firebase/firebase.json:/firebase/firebase.json
    tty: true
    working_dir: /firebase
    command: firebase emulators:start
```

## Goサンプルコードの作成
[Firebase側のドキュメント](https://firebase.google.com/docs/firestore/quickstart?hl=ja#set_up_your_development_environment)ではFirebaseのクライアントライブラリとして`firebase.google.com/go`を使用しています。

[GCP側のドキュメント](https://cloud.google.com/firestore/docs/create-database-server-client-library?hl=ja#add_the_server_client_library_to_your_app)ではFirestoreのクライアントライブラリとして`cloud.google.com/go/firestore`を使用しています。

[`firebase.google.com/go`のドキュメント](https://pkg.go.dev/firebase.google.com/go#App.Firestore)によると、`firebase.google.com/go`はFirestoreクライアントインスタンスを作るのに`cloud.google.com/go/firestore`を使っているため、Firestoreだけを使うのであれば後者だけインストールしておけばOKです。

>Firestore returns a new firestore.Client instance from the https://godoc.org/cloud.google.com/go/firestore package.

```go:main.go
package main

import (
	"context"
	"log"

	"cloud.google.com/go/firestore"
)

func main() {
	ctx := context.Background()
	client := createClient(ctx)
	defer client.Close()
	createSample(ctx, client)
}

func createClient(ctx context.Context) *firestore.Client {
	projectID := "test-project"

	client, err := firestore.NewClient(ctx, projectID)
	if err != nil {
		log.Fatalf("Failed to create client: %v", err)
	}
	return client
}

func createSample(ctx context.Context, client *firestore.Client) {
	_, _, err := client.Collection("users").Add(ctx, map[string]interface{}{
		"first": "Ada",
		"last":  "Lovelace",
		"born":  1815,
	})
	if err != nil {
		log.Fatalf("Failed adding alovelace: %v", err)
	}
}
```

サンプルコードは[クイックスタート](https://cloud.google.com/firestore/docs/create-database-server-client-library?hl=ja#go)のものを拝借しました。

## 確認
dockerを立ち上げてGoのサンプルコードを実行してFirestoreのUI上で、サンプルユーザーが追加されることを確認します。
```bash:コマンドライン
# コンテナ起動
docker compose up -d

# ログ確認してエミュレータの起動を確認
docker compose logs
```

![スクリーンショット 2023-12-03 14.59.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2627595/7646d76e-87bc-22e5-c0c1-fac93877ece1.png)

上のようなログが出ていれば無事Firebaseのエミュレータが起動できてるので、ブラウザで http://localhost:4000 にアクセスしてエミュレータUIを確認しましょう。
エミュレータの起動には少し時間がかかるのでコンテナが異常終了してなければ少し待ってみるといいです。

続いてGoの初期設定をしてから、サンプルコードを実行してFirestoreエミュレータにデータが登録されることを確認します。
```bash:コマンドライン
# モジュール初期化
docker compose exec app go mod init firestore-test

# クライアントライブラリインストール
docker compose exec app go get cloud.google.com/go/firestore

# サンプルコード実行
docker compose exec app go run main.go
```

http://localhost:4000/firestore にアクセスしてみましょう。
処理が完了すれば以下のようにFirestoreエミュレータにサンプルデータが登録されます。

![スクリーンショット 2023-12-03 15.05.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2627595/b9160955-0ca6-aa1d-1c2e-fe6efbdd6932.png)

# まとめ
複雑なデータ構造になってくると、データを確認したいときにデータ構造を加工して読みやすい形にして出力する必要が出てくると思います。
FirestoreのUI上で構造を確認できた方が直感的に理解できるので効率的に作業を進められるでしょう。