Open8

Cloud Functions 用のローカル開発環境

hankei6kmhankei6km

以前に試したこれをもう少し試すために、ローカルで Cloud Functions + Pub/Sub 用の開発環境を作っているメモ。

メモは下記のリポジトリで試している。
https://github.com/hankei6km/test-functions-framework-pubsub-ts

このメモではとりあえず Functions の部分について。VSCode と Dev Containers(docker-compose) を使うが、Dev Container についてはあまり触れない(Pub/Sub 用のscrap を作ったらそちらに書くかも)。

hankei6kmhankei6km

ローカルの開発環境を作成する方針

ローカル側での実行環境について、Cloud Functions の場合はドキュメントに記述がある。

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

独自の関数ホスティング環境を設定する前に、次の 2 つの重要な選択を行う必要があります。

  • 使用する抽象化レイヤ
  • 実行する関数のタイプ

抽象化レイヤは下記の2つから選択。

  • Function Frameworks - トリガーを関数に渡すフレームワーク、HTTP トリガーなら(おそらく内部は express の)サーバーを起動できる
  • Buildpack - コンテナで実行環境を作る、後述の Pub/Sub エミュレーターからの push はこちらを使う記述がある(push のトリガーを扱うため?)

関数のタイプはイベントタイプを選択。

今回は Functions Framworks で HTTP 関数を使う。ということで、この後ローカルで環境を作るときは上記にあわせて実施することになる。

hankei6kmhankei6km

Functions Frameworks の利用

これは NPM のパッケージなので npm init などでパッケージを作った後にインストールすれば利用できる。

https://cloud.google.com/functions/docs/running/function-frameworks?hl=ja

利用方法は基本的には上記のドキュメント通り。デプロイまで考慮すると gcloud CLI が必要そうなどいろいろ違いそうだが、コードを記述するだけなら express(http server)などを使うのとあまり変わらない、と思う(Dev Container を作ったりするときもとくに変わったところはなさそうだった)。

下記は少し気になった点など。

  • ドキュメントには —save-dev でインストールするように記載されている
  • Function のソースは index.js に記述、正確には package.jsonmain で指定したファイルが利用される
  • ESM でも利用できる
    • 使える機能はデプロイ時のランタイムで決まる、のかな?
    • フレームワーク側の package.jsonexports が使われているので TypeScript では少し注意が必要
  • ホットリロードには対応していないようなので自前で対応することになる。

参考

--save-dev について。

注: このライブラリは、関数の依存関係内で指定しない場合、関数をデプロイする際に自動的に追加されます。

ESM について。

https://cloud.google.com/functions/docs/concepts/nodejs-runtime?hl=ja#using_es_modules

hankei6kmhankei6km

ホットリロード

フレームワークの機能には含まれていないようなので npm-check などで対応することになる。とくに引っかかる点はなかったが、リロードはデバッグ用に使いたくなると思うので、 start スクリプトなどとは別に debug スクリプトを用意してそちらに設定しておくのがよいかと思う。

https://medium.com/google-cloud/hot-reload-node-cloud-functions-64ffdb095a00

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

また、ホットリロードの設定についてではないが、 tsc でビルドしているとやはりちょっと遅い。この辺は swc の CLI などを検討している。

hankei6kmhankei6km

TypeScrtipt

Firebase の Functions のドキュメントは出てくるのだが Cloud Functions はなかった(Firebase Functions と何が違う?)。

https://firebase.google.com/docs/functions/typescript

フレームワーク側で型の情報(.d.ts)もエクスポートされているので「使えるかな?」と試したら使えた。

少し気になる点としては maindist/index.js にしたのだが、プロジェクトのルートから移動しているとデプロイ時に面倒なことなるようなことを見かけた(ページのアドレスをメモっておくのを忘れた)。この辺は「そのときはそのとき」ということでとりあえず進める。

Source map

Frerbase のドキュメントではログ出力時に .map ファイルがあった方がよいとある。Cloud Functions の方ではわからないが、後述のデバッグでも必要になってくるので、出力するようにしておいた方がよいと思う。

https://firebase.google.com/docs/functions/typescript#functions_logs_for_typescript_projects

moduleResolution

フレームワークのテスト用の関数 getFunction などは package.jsonexports の指定でエクスポートされている。TypeScript で使う場合は tsconfig.json などで moduleResolutionNodeNext にする必要がある(たぶん node16 でもいける)。

hankei6kmhankei6km

デバッグ

フレームワークで関数を実行しているとき、VSCode でブレークポイントなどを使えるようにする場合。

基本的にはホットリロードを使ったデバッグになるので、下記の --inspect と attach が基本的な方針となる。

https://medium.com/google-cloud/debugging-node-google-cloud-functions-locally-in-vs-code-e6b912eb3f84

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

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

ただし、上記の方法でフレームワークを利用しても HTTP サーバーが起動されない(すぐに終了してしまう)。これは下記を参考に .bin の実行用ファイルを使うことで解決された。

https://medium.com/@gajigesa/debugging-nodejs-google-cloud-functions-locally-with-functions-framework-in-vscode-6bd401dd85ac

node --inspect node_modules/.bin/functions-framework --target=<FUNCTION NAME> --signature-type=http

また、Auto Attach の設定は新しい JavaScript デバッガーではフィールド名が変更されている。これでソースを変更してリロードされても自動的に attach される。

"debug.javascript.autoAttachFilter": "smart",

ただし、これを設定すると Jest のテストでも反応するのだが、Jest の --watch ではブレークポイントで停止しない。devcontainer.jsonsettings などに含めるかは悩み中。

フレームワークでデバッグしているときは下図のような感じ。ソースを変更するとリロードされる。

VSCode のデバッガーで実行中の関数を一時的しているスクリーンショット

テストのとき。上記のように --watch だと停止しないので、デバッグするときはオプションなしで実行することになる(少しめんどう)。

VSCode のデバッガーで実行中の手うsとを一時的しているスクリーンショット

hankei6kmhankei6km

テスト

ドキュメントにテストについての記述がある。

https://cloud.google.com/functions/docs/testing/test-http?hl=ja

下記の 3 通りのテスト方法が記載されている

  • 単体テスト - フレームワークのテスト用ユーティリティ(getFunction)で関数を取得、テストコードの中で実行する
  • 統合テスト - フレームワークのテスト用ユーティリティ(getTestServer)でテスト用サーバーを取得して supertest でテストする
  • システムテスト - Cloud Functions の環境にデプロイしてテストする(たぶん)

TypeScript(ESM)

上記のドキュメントは CJS で sinonmocha を使っている前提。ESM の場合は下記がわかりやすい。

https://github.com/GoogleCloudPlatform/functions-framework-nodejs/blob/master/docs/testing-functions.md#testing-http-functions

少し注意点。

ドキュメントに書いてあるようにユーティリティ(getFunction など)を実行する前に関数をロード(importして関数登録のコードを実行させる)しておく必要がある。

  beforeAll(async () => {
    // load the module that defines `chk1`
    await import('../src/index.js')
  })

前述のように TypeScript では moduleResolutionNodeNext などにしておかないと tsserver では下記がエラーになる(VSCode のエディターでエラーが表示される)。

import {getFunction} from "@google-cloud/functions-framework/testing";

単体テスト

getFunction で取得した関数の型は HandlerFunction で各種関数の union になっている。そのままでは HttpFunction として扱えない。しかたないので、暫定的に型ガード関数を使っている(この辺の定番な記述方法は不明)。

  const isHttpFunction = (
    func: HandlerFunction | undefined
  ): func is HttpFunction => {
    // TODO: HttpFunction であるかの判定を確実にできるか調べる.
    if (typeof func === 'function' && func.length === 2) {
      return true
    }
    return false
  }

統合テスト

いまのところ、とくに引っかかるところはなかった。

システムテスト

まだ、試していない。