Cloud Run functions × TypeScript 開発をしよう！【デプロイ：Cloud Run functions をデプロイする】

# はじめに
この章では以下のことについて説明します。
- GCPプロジェクトのセットアップ
- Node.js関数の Cloud Run functions へのデプロイ方法
---

いよいよGCP上に Cloud Run functionsのインスタンスを作成していきましょう。
まだGCPに登録していないという方は登録しましょう。以下の「IT 管理」「FinOps 管理」を参考に登録してください。「DevOps エンジニアリング」は飛ばしてもいいです（できれば見てほしくはある）。この記事で解説するのはこのページで言う「アプリケーション開発」の部分です。

https://cloud.google.com/docs/get-started?hl=ja

基本はリファレンス通りにやればできます。今回はとりあえず動かしてみるというゴールを達成しましょう。

https://cloud.google.com/functions/docs/create-deploy-gcloud?hl=ja#before-you-begin

リファレンスは以下のような構成になっていますが、プロジェクトのセットアップなどは自分で行ってください。
これまでの章で完了している項目と合わせて説明しない部分は斜線を引いておきます。

>- 始める前に
    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. ~~[開発環境の準備](https://qiita.com/KyongminS/items/f3848f36d6f2bb7b07b7)~~
>- ~~サンプルコードを取得する~~
>- **関数のデプロイ**


# Google Cloud CLI を初期化
後の記事でTerraformを使ってデプロイする方法などを解説しますが、その際Google Cloud CLIは必ず必要になってきます。これを有効化することでコマンドラインツールからGCPのリソースを操作することができます。

まずコマンドの初期化を実行して、操作するアカウントの設定を行いましょう。想定するケースは以下です。
- GCPの初期設定は諸々終わっている
- CLIのインストールは済んでいる
- プロジェクトも何個か作ったことがある
- 新たにプロジェクトを作成してそれを使う

```bash: CLIの初期化
gcloud init
```
```bash: インタラクティブで入力を何回か促されるので入力
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を利用するアカウントにログインする
- コマンド実行する際のリージョンの設定
- プロジェクトを作成する
- プロジェクトを設定する

```bash: CLIを使うアカウントでログイン
gcloud auth login
```

```bash: リージョンの設定
gcloud config set functions/region asia-northeast1
```

```bash: プロジェクトの作成
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.
```

```bash: プロジェクト設定
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` ディレクトリの中身がデプロイ対象のファイル群です。

```bash: ディレクトリ構成
├── 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 というファイルからソースコードを読み込みます。現状の構成でこの要件は満たされます。

```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 を作成します。

```js: ./package.prod.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` ディレクトリにコピーするスクリプトを書きましょう。
```js: package.json
{
  // ...
  "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` 「だけ」を作成するコマンドは以下のようになります。

```bash: package-lock.json だけを作成するコマンドを dist ディレクトリで実行する
npm install --package-lock-only # node_modules を作成しないで lockファイルだけ作成する
```

ということで先ほどの開発用 `package.json` の `postBuild` にこちらのコマンドも実行するようにしましょう。

```js: package.json
{
  // ...
  "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形式で出力されてしまいます。
```ts: 公式のサンプル(TS版)
import * as ff from '@google-cloud/functions-framework'

ff.http('helloGET', (req: ff.Request, res: ff.Response) => {
 // ...
});
```

この `ff.http('エントリーポイント', () => {})` の書き方を以下の `export const エントリーポイント` という形式で書き換えましょう。

```ts: 改良後
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にデプロイする

デプロイファイルの構成は以下のようになっています。

```bash: ディレクトリ構成
dist
├── index.js
├── index.js.map
├── package-lock.json
└── package.json
```

要件として、
- Node.js v20を使用する
- distフォルダにビルドファイル `index.js` を置いている
- エントリーポイントを `helloGET` と定義している
- 認証なしで呼び出せるようにする
- 東京リージョン（`asia-northeast1`）で使う

を考慮してリファレンスに書いてあるコマンドを以下のように変更します。

```bash
gcloud functions deploy sample-fc \
--gen2 \
--runtime=nodejs20 \
--region=asia-northeast1 \
--entry-point=helloGET \
--trigger-http \
--source ./dist # デプロイファイルのあるディレクトリ
```

実行時に、Cloud Run functionsのインスタンスを作成する上で必要なAPIの有効化ができていない場合、適宜有効にするか聞かれるので、「y」と入力しましょう。

```bash: 結果
# ↓ 認証なしで呼び出せるようにするか。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!」と返ってきます。

```bash
$ curl https://xxx.cloudfunctions.net/sample-cf
Hello World!
```

GCPコンソールの方でもインスタンスがあるか確認しましょう。

![スクリーンショット 2024-12-14 17.24.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/374872/baf054d7-7524-e6fe-1d84-1da5eb358333.png)


# Cloud Run functionsのインスタンスを削除する
このまま放置すると、仮に大量アクセスされると課金されるので消しましょう。
以下のコマンドを実行してCloud Run functionsの関数の一覧を見ます。

```bash
gcloud functions list
```

```
NAME       STATE   TRIGGER       REGION           ENVIRONMENT
sample-cf  ACTIVE  HTTP Trigger  asia-northeast1  2nd gen
```

対象のインスタンスを削除しましょう。`--region` を指定しないと違う場所が指定されてしまうので指定します。
```bash
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].
```

これで削除されました。コンソールでも一応確認しましょうね。

![スクリーンショット 2024-12-14 16.17.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/374872/a5e84ce1-a712-79c3-9e7c-dc484273edae.png)

# おわりに
これで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形式に書き換えましょう。

```ts: NG
// ## NG
import * as ff from '@google-cloud/functions-framework'

ff.http('helloGET', (req: ff.Request, res: ff.Response) => {
 // ...
});
```

```ts: OK
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) => {
 // ...
};
```

## 参考文献
https://qiita.com/99no_exit/items/988460d9de90a9dbe650

https://cloud.google.com/functions/docs/deploy?hl=ja