functions-framework を利用したGoogle Cloud Functionsにおいてpubsubのテストをする方法

## このドキュメントに書いてあること

これまで Google Cloud Functionsをローカル環境でテストするときは、[cloud-functions-emulator ](https://github.com/googlearchive/cloud-functions-emulator) という公式で提供されているツールが一般的に利用されていました。しかしながらこのツールは現在archiveされており、作者が2019年5月16日に作成した issueによると [functions-framework](https://github.com/GoogleCloudPlatform/functions-framework-nodejs) という新しいツールの利用を推奨しています。

このドキュメントでは、functions-framework を利用して Google Cloud FunctionsをLocalから実行する方法、特に[公式では提供されていない](https://github.com/GoogleCloudPlatform/functions-framework-nodejs/pull/40) eventsトリガーを利用してpubsubのメッセージを読み込ませる方法について記載します。

※ なお、2019年6月26日になっても公式ドキュメントでは `@google-cloud/functions-emulator`を利用するようにと書かれています。
https://cloud.google.com/functions/docs/emulator?hl=ja#getting_started

<img width="700" alt="スクリーンショット 2019-06-26 16.07.17.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/23606/9a2a6c82-7ac4-e002-2646-50a3c37cd2eb.png">


## functions-frameworkとは？

Node.jsを利用してFaaSを書くためのOSSのフレームワークです。このフレームワークを利用して書かれたコードは Cloud Functionsだけでなく、Cloud Runなどでも利用することができます。

そのようなことを目的としていると公式ドキュメントには書かれているものの、Cloud Run複数のAPIを持つ場合のケースに対応しておらず、現状では Cloud Functionsを Localで起動するためのツールとして使われることがメインになるかと思います。Cloud Functionsでの利用であれば、Localの開発環境のみinstallするだけですぐに利用することができます。

```
npm i -D @google-cloud/functions-framework
```

[このあたりの実装](https://github.com/GoogleCloudPlatform/functions-framework-nodejs/blob/master/src/invoker.ts#L549-L577)を読んでみると、内部でexpressサーバーを起動して、利用者が作成した functionをラッピングしていることが見て取れます。実行は簡単で、ラッピングしたいfunctionを下記のように指定してコマンドを実行すれば動きます。

```console
npx functions-framework --target=helloWorld
```

## 実際に動かしてみる
### プログラムの用意

僕が実際に利用しているプログラムからの抜粋です。cloud pubsubのmessageを受け取った後、その中身を見てSlackに通知しています。

```typescript
export async function slack_reporter(data: any) {
  const dataBuffer = Buffer.from(data.data, "base64");
  const body = dataBuffer.toString("ascii");
  const client = await SlackClient.create();
  await client.post(body);
}
```

```typescript
import { WebAPICallResult, WebClient } from "@slack/client";

export class SlackClient {
  public static async create(): Promise<SlackClient> {
    if (!this.instance) {
      const token = process.env.SLACK_TOKEN as string;
      const channel = process.env.SLACK_CHANNEL_ID as string;
      this.instance = new SlackClient(token, channel);
    }

    return this.instance;
  }

  private static instance: SlackClient;

  private slackCleint: WebClient;
  private channel: string;

  constructor(token: string, channel: string) {
    this.slackCleint = new WebClient(token);
    this.channel = channel;
  }

  public async post(text: string): Promise<WebAPICallResult> {
    return this.slackCleint.chat.postMessage({
      username: "ERP-HR Bot",
      channel: this.channel,
      text,
    });
  }
}
```

### pubsubのmessageを作成する

pubsubで送信されるmessageのフォーマットについては、[公式ドキュメント](https://cloud.google.com/pubsub/docs/reference/rest/v1/PubsubMessage?hl=ja)によると下記のように指定されています。

```json
{
  "data": string,
  "attributes": {
    string: string,
    ...
  },
  "messageId": string,
  "publishTime": string
}
```

このように色々なパラメータが存在していますが、今回のケースで利用するのは `data` のみです。理由については公式ドキュメントに記載されています。ということで messageを作成していきます。この `data`パラメータはbase64でエンコードする必要があるため、下記のコマンドでエンコードします。

```console
$ echo -n "hogehoge" | base64
aG9nZWhvZ2U=
```

これをmessageで送信するjsonに組み込むと下記のようになります。

```mock_pubsub.json
{
  "data": "aG9nZWhvZ2U="
}
```

### Local環境でCloud Functionsを起動する

[公式のドキュメント](https://github.com/GoogleCloudPlatform/functions-framework-nodejs#configure-the-functions-framework)によると、functions frameworkを起動する際のoptionは --port, --target, --signature-typeの3点です。ここでは実行したい functionは `slack_reporter`であり、トリガーはpubsubにしたいので、下記のように設定をしました。

```console
$ npx functions-framework --target=slack_reporter --signature-type=event

Serving function...
Function: slack_reporter
URL: http://localhost:8080/
```

### 呼び出し

Cloud FunctionもLocalで起動したので、次は起動しているCloud Functionを実行していきます。ここで Functions Frameworkのissueを読んでいくと次のようなissueが見つかります。

https://github.com/GoogleCloudPlatform/functions-framework-nodejs/issues/37

そして[こちらのPR上](https://github.com/GoogleCloudPlatform/functions-framework-nodejs/pull/40)で、どのようにmessageを送信することが適切なのか議論されています。ということで、これから記述する方法は将来正しくない方法になってしまう可能性がありますが、とはいえ今試す必要がある人がいるとも思うので書いておきます。

curlを使って下記のように実行すると、Localで動いているCloud Functionsが pubsubのメッセージを読み込むことができます。

```console
$ curl -X POST -H 'Content-Type:application/json; charset=utf-8' \
  -H 'ce-type: xxx' \
  -H 'ce-specversion: xxx' \
  -H 'ce-source: xxx' \
  -H 'ce-id: xxx' \
  -d "$(cat mock_pubsub.json)" http://localhost:8080
```

すると、こんな感じでslackに通知されました。めでたしめでたし。

<img width="345" alt="スクリーンショット 2019-06-26 18.47.51.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/23606/6421823e-b1a2-5bb4-ffd4-ebb08373d5d4.png">

この理由については、[このあたりのコード](https://github.com/GoogleCloudPlatform/functions-framework-nodejs/blob/master/src/invoker.ts#L412-L469)に書いてあるのですが、もうちょっと追加調査したいことがあるので、またの機会に書こうと思います〜。