🐈

Functions Framework +α でサーバーレス開発環境を整える

2021/06/27に公開

この記事では、Typescript 初心者が Functions Framework を利用してサーバーレス開発環境を整備した際の作業ログをまとめています。
GCP の Cloud Functions での開発での開発を想定しています。

開発

開発時に必要な Linter などの基本設定は gts を利用して設定しています。

gts

gts は Google の TypeScirpt style guide と Linter の設定が入った npm package です。
https://github.com/google/gts

npx gts init でポチポチしていくと、一通り設定されたプロジェクトとして設定されます。

$ npx gts init  
version: 14
Already have devDependency for typescript:
-^4.3.2
+^4.0.3
? Overwrite No

...

./.prettierrc.js already exists
? Overwrite No
Target src directory already has ts files. Template files not installed.
npm WARN functions_framework_ts_sample@1.0.0 No repository field.

updated 1 package and audited 539 packages in 3.568s

99 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

設定後は、package.json が正常に書き換えられていれば、npm run check で lint を実行できます。
下記などのツールを利用して正常に formatter, linter が実行されていることがわかります。

  • @typescript-eslint
  • prettier
$ npm run check

> functions_framework_ts_sample@1.0.0 check /xx
> gts check

version: 14

/xx/src/echo.ts
   1:28  error  "@google-cloud/functions-framework" is not published  node/no-unpublished-import
   1:83  error  Insert `;`                                            prettier/prettier
...

localでの実行

GCP の Cloud Functions での開発を想定しているため、local での Functions の実行用に Functions Framework を利用できます。

functions framework

functions framework は Cloud Functions などで稼働させる Function を開発するためのフレームワークです。
local での開発に対応しており、Go, Python, Node.js などの主要言語に対応しています。
https://github.com/GoogleCloudPlatform/functions-framework-nodejs

README.md 通りに、package を install した後には、scripts の部分を下記のように書き換えます。
target は実装したコード側で named exports している module (=Entrypoint) を指定しています。

  "scripts": {
    "start": "functions-framework --target=mainHandler"
  }

余談ですが、default exports / named exports 周りの理解について、下記の記事が参考になりました。
https://engineering.linecorp.com/ja/blog/you-dont-need-default-export/

npm run start で local で functions を起動して、http://localhost:8080/ でリクエストを受け付けます。

$ npm run start  

> functions_framework_ts_sample@1.0.0 start /xx
> functions-framework --source=build/src/ --target=mainHandler

Serving function...
Function: helloWorld
Signature type: http
URL: http://localhost:8080/

テスト

local での unit test の実行には、jest(ts-jest) + supertest を利用します。
メソッド単位でのテストに加えて、リクエストパスベースのテストも必要になるので supertest が必要になります。

jest(ts-jest)

下記の要領で必要なパッケージを一通りインストールします。
今回は、ts-jest (jest の Typescript 用のプリプロセッサ) を利用して Typescript でテストコードを書いていきます。

$ npm install --save-dev typescript jest ts-jest @types/jest

インストールが完了したら、ts-jest を利用するために jest.config.js に設定を追記していきます。
設定内容は下記のリンクを参考にしています。

jest.config.js
module.exports = {
	globals: {
		"ts-jest": {
			"tsConfig": "tsconfig.json"
		}
	},
	clearMocks: true,
	preset: "ts-jest",
	testEnvironment: "node",
	transform: {
		"^.+\\.(ts|tsx)$": "ts-jest"
	},
	testMatch: [
		"**/test/**/*.test.ts"
	]
}

supertest

次に、supertest 関連のパッケージもインストールしていきます。
supertest は HTTP request を投げるようなパスベースのテストを実施する際に利用します。

https://github.com/visionmedia/supertest

こちらも、必要なパッケージを一通りインストールします。

$ npm install supertest @types/supertest --save

テストするコードは下記を利用します。
Function Framework 側で提供されている HttpFunction の interface を利用して下記のように処理を書きます。
https://github.com/GoogleCloudPlatform/functions-framework-nodejs/blob/master/src/functions.ts#L21

handler.ts
import {HttpFunction} from '@google-cloud/functions-framework/build/src/functions'

export const mainHandler: () => HttpFunction = () => {
  return (req, res) => {
    try {
      res.status(200).send(`Hello, ${req.query.user ?? 'Cloud Functions'}`)
    } catch (error) {
      console.log("Error: ", error);
      res.status(500).send(error);
    }
  }
}

テストコードに関しては、下記のように書きます。
(/ で GET の request を送った際の HTTP Status と Response の中身をチェックしています)

handler.test.ts
import { getServer } from '@google-cloud/functions-framework/build/src/server'
import { SignatureType } from '@google-cloud/functions-framework/build/src/types'

import { mainHandler } from '../src/handler';

const request = require('supertest')

it('mainHandler: returns "Hello, Cloud Functions" string', () => {
	const name: string = 'test';
	const req: any = {
		query: {},
		body: {
			name: name,
		},
	};
	const app = getServer(mainHandler(), SignatureType.HTTP)

	request(app)
		.get('/')
		.then((response: any) => {
			expect(response.statusCode).toBe(200);
			expect(response.text).toStrictEqual("Hello, Cloud Functions");
		})
});

ここでも、Function Framework 側で提供されている getServer() を用いて http.Server を生成して supertest の request() に渡されています。
(実際の getServer は下記のようになっています。)

/**
 * Creates and configures an Express application and returns an HTTP server
 * which will run it.
 * @param userFunction User's function.
 * @param functionSignatureType Type of user's function signature.
 * @return HTTP server.
 */
export declare function getServer(userFunction: HandlerFunction, functionSignatureType: SignatureType): http.Server;

最後に一通り設定が完了した上で、package.json に test script をはやしてテストを実行します。

$ npm run test

> functions_framework_ts_sample@1.0.0 test ...
> jest --detectOpenHandles

 PASS  test/handler.test.ts
  ✓ helloWorld: returns "Hello, Cloud Functions" string (48 ms)
  ✓ helloWorld: returns "Hello, {string}" string (9 ms)

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        2.822 s, estimated 3 s

デプロイ

テストが完了したら、最後は実際に Cloud Function 上にデプロイを行います。
やり方は色々あるかと思いますが、一例として自分は concurrently を利用して、デプロイまでの一連の処理を実行できるようにしています。
( npm_package_config_project_id の部分は、package.jsonで config を定義して環境変数的にセットした値を渡しています。)

https://www.npmjs.com/package/concurrently

  "config": {
    "func_name": "mainHandler",
    "project_id": "xxx"
  },
  ...
  "scripts": {
    "start": "functions-framework --source=build/src/ --target=mainHandler",
    "test": "jest --detectOpenHandles",
    "check": "gts check",
    "clean": "gts clean",
    "compile": "tsc -p .",
    "deploy": "concurrently \"npm run clean\" \"npm run compile\" \"gcloud functions deploy $npm_package_config_func_name --project $npm_package_config_project_id --allow-unauthenticated --runtime nodejs12 --trigger-http --entry-point mainHandler\"",
    "fix": "gts fix"
  },

そして、npm run deploy を実行すると正常にデプロイされていることが確認できます。

npm run deploy

> functions_framework_ts_sample@1.0.0 deploy ...
> concurrently "npm run clean" "npm run compile" "gcloud functions deploy $npm_package_config_func_name --project $npm_package_config_project_id --allow-unauthenticated --runtime nodejs12 --trigger-http --entry-point mainHandler"

[1] 
[1] > functions_framework_ts_sample@1.0.0 compile ...
[1] > tsc -p .
[1] 
[0] 
[0] > functions_framework_ts_sample@1.0.0 clean ...
[0] > gts clean
[0] 
[0] version: 14
[0] Removing build ...
[0] npm run clean exited with code 0
[1] npm run compile exited with code 0
[2] Deploying function (may take a while - up to 2 minutes)...
[2] .
[2] .................................................done.
...
[2] status: ACTIVE
[2] timeout: 60s
[2] updateTime: '2021-06-27T13:20:02.696Z'
[2] versionId: '1'
[2] gcloud functions deploy mainHandler --project xxx --allow-unauthenticated --runtime nodejs12 --trigger-http --entry-point mainHandler exited with code 0

最後に

Functions Framework を利用して開発 + テストからデプロイまでの環境整備を実施した際の作業ログをまとめました。
Functions Framework 自体の利便性も含めて、開発しやすい環境作りができたかと思います。

他にもこんな方法がある、改善点などあればコメントいただけると幸いです。

Discussion