Firebase Firestore Database のマスタデータを Terraform で一元管理する
こんにちは、クラウドエース フロントエンド・UI/UX 部の 小堀内 です。
Firebase プロジェクトを複数環境(開発、本番など)で運用する際、Firestore Database 上でのマスタデータの管理は手間がかかります。
本記事では、Terraform を使用して Firestore Database のマスタデータを一元管理する方法を紹介します。
やること/やらないこと
- やること
- 複数環境(開発、本番など)の Firebase Firestore のマスタデータを Terraform コードにて一元管理する
- やらないこと
- Firestore データモデルの説明
- 既存 Firebase プロジェクトでのマスタデータの Terraform 管理
- デプロイパイプラインの構築
マスタデータとは
マスタデータは、アプリケーションの基本的な設定や参照データを指します。
例えば、
- トランザクションデータ
- ユーザーの注文履歴、支払い記録など
- マスタデータ
- 商品カタログ、都道府県リスト、ユーザー設定など
Firestore でのマスタデータ管理における主な課題
- 手作業による登録の手間
(さらに、サブコレクション構造になったマスタデータだと、Firebase コンソールからの登録や参照が非常に面倒) - 複数環境間でのデータ整合性の維持
- 変更履歴管理の難しさ
これらの課題に対して、管理画面の作成やデータ登録用プログラムの開発などの対策も考えられますが、小規模プロジェクトではコストや時間の面で現実的でない場合があります。
Terraform による Firestore 上のマスタデータ管理
Terraform を使用して、Firestore のコレクションとドキュメントをコード化することで、以下のメリットが得られます。
- 複数環境のプロジェクトが存在している際の、データソースの一元化
- 環境間の整合性の確保
- バージョン管理と変更履歴の追跡
- 自動化によるヒューマンエラーの削減
実装例
トップレベルコレクション構造の管理
例えば、クイズアプリを作ろうとした際の、クイズデータをトップレベルコレクション構造のマスタデータとします。
quiz (コレクション)
|
├── question-1 (ドキュメント)
| ├── question: "First Question"
| └── answer: "Answer 1"
|
├── question-2 (ドキュメント)
| ├── question: "Second Question"
| └── answer: "Answer 2"
|
└── question-3 (ドキュメント)
├── question: "Third Question"
└── answer: "Answer 3"
これを Terraform コードで管理しようとすると下記のようになります。
locals {
docs = [
{
collection = "quiz"
document_id = "question-1"
fields = jsonencode({
"question" = {
"stringValue" = "First Question"
},
"answer" = {
"stringValue" = "Answer 1"
},
})
},
{
collection = "quiz"
document_id = "question-2"
fields = jsonencode({
"question" = {
"stringValue" = "Second Question"
},
"answer" = {
"stringValue" = "Answer 2"
},
})
},
{
collection = "quiz"
document_id = "question-3"
fields = jsonencode({
"question" = {
"stringValue" = "Third Question"
},
"answer" = {
"stringValue" = "Answer 3"
},
})
},
]
}
resource "google_firestore_document" "docs" {
for_each = { for doc in local.docs : doc.document_id => doc }
provider = google-beta
project = var.project_id
collection = each.value.collection
document_id = each.value.document_id
fields = each.value.fields
}
サブコレクション構造の管理
例えば、都道府県データをサブコレクション構造のマスタデータとします。
regions (コレクション)
|
├── kanto (ドキュメント)
| ├── name: "関東地方"
| └── prefectures (サブコレクション)
| ├── tokyo (ドキュメント)
| | ├── name: "東京都"
| | └── population: 13929280
| |
| └── kanagawa (ドキュメント)
| ├── name: "神奈川県"
| └── population: 9237337
|
└── kansai (ドキュメント)
├── name: "関西地方"
└── prefectures (サブコレクション)
├── osaka (ドキュメント)
| ├── name: "大阪府"
| └── population: 8837685
|
└── kyoto (ドキュメント)
├── name: "京都府"
└── population: 2583140
これを Terraform コードで管理しようとすると下記のようになります。
locals {
regions = [
{
collection = "regions"
document_id = "kanto"
fields = jsonencode({
"name" = {
"stringValue" = "関東地方"
}
})
prefectures = [
{
collection = "prefectures"
document_id = "tokyo"
fields = jsonencode({
"name" = {
"stringValue" = "東京都"
},
"population" = {
"integerValue" = "13929280"
}
})
},
{
collection = "prefectures"
document_id = "kanagawa"
fields = jsonencode({
"name" = {
"stringValue" = "神奈川県"
},
"population" = {
"integerValue" = "9237337"
}
})
}
]
},
{
collection = "regions"
document_id = "kansai"
fields = jsonencode({
"name" = {
"stringValue" = "関西地方"
}
})
prefectures = [
{
collection = "prefectures"
document_id = "osaka"
fields = jsonencode({
"name" = {
"stringValue" = "大阪府"
},
"population" = {
"integerValue" = "8837685"
}
})
},
{
collection = "prefectures"
document_id = "kyoto"
fields = jsonencode({
"name" = {
"stringValue" = "京都府"
},
"population" = {
"integerValue" = "2583140"
}
})
}
]
}
]
}
resource "google_firestore_document" "regions" {
for_each = { for region in local.regions : region.document_id => region }
provider = google-beta
project = var.project_id
collection = each.value.collection
document_id = each.value.document_id
fields = each.value.fields
}
resource "google_firestore_document" "prefectures" {
for_each = merge([
for region in local.regions : {
for prefecture in region.prefectures : "${region.document_id}/${prefecture.document_id}" => merge(prefecture, {
parent_doc_id = region.document_id
})
}
]...)
provider = google-beta
project = var.project_id
collection = "regions/${each.value.parent_doc_id}/prefectures"
document_id = each.value.document_id
fields = each.value.fields
depends_on = [google_firestore_document.regions]
}
まとめ
今回は、Firestore Database におけるマスタデータ管理の方法を紹介させていただきました。
Firestore は NoSQL タイプのデータベースであり、RDB と比較すると、テーブル定義などの概念がなく、アプリケーション側の処理で自由にスキーマを定義できるので、当初は IaC 管理の必要性は感じられていませんでした。
しかし、マスタデータのような、固定でありながら将来的に更新の可能性があるデータに関しては、IaC 管理が有効に感じました。
また、Firestore はコンソールからデータの登録ができるものの非常に手間がかかり、ヒューマンエラーのリスクも高くなります。
特に、複数の環境を用意していた場合は、環境間でマスタデータの差分が生じ、意図しない不具合を起こす可能性があります。
ですので、このようなケースには Firebase を IaC 管理することも視野に入れても良いのではないかと感じました。
最後まで読んでいただきありがとうございました。
関連コンテンツ
本記事では Firestore のマスタデータ管理に焦点を当てていますが、Firebase プロジェクト全体の Terraform 管理に興味がある方は、過去に公開した記事や動画をご覧ください。
Discussion