🔖

CloudRun + CloudSQL環境でNext.js動かしてみた

2024/02/15に公開

以前、Next.jsのApp RouterとPrisma,PandaCSSを使ってTODOアプリを作って検証してみたのですが、今回はそれをGoogle Cloud上にTerraformを使ってデプロイしたのでやり方をご紹介します。

https://zenn.dev/tokku5552/articles/nextjs-13-catchup

全体概要

以下はアーキテクチャ図です。

image

Artifact Registryにイメージをpushし、terraformでCloud RunCloud SQLを作成します。

リポジトリは以下です。

https://github.com/tokku5552/nextjs-gcp-sample

Terraformの実行準備とArtifact Registryの構築

gcloud CLIとTerraformのインストール

まずは事前準備としてterraformとgcloud CLIのインストールを行います。
詳細はここには記載しませんが、以下の公式ドキュメントに従ってインストールしてください。

  • gcloud CLIのインストール

https://cloud.google.com/sdk/docs/install?hl=ja

  • Terraformのインストール

https://developer.hashicorp.com/terraform/install

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を作成します。

https://cloud.google.com/sql/mysql?hl=ja

Google CloudでリレーショナルDBを扱いたい場合は、他にもCloud SpannerやAlloy DBなどの選択肢がありますが、今回はCloud SQLを選択しました。
比較については以下の公式ブログの記事が参考になります。

https://cloud.google.com/blog/ja/topics/developers-practitioners/your-google-cloud-database-options-explained

サンプルリポジトリで作成するには、以下のディレクトリにてterraform applyを行います。

  • terraform/resources/environments/dev/database

https://github.com/tokku5552/nextjs-gcp-sample/tree/main/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経由で実行します。

https://cloud.google.com/sql/docs/mysql/connect-auth-proxy?hl=ja#mac-m1

  • 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 --from=builder /app/next.config.js ./
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static

EXPOSE 3000
ENV PORT 3000
ENV HOSTNAME "0.0.0.0"

CMD ["node", "server.js"]

next.config.jsoutput: standaloneを指定してスタンドアロンモードでビルドしています。
また、Cloud RunではHOSTNAME "0.0.0.0"を指定する必要があることに注意してください。

https://cloud.google.com/run/docs/troubleshooting?hl=ja#container-failed-to-start

以下のコマンドでビルドしてプッシュを行います。

cd my-app
docker buildx build \
 --platform linux/amd64 \
 -t <tag> \
 -f ./Dockerfile \
 --push \
 .

<tag>の部分にはコンソールから取得できるURLを含める形でタグを指定します。
image

例えば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]
      }
    }
  }
}

volumescloudsqlとして接続名を指定することで、Unixソケットによる接続が行なえます。

https://cloud.google.com/sql/docs/mysql/connect-run?hl=ja#node.js

このように指定してやることで、/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