超簡単！Firestore を Firebase Cloud Function 経由で BigQuery に定期インポートする方法

# はじめに

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41793/46b4a0ac-51b9-54a9-9276-3d074aac25c1.png)
出典: https://medium.com/google-cloud-jp/firestore-bigquery-3b887a5bc27e

firestore はリアルタイム性に優れている一方で、過去まで遡ったで大量のデータに対して複雑なクエリーを実行することができません。
自分が担当しているサービス規模だと、今まではギリギリ対応できるデータ量だったので直接 firestore で全件取得した後集計処理を書いていたのですが、サービスが成長するにつれてこれではまかないきれなくなってきてしまいました。
そこで今回は、firestore の集計に関するコレクションを BigQuery にインポートするまでに行った手順をまとめてみます。

# 手順

## Cloud Storage のバケットを作成

### バケットの作成

https://cloud.google.com/storage/docs/creating-buckets?hl=ja


### ライフサイクル設定

https://cloud.google.com/storage/docs/managing-lifecycles?hl=ja

<img width="668" alt="スクリーンショット 2019-04-17 21.25.30.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41793/b75d0776-4c33-09dc-298c-233ecaf68d1f.png">


## BigQuery のデータセットを作成

今回は firestore.~ としたかったので `firestore` で作成
https://cloud.google.com/bigquery/docs/datasets?hl=ja


<img width="915" alt="スクリーンショット 2019-04-17 21.20.51.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41793/bb79385d-a36b-07a2-1337-da72ddf1f976.png">

↓

<img width="143" alt="スクリーンショット 2019-04-17 21.20.10.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41793/b7abe724-88e4-ee53-f6e8-d0cbfffddb94.png">


## Cloud Function の関数作成

### Export 用関数の作成

対象コレクションを指定の Bucket へエクスポートします。
実際は引数を取って好きな日付で実行できるようにしておくとベターかと思います。

```typescript
import { google } from 'googleapis';

import { Config } from 'src/config';

import * as Moment from 'moment';
import * as Functions from 'firebase-functions';

export const firestoreExportToBigQueryCron = Functions.pubsub
    .topic('topicName')
    .onPublish(async message => {
        const targetYmd = Moment().subtract(1, 'day').format('YYYYMMDD');

        const auth = await google.auth.getClient({
            scopes: ['https://www.googleapis.com/auth/datastore', 'https://www.googleapis.com/auth/cloud-platform'],
        });

        await google.firestore('v1').projects.databases.exportDocuments({
            name: `projects/${Config.projectId}/databases/${Config.projectId}`,
            requestBody: {
                collectionIds: ['collectionId'],
                outputUriPrefix: `gs://${Config.gcs.firestoreExportBucket}/${targetYmd}`,
            },
            auth,
        });
    });
```

### Import 用関数の作成

対象 Bucket のアップロードに反応し、エクスポート結果を BigQuery にインポートします。

```typescript
import { google } from 'googleapis';

import { Config } from 'src/config';

import * as Functions from 'firebase-functions';

export const bigQueryImportStorageTrigger = Functions.storage
    .bucket(Config.gcs.firestoreExportBucket)
    .object()
    .onFinalize(async object => {
        const name = object.name!;
        const matched = name.match(/all_namespaces_kind_(.+)\.export_metadata/);
        if (!matched) {
            return console.log(`invalid object: ${name}`);
        }

        const collectionName = matched[1];
        const auth = await google.auth.getClient({ scopes: ['https://www.googleapis.com/auth/bigquery'] });
        const result = await google.bigquery('v2').jobs.insert({
            auth,
            projectId: Config.projectId,
            requestBody: {
                configuration: {
                    load: {
                        destinationTable: {
                            tableId: collectionName,
                            datasetId: 'firestore',
                            projectId: Config.projectId,
                        },
                        sourceFormat: 'DATASTORE_BACKUP',
                        writeDisposition: 'WRITE_TRUNCATE',
                        sourceUris: [`gs://${Config.gcs.firestoreExportBucket}/${name}`],
                    },
                },
            },
        });

        console.log(result);
    });
```

## IAM の設定

[どうやら GAE デフォルトアカウントにはエクスポートやインポートの権限がない](https://github.com/googleapis/google-auth-library-nodejs/issues/532#issuecomment-455966970)ようなので IAM から追加します
https://cloud.google.com/iam/docs/quickstart?hl=ja


<img width="676" alt="スクリーンショット 2019-04-17 11.02.10.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41793/bd269d78-2851-3738-d0eb-c3cae4cda41a.png">


## スケジューリング

GAE か Cloud Scheduler で先程の関数を定期実行できるように設定します。
https://cloud.google.com/scheduler/docs/quickstart

# ハマったこと

cloud storage のエクスポートからのインポートを手動で確認する工程にて...
エクスポート実行直後、パッと見ディレクトリとかは生成されているのでエクスポートが終了したと勘違いし、その中途半端なディレクトリの再インポートに失敗してハマっていました...

データ量の多いプロジェクトの場合は下記コマンドでジョブの終了をちゃんと確認しましょう...

```shell
gcloud beta firestore operations list
```

---

また、現在の BigQuery は以前までよく使われていた日付別のテーブル分割が非推奨になっているようですが、大量のデータがあるスコープにおいては上書きインポートではなく、追加で管理したいものです。
そうしないと全データを firestore 側にも保持しないといけなくなるため、DB 復元時間やコスト面に支障が出てきます。
しかし firestore からエクスポートしたファイルからは BigQuery への追加インポートができないため、大量のデータを扱う必要のあるスコープにおいては日付別テーブル分割で妥協することにしました...

# おわりに

今回の対応で 10 分近くかかっていた集計処理を 1 分未満に短縮できました。
また、firebase と BigQuery はどちらも GCP に属するサービスなので、IAM を介して簡単安全に連携できる点も素晴らしいです。（今回作成したスクリプトはどちらも認証情報の記述が不要）
今後も引き続き BigQuery を活用していきたいと思います！

# 参考

- https://qiita.com/raynux/items/c653bb9826464f8faf40
- https://qiita.com/kurikei/items/ece7f6a21115494386c9
- https://medium.com/google-cloud-jp/firestore-bigquery-3b887a5bc27e