はじめに
この章では以下のことについて説明します。
- GCPプロジェクトのセットアップ
- Node.js関数の Cloud Run functions へのデプロイ方法
いよいよGCP上に Cloud Run functionsのインスタンスを作成していきましょう。
まだGCPに登録していないという方は登録しましょう。以下の「IT 管理」「FinOps 管理」を参考に登録してください。「DevOps エンジニアリング」は飛ばしてもいいです(できれば見てほしくはある)。この記事で解説するのはこのページで言う「アプリケーション開発」の部分です。
基本はリファレンス通りにやればできます。今回はとりあえず動かしてみるというゴールを達成しましょう。
リファレンスは以下のような構成になっていますが、プロジェクトのセットアップなどは自分で行ってください。
これまでの章で完了している項目と合わせて説明しない部分は斜線を引いておきます。
- 始める前に
1.Google Cloud コンソールのプロジェクト セレクタ ページで、Google Cloud プロジェクトを選択または作成
1.Google Cloudプロジェクトで課金が有効になっていることを確認
1.Cloud Functions、Cloud Build、Artifact Registry、Cloud Run、Logging APIを有効にする
1.Google Cloud CLIをインストール
1. gcloud CLIを初期化
1.開発環境の準備サンプルコードを取得する- 関数のデプロイ
Google Cloud CLI を初期化
後の記事でTerraformを使ってデプロイする方法などを解説しますが、その際Google Cloud CLIは必ず必要になってきます。これを有効化することでコマンドラインツールからGCPのリソースを操作することができます。
まずコマンドの初期化を実行して、操作するアカウントの設定を行いましょう。想定するケースは以下です。
- GCPの初期設定は諸々終わっている
- CLIのインストールは済んでいる
- プロジェクトも何個か作ったことがある
- 新たにプロジェクトを作成してそれを使う
gcloud init
bash-5.2$ gcloud init
Welcome! This command will take you through the configuration of gcloud.
Settings from your current configuration [xxx] are:
core:
disable_usage_reporting: 'False'
Pick configuration to use:
[1] Re-initialize this configuration [default] with new settings
[2] Create a new configuration
Please enter your numeric choice: 1 # <---- 選択
Your current configuration has been set to: [default]
You can skip diagnostics next time by using the following flag:
gcloud init --skip-diagnostics
Network diagnostic detects and fixes local network connection issues.
Checking network connection...done.
Reachability Check passed.
Network diagnostic passed (1/1 checks passed).
Choose the account you would like to use to perform operations for this configuration:
[1] xxx@gmail.com
[2] Log in with a new account
Please enter your numeric choice: 1 # <---- 選択
You are logged in as: [xxx@gmail.com].
Pick cloud project to use:
[1] xxxx
[2] Enter a project ID
[3] Create a new project
Please enter numeric choice or text value (must exactly match list item): 3 # <--- 選択
Enter a Project ID. Note that a Project ID CANNOT be changed later.
Project IDs must be 6-30 characters (lowercase ASCII, digits, or
hyphens) in length and start with a lowercase letter. プロジェクト名 # <--- 何か入力する
Create in progress for [https://cloudresourcemanager.googleapis.com/v1/projects/プロジェクト名].
Waiting for [operations/cp.9109827928246682498] to finish...done.
Enabling service [cloudapis.googleapis.com] on project [プロジェクト名]...
Operation "operations/acat.p2-830312214387-70f8a33f-9ce0-4192-bbff-070e85e0e300" finished successfully.
別パターン
- CLIを利用するアカウントにログインする
- コマンド実行する際のリージョンの設定
- プロジェクトを作成する
- プロジェクトを設定する
gcloud auth login
gcloud config set functions/region asia-northeast1
gcloud projects create プロジェクト名
---
Create in progress for [https://cloudresourcemanager.googleapis.com/v1/projects/プロジェクト名].
Waiting for [operations/cp.9109827928246682498] to finish...done.
Enabling service [cloudapis.googleapis.com] on project [プロジェクト名]...
Operation "operations/acat.p2-830312214387-70f8a33f-9ce0-4192-bbff-070e85e0e300" finished successfully.
gcloud config set project プロジェクト名
WARNING: Your active project does not match the quota project in your local Application Default Credentials file. This might result in unexpected quota issues.
To update your Application Default Credentials quota project, use the `gcloud auth application-default set-quota-project` command.
Updated property [core/project].
Node.js 関数をデプロイする前の準備
ここまでの解説で作成したプロジェクト構成は以下のようになっています。
dist
ディレクトリの中身がデプロイ対象のファイル群です。
├── README.md
├── dist
│ ├── index.js
│ └── index.js.map
├── src
│ ├── hoge
│ │ └── hoge.ts
│ └── index.ts
├── test
│ └── hoge.test.ts
├── package-lock.json
├── package.json
├── jest.config.js
└── tsconfig.json
Node.js 関数を Cloud Run functions にデプロイする際の要件は以下の3つです。
- index.js と同階層に
package.json
が存在する - package.json の
dependencies
に Functions Framework for Node.js が存在する - Functions Frameworkが index.js のエントリーポイントを見つけることができる
現状のdist
ディレクトリには package.json がなく、既存のpackage.json も開発環境用に書かれたものです。
なので本番用の package.json が自動で dist
ディレクトリに入るビルドプロセスを既存のビルドプロセスに組みこみましょう。
1. index.js というファイル名で Cloud Run functions のNode.js関数をビルドする
デフォルトでは、Cloud Run functions は index.js というファイルからソースコードを読み込みます。現状の構成でこの要件は満たされます。
{
// ...
"scripts": {
"build": "esbuild ./src/index.ts --bundle --minify --sourcemap --platform=node --format=esm --outfile=./dist/index.js",
}
// ...
}
2. package.prod.json を プロジェクトルートに配置する
package.json が含まれていないとエラーになるので含めます。
"main"
"type"
"dependencies"
は正しく指定して、最低限の内容のみ書かれた package.json を作成します。
{
"name": "advent-calendar-2024",
"version": "1.0.0",
"description": "",
"main": "index.js",
"type": "module",
"author": "",
"license": "ISC",
"dependencies": {
"@google-cloud/functions-framework": "^3.4.2"
}
}
3. ビルド完了後に package.prod.json を dist
ディレクトリにコピーする
npm のスクリプトには postBuild
という便利なコマンドが存在しており、build
コマンド実行後に実行するコマンドを書くことができます。
開発用の package.json にpostBuild
スクリプトを追加しましょう。そこに package.prod.json を package.json という名前で dist
ディレクトリにコピーするスクリプトを書きましょう。
{
// ...
"scripts": {
"build": "esbuild ./src/index.ts --bundle --minify --sourcemap --platform=node --format=esm --outfile=./dist/index.js",
"postbuild": "cp package.prod.json ./dist/package.json",
}
// ...
}
4. package-lock.json も同階層に配置されるようにする
Cloud Run functionsにデプロイする際、index.js がある階層に package-lock.json
があるかないかでデプロイ時間が結構変わります。package-lock.json
「だけ」を作成するコマンドは以下のようになります。
npm install --package-lock-only # node_modules を作成しないで lockファイルだけ作成する
ということで先ほどの開発用 package.json
の postBuild
にこちらのコマンドも実行するようにしましょう。
{
// ...
"scripts": {
// ...
"postbuild": "cp package.prod.json ./dist/package.json && cd dist && npm install --package-lock-only",
}
// ...
}
5. ビルド後のファイルがESM形式になるように修正する
公式のCloud Run functionsのサンプルでは、ビルド後のJSファイルがESM形式で表示されません。esbuildコマンドのオプションで --format=esm
と指定しても CommonJS形式で出力されてしまいます。
import * as ff from '@google-cloud/functions-framework'
ff.http('helloGET', (req: ff.Request, res: ff.Response) => {
// ...
});
この ff.http('エントリーポイント', () => {})
の書き方を以下の export const エントリーポイント
という形式で書き換えましょう。
import * as ff from '@google-cloud/functions-framework'
import type { HttpFunction } from "@google-cloud/functions-framework";
export const helloGET: HttpFunction = (req: ff.Request, res: ff.Response) => {
// ...
};
ここまでしてやっと Node.js関数をCloud Run functionsにデプロイできるようになります。
Cloud Run functionsにデプロイする
デプロイファイルの構成は以下のようになっています。
dist
├── index.js
├── index.js.map
├── package-lock.json
└── package.json
要件として、
- Node.js v20を使用する
- distフォルダにビルドファイル
index.js
を置いている - エントリーポイントを
helloGET
と定義している - 認証なしで呼び出せるようにする
- 東京リージョン(
asia-northeast1
)で使う
を考慮してリファレンスに書いてあるコマンドを以下のように変更します。
gcloud functions deploy sample-fc \
--gen2 \
--runtime=nodejs20 \
--region=asia-northeast1 \
--entry-point=helloGET \
--trigger-http \
--source ./dist # デプロイファイルのあるディレクトリ
実行時に、Cloud Run functionsのインスタンスを作成する上で必要なAPIの有効化ができていない場合、適宜有効にするか聞かれるので、「y」と入力しましょう。
# ↓ 認証なしで呼び出せるようにするか。yで答える
Allow unauthenticated invocations of new function [sample-cf]? (y/N)? y
Preparing function...done.
✓ Deploying function...
✓ [Build] Logs are available at [https://console.cloud.google.com/cloud-build/builds;region=asia-northeast1/ed36af68-ad0f-4ed9-9c05-8b728f466a5a?project=830312214387]
✓ [Service]
. [ArtifactRegistry]
. [Healthcheck]
. [Triggercheck]
Done.
You can view your function in the Cloud Console here: https://console.cloud.google.com/functions/details/asia-northeast1/sample-cf?project=xxx
buildConfig:
automaticUpdatePolicy: {}
build: projects/830312214387/locations/asia-northeast1/builds/ed36af68-ad0f-4ed9-9c05-8b728f466a5a
dockerRegistry: ARTIFACT_REGISTRY
### ...
### 省略
### ...
url: https://xxx.cloudfunctions.net/sample-cf
末尾に表示されたURLをcurlで呼び出すと「Hello, World!」と返ってきます。
$ curl https://xxx.cloudfunctions.net/sample-cf
Hello World!
GCPコンソールの方でもインスタンスがあるか確認しましょう。
Cloud Run functionsのインスタンスを削除する
このまま放置すると、仮に大量アクセスされると課金されるので消しましょう。
以下のコマンドを実行してCloud Run functionsの関数の一覧を見ます。
gcloud functions list
NAME STATE TRIGGER REGION ENVIRONMENT
sample-cf ACTIVE HTTP Trigger asia-northeast1 2nd gen
対象のインスタンスを削除しましょう。--region
を指定しないと違う場所が指定されてしまうので指定します。
gcloud functions delete sample-cf --region=asia-northeast1
2nd gen function [projects/xxx/locations/asia-northeast1/functions/sample-cf] will be deleted.
Do you want to continue (Y/n)? Y
Preparing function...done.
✓ Deleting function...
✓ [Artifact Registry]
✓ [Service]
Done.
Deleted [projects/xxx/locations/asia-northeast1/functions/sample-cf].
これで削除されました。コンソールでも一応確認しましょうね。
おわりに
これでCloud Run functions上にNode.js関数のインスタンスを作成することができました。今度は別のサービスと組み合わせて使っていく例を示したいと思います。
参考
トラブルシューティング
Provided module can't be loaded. Is there a syntax error in your code?
エラー文
Provided module can't be loaded. Is there a syntax error in your code? Detailed stack trace: ReferenceError: module is not defined in ES module scope
This file is being treated as an ES module because it has a '.js' file extension and '/workspace/package.json' contains "type": "module". To treat it as a CommonJS script, rename it to use the '.cjs' file extension.
at file:///workspace/index.js:1:457
at ModuleJob.run (node:internal/modules/esm/module_job:234:25)
at async ModuleLoader.import (node:internal/modules/esm/loader:473:24)
at async getUserFunction (/workspace/node_modules/@google-cloud/functions-framework/build/src/loader.js:95:30)
at async main (/workspace/node_modules/@google-cloud/functions-framework/build/src/main.js:40:32)
Could not load the function, shutting down.
Container called exit(1).
Default STARTUP TCP probe failed 1 time consecutively for container "worker" on port 8080. The instance was not started
解決策
package.jsonで要求されているモジュールとindex.jsのモジュールが一致していないため、合わせましょう。esbuildでビルドする際に --format=esm
を指定すると解決します(多分)。
Function 'エントリーポイント' is not defined in the provided module.
エラー文
Function 'helloGET' is not defined in the provided module. Did you specify the correct target function to execute?
Could not load the function, shutting down.
Container called exit(1).
解決法
ESM形式でビルドする設定にして、公式のサンプルをそのまま使うとエラーになります。ビルド後のファイルがCommonJS形式で出力されます。
ESM形式に書き換えましょう。
// ## NG
import * as ff from '@google-cloud/functions-framework'
ff.http('helloGET', (req: ff.Request, res: ff.Response) => {
// ...
});
import * as ff from '@google-cloud/functions-framework'
import type { HttpFunction } from "@google-cloud/functions-framework";
export const helloGET: HttpFunction = (req: ff.Request, res: ff.Response) => {
// ...
};
参考文献