🐳

TerraformでCloud RunにNestJSバックエンドをデプロイしてみる - 実践編

に公開

本記事のサマリ

前回のTerraform入門に続き、実際にNestJS GraphQL APIをCloud Runにデプロイしてみました。AIにTerraformコードを生成させて、それを読み解きながら実装していく過程で、Apple SiliconでのDockerビルド問題や組織ポリシーによるIAM制限など、実際のつまずきポイントも含めて記録しています。

今回のコードは下記のリポジトリにあります。

https://github.com/toto-inu/lab-202511-cloudrun-terraform

前回の振り返り

前回の記事では、Terraformの基礎を学びました。

  • HCL構文: provider、resource、variable、outputの基本的な書き方
  • Google Cloud認証: gcloud CLIとApplication Default Credentialsの設定
  • 初回リソース作成: GCSバケットを例にした基本的なリソース管理
  • Terraformワークフロー: terraform init → plan → apply の流れ
  • 状態管理: terraform.tfstateファイルの重要性

https://zenn.dev/stellarcreate/articles/terraform-gcp-intro

今回はこの知識を活用して、実際のアプリケーションをデプロイしてみます。

今回やること

今回は、NestJSで作ったGraphQL APIをCloud Runにデプロイします。プロジェクトID lab-202511-cloudrun-terraform、リージョン asia-northeast1で進めていきます。

具体的には:

  • Artifact Registry: Dockerイメージ保存用リポジトリの作成
  • Cloud Run: NestJS GraphQL APIの実行環境
  • デプロイフロー: TerraformでのインフラからDockerイメージのデプロイまで

なぜCloud Runなのか

バックエンドAPIの選択肢は色々ありますが、Cloud Runを選んだ理由:

  • スケールゼロ: リクエストがない時は0にスケールダウン
  • コスト効率: AWSのFargateは最小1インスタンスだが、Cloud Runは完全に0まで下がる
  • シンプル: コンテナを用意すれば動く、Kubernetesの知識不要
  • 従量課金: 実際に処理している時間だけ課金

特に個人プロジェクトでは、このコスト効率の良さが魅力的でした!

全体構成

今回の構成は簡潔に下記の通りです。

NestJS GraphQL API → Artifact Registry → Cloud Run
  • NestJS: Todo管理のGraphQL API
  • Artifact Registry: Dockerイメージの保管場所
  • Cloud Run: コンテナの実行環境

Terraformコードを書いてみる

まずは必要なリソースを定義していきます。前回学んだprovider、resource、variableの知識を使って書いてみました:

resource "google_artifact_registry_repository" "main" {
  location      = var.region
  repository_id = "${var.service_name}-repo"
  description   = "Docker repository for ${var.service_name}"
  format        = "DOCKER"
}

resource "google_cloud_run_service" "main" {
  name     = var.service_name
  location = var.region

  template {
    spec {
      containers {
        image = "${var.region}-docker.pkg.dev/${var.project_id}/${google_artifact_registry_repository.main.repository_id}/${var.service_name}:latest"
    
        ports {
          container_port = 3000
        }

        resources {
          limits = {
            cpu    = "1000m"
            memory = "512Mi"
          }
        }
      }
    }
  }

  traffic {
    percent         = 100
    latest_revision = true
  }
}

書いたコードの各オプションが何を意味するのか、公式ドキュメントを確認しながら理解を深めていきました。

Terraform Registryで設定オプションを調べる

Artifact Registry

https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/artifact_registry_repository

ドキュメントを見ると、主なオプションは:

  • format: DOCKER、MAVEN、NPMなどのリポジトリ形式
  • cleanup_policies: 古いイメージの自動削除ルール
  • labels: タグ付け

今回は format = "DOCKER"を指定して、Dockerイメージ用のリポジトリを作成します。

Cloud Run

https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/cloud_run_service

オプションが驚くほど多い!重要なポイント:

  • resources: CPU 1000m(1コア)、メモリ 512Mi(512MiB)
  • traffic: 新バージョンへの100%トラフィック配分
  • image: Artifact Registryの完全パスを文字列テンプレートで生成

NestJS側の準備

Dockerfile作成

FROM node:18-alpine AS builder
WORKDIR /app
RUN apk add --no-cache yarn
COPY package.json yarn.lock ./
RUN yarn install

FROM node:18-alpine AS runner
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY . .
RUN mkdir -p /app/src && chown -R node:node /app
USER node
RUN yarn build

EXPOSE 3000
CMD ["yarn", "start:prod"]

PORT環境変数対応

Cloud Runは PORT環境変数でポートを指定するため、NestJSで対応が必要:

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  const port = process.env.PORT || 3000;
  await app.listen(port, '0.0.0.0');
}

実際にデプロイしてみる

Terraformでインフラ作成

terraform init
terraform plan
terraform apply

Dockerイメージのビルド

ここで最初のつまずき。Apple Silicon環境では:

# これは失敗する
docker build -t asia-northeast1-docker.pkg.dev/lab-202511-cloudrun-terraform/nestjs-todo-api-repo/nestjs-todo-api:latest ./app

# 正解:プラットフォーム指定が必要
docker build --platform linux/amd64 \
  -t asia-northeast1-docker.pkg.dev/lab-202511-cloudrun-terraform/nestjs-todo-api-repo/nestjs-todo-api:latest \
  ./app

Apple SiliconではARMアーキテクチャでビルドされるため、Cloud Run用に明示的に linux/amd64を指定する必要がありました。

プッシュとデプロイ

docker push asia-northeast1-docker.pkg.dev/lab-202511-cloudrun-terraform/nestjs-todo-api-repo/nestjs-todo-api:latest

プッシュ完了後、Cloud Runが自動的に新しいリビジョンをデプロイしました👍

つまずいたポイント

1. アーキテクチャ不一致

問題: Apple Silicon環境でビルドしたイメージがCloud Runで動かない
解決: --platform linux/amd64フラグでx86_64アーキテクチャを明示

2. 組織ポリシー制限

問題: Terraformで allUsersのIAM設定時に「One or more users named in the policy do not belong to a permitted customer」エラー
解決: 認証付きアクセスで運用することに決定

認証付きでのテスト:

curl -H "Authorization: Bearer $(gcloud auth print-identity-token)" \
  https://nestjs-todo-api-5szbrnrtva-an.a.run.app/graphql \
  -H "Content-Type: application/json" \
  -d '{"query":"{ todos { id title completed } }"}'

結果、GraphQL APIが正常に動作していることを確認できました!

この辺りは以前書いた、Google Cloudのゼロトラストの思想なのかも。

https://zenn.dev/stellarcreate/articles/google-cloud-zero-trust-vs-aws-security-comparison

まとめ

前回学んだTerraformの基礎知識を活かして、実際にNestJS GraphQL APIをCloud Runにデプロイできました。Apple SiliconでのDockerビルド問題や組織ポリシー制限など、ドキュメントだけでは分からない実践的な課題も経験できました。

次のステップでは、Secret ManagerやCI/CDパイプラインの構築に挑戦してみたいと思います✨

株式会社StellarCreate | Tech blog📚

Discussion