created: 2022-06-08T14:34:59.234Z

GCP の CloudStorage でオブジェクトの追加/更新イベントを PubSub で拾う

オブジェクトが変更されたら PubSub にメッセージがいくようにする

$ gsutil notification create -t myappb-notification -f json gs://myappb
Created Cloud Pub/Sub topic projects/myapp-123456/topics/myappb-notification
Created notification config projects/_/buckets/myappb/notificationConfigs/1
  • -t
    • PubSubのトピック名(新規に作成される)
  • -f
    • format の略
    • 通知のデータ形式。 json|nonenone を選ぶとpayloadがなくなるらしい

そのほか色々オプションがある。

Funcion をデプロイ

CloudFunctionの仕様は gcloud で関数をデプロイするためのnpmスクリプトでIaC管理するので terraform での管理はしないことにした。

$ gcloud functions deploy FUNC --trigger-topic MY_TOPIC --runtime RUNTIME

作成したリソースを terraform import

$ terraform import google_storage_notification.notification myappb/notificationConfigs/1
$ terraform import google_pubsub_topic.myappb_notification_topic myapp-123456/myappb-notification

実装後の tf ファイル

plan すると create/destroy となったが、まだ使い始める前なので create/destroy にしてもらった。

resource "google_storage_notification" "myappb_storage_notification" {
  bucket = google_storage_bucket.myappb.name
  topic = google_pubsub_topic.myappb_notification_topic.id
  payload_format = "JSON_API_V1"
}

resource "google_pubsub_topic" "myappb_notification_topic" {
  name = "myappb-notification"
}

CloudFunction に届く通知

ログを出すだけのコードを書いておく。

import type { EventFunction } from "@google-cloud/functions-framework/build/src/functions";
export const CsvHandler: EventFunction = async (data, event) => {
  console.log(JSON.stringify({ data, event }));
  return ;
};

たとえば /debug/test.txt にファイルを配置すると、関数が動いてこんな出力が得られる。

{
  "data": {
    "@type": "type.googleapis.com/google.pubsub.v1.PubsubMessage",
    "attributes": {
      "bucketId": "myappb",
      "eventTime": "2022-06-07T23:10:26.918668Z",
      "eventType": "OBJECT_FINALIZE",
      "notificationConfig": "projects/_/buckets/myappb/notificationConfigs/3",
      "objectGeneration": "1654643426818516",
      "objectId": "debug/test.txt",
      "payloadFormat": "JSON_API_V1"
    },
    "data": "ewogICJraW5kIjogInN0b3JhZ2Ujb2JqZWN0IiwKICAiaWQiOiAia25ldGIvZGVidWcvdGVzdC50eHQvMTY1NDY0MzQyNjgxODUxNiIsCiAgInNlbGZMaW5rIjogImh0dHBzOi8vd3d3Lmdvb2dsZWFwaXMuY29tL3N0b3JhZ2UvdjEvYi9rbmV0Yi9vL2RlYnVnJTJGdGVzdC50eHQiLAogICJuYW1lIjogImRlYnVnL3Rlc3QudHh0IiwKICAiYnVja2V0IjogImtuZXRiIiwKICAiZ2VuZXJhdGlvbiI6ICIxNjU0NjQzNDI2ODE4NTE2IiwKICAibWV0YWdlbmVyYXRpb24iOiAiMSIsCiAgImNvbnRlbnRUeXBlIjogInRleHQvcGxhaW4iLAogICJ0aW1lQ3JlYXRlZCI6ICIyMDIyLTA2LTA3VDIzOjEwOjI2LjkxOFoiLAogICJ1cGRhdGVkIjogIjIwMjItMDYtMDdUMjM6MTA6MjYuOTE4WiIsCiAgInN0b3JhZ2VDbGFzcyI6ICJTVEFOREFSRCIsCiAgInRpbWVTdG9yYWdlQ2xhc3NVcGRhdGVkIjogIjIwMjItMDYtMDdUMjM6MTA6MjYuOTE4WiIsCiAgInNpemUiOiAiNSIsCiAgIm1kNUhhc2giOiAiMk9qOG90d1BpVy9YeTB5d0F4dWlTUT09IiwKICAibWVkaWFMaW5rIjogImh0dHBzOi8vd3d3Lmdvb2dsZWFwaXMuY29tL2Rvd25sb2FkL3N0b3JhZ2UvdjEvYi9rbmV0Yi9vL2RlYnVnJTJGdGVzdC50eHQ/Z2VuZXJhdGlvbj0xNjU0NjQzNDI2ODE4NTE2JmFsdD1tZWRpYSIsCiAgImNvbnRlbnRMYW5ndWFnZSI6ICJlbiIsCiAgImNyYzMyYyI6ICIrcFQ5M3c9PSIsCiAgImV0YWciOiAiQ05UenZZMjduUGdDRUFFPSIKfQo="
  },
  "event": {
    "eventId": "4767758010631234",
    "eventType": "google.pubsub.topic.publish",
    "resource": {
      "name": "projects/myapp-123456/topics/myappb-notification",
      "service": "pubsub.googleapis.com",
      "type": "type.googleapis.com/google.pubsub.v1.PubsubMessage"
    },
    "timestamp": "2022-06-07T23:10:27.098Z"
  }
}

jsonPayload の中の data は base64 でエンコードされているJSONである。

$ pbpaste | jq -r .jsonPayload.data.data | base64 -d
{
  "kind": "storage#object",
  "id": "myapp/debug/test.txt/1654643426818516",
  "selfLink": "https://www.googleapis.com/storage/v1/b/myapp/o/debug%2Ftest.txt",
  "name": "debug/test.txt",
  "bucket": "myapp",
  "generation": "1654643426818516",
  "metageneration": "1",
  "contentType": "text/plain",
  "timeCreated": "2022-06-07T23:10:26.918Z",
  "updated": "2022-06-07T23:10:26.918Z",
  "storageClass": "STANDARD",
  "timeStorageClassUpdated": "2022-06-07T23:10:26.918Z",
  "size": "5",
  "md5Hash": "2Oj8otwPiW/Xy0ywAxuiSQ==",
  "mediaLink": "https://www.googleapis.com/download/storage/v1/b/myapp/o/debug%2Ftest.txt?generation=1654643426818516&alt=media",
  "contentLanguage": "en",
  "crc32c": "+pT93w==",
  "etag": "CNTzvY27nPgCEAE="
}

参考