はじめに
バックエンドに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オプションに指定できない(参考)
- runtimeOptionsでソース内指定は可能。
- 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.
の記述があります。
-
google-github-actions/auth@v2には
テンプレート
- 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の調整
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と併用する場合、工夫が必要でした。
使うパッケージ
- nodemon
- concurrently
- コマンドを同時に実行するライブラリ
- https://github.com/open-cli-tools/concurrently
- wait-on
- dist/以下の生成を待ってスクリプトを実行させる
- https://github.com/jeffbski/wait-on
スクリプト
"concurrently \"nest build -w\" \"wait-on dist && nodemon --watch ./dist --exec 'pnpm run start'\""
流れ
concurrently
でnest build -w
と同時にfunctions-framework
を実行すると、最初にエラーが出て、その後、serveされる挙動になります。これはnest build
の時に一回dist/が消え、エントリーポイントを見失うため。
対策として、wait-on
を使ってdist/
の生成を待って、functions-framework
を実行するようにしました。もっとスマートなやり方がありそうですが、これで動作しています。
GitHub Actions
設定が必要な値は<>
で囲っています。
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
Cloud Functions
How to develop and test your Cloud Functions locally
Google Cloudのblog記事。TypeScriptでのhot reloadについて書かれています。
紹介されている例だとtsc
を使っていますが、今回はdist/src
にbuildしてしまうのでうまくいきません。(tsconfigをいじれば良いのですが、なるべくnest new
の原型を保ちたい。)
GitHub Actions
https://dev.classmethod.jp/articles/deploy-the-cloud-functions-gen2-using-github-actions/
-
参考: https://manuel-heidrich.dev/blog/how-to-secure-your-openapi-specification-and-swagger-ui-in-a-nestjs-application/ ↩
-
https://github.com/fireship-io/nest-cloud-functions/blob/master/B-functions/functions/src/index.ts など ↩
-
サービスアカウントには「Cloud Functions 開発者」、「Artifact Registry 管理者」、「サービス アカウント トークン作成者」、「サービス アカウント ユーザー」のロールを付与しています。Cloud Functions 2nd.genはCloud Runで動くので、Artifact Registryを付与しています。これらが最小権限かは検証できていません。 ↩