Functions Framework +α でサーバーレス開発環境を整える
この記事では、Typescript 初心者が Functions Framework を利用してサーバーレス開発環境を整備した際の作業ログをまとめています。
GCP の Cloud Functions での開発での開発を想定しています。
開発
開発時に必要な Linter などの基本設定は gts を利用して設定しています。
gts
gts は Google の TypeScirpt style guide と Linter の設定が入った npm package です。
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 などの主要言語に対応しています。
README.md
通りに、package を install した後には、scripts の部分を下記のように書き換えます。
target は実装したコード側で named exports している module (=Entrypoint) を指定しています。
"scripts": {
"start": "functions-framework --target=mainHandler"
}
余談ですが、default exports / named exports 周りの理解について、下記の記事が参考になりました。
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
に設定を追記していきます。
設定内容は下記のリンクを参考にしています。
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 を投げるようなパスベースのテストを実施する際に利用します。
こちらも、必要なパッケージを一通りインストールします。
$ npm install supertest @types/supertest --save
テストするコードは下記を利用します。
Function Framework 側で提供されている HttpFunction
の interface を利用して下記のように処理を書きます。
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 の中身をチェックしています)
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 を定義して環境変数的にセットした値を渡しています。)
"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