見出し画像

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のトリガーを作成する

以下の設定でトリガーを作成します。

● 名前 / nextjs-cloud-run
● リージョン / グローバル
● イベント / ブランチに push する
● ソース / リポジトリを作成して連携しておきます
● ブランチ / main*
● 構成(形式) / Cloud Build 構成ファイル(yaml または json)
● 構成(ロケーション) / リポジトリ
● Cloud Build 構成ファイルの場所 / cloudbuild.yaml

それぞれの設定を行いましたが、コードが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を実行する動きが活発です。

● AWS Lambda@Edge
● Vercel Edge Function
● Fastly Compute@Edge
● Cloudflare Workers

このCDNエッジでJSを実行できることにより、コンテンツのキャッシュだけでなく、オリジンサーバーで行っていた以下のような処理もエッジで高速に実行できるようになります。

● Basic 認証などのアクセス制御のための認証や署名検証
● ヘッダの書き換えやレスポンスの変換やフィルタリング
● Cookieのパースなどクライアント側のデバイスやブラウザの情報に基づいたパーソナライズ
● ルーティングやリダイレクトの処理
● サーバーサイドのロジックの実行や動的コンテンツの生成や組み立て
● IPでのブロック

CloudFlareでは、D1などのエッジで実行されるデータベースのリリースなど、エッジ技術はますます注目を集めています。

今回はNext.jsプロジェクトをCloud Buildから継続的にCloud Runにデプロイする方法についてお話ししました。

それでは。

いいなと思ったら応援しよう!