0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Cloud Functions 2nd gen.でNestJSを使うためのテンプレート(Hot Reload付き)

Posted at

はじめに

バックエンドにNesJSを個人的に使うようになってしばらく経ちますが、NestJSの精緻な公式ドキュメント・豊富なコミュニティ記事が心強いです。

とはいえ、試行錯誤が必要になったケースもあって、筆者の場合、サーバーレス環境へのデプロイ、具体的にはCloud Functionsへのデプロイがその一つでした。

公式ドキュメントにはなるほどServerlessの一項は設けられていますが、AWS Lambdaこそあれ、Cloud Functionsに関する具体的な記述はありません。

本記事は上記ドキュメントの追補となるように、Cloud Functions向けの開発テンプレートを紹介するものです。

本記事のCloud Functionsはgcloud CLIで操作する通常のCloud Functionsであって、Firebase CLIで扱うCloud Functions for Firebaseではありません。

Cloud Functions for Firebaseの場合、以下が課題になっています。

  • Firebase CLIでは実行サービスアカウントをdeployオプションに指定できない(参考)
  • Workload Identity連携でのGitHub Actionsがうまくいかない。
    • google-github-actions/auth@v2にはThis option [Inputs: Workload Identity Federation] is not supported by Firebase Admin SDK. Use Service Account Key JSON authentication instead.の記述があります。

テンプレート

  • GitHub Actions
    • 以下のセットアップが必要
      • <>で囲まれた変数の設定
      • workload_identity_providerの設定
  • Functions Frameworkでのローカル開発
  • watchコマンドでのホットリロード
  • Swagger
    • 認証保護はなし。

ポイント

nest newから変更が必要な箇所を摘要します。

package.json

"main": "dist/main.js"を追加

build後にCloud Functionsのエントリーポイントとなるファイルパス。

"start": "functions-framework --target=nestjs-cloud-functions-starter",

targetにはmain.tsで定義する関数名を入れます。

"watch": "concurrently "nest build -w" "wait-on dist && nodemon --watch ./dist --exec 'pnpm run start'""

Hot Reloadの設定。後述します。

main.tsの調整

src/main.ts
import { NestFactory } from '@nestjs/core';
import { ExpressAdapter } from '@nestjs/platform-express';
import { AppModule } from './app.module';
import * as express from 'express';
import helmet from 'helmet';
import { ValidationPipe } from '@nestjs/common/pipes/validation.pipe';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import * as functions from '@google-cloud/functions-framework';

const server = express();
let isApplicationReady = false;

const createNestServer = async (expressInstance) => {
  const app = await NestFactory.create(
    AppModule,
    new ExpressAdapter(expressInstance),
  );

  app.use(helmet());
  app.enableCors({
    origin: ['*'],
  });
  app.useGlobalPipes(new ValidationPipe());

  // Documentation
  const config = new DocumentBuilder()
    .setTitle('Nest.js Cloud Functions Starter')
    .setDescription('A Minimal Nest.js Cloud Functions Starter')
    .setVersion('1.0')
    .build();
  const document = SwaggerModule.createDocument(app, config);
  SwaggerModule.setup('api', app, document);

  console.log('the server is starting @ Cloud Functions');
  return app.init();
};

const applicationReady = async () => {
  try {
    await createNestServer(server);
    isApplicationReady = true;
  } catch {
    (err) => console.error('Nest broken', err);
  }
};

functions.http('nestjs-cloud-functions-starter', async (...args) => {
  if (!isApplicationReady) {
    await applicationReady();
  }
  server(...args);
});

Swaggerの保護は行なっていません。必要に応じてBasic Authや環境に応じた保護を追加してください。1

isApplicationReady

Cloud Functionsを含むServerless環境では、インスタンスの最初のリクエスト処理の前にだけ、serverを初期化する必要があります。リクエストごとにserverを初期化するのを避けるためのフラグとして、isApplicationReadyを使い、初期化が一度だけ行われるようにしています。

この初期化分岐を書いていないサンプルコードが散見されますが2、これを考慮しないと、たとえば、Prismaをグローバルで使っている場合にPrismaインスタンスがリクエストごとに作られてしまい、

warn(prisma-client) This is the 10th instance of Prisma Client being started.

というエラーが出ます。Nest.js公式ドキュメントのAWS Lambdaのケースでは、

  server = server ?? (await bootstrap());

として初期化を一度だけに制御していました。

Hot Reload

Nest.js単体の場合、nest start --watchとすればHot Reloadされます。
Functions Frameworkと併用する場合、工夫が必要でした。

使うパッケージ

スクリプト

"concurrently \"nest build -w\" \"wait-on dist && nodemon --watch ./dist --exec 'pnpm run start'\""

流れ

concurrentlynest build -wと同時にfunctions-frameworkを実行すると、最初にエラーが出て、その後、serveされる挙動になります。これはnest buildの時に一回dist/が消え、エントリーポイントを見失うため。


一回エラーになってから起動する

対策として、wait-onを使ってdist/の生成を待って、functions-frameworkを実行するようにしました。もっとスマートなやり方がありそうですが、これで動作しています。

GitHub Actions

設定が必要な値は<>で囲っています。

.github/workflows/deploy_to_cloud_functions.yml
name: Deploy to Cloud Functions

on:
  push:
    branches:
      - <SPECIFY BRANCH NAME>

jobs:
  deploy:
    name: Deploy
    runs-on: ubuntu-latest
    permissions:
      contents: read
      id-token: write
    env:
      function_name: <SPECIFY FUNCTION NAME>
      entry_point: <SPECIFY ENTRY POINT>
      workload_identity_provider: "projects/<YOUR PROJRCT ID>/locations/global/workloadIdentityPools/<YOUR POOLNAME>/providers/<YOUR PROVIDER NAME>"
      service_account: <SPECIFY SERVICE ACCOUNT WITH DUE PRIVILEGES FOR DEPLOYMENT>
      cloud_run_service_account: <SPECIFY SERVICE ACCOUNT WITH DUE PRIVILEGES FOR EXECUTION>
    strategy:
      matrix:
        node-version: [20]
    steps:
      - uses: actions/checkout@v4
      - name: 'Authenticate to Google Cloud'
        id: auth
        uses: 'google-github-actions/auth@v2'
        with:
          workload_identity_provider: ${{ env.workload_identity_provider }}
          service_account: ${{ env.service_account }}
      - name: 'Set up Cloud SDK'
        uses: 'google-github-actions/setup-gcloud@v2'
        with:
          version: '>= 363.0.0'

      - uses: pnpm/action-setup@v3
        with:
          version: 8
      - name: Use Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
          cache: 'pnpm'
          cache-dependency-path: './functions/pnpm-lock.yaml'

      - name: Install dependencies
        run: |
          cd functions
          pnpm install

      - name: Deploy to Cloud Functions
        run: >-
          gcloud functions deploy ${{ env.function_name }}
          --gen2
          --runtime=nodejs${{ matrix.node-version }}
          --region=asia-northeast1
          --source=./functions
          --entry-point=${{ env.entry_point }}
          --trigger-http
          --allow-unauthenticated
          --run-service-account=${{ env.cloud_run_service_account }}

pnpmを使っている関係でやや長くなっています。gcloud functions deployで事前実行されるので、buildは不要でした。

認証にはgoogle-github-actions/auth@v2でWorkload Identity Federation方式で行なっています。

Workload Identity Federationの設定については、詳細な記事がありますので下記に譲ります。3

https://zenn.dev/cloud_ace/articles/7fe428ac4f25c8

おわりに

以上、Cloud Functionsで使えるNest.jsお手製テンプレートの紹介でした。

まだまだ改善の余地がありそうですが、このテンプレートがNest.jsで開発するスタートポイントになれば幸いです。

参考記事

Nest.js

https://docs.nestjs.com/faq/serverless#example-integration

https://qiita.com/0622okakyo/items/d69209b8b01c474c36be#firebase-%E3%81%AE%E8%A8%AD%E5%AE%9A

https://qiita.com/chelproc/items/37ed6ed27ee599b586bf

https://manuel-heidrich.dev/blog/how-to-secure-your-openapi-specification-and-swagger-ui-in-a-nestjs-application/

Cloud Functions

How to develop and test your Cloud Functions locally

Google Cloudのblog記事。TypeScriptでのhot reloadについて書かれています。

紹介されている例だとtscを使っていますが、今回はdist/srcにbuildしてしまうのでうまくいきません。(tsconfigをいじれば良いのですが、なるべくnest newの原型を保ちたい。)

https://cloud.google.com/blog/ja/topics/developers-practitioners/how-to-develop-and-test-your-cloud-functions-locally

GitHub Actions

https://dev.classmethod.jp/articles/deploy-the-cloud-functions-gen2-using-github-actions/

https://zenn.dev/cloud_ace/articles/7fe428ac4f25c8

  1. 参考: https://manuel-heidrich.dev/blog/how-to-secure-your-openapi-specification-and-swagger-ui-in-a-nestjs-application/

  2. https://github.com/fireship-io/nest-cloud-functions/blob/master/B-functions/functions/src/index.ts など

  3. サービスアカウントには「Cloud Functions 開発者」、「Artifact Registry 管理者」、「サービス アカウント トークン作成者」、「サービス アカウント ユーザー」のロールを付与しています。Cloud Functions 2nd.genはCloud Runで動くので、Artifact Registryを付与しています。これらが最小権限かは検証できていません。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?