GCSの変更をPub/Sub経由でAppEngineに通知する


SENSY株式会社のwasnotです。

さて、Adventカレンダー、みんな埋めてくれないので作成者として埋めていきます。
ネタがあまりないので、先日の[DatastoreのExport機能でバックアップの自動化](https://qiita.com/wasnot/items/c2992dfc6f2919181319)の続きとして、
Datastoreのバックアップが完了した時にGAE/SEアプリに通知を送りたいと思います。

GAE/SEを使うのは単にこのために外部サービスやサーバを立てたくないからです。
先日のDatastoreバックアップのフックもSEでしたね。

# GCSの変更通知

先日はDatastoreのExport機能を使ってGoogle Cloud Storage(GCS)のバケットに保存しました。
このイベントはGCSについている変更通知機能を使って取得できます。

この機能にも先日と同じように既存の通知機能とベータ版のpub/sub連携版があります。
今回は新しそうなpub/sub連携版を試したいと思います。

## 既存のオブジェクト変更通知

現在のGA機能にもGCS上の、アップロードや削除などのファイル変更の通知を
webhookとして受け取ることのできる機能が備わっています。

- [オブジェクト変更通知](https://cloud.google.com/storage/docs/object-change-notification?hl=ja)

これは以下のようなgsutilコマンドで設定できます。

```shell-session
> gsutil notification watchbucket [-i ChannelId] [-t ClientToken] ApplicationUrl gs://BucketName
```

これで終わりだったら簡単なんですが、
webmasterツールでwebhook先アプリのドメインをホワイトリストに追加したりする必要があるようです。

詳しいことはこちらでもわかりやすく解説されています。

[Object Change NotificationをApp Engineで受け取る設定](https://qiita.com/sinmetal/items/0438203034a0cb448448)

## Cloud Pub/Sub Notifications for Cloud Storage

できることはほぼ同等なのですが、最近はGCSからの変更通知をpub/subにpushする機能も備わっています。

- [Google Cloud Storage 用 Cloud Pub/Sub Notifications](https://cloud.google.com/storage/docs/pubsub-notifications?hl=ja)

今まで自分の指定したアプリへのwebhookを直接していたところをpub/sub経由で行うようになった、
という感じだと思っています。（違ったらすみません）

こちらはpub/subを経由することでコンソール上でpub/subのtopicを見たり設定できます。
また、一つのtopicから複数のwebhookへpushもできるのでより柔軟になった、と考えることができそうです。

さらに後述しますが、pub/subを経由することで、同一のプロジェクトなら、AppEngine/SEのドメインはホワイトリストに追加不要でpushできます！
これは手間が減るのでとてもいいかな、と思います。


# 設定
## 変更通知をpub/subへ登録

まず、オブジェクトの変更を通知する機能を登録します。

[オブジェクト変更の登録](https://cloud.google.com/storage/docs/reporting-changes?hl=ja)

これは既存の登録コマンドととても似ていて、gsutil経由で行います。

```shell-session
> gsutil notification create -t [TOPIC_NAME] -f json gs://[BUCKET_NAME]
```

`watchbucket`を設定する、ではなく`notification`を作成する、という感じでしょうか。
`-f json`は通知の中身をjsonで受け取るかどうかを指定するものです。
`json|none`なので指定しないと、bodyが何も送られてこないと思います。

### projectの指定が必要な場合がある。

上記の記事の説明だと-tのオプションはpub/subのトピック名だけを指定すればいいのかと思っていました。

しかし、下記のように怒られてしまいました。
今回は`my-datastore-backup`バケットに`datastore-backup`という名前のトピックを作成した例です。

```shell-session
> gsutil notification create -t datastore-backup -f json gs://my-datastore-backup

You are attempting to perform an operation that requires a project id, with none configured. Please re-run gsutil config and make sure to follow the instructions for finding and entering your default project id.
```

今回は自分のアカウントで叩いていましたが、projectを複数持っていて、gcloudコマンドにデフォルトproject idを設定していないせいかもしれません。
他のgsutilのように`-p`オプションはproject_id指定ではなく、prefix指定になっていたので個別には指定できませんでした。

`gsutil help notification`の例にあるように、topicsの名前を絶対パスで指定すると設定できました。
プロジェクトIDが`my-project`だとすると以下のようにしたら作成されます。

```shell-session
> gsutil notification create -f json -t projects/my-project/topics/datastore-backup gs://my-datastore-backup
Created Cloud Pub/Sub topic projects/my-project/topics/datastore-backup
Created notification config projects/_/buckets/my-datastore-backup/notificationConfigs/2


> gsutil notification list gs://my-datastore-backup
projects/_/buckets/my-datastore-backup/notificationConfigs/2
	Cloud Pub/Sub topic: projects/my-project/topics/datastore-backup
```

この状態で[コンソールのPub/sub画面](https://console.cloud.google.com/cloudpubsub/topicList)を見るときちんと追加されています。
![スクリーンショット_2017-12-04_16_18_54_2_2.png](https://qiita-image-store.s3.amazonaws.com/0/42594/c6059839-3748-e2a6-b380-49f30ad5ffa7.png)



## サブスクリプションの登録

次はGCSではなくPub/Subの話題です。

Pub/Subの機能については特に書きませんが、topicへの通知をsubscriptionを設定して他のendpointにpushする方法はこちらに書かれています。

- [push サブスクライバー ガイド](https://cloud.google.com/pubsub/docs/push)

このページの[App Engine スタンダード環境のエンドポイント](https://cloud.google.com/pubsub/docs/push#app-engine-standard-endpoints)の項目にGAE/SEで受け取る方法についてヒントが書いてあります。

ここにあるように、同一プロジェクトであればpush先のドメインをホワイトリストに登録するのが不要になります！
なのでpushのサブスクリプションを作ってappengineのエンドポイントを設定すれば完了になります。

上の画面で確認したtopicをクリックして、
![スクリーンショット_2017-12-04_16_19_13_2_2.png](https://qiita-image-store.s3.amazonaws.com/0/42594/fb39c8e9-7c59-f5f9-58a9-51e316c90002.png)

「サブスクリプションを作成」をクリック

![スクリーンショット 2017-12-04 18.05.48_2.png](https://qiita-image-store.s3.amazonaws.com/0/42594/eda8c905-53d6-d89d-7b28-d85e3691475e.png)
サブスクリプション名を設定（ここでは`finish-datastore-backup`）、
配信タイプを「エンドポイントURLにpush」を選択しpush先のendpointを登録します。

これで変更時に設定したエンドポイントに向けて通知が届きます。


## GCSからの通知を受け取る

基本的に先ほどの公式ページに[通知形式](https://cloud.google.com/storage/docs/pubsub-notifications?hl=ja#format)について記載があります。
前の項で設定したendpointにPOSTで通知が来ます。

例を示すとこのような感じのjsonが届きます。

```:header
Accept-Charset: UTF-8 
Content-Length: xxx 
Content-Type: application/json 
Host: my-project.appspot.com 
User-Agent: CloudPubSub-Google 
X-Appengine-Country: ZZ 
X-Cloud-Trace-Context: xxx/xxx;o=1
```


```json:body
{
	"message": {
		"data": "***",
		"attributes": {
			"objectGeneration": "1512372633478813",
			"bucketId": "my-datastore-backup",
			"eventType": "OBJECT_FINALIZE",
			"notificationConfig": "projects/_/buckets/my-datastore-backup/notificationConfigs/1",
			"payloadFormat": "JSON_API_V1",
			"objectId": "test.txt"
		},
		"message_id": "178388778619664",
		"messageId": "178388778619664",
		"publish_time": "2017-12-04T07:30:33.720Z",
		"publishTime": "2017-12-04T07:30:33.720Z"
	},
	"subscription": "projects/my-project/subscriptions/finish-datastore-backup"
}
```

よく使うのは`objectId`や`eventType`あたりですね。

eventTypeは[イベントの種類](https://cloud.google.com/storage/docs/pubsub-notifications?hl=ja#events)に記載されていますが、4種類あるようです。

- OBJECT_FINALIZE: アップロード完了イベント。失敗は含まず。
- OBJECT_METADATA_CHANGE: メタデータ変更時。
- OBJECT_DELETE: 削除時。上書き時にも送信される。バージョニング有効時のバージョン削除はARCHIVEが送信される。
- OBJECT_ARCHIVE: バージョニング有効時のみ、上書き等で送信される。


### 注意点: admin権限について

pub/subとGAE/SEのヒントでは、pub/sub用のendpointがcurl等で叩かれないために、
app.yamlでadmin権限を付与することを推奨しています。

```yaml:app.yaml
handlers:
- url: /_ah/push-handlers/.*
  script: main.APPLICATION
  login: admin
```

そして、その際に`/_ah/push-handlers/`プレフィックスが必須であると書いてあります。
最初読んだときはこのプレフィックス自体が必須ではなく、admin制限さえやっていればいいのかな、と思いましたが、このプレフィックスは**必須**でした。

具体的には別のprefixやちょっとでもtypoしたりするとredirect loopが起きてしまいました。
<img width="661" alt="スクリーンショット 2017-12-04 18.25.43.png" src="https://qiita-image-store.s3.amazonaws.com/0/42594/6f414f7b-0bb3-df7f-0e4f-ad077110c0a4.png">
typoで`/ah/push-handlers/`にしてしまったり、

<img width="593" alt="スクリーンショット 2017-12-04 18.32.19.png" src="https://qiita-image-store.s3.amazonaws.com/0/42594/40e3a024-8075-2427-15b3-815f78ee9dab.png">
`/_ah/.*`でいいだろうと他のprefixを指定していたら、ループで、いつまでも回り続けてしまいます。
しかも10秒以内に302レスポンスが返ってくるので失敗にもならずに気付きにくいですね。。

ちなみに`login:admin`を外せば普通に動きます。
でもあまり良くないので、決め打ちのprefixに従っておいた方が良さそうです。

taskqueue/cronのように、どんなendpointに対してもadmin権限が付与されて送信されるわけではないので注意が必要でした。


# まとめ

今回はGCSのpub/sub連携とpub/sub->appengineの連携機能を触ってみました。
特にpub/sub->appengineなどは今更感ありますが、taskqueueとの違いなどもわかってよかった、と思っています。

この記事ではappengine/SE側のコードを書かなかったですね。
endpointがappengineなだけなので、FEとかでもホワイトリスト設定なしに動くと思います。

サンプルプロジェクトもとても参考になりました。

- [cloud-pubsub-samples-python](https://github.com/GoogleCloudPlatform/cloud-pubsub-samples-python/)

以上です。