コンテナー化したNext.jsをGoogle CloudにデプロイするはじめてのCI/CDパイプライン

# 1. はじめに
これまでNext.jsのデプロイはVercelを使っていてほとんどホスティングの環境について考えることがなかったのですが、この度Google Cloudを使うことになり、少々苦戦したので忘備録も兼ねて整理してみようと思います。

# 目次
- [1. はじめに](#1-はじめに)
- [2. 全体像](#2-全体像)
    - [2-1. CI/CDパイプライン](#2-1-cicdパイプライン)
    - [2-2. Google CloudにおけるCI/CDパイプライン](#2-2-google-cloudにおけるcicdパイプライン)
- [3. dockerfileとcloudbuild.yaml](#3-dockerfileとcloudbuildyaml)
- [4. Cloud Run サービスとCloud Build](#4-cloud-run-サービスとcloud-build)
    - [4-1. 基本的な設定手順](#4-1-基本的な設定手順)
    - [4-2. トリガーの作成](#4-2-トリガーの作成)
- [5. 補足](#5-補足)
- [6. おわりに](#6-おわりに)
- [参考](#参考)

# 2. 全体像
Vercelを使っていると、GithubのURLとリポジトリを指定して環境変数を設定すればそのままNext.jsが動いていたのでほとんど意識していませんでしたが、デプロイに関してなかなか面倒なプロセスがあまり意識しなくてもいいように設計されていたようです。Google Cloudを使うにあたってそれを意識しないといけなかったので、どういうシステムが背後にあったのか調べることになりました。以下、その全体像をお示しします。

## 2-1. CI/CDパイプライン
**Continuous Integration（継続的統合）**
ソースコードの変更を継続的に統合・テストするプロセスを意味します。例えば、Githubなどのリモートリポジトリがpushされて更新されたならば、CIサーバーが変更を検知してマージして自動的にビルドします。ビルドログなどを残せばビルドが失敗した時に原因を調べられるでしょう。

**Continuous Delivery（継続的デリバリー）**
マージやビルドが行われ準備が整った後、自動的にデプロイしてリリースするプロセスを意味します。CDはCIも含む概念としてみるのが基本のようですが、CDを自動ビルド、自動デプロイの狭い意味として捉えることもあるようです。

このような一連の長い作業が自動化されることによって、更新のたびに行わなければならない長く複雑な処理から解放されます。
以下、Google Cloudにおいては具体的にどういう流れでCI/CDパイプラインができているのか見てみましょう。

※この節の参考記事

https://qiita.com/shinkai_/items/13e76a5641d9ec9a41d2


## 2-2. Google CloudにおけるCI/CDパイプライン
今回のプロジェクトではCloud Build, Cloud Run, Artifacts Registry を使うことによってCI/CDパイプラインを実装する手段を採用しました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2826164/140b2440-de85-e35c-2c3d-5ef6d45a4e82.png)

（画像：「Cloud Run サービスのデプロイのよくあるパターン 3 選」より）

Cloud buildはGithubの変更がトリガーとなり自動的にdockerイメージのビルドに取り掛かります。ここで作られたイメージはArtifact Registryに送られ、Cloud Runにデプロイされる際にそのイメージがダウンロードされます。このようにして、Githubに加えられた変更をもとにリリースまで自動化されるシステムがつくられています。

変更をマージ、ビルドするのがCloud BuildでCIを担当し、デプロイ・リリースするのがCloud RunでCDを担当するのだとすると、その傍流にArtifact RegistryなるものがあるのがGoogle Cloudの特徴になると思います。
これはDockerイメージのリポジトリーであり、Google CloudにDockerイメージをもとにサービスをホスティングする場合は必要な倉庫です。

Google CloudにおけるCI/CDの構築手段としては上記の他に、コードのリポジトリーをGithubからCloud Sorce Repositryにする方法、即ちリポジトリもGoogle Cloudにする方法や、逆にCloud Buildの代わりにGithub Actionsを使う方法、即ちCIもGithubにする方法などもあるようです。

※この節の参考記事
デプロイの方法が包括的に解説されてあるので、やり方が多くて迷った時は参考になると思います。

https://zenn.dev/google_cloud_jp/articles/cloudrun-deploy-pattern#google-cloud-%E3%81%A7-ci%2Fcd-%E3%81%99%E3%82%8B%E3%83%91%E3%82%BF%E3%83%BC%E3%83%B3

# 3. dockerfileとcloudbuild.yaml
Dockerfileだけでもデプロイできますが、今回は環境変数を使わなければならない関係上cloudbuild.yamlファイルも作りました。

まず、Next.jsをコンテナー化するDockerfileの記述を紹介します。
これは公式サンプルをもとに記述しました。
```Dockerfile
FROM node:22.11.0-slim AS base

FROM base AS deps
RUN apt-get update && apt-get install -y libc6 && rm -rf /var/lib/apt/lists/*
WORKDIR /app

COPY package.json yarn.lock* package-lock.json* ./
RUN \
  if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
  elif [ -f package-lock.json ]; then npm ci; \
  else echo "Lockfile not found." && exit 1; \
  fi

FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .

RUN yarn build


FROM base AS runner
WORKDIR /app

ENV NODE_ENV=production

RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs

COPY --from=builder /app/public ./public

RUN mkdir .next
RUN chown nextjs:nodejs .next

COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static

USER nextjs

EXPOSE 3000

ENV PORT=3000

ENV HOSTNAME="0.0.0.0"

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

次に、cloudbuild.yamlファイルの記述を紹介します。
例えばVercelにおいては、環境変数を記述すればそれがそのままコードの中で用いられたのですが、Google Cloudの場合はトリガーなどに環境変数を設定してもそれがそのまま使われることはありません。コンテナーイメージが作られる際に環境変数を取り込ませなければならないのですがDockerfileに直書きすることもできないので、cloudbuild.yamlを通してトリガーの作成の際に設定した環境変数をイメージのビルドに渡すようにしました。

```cloudbuild.yaml
steps:
  - name: "gcr.io/cloud-builders/docker"
    entrypoint: "bash"
    args:
      - "-c"
      - |
        echo "AUTH_SECRET=${_AUTH_SECRET}" >> .env

        docker build -f Dockerfile -t gcr.io/$PROJECT_ID/[cloud run service name]:$COMMIT_SHA .

  - name: "gcr.io/cloud-builders/docker"
    args: ["push", "gcr.io/$PROJECT_ID/[cloud run service name]:$COMMIT_SHA"]

  - name: "gcr.io/cloud-builders/gcloud"
    args:
      - "run"
      - "deploy"
      - "[cloud run service name]"
      - "--image"
      - "gcr.io/$PROJECT_ID/[cloud run service name]:$COMMIT_SHA"
      - "--region"
      - "asia-northeast1"
images:
  - "gcr.io/$PROJECT_ID/[cloud run service name]:$COMMIT_SHA"
timeout: 900s

options:
  logging: CLOUD_LOGGING_ONLY
```

トリガーの編集が必要なのですが、まだCloud Runも作っていないので、また後で説明を加えます。

※この章の参考

https://github.com/vercel/next.js/tree/canary/examples/with-docker


https://zenn.dev/igz0/articles/76c3214392986f

# 4. Cloud Run サービスとCloud Build
今回はCloud Run サービスを使います。
先述したように、Cloud RunサービスがCDにあたり、Cloud BuildがCIにあたります。

## 4-1. 基本的な設定手順
Google Runの設定ページを開いて「リポジトリを接続」を選択します。あたかもGoogle Runに直接Githubが接続されるかのように思われそうですが（私は最初そう勘違いしていました）、間にGloud Buildが入ります。その証拠に、Cloud Buildの設定が必要です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2826164/912f03bf-33d0-96d9-0d66-8b4e63ca2f3c.png)

Cloud Buildの設定では、ソースリポジトリの選択を行います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2826164/09cbd6b1-7f2d-bd26-5cfd-574452ca409a.png)

次にビルド構成の設定をすれば、cloud buildのひとまずの設定は完了します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2826164/bba0617f-77ad-98a6-da63-31408a044bda.png)

最後に、Cloud Run サービスの設定を行います。
サービス名は任意のものを書いてください。ただし、cloudbuild.yamlファイルに記述したものと一致する必要があります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2826164/13e475e5-f3a9-be57-79e3-72b31c1f78ce.png)

公開サービスにするときには「未認証の呼び出しを許可」し、処理の減少のためにCPUの割り当ては「リクエストの処理中にのみCPUを割り当てる」を選び、サービスのスケーリングは0にします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2826164/b13b722f-8c06-0a8c-6ac5-92a966e93b90.png)

「コンテナの編集」では、Dockerfileで設定したポート番号を記述してください。今回の場合では3000番です。

※この節の参考記事
より詳しい設定の解説などはこの記事を参考にしていただければと思います。

https://zenn.dev/google_cloud_jp/articles/5104d1d1f28560


## 4-2. トリガーの作成
先程の設定ではトリガーの詳しい設定ができませんでした。
コンソール画面にも、「サービスが作成された後で、Cloud Build トリガーが作成されます」というポップアップが出てきたかと思います。

このため、改めてトリガーを編集していこうと思います。
トリガーを発火させるイベントは、「ブランチへのpush」を選択しました。このとき、Githubのリモートリポジトリにpushした時点でマージ・ビルド、デプロイ・リリースまでが一貫して自動的に行われることになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2826164/1ae0a321-21e4-3ef0-ba44-7355c9cc2a46.png)

ブランチ名はここではmainとしていますが、GitFlowではmain -> release -> develop -> feature, fix ブランチなどと流れていきますので、例えばrelease/v1などとすることもできます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2826164/a7040f07-a142-cd0a-8f17-72ae74928d63.png)

構成はcloudbuild.yamlファイルを選ぶようにしてください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2826164/c76750d9-d7ae-5bb7-7bbf-de37b4ef12bc.png)

3章で設定していた環境変数の値を「代入変数」の個所で設定します。代入する変数にはアンダーバーを必ずつけなければならないため、cloudbuild.yamlの変数にも`        echo "AUTH_SECRET=${_AUTH_SECRET}" >> .env`のようにアンダーバーを付けていました。
プロジェクトにおいては.envファイルに環境変数を設定することと思いますが、その値はcloudbuild.yamlファイルとトリガーの両方に設定しなければならないということです。記述の間違いがエラーの原因になりうるため、十分に注意してください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2826164/32c4c07a-9ee5-1653-5915-19045c0801e3.png)


# 5. 補足
今回はcloud run サービスの作成の際に同時にcloud buildの設定も行いましたが、完全に別にすることもできます。
その時は、
1．cloud run サービスの新規作成の際にコンテナーを選択し、サンプルコンテナーを設定します。（その他の設定は同じです）
2．cloud buildにおいてトリガーの作成を行います。（各種設定は同じです）
このとき、サンプルコンテナーがcloud buildにおいて作られたコンテナーに置き換わるため、cloud buildとcloud runが繋がります。

# 6. おわりに
Google Cloudに触れたのは初めてで、同時にCI/CDパイプラインの考え方を知ったのも初めてだったので、とても新鮮でした。
この記事を作るにあたって改めてもう一度知ることができ、ある程度丁寧に整理することができたので、よかったと思います。
この記事がはじめてパイプラインを作る人の参考になれば幸いです。

### 参考

**2章**
2-2: [今さら聞けないCI/CD（継続的インテグレーション/継続的デリバリー）とは](https://qiita.com/shinkai_/items/13e76a5641d9ec9a41d2)
2-3: [Cloud Run サービスのデプロイのよくあるパターン 3 選](https://zenn.dev/google_cloud_jp/articles/cloudrun-deploy-pattern#google-cloud-%E3%81%A7-ci%2Fcd-%E3%81%99%E3%82%8B%E3%83%91%E3%82%BF%E3%83%BC%E3%83%B3)

**3章**
[nextjsコンテナー化公式サンプル](https://github.com/vercel/next.js/tree/canary/examples/with-docker)
[Next.jsをCloud Runにデプロイする際にビルド時の環境変数を設定する](https://zenn.dev/igz0/articles/76c3214392986f)

**4章**
[徹底解説！Cloud Run サービス作成（コンソール画面）](https://zenn.dev/google_cloud_jp/articles/5104d1d1f28560)