NestJS+cloud function+firestoreでAPIサーバーを構築
背景
直近の案件でGCPの環境を構築する必要がありましたが、terraformで構築する際に色々躓きかけた箇所が多かったのでその時の覚書になります。
普段AWSばかり触っており、GCPに関する知識が浅かったのもありますが、cloud function,firestoreをterraformで構築するノウハウは検索情報に乏しかったり、AIに聞いたりしても解決が少し難しかった気がします。
構成
フロントエンドはvite+reactで構築し、SPAをFirebase Hostingにホストしています(これはterraformの管理外)。
APIはNestJSで構築しており、cloud function上で実行し、firestore,cloud storageにアクセスします。
共通設定
コンソール上から既にプロジェクトを作ってる状態とします。
ディレクトリ構成は色々工夫できるようですが、利用するリソースがそこまで多くないので全て同一ディレクトリ内で設定しました。
先に共通で使用する変数の定義です。
project_name,project_id,location_id,org_idをコンソールかクレデンシャルから得られる情報を設定していきます。
jwt_secret_keyはNestJSで使用する環境変数になります。
variable "project_name" {
default = "sample-project"
type = string
}
variable "project_id" {
default = "sample-project-12345"
type = string
}
variable "location_id" {
default = "asia-northeast1"
type = string
}
variable "org_id" {
default = "----------"
type = string
}
variable "jwt_secret_key" {
description = "The JWT secret key"
type = string
}
jwt_secret_keyに指定する環境変数は以下に定義します。※git管理外
jwt_secret_key = "hoge"
terraformで利用するプロバイダの設定です。
terraform {
required_providers {
google = {
source = "hashicorp/google"
version = "5.12.0"
}
}
}
provider "google" {
credentials = file("credential.json") ※git管理外に
project = var.project_id
region = "asia-northeast1"
user_project_override = true
}
使用するプロジェクトの指定を行います。
resource "google_project" "default" {
provider = google
project_id = var.project_id
name = var.project_name
org_id = var.org_id
# Firebase のプロジェクトとして表示するため
labels = {
"firebase" = "enabled"
}
}
firestoreの設定
※datastoreモードです。
複合インデックスが必要な場合は以下のように設定してしまいます。
resource "google_firestore_database" "datastore_mode_database" {
project = var.project_id
name = "(default)"
location_id = var.location_id
type = "DATASTORE_MODE"
delete_protection_state = "DELETE_PROTECTION_DISABLED"
deletion_policy = "ABANDON"
timeouts {}
}
// インデックス
resource "google_datastore_index" "TestIndex" {
project = var.project_id
kind = "TestTable"
properties {
name = "testId"
direction = "ASCENDING"
}
properties {
name = "createdAt"
direction = "DESCENDING"
}
}
cloud functionの設定
ポイントについてはコメントで記しています。
data "archive_file" "function_archive" {
type = "zip"
// NestJSのソースがあるディレクトリを指定
source_dir = "../../api"
// zipが肥大化しないように以下のものは指定から外す
excludes = [ "node_modules", "dist", "credential.json" ]
output_path = "./function.zip"
}
// 関数ソースが入ったzipを配置するストレージバケット
resource "google_storage_bucket" "api_function_bucket" {
name = "api-function-${var.project_id}"
location = var.location_id
storage_class = "STANDARD"
}
// cloud functionの対象となるオブジェクトを指定するリソース
resource "google_storage_bucket_object" "packages" {
name = "function.zip"
bucket = google_storage_bucket.api_function_bucket.name
source = data.archive_file.function_archive.output_path
}
// IAMを設定するために必要なサービスアカウント
// これに必要な権限を付与してcloud-functionに紐付ける
resource "google_service_account" "api-function-user" {
project = var.project_id
account_id = "api-function-user"
display_name = "api-function-user"
}
// アカウントの権限を借用する権限
resource "google_project_iam_member" "project" {
project = var.project_id
role = "roles/iam.serviceAccountTokenCreator"
member = "serviceAccount:${google_service_account.api-function-user.email}"
}
resource "google_project_iam_member" "service-account-user-iam" {
project = var.project_id
role = "roles/iam.serviceAccountUser"
member = "serviceAccount:${google_service_account.api-function-user.email}"
}
// firestore(datastore)をたたくために必要なIAM
resource "google_project_iam_member" "firestore-user-iam" {
project = var.project_id
role = "roles/datastore.user"
member = "serviceAccount:${google_service_account.api-function-user.email}"
}
// ストレージバケット(sampleバケット)をたたくために必要なIAM
resource "google_storage_bucket_iam_member" "bucket_iam" {
bucket = "sample"
role = "roles/storage.objectViewer"
member = "serviceAccount:${google_service_account.api-function-user.email}"
}
// cloud functionはデフォルトでアクセス不可
// memberをallUsersとして誰でもコール可能にする
resource "google_cloudfunctions_function_iam_member" "api-events-iam" {
project = var.project_id
region = var.location_id
cloud_function = google_cloudfunctions_function.api-function.name
role = "roles/cloudfunctions.invoker"
member = "allUsers"
}
resource "google_cloudfunctions_function" "api-function" {
// 先に設定したサービスアカウントを設定
service_account_email = google_service_account.api-function-user.email
name = "api-function"
runtime = "nodejs18"
source_archive_bucket = google_storage_bucket.api_function_bucket.name
source_archive_object = google_storage_bucket_object.packages.name
trigger_http = true
available_memory_mb = 512
timeout = 60
entry_point = "handler" // 実行する関数名
environment_variables = {
// デフォルトのタイムゾーンはUTC。日付周りの計算でJTCにする必要があった
TZ = "Asia/Tokyo"
VITE_FIRESTORE_NAMESPACE = "TestNameSpace"
VITE_JWT_SECRET_KEY = var.jwt_secret_key
}
}
NestJS側の設定
cloud functionをterraformでapplyする際は、package.jsonのscripts上に書かれた"build"が自動的に実行されます。なのでコマンドが存在しない場合はエラーとなります。
また、エントリーポイントとなる関数のファイル名がデフォルトでfunction.jsになります。
import { bootstrapNest } from './bootstrap';
import 'reflect-metadata';
import { http, Request, Response } from '@google-cloud/functions-framework';
async function bootstrapServer() {
// bootstrapNestでNestFactory.create, initしたものを返却しています
const nestApp = await bootstrapNest();
return nestApp.getHttpAdapter().getInstance();
}
export const handler = async (req: Request, res: Response) => {
const server = await bootstrapServer();
return server(req, res);
};
Discussion