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](https://www.npmjs.com/package/@google-cloud/pubsub) というライブラリです。しかしながら、このライブラリはCloud Functionsで利用することは難しいです。なぜなら、`@google-cloud/pubsub` で利用されている `node-pre-gyp` は、[webpackでの利用を想定していない](https://github.com/mapbox/node-pre-gyp/issues/308)からです。

> 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](https://github.com/mapbox/node-pre-gyp/issues/308) にたどり着きました。

## 対応方法

`node-pre-gyp` がwebpackでの利用を想定していないということなので、 `@google-cloud/pubsub` の利用を諦めて、直接 REST APIでpubsubを実行することにしました。GCPにおいてリソースを操作する方法は REST APIとgRPCの２つあります。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

<img src="https://developers.google.com/accounts/images/serviceaccount.png">

認証は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 ](https://openid-foundation-japan.github.io/rfc6749.ja.html#token-types)


### コード

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

```typescript
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から実行しても問題なく動きました )