はじめに
この記事はNTTテクノクロス Advent Calendar 2025(シリーズ2)の10日目の記事です。
NTTテクノクロスの西園です。
久しぶりのアドベントカレンダー参加となりますが、よろしくお願いします。
みなさん、Kubernetes自体の可観測性(オブザーバビリティ)について関心ありますか?
メトリクスやログの収集ならやってるよという方はいると思いますが、トレース(分散トレーシング)も(!)という方はまだまだ少ないのではないでしょうか。
実は最近Kubernetesシステムコンポーネントのトレース取得がネイティブ対応1したらしく、公式ドキュメント2に取得方法の記載がありました。
とはいえ、実際に取得・可視化しているという記事やサンプルはなかなか見当たりません(2025/12初旬)。
そこで、Kubernetesシステムコンポーネントの1つであるkube-apiserverについて、トレースの取得・可視化に手探りでチャレンジしてみました。
可観測性(オブザーバビリティ)とは?
そもそもという話ではありますが。
- 可観測性(オブザーバビリティ)って何?
- 監視(モニタリング)との違いは?
- トレース(分散トレーシング)って何?
上記のような疑問については以下を参考にしてみてください。
環境構成
今回Kubernetesクラスタは以下の環境構成としています。
- Kubernetes構築環境: WSL
- Kubernetes構築ツール: minikube
- Kubernetesのバージョン: v1.34.0
- ノード数: Control Plane 1台、Worker Node 2台
また、kube-apiserverのトレースを取得・可視化するために以下のソフトウェアを使用しています。
| ソフトウェア | バージョン | 役割 |
|---|---|---|
| OpenTelemetery Collector3 | 0.141.0 | kube-apiserverから送信されるトレースを受信し、Grafana Tempoに格納する |
| Grafana Tempo4 | 2.9.0 | OpenTelemetery Collectorに1度集約されたトレースを再受信し、ストレージに格納する |
| Prometheus5 | v3.8.0 | Grafana Tempoに格納されたトレースを統計処理する |
| Grafana6 | 12.3.0 | Grafana Tempoに格納された、もしくは、Prometheusで集計処理したトレースを可視化する |
ソフトウェア構成とトレースの流れ
ソフトウェア構成とトレースの流れは以下となっています。
kube-apiserverのトレースの取得・可視化
以下1~5の流れで環境構築します。
なお、事前にKubernetesクラスタを構築し、monitoring Namespaceを作成しておいてください。
また、今回はHelmを使用しません。
さらに、Grafana Tempo、Prometheus、Grafanaは、HostPathを使用してデータを永続化しています。
本番環境では推奨されないため、本番環境ではS3互換ストレージやPVCを使用してください。
- Grafana Tempoの構築
- OpenTelemetery Collectorの構築
- Prometheusの構築
- kube-apiserverについてトレースの送信設定追加
- Grafanaの構築
$ kubectl get node
NAME STATUS ROLES AGE VERSION
minikube Ready control-plane 41d v1.34.0
minikube-m02 Ready <none> 41d v1.34.0
minikube-m03 Ready <none> 41d v1.34.0
$ kubectl create namespace monitoring
$ kubectl get namespaces monitoring
NAME STATUS AGE
monitoring Active 4d22h
1. Grafana Tempoの構築
以下を実現できるようにGrafana Tempoを構築します。
- Receiver: OTel Collectorからトレースを受信する
- Ingester/Storage: 受信した生トレースをHostPathをストレージとして格納する
- Metrics Generator: 格納されたトレース(スパン)をリアルタイムで処理し、REDメトリクス(リクエストレート、期間など)を生成する
- Exporter: 生成したメトリクスを、自身の /metrics エンドポイント(ポート3200)で公開する
適用したマニフェストは以下となります。
apiVersion: v1
kind: ConfigMap
metadata:
name: tempo-config
namespace: monitoring
data:
tempo.yaml: |
server:
http_listen_port: 3200
grpc_listen_port: 9095
distributor:
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
ingester:
lifecycler:
ring:
kvstore:
store: inmemory
# トレースの統計情報を作成し、Prometheusに送信する
metrics_generator:
registry:
external_labels:
source: tempo
collection_interval: 15s
processor:
service_graphs:
histogram_buckets: [0.001, 0.005, 0.05, 0.5, 1.0, 3.0, 5.0, 10.0]
span_metrics:
histogram_buckets: [0.001, 0.005, 0.05, 0.5, 1.0, 3.0, 5.0, 10.0]
storage:
path: /tmp/tempo/generator/wal
remote_write:
- url: http://prometheus.monitoring.svc:9090/api/v1/write
send_exemplars: true
compactor:
compaction:
compaction_window: 1h
max_block_bytes: 100_000_000
block_retention: 1h
compacted_block_retention: 10m
storage:
trace:
backend: local
local:
path: /var/tempo/traces
wal:
path: /var/tempo/wal
auth_enabled: false
overrides:
metrics_generator_processors: [ "service-graphs", "span-metrics" ]
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: tempo
namespace: monitoring
spec:
selector:
matchLabels:
app: tempo
template:
metadata:
labels:
app: tempo
spec:
nodeSelector:
kubernetes.io/hostname: minikube-m02
initContainers:
- name: fix-permissions
image: busybox:latest
# Tempoのコンテナ実行ユーザー(UID 10001)に所有権を変更
command: ["sh", "-c", "chown -R 10001:10001 /var/tempo/traces"]
volumeMounts:
- mountPath: /var/tempo/traces
name: tempo-storage
containers:
- name: tempo
image: grafana/tempo:2.9.0
args:
- "-config.file=/etc/tempo.yaml"
- "-target=all"
- "-storage.trace.backend=local"
- "-storage.trace.local.path=/var/tempo/traces"
- "-auth.enabled=false"
ports:
- containerPort: 4317 # OTLP gRPC receiver
volumeMounts:
- mountPath: /var/tempo/traces
name: tempo-storage
- mountPath: /etc/tempo.yaml
subPath: tempo.yaml
name: tempo-config-volume
volumes:
- name: tempo-storage
hostPath:
path: /var/lib/tempo-data
type: DirectoryOrCreate
- name: tempo-config-volume
configMap:
name: tempo-config
適用後、PodとServiceが正常に作成され、PodのIPアドレスとServiceが紐づけられている(=ENDPOINTSのIPアドレスとして登録されている)ことを確認します。
$ kubectl get -n monitoring pod tempo-576d5c6c-f7qzl -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
tempo-576d5c6c-f7qzl 1/1 Running 0 5h42m 10.244.205.230 minikube-m02 <none> <none>
$ kubectl get -n monitoring svc tempo
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
tempo ClusterIP 10.105.187.246 <none> 4317/TCP,3200/TCP 2d23h
$ kubectl get -n monitoring endpointslices.discovery.k8s.io tempo-brlhb
NAME ADDRESSTYPE PORTS ENDPOINTS AGE
tempo-brlhb IPv4 4317,3200 10.244.205.230 3d7h
2. OpenTelemetery Collectorの構築
以下を実現できるようにOpenTelemetery Collectorを構築します。
- Receiver: Kube-apiserverからOTLP (gRPC) でトレースを受信する
- Processor: 受信したトレースをバッチ処理(一時的に集約)する
- Exporter: 処理したトレースを、Tempo ServiceのDNS名(またはClusterIP)のポート4317へOTLP (gRPC)で転送する
適用したマニフェストは以下となります。
apiVersion: v1
kind: ConfigMap
metadata:
name: otel-collector-conf
namespace: monitoring
data:
otel-collector-config.yaml: |
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
processors:
batch:
exporters:
otlp:
endpoint: tempo:4317
tls:
insecure: true
service:
pipelines:
traces:
receivers: [otlp]
processors: [batch]
exporters: [otlp]
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: otel-collector
namespace: monitoring
spec:
selector:
matchLabels:
app: otel-collector
template:
metadata:
labels:
app: otel-collector
spec:
containers:
- name: otel-collector
image: otel/opentelemetry-collector:0.141.0
args: ["--config=/conf/otel-collector-config.yaml"]
volumeMounts:
- mountPath: /conf
name: otel-collector-conf-vol
volumes:
- name: otel-collector-conf-vol
configMap:
name: otel-collector-conf
items:
- key: otel-collector-config.yaml
path: otel-collector-config.yaml
---
apiVersion: v1
kind: Service
metadata:
name: otel-collector
namespace: monitoring
spec:
ports:
- name: otlp-grpc
port: 4317
protocol: TCP
targetPort: 4317
selector:
app: otel-collector
適用後、PodとServiceが正常に作成され、PodのIPアドレスとServiceが紐づけられている(=ENDPOINTSのIPアドレスとして登録されている)ことを確認します。
$ kubectl get -n monitoring pod otel-collector-7944475d96-mbbpt -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
otel-collector-7944475d96-mbbpt 1/1 Running 0 3d6h 10.244.205.212 minikube-m02 <none> <none>
$ kubectl get -n monitoring svc otel-collector
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
otel-collector ClusterIP 10.111.150.39 <none> 4317/TCP 2d23h
$ kubectl get -n monitoring endpointslices.discovery.k8s.io otel-collector-rfkhb
NAME ADDRESSTYPE PORTS ENDPOINTS AGE
otel-collector-rfkhb IPv4 4317 10.244.205.212 2d23h
3. Prometheusの構築
以下を実現できるようにPrometheusを構築します。
- Scraper: Service Discovery機能と設定(prometheus.yml)に基づき、Grafana Tempo の /metrics エンドポイント(ポート3200)に定期的にアクセスし、メトリクスを収集・時系列データベースに格納する
適用したマニフェストは以下となります。
apiVersion: v1
kind: ServiceAccount
metadata:
name: prometheus
namespace: monitoring
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: prometheus-k8s-discovery
namespace: monitoring
rules:
- apiGroups: [""]
resources: ["endpoints", "pods", "services"]
verbs: ["get", "list", "watch"]
- apiGroups: ["discovery.k8s.io"]
resources: ["endpointslices"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: prometheus-k8s-discovery
namespace: monitoring
subjects:
- kind: ServiceAccount
name: prometheus
namespace: monitoring
roleRef:
kind: Role
name: prometheus-k8s-discovery
apiGroup: rbac.authorization.k8s.io
---
apiVersion: v1
kind: ConfigMap
metadata:
name: prometheus-config
namespace: monitoring
data:
prometheus.yml: |
global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
- job_name: 'kubernetes-service-endpoints'
kubernetes_sd_configs:
- role: endpoints
namespaces:
names: ['monitoring']
relabel_configs:
- source_labels: [__meta_kubernetes_service_name]
regex: tempo
action: keep
- source_labels: [__meta_kubernetes_endpoint_port_name]
regex: http-query
#action: keep
action: replace
target_label: __address__
replacement: tempo.monitoring.svc.cluster.local:3200
- source_labels: [__meta_kubernetes_service_annotation_prometheus_io_scrape]
regex: "true"
action: keep
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: prometheus
namespace: monitoring
labels:
app: prometheus
spec:
replicas: 1
selector:
matchLabels:
app: prometheus
template:
metadata:
labels:
app: prometheus
spec:
serviceAccountName: prometheus
nodeSelector:
kubernetes.io/hostname: minikube-m03
initContainers:
- name: fix-permissions
image: busybox:latest
# PrometheusコンテナユーザーのUID 65534に所有権を変更
# HostPathのマウント先が /prometheus です。
command: ["sh", "-c", "chown -R 65534:65534 /prometheus && chmod 775 /prometheus"]
securityContext:
runAsUser: 0 # busyboxをrootで実行し、所有権変更を許可
volumeMounts:
# Prometheusのデータボリュームをマウント
- mountPath: /prometheus
name: prometheus-storage
containers:
- name: prometheus
image: prom/prometheus:v3.8.0
args:
- "--config.file=/etc/prometheus/prometheus.yml"
- "--storage.tsdb.path=/prometheus"
# Grafana Tempoからトレースの統計情報を受信する
- "--web.enable-remote-write-receiver"
- "--enable-feature=exemplar-storage"
ports:
- containerPort: 9090
volumeMounts:
- name: config-volume
mountPath: /etc/prometheus
- name: prometheus-storage
mountPath: /prometheus
volumes:
- name: config-volume
configMap:
name: prometheus-config
- name: prometheus-storage
hostPath:
path: /var/lib/prometheus-data
type: DirectoryOrCreate
適用後、PodとServiceが正常に作成され、PodのIPアドレスとServiceが紐づけられている(=ENDPOINTSのIPアドレスとして登録されている)ことを確認します。
$ kubectl get -n monitoring pod prometheus-66846c6c48-gzg46 -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
prometheus-66846c6c48-gzg46 1/1 Running 0 5h26m 10.244.151.44 minikube-m03 <none> <none>
$ kubectl get -n monitoring svc prometheus
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
prometheus LoadBalancer 10.100.159.34 127.0.0.1 9090:30580/TCP 79m
$ kubectl get -n monitoring endpointslices.discovery.k8s.io prometheus-gkr8d
NAME ADDRESSTYPE PORTS ENDPOINTS AGE
prometheus-gkr8d IPv4 9090 10.244.151.44 9h
minikubeの機能を使用して、WSLのWindowsホストからLoadBalancer経由でPrometheusにアクセスし、PrometheusがGrafana Tempoのメトリクスをスクレイピングしていることを確認します。
# トンネル作成、以降当該プロセスは起動したままとする
$ minikube tunnel
✅ Tunnel successfully started
📌 NOTE: Please do not close this terminal as this process must stay alive for the tunnel to be accessible ...
🏃 Starting tunnel for service grafana.
🏃 Starting tunnel for service prometheus.-
4. kube-apiserverについてトレースの送信設定追加
kube-apiserverで以下を実現できるように設定変更します。
- APIリクエストを処理する際に、内部処理の開始・終了(スパン)を記録し、トレースを生成する
- TracingConfiguration で設定された OTLP(OpenTelemetry Protocol)を使用し、Collector用ServiceのClusterIP7:4317ポートへgRPCでトレースを送信する
Control Planeノードにssh接続し、以下を実施します。
$ minikube ssh --node minikube
docker@minikube:~$
-
kube-apiserver-tracing.yamlの作成と配置
/etc/kubernetes/kube-apiserver-tracing.yamlapiVersion: apiserver.config.k8s.io/v1 kind: TracingConfiguration samplingRatePerMillion: 10000 # endpoint: "otel-collector.monitoring.svc.cluster.local:4317" # otel-collector ServiceのClusterIPを指定、環境によってClusterIPは変わるため、自身の環境に合わせること endpoint: "10.111.150.39:4317" -
kube-apiserverのマニフェストの変更
/etc/kubernetes/manifests/kube-apiserver.yaml<前略> spec: containers: - command: - kube-apiserver - --advertise-address=192.168.49.2 <中略> # 1行追加 - --tracing-config-file=/etc/kubernetes/kube-apiserver-tracing.yaml <中略> volumeMounts: - mountPath: /etc/ssl/certs name: ca-certs readOnly: true <中略> # 3行追加 - mountPath: /etc/kubernetes/kube-apiserver-tracing.yaml name: tracing-config readOnly: true <中略> volumes: - hostPath: path: /etc/ssl/certs type: DirectoryOrCreate name: ca-certs <中略> # 3行追加 - hostPath: path: /etc/kubernetes/kube-apiserver-tracing.yaml type: File name: tracing-config
変更後、kube-apiserver Podが再起動され、アクセス可能であることを確認します。
docker@minikube:~$ exit
logout
$ kubectl get node
NAME STATUS ROLES AGE VERSION
minikube Ready control-plane 41d v1.34.0
minikube-m02 Ready <none> 41d v1.34.0
minikube-m03 Ready <none> 41d v1.34.0
5. Grafanaの構築
以下を実現できるようにGrafanaを構築します。
- Tempoデータソース: TraceQLを使用してTempoから生トレースリストを直接取得し、詳細なトレース分析(Trace List、Trace View)を可能にする
- Prometheusデータソース: Prometheusから時系列メトリクスを取得し、グラフ化する
適用したマニフェストは以下となります。
apiVersion: v1
kind: ConfigMap
metadata:
name: grafana-datasources-config
namespace: monitoring
data:
tempo-datasource.yaml: |
apiVersion: 1
datasources:
- name: Tempo
type: tempo
access: proxy
# Tempo Serviceを指定
url: http://tempo.monitoring.svc.cluster.local:3200
isDefault: false
prometheus-datasource.yaml: |
apiVersion: 1
datasources:
- name: Prometheus
type: prometheus
uid: prometheus-internal-metrics
# Prometheus Serviceを指定
url: http://prometheus.monitoring.svc.cluster.local:9090
access: proxy
isDefault: true
editable: true
jsonData:
# Tempoとの連携設定
spanMetrics:
datasourceUid: tempo
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: grafana
namespace: monitoring
spec:
replicas: 1
selector:
matchLabels:
app: grafana
template:
metadata:
labels:
app: grafana
spec:
nodeSelector:
kubernetes.io/hostname: minikube-m03
initContainers:
- name: fix-permissions
image: busybox:latest
# GrafanaコンテナユーザーのUID 472 に所有権を変更
command: ["sh", "-c", "chown -R 472:472 /var/lib/grafana && chmod 770 /var/lib/grafana"]
securityContext:
runAsUser: 0 # busyboxをrootで実行し、所有権変更を許可
volumeMounts:
- mountPath: /var/lib/grafana
name: grafana-storage
containers:
- name: grafana
image: grafana/grafana:12.3.0
env:
- name: GF_SECURITY_ADMIN_USER
value: admin
- name: GF_SECURITY_ADMIN_PASSWORD
value: admin
- name: GF_PATHS_DATA
value: /var/lib/grafana
ports:
- containerPort: 3000
name: http
volumeMounts:
- name: grafana-data-source
mountPath: /etc/grafana/provisioning/datasources/
readOnly: true
- name: grafana-storage
mountPath: /var/lib/grafana
volumes:
- name: grafana-data-source
configMap:
name: grafana-datasources-config
items:
- key: tempo-datasource.yaml
path: tempo-datasource.yaml
- key: prometheus-datasource.yaml
path: prometheus-datasource.yaml
- name: grafana-storage
hostPath:
path: /var/lib/grafana-data
type: DirectoryOrCreate
---
apiVersion: v1
kind: Service
metadata:
name: grafana
namespace: monitoring
spec:
# minikubeの機能を使用してWSLのWindowsホストからLoadBalancerでアクセス可能とする
type: LoadBalancer
ports:
- port: 8080
targetPort: 3000
protocol: TCP
name: http
selector:
app: grafana
適用後、PodとServiceが正常に作成され、PodのIPアドレスとServiceが紐づけられている(=ENDPOINTSのIPアドレスとして登録されている)ことを確認します。
$ kubectl get -n monitoring pod grafana-78fcfbcccc-t9j6h -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
grafana-78fcfbcccc-t9j6h 1/1 Running 0 8h 10.244.151.27 minikube-m03 <none> <none>
$ kubectl get -n monitoring svc grafana
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
grafana LoadBalancer 10.110.39.106 127.0.0.1 8080:30080/TCP 2d23h
$ kubectl get -n monitoring endpointslices.discovery.k8s.io grafana-xsm9c
NAME ADDRESSTYPE PORTS ENDPOINTS AGE
grafana-xsm9c IPv4 3000 10.244.151.27 2d23h
minikubeの機能を使用して、WSLのWindowsホストからLoadBalancer経由でGrafanaにアクセスし、以下を確認します。
- Tempoデータソースからkube-apiserverの生トレースリストを直接取得し、詳細なトレース分析が可能であること
- Prometheusデータソースからkube-apiserverのトレースの時系列メトリクスを取得し、グラフ化可能であること
おわりに
規模の大きなKubernetesクラスタを構築しがっつり使用するまでは、Kubernetes自体のトレースの必要性はあまり感じないかもしれません。
しかし、Kubernetes運用者にとって可観測性に関する技術はぜひとも押さえておくべき重要な技術と考えています。
本記事がKubernetes運用者の方々の一助となれば幸いです。
引き続き、NTTテクノクロス Advent Calendar 2025 をお楽しみください!


