🗒️

Firebase ProjectをTerraformを使って管理

2024/01/22に公開

以前こちらで紹介したリポジトリの管理方法のterraformを利用した環境構築をコードベースで紹介しようと思います。
https://zenn.dev/cilly/articles/8d07823c268e1b

.
├── .envrc
├── .gitignore
├── _modules # <-- 全プロジェクト共通のmodule (現在は廃止)
│   ├── module_1
│   └── module_2
├── Makefile
├── project_1
│   ├── .envrc
│   ├── env_1
│   │   ├── .envrc
│   │   ├── Makefile
│   │   ├── main.tf
│   │   └── variables.tf
│   └── env_2
│       ├── .envrc
│       ├── Makefile
│       ├── main.tf
│       └── variables.tf
├── project_2
│   ├── .envrc
│   ├── env_1
│   │   ├── modules # <-- プロジェクト固有のmodule
│   │   │   ├── module_1
│   │   │   └── module_2
│   │   ├── .envrc
│   │   ├── Makefile
│   │   ├── main.tf
│   │   └── variables.tf
│   └── env_2
│       ├── .envrc
│       ├── Makefile
│       ├── main.tf
│       └── variables.tf
└── project_3
    ├── .envrc
    ├── env_1
    │   ├── .envrc
    │   ├── Makefile
    │   ├── main.tf
    │   └── variables.tf
    └── env_2
        ├── .envrc
        ├── Makefile
        ├── main.tf
        └── variables.tf

こんな構造をしています。

全体の流れ

  1. Makefileでgcloudコマンドを実行し最低限の状態にする
  2. terraformのmain.tfファイルの用意
  3. terraform cloudにworkspaceの作成
  4. 環境変数の設定
  5. terraformの実行(Run)

以上です

事前準備周り

gcloudコマンドが使えるように(brewでもinstallできます)

https://cloud.google.com/sdk/docs/install?hl=ja#linux

configuration を使用した project の切り替え(オプション)

https://cloud.google.com/sdk/docs/configurations?hl=ja

direnvを利用して作業時のディレクトリ内で使用する環境変数を管理します(オプション)

https://github.com/direnv/direnv

Makefileでgcloudコマンドを実行し最低限の状態にする

Workload Identity連携を利用するなどすれば作成段階も全てterraformで賄えそうですが、そこまでする必要はないかなと思い、作成と支払いアカウントの紐付け、terraformで実行する用のサービスアカウントの作成。
このあたりはgcloudコマンドで行う方針にしました。

一応理由
  • 請求先アカウントに紐付けられるプロジェクトはデフォルト5件まで(申請すれば50件くらいまで上げられる)なので、プロジェクト毎に請求先アカウントを作成する運用にしているため、請求先アカウントの作成と紐付けを考えるとあまり恩恵を感じなかった
  • 普段の状態管理と作成周りを一緒のworkspaceにしてしまうとstateの変化があった場合の影響範囲が未知で不安だった(別workspaceにするのもなんか違う気がした)
  • 正直面倒そうだった(Workload Identityの発行や管理が)

といった結構偏見を交えつつ個人的精神バランスを取った結果、プロジェクトの作成と最低限の処理をgcloudコマンドに任せることにしました。

手順

.envrcファイルで環境変数を設定

/.envrc
export CLOUDSDK_ACTIVE_CONFIG_NAME=xxx
export ORG_ID=01234567890
/project_1/.envrc
source_up
export PROJECT_PREFIX=project_1

export BILLING_ACCOUNT=xxxxxx-xxxxxx-xxxxxx
export DIR_NAME=${PROJECT_PREFIX}
/project_1/env_1/.envrc
source_up

export CONFIGURATION={env_name} #ex: prd, stg
export PROJECT_SUFFIX={identification_number} #ex: 001

export DIR_NAME=${DIR_NAME}/${CONFIGURATION}


export PROJECT_NAME=${PROJECT_PREFIX}-${CONFIGURATION}
変数の説明
変数 内容
CLOUDSDK_ACTIVE_CONFIG_NAME gcloud configurationで設定された設定切り替え名
ORG_ID GCPの組織ID
PROJECT_PREFIX これがプロジェクトのベースとなる名前
BILLING_ACCOUNT 請求アカウントID
CONFIGURATION prg, stgなど環境レベルを表す値
PROJECT_SUFFIX 作り直しなどをする可能性を考えたり、同じ名前のプロジェクトが存在している可能性を考慮し001など一意性が保たれるような値を入れ、その値をproject_idに設定する
DIR_NAME source_upによって上位階層のDIR_NAMEの値を継承しつつ、現在のDIR_NAMEを更新する。この値を利用して最終的にサービスアカウントのjsonファイルを吐き出し先を指定しています。
PROJECT_NAME PROJECT_PREFIXとCONFIGURATIONを繋げてプロジェクト名にする

Makefileの用意

/Makefile
## 定数
PROJECT_ID = ${PROJECT_NAME}-${PROJECT_SUFFIX}
SERVICE_ACCOUNT_KET_DIR = ./${DIR_NAME}/service-accounts/${PROJECT_ID}
SERVICE_ACCOUNT = terraform@${PROJECT_ID}.iam.gserviceaccount.com

#-----------------------------------------------------------------------------------------------------------------------

iam/add:
	gcloud projects add-iam-policy-binding ${PROJECT_ID} --member=serviceAccount:${SERVICE_ACCOUNT} --role=roles/${ROLE}

project/create:
	gcloud projects create ${PROJECT_ID} --name=${PROJECT_NAME} --organization=${ORG_ID}

billing/link:
	gcloud beta billing projects link ${PROJECT_ID} --billing-account=${BILLING_ACCOUNT}

budgets/enable/api:
	gcloud services enable billingbudgets.googleapis.com --project=${PROJECT_ID}

budgets/create:
	gcloud alpha billing budgets create \
	  --project=${PROJECT_ID} \
	  --billing-project=${PROJECT_ID} \
	  --billing-account=${BILLING_ACCOUNT} \
	  --filter-projects=projects/${PROJECT_ID} \
	  --display-name=${PROJECT_ID} \
	  --budget-amount=10000 \ # 適宜修正
	  --threshold-rule=percent=1.00 \ # 適宜修正
	  --threshold-rule=percent=0.90 \
	  --threshold-rule=percent=0.50 \
	  --threshold-rule=percent=0.30 \
	  --threshold-rule=percent=0.10 \
	  --quiet

service_account/create/terraform:
	gcloud iam service-accounts create terraform \
	--display-name="terraform" \
	--project=${PROJECT_ID}

service_account/compact_output:
	cat ${SERVICE_ACCOUNT_KET_DIR}/service-account.json | jq -c > ${SERVICE_ACCOUNT_KET_DIR}/service-account.compact-output.json

service_account/create/key:
	mkdir -p ${SERVICE_ACCOUNT_KET_DIR} \
	&& gcloud iam service-accounts keys create ${SERVICE_ACCOUNT_KET_DIR}/service-account.json --iam-account=${SERVICE_ACCOUNT} \
	&& make service_account/compact_output
	
#-----------------------------------------------------------------------------------------------------------------------

iam/add/all:
	@make iam/add SERVICE_ACCOUNT=${SERVICE_ACCOUNT} ROLE=serviceusage.serviceUsageAdmin \
	&& make iam/add SERVICE_ACCOUNT=${SERVICE_ACCOUNT} ROLE=servicemanagement.admin \
	&& make iam/add SERVICE_ACCOUNT=${SERVICE_ACCOUNT} ROLE=iam.securityAdmin \
	&& make iam/add SERVICE_ACCOUNT=${SERVICE_ACCOUNT} ROLE=firebase.admin \
	&& make iam/add SERVICE_ACCOUNT=${SERVICE_ACCOUNT} ROLE=appengine.appAdmin \
	&& make iam/add SERVICE_ACCOUNT=${SERVICE_ACCOUNT} ROLE=iam.serviceAccountUser \
	&& make iam/add SERVICE_ACCOUNT=${SERVICE_ACCOUNT} ROLE=cloudbuild.builds.editor \
	&& make iam/add SERVICE_ACCOUNT=${SERVICE_ACCOUNT} ROLE=storage.objectAdmin \
	&& make iam/add SERVICE_ACCOUNT=${SERVICE_ACCOUNT} ROLE=appengine.appCreator \
	&& make iam/add SERVICE_ACCOUNT=${SERVICE_ACCOUNT} ROLE=storage.admin \
	&& make iam/add SERVICE_ACCOUNT=${SERVICE_ACCOUNT} ROLE=iam.workloadIdentityPoolAdmin

init:
	@make project/create \
	&& make billing/link \
	&& make budgets/enable/api \
	&& make budgets/create \
	&& make service_account/create/terraform \
	&& make iam/add/all \
	&& make service_account/create/key
/project_1/env_1/Makefile
init:
	cd ../.. && make init

main.tf と variables.tfの用意

https://registry.terraform.io/modules/cilly-yllic/firebase-project-factory/google/latest

/project_1/env_1/main.tf
terraform {
  required_version = "~> 1.7.0"
  required_providers {
    google = {
      source  = "hashicorp/google"
      version = "~> 5.12.0"
    }
    google-beta = {
      source  = "hashicorp/google-beta"
      version = "~> 5.12.0"
    }
  }
}

locals {
  api_services                 = ["cloudtasks.googleapis.com"]
  organization_id              = var.organization_id
  project_id                   = var.project_id
  region                       = "asia-northeast1"
  editors                      = var.editors
  hosting_names                = var.hosting_names
  firestore_backup_buckets     = [{
    bucket_name                = "firestore-backups"
    export_platform            = "cloud_run"
  }]
  storage_buckets = [
    { bucket_name = "user-icons" },
  ]
}

provider "google-beta" {
  project               = local.project_id
  billing_project       = local.project_id
  user_project_override = true
  region                = local.region
}

provider "google" {
  project = local.project_id
  region  = local.region
}

module "initial" {
  source                       = "cilly-yllic/firebase-project-factory/google"
  version                      = "1.1.0"
  api_services                 = local.api_services
  editors                      = local.editors
  firestore_backup_buckets     = local.firestore_backup_buckets
  hosting_names                = local.hosting_names
  organization_id              = local.organization_id
  project_id                   = local.project_id
  region                       = local.region
  storage_buckets              = local.storage_buckets
}
variable "organization_id" {
  description = "GCP organizationId."
  type        = string
}

variable "project_id" {
  description = "Firebase project id"
  type        = string
}

variable "editors" {
  description = "Firebase project Development member's emails."
  type        = list(string)
  default     = []
}

variable "hosting_names" {
  description = "Firebase project Hosting names."
  type        = list(string)
  default     = []
}
全プロジェクト用modulesディレクトリの廃止理由
  • modules内の処理を修正したら全てのプロジェクトのpaln(&apply)処理が走ってしまう。
  • terraform registryであればバージョンで管理しているので、プロジェクト毎にバージョン指定を変更すれば影響を受けない

自作moduleの内容

気になる方はREADME.mdを見ていただくと大体わかるとは思いますが、まだ未熟なため、READMEの内容が完全ではないです。。。
なので興味を持っていただける方がいらっしゃるかもしれないので、一応説明だけさせていただきます。

  1. 必要なAPIの有効化
    • api_servicesに追加することでデフォルトで有効化される処理に追加され同様に有効化されます。
デフォルトで有効化されるもの
  • cloudbilling.googleapis.com
  • cloudresourcemanager.googleapis.com
  • identitytoolkit.googleapis.com
  • firebase.googleapis.com
  • appengine.googleapis.com
  • firebasestorage.googleapis.com
  • firestore.googleapis.com
  • cloudfunctions.googleapis.com
  • cloudbuild.googleapis.com
  • artifactregistry.googleapis.com
  • eventarc.googleapis.com
  • cloudscheduler.googleapis.com
  • run.googleapis.com
  1. Firestore(データベース)を作成します
  2. デフォルトで作成されるStorage Bucketを作成します
    • "${var.project}.appspot.com"という感じで作成
  1. 編集者の追加
Firebase Consoleを利用したり、実際に開発する人に以下の権限を付与
  • roles/editor
  • roles/cloudfunctions.admin
  • roles/artifactregistry.reader

下二つはfunctionのデプロイ時にこれがないとエラーがでる

  1. Web用のアプリの追加とHostingのサイトを作成
  1. Firestoreのデータをバックアップする用のStorage Bucketを作成します
  1. 上記Storage Bucket以外のバケットを作成します。

Terraform CloudにWorkspaceを作成し環境変数を設定しRunする。

  • Terraform Working Directoryの設定を {project}/{env}(対象のmain.tfファイルまでのパス)を設定
  • Version Controlの VCS Triggersでパターン登録
  • Workspace variablesの登録

最低限以上の設定をしてRunをすれば無事plan & applyが通るかと思います。無事Firebase Consoleでプロジェクトが確認できれば完了です。

まとめ

以上一旦基盤を作ってしまえば10分かかるかかからないかくらいでFirebase Consoleのページが開けるようになるので、productionとstaging環境が必要になった場合にそこまでストレスなく作成できるかと思います。

Firebase Consoleで作成するのが一番早いかもしれないですが、同じサービスで環境毎に本当に同じ状況が整っているか不安になるリスクを考えればTerraformなどのlaCを利用してコードベースで管理できる方が安心感があるかと思います。

Discussion