terraformを使って環境別にfirebase構築を行う

# terraformを使って環境別にfirebase構築を行う

## はじめに

Google I/O 2024 が開催され、Firebaseも予想以上に多くのアップデートがあり、大変盛り上がりました。
PostgresSQLとgraphqlを利用したサーバ環境を簡単に構築できるDataConnectは今後に期待ですね...!!

さて、今回は、それとは全く関係なく、firebaseのterraformによる構築について、自己整理も兼ねて記事としてみます。
terraformについてまだまだ学習したてなので、ツッコミどころがあるかと思います。。。ぜひ指摘いただけたら幸いです。　:bow: :bow: :bow:

## Terraformとは？

一言で言うと、**インフラの構成をソースコードとして管理できる機能**です。
同じ構成を本番環境や検証環境など複数環境に用意したい場合とても有用です。

公式では以下のようにメリットが説明されています。
> Terraformには、チームでの共用や再利用、バージョン管理がしやすく、自動化によってオペレーションコストを削減し、人的ミスが発生する可能性を減らせるというメリットがあります。

## 今回導入するFirebaseのサービス

以下のサービスをterraformで構築します

```
- firebase 
  - cloud functions (firebase functions)
  - firestore
  - storage
  - authentication
```

##　terraformの実装

### 設計
今回は以下のようなディレクトリで構成しました

```
- terraform
  - main.tf # 全構成の設定 (今回はfirebase_project/mainの利用が殆ど)
  - variables.tf　# mainで利用する変数の定義
  - env.tfvars # 実行時環境変数の定義
  - firebase_project
    - main.tf # firebase周りの設定
    - variables.tf # firebase_project/main.tfで利用する変数の定義
```

(今回、一回のterraform実行で本番・検証・開発環境をdeployできるようにしました)
(terraformについてまだ学習したてのため、もしアンチパターンとなってたら指摘いただけると幸いです)

### providerの設定

```terraform:terraform/main.tf
# 利用プロバイダのセットアップ
terraform {
  required_providers {
    google-beta = {
      source  = "hashicorp/google-beta"
      version = "~> 4.0"
    }
  }
}
provider "google-beta" {
  alias = "default"
  user_project_override = true
  region = var.region
}

# クォータチェック(上限割り当て)を回避したプロバイダの設定
provider "google-beta" {
  alias = "no_user_project_override"
  user_project_override = false
}

# ...以降に環境ごとの設定のため、moduleを定義する
```

### google serviceの設定

環境ごとに利用するresourceなどを定義していきます。

```terraform:terraform/google_project/main.tf
# GCPプロジェクトの作成
resource "google_project" "default" {
  provider   = google-beta.no_user_project_override
  name       = var.project_id # 簡単のため、プロジェクト名はprojectIDと同じにしています。
  project_id = var.project_id
  billing_account = var.billing_account
  labels = {
    "firebase" = "enabled"
  }
}

# 各APIの有効化
resource "google_project_service" "default" {
  provider = google-beta.no_user_project_override
  project = google_project.default.project_id
  for_each = toset([
    "serviceusage.googleapis.com",
    "cloudbuild.googleapis.com",
    "cloudbilling.googleapis.com",
    "cloudresourcemanager.googleapis.com",
    "firebase.googleapis.com",
    "identitytoolkit.googleapis.com",
    "firestore.googleapis.com",
    "firebaserules.googleapis.com",
    "firebasestorage.googleapis.com",
    "storage.googleapis.com",
    "cloudfunctions.googleapis.com",
    "firebasehosting.googleapis.com",
  ])
  service = each.key
  disable_on_destroy = false
}

# Firebaseの設定
resource "google_firebase_project" "default" {
  provider = google-beta
  project  = google_project.default.project_id

  depends_on = [
    google_project_service.default
  ]
}

# Locationの設定 (hashicorp/google-beta@5.0以上のversionは別の定義)
# 参考: https://zenn.dev/tkow/scraps/8bc5d0ab4d7b8b
resource "google_firebase_project_location" "default" {
  provider = google-beta
  project  = google_project.default.project_id
  location_id = var.region
}

# Firestoreの有効化
resource "google_firestore_database" "default" {
  provider   = google-beta.no_user_project_override
  project                     = google_project.default.project_id
  name                        = "(default)"
  location_id                 = var.region
  type                        = "FIRESTORE_NATIVE"
  concurrency_mode            = "OPTIMISTIC"
  app_engine_integration_mode = "DISABLED"

  depends_on = [
    google_firebase_project.default,
  ]
}

# Firebase CloudStorageの有効化
## AppEngineの有効化が必要
resource "google_app_engine_application" "default" {
  provider   = google-beta
  project     = google_project.default.project_id
  location_id = var.region

  # Cloud Firestore DB を作成する場合は、その作成を待つ必要がある
  depends_on = [
    google_firestore_database.default,
  ]
}

## Storage バケット
resource "google_firebase_storage_bucket" "default" {
  provider   = google-beta
  project   = google_project.default.project_id
  bucket_id = google_app_engine_application.default.default_bucket
}

# FirebaseAuthenticationの有効化
resource "google_identity_platform_config" "default" {
  provider   = google-beta
  project  = google_project.default.project_id

  depends_on = [
    google_project_service.default,
  ]
}

resource "google_identity_platform_project_default_config" "default" {
  provider   = google-beta
  project  = google_project.default.project_id
  sign_in {
    allow_duplicate_emails = false
  }

  depends_on = [
    google_identity_platform_config.default
  ]
}

# Firebase Web Appの有効化
resource "google_firebase_web_app" "default" {
  provider   = google-beta
  project      = google_project.default.project_id
  display_name = "Web App"
  deletion_policy = "DELETE"

  depends_on = [
    google_firebase_project.default,
  ]
}

data "google_firebase_web_app_config" "basic" {
  provider   = google-beta
  project =  google_project.default.project_id
  web_app_id = google_firebase_web_app.default.app_id
}

```

### google_projectで利用する変数の定義

```terraform:terraform/google_project/variables.tf
variable "project_id" {
  description = "The project ID"
  type = string
  nullable = false
}

variable "billing_account" {
  description = "Billing account ID for the project"
  type = string
  nullable = false
}

variable "region" {
  type = string
  nullable = false
}
```

### 環境ごとにmoduleとしてgoogle serviceを設定

```terraform:terraform/main.tf
# ...providerの設定コード

module "prod" {
  source = "./firebase_project"
  providers = {
    google-beta = google-beta.default
    google-beta.no_user_project_override = google-beta.no_user_project_override
  }
  project_id = var.prod_project_id
  billing_account = var.billing_account
  region = var.region
}

module "stg" {
  source = "./firebase_project"
  providers = {
    google-beta = google-beta.default
    google-beta.no_user_project_override = google-beta.no_user_project_override
  }
  project_id = var.stg_project_id
  billing_account = var.billing_account
  region = var.region
}

module "dev" {
  source = "./firebase_project"
  providers = {
    google-beta = google-beta.default
    google-beta.no_user_project_override = google-beta.no_user_project_override
  }
  project_id = var.dev_project_id
  billing_account = var.billing_account
  region = var.region
}

```

### 上記で用いる変数の定義

```terraform:terraform/variables.tf
# terraform/google_project/variables.tfと重複がある。重複を無くしたいが、やり方が不明...
variable "prod_project_id" {
  description = "The project ID for prod"
  type = string
  nullable = false
}

variable "stg_project_id" {
  description = "The project ID for stg"
  type = string
  nullable = false
}

variable "dev_project_id" {
  description = "The project ID for dev"
  type = string
  nullable = false
}


variable "billing_account" {
  description = "Billing account ID for the project"
  type = string
  nullable = false
}

variable "region" {
  type = string
  nullable = false
}
```

### 実際の変数ファイルの用意

```terraform:env.tfvars
prod_project_id = "prod_project_id"
stg_project_id = "stg_project_id"
dev_project_id = "dev_project_id"
region = "asia-northeast1"
billing_account = "billing-account-id"
```

以上で、各環境ごとに同一の構成でfirebaseを構築する用意ができました.

## terraformの実行

### 初期化

```shell
terraform init
```

### plan/deploy
```shell
terraform plan -var-file=env.tfvars # 実行計画の確認
terraform apply -var-file=env.tfvars # 実行
```

以上で、firebase projectが環境ごろに別プロジェクトとして用意されます。　🙌🙌🙌🙌
(APIが有効になっていない、などのエラーが出た場合は、APIの有効化に時間がかかっているためです。数分間時間をおいて再度applyしてみてください)

## おわりに

今回はterraformを用いて、firebaseでよく使うサービス周りの構築を行いました。
今回記載したサービス以外も多くのものがterraformで構築可能ですので、ぜひ試してみてください。

はじめに述べたように、terraformに関してまだ学習した手であり、つっこみどころがあるかと思います。
ぜひ優しく指摘いただけると幸いです...!

複数環境を用意したい場合にGUIでちょこちょこ行うのは大変ですし、今後のためにも、BestPracticeなど学んでいきたいと思います。