CloudRun + CloudSQL環境でNext.js動かしてみた
以前、Next.jsのApp RouterとPrisma,PandaCSSを使ってTODOアプリを作って検証してみたのですが、今回はそれをGoogle Cloud上にTerraformを使ってデプロイしたのでやり方をご紹介します。
全体概要
以下はアーキテクチャ図です。
Artifact Registryにイメージをpushし、terraformでCloud Run
とCloud SQL
を作成します。
リポジトリは以下です。
Terraformの実行準備とArtifact Registryの構築
gcloud CLIとTerraformのインストール
まずは事前準備としてterraformとgcloud CLIのインストールを行います。
詳細はここには記載しませんが、以下の公式ドキュメントに従ってインストールしてください。
- gcloud CLIのインストール
- Terraformのインストール
homebrewを使っている場合は以下のコマンドでもインストールすることができます。
# gcloud CLI
brew install --cask google-cloud-sdk
# terraform
brew tap hashicorp/tap
brew install hashicorp/tap/terraform
インストールが済んだら、gcloud CLIを初期化しておきます。
gcloud init
tfstate保管用のCloud Storage作成
tfstateをgcs(Google Cloud Storage)に置きたいため、バケットを作成しておきます。
# gcloud cliでのログイン
gcloud auth login
gcloud projects list
gcloud config set project <PROJECT ID>
gcloud auth application-default login
# バケットの作成
gsutil mb gs://<BUCKET_NAME>
作成できたかどうかは以下のコマンドで一覧表示して確認できます。
gsutil ls
TerraformのバックエンドとしてGCSを指定するには、例えば以下のように指定します。
terraform {
required_providers {
google = {
source = "hashicorp/google"
version = "~> 4.0"
}
}
backend "gcs" {
bucket = "<BUCKET NAME>"
prefix = "artifact-registry"
}
}
Artifact Registoryの構築
実際にArtifact Resistoryを構築しますがtfファイルに必要なリソース定義は以下だけです。
resource "google_artifact_registry_repository" "app" {
project = var.project_id
location = var.region
repository_id = "${var.environment}-${var.project}-app"
format = "DOCKER"
}
formatにはDOCKER
を指定しておきます。
これで例えば以下のようなコマンドを打てば構築できます。(Terraformの詳細な実行方法等は例えば公式チュートリアルなどを御覧ください。)
terraform init
terraform plan
terraform apply --auto-approve
Cloud SQLの作成
次にGoogle CloudのマネージドRDBサービスである、Cloud SQLを作成します。
Google CloudでリレーショナルDBを扱いたい場合は、他にもCloud SpannerやAlloy DBなどの選択肢がありますが、今回はCloud SQLを選択しました。
比較については以下の公式ブログの記事が参考になります。
サンプルリポジトリで作成するには、以下のディレクトリにてterraform apply
を行います。
terraform/resources/environments/dev/database
Terraformを用いたCloud SQLの作成
resource "google_sql_database_instance" "db" {
name = "${var.environment}-${var.project}-db-instance"
database_version = "MYSQL_8_0"
region = var.region
deletion_protection = false # 検証用のため削除保護は無効化
settings {
tier = "db-f1-micro"
availability_type = "REGIONAL"
disk_size = "20" # minimumは10GB
disk_type = "PD_SSD" # default
# Only Standalone Instance for HA enabled
backup_configuration {
enabled = true
binary_log_enabled = true
}
}
}
# databaseを作成
resource "google_sql_database" "db" {
name = "${var.environment}-${var.project}-db"
instance = google_sql_database_instance.db.name
}
# 接続ユーザー
resource "google_sql_user" "db" {
name = "${var.environment}-${var.project}-db-user"
instance = google_sql_database_instance.db.name
host = "%" # すべてのホストという意味
password = var.db_password
}
settingsの項目は検証用なので気にせずSSDを指定していますが、料金に関わりますので注意してください。
Cloud SQLの作成は少し時間がかかります。
ローカルから接続してmigrationの実施
Cloud SQLが作成できたら、prismaで作成していたマイグレーションファイルを実行します。
今回は簡単のため、Public IPを有効にしているので、ローカルからCloud SQL Auth Proxy経由で実行します。
- Cloud SQL Auth Proxyのインストール
curl -o cloud-sql-proxy https://storage.googleapis.com/cloud-sql-connectors/cloud-sql-proxy/v2.6.1/cloud-sql-proxy.darwin.arm64
chmod +x cloud-sql-proxy
- INSTANCE_CONNECTION_NAMEの取得
コンソールから確認できるインスタンスIDをINSTANCE_NAMEに指定して、INSTANCE_CONNECTION_NAMEを取得します。
gcloud sql instances describe <INSTANCE_NAME> --format='value(connectionName)'
- Cloud SQL Auth Proxyの起動
上記のコマンドで取得したINSTANCE_CONNECTION_NAMEを使ってCloud SQL Auth Proxyを起動します。
./cloud-sql-proxy --port 3306 <INSTANCE_CONNECTION_NAME>
上記で起動できたら、mysqlコマンドで接続確認を行います。
# mysqlコマンドがインストールされていない場合
brew install mysql-client
mysql -u <接続ユーザー名> -p --host 127.0.0.1
Enter password:
と表示されるので、設定したパスワードを入力して接続します。
次に、migrationを実行するために、prismaの接続設定を書き換えます。
サンプルリポジトリのプロジェクトルートからmy-app
に移動して、.env
ファイルを作成します。
cd my-app
yarn install
cp -pr .env.sample .env
DATABASE_URL
に指定するのは以下のような文字列です
DATABASE_URL="mysql://<接続ユーザー>:<パスワード>@localhost:3306/<作成したデータベース名>"
上記まで完了したら以下のコマンドでmigrateが行なえます。(実際のコマンドはpackage.jsonを参照ください。)
yarn db:generate
yarn db:migrate
Cloud Runの作成とデプロイ
最後にCloud Runの作成を行います。
事前にビルドしてArtifact Registoryにpush
Cloud RunへはArtifact Registoryに事前にpushしたイメージをterraform apply
時に参照してデプロイします。
Dockerfileは以下です。
FROM node:20.11.0-alpine as builder
ENV NODE_ENV=development
WORKDIR /app
COPY ./package.json ./
COPY ./panda.config.ts ./
COPY ./postcss.config.cjs ./
COPY ./yarn.lock ./
RUN yarn install
COPY . .
RUN yarn prisma generate
RUN yarn build
FROM node:20.11.0-alpine as runner
ENV NODE_ENV=production
WORKDIR /app
COPY /app/next.config.js ./
COPY /app/public ./public
COPY /app/.next/standalone ./
COPY /app/.next/static ./.next/static
EXPOSE 3000
ENV PORT 3000
ENV HOSTNAME "0.0.0.0"
CMD ["node", "server.js"]
next.config.js
でoutput: standalone
を指定してスタンドアロンモードでビルドしています。
また、Cloud RunではHOSTNAME "0.0.0.0"
を指定する必要があることに注意してください。
以下のコマンドでビルドしてプッシュを行います。
cd my-app
docker buildx build \
--platform linux/amd64 \
-t <tag> \
-f ./Dockerfile \
--push \
.
<tag>
の部分にはコンソールから取得できるURLを含める形でタグを指定します。
例えばasia-northeast1
でプロジェクトIDがproject-id
、リポジトリがrepository-name
の場合、アプリケーション名をapp
、タグをlatest
として以下のようになります。
asia-northeast1-docker.pkg.dev/project-id/repository-name/app:latest
Cloud Runの作成
今回はtfvars
に上記のimage urlを逃してvar.image_name
で参照しています。
resource "google_cloud_run_v2_service" "app" {
name = "app"
location = var.region
template {
timeout = "300s"
max_instance_request_concurrency = 50
containers {
image = var.image_name
resources {
limits = {
"memory" : "512Mi",
"cpu" : "1"
}
}
ports {
container_port = 3000
}
}
scaling {
min_instance_count = 0
max_instance_count = 5
}
volumes {
name = "cloudsql"
cloud_sql_instance {
instances = [var.db_connection_name]
}
}
}
}
volumes
にcloudsql
として接続名を指定することで、Unixソケットによる接続が行なえます。
このように指定してやることで、/cloudsql
にソケットが作成されるので、.env
ファイルの接続名を以下のようにして再度プッシュしてからapplyします。
mysql://user:password@localhost/db_name?socket=/cloudsql/INSTANCE_CONNECTION_NAME
min_instance_count
を0にすることでコールドスタートとなります。
まとめ
最も簡単な構成(Cloud RunからはPublic IPで接続)で作成しましたがハマりポイントが結構多くて苦労しました。
ここからさらに、Private IPでの接続や、Cloud Buildによるデプロイ、マイグレーションも検証してそのうち記事にしようと思います。
Discussion