OpenTelemetryで始める分散トレーシング
はじめに
マイクロサービスアーキテクチャの普及に伴い、「オブザーバビリティ(Observability)」がインフラ運用の重要テーマになっています。
従来のモニタリング(監視)が「何が壊れたか」を検知するのに対し、オブザーバビリティは 「なぜ壊れたか」を追跡できる仕組み です。複数のサービスが連携するマイクロサービス環境では、1つのリクエストがどのサービスを通過し、どこでエラーや遅延が発生したかを把握する必要があります。
そこで注目されているのがOpenTelemetryです。
OpenTelemetryとは
オブザーバビリティを実現するには、アプリケーションの各所からテレメトリデータ(トレース・メトリクス・ログ)を取得する計装(instrumentation)が不可欠です。OpenTelemetry(OTel)は、CNCF(Cloud Native Computing Foundation)が管理するオープンソースのオブザーバビリティフレームワークで、計装から収集までの標準仕様を提供します。
OpenTelemetryの主要コンポーネント
OpenTelemetryは主に次の3つの要素で構成されています。
| 要素 | 役割 | 動く場所 |
|---|---|---|
| API / SDK(言語別) | アプリケーションに組み込み、テレメトリデータを生成・送信する | アプリケーションのプロセス内 |
| OTLP(プロトコル) | テレメトリデータをやり取りする標準プロトコル(gRPC / HTTP) | ネットワーク上 |
| Collector | 受信したテレメトリデータを加工・転送する中継サーバー | アプリとは別プロセス |
本記事では、API / SDK(計装の標準化)→ 扱うシグナル → Collectorの順で各要素を解説します。
計装を標準化することのメリット
OpenTelemetry登場以前は、監視ツールごとに独自のSDKやエージェントが必要でした。
【Before】ベンダーごとにバラバラ
App → Datadog Agent → Datadog
App → Jaeger SDK → Jaeger
App → Zipkin SDK → Zipkin
OpenTelemetryは、この計装の部分を標準化します。アプリケーション側はOTelの仕様に従って計装すれば、送信先を自由に切り替えられます。
【After】OpenTelemetryで統一
App → OTel SDK → OpenTelemetry Collector → Jaeger / ClickHouse / Datadog など任意のバックエンドへ
OpenTelemetryが扱うテレメトリデータ
OpenTelemetryは次の3種類のシグナルを標準仕様として扱います。
| シグナル | 内容 | 例 |
|---|---|---|
| トレース | リクエストがサービスを横断する流れを追跡 | ユーザーのAPIリクエストがどのサービスを通過したか |
| メトリクス | 数値データの集計 | リクエスト数、レイテンシ、エラーレート |
| ログ | イベントの記録 | エラーメッセージ、デバッグ情報 |
この記事では、マイクロサービス環境で最も価値が出やすいトレース(分散トレーシング)に絞ってハンズオンを行います。自動計装を使い、アプリコードを変更せずに導入するところまで試します。
トレースの基本概念
分散トレーシングを理解するために、2つの用語を押さえておきましょう。
- Trace(トレース): 1つのリクエスト全体の流れ。複数のSpanで構成される
- Span(スパン): 1つの処理単位。開始時刻、終了時刻、ステータスなどを持つ
Trace(1リクエスト全体)
├── Span: App API(受信 → Backend呼び出し)
│ └── Span: Backend API(処理 → レスポンス)
サービス間でTrace IDが伝搬される仕組み(Context Propagation)により、別々のサービスのSpanが1つのTraceとして紐づきます。
OpenTelemetry Collector
ここからは、前述のOpenTelemetryの3つ目の要素である Collector を詳しく見ていきます。
Collectorとは
OpenTelemetry Collector は、アプリケーションから送信されたテレメトリデータを受け取る中継サーバーです。必要に応じてデータを加工したうえで、バックエンド(Jaeger / Datadog / Grafana Tempo 等)へ転送します。OpenTelemetry プロジェクトが公式に提供するベンダー中立なコンポーネントで、単体のバイナリまたはコンテナとして動作します。
App → OpenTelemetry Collector → バックエンド(Jaeger / Datadog / Grafana Tempo 等)
Collectorを挟むメリット
アプリケーションから直接バックエンドへ送らずにCollectorを経由させることで、次のメリットがあります。
- 送信先の切り替えをアプリ側で意識しなくてよい — アプリはOTLPでCollectorに送るだけ。Jaeger → Grafana Tempo などの切り替えはCollector側のExporter設定だけで完結する
- アプリの負荷を下げる — バッチ化・リトライ・圧縮などをCollectorに任せられる
- データの加工を集中管理できる — 機密情報のフィルタ、サンプリング、属性の付与などをCollectorで一元化できる
Collectorの内部構造
OpenTelemetry Collectorは、次の3つのコンポーネントを連結して処理パイプラインを構成します(トレース・メトリクス・ログそれぞれで別のパイプラインを定義できます)。
| コンポーネント | 役割 |
|---|---|
| Receiver | データを受け取る(OTLP, Prometheus等) |
| Processor | データを加工する(バッチ化、フィルタリング等) |
| Exporter | データを送信する(Jaeger, ClickHouse等) |
構築する環境
本記事では、Kubernetes上に簡単なサンプルアプリケーションとOpenTelemetry Collectorを構築してトレースを確認します。
全体構成図
curl ──▶ App API ──▶ Backend API
(Pod) (Pod)
│ │
└─────┬──────┘
▼
OpenTelemetry Collector (Pod)
│
▼
Jaeger (Pod) ← ブラウザでトレースを確認
各コンポーネントの役割
| Pod | 役割 | 使用イメージ |
|---|---|---|
| App API | 中間API。リクエストを受けてBackend APIを呼び出す | Python FastAPI |
| Backend API | 最終API。データを返す | Python FastAPI |
| OpenTelemetry Collector | テレメトリデータの受信・加工・転送 | otel/opentelemetry-collector-contrib |
| Jaeger | トレースの保存・可視化(Web UI付き) | jaegertracing/all-in-one |
※本記事はCollectorの設定(Receiver → Processor → Exporter)を自分で書き、構造を理解することを目的としているため、Helmは使わずにYAMLを直接記述します。
前提条件
以下のツールがインストール済みであること。
| ツール | 用途 | インストール |
|---|---|---|
| Docker Desktop | コンテナ実行環境 | brew install --cask docker |
| minikube | ローカルKubernetes環境 | brew install minikube |
| kubectl | Kubernetes操作CLI | brew install kubectl |
※本記事のインストール例は macOS(Homebrew)を前提にしています。Linux / Windows の場合は各ツールの公式ドキュメントを参照してください。
なお、本記事ではコンテナイメージに :latest を使用しています。再現性のため、以下のバージョン要件を確認してください。
-
Jaeger all-in-one: v1.35 以降(OTLP gRPC 受信
4317に対応したバージョン) - OpenTelemetry Collector (contrib): 任意の安定版
- Python: 3.10 以降推奨(自動計装ライブラリの動作確認のため)
Kubernetesクラスタの起動
Docker Desktopを起動した状態で、minikubeを起動します。
# Kubernetesクラスタを作成・起動(Dockerドライバを指定)
minikube start --driver=docker
起動後、kubectl get nodes で1ノードが Ready になっていれば成功です。
サンプルアプリの作成
curlでApp APIを叩くと、App APIがBackend APIを呼び出してレスポンスを返すシンプルな構成です。以下のディレクトリ構成で作成します。
otel-handson/
├── app-api/
│ ├── main.py
│ ├── requirements.txt
│ └── Dockerfile
└── backend-api/
├── main.py
├── requirements.txt
└── Dockerfile
Backend API
from fastapi import FastAPI
app = FastAPI()
@app.get("/data")
def get_data():
return {"message": "Hello from Backend API"}
App API
import httpx
from fastapi import FastAPI
app = FastAPI()
BACKEND_URL = "http://backend-api:8000"
@app.get("/hello")
def hello():
response = httpx.get(f"{BACKEND_URL}/data")
return {"backend": response.json()}
BACKEND_URL の backend-api は、KubernetesのService名として名前解決されます。
Dockerfile と requirements.txt(自動計装のポイント)
OpenTelemetryは主要言語(Python、Java、Node.js、.NET、Ruby、Go 等)で自動計装(Auto-Instrumentation)を提供しており、アプリケーションコード(main.py)を変更せずに計装を有効化できます。従来はSpanの開始・終了やContext伝搬をSDK呼び出しで main.py に書く必要がありましたが、その手間が不要になります。
App API 側は httpx を追加します。
fastapi
uvicorn
httpx
opentelemetry-distro
opentelemetry-exporter-otlp
-
opentelemetry-distro— OpenTelemetryの自動計装に必要なパッケージ一式 -
opentelemetry-exporter-otlp— OpenTelemetry CollectorへOTLPプロトコルでデータを送信するExporter
Backend API 側は他サービスを呼び出さないため httpx の行は不要です。
fastapi
uvicorn
opentelemetry-distro
opentelemetry-exporter-otlp
以下のDockerfileをApp API / Backend API 両方のディレクトリに配置してください。
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
RUN opentelemetry-bootstrap -a install
COPY main.py .
CMD ["opentelemetry-instrument", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
通常のDockerfileとの差分は2点です。1つ目は RUN opentelemetry-bootstrap -a install の追加で、使用ライブラリを検出して計装ライブラリを自動インストールします。2つ目は起動コマンドを opentelemetry-instrument でラップする変更です。
【Before】通常の起動コマンド
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
【After】opentelemetry-instrument でラップする
CMD ["opentelemetry-instrument", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
これでFastAPIのリクエスト処理やhttpxのHTTP呼び出しが自動的にSpanとして記録され、後ほどJaeger UIで可視化されます。
opentelemetry-instrument は、プロセス起動時に計装対象ライブラリ(FastAPI、httpx 等)の関数をモンキーパッチ(実行時に差し替え)します。これにより、リクエスト処理の前後でSpanの生成・終了が自動的に行われます。main.py を一切変更しなくてよいのはこの仕組みによるものです。
Kubernetesマニフェストの作成
アプリのマニフェスト
App API、Backend APIそれぞれにDeploymentとServiceを定義します。ここではApp APIの例を示します(Backend APIも同じ構造です)。
apiVersion: apps/v1
kind: Deployment
metadata:
name: app-api
spec:
replicas: 1
selector:
matchLabels:
app: app-api
template:
metadata:
labels:
app: app-api
spec:
containers:
- name: app-api
image: app-api:latest
imagePullPolicy: Never
ports:
- containerPort: 8000
env:
- name: OTEL_SERVICE_NAME
value: "app-api"
- name: OTEL_EXPORTER_OTLP_ENDPOINT
value: "http://otel-collector:4317"
---
apiVersion: v1
kind: Service
metadata:
name: app-api
spec:
selector:
app: app-api
ports:
- port: 8000
targetPort: 8000
ポイント:
-
imagePullPolicy: Never— レジストリからpullせずminikube内でビルドしたローカルイメージを使用 -
OTEL_SERVICE_NAME— Jaegerに表示されるサービス名 -
OTEL_EXPORTER_OTLP_ENDPOINT— トレースの送信先(OpenTelemetry CollectorのgRPCエンドポイント)
Backend APIのマニフェストは、上記マニフェストの app-api を backend-api に置換するだけです。
OpenTelemetry Collectorのマニフェスト
公式のKubernetesマニフェスト例をベースに、学習用の最小構成としました。公式例は Agent / Gateway パターン(DaemonSetのAgentとDeploymentのGatewayを併用)ですが、ここではGateway相当のDeploymentのみに簡略化しています。
apiVersion: v1
kind: ConfigMap
metadata:
name: otel-collector-conf
labels:
app: opentelemetry
component: otel-collector
data:
otel-collector-config: |
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
processors:
batch: {}
exporters:
otlp/jaeger:
endpoint: jaeger:4317
tls:
insecure: true
debug:
verbosity: detailed
service:
pipelines:
traces:
receivers: [otlp]
processors: [batch]
exporters: [otlp/jaeger, debug]
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: otel-collector
labels:
app: opentelemetry
component: otel-collector
spec:
replicas: 1
selector:
matchLabels:
app: opentelemetry
component: otel-collector
template:
metadata:
labels:
app: opentelemetry
component: otel-collector
spec:
containers:
- name: otel-collector
image: otel/opentelemetry-collector-contrib:latest
args: ["--config", "/conf/otel-collector-config.yaml"]
ports:
- containerPort: 4317
- containerPort: 4318
volumeMounts:
- name: otel-collector-config-vol
mountPath: /conf
volumes:
- name: otel-collector-config-vol
configMap:
name: otel-collector-conf
items:
- key: otel-collector-config
path: otel-collector-config.yaml
---
apiVersion: v1
kind: Service
metadata:
name: otel-collector
labels:
app: opentelemetry
component: otel-collector
spec:
selector:
app: opentelemetry
component: otel-collector
ports:
- name: otlp-grpc
port: 4317
protocol: TCP
targetPort: 4317
- name: otlp-http
port: 4318
protocol: TCP
targetPort: 4318
このConfigMapには、前述のReceiver / Processor / Exporterの3コンポーネントが定義されています。OTLP(gRPC: 4317 / HTTP: 4318)で受信し、batchプロセッサでバッチ化したのち、otlp/jaeger exporterでJaegerへ転送します(debug exporterはstdoutへのデバッグ出力用)。
なお、本ハンズオンではアプリからCollectorへの送信はgRPC (4317) のみを使います(OpenTelemetry Pythonでは OTEL_EXPORTER_OTLP_PROTOCOL 未指定時のデフォルトがgRPCです)。HTTP (4318) は「どちらでも受けられる」ことを示すために定義していますが、本記事のフローでは通信していません。
DeploymentではこのConfigMapをVolumeとしてマウントし、--config引数で読み込みます。Serviceは他のPod(App API / Backend API)からhttp://otel-collector:4317で名前解決できるよう公開しています。
Jaegerのマニフェスト
公式のKubernetesデプロイテンプレートをベースとして最小構成にしました(リポジトリはアーカイブ済みですが、all-in-one構成のリファレンスとして引用しています。本番運用では Jaeger公式のデプロイドキュメント を参照してください)。公式テンプレートにはZipkinやThriftなど複数のプロトコル用ポートがありますが、今回はOTLP受信(4317)とWeb UI(16686)のみに絞っています。
apiVersion: apps/v1
kind: Deployment
metadata:
name: jaeger
labels:
app.kubernetes.io/name: jaeger
app.kubernetes.io/component: all-in-one
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: jaeger
app.kubernetes.io/component: all-in-one
template:
metadata:
labels:
app.kubernetes.io/name: jaeger
app.kubernetes.io/component: all-in-one
spec:
containers:
- name: jaeger
image: jaegertracing/all-in-one:latest
ports:
- containerPort: 16686
- containerPort: 4317
readinessProbe:
httpGet:
path: "/"
port: 14269
initialDelaySeconds: 5
---
apiVersion: v1
kind: Service
metadata:
name: jaeger
labels:
app.kubernetes.io/name: jaeger
app.kubernetes.io/component: all-in-one
spec:
type: NodePort
selector:
app.kubernetes.io/name: jaeger
app.kubernetes.io/component: all-in-one
ports:
- name: query-http
port: 16686
protocol: TCP
targetPort: 16686
- name: otlp-grpc
port: 4317
protocol: TCP
targetPort: 4317
Serviceは2つのポートを公開します。query-http(16686)はブラウザからJaeger UIを開くために、otlp-grpc(4317)はOpenTelemetry Collectorからトレースを受け取るためにそれぞれ使用します。
デプロイ
Dockerイメージのビルド
通常、ローカルの docker build で作成したイメージは Docker Desktop側に保存されるため、minikube内のPodからは参照できません。そこで、docker コマンドの接続先を Docker DesktopのDockerデーモン → minikube内のDockerデーモン に切り替えてからビルドします。これにより、レジストリへのpushなしでPodからそのまま使えます。
# minikubeのDocker環境に切り替え
eval $(minikube docker-env)
# イメージをビルド
docker build -t app-api:latest app-api/
docker build -t backend-api:latest backend-api/
マニフェストの適用
kubectl apply -f k8s/jaeger.yaml
kubectl apply -f k8s/otel-collector.yaml
kubectl apply -f k8s/backend-api.yaml
kubectl apply -f k8s/app-api.yaml
起動確認
kubectl get pods
全てのPodが STATUS: Running かつ READY: 1/1 になれば成功です(Jaegerは readinessProbe を定義しているため、Running でも probeが成功するまでの数秒は READY: 0/1 で表示されます)。
NAME READY STATUS RESTARTS AGE
app-api-5b54dd6f9d-bqkz6 1/1 Running 0 30s
backend-api-696b97d89c-b7jcl 1/1 Running 0 30s
jaeger-c9b947d75-7nls6 1/1 Running 0 30s
otel-collector-6f659fb8c6-lk27k 1/1 Running 0 30s
トレースの確認
リクエストを送る
App APIにポートフォワードしてリクエストを送ります。
kubectl port-forward svc/app-api 8000:8000
別のターミナルで:
curl -s http://localhost:8000/hello
{"backend":{"message":"Hello from Backend API"}} が返ってくれば、App API → Backend APIの通信は正常です。
Jaeger UIでトレースを確認
Jaegerにポートフォワードします。
kubectl port-forward svc/jaeger 16686:16686
ブラウザで http://localhost:16686 を開きます。
左側のServiceドロップダウンでapp-apiを選択し、「Find Traces」をクリックすると、先ほどのリクエストのトレースが表示されます。
トレースをクリックすると、タイムライン表示でSpanの親子関係が確認できます。

main.pyには一切トレースのコードを書いていません。自動計装によって、FastAPIのリクエスト処理やhttpxのHTTP呼び出しがSpanとして記録されています。さらに、サービス間でTrace IDが伝搬(Context Propagation)されていることも確認できます。
まとめ
本記事では、ローカルKubernetes上にOpenTelemetryの分散トレーシング環境を構築しました。
- OpenTelemetryの自動計装により、アプリケーションコードを変更せずにトレースを導入できた
- OpenTelemetry CollectorがReceiver → Processor → Exporterのパイプラインでデータを中継する仕組みを体験した
- Jaegerでサービス間のSpan伝搬(Context Propagation)を視覚的に確認できた
OpenTelemetry CollectorのExporter設定を変えるだけで、JaegerからClickHouseやGrafana Tempoなど別のバックエンドに切り替えることも可能です。OpenTelemetryによる標準化の恩恵を実感できるハンズオンです。
Discussion