yashiganiの英傑になるまで死ねない日記

週末はマスターバイクでハイラルを走り回ります

Cloud FunctionsのNode.jsランタイムを使うときに毎回設定していること

Cloud Function使ってますか?簡単なサービスをシュッと作って公開するにはとても便利ですよね。筆者もよく使っています。今やGCPの中では一番手に馴染んだサービスのひとつです。 最近Cloud Functionsを使っていくつかサービスを作っていて、最初にすることが決まってきたのでご紹介します。

TypeScriptの導入

初手はTypeScriptの導入です。みなさんはTSを導入するときどうしていますか?入れるだけなら簡単なんですが、ESLintだとかPrettierだとか考え始めるとゾッとしますよね。これから楽しくプログラミングを始めるのにゾッとはしたくないので、で頭をカラッポにしてgtsを使います。gtsというのはgoogleが作っているいい感じにTypeScriptを使えるやつで、入れておくだけでそれはそれはいい感じになります。詳しくはこのエントリーを見てくれ。npx gts initしたらだいたいいい感じになりますが、ちょっとだけいじります。

テストを書きたいのでgtsが作ってくれたtsconfig.jsonexcludeを追加します。

{
  "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にインストールするように設定すると良いです。

github.com

エントリーポイント

今回は試しに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トリガーの関数の型が定義されているのでありがたく使わせてもらいましょう。

github.com

ローカル実行

JavaScriptなら簡単なんですが、TypeScriptではちょっと面倒です。公式のドキュメントで良い例が紹介されているので真似してnpm scriptsにstartwatchを設定します。

"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を使うとシュッと作れていいんですが、ローカル開発の環境をきちんと整えようとかユニットテストをちゃんと書こうとか思うとちょっと考えることが多いので普段からよくやるやつをまとめてみました。ぼくのかんがえたさいきょうのシリーズなんですが、少しばかりでも皆様のお役に立つことがあると幸いです。

ここまでに紹介した知見をまとめたリポジトリを公開しました。テンプレートリポジトリというやつにしてみたので、これを使ったらすぐに楽しくプログラミングできると思います。もしよければ使ってみて下さい。

github.com

PR

筆者の所属する株式会社はてなでは、現在Webアプリケーションエンジニアを絶賛募集しています。

hatenacorp.jp

もし興味があれば応募してみてください。一緒にGCPを使ったプロダクトを開発しましょう。

また、恒例のはてなインターンシップは今年も元気に開催されます。

developer.hatenastaff.com

応募お待ちしています。一緒に東京オリンピックより熱い夏を過ごしましょう。