Pub/Sub drivenなmicroserviceにPR単位の検証環境を導入した話

こんにちは。今年4月に入社したメルペイ Loyalty & Santa(Growth Platform)チームでBackend Engineerをしている@mikupoです。この記事は「Merpay&Mercoin Tech Openness Month 2026」の8日目の記事です!

はじめに

私が所属しているSantaチームは、メルカリ・メルペイにおけるポイント還元やキャンペーンの基盤となるシステムを開発・運用しています。Santaの処理はすべて非同期で、Pub/SubのPull型 subscriptionを中心に構成されています。

Santaの開発で課題になっていたのが、QAプロセスです。検証に使える開発環境(Dev環境)はチームに1つしかなく、複数の開発(Pull Request 以降、 PR) が重なるとQAを並列に進められませんでした。実装は終わっているのに、検証環境が空くのを待つ、そんな状況が発生していました。

PRごとに独立した環境を用意できれば、この待ち時間はなくせます。ただ、Santaでそれを実現するのは簡単ではありませんでした。Pull型のsubscriptionでは、複数のconsumerが同じsubscriptionに接続している場合、どのconsumerがどのmessageを受け取るかをpublisher側から指定できません。そのため、「このイベントはこのPR環境へ」と狙って届けることができません。

社内にはすでに、こうした課題に使えそうな仕組みもいくつかありました。ただ、それらを Santa に取り入れるには、本番の非同期処理の作りに大きく手を入れる必要がありました。今回達成したいのは QA の改善であり、そのために本番の非同期処理のかたちを大きく作り変えるのは避けたいと考えました。

本記事では、この制約のなかで「Pull型を保ったまま、PRごとに環境を分ける」 をどう実現したのかを紹介します。

Santaについてくわしく知りたい方は、「メルペイのキャンペーンを支えるサンタの秘密」をご確認ください。

PR単位の検証環境が必要になった背景

従来、Santaチームの検証に使えるDev環境は1つしかありませんでした。複数のPRを同じDev環境へ同時に反映すると、不具合が起きたときに原因となった変更を切り分けづらくなります。変更同士がconflictする場合は、そもそも並行してQAを進められませんでした。

このボトルネックを減らすための選択肢として、SantaではPull Request Replication Controller(PRRC)に注目しました。PRRCとは、GitHubのPRを起点としてDev環境とは別に、PRごとの検証環境(PRRC環境)を作成するためのKubernetes custom controllerを使った仕組みです。

PRRCによってPRごとの検証環境を作る道筋は見えました。次に検討したのが、社内で使われているPub/Sub gRPC Pusherです。Pub/Sub messageをgRPC requestとして扱えれば、既存のDynamic Service Routingと組み合わせてPRRC環境へルーティングできるように見えました

しかし、SantaのPub/Sub処理はPull型subscriptionを前提に作られています。今回解決したかったのはQAプロセスのボトルネックであり、本番の非同期処理の仕組みを大きく変えることではありませんでした。一方で、gRPC Pusherをそのまま使うには、既存のPub/Sub handlerをgRPC endpointとして受けられる形に変える必要があります。そのため、QA環境の改善として取り組むには、本番環境への影響や変更範囲が大きくなりすぎると判断しました。

そのため、私たちは既存のPull型Pub/Subを保ったままPR単位の検証環境を実現するために、Santa側で満たすべき要件を整理しました。

Pull型Pub/Subを保ったままPR単位の検証環境を作るための要件

既存のgRPC Pusherをそのまま使うのではなく、SantaのPull型 subscriptionを保ったままPR単位の検証環境を実現するには、まず満たすべき要件を整理する必要がありました。PRRC環境を実際の検証に使える状態にするには、messageを意図した環境で受け取れることと、PRRC向けの文脈を後続処理へ引き継げることが重要でした。

ここでは、この2つの要件を順に説明します。

要件1:Dev環境とPRRC環境でmessageを分けて受け取れること

1つ目の要件は、Dev環境で確認したいmessageと、特定のPRRC環境で確認したいmessageを分けて受け取れることです。PRごとの検証環境を作れたとしても、それぞれの環境で確認したいmessageが混ざってしまうと、どのPRの変更による挙動なのかを切り分けづらくなります。

messageが混ざる原因は、Pull型subscriptionの受け取り先をpublisher側から指定できないことにあります。図1のように、PRRC環境のconsumerをDev環境と同じsubscriptionにつなぐと、複数のconsumerが同じsubscriptionからmessageを受け取る構成になります。そのため、Dev環境で確認したいmessageをPRRC環境が受け取ったり、PRRC環境で確認したいmessageをDev環境が受け取ったりする可能性があります。

このため、PR単位の検証環境として使うには、messageを環境ごとに分けて受け取れる仕組みが必要になります。

要件2:PRRC環境向けのルーティング情報を後続処理へ引き継げること

2つ目の要件は、messageがどのPRRC環境向けなのかを、後続の処理にも引き継げることです。Santaの処理はPub/Sub messageを受け取って終わりではなく、処理の途中で他のmicroserviceをgRPCで呼び出したり、別のPub/Sub messageをpublishしたりします。

そのため、SantaのDev環境とPRRC環境でmessageを分けて受け取れるだけでは不十分です。結合テストでは、Santaから呼び出すmicroservice側にもPRRC環境が用意されている場合があります。その場合、Santaからの呼び出しも通常のDev環境ではなく、対応するmicroserviceのPRRC環境へルーティングできる必要があります。

図2のように、このルーティングを実現するには、Santaが受け取ったmessageに付与されたルーティング情報を、後続のgRPC呼び出しやPub/Sub publishにも引き継ぐ必要があります。つまり、message自体にルーティング情報を持たせ、Santa内の処理でもその情報を落とさない仕組みが必要になります。

実現方法の検討

ここまで整理した2つの要件を満たすには、Pub/Sub messageにルーティング情報を持たせたうえで、その情報をどの段階で使ってmessageを振り分けるかを決める必要がありました。大きく分けると、consumerがmessageを受け取ったあとに判断する方法、Topic自体を分ける方法、subscriptionのfilterで受け取るmessageを分ける方法があります。

私たちは、それぞれの方法について、Pull型subscriptionを維持できるか、対象外のmessageを余計にpullしないか、PRRCごとの運用負荷が大きくなりすぎないか、という観点で比較しました。

最初に検討したのは、consumer側でmessage attributeを見て、対象外のmessageを処理しない方法でした。この方法は実装範囲が小さく見えますが、対象外のmessageも一度pullしてしまいます。対象外のmessageをackすると本来処理すべき環境に届かず、nackするとredeliveryが繰り返される可能性があります。

TopicをDev用とPRRC用に分ける方法も検討しました。この方法では、あらかじめPRRC用のTopicとsubscriptionのペアを用意しておき、PRごとにどのペアを使うかを割り当てます。Topic単位で分離できるため構成は直感的ですが、PRごとの割り当てを手動で管理する必要があります。そのため、割り当て忘れや重複割り当てが起きやすく、PRRC環境が増えるほど運用負荷が高くなります。

これらに対して、subscription filterを使う方法では、consumerがmessageをpullする前に、subscription側で受け取るmessageを分けられます。さらに、振り分けに使うmessage attributeをそのままルーティング情報として扱えるため、後続のPub/Sub publishやgRPC呼び出しにも同じ情報を引き継げます。つまり、要件1の「messageを環境ごとに分けて受け取ること」と、要件2の「ルーティング情報を後続処理へ引き継ぐこと」を、同じmessage attributeを軸に実現できます。

最終的な方針

最終的な方針は、Pull型subscriptionを維持したまま、pubsub messageのattributeで配送先を分けることです。各messageのattributeにルーティング情報を付け、subscription側はその値をfilterの条件にします。こうすると、同じtopicに届いたmessageでも、条件に一致したsubscriptionだけがmessageを受け取ります。

図のように、たとえば PR番号1234 を検証する場合、その message の attribute には、PR番号に対応するルーティング情報(PR1234)を付与します。Dev環境のsubscriptionはこのmessageを受け取らず、PR1234用のsubscriptionだけが受け取ります。

さらにこのルーティング情報は、Santaがpublishするmessageにも引き継げます。受け取ったPub/Sub messageのattributeからルーティング情報を読み取り、contextに格納し、後続のpublishや外部microserviceの呼び出しへ渡します。これにより、PRRC の文脈が処理の途中で途切れません。

一方で、外部サービスの subscription にそのまま filterを足すことはできませんでした。Santa は、ポイント還元などのトリガーとなるイベントを、決済をはじめとする他のmicroservice から Pub/Sub で受け取っています。これらの外部 subscriptionは発行側の microservice(=別チーム)の管理範囲にあり、Santa が直接 filterを足すと、別チームが管理するリソースに手を入れることになってしまいます。そこで外部 subscription については proxy sidecar を挟み、Santa 側の proxy topicへ再 publish する構成にしました。Santa の Pod は外部 subscription を直接 pullせず、proxy topic に対して作った Dev / PRRC 用の filtered subscriptionを読みます。

この方針により、Pull型subscriptionを維持したまま、Pub/Sub messageを意図した環境だけに届けられ、しかも変更をSantaの管轄内だけで完結できます。一方で、PRRCごとにsubscriptionを作る必要があるため、その自動作成・削除の設計が新たに必要になりました。

まとめ

本記事では、Pull型Pub/Subを維持したまま、Pub/Sub message attributeとfiltered subscriptionを使ってSantaにPRRC環境を導入した取り組みを紹介しました。

従来は、QA期間が重複する場合には統合環境を用意するか、複雑なプロジェクト同士の場合は直列で進めてQA待ちが発生するしかありませんでした。しかし、実際にこの仕組みを用いることで、直近の大きな2つのプロジェクトではQAを並列化して進めることができました。また、共有設定を変更するテストを直列でしか実施できなかったのが、QAメンバーの人数に合わせて3つ4つとスケールできるようになり、QA期間の短縮にも貢献できました。

自分たちのチームの状況も踏まえながら、よりチームに合った解決策を模索し、それを実現できたのは、チームの皆さんの協力があってこそです。Loyalty & Incentiveチームの皆様、ありがとうございました!

次の記事は sapuriさんの「内製ワークフローエンジンの設計とメルカリでの活用事例」です。引き続きお楽しみください。

  • X
  • Facebook
  • linkedin
  • このエントリーをはてなブックマークに追加