Cloud Function使ってますか?簡単なサービスをシュッと作って公開するにはとても便利ですよね。筆者もよく使っています。今やGCPの中では一番手に馴染んだサービスのひとつです。 最近Cloud Functionsを使っていくつかサービスを作っていて、最初にすることが決まってきたのでご紹介します。
TypeScriptの導入
初手はTypeScriptの導入です。みなさんはTSを導入するときどうしていますか?入れるだけなら簡単なんですが、ESLintだとかPrettierだとか考え始めるとゾッとしますよね。これから楽しくプログラミングを始めるのにゾッとはしたくないので、で頭をカラッポにしてgtsを使います。gtsというのはgoogleが作っているいい感じにTypeScriptを使えるやつで、入れておくだけでそれはそれはいい感じになります。詳しくはこのエントリーを見てくれ。npx gts init
したらだいたいいい感じになりますが、ちょっとだけいじります。
テストを書きたいのでgtsが作ってくれたtsconfig.json
にexclude
を追加します。
{ "extends": "./node_modules/gts/tsconfig-google.json", "compilerOptions": { "rootDir": ".", "outDir": "build" }, "include": [ "src/**/*.ts", "test/**/*.ts" ], "exclude": [ "src/**/*.test.ts", "test/**/*.test.ts" ], }
セミコロンは死んでも書きたくないので.prettierrc.js
にはsemi: false
を追加します。
module.exports = { ...require('gts/.prettierrc.json'), semi: false, };
これでTypeScriptの準備はできました。npm scriptsでやたらとコンパイルしたりlintしたりされるけど別に害はないのでこの段階では放っておきます。時間かかってうざいなと思ったらチューニングするか消すかしましょう。
ちなみに、yarnでmonorepoしている場合にはtsconfigやESLint、Prettierの設定ファイルからgtsの設定ファイルをうまく読み込めません。親packageのnode_modulesにgtsがインストールされてしまうからなんですが、このissueで紹介されているようにworkspaceの設定にnohoist
を指定して子packageにインストールするように設定すると良いです。
エントリーポイント
今回は試しにCloud Functionsで簡単なechoサーバーを作ってみます。index.ts
の他にecho.ts
を作ってこんな感じにしました。
// echo.ts import {HttpFunction} from '@google-cloud/functions-framework/build/src/functions' type Logger = Console export const echo: (logger?: Logger) => HttpFunction = logger => { return (req, res) => { logger?.info(req.query) res.status(200) res.send(`Hello, ${req.query.user ?? 'Cloud Functions'}`) } } // index.ts import {echo as _echo} from './echo' export const hello = _echo(console)
関数の処理をindex.ts
にベタ書きしてもいいんですが、関数の本体とCloud Functionsのエントリーポイントを分けるようにしています。Cloud FunctionsのNode.jsランタイムでは、複数の呼び出しで同じインスタンスが使いまわされることがあります。その際、グローバルスコープの変数は実行ごとに評価されませんのでキャッシュやもろもろのクライアントの初期化などに使えます。関数本体をエントリーポイントから分離しておくと、テストを気にせずグローバルスコープを使えるようになるので便利だからです。ここでは、type Logger = Console
としてconsole
をDIしているので、テストを気にせずログの書き出しができますね。
それと、関数本体の型にHttpFunction
を使っているのもポイントです。これは同僚のid:pokutunaに教わったんですが、@google-cloud/functions-frameworkにはhttpトリガーやpubsubトリガーの関数の型が定義されているのでありがたく使わせてもらいましょう。
ローカル実行
JavaScriptなら簡単なんですが、TypeScriptではちょっと面倒です。公式のドキュメントで良い例が紹介されているので真似してnpm scriptsにstart
とwatch
を設定します。
"scripts": { "start": "functions-framework --target=echo --source=build/src/ --port=8000", "watch": "concurrently \"tsc -w\" \"nodemon --watch ./build/ --exec yarn start\"" },
ドキュメントではnpmですが、yarnを使いたいのでyarnに書き換えます。デプロイするのが簡単なので未リリースのサービスならデプロイしてしまっても良いんですが、ローカルで動かせるようにしておくと何かと便利なので設定しておくと吉です。デプロイは数分くらいかかるし。他のGCPサービスと連携したければ、開発用のSAを用意してGOOGLE_APPLICATION_CREDENTIALS=... yarn watch
みたいな感じで実行してください。いつも通りですね。
テスト
もちろんテストは書きたいですよね。なるべくCloud Functionsの環境を再現してユニットテストを書くと実際のリクエストに近くなります。そこで、ここではsupertest
を使ってテストしたいと思います。
import { getServer, SignatureType, } from '@google-cloud/functions-framework/build/src/invoker' import supertest from 'supertest' import {echo} from './echo' describe('cloud function', () => { // テストなのでechoにloggerは差し込まない const app = getServer(echo(), SignatureType.HTTP) it('should return 200', async done => { supertest(app) .get('/echo') .expect(200) .expect('Hello, Cloud Functions', done) supertest(app) .post('/echo') .send({user: 'John'}) .expect(200) .expect('Hello, John', done) }) })
実は、Cloud Functionsのexpressはなんか色々良きにはからってくれます。その挙動についてはここに記載されていますが、詳細には書かれていません。そこで、functions-frameworkの実装を見てみましょう。こんな感じでbodyParser
が差し込まれていたり、オリジナルのリクエストボディがrequest.rawBody
に退避されていることがわかりますね。この辺りのCloud Functionsの挙動はよくわかんないなあと思っていたんですが、同僚のid:pokutunaがfunctions-frameworkの実装を見るといいよと教えてくれました。そりゃそうだろという感じなんですが、その手があったか!って思いますよね。ちょうど今眺めていたgetServer
を使えば簡単にsupertest
でテストが書けます。
また、ユニットテストではログが表示されても邪魔なだけなのでlogger
はDIしないでおきます。こんな感じでDIするポイントを作っておくと、後で依存を増やしたくなった時に楽ができていいですね。例えばGoogle Cloud Storageと連携した関数を作りたい場合、同様にGCSService
みたいなインターフェースを書いてDIすればよろしい。Cloud Functionsで作りたい程度の関数の規模なら素朴なDIで事足ります。ユニットテストではみんな大好きjest.fn()
を使ってモックしましょう。よかったですね。
Enjoy Cloud Functions!
そんなこんなで雛形ができたので、必要に応じてecho.ts
を改造していけばいい感じに関数が作れます。あとは楽しくプログラミングするだけです。めでたしめでたし。
まとめ
Cloud Functionsでサービスを作るときに毎回やってることを紹介しました。Cloud Functionsを使うとシュッと作れていいんですが、ローカル開発の環境をきちんと整えようとかユニットテストをちゃんと書こうとか思うとちょっと考えることが多いので普段からよくやるやつをまとめてみました。ぼくのかんがえたさいきょうのシリーズなんですが、少しばかりでも皆様のお役に立つことがあると幸いです。
ここまでに紹介した知見をまとめたリポジトリを公開しました。テンプレートリポジトリというやつにしてみたので、これを使ったらすぐに楽しくプログラミングできると思います。もしよければ使ってみて下さい。
PR
筆者の所属する株式会社はてなでは、現在Webアプリケーションエンジニアを絶賛募集しています。
もし興味があれば応募してみてください。一緒にGCPを使ったプロダクトを開発しましょう。
また、恒例のはてなインターンシップは今年も元気に開催されます。
応募お待ちしています。一緒に東京オリンピックより熱い夏を過ごしましょう。