selmertsxの素振り日記

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

TypeScriptで書かれているCloud FunctionsからCloud PubSubのREST APIを叩く

この記事に書いてあること

この記事には、 TypeScriptで書かれているCloud FunctionsからCloud PubSubのAPIを叩く方法 が書かれています。それだけのことなのですが、現在GCPから公式で提供されているライブラリで実現するにはとても大変でした。

僕が把握している限り、firestoreを利用する際も同じ問題が発生しています。そのような問題に対処する際に参考になればと思い書きました。

概要

TypeScriptで書かれたCloud Functionsから、任意の条件を満たしたときに Cloud PubSubの特定のTOPICに対してメッセージを送信しようとしました。Cloud PubSubをnodeで利用する際、公式から提供されているのは @google-cloud/pubsub というライブラリです。しかしながら、このライブラリはCloud Functionsで利用することは難しいです。なぜなら、@google-cloud/pubsub で利用されている node-pre-gyp は、webpackでの利用を想定していないからです。(きっと現状、多くの人が Cloud Functions のbundleにはwebpackを利用していることでしょう! )

No. I designed node-pre-gyp and I've never used webpack nor do I understand what it is. So, its definitely not supposed to work. That said if it is feasible to get it working, I'd review a PR with tests. Until then I'll close this issue to avoid confusion/the assumption that things should work. (Issueのコメントから抜粋)

そこで、今回はCloud Functionsの中からCloud PubSub のAPIを直接実行するような方法で実装を行いました。以下、その詳細について記述します。

環境

  • Cloud Functions: runtime=nodejs8
  • TypeScript
  • webpackでbundle
  • Cloud Functionsを実行するサービスアカウントには、任意のPubSub Topicに対してメッセージを送信する権限を付与済み

問題

@google-cloud/pubsub を利用して特定のtopicに対してpubsubをするとき、local環境で ts-node を使って実行すると問題なく動作しました。しかしながら、webpackでbundleして cloud functionsにデプロイしようとしたところ、下記のようなエラーが出ました。

Detailed stack trace: Error: package.json does not exist at /package.json
    at Object.exports.find (webpack:///./node_modules/grpc/node_modules/node-pre-gyp/lib/pre-binding.js?:18:15)
    at Object.eval (webpack:///./node_modules/grpc/src/grpc_extension.js?:29:12)
    at eval (webpack:///./node_modules/grpc/src/grpc_extension.js?:63:30)
    at Object../node_modules/grpc/src/grpc_extension.js (/srv/index.js:11604:1)
    at __webpack_require__ (/srv/index.js:20:30)
    at eval (webpack:///./node_modules/grpc/src/client_interceptors.js?:144:12)
    at Object../node_modules/grpc/src/client_interceptors.js (/srv/index.js:11557:1)
    at __webpack_require__ (/srv/index.js:20:30)
    at eval (webpack:///./node_modules/grpc/src/client.js?:35:27)
    at Object../node_modules/grpc/src/client.js (/srv/index.js:11545:1)

この問題について調査していったところ、node-pre-gypのissue にたどり着きました。

対応方法

node-pre-gyp がwebpackでの利用を想定していないということなので、 @google-cloud/pubsub の利用を諦めて、直接 REST APIでpubsubを実行することにしました。GCPにおいてリソースを操作する方法は REST APIとgRPCの2つあります。nodeでgRPCで実行する際には node-pre-gyp が必須となってしまうため、今回は REST API で Cloud PubSubを操作することにしました。

メッセージ送信

GCPから提供されている Cloud PubSubのREST APIドキュメントは下記になります。

https://cloud.google.com/pubsub/docs/reference/rest/v1/projects.topics/publish https://cloud.google.com/pubsub/docs/reference/rest/v1/PubsubMessage

こちらのドキュメントには、Cloud PubSubでメッセージを送信するために必要なパラメータは下記のようになると記載されています。(こちらは必要最小限のデータのみを載せています)

POST https://pubsub.googleapis.com/v1/projects/${project_name}/topics/${topic_name}:publish
{
  "messages": [ { "data": string(Base 64でエンコードされている文字列) } ]
}

REST APIで実施する場合は、認証をする必要があります。認証に関するドキュメントはこちらです。 https://developers.google.com/identity/protocols/OAuth2#serviceaccount

認証はOAuth 2.0で行う必要があります。GoogleのサーバーからAccessTokenを取得し、そのAccessTokenをAPI Requestのbearer トークンタイプとして渡す必要があります。このとき、API Requestは下記のようになります。

POST https://pubsub.googleapis.com/v1/projects/${project_name}/topics/${topic_name}:publish
Content-Type: "application/json"
Authorization: Bearer ${accessToken}

{
  "messages": [ { "data": string(Base 64でエンコードされている文字列) } ]
}

参考: OAuth2.0 rfc アクセストークンを利用したAPI Request

コード

上記 REST API をリクエストするnodeのコードは下記のようになります。googleapis にはアクセストークンを取得するメソッドが存在するので、簡単に実現することができました。

import { google } from "googleapis";
import axios from "axios";
const url = "https://pubsub.googleapis.com/v1/projects/${project_name}/topics/${topic_name}:publish";

async function main() {
  const token = await google.auth.getAccessToken();
  const data = { messages: [ { data: new Buffer("hogehoge").toString("base64") }]}
  const config = {
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${token}`,
    },
  };
  return await axios.post(url, data, config);
}

main();

結果

$ npx ts-node sample.ts
{ messageIds: [ '512512244825408' ] }

ということで、Cloud PubSubでメッセージを送信できていることを確認できました!(ちなみに、諸々の事情で載せてはいませんが、Cloud Functionsから実行しても問題なく動きました )