Firestoreのプロジェクト間データ移行の振り返り（困ったところのメモ）

Firestoreのプロジェクト間移行をやってみて困ったところのメモ。タイトルのままだけど。。

# 基本

[公式Doc](https://firebase.google.com/docs/firestore/manage-data/move-data?hl=ja)があるので、基本的にはこれに沿ってやっていった。
が、ここに明記されてないポイントで詰まった部分などがあったので、ここにまとめる。
このDocを読めばわかる通り、非常にざっくり言えば以下の流れで作業することになっている。

- 移行「元」のプロジェクト側にCloud Storage（バケット）を用意し、移行「元」データをそのバケットに吐き出す
- 移行「先」からそのバケットにアクセスして、移行「先」のFirestoreにimportする

# 前提

- 移行「元」と移行「先」のプロジェクトと、そのプロジェクトにアクセスするService Account、及びそのService Accountを使ったconfigがそれぞれ作成済みであること
- 移行「元」のほうにExportするべきコレクションが既に作成済みであること（移行「先」のほうにはコレクションがなくてもいい。importで勝手に作られる）
- 移行「先」のほうで[firestore.googleapis.com]が有効化されていること。ただこれは、仮に無効化のまま進めても、移行「先」でimportを実行した際に、「有効化します？」と聞いてくるので、最悪それまで放置でも良い。
- 移行「先」のほうでDatastoreではなくFirestoreが使える状態になっている（[Firestoreへの切り替えが済んでいる](https://cloud.google.com/datastore/docs/firestore-or-datastore?hl=ja)）こと。これは作業前に私がそういう状況だったからという理由で書いており、実際前提条件になってるかはわからない。なんとなく前提条件な気がしている。

# 1.Cloud Storageの用意

- 移行「元」プロジェクトのほうに切り替える

```shell
gcloud config configurations activate [移行元プロジェクトconfig名]
```

- 管理コンソールでCloud Storageを1個作る。

- ★Docには特に明記がないが、公開アクセスは「非公開」でも問題ない。
  ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/392942/54504501-6a86-42ae-af9f-191eaed3bb01.png)

- 公式Docにあるとおり、`[SOURCE_PROJECT_ID]@appspot.gserviceaccount.com`にStorage AdminのPermissionを与える。公式DocだとCLIで設定してるが私はコンソール上で設定しました
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/392942/11e85795-add4-baed-0bdf-97f2792f2363.png)

- ロールは「storage オブジェクト管理者」
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/392942/faf32368-01ce-ca33-3e13-8d6715d9bf8b.png)


# 2. Export

- Exportする。以下は移行元バケット名＝`src-bucket`、コレクション名は`Test`の例

```shell
gcloud firestore export gs://src-bucket --collection-ids=Test --async
```

- 以下のような標準出力が出るので公式Docに記載のあるように`outputUriPrefix` の値を目盛る（下記の例だと`gs://src-bucket/2022-06-06T09:38:10_9672`）

```shell
metadata:
  '@type': type.googleapis.com/google.firestore.admin.v1.ExportDocumentsMetadata
  collectionIds:
  - Test
  operationState: PROCESSING
  outputUriPrefix: gs://src-bucket/2022-06-06T09:38:10_9672
  startTime: '2022-06-06T09:38:10.886670Z'
name: projects/src-project/databases/(default)/operations/ASAwODYwNTQxODMJGnRsdWFmZWQHEjF0c2FlaHRyb24tYWlzYS1zYm9qLW5pbWRhGgoyEg
```

- この例だとステータスが「PROCESSING」になっているのでまだ出力が完了していないが、公式Docにもあるように、`gcloud firestore operations list`を使えば状況の確認はできる

```shell
> gcloud firestore operations list
---
done: true
metadata:
  '@type': type.googleapis.com/google.firestore.admin.v1.ExportDocumentsMetadata
  collectionIds:
  - Test
  endTime: '2022-06-06T09:38:18.301258Z'
  operationState: SUCCESSFUL
  outputUriPrefix: gs://src-bucket/2022-06-06T09:38:10_9672
  progressBytes:
    completedWork: '546'
    estimatedWork: '444'
  progressDocuments:
    completedWork: '3'
    estimatedWork: '3'
  startTime: '2022-06-06T09:38:10.886670Z'
name: projects/src-project/databases/(default)/operations/ASAwODYwNTQxODMJGnRsdWFmZWQHEjF0c2FlaHRyb24tYWlzYS1zYm9qLW5pbWRhGgoyEg
response:
  '@type': type.googleapis.com/google.firestore.admin.v1.ExportDocumentsResponse
  outputUriPrefix: gs://src-bucket/2022-06-06T09:38:10_9672
```


# 3. Import(試行錯誤)

- 移行「先」のほうのプロジェクトに切り替える。

```shell
gcloud config configurations activate [移行先プロジェクトconfig名]
```

- 公式Docにもある通り、移行「先」のほうのプロジェクト設定で使用するService Accountに、移行「元」のほうのバケットに対して「Storage オブジェクト管理者」のロールを付与する。設定方法は1.と同様。移行「先」のほうのバケットで「プリンシパルの追加」を行った後、対象プリンシパルの検索ワードに移行「先」のほうのService Account名を入力して検索し、ロールを付与する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/392942/c423a554-20b9-0ac6-606a-e6e5dc54e566.png)


- Importする。この際2. の標準出力の`outputUriPrefix`を引数に指定する。

```shell
> gcloud firestore import gs://src-bucket/2022-06-06T09:38:10_9672
ERROR: (gcloud.firestore.import) PERMISSION_DENIED: The caller does not have permission
```

- ★失敗した。これは、この`gcloud firestore import`コマンドを実行するユーザー（＝Service Account）に権限がないのだとエラーメッセージから推察。
  移行「先」のプロジェクトのIAM管理にて、このコマンドを実行するService Accountに「Firestoreサービスエージェント」というロールを付与。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/392942/1a468e29-780d-17aa-fc9e-cbe22910a9b0.png)

- 再実行。

```shell
> gcloud firestore import gs://src-bucket/2022-06-06T09:38:10_9672 --async
ERROR: (gcloud.firestore.import) PERMISSION_DENIED: Service account does not have access to Google Cloud Storage file: /src-bucket/2022-06-06T09:38:10_9672/2022-06-06T09:38:10_9672.overall_export_metadata. See https://cloud.google.com/datastore/docs/export-import-entities#permissions for a list of permissions needed. Error details: service-123456789012@gcp-sa-firestore.iam.gserviceaccount.com does not have storage.buckets.get access to the  Google Cloud Storage bucket.
```

- ★また失敗した。今度はまた違うエラーメッセージが出ている。公式Docには書かれていないが、どうやら`service-123456789012@gcp-sa-firestore.iam.gserviceaccount.com` (※`123456789012`の部分は例です。実際には移行「先」のProject ID値が入ると思う)というService Accountにこのバケットに対するアクセス権が必要なようだ。これ、ロール名がわかりづらく、「storage レガシーバケット読み取り」というロールが正解らしい。1.で`[SOURCE_PROJECT_ID]@appspot.gserviceaccount.com`にStorage AdminのPermission与えたのと同様に、バケットの権限画面で`service-123456789012@gcp-sa-firestore.iam.gserviceaccount.com`を追加して、「storage レガシーバケット読み取り」というロールを与える。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/392942/b279515c-bc37-1107-b644-7db3f5661170.png)

- 再実行。

```shell
>  gcloud firestore import  gs://src-bucket/2022-06-06T09:38:10_9672 --async
ERROR: (gcloud.firestore.import) PERMISSION_DENIED: Service account does not have access to Google Cloud Storage file: /src-bucket/2022-06-06T09:38:10_9672/2022-06-06T09:38:10_9672.overall_export_metadata. See https://cloud.google.com/datastore/docs/export-import-entities#permissions for a list of permissions needed.
```

- ★また失敗した。どうやら「storage レガシーバケット読み取り」だけでは足りないらしい。エラーメッセージを読む限りではオブジェクトをリストする（閲覧する）権限があれば事足りるように見える。とりあえず「Storage オブジェクト閲覧者」をつけてみる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/392942/ea16befb-bd63-379f-f078-c1bb9a13d527.png)

- 再実行。

```shell
> gcloud firestore import gs://src-bucket/2022-06-06T09:38:10_9672 --async
metadata:
  '@type': type.googleapis.com/google.firestore.admin.v1.ImportDocumentsMetadata
  collectionIds:
  - Test
  inputUriPrefix: gs://src-bucket/2022-06-06T09:38:10_9672
  operationState: PROCESSING
  startTime: '2022-06-06T10:03:56.622775Z'
name: projects/dst-project/databases/(default)/operations/AiA3NTEwMzE2NzMJGnRsdWFmZWQHEjF0c2FlaHRyb24tYWlzYS1zYm9qLW5pbWRhGgoyEg
```

- ようやく正常に動作した。。長い戦いだった。。。

# おわりに

- 上で「★」をつけた部分は公式Docには書かれていなかった（と私が思ってる）部分である。特に後半のImport周りは謎のServiceAccountが登場したりロールが色々必要だったり大変だった。Service Accountに関しては、「CLIを実行するユーザー（Service Account）」と移行元・先両方のプロジェクトの`[PROJECT_ID]@appspot.gserviceaccount.com`と、２つが登場人物として出てくるあたりが混乱の要因になってる気はする。そもそもCLIも`[PROJECT_ID]@appspot.gserviceaccount.com`で実行しろってことだったのかな？？この辺は確かめられていない。
- まあでもPermission周りに関しては、都度エラーメッセージを見ていけば割となんとかなる部分があったので、基本はエラーメッセージの指摘通りにやっていけばよいのだと思う。「レガシー閲覧」みたいな、いかにも怪しいロール名もあるし、そのうち変わりそうな気はプンプンするし、今後同じことをやるときも、冒頭の公式Docを「参考」にして、ちょいちょいやっていけば、あとはエラーメッセージが導いてくれると信じている。