Cloud Buildを使ってCloud Runに継続的デプロイ環境を作成する
どうもです。
Next.jsをデプロイする際には、Vercelがよく知られていますが、今日はCloud Runを使ってNext.jsをデプロイしてみたいと思います。
まず、GCPのアカウントを事前に作成しておきます。
デプロイにはBuildPackを使用する方法もありますが、今回はDockerfileを使って進めていきたいと思います。
Buildpacksを使用してスムーズに進めたかったのですが、CIでテストを実行したり、通常の手順を踏んで作業を進める場合は、断然Dockerの方が簡単そうでした。
Buildpacksは多くのことを隠蔽してくれる分、特殊な操作を行おうとすると情報が限られていることがありますね。
では、さっそく始めていきましょう。
1.Next.jsのプロジェクトを作成
まず、プロジェクトを作成しましょう。
$ npx create-next-app nextjs-cloudrun --typescript
$ cd nextjs-cloudrun
next.config.jsの設定を以下のように変更することで、standalone機能が有効になります。
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'standalone' // standaloneにする
}
module.exports = nextConfig
1-1.standalone機能とは
このモードが有効になった状態でビルドすると、動作に必要な最小限のファイル群がコピーされるという便利な機能です。
これによってビルドサイズが削減され、Dockerでイメージ化してCloud Runへのデプロイが行われます。
2.テストを入れる
Product環境ではデプロイの前にテストを実施することが当たり前ですので、今回はテストパッケージを導入する必要があります。
$ yarn add -D @testing-library/jest-dom @testing-library/react jest jest-environment-jsdom
パッケージのインストールが完了したら、jest.config.jsを作成します。
// jest.config.js
const nextJest = require("next/jest");
const createJestConfig = nextJest({
// next.config.jsとテスト環境用の.envファイルが配置されたディレクトリをセット。基本は"./"で良い。
dir: "./",
});
// Jestのカスタム設定を設置する場所。従来のプロパティはここで定義。
const customJestConfig = {
// jest.setup.jsを作成する場合のみ定義。
// setupFilesAfterEnv: ["<rootDir>/jest.setup.js"],
moduleNameMapper: {
// aliasを定義 (tsconfig.jsonのcompilerOptions>pathsの定義に合わせる)
"^@/pages/(.*)$": "<rootDir>/pages/$1",
"^@/components/(.*)$": "<rootDir>/components/$1"
},
testEnvironment: "jest-environment-jsdom",
};
// createJestConfigを定義することによって、本ファイルで定義された設定がNext.jsの設定に反映されます
module.exports = createJestConfig(customJestConfig);
configファイルが用意できたら、簡単なテストを書いておきましょう。
// components/sample.tsx
import React from 'react';
type IProps = {
text: string;
}
const Index = ({ text }: IProps) => {
return <div>{text}</div>;
}
export default Index
// components/sample.test.tsx
import React from 'react';
import { render, screen } from '@testing-library/react'
import '@testing-library/jest-dom/extend-expect'
import Sample from './sample';
describe('sample test', () => {
it('テキストが表示されている', () => {
render(<Sample text={'hello'} />);
// このテキストを変更すればテストに失敗する
expect(screen.getByText('hello')).toBeInTheDocument();
})
})
3.Dockerfileを作成
$ vi Dockerfile
まずはDockerfileの中身を記述していきましょう。
// Dockerfile
FROM node:16 AS build
ADD . /app
WORKDIR /app
RUN yarn install --production=false
RUN yarn build
FROM gcr.io/distroless/nodejs:16
WORKDIR /app
COPY --from=build /app/next.config.js ./
COPY --from=build /app/public ./public
COPY --from=build /app/package.json ./package.json
COPY --from=build /app/.next/static ./.next/static
COPY --from=build /app/.next/standalone ./
CMD ["server.js"]
4.cloudbuild.yamlを作成
$ vi cloudbuild.yaml
次に、cloudbuild.yamlの中身を記述していきましょう。
// cloudbuild.yaml
steps:
#Package Install
- name: "gcr.io/cloud-builders/npm:node-${_NODE_VERSION}"
args: ["install"]
#Unit test
- name: "gcr.io/cloud-builders/npm:node-${_NODE_VERSION}"
args: ["test"]
#Build image
- name: "gcr.io/cloud-builders/docker"
dir: .
args: ["build", "-t", "gcr.io/$PROJECT_ID/${_IMAGE_NAME}:${_TAG}", "."]
#Push image
- name: "gcr.io/cloud-builders/docker"
args: ["push", "gcr.io/$PROJECT_ID/${_IMAGE_NAME}"]
#Deploy image
- name: "gcr.io/cloud-builders/gcloud"
id: "deploy-cloud-run"
args:
[
"beta",
"run",
"deploy",
"${_SERVICE_NAME}",
"--image",
"gcr.io/$PROJECT_ID/${_SERVICE_NAME}",
"--region",
"${_REGION}",
"--platform",
"managed",
"--allow-unauthenticated",
]
substitutions:
_REGION: asia-northeast1
_SERVICE_NAME: nextjs-cloudrun
_IMAGE_NAME: nextjs-cloudrun
_TAG: latest
_NODE_VERSION: 19.0.0
4-1.コンテナイメージのビルド
#Build image
- name: "gcr.io/cloud-builders/docker"
dir: .
args: ["build", "-t", "gcr.io/$PROJECT_ID/${_IMAGE_NAME}:${_TAG}", "."]
-tフラグを使用することで、名前付きでビルドすることができます。
これは通常のDockerイメージのビルドと同様であり、以下のコマンドと同じ意味になります。
$PROJECT_IDはデフォルトでプロジェクトIDに置換されます。
$ docker build -t gcr.io/$PROJECT_ID/node-image:latest
4-2.コンテナイメージの登録
ビルドしたコンテナイメージをContainer Registryに登録します。
#Push image
- name: "gcr.io/cloud-builders/docker"
args: ["push", "gcr.io/$PROJECT_ID/${_IMAGE_NAME}"]
4-3.コンテナイメージのデプロイ
#Deploy image
- name: "gcr.io/cloud-builders/gcloud"
id: "deploy-cloud-run"
args:
[
"beta",
"run",
"deploy",
"${_SERVICE_NAME}",
"--image",
"gcr.io/$PROJECT_ID/${_SERVICE_NAME}",
"--region",
"${_REGION}",
"--platform",
"managed",
"--allow-unauthenticated",
]
4-4.substitutionsの登録
substitutionsでは実行時にユーザー定義の変数の置換として扱われます。substitutionsで定義した_REGIONは${_REGION}として扱うことができます。変数値はアンダースコアで始まり、大文字と数字のみを使用可能です。_REGIONはCloud Runへデプロイするリージョンと同じものを設定しておきます。
substitutions:
_REGION: asia-northeast1
_SERVICE_NAME: nextjs-cloudrun
_IMAGE_NAME: nextjs-cloudrun
_TAG: latest
_NODE_VERSION: 19.0.0
5.権限を設定する
Cloud BuildからCloud Runへデプロイするためには、適切な権限設定を有効にする必要があります。
GCPのダッシュボードで、Cloud Build > 設定のページからCloud Runを有効にします。
これにより、Cloud BuildからCloud Runへのデプロイが可能になります。
6.Cloud Buildのトリガーを作成する
以下の設定でトリガーを作成します。
それぞれの設定を行いましたが、コードがmainブランチにプッシュされると、Cloud Buildのトリガーが作動し、cloudbuild.yamlのステップが順番に実行されるはずです。
テストが組み込まれているため、テストが失敗するとCloud Buildが中断されることも確認できるはずです。
これにより、簡単にCI/CD環境を構築し、Cloud Runにデプロイすることができました。
ただし、Next.js × Vercelとは異なり、CDNにキャッシュなどの自動処理は行われないため、FastlyやCloud CDNなどを使用してCDNキャッシュを設定しましょう。
7.CDNの何が嬉しいのか?
CDNへのキャッシュは、LPやコーポレートサイトなどの静的なコンテンツや、リアルタイム性が必要ではないブログなどに非常に有効です。
しかし、すべてのコンテンツが静的で更新頻度が低いわけではありません。
ECサイトの在庫情報や決済処理などは常にリアルタイムな情報が必要です。では、リアルタイムな処理が必要なものは、すべてオリジンサーバーにリクエストを送る必要があるのでしょうか?
ここで、CDNエッジサーバーでJavaScriptを実行できる仕組みが登場します。最近では、以下のようなサービスがCDNのエッジでJavaScriptを実行する動きが活発です。
このCDNエッジでJSを実行できることにより、コンテンツのキャッシュだけでなく、オリジンサーバーで行っていた以下のような処理もエッジで高速に実行できるようになります。
CloudFlareでは、D1などのエッジで実行されるデータベースのリリースなど、エッジ技術はますます注目を集めています。
今回はNext.jsプロジェクトをCloud Buildから継続的にCloud Runにデプロイする方法についてお話ししました。
それでは。