Next.js アプリを Cloud Run へデプロイする(Secret Manager利用)
Next.jsアプリケーションのデプロイ先といえばVercelが筆頭ですが、他のプラットフォームでも起動できます[1]。この記事は Next.js のDocker Imageをつくり、Google Cloud の Cloud Run へデプロイしたときの記録です。なお、Next.jsはGraphQLを呼ぶServer Side Rendering(SSR)アプリを想定しています。
Terraform で Artifact Registry をつくる
Cloud Run へデプロイするにはコンテナイメージが必要です。そしてコンテナイメージを保存する場所は、Google Cloud の Artifact Registry がオススメです。以下を参考に Artifact Registry のリポジトリを作成してください
参考までに、こちらのリポジトリで今回用に改変したものを用意しています。
variable "gcp_project_id" {}
variable "artifact_registry_location" {
type = string
# https://cloud.google.com/storage/docs/locations
description = "Artifact Registry のロケーションをどこにするか"
}
# backendアプリケーション用の Artifact Registry リポジトリ
resource "google_artifact_registry_repository" "blog-backend-training-app" {
provider = google-beta
project = var.gcp_project_id
location = var.artifact_registry_location
repository_id = "blog-backend-training-app"
description = "バックエンドアプリケーション"
format = "DOCKER"
}
+# frontendアプリケーション用の Artifact Registry リポジトリ
+resource "google_artifact_registry_repository" "blog-frontend-training-app" {
+ provider = google-beta
+
+ project = var.gcp_project_id
+ location = var.artifact_registry_location
+ repository_id = "blog-frontend-training-app"
+ description = "フロントエンドアプリケーション"
+ format = "DOCKER"
+}
Dockerfile をつくる
イメージビルド用のDockerfileを作ります。Next.jsをビルドするためのDockerfileは、公式がコレ!というものを公開しているわけではなさそうです(あったら教えてください)。GitHub Discussions で見かけた記述を参考にさせていただきました。
FROM node:16 AS builder
ARG graphql_endpoint
# ビルドには devDependencies もインストールする必要があるため
ENV NODE_ENV=development
# アプリに埋め込む環境変数
ENV NEXT_PUBLIC_GRAPHQL_ENDPOINT=$graphql_endpoint
WORKDIR /app
COPY package.json ./
COPY yarn.lock ./
RUN yarn install
COPY . .
RUN yarn build
FROM node:16-stretch-slim AS runner
ENV NODE_ENV=production
WORKDIR /app
COPY package.json ./
COPY yarn.lock ./
# NODE_ENV=productionにしてyarn install(npm install)するとdevDependenciesがインストールされません
RUN yarn install
COPY /app/next.config.js ./
COPY /app/.next ./.next
COPY /app/public ./public
CMD ["yarn", "start"]
いくつかポイントを抜粋します。
マルチステージビルド
Docker Image のビルドでは、ビルド途中はさまざまなライブラリが必要なものの、実行ファイルは少量、途中インストールしたツールがムダになってしまうというシーンがあります。今回の例でいくと package.json
の devDependencies
がそれです。マルチステージビルドを使うと、ビルド時と成果物のイメージを分けることができます。FROM node:16 AS builder
はビルドで、FROM node:16-stretch-slim AS runner
は実行用です。後者ではENV NODE_ENV=production
としていることがわかります。
builder
の成果物は、
COPY /app/next.config.js ./
COPY /app/.next ./.next
COPY /app/public ./public
このようにしてrunner
へ渡します。
next.config.js
を本番ファイルへ含めることを忘れない
これを私は忘れまして、原因がわからぬエラーに悩まされました。next.config.js
でNextImage
のための画像ドメイン設定を入れていたのですが、このファイルを本番デプロイへ含めなかったために画像が表示されないという事象につながりました。原因がわからず、画像URLも合っているのに表示されなかったためかなり時間がかかってしまいました。本番イメージを軽くしようとしたのがアダに。チクショウ...
RUN yarn install
+ COPY /app/next.config.js ./
COPY --from=builder /app/.next ./.next
COPY --from=builder /app/public ./public
この部分ですね。みなさんは忘れないようにしてください。
NEXT_PUBLIC_XXX として公開する環境変数
Next.jsアプリでは、NEXT_PUBLIC_
というプレフィックスを環境名につけることで、成果物のJavaScriptファイルへ埋め込んでくれます。結果、ブラウザでその環境変数が使えます。注意点としてこの変数はビルド時に必要で、起動するタイミングでセットしてもブラウザは環境変数を使えません。Docker Image とする場合も同様で、ビルドのタイミングで環境変数として設定しています。
docker build
コマンド時に注入したいため、外部から変数を受け付けられるよう、ARG graphql_endpoint
を宣言しています。docker build
を次のように叩きます(cloudbuild.ymlは後述します)。
docker build \
--file=Dockerfile \
--build-arg=graphql_endpoint=$NEXT_PUBLIC_GRAPHQL_ENDPOINT \
.
ビルドする環境にNEXT_PUBLIC_GRAPHQL_ENDPOINT
変数があればそれを使ってdocker build
へ値を渡せます。
Cloud Build Trigger をつくる
トリガー自体は画面から作るでもTerraformで作るでも構いません。例としてTerraformでデプロイする場合、以下のようなモジュールを追加します。
resource "google_cloudbuild_trigger" "deploy-frontend-training-app" {
name = "deploy-frontend-training-app"
description = "Next.jsアプリを Cloud Run へdeployする"
github {
owner = var.github_owner
name = var.github_app_repo_name
push {
branch = "^main$"
}
}
included_files = ["blog-deploy-cloud-run/frontend/**"]
filename = "blog-deploy-cloud-run/frontend/cloudbuild.yml"
substitutions = {
_REGION = var.region
_SERVICE_ACCOUNT = var.cloud_run_service_account
_ARTIFACT_REPOSITORY_IMAGE_NAME = "${var.region}-docker.pkg.dev/${var.gcp_project_id}/${var.frontend_app_name}/blog-frontend"
}
}
main.ts から次のようにしてよびます。
locals {
backend_app_name = "blog-training-backend-app"
frontend_app_name = "blog-training-frontend-app"
}
# Cloud Build
# マイグレーション+バックエンドデプロイ
# フロントエンドデプロイ
module "cloud-build" {
source = "./modules/cloud-build"
gcp_project_id = var.gcp_project_id
region = var.primary_region
cloud_run_service_account = module.cloud-run.blog_training_app_runner_service_account
frontend_app_name = local.frontend_app_name
github_owner = "cm-wada-yusuke"
github_app_repo_name = "gql-nest-prisma-training"
}
terraform apply
すれば Cloud Build Trigger が作成されます。
cloudbuild.yml をつくる
steps:
+ - id: build-frontend
+ name: "gcr.io/cloud-builders/docker"
+ entrypoint: "bash"
+ args:
+ - -c
+ - >-
+ docker build
+ --file=Dockerfile
+ --build-arg=graphql_endpoint=$$GRAPHQL_ENDPOINT
+ --tag=$_ARTIFACT_REPOSITORY_IMAGE_NAME:$SHORT_SHA
+ --tag=$_ARTIFACT_REPOSITORY_IMAGE_NAME:latest
+ --cache-from=$_ARTIFACT_REPOSITORY_IMAGE_NAME:latest
+ .
+ secretEnv: ["GRAPHQL_ENDPOINT"]
dir: "blog-deploy-cloud-run/frontend"
- id: push-frontend
name: "docker"
args:
- push
- --all-tags
- $_ARTIFACT_REPOSITORY_IMAGE_NAME
dir: "blog-deploy-cloud-run/frontend"
waitFor: ["build-frontend"]
- id: deploy-frontend
name: gcr.io/cloud-builders/gcloud
args:
- beta
- run
- deploy
- training-frontend
- --quiet
- --platform=managed
- --project=$PROJECT_ID
- --region=$_REGION
- --image=$_ARTIFACT_REPOSITORY_IMAGE_NAME:$SHORT_SHA
- --service-account=$_SERVICE_ACCOUNT
- --revision-suffix=$SHORT_SHA
- --tag=latest
- --concurrency=40
- --cpu=1
- --memory=512Mi
- --max-instances=3
- --min-instances=0
- --no-use-http2
- --allow-unauthenticated
- --no-cpu-throttling
- --ingress=all
+ - --update-secrets=GRAPHQL_ENDPOINT=BLOG_TRAINING_GRAPHQL_ENDPOINT:latest
dir: "blog-deploy-cloud-run/frontend"
waitFor: ["push-frontend"]
timeout: 2000s
substitutions:
_REGION: by-terraform
_ARTIFACT_REPOSITORY_IMAGE_NAME: by-terraform
_SERVICE_ACCOUNT: by-terraform
+availableSecrets:
+ secretManager:
+ - versionName: projects/$PROJECT_ID/secrets/BLOG_TRAINING_GRAPHQL_ENDPOINT/versions/latest
+ env: GRAPHQL_ENDPOINT
# ビルド結果に生成したイメージ情報を表示する
# https://cloud.google.com/build/docs/building/build-containers
images:
- $_ARTIFACT_REPOSITORY_IMAGE_NAME:$SHORT_SHA
こちらもポイントを抜粋します。
Secret Manager からシークレット値を呼び出し
availableSecrets
を使うことで、Secret Manager の値をビルド時の環境変数として展開できます。それを使っているのがさきほど docker build
で使ったこの部分です。
- id: build-frontend
name: "gcr.io/cloud-builders/docker"
entrypoint: "bash"
args:
- -c
- >-
docker build
--file=Dockerfile
+ --build-arg=graphql_endpoint=$$GRAPHQL_ENDPOINT
--tag=$_ARTIFACT_REPOSITORY_IMAGE_NAME:$SHORT_SHA
--tag=$_ARTIFACT_REPOSITORY_IMAGE_NAME:latest
--cache-from=$_ARTIFACT_REPOSITORY_IMAGE_NAME:latest
.
+ secretEnv: ["GRAPHQL_ENDPOINT"]
Secret Manager から読み出すときは、ビルドステップの bash
エントリーポイントから使う必要があります。よくみるとbash
コマンドの中ではgraphql_endpoint=$$GRAPHQL_ENDPOINT
のようにダラーふたつ$$
指定していますね。docker build
の実行環境で環境変数として展開されているため、このような使い方になります。Cloud Build と Secret Managerのからみはcatnoseさんの記事がよくまとまっているのでそちらも参照ください。
Cloud Run の環境変数とSecret Managerをマウント
Cloud Run へのデプロイコマンド、この部分です。
--update-secrets=GRAPHQL_ENDPOINT=BLOG_TRAINING_GRAPHQL_ENDPOINT:latest
こちらは、ビルド時に埋め込むタイプではなく、Cloud Runの実行環境で展開してくれるオプションです。なので、さきほどdocker build
で使った方法とは設定方法が異なるため注意してください。
Secret Manager へ GRAPHQL_ENDPOINT 値を登録
あとは、ビルドと実行で必要なGraphQLのエンドポイントを Secret Manager へ登録します。GraphQLを使う場合、SSRアプリを想定しているのでgetServerSideProps
からGRAPHQL_ENDPOINT
を使うことになりそうですが、フォーム入力なども考慮するとブラウザからもよびたいところです。というわけでcloudbuild.ymlにて NEXT_PUBLIC_GRAPHQL_ENDPOINT
も設定しています。"Secret"ではなくなりますが、環境変数の設定をビルド・デプロイ時まで遅延させるサンプルとして活用いただければと思います。
Secret Manager への登録方法はいろいろありますが、一般的にSecret Managerへ登録するということは秘匿情報なので手作業が安全ではなかろうかという考えです。画面から登録します。
- 名前:
BLOG_TRAINING_GRAPHQL_ENDPOINT
- シークレットの値: GraphQL バックエンドのURL
- 例:
https://training-backend-xxxxxxxxx-uc.a.run.app/graphql
- 例:
ビルド実行
Cloud Build から Trigger を起動します。
Cloud Run にデプロイされることを確認してください。
(この記事ではバックエンドに言及していませんが)バックエンドのGraphQLとつながってデータが読み出せればOKです。
おわりに
Next.jsはデプロイ先を公式でサポートしているということもあり、Vercelのノウハウが多いです。他のプラットフォームへデプロイするにあたり、Docker Image として構築できると取り回しやすく便利かと思いためしました。また、Cloud Run へ実際にデプロイして意図どおり動作することを確認しました。どなたかの参考になれば幸いです。
参考
ソースコード
この記事で使ったソースコードはGitHubで公開しています。
-
ISRなどのVercelプラットフォームならでは機能が一部使えない点にご注意ください。参考:Next.jsアプリをVercelからGoogle Cloudに移行した話 ↩︎
Discussion