この記事は 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 カードは家電量販店等で販売中です。クリスマスプレゼントにいかがでしょうか?!?!