マルチテナント環境でのOpenTelemetry計装戦略 ​

SaaSプラットフォームやマルチテナント型のシステムでは、複数の顧客(テナント)が共有されたインフラストラクチャを利用しています。このような環境でOpenTelemetryを運用する際には、テナント間のデータを確実に分離し、各テナントのトレースを独立して分析できることが重要です。本稿では、マルチテナント環境でのOpenTelemetry計装について、実装レベルの工夫を説明します。

マルチテナント環境における可視性の課題 ​

マルチテナント環境では、異なるテナントのリクエストが同じアプリケーションインスタンスを通じて処理されます。複雑に絡み合ったトレースの中から、特定のテナントに関連するデータを抽出するには、テナント情報を全てのスパンに含める必要があります。

テナントIDをトレースに含めない場合、以下の問題が生じます。テナントAの処理が遅延したとき、実際には共有されたデータベースの負荷がテナントBのクエリによって増加していたとしても、区別できません。また、セキュリティ監査でテナントCのアクセスパターンを追跡したくても、トレースだけでは判定できません。複数テナントのデータが混在したメトリクスやログも、分析の精度を低下させます。

Baggageを使ったテナントIDの伝播 ​

OpenTelemetryのBaggage(バゲッジ)は、トレースコンテキストと共に伝播する付加的な情報です。テナントIDをBaggageに格納することで、リクエスト全体を通じてテナント情報を自動的に伝播させられます。

実装例:Pythonでのテナント情報の設定

python
from opentelemetry import trace, baggage, context

# リクエスト受信時にテナントIDをBaggageに設定
# requestオブジェクトはWebフレームワークから渡されることを想定
tenant_id = request.headers.get('X-Tenant-ID')
ctx = baggage.set_baggage('tenant.id', tenant_id)

# これ以降のトレースでテナント情報が自動伝播するようにコンテキストを有効化
with context.attach(ctx):
    tracer = trace.get_tracer(__name__)
    with tracer.start_as_current_span('process_request'):
        # このスパンと、この中で作成される子スパンには
        # 'tenant.id' がBaggageとして含まれる
        pass

Baggageのメリットは、各スパンで明示的にテナントIDを指定する必要がないことです。設定後、呼び出しチェーン全体を通じて自動的に伝播します。W3CBaggagePropagatorを使用することで、マイクロサービス間でも情報が失われません。

ただしBaggageには制限があります。送信されるHTTPヘッダサイズに影響するため、小さなデータ(テナントIDのような短い文字列)に適しています。大量のメタデータが必要な場合は、別のアプローチを検討します。

リソース属性によるテナント識別 ​

スパンのメタデータとしてテナント情報を含める方法として、リソース属性を活用できます。リソース属性は、アプリケーション全体の特性を表す情報で、全スパンに自動的に付与されます。

実装例:Pythonでの設定

python
import os
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter

# テナントIDをリソース属性に含める
resource = Resource.create({
    'service.name': 'api-service',
    'service.instance.id': 'instance-001',
    'tenant.id': os.getenv('TENANT_ID', 'default'),
    'deployment.environment': 'production'
})

exporter = OTLPSpanExporter(endpoint='localhost:4317')
trace_provider = TracerProvider(resource=resource)
trace.set_tracer_provider(trace_provider)

リソース属性の利点は、Collectorやバックエンドで一貫性のあるテナント情報を確認できることです。ただしこのアプローチはテナントごとに異なるトレーサープロバイダインスタンスが必要な場合に向いており、単一プロセスで複数テナントを処理するマルチテナント環境では、カスタムスパン属性を併用するのが現実的です。

カスタムスパン属性での細粒度制御 ​

各スパンにテナントID属性を明示的に追加することで、最も確実な実装が実現します。この方法は、複数テナントの処理が同じプロセス内で実行される典型的なマルチテナント環境に最適です。

実装例:スパン作成時の属性指定

python
tracer = trace.get_tracer(__name__)
tenant_id = extract_tenant_id(request)

with tracer.start_as_current_span('handle_user_request') as span:
    span.set_attribute('tenant.id', tenant_id)
    span.set_attribute('tenant.plan', get_tenant_plan(tenant_id))
    span.set_attribute('request.user_id', user_id)
    # 処理ロジック

この方法では、各スパンの属性として記録されるため、バックエンドで明確に検索・フィルタリングできます。ただし各処理でテナント情報を設定する必要があるため、開発チーム内で一貫した実装ガイドラインを整備することが重要です。

Collectorによるデータ分離とルーティング ​

Collectorのプロセッサー機能を使うことで、テナントIDに基づいてトレースデータを自動的に分離・ルーティングできます。これにより、バックエンド側の負荷を軽減し、テナントごとに異なる送信先を指定することが可能になります。

Collector設定例:テナントIDに基づくルーティング

yaml
receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317

processors:
  # リソース属性からテナントIDを取得してルーティング判定に使用
  routing:
    default_pipelines: [traces/default]
    routes:
      - statement: resource.attributes["tenant.id"] == "tenant-001"
        pipelines: [traces/tenant-001]
      - statement: resource.attributes["tenant.id"] == "tenant-002"
        pipelines: [traces/tenant-002]
  batch: {} # パフォーマンス向上のためバッチプロセッサーを追加

exporters:
  otlp/tenant-001:
    endpoint: jaeger-tenant-001.internal:4317
    tls:
      insecure: true
  otlp/tenant-002:
    endpoint: jaeger-tenant-002.internal:4317
    tls:
      insecure: true
  otlp/default:
    endpoint: jaeger-default.internal:4317
    tls:
      insecure: true

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [routing]
    traces/tenant-001:
      processors: [batch]
      exporters: [otlp/tenant-001]
    traces/tenant-002:
      processors: [batch]
      exporters: [otlp/tenant-002]
    traces/default:
      processors: [batch]
      exporters: [otlp/default]

このアプローチにより、Collectorが自動的に各テナントのデータを適切な送信先に振り分けます。テナント専用のバックエンドが運用されている場合、テナントIDに基づいてデータを分離できるため、テナント間のデータ漏洩リスクを低減できます。

ベストプラクティスと実装上の注意点 ​

マルチテナント環境でのOpenTelemetry計装には、いくつかの実装パターンがあります。特にテナント識別情報の伝播方法は、アーキテクチャに応じて使い分ける必要があります。

複数テナント処理を行う場合の推奨構成は、カスタムスパン属性とCollectorルーティングの組み合わせです。スパン属性でテナント情報を明示的に記録し、Collectorで自動的に分離することで、バックエンド側での仕分けが不要になり、運用が簡潔になります。

パフォーマンスと粒度のバランスも考慮すべきです。Baggageは伝播の自動化が利点ですが、HTTPヘッダサイズに制限があります。リソース属性は全スパンに付与されるため効率的ですが、リソースがテナントごとに異なる場合は管理が複雑になります。カスタムスパン属性は粒度が高い反面、手動設定の負担があります。

セキュリティ上の配慮として、テナント情報が無意識のうちに他のテナントに露出しないよう注意してください。Collectorの設定が不完全だと、テナント間でデータが混在するリスクがあります。また、テナント情報が含まれたトレースデータにアクセスする権限管理も重要です。

マルチテナント環境では、こうした複数のアプローチを理解した上で、自社のシステムアーキテクチャと運用方針に適した実装を選択することが成功の鍵になります。