Sansan Tech Blog

Sansanのものづくりを支えるメンバーの技術やデザイン、プロダクトマネジメントの情報を発信

Cloud Pub/Subにおけるペイロードラップ解除のローカル実行環境でハマったこと

はじめに

こんにちは、技術本部 Bill One Engineering Unitの石川です。

Bill OneのComplicated Sub Systemチーム(通称Kombu)に所属していて、システム全体の課題を解決しつつメインでは請求書検索の改善に取り組んでいます。

本記事は Sansan Advent Calendar 2023 の4日目の記事になります。

adventar.org

Bill Oneでは非同期処理をCloud Tasksを使って実装していますが、アクセスがスパイクした時や大量のデータを処理する時などにCloud Tasksの発行に時間がかかり、ボトルネックになることが増えてきました。 その解決策としてCloud Pub/Subの導入が進められていたのですが、以下の課題があり保留状態でした。

Trace Contextを引き継げない

Bill Oneでは分散トレーシングを導入しており、Cloud TasksではHTTP HeaderでTrace Contextを指定することで、非同期処理の呼び出し元と呼び出し先のリクエストを紐づけることができていました。しかし、Cloud Pub/SubではHTTP Headerを直接指定できないのでひと工夫が必要でした。

Cloud Tasksが実行しているエンドポイントのrequest bodyをPubsubMessageのフォーマットに変える必要がある

稼働中のエンドポイントはPubsubMessageのフォーマットに対応していないため、何らかの手段でPubsubMessageのフォーマットにも対応する必要がありました。

これらを解決するためにPub/Sub push サブスクリプションのペイロード ラップ解除(メタデータ書き込み)を利用するのですが、今回は導入時にハマったことについて書いていきます。

目次

TL;DR

  • ローカル環境ではPubSub Emulatorを使うが、ラップ解除に対応していない(執筆時点)
  • EmulatorとApplicationの間にproxyを挟んで、ラップ解除時の挙動を再現した
  • proxyにはenvoyを使い、ラップ解除とメタデータ書き込み相当の機能をLuaで実装した

Cloud Pub/Subのラップ解除機能について

cloud.google.com

Bill OneではPubSubをPushサブスクリプションで使っており、以下のようなPubsubMessageの型でSubscriberへデータがPOSTされます。

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

PubsubMessage  |  Cloud Pub/Sub Documentation  |  Google Cloud

dataにはSubscriberへのmessageがbase64でエンコードされたデータが格納されているのですが、ラップ解除を指定するとbase64デコードされたデータがそのままrequest bodyとして送られてきます。 これを使うことで、Subscriber側のエンドポイントでPubsubMessageに対応する必要がなくなりました。

また、ラップ解除にはメタデータ書き込みのオプションがあります。 これはPubsubMessageのattributesに指定されたデータを全てリクエストヘッダーとして指定してくれるもので、attributesにTraceContextを指定することで解決できそうです。

Emulatorでハマったこと

GCP上の検証環境では問題なくラップ解除の機能が想定通り動いているのですが、ローカル環境で使用しているPubSub Emulatorで問題が発生しました。

Emulatorがラップ解除に対応しておらず、ローカルで動かせないことがわかりました。

cloud.google.com

どう解決したか

PubSub EmulatorがOSSであればPRを送ることもできましたが、今回はEmulatorとApplicationの間にproxyを挟み、そこでラップ解除とメタデータ書き込みを実装することで解決しました。

proxyで機能を代替するため、jsonをparseできること・base64 decodeできること、くらいのscriptが書けるもので手軽にproxyサーバーが立てられるものであればなんでも良さそうです。 候補としてはnginxとenvoyがあり、ローカル環境でしか使わないためどちらでも問題ないと判断し、今回はenvoyを採用しました。

あとは実装するだけなので、以下のようにdockerでenvoyを立てて、luaでscriptを書いてrequest body/headerを書き換えればOKです!

Dockerfile

FROM envoyproxy/envoy:v1.27-latest

RUN apt update && apt install -y luarocks \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/*
RUN luarocks install base64 \
    && luarocks install lua-cjson

COPY envoy.yaml /etc/envoy/envoy.yaml

CMD ["envoy", "-c", "/etc/envoy/envoy.yaml"]

envoy.ymlでのlua script

http_filters:
  - name: envoy.filters.http.lua.unwrap_request
    typed_config:
      "@type": type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua
      default_source_code:
        inline_string: |
          function envoy_on_request(request_handle)
            local json = require 'cjson'
            local base64 = require 'base64'
            local body = request_handle:body()
            local bodyString = tostring(body:getBytes(0, body:length()))
            local body = json.decode(bodyString)
            if body["subscription"] ~= nil and body["message"] ~= nil and body["message"]["data"] ~= nil and body["message"]["attributes"] ~= nil then
              local attributes = body["message"]["attributes"]
              for key, value in pairs(attributes) do
                request_handle:headers():replace(key, value)
              end
              local data = body["message"]["data"]
              local requestBody = base64.decode(data)
              local content_length = request_handle:body():setBytes(requestBody)
              request_handle:headers():replace("content-length", content_length)
            end
          end

所感

プロダクションコードへの影響を抑えられて、かつEmulatorがラップ解除への対応をした時に剥がしやすくなっており、シンプルに解決できたのではないかと思っています。

今回の対応でローカルでもラップ解除が使えるようになり、PubSubの導入を進めることができました。 ラップ解除を利用できることで、Subscriber側のPubsubMessageに対応するコストが抑えられたのも嬉しいポイントでした。

おわりに

今回はCloud Pub/Subのペイロードラップ解除を利用しようとしたときにローカル環境のPubSub Emulatorが対応していなくてハマったことを書かせていただきました。 稼働中のシステムにPub/Subを導入する場合はラップ解除を利用するケースがあると思いますが、今回ハマったことで導入を見送るケースもあるかと思います。 そういった方々に少しでも役に立てれば幸いです。

最後まで読んでいただき、ありがとうございました!

© Sansan, Inc.