Google Cloud×Terraformで最小サーバーレス・チャットを作った
TL;DR この記事は普段はアプリ側メインのエンジニアが「Terraform」を使用してみて、失敗したブブなどをメモ書き程度に残すものになります。
生成AIを使って作成している部分もあるので、同じように作成された場合にも同様のつまづきがあるかもしれません...
何を作ったか
構成イメージ
- フロント: Cloud Storage 静的ホスティング(index.html)
- API: Cloud Run Functions(Python, GET/POST)
-
DB: Firestore(今回は
(default)
を避けて 名前付きDB:chat
) - CORS: 関数側で許可。Storage→Functions 直fetch。
資源一覧
project/
├─ main.tf # 各リソースの作成を行うメインの処理
├─ variables.tf # プロジェクトIDやregionなどの変数用
├─ outputs.tf # 作成後のエンドポイントなどの処理後の出力フォーマット
├─ files/
│ └─ index.html.tmpl # 静的ホストするhtml
└─ functions/
├─ main.py # Firestoreへのget/post処理のPythonコード
└─ requirements.txt # 使用するライブラリの定義
(ちなみに)今回のデプロイ手順
1. Google Cloud Consoleにアクセスする
2. Cloud Shellを起動する
3. 資材を上げる(terraformフォルダ一式)
4. terraformの初期化を行う
terraform init
5. コードの検証
terraform validate
6. リソースの展開
terraform apply -auto-approve \
-var="project=$(gcloud config get-value project)" \
-var="region=asia-northeast1" \
-var="firestore_location=asia-northeast1"
7. console上に出力されるURLをブラウザでアクセス
※お片付け
terraform destroy -auto-approve -var="project=$(gcloud config get-value project)"
静的ホストのfrontend_url
を開ければ完成。
動かない?…ログへ直行です。
躓きポイント
1. Firestore ドキュメント作成で fields
必須 / fields_json
使えない
-
事象:
The argument "fields" is required
、fields_json not expected
。 -
原因: Google provider v7 系の仕様。Firestore型ラッパで JSON 文字列を渡す必要あり。
-
解決策:
fields = jsonencode({ username = { stringValue = "System" } text = { stringValue = "ようこそ!" } created_at = { integerValue = "0" } })
2. ${}
によってバグる
-
事象:
Invalid character; Single quotes are not valid…
などテンプレ展開時エラー。 -
原因: Terraform でも
${...}
を使用しており、JS テンプレート中の${...}
をTerraformの構文だと解釈。 -
解決策: Terraform に解釈させたくない
${
は$${
にエスケープ。"${API_URL}"
だけはそのまま。
3. Firestore API が未有効で 403 SERVICE_DISABLED
-
事象: Firestoreにデフォルトデータ投入時に
403 SERVICE_DISABLED
が発生。 - 原因: API 有効化直後で伝播待ちが足りない。
-
解決策:
google_project_service
で有効化 →time_sleep
で 30–60s 待ってから DB 作成にdepends_on
。
4. Firestoreを作成で 409 already exists
- 事象: terraform destroy したのに Firestore が消えない
- 原因: Firestoreに関しては、既定は ABANDON(破棄しない)。さらに Delete Protection が有効なことも。
-
解決策: DB リソースに以下を追加する
ConsoleでポチポチしたDBは gcloud firestore databases delete も視野に。
delete_protection_state = "DELETE_PROTECTION_DISABLED" deletion_policy = "DELETE"
5. Cloud Run Functions起動失敗:Container Healthcheck failed
-
事象:
PORT=8080
で待受できず Ready にならない。 - 原因: 単純にPython上のエラー....。
- 解決策: ローカルできちんと試さないと...。
Terraform の“性格”を掴む(運用Tips)
-
Terraform は常に差分実行:
plan
→apply
。差分が無ければ何もしません。 -
State が真実:State に無い既存リソースは「無い扱い」。衝突しそうなら import 一択。
terraform import 'google_firestore_database.named' "projects/$PROJECT/databases/chat"
-
部分適用は最終手段:
-target=...
は便利だけど常用は非推奨。緊急のときだけ。 -
状態だけ同期:実体に合わせたいだけなら
-refresh-only
。terraform plan -refresh-only && terraform apply -refresh-only
-
依存は明示&バージョン固定:
required_providers
とrequirements.txt
をナメない。 -
時間は偉大:API有効化直後は世界がまだ知らない。
time_sleep
で数十秒の平和を。 -
テンプレと
$
の三角関係:templatefile()
と JS の`...${}...`
は $${} で平和に暮らす。
一言で言うと「神は State に宿る」。Console 先輩を尊重するなら、まず import しましょう。
原因を特定するログ調査
# Cloud Run Functionsの最新リビジョン名
REV=$(gcloud run services describe chat-api --region=asia-northeast1 \
--format='value(status.latestCreatedRevisionName)')
# その起動ログ(最重要)
gcloud logging read \
"resource.type=cloud_run_revision AND resource.labels.revision_name=$REV" \
--limit=200 --order=desc --format='table(timestamp,severity,textPayload)'
# 関数ログ(エラー要約)
gcloud functions logs read chat-api --gen2 --region=asia-northeast1 --limit=200
# Cloud Build(依存が本当に入ったか)
gcloud builds list --limit=5
# ID が出たら
#gcloud builds log --stream --id <BUILD_ID>
読むポイント:
-
ImportError/ModuleNotFoundError
→ requirements を直す -
TypeError: ... database
→ Firestore SDK を上げる -
Failed to find attribute 'chat'
→entry_point
と関数名を合わせる - 待受開始メッセージが無い → 起動前に落ちてる(遅延初期化で回避)
おわりに
クラウド×サーバーレス×IaCの現場は動くまでが大変。将来同じ轍を踏む方の参考になれば嬉しいです。