🐈

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

8 min read

この記事では、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

ログインするとコメントできます