AIアプリケーションの本番運用を続けていると、ある壁に突き当たります。「インフラの変更を手作業でやると怖い」「環境ごとの設定差異でバグが起きる」「チームが増えたときに再現性が担保できない」——こうした悩みを根本から解決するのが、Infrastructure as Code(IaC)の考え方です。
なぜ Gemini API アプリに IaC が必要なのか
AIアプリケーションのインフラには、従来のWebアプリとは異なる特有の課題があります。
まず、APIキーと認証情報の管理が複雑です。Gemini APIキーやサービスアカウントの鍵は、開発・ステージング・本番それぞれで別のものを使うべきですが、手動管理では混在しやすくなります。Secret Managerとのインテグレーションを自動化することで、この問題を構造的に解決できます。
次に、コスト管理とクォータ制御の問題があります。Gemini APIには利用クォータがあり、環境ごとに適切な上限を設定しないと、開発環境の暴走が本番の可用性に影響することもあります。Terraformでバジェットアラートやクォータ設定を管理することで、安全なコスト管理が実現します。
さらに、スケーラビリティの設定です。Cloud Runの最小・最大インスタンス数、タイムアウト、メモリ設定などは、AIワークロードの特性(推論時間が長い、メモリ消費が大きい)に合わせた調整が必要です。これをコードで管理することで、環境間の一貫性が保たれます。
プロジェクト構成と前提環境
本ガイドで使用するTerraformプロジェクトの構成は以下の通りです。
gemini-ai-infra/
├── main.tf # メインリソース定義
├── variables.tf # 変数定義
├── outputs.tf # 出力値
├── provider.tf # プロバイダー設定
├── backend.tf # Terraformステート管理
├── modules/
│ ├── iam/ # IAM・サービスアカウント
│ ├── secrets/ # Secret Manager
│ ├── cloud_run/ # Cloud Runサービス
│ └── monitoring/ # Cloud Monitoring
└── environments/
├── dev/ # 開発環境
├── staging/ # ステージング環境
└── prod/ # 本番環境
前提条件:
- Terraform 1.7以上(またはOpenTofu 1.6以上)がインストール済み
- Google Cloud CLIがインストール・認証済み(
gcloud auth application-default login)
- Terraform Cloudアカウント(CI/CD連携用、任意)
# バージョン確認
terraform version
# Terraform v1.7.5
gcloud --version
# Google Cloud SDK 478.0.0
Step 1: プロバイダーとバックエンドの設定
まず Terraform の基盤となるプロバイダーとリモートステートの設定を行います。Gemini APIプロジェクトでは、状態ファイルをチームで共有するためにGoogle Cloud Storageバケットをバックエンドとして使うのが定石です。
# provider.tf
terraform {
required_version = ">= 1.7"
required_providers {
google = {
source = "hashicorp/google"
version = "~> 5.20"
}
google-beta = {
source = "hashicorp/google-beta"
version = "~> 5.20"
}
}
}
provider "google" {
project = var.project_id
region = var.region
}
provider "google-beta" {
project = var.project_id
region = var.region
}
# backend.tf
terraform {
backend "gcs" {
# バケット名は事前に手動で作成しておく
bucket = "your-project-terraform-state"
prefix = "gemini-ai-infra"
}
}
# variables.tf
variable "project_id" {
description = "Google CloudプロジェクトID"
type = string
}
variable "region" {
description = "デプロイリージョン"
type = string
default = "asia-northeast1" # 東京リージョン
}
variable "environment" {
description = "環境名(dev / staging / prod)"
type = string
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "environment は dev, staging, prod のいずれかを指定してください。"
}
}
variable "gemini_api_key" {
description = "Gemini APIキー(Secret Managerで管理)"
type = string
sensitive = true # planコマンドでも値が表示されない
}
Step 2: IAM とサービスアカウントの自動化
Gemini APIアプリにおけるIAM設計は、最小権限の原則を徹底する点が肝心です。Cloud Runサービスが実行時に使うサービスアカウントと、デプロイ用のサービスアカウントを分離します。
# modules/iam/main.tf
# アプリケーション実行用サービスアカウント
resource "google_service_account" "gemini_app_runner" {
account_id = "gemini-app-runner-${var.environment}"
display_name = "Gemini App Runtime SA (${var.environment})"
description = "Cloud Run上のGeminiアプリが使用するサービスアカウント"
project = var.project_id
}
# Secret Manager へのアクセス権限(APIキー取得用)
resource "google_project_iam_member" "secret_accessor" {
project = var.project_id
role = "roles/secretmanager.secretAccessor"
member = "serviceAccount:${google_service_account.gemini_app_runner.email}"
}
# Cloud Run Invoker(サービス間呼び出し用)
resource "google_project_iam_member" "run_invoker" {
project = var.project_id
role = "roles/run.invoker"
member = "serviceAccount:${google_service_account.gemini_app_runner.email}"
}
# Vertex AI ユーザー権限(Vertex AI経由でGeminiを使う場合)
resource "google_project_iam_member" "vertex_user" {
project = var.project_id
role = "roles/aiplatform.user"
member = "serviceAccount:${google_service_account.gemini_app_runner.email}"
}
# Cloud Logging への書き込み権限
resource "google_project_iam_member" "log_writer" {
project = var.project_id
role = "roles/logging.logWriter"
member = "serviceAccount:${google_service_account.gemini_app_runner.email}"
}
output "runner_service_account_email" {
value = google_service_account.gemini_app_runner.email
}
Step 3: Secret Manager で API キーを安全に管理
Gemini APIキーをSecret Managerで管理することで、ソースコードやDockerイメージにキーを含めることなく、安全にアプリケーションへ渡すことができます。
# modules/secrets/main.tf
# Gemini APIキーのシークレット作成
resource "google_secret_manager_secret" "gemini_api_key" {
secret_id = "gemini-api-key-${var.environment}"
project = var.project_id
replication {
auto {} # 自動レプリケーション(推奨)
}
labels = {
environment = var.environment
managed-by = "terraform"
service = "gemini-api"
}
}
# シークレットのバージョン(実際の値を格納)
resource "google_secret_manager_secret_version" "gemini_api_key_v1" {
secret = google_secret_manager_secret.gemini_api_key.id
secret_data = var.gemini_api_key
# Lifecycle設定: Terraformがバージョンを削除しないようにする
lifecycle {
ignore_changes = [secret_data]
}
}
# シークレットのIAMポリシー(サービスアカウントのみアクセス可)
resource "google_secret_manager_secret_iam_member" "gemini_api_key_access" {
secret_id = google_secret_manager_secret.gemini_api_key.secret_id
role = "roles/secretmanager.secretAccessor"
member = "serviceAccount:${var.runner_service_account_email}"
}
output "gemini_api_key_secret_name" {
value = google_secret_manager_secret.gemini_api_key.name
}
Step 4: Cloud Run サービスのデプロイ自動化
Cloud RunにGemini APIを使うアプリケーションをデプロイする際、AIワークロード特有の設定(長めのタイムアウト・多めのメモリ)を適切に設定する点が肝心です。
# modules/cloud_run/main.tf
resource "google_cloud_run_v2_service" "gemini_app" {
name = "gemini-app-${var.environment}"
location = var.region
project = var.project_id
# AIワークロード向けの実行設定
template {
service_account = var.runner_service_account_email
scaling {
# 開発環境はコスト節約のため最小インスタンスを0に
min_instance_count = var.environment == "prod" ? 1 : 0
# 本番環境はスパイクを考慮して上限を設定
max_instance_count = var.environment == "prod" ? 20 : 5
}
containers {
image = var.container_image
# AIアプリはメモリ消費が多いため多めに設定
resources {
limits = {
cpu = var.environment == "prod" ? "2" : "1"
memory = var.environment == "prod" ? "4Gi" : "2Gi"
}
# CPU常時割り当て(推論処理の遅延を防ぐ)
cpu_idle = false
startup_cpu_boost = true
}
# 環境変数の設定
env {
name = "APP_ENV"
value = var.environment
}
env {
name = "GOOGLE_CLOUD_PROJECT"
value = var.project_id
}
# Secret ManagerのAPIキーをマウント
env {
name = "GEMINI_API_KEY"
value_source {
secret_key_ref {
secret = var.gemini_api_key_secret_name
version = "latest"
}
}
}
# ヘルスチェック
startup_probe {
http_get {
path = "/health"
port = 8080
}
initial_delay_seconds = 10
period_seconds = 5
failure_threshold = 10
}
liveness_probe {
http_get {
path = "/health"
port = 8080
}
period_seconds = 30
failure_threshold = 3
}
}
# Gemini APIへのリクエストは時間がかかるため長めに設定
timeout = "300s"
}
traffic {
type = "TRAFFIC_TARGET_ALLOCATION_TYPE_LATEST"
percent = 100
}
}
# Cloud Run サービスへの公開アクセス設定
# 本番環境では認証を必須にすることを推奨
resource "google_cloud_run_service_iam_member" "public_access" {
count = var.allow_unauthenticated ? 1 : 0
location = google_cloud_run_v2_service.gemini_app.location
project = var.project_id
service = google_cloud_run_v2_service.gemini_app.name
role = "roles/run.invoker"
member = "allUsers"
}
output "service_url" {
value = google_cloud_run_v2_service.gemini_app.uri
}
Step 5: 環境分離と Terraform Workspace の活用
開発・ステージング・本番の3つの環境を安全に管理するには、環境ごとにTerraformのステート(状態ファイル)を分離することが必須です。
Terraform Workspaceを使った環境分離:
# 開発環境のworkspaceを作成・切り替え
terraform workspace new dev
terraform workspace select dev
# 開発環境にデプロイ
terraform apply -var-file="environments/dev/terraform.tfvars"
# ステージング環境へ切り替えてデプロイ
terraform workspace select staging
terraform apply -var-file="environments/staging/terraform.tfvars"
# environments/dev/terraform.tfvars
project_id = "my-project-dev"
environment = "dev"
region = "asia-northeast1"
container_image = "asia-northeast1-docker.pkg.dev/my-project-dev/app/gemini-app:latest"
allow_unauthenticated = true # 開発環境はURLを共有するため認証不要
# environments/prod/terraform.tfvars
project_id = "my-project-prod"
environment = "prod"
region = "asia-northeast1"
container_image = "asia-northeast1-docker.pkg.dev/my-project-prod/app/gemini-app:v1.2.0"
allow_unauthenticated = false # 本番環境は認証を必須化
重要なポイント: 開発環境と本番環境は 別のGCPプロジェクト を使うことを強くお勧めします。これにより、IAMポリシーの誤設定が本番に影響するリスクを構造的に排除できます。
Step 6: Cloud Monitoring とバジェットアラートの設定
AIアプリケーションでコスト管理を怠ると、想定外の請求が発生します。Terraformでバジェットアラートと監視ダッシュボードを設定しておきましょう。
# modules/monitoring/main.tf
# Cloud Billing バジェットアラート
resource "google_billing_budget" "gemini_api_budget" {
billing_account = var.billing_account_id
display_name = "Gemini API Budget (${var.environment})"
budget_filter {
projects = ["projects/${var.project_number}"]
services = ["services/6F81-5844-456A"] # Cloud AI Platform
# 月初からリセット
calendar_period = "MONTH"
}
amount {
specified_amount {
currency_code = "JPY"
# 環境ごとに予算を変える
units = var.environment == "prod" ? "50000" : "5000"
}
}
# 70%, 90%, 100%で通知
threshold_rules {
threshold_percent = 0.7
spend_basis = "CURRENT_SPEND"
}
threshold_rules {
threshold_percent = 0.9
spend_basis = "CURRENT_SPEND"
}
threshold_rules {
threshold_percent = 1.0
spend_basis = "FORECASTED_SPEND"
}
all_updates_rule {
pubsub_topic = google_pubsub_topic.budget_alerts.id
schema_version = "1.0"
monitoring_notification_channels = [google_monitoring_notification_channel.email.name]
disable_default_iam_recipients = false
}
}
# Cloud Run のレスポンスレイテンシーアラート
resource "google_monitoring_alert_policy" "high_latency" {
display_name = "Gemini App High Latency (${var.environment})"
combiner = "OR"
conditions {
display_name = "Request latency > 10s"
condition_threshold {
filter = "resource.type=\"cloud_run_revision\" AND metric.type=\"run.googleapis.com/request_latencies\""
duration = "300s"
comparison = "COMPARISON_GT"
threshold_value = 10000 # 10秒(ミリ秒単位)
aggregations {
alignment_period = "60s"
per_series_aligner = "ALIGN_PERCENTILE_99"
}
}
}
notification_channels = [google_monitoring_notification_channel.email.name]
alert_strategy {
auto_close = "1800s"
}
}
# メール通知チャンネル
resource "google_monitoring_notification_channel" "email" {
display_name = "Ops Email (${var.environment})"
type = "email"
labels = {
email_address = var.ops_email
}
}
Step 7: GitHub Actions で Terraform CI/CD を自動化
インフラの変更を手作業で行う「ClickOps」から脱却し、プルリクエストベースのレビューフローを導入しましょう。
# .github/workflows/terraform.yml
name: Terraform CI/CD
on:
push:
branches: [main]
paths: ['gemini-ai-infra/**']
pull_request:
paths: ['gemini-ai-infra/**']
env:
TF_VERSION: "1.7.5"
WORKING_DIR: ./gemini-ai-infra
jobs:
terraform-check:
name: Terraform Validate & Plan
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
id-token: write # Workload Identity Federation用
steps:
- uses: actions/checkout@v4
# Google Cloud に Workload Identity Federation で認証
# (サービスアカウントキーファイル不要のセキュアな方法)
- id: auth
uses: google-github-actions/auth@v2
with:
workload_identity_provider: ${{ secrets.WIF_PROVIDER }}
service_account: ${{ secrets.WIF_SERVICE_ACCOUNT }}
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: ${{ env.TF_VERSION }}
cli_config_credentials_token: ${{ secrets.TF_CLOUD_TOKEN }}
- name: Terraform Format Check
run: terraform fmt -check -recursive
working-directory: ${{ env.WORKING_DIR }}
- name: Terraform Init
run: terraform init
working-directory: ${{ env.WORKING_DIR }}
- name: Terraform Validate
run: terraform validate
working-directory: ${{ env.WORKING_DIR }}
- name: Terraform Plan (Staging)
run: |
terraform workspace select staging
terraform plan \
-var-file="environments/staging/terraform.tfvars" \
-var="gemini_api_key=${{ secrets.GEMINI_API_KEY_STAGING }}" \
-out=tfplan
working-directory: ${{ env.WORKING_DIR }}
# PRにplanの結果をコメントとして投稿
- name: Post Plan to PR
uses: actions/github-script@v7
if: github.event_name == 'pull_request'
with:
script: |
const output = `#### Terraform Plan 📋
\`\`\`
${{ steps.plan.outputs.stdout }}
\`\`\`
*Pushed by: @${{ github.actor }}*`;
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: output
});
terraform-apply:
name: Terraform Apply (prod)
runs-on: ubuntu-latest
needs: terraform-check
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
environment: production # GitHub Environments で承認フローを設定
steps:
- uses: actions/checkout@v4
- id: auth
uses: google-github-actions/auth@v2
with:
workload_identity_provider: ${{ secrets.WIF_PROVIDER }}
service_account: ${{ secrets.WIF_SERVICE_ACCOUNT }}
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: ${{ env.TF_VERSION }}
- name: Terraform Apply (prod)
run: |
terraform init
terraform workspace select prod
terraform apply \
-var-file="environments/prod/terraform.tfvars" \
-var="gemini_api_key=${{ secrets.GEMINI_API_KEY_PROD }}" \
-auto-approve
working-directory: ${{ env.WORKING_DIR }}
Workload Identity Federation(WIF)の活用: サービスアカウントキーファイルをGitHub Secretsに保存する代わりに、WIFを使うことでキーレスな認証が実現します。これにより、キーの漏洩リスクを大幅に低減できます。
Step 8: Terraform State のロックとチーム開発
チームで同時にTerraformを実行すると、ステートファイルの競合が起きる危険があります。GCSバックエンドはCloud Spannerによるステートロックをサポートしており、自動的に排他制御が行われます。ただし、次のような運用ルールも合わせて設定しておくことをお勧めします。
まず、terraform plan のみをPRで実行し、terraform apply はmainブランチへのマージ後にのみ実行するというフローを確立することです。GitHub Environments機能を使って本番適用に承認フローを追加するとさらに安全です。
次に、terraform state コマンドの直接実行を制限することです。ステートの直接操作は緊急時のみに限定し、通常の変更はプルリクエスト経由で行うポリシーをチームで統一しましょう。
そして、terraform.tfvars ファイルをGitにコミットしないことです。機密情報が含まれるため、.gitignore に追加し、CI/CDパイプラインの環境変数またはTerraform Cloud Variables経由で渡すようにします。
よくあるエラーと対処法
エラー1: Error: Error creating Service: googleapi: Error 403
サービスアカウントに必要な権限が不足しています。google_project_iam_member リソースで適切なロールを付与しているか確認してください。特に roles/run.admin と roles/iam.serviceAccountUser が必要なケースが多いです。
# 現在の権限を確認
gcloud projects get-iam-policy YOUR_PROJECT_ID \
--flatten="bindings[].members" \
--filter="bindings.members:serviceAccount:YOUR_SA_EMAIL" \
--format="table(bindings.role)"
エラー2: Error: Error waiting for Creating CloudRun Service: context deadline exceeded
Cloud RunのデプロイはTerraformのデフォルトタイムアウト(10分)を超えることがあります。プロバイダーの timeouts ブロックで上限を延ばしてください。
resource "google_cloud_run_v2_service" "gemini_app" {
# ...
timeouts {
create = "20m"
update = "20m"
}
}
エラー3: Error: Secret "gemini-api-key-prod" not found
Secret Managerのシークレットが存在しないか、サービスアカウントにアクセス権限がありません。google_secret_manager_secret_iam_member リソースが正しく設定されているか確認してください。また、シークレットのバージョンが enabled 状態になっているかも確認が必要です。
個人開発者の視点から(実体験メモ)
全体を振り返って
TerraformでGemini APIアプリのインフラを管理することで、手作業によるミスの排除、環境間の一貫性確保、チーム開発での安全なインフラ変更フローを実現できます。本記事で紹介したパターンを活用して、あなたのAIアプリケーションの運用品質を一段階引き上げていただければ幸いです。
次のステップとして、Cloud Runへのデプロイパターンをさらに詳しく学びたい方はGemini 3.1 Pro × Cloud Run:サーバーレスAI APIの本番構築完全ガイドを、セキュリティ設計について深掘りしたい方はGemini API 本番環境セキュリティ完全ガイドをあわせてご覧ください。
インフラのコード化に関して