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


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

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

事前準備

2021/03/22時点で、Cloud FunctionsにおけるNode.jsの安定バージョンは12系なので、ローカルにもNode.jsの12系をインストールしてください。
また、Cloud Funcionsにデプロイするためにgcloudコマンドが必要です。Macを使っている方はHomebrew-caskでインストールできます。

$ brew cask install google-cloud-sdk

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

リポジトリにFunctions FrameworkでTypeScriptを使う場合のドキュメントがあるので、説明に従って開発環境を作っていきます。

$ 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を動かすためのコマンドを追記します。

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 を以下のサンプルの内容で上書きし、

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 の開発環境を構築できました

Cloud Functionsにデプロイする

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

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

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

実行結果(抜粋)
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.jsonpackage-lock.json を含めてデプロイすると、 npm ci が使用され、prepare スクリプトが常に実行されます。

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

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

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

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

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

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

$ curl https://us-central1-{YOUT_PROJECT_ID}.cloudfunctions.net/sample
実行結果
Hello, World

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

最後に

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

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