Functions FrameworkでTypeScriptを使いつつCloud Functionsにデプロイする

GCPでサーバレスアーキテクチャを採用する場合、FaaSであるCloud Functionsの利用を検討するかと思います。FaaSといえばNode.jsというイメージ（個人の感想です）もありますし、Node.jsを使うならTypeScriptを使いたいですよね。

一方、サーバレスアーキテクチャはクラウドサービス依存が強く、ローカルに開発環境を作るのに苦労しがちです。そこで、Googleが提供している__Functions Framework__を使って開発環境を整え、Cloud Functionsにデプロイするところまでをやってみます。

https://github.com/GoogleCloudPlatform/functions-framework-nodejs

# 事前準備

2021/03/22時点で、[Cloud FunctionsにおけるNode.jsの安定バージョンは12系](https://cloud.google.com/functions/docs/migrating/nodejs-runtimes?hl=ja)なので、ローカルにもNode.jsの12系をインストールしてください。
また、Cloud Funcionsにデプロイするためにgcloudコマンドが必要です。Macを使っている方はHomebrew-caskでインストールできます。

```
$ brew cask install google-cloud-sdk
```

# Functions Framework + TypeScript の開発環境を作る

リポジトリに[Functions FrameworkでTypeScriptを使う場合のドキュメント](https://github.com/GoogleCloudPlatform/functions-framework-nodejs/blob/master/docs/typescript.md)があるので、説明に従って開発環境を作っていきます。

```
$ cd /path/to/your/workspace
$ npx gts init
```

`package.json` を作るかどうか聞かれるので、そのままEnterで作ってもらいましょう。

次に、必要なパッケージをインストールし、

```
$ npm install @google-cloud/functions-framework
$ npm install @types/express concurrently nodemon --save-dev
```

npm-scriptsにローカルでFunctions Frameworkを動かすためのコマンドを追記します。

```json:package.json
  "scripts": {
    "start": "functions-framework --source=build/src/ --target=helloWorld",
    "watch": "concurrently \"tsc -w\" \"nodemon --watch ./build/ --exec npm run start\"",
    ...
  }
```

自動生成された `src/index.ts` を以下のサンプルの内容で上書きし、

```ts:src/index.ts
import type { HttpFunction } from '@google-cloud/functions-framework/build/src/functions';

export const helloWorld: HttpFunction = (req, res) => {
  res.send('Hello, World');
};
```

起動すると、ローカルのURLにアクセスできるようになります。


```
$ npm run watch
```

```
$ curl http://localhost:8080
```

```:実行結果
Hello, World
```

たったこれだけで、Cloud Functions + TypeScript の開発環境を構築できました :rocket: 

# Cloud Functionsにデプロイする

gcloudコマンドでデプロイしてみましょう。

```
$ gcloud functions deploy sample \
    --project YOUT_PROJECT_ID \
    --allow-unauthenticated \
    --runtime nodejs12 \
    --trigger-http \
    --entry-point helloWorld
```

…

はい、失敗しましたね :sob: 
以下のようなエラーが出ているはずです。

```:実行結果（抜粋）
ERROR: (gcloud.functions.deploy) OperationError: code=3, message=Build failed: > @0.0.0 prepare /workspace
> npm run compile


> @0.0.0 compile /workspace
> tsc

sh: 1: tsc: not found
```

Cloud Functionsでは、`package.json` と `package-lock.json` を含めてデプロイすると、 `npm ci` が使用され、`prepare` スクリプトが常に実行されます。

https://cloud.google.com/functions/docs/migrating/nodejs-runtimes?hl=ja#nodejs-12-changes

`package.json` を見ると、`prepare` スクリプトとしてtscコマンドが実行されるようになっていますが、`npm ci` ではdevDependenciesがインストールされないため、tscコマンドが見つからずエラーになってしまいます。


```json:package.json
  "scripts": {
    ...
    "compile": "tsc",
    "fix": "gts fix",
    "prepare": "npm run compile",
    ...
  }
```

typescript npmをdependenciesとしてインストールすれば解決しそうですが、合わせて他のnpmもdependenciesとしてインストールしなければならず、アプリケーションの実行と関係ないファイルが大量に含まれてしまいます。

そこで、`prepare` スクリプトを削除し、デプロイする前にコンパイルしておくことで、余計なファイルが含まれないようにします。

```diff_json:package.json
  "scripts": {
    ...
    "compile": "tsc",
    "fix": "gts fix",
-   "prepare": "npm run compile",
    ...
  }
```

```
$ npm run compile
```

コンパイル結果は `build` ディレクトリに出力されます。

デプロイする際 `.gcloudignore` というファイルが自動生成されますが、そのままだと余計なファイルが含まれてしまうため、以下の内容で上書きします。

```:.gcloudignore
*
!build/**
!package*.json
!.
```

コンパイル結果と `package.json` `package-lock.json` があればアプリケーションを実行するには十分、ということです。

# デプロイに再チャレンジ

それでは、再度gcloudコマンドでデプロイしてみましょう。

```
$ gcloud functions deploy sample \
    --project YOUT_PROJECT_ID \
    --allow-unauthenticated \
    --runtime nodejs12 \
    --trigger-http \
    --entry-point helloWorld
```

```:実行結果（抜粋）
entryPoint: helloWorld
httpsTrigger:
  securityLevel: SECURE_OPTIONAL
  url: https://us-central1-{YOUT_PROJECT_ID}.cloudfunctions.net/sample
ingressSettings: ALLOW_ALL
labels:
  deployment-tool: cli-gcloud
name: projects/{YOUR_PROJECT_ID}/locations/us-central1/functions/sample
runtime: nodejs12
```

無事にデプロイできました :tada: 

```
$ curl https://us-central1-{YOUT_PROJECT_ID}.cloudfunctions.net/sample
```

```:実行結果
Hello, World
```

Cloud FunctionsのURLにもアクセスできましたね :sparkles: 
作成したFunctionをGCPの管理コンソールで見ると、 以下のように余計なファイルが含まれていないことが分かります。

![スクリーンショット 2021-03-20 1.15.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28249/c8a2ddeb-640d-0987-c79a-b73825d5e914.png)

# 最後に

今回はトリガーとしてHTTPを試しましたが、Cloud StorageやCloud Pub/Sub等のGCPで発生する各種イベントをトリガーとして実行させることもできます。また、Cloud FunctionsだけではなくCloud RunやKnativeで実行させることもできるため、用途に応じて使い分けられるのも良いですね。

なお、Functions FrameworkはNode.js以外の言語でも提供されているので、他の言語で使いたい方はぜひリポジトリを探してみてください。