selmertsxの素振り日記

ひたすら日々の素振り内容を書き続けるだけの日記

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

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

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

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

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

スクリーンショット 2019-06-26 16.07.17.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

このあたりの実装を読んでみると、内部でexpressサーバーを起動して、利用者が作成した functionをラッピングしていることが見て取れます。実行は簡単で、ラッピングしたいfunctionを下記のように指定してコマンドを実行すれば動きます。

npx functions-framework --target=helloWorld

実際に動かしてみる

プログラムの用意

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

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);
}
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のフォーマットについては、公式ドキュメントによると下記のように指定されています。

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

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

$ echo -n "hogehoge" | base64
aG9nZWhvZ2U=

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

{
  "data": "aG9nZWhvZ2U="
}

Local環境でCloud Functionsを起動する

公式のドキュメントによると、functions frameworkを起動する際のoptionは --port, --target, --signature-typeの3点です。ここでは実行したい functionは slack_reporterであり、トリガーはpubsubにしたいので、下記のように設定をしました。

$ 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上で、どのようにmessageを送信することが適切なのか議論されています。ということで、これから記述する方法は将来正しくない方法になってしまう可能性がありますが、とはいえ今試す必要がある人がいるとも思うので書いておきます。

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

$ 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に通知されました。めでたしめでたし。

スクリーンショット 2019-06-26 18.47.51.png

この理由については、このあたりのコードに書いてあるのですが、もうちょっと追加調査したいことがあるので、またの機会に書こうと思います〜。