Google Workspace カードが店頭販売の棚に並ぶ!~リリースを支えるSagaパターンの分散トレーシングとコンテキスト管理~

この記事は GMOインターネットグループ Advent Calendar 2025 23日目の記事です。

みなさん、おはようございます!こんにちは!こんばんは!
GMOペパボ株式会社の横山です!!!!!!

はじめに

こんにちは!GMOペパボ株式会社所属の横山です。社内ではあだ名で呼び合う文化があり、はるおつと呼ばれています。普段はロリポップ・ムームードメイン事業部にてムームードメインの開発を主に行なっています。

今回のアドベントカレンダーでは、先月11月18日にリリースされたばかりの、「Google Workspace カード」の信頼性向上に向けて、「可観測性ってなに?」状態から行なった可観測性向上とSRE的アプローチについて、つまづきポイントとそこからの学びについてまとめたいと思います。


Google Workspaceカードとは?

みなさん、Google Workspaceカードを知っていますか?!?!

ムームードメインは、ドメイン取得サービスの一環として、2025年2月からは Google Workspace の販売も開始し、ドメインと Google Workspace のシームレスな連携を提供してきました。

今回、このドメインとGoogle Workspaceの連携を、家電量販店などでカードを購入することで、購入者に対して Google Workspace の契約管理からドメイン取得・管理まで一貫して提供することが可能になりました! (ぜひお店で見かけた際には、あの記事のあれだ!と思ってくれたら嬉しいです)

システム概要

ユーザーの購入フローは以下の通りです:

店舗でGoogle Workspace カードを購入

プロダクトキーを入力

ムームードメインでドメインを選択 (.com, .jp, .devなど)

Google Workspace情報の入力 (組織名, adminメールアドレスなど)

ムームードメインにログイン・登録

決済情報を入力

ドメイン取得やGoogle Workspaceのセットアップ処理開始

このフローでは、複数の外部APIとの連携、決済処理、ドメイン取得、Google Workspace設定など、多くのステップが連鎖的に実行されます。

Saga Orchestrationパターンの採用

このような長時間にわたる複数サービス間のトランザクションを安全に処理するため、Saga Orchestrationパターンを採用しました。

Saga Orchestrationパターンの特徴は以下の通りです:

  • 長時間トランザクションを小さなローカルトランザクションの連鎖として実装
  • 各トランザクションには補償トランザクションを定義
  • 失敗時は実行済みステップを逆順で補償

具体的なステップ構成は以下のようになっています:

[正常フロー]
Step1: アカウント検証

Step2: カードの利用

Step3: 契約作成(DB書き込み)

Step4: 決済完了(3DS認証)

Step5: クレカDB登録

Step6: ドメイン取得

Step8: GWS設定(Google API)

Step9: 注文確定

[補償フロー(失敗時は逆順に実行)]
Step2の補償: カードの再有効化

Step3の補償: 取得契約の削除

Step4の補償: 決済返金

Step5の補償: skip (ドメインは取得済みのため)

Step6の補償: skip (クレカの登録は削除する必要がないため)

Step8の補償: GWS設定解除job作成

現状の課題とやるべきこと

可観測性向上に取り組むにあたり、まずムームードメインにおける現状を整理しました。

観点現状やるべきこと
ログ分散、非統一集中、構造化
トレース一部のAPMEnd-to-End
外部API部分的全て
Sagaログのみトレースで状態追跡、分散トレーシング

私はこのGoogle Workspace カードという新機能追加を切り口として、長年続くサービスに可観測性を組み込むチャンスと捉えました。

段階的アプローチ

一度に全てを実装するのではなく、価値の高い部分から小さくリリースしていく方針を取りました。

Phase1: OpenTelemetry基盤構築
→ 全ての前提、ベンダーロックインを回避し観測の標準化

Phase2: BaseControllerトレーシング追加
→ 全コントローラアクションでのトレース・スパン自動付与

Phase3: Sagaトレーシング
→ GWSカードにおける処理の根幹、最も影響が大きい

Phase4: 外部API可視化
→ 外部API起因の障害の見える化、既存システムの信頼性向上

Phase5: アラート、SLI/SLO設定
→ 検知自動化で運用負荷削減

つまずきポイント①:SpanContextの永続化問題

問題

Sagaパターンでは、セッションとして状態を保存・復元する必要があります。補償処理を実行する際に、元のステップのSpanと関連づけたいと考えました。

OpenTelemetryのLink機能を使うためにSpanContextを使いたかったので、最初はセッションにSpanContextをそのまま保存しようとしました。

# 各ステップでSpanContextをそのまま保存
@step_results[step] = result.merge(
  span_context: span.context  # ← OpenTelemetryオブジェクト
)
session.saga = @step_results.to_json

エラー発生

span.context.to_jsonを試みましたが、NoMethodErrorが発生して頭を抱えていました。

undefined method `valid?' for
"#<OpenTelemetry::Trace::SpanContext:0x000055f2740a3c70>":String

なぜシリアライズできないのか

span.context
# => #<OpenTelemetry::Trace::SpanContext:0x000055f2740a3c70
#      @trace_id="\xA1\xB2\xC3...",  (16バイトのバイナリ)
#      @span_id="\x1A\x2B\x3C...",   (8バイトのバイナリ)
#      @trace_flags=1,
#      @trace_state=...>

SpanContextの実体を見てみると:

つまり、単にSpanContextは実行時の一時オブジェクトなのですね。

改めてなぜOpenTelemetryがSpanContextをシリアライズ可能にしていないか考えてみると、以下のような納得感がありました。

  • SpanContextは実行時の一時オブジェクトとして設計されている
  • トレースはOTLP Exporterで送信されるべき
  • アプリケーション側で永続化されることを想定していない

しかし、補償処理を実行する際に、元のステップのSpanと関連づけたい場合はどうしたらよいのでしょうか?そのほかにも、3DS認証などに対しても、同じような問題にあたるのではないかと思います。

解決策:Span属性による関連付け

Linkを使う代わりに、trace_idとspan_idを文字列として保存し、Span属性でオリジナルのtrace_idを関連付ける方式に変更しました。

# ステップ実行時
@step_results[step] = result.merge(
  span_context: {
    trace_id: span.context.trace_id.unpack1('H*'),
    span_id: span.context.span_id.unpack1('H*')
  }
)
# セッション保存 - 成功!
session.saga = @step_results.to_json  # JSON化可能

# 補償処理実行時:Span属性として関連付け
original_span_info = @step_results.dig(step, :span_context)

tracer.in_span("saga.compensation.#{step}") do |span|
  if original_span_info
    span.set_attribute('muu.compensation.original_trace_id',
                       original_span_info[:trace_id])
    span.set_attribute('muu.compensation.original_span_id',
                       original_span_info[:span_id])
  end
  # 補償処理実行
end

これにより、以下が実現可能となりました。

  • セッション復元後も使用可能
  • Grafana/Tempoで検索可能
  • 因果関係の追跡が可能

つまずきポイント②:Sagaパターンのトレーシング設計

可視化したいもの

Sagaパターンで可視化したいものを洗い出しました:

  • フロー全体: Saga実行の成功/失敗
  • 個別ステップ: どのステップで失敗/遅延が起きたか
  • 補償処理: ロールバックが正しく動いたか

Spanの階層設計

どこにSpanを貼るべきかを検討し、以下のような階層構造にしました:

  • 階層構造で因果関係が一目瞭然
  • 各ステップの実行時間を個別に測定して改善可能
  • 補償処理と元のステップの関係を追跡可能

order_event_idによる全リクエスト追跡

もう一つの課題として、HTTPリクエストが分断される問題がありました。

例えば:

  • リクエスト1: 申し込み開始 → Saga → 3DS認証へリダイレクト(HTTP分断)
  • リクエスト2: 3DS認証完了 → Saga再開 → 完了

通常のtrace_idでは2つのリクエストが別々のトレースになってしまいます。

これを解決するため、ビジネスキーであるorder_event_idで全体の紐付けを行いました:

# rb/app/controllers/concerns/google_workspace_card/tracing.rb
def set_session_attributes(span)
  # セッション情報をSpan属性として記録
  span.set_attribute('gws_card.session_id', current_gws_card_db_session.id)
  span.set_attribute('gws_card.current_step', current_gws_card_db_session.current_step)
  span.set_attribute('gws_card.order_event_id', current_gws_card_db_session.gws_order_event_id)
  span.set_attribute('gws_card.account_id', account_id)
end

これにより、ビジネスロジックとしてDBに保存しているキーからもトレースを可能にすることが実現できました!

成果

これらの取り組みにより、Grafanaで以下のようなアラートルールを設定できるようになり、Google Workspaceカード、そしてそれを取り巻くムームードメインの既存資産の可観測性を向上させることができました。

  • Saga処理失敗
  • 補償処理失敗(データ不整合の可能性)
  • 各ステップの失敗(GWSカード利用、ドメイン注文、決済準備、決済完了、GWS設定など)
  • 外部APIの接続失敗
  • エラー率上昇
  • レート制限
  • DB接続エラー

など、アラートルールを設定し、問題の早期検知が可能になりました。

まとめ

SpanContextを保存したい時の注意点

  • SpanContextはJSON化できない – OpenTelemetryの設計思想として、永続化は想定されていない
  • trace_id, span_idを文字列として保存し、Linkではなくspan属性で関連付け

分散システムには分散トレーシングが効果的

  • Sagaパターンのような分散システムにはログのみで確認するのは厳しい
  • OpenTelemetryで階層構造のトレーシングを実現
  • 小さな切り口から開始して、価値のある場所から攻める

「可観測性って何?」から始まった取り組みでしたが、新機能開発を切り口にすることで、段階的に可観測性を高めることができました。同じような課題を持つ方の参考になれば幸いです。

最後に、
Google Workspace カードは家電量販店等で販売中です。
クリスマスプレゼントにいかがでしょうか?!?!

ブログの著者欄

横山 遥乙

GMOペパボ株式会社

GMOペパボ株式会社/2024年新卒入社/ロリポップ・ムームードメイン事業部 ムームードメイングループ

採用情報

関連記事

KEYWORD

TAG

もっとタグを見る

採用情報

SNS FOLLOW

GMOインターネットグループのSNSをフォローして最新情報をチェック