Cloud Run+Cloud SQLでPrisma in NestJSを動かすお話

## 概要

Nodeのバックエンド `NestJS` をどこにデプロイしようか考えた時、Serverlessをやりたいと思い、Cloud Runを使うことを決めました。
データベースについては、Cloud RunのオプションでサポートされているCloud SQLを使います。

Next.js⇒Vercelなどと違って明確に推奨されているデプロイ先が分かりませんでしたので、今回は一例として私が使ってみたCloud Runの手順を紹介いたします。

## 手順

### 1. 前準備
[本ページ](https://cloud.google.com/run/docs/quickstarts/build-and-deploy)に従って前準備を進めます。

- GCPプロジェクトの作成
- 課金の有効化
- Cloud SDKのインストール


### 2. APIの有効化
必要なAPIを有効化します。

- [Google Container Registry API](https://console.cloud.google.com/apis/library/containerregistry.googleapis.com?q=ontainer&id=345e3473-111d-4883-bae6-76295c034ed8)
- [Cloud Build API](https://console.cloud.google.com/apis/library/cloudbuild.googleapis.com?q=cloud%20build&id=9472915e-c82c-4bef-8a6a-34c81e5aebcc)
- [CloudRunAPI](http://console.cloud.google.com/apis/library/run.googleapis.com?_ga=2.163590393.-755609582.1558849277)
- [Cloud SQL](https://console.cloud.google.com/apis/library/sql-component.googleapis.com?q=cloud%20sql&id=41f27cc7-7e8b-49b8-a06a-0607cfdb935c)
- [Cloud SQL Admin API](https://console.cloud.google.com/apis/library/sqladmin.googleapis.com?q=cloud%20sql&id=bab64e93-7808-4c27-8027-e11c9ae2b5c6)

### 3. appのポートを変更する
(今回は事前にNestアプリケーションを作成している前提で進めます。)
私が試した限りでは、8080番以外のポートだと、この後のデプロイで失敗してしまいます。
環境変数を渡して8080番ポートを使用するように変更します。

```src/main.ts
// Expressの代わりにFastifyを使用しているのは、今回関係ありません
import { NestFactory } from '@nestjs/core';
import {
  FastifyAdapter,
  NestFastifyApplication,
} from '@nestjs/platform-fastify';
import { AppModule } from '@/modules/app.module';

async function bootstrap() {
  const app = await NestFactory.create<NestFastifyApplication>(
    AppModule,
    new FastifyAdapter(),
    {
      logger: ['log'],
    },
  );
  await app.listen(parseInt(process.env.PORT), '0.0.0.0');
}
bootstrap();
```

### 4. Dockerfileの作成
Dockerfileとそこで用いるシェルスクリプトを用意します。
私の場合`prisma generate` を忘れていて数日間ハマってしまったので、この記事を読んで下さった方は同じ思いをしないようにと願うばかりです...
(もうちょっとわかりやすいエラーログが欲しかったです。`Container called exit(0)`は少し汎用的すぎて特定がキツイです。)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478011/afe4f41c-1448-c710-427b-549efdd94a8a.png)


```Dockerfile
FROM node:12.19.1

WORKDIR /usr/src/app

COPY package*.json ./
RUN npm install
COPY . ./

EXPOSE 8080

RUN chmod +x ./start.sh
CMD  ["./start.sh"]
```
```start.sh
npm run prisma:generate # package.jsonで prisma generateをscript化
npm run prisma:migrate # 同じく prisma migrate dev --preview-featureをscript化
npm run start
```
### 5. Cloud Buildへのプッシュ

`start.sh` と `Dockerfile` を含むディレクトリから次のコマンドを実行します。
プロジェクトIDとサービス名はご自身のものに書き換えてください。

```
$ gcloud builds submit --project [プロジェクトID] --tag gcr.io/[プロジェクトID]/[サービス名]
```

終了したら、[Cloud Buildコンソール](https://console.cloud.google.com/cloud-build/builds)からビルドの成功が確認できます。

### 6. Cloud SQLインスタンスの作成
[Cloud SQL](https://console.cloud.google.com/sql)コンソールから新しいインスタンスを作成します。
今回私は開発環境に合わせて、Mysql8.0を選択しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478011/c7ec80d8-67e5-dcc5-86d4-b987ea3f565e.png)

インスタンスを作成したら、左サイドバー「ユーザー」「データベース」から、それぞれユーザーとデータベースを作成します。

### 7. Cloud Runへのデプロイ
[Cloud Runコンソール](https://console.cloud.google.com/run)からサービスの作成を進めます。
詳細については、以下のように設定します。

- 任意のregionとサービス名を入力して次へ
- コンテナ イメージに先ほどプッシュしたイメージを選択
- 詳細設定 => コンテナからメモリを増量(私の場合、256MiBだと落ちてしまったので、1GiBに設定しています)
- 詳細設定 => 変数に環境変数を追加
  - 名前に`DATABASE_URL`, 値に`mysql://[ユーザー名]:[パスワード]@localhost/[データベース名]?socket=/cloudsql/[プロジェクトID]:[Cloud SQLインスタンスのリージョン]:[インスタンス名]`を入力します
  - ここで指定したDBをprismaに読み込ませます
- 詳細設定 => 接続 => Cloud SQL 接続で作成したCloud SQLインスタンスを選択

デプロイが終了したらサービスのURLが確認できますので、実際にURLへとアクセスして動作確認をしてください。
### 8. cloudbuild.ymlを作成
ここまで完了したら、上記のデプロイ操作を自動化するフローを作成します。
適当なディレクトリ(拘りがなければルートディレクトリ)に、`cloudbuild.yaml`というCI/CDのステップを記載した構成ファイルを追加します。

```cloudbuild.yml
steps:
  - name: gcr.io/cloud-builders/docker
    args:
      - build
      - '--no-cache'
      - '-t'
      - '$_GCR_HOSTNAME/$PROJECT_ID/$REPO_NAME/$_SERVICE_NAME:$COMMIT_SHA'
      - '.'
    id: Build
  - name: gcr.io/cloud-builders/docker
    args:
      - push
      - '$_GCR_HOSTNAME/$PROJECT_ID/$REPO_NAME/$_SERVICE_NAME:$COMMIT_SHA'
    id: Push
  - name: gcr.io/google.com/cloudsdktool/cloud-sdk
    args:
      - run
      - deploy
      - $_SERVICE_NAME
      - '--platform=managed'
      - '--image=$_GCR_HOSTNAME/$PROJECT_ID/$REPO_NAME/$_SERVICE_NAME:$COMMIT_SHA'
      - '--region=$_DEPLOY_REGION'
      - '--allow-unauthenticated'
    id: Deploy
    entrypoint: gcloud
images:
  - '$_GCR_HOSTNAME/$PROJECT_ID/$REPO_NAME/$_SERVICE_NAME:$COMMIT_SHA'
```
Dockerfileのビルド => Cloud Buildへのプッシュ => Cloud Runへのデプロイの3ステップを実行しています。
Dockerfileを置く場所がルートディレクトリではなかったり、Dockerfileの名前を変えたかったりするときはこのように調整します。

```cloudbuild.yml
(対象箇所抜粋)
  - name: gcr.io/cloud-builders/docker
    args:
      - build
      - '--no-cache'
      - '-t'
      - '$_GCR_HOSTNAME/$PROJECT_ID/$REPO_NAME/$_SERVICE_NAME:$COMMIT_SHA'
      - packages/server
      - '-f'
      - packages/server/Dockerfile.prod
```

### 9. Cloud Run への継続的デプロイ設定
Cloud Run サービスの詳細 => 画面上部の継続的デプロイから、CloudBuildのトリガー作成画面を開きます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478011/2465911a-ca49-7a47-2803-eaa8b6d16dd7.png)

詳細については、以下のように設定します。

- ソースにGitHubを選択
- リポジトリに対象のリポジトリを選択
- ブランチを選択(メインブランチのpush時に絞った方が良いです。ただ、動作テスト時にだけ、全てのブランチを含めても良いかもしれません)
- ビルド構成に`Cloud Build 構成ファイル（yaml または json`を選択
- 代入変数の設定(cloudbuild.ymlで定義した変数の中身を記載します)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478011/8d64db56-c22a-e004-f77b-2e68c02cd1c4.png)

以上で、自動デプロイのフローが全て完了しました！
GitHubのメインブランチにpushすると、自動でCloud Runへのデプロイが走ります。

## 最後に
初めてコンテナデプロイに挑戦してみて、得られる学びが多かったです。
今までCMDとENTRYPOINTの違いとか何も知りませんでした(汗
若干起動が遅いような気もしますが、フルマネージドなServerlessなので、インスタンスの状態を気にしなくてすむのがとても良きです。

ここまで読んでくださりありがとうございました。

## 参考文献
- https://zenn.dev/terrierscript/articles/2020-06-10-cloud-run-cloud-sql-hasura-prisma
- https://qiita.com/fuku_tech/items/774c2c2f941bdfd2c5fd
- https://qiita.com/riku-shiru/items/d3f7dda5a5e87c4b26e9