ランタイムオブザーバビリティを備えた Kubernetes クラスターの作成
Blog posts are not updated after publication. This post is more than a year old, so its content may be outdated, and some links may be invalid. Cross-verify any information before relying on it.
Sebastian Choren、 Adnan Rahić、 Ken Hamric の協力のもと執筆。
Kubernetes は、クラウドネイティブ領域で広く使われているオープンソースシステムであり、クラウド上でコンテナ化されたアプリケーションをデプロイおよびスケールする方法を提供します。 ログやメトリクスを観測する機能はよく知られドキュメント化されていますが、アプリケーショントレースに関するオブザーバビリティは新しい取り組みです。
以下は、Kubernetes エコシステムにおける最近の動向の概要です。
- 最初の議論は 2018年12月に始まり、計装の実装 に関する最初の PR が作成されました。
- KEP(Kubernetes Enhancement Proposal)が 2020年1月に作成され、のちに API Server にスコープが絞られました (KEP 647 - API Server Tracing)。 一方、Kubelet 向けの新しい KEP が 2021年7月に提案されました (KEP 2831 Kubelet Tracing)。
- etcd(Kubernetes が内部データストアとして使用)は、2020年11月にトレーシングの議論を開始し (Add Distributed Tracing using OpenTelemetry)、 2021年5月に最初のバージョンがマージされました。
- Kubernetes の 2 つの Container Runtime Interface である containerd と CRI-O は、2021年にトレーシングの実装を開始しました (CRI-O は 2021年4月、 containerd は 2021年8月)。
- API Server トレーシングは、 v1.22 でアルファ版 (2021年8月)としてリリースされ、 v1.27 でベータ版 (2023年4月)としてリリースされました。
- Kubelet トレーシングは、 v1.25 でアルファ版 (2022年8月)としてリリースされ、 v1.27 でベータ版 (2023年4月)としてリリースされました。
Kubernetes でのトレーシングの現状を調査する中で、有効化する方法をドキュメント化した記事がほとんどないことがわかりました。
たとえば、kubelet のオブザーバビリティに関する Kubernetes ブログの記事 がある程度です。
そこで、私たちの調査結果をドキュメント化し、ローカルで Kubernetes をセットアップしてトレースを確認するための手順を提供することにしました。
この記事では、Kubernetes の計装を使って、API (kube-apiserver)、 ノードエージェント (kubelet)、 コンテナランタイム(containerd)のトレースを観測する方法を学びます。 ローカルのオブザーバビリティ環境をセットアップし、その後トレーシングを有効にした Kubernetes をローカルにインストールします。
まず、ローカルマシンに以下のツールをインストールしてください。
- Docker: コンテナ化された環境を実行できるコンテナ環境
- k3d: k3s(軽量な Kubernetes ディストリビューション)を Docker で実行するためのラッパー
- kubectl: クラスターとやり取りするための Kubernetes CLI
トレースを監視するためのオブザーバビリティスタックのセットアップ
オブザーバビリティスタックをセットアップするために、OpenTelemetry(OTel)Collector を実行します。 これは、さまざまなアプリからテレメトリーデータを受信し、トレーシングバックエンドに送信するツールです。 トレーシングバックエンドとして、トレースを収集しクエリできるオープンソースツールである Jaeger を使用します。
マシン上に kubetracing というディレクトリを作成し、
otel-collector.yaml
というファイルを作成します。
以下のスニペットの内容をコピーして、任意のフォルダに保存してください。
このファイルは、OpenTelemetry Collector が OpenTelemetry 形式でトレースを受信し、Jaeger にエクスポートするように設定します。
receivers:
otlp:
protocols:
grpc:
http:
processors:
probabilistic_sampler:
hash_seed: 22
sampling_percentage: 100
batch:
timeout: 100ms
exporters:
logging:
logLevel: debug
otlp/jaeger:
endpoint: jaeger:4317
tls:
insecure: true
service:
pipelines:
traces:
receivers: [otlp]
processors: [probabilistic_sampler, batch]
exporters: [otlp/jaeger, logging]
次に、同じフォルダに docker-compose.yaml ファイルを作成します。 このファイルには、Jaeger 用と OpenTelemetry Collector 用の 2 つのコンテナが含まれます。
services:
jaeger:
healthcheck:
test:
- CMD
- wget
- --spider
- localhost:16686
timeout: 3s
interval: 1s
retries: 60
image: jaegertracing/all-in-one:latest
restart: unless-stopped
environment:
- COLLECTOR_OTLP_ENABLED=true
ports:
- 16686:16686
otel-collector:
command:
- --config
- /otel-local-config.yaml
depends_on:
jaeger:
condition: service_started
image: otel/opentelemetry-collector:0.54.0
ports:
- 4317:4317
volumes:
- ./otel-collector.yaml:/otel-local-config.yaml
次に、kubetracing フォルダで以下のコマンドを実行してオブザーバビリティ環境を起動します。
docker compose up
これにより、Jaeger と OpenTelemetry Collector の両方が起動し、他のアプリからトレースを受信できるようになります。
ランタイムオブザーバビリティを備えた Kubernetes クラスターの作成
オブザーバビリティ環境のセットアップが完了したら、kube-apiserver、kubelet、containerd で OpenTelemetry トレーシングを有効にするための設定ファイルを作成します。
kubetracing フォルダ内に config というサブフォルダを作成し、以下の 2 つのファイルを配置します。
1 つ目は
apiserver-tracing.yaml
で、Kubernetes API の実行データを含むトレースをエクスポートするために kube-apiserver が使用するトレーシング設定が含まれています。
この設定では、samplingRatePerMillion 設定を使って API がトレースの 100% を送信するように設定します。
エンドポイントを host.k3d.internal:4317 に設定して、k3d/k3s で作成されたクラスターがマシン上の別の API を呼び出せるようにします。
この場合、docker compose でポート 4317 にデプロイされた OpenTelemetry Collector です。
apiVersion: apiserver.config.k8s.io/v1beta1
kind: TracingConfiguration
endpoint: host.k3d.internal:4317
samplingRatePerMillion: 1000000 # 100%
2 つ目のファイルは
kubelet-tracing.yaml
で、kubelet の追加設定を提供します。
ここでは、フィーチャーフラグ KubeletTracing(この記事の執筆時点での最新バージョンである Kubernetes 1.27 のベータ機能)を有効にし、kube-apiserver と同じトレーシング設定を行います。
apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
featureGates:
KubeletTracing: true
tracing:
endpoint: host.k3d.internal:4317
samplingRatePerMillion: 1000000 # 100%
kubetracing フォルダに戻り、最後のファイル
config.toml.tmpl
を作成します。
これは、k3s が containerd を設定するために使用するテンプレートファイルです。
このファイルは k3s が使用するデフォルト設定に似ていますが、ファイルの末尾に containerd がトレースを送信するように設定する 2 つのセクションが追加されています。
version = 2
[plugins."io.containerd.internal.v1.opt"]
path = "{{ .NodeConfig.Containerd.Opt }}"
[plugins."io.containerd.grpc.v1.cri"]
stream_server_address = "127.0.0.1"
stream_server_port = "10010"
enable_selinux = {{ .NodeConfig.SELinux }}
enable_unprivileged_ports = {{ .EnableUnprivileged }}
enable_unprivileged_icmp = {{ .EnableUnprivileged }}
{{- if .DisableCgroup}}
disable_cgroup = true
{{end}}
{{- if .IsRunningInUserNS }}
disable_apparmor = true
restrict_oom_score_adj = true
{{end}}
{{- if .NodeConfig.AgentConfig.PauseImage }}
sandbox_image = "{{ .NodeConfig.AgentConfig.PauseImage }}"
{{end}}
{{- if .NodeConfig.AgentConfig.Snapshotter }}
[plugins."io.containerd.grpc.v1.cri".containerd]
snapshotter = "{{ .NodeConfig.AgentConfig.Snapshotter }}"
disable_snapshot_annotations = {{ if eq .NodeConfig.AgentConfig.Snapshotter "stargz" }}false{{else}}true{{end}}
{{ if eq .NodeConfig.AgentConfig.Snapshotter "stargz" }}
{{ if .NodeConfig.AgentConfig.ImageServiceSocket }}
[plugins."io.containerd.snapshotter.v1.stargz"]
cri_keychain_image_service_path = "{{ .NodeConfig.AgentConfig.ImageServiceSocket }}"
[plugins."io.containerd.snapshotter.v1.stargz".cri_keychain]
enable_keychain = true
{{end}}
{{ if .PrivateRegistryConfig }}
{{ if .PrivateRegistryConfig.Mirrors }}
[plugins."io.containerd.snapshotter.v1.stargz".registry.mirrors]{{end}}
{{range $k, $v := .PrivateRegistryConfig.Mirrors }}
[plugins."io.containerd.snapshotter.v1.stargz".registry.mirrors."{{$k}}"]
endpoint = [{{range $i, $j := $v.Endpoints}}{{if $i}}, {{end}}{{printf "%q" .}}{{end}}]
{{if $v.Rewrites}}
[plugins."io.containerd.snapshotter.v1.stargz".registry.mirrors."{{$k}}".rewrite]
{{range $pattern, $replace := $v.Rewrites}}
"{{$pattern}}" = "{{$replace}}"
{{end}}
{{end}}
{{end}}
{{range $k, $v := .PrivateRegistryConfig.Configs }}
{{ if $v.Auth }}
[plugins."io.containerd.snapshotter.v1.stargz".registry.configs."{{$k}}".auth]
{{ if $v.Auth.Username }}username = {{ printf "%q" $v.Auth.Username }}{{end}}
{{ if $v.Auth.Password }}password = {{ printf "%q" $v.Auth.Password }}{{end}}
{{ if $v.Auth.Auth }}auth = {{ printf "%q" $v.Auth.Auth }}{{end}}
{{ if $v.Auth.IdentityToken }}identitytoken = {{ printf "%q" $v.Auth.IdentityToken }}{{end}}
{{end}}
{{ if $v.TLS }}
[plugins."io.containerd.snapshotter.v1.stargz".registry.configs."{{$k}}".tls]
{{ if $v.TLS.CAFile }}ca_file = "{{ $v.TLS.CAFile }}"{{end}}
{{ if $v.TLS.CertFile }}cert_file = "{{ $v.TLS.CertFile }}"{{end}}
{{ if $v.TLS.KeyFile }}key_file = "{{ $v.TLS.KeyFile }}"{{end}}
{{ if $v.TLS.InsecureSkipVerify }}insecure_skip_verify = true{{end}}
{{end}}
{{end}}
{{end}}
{{end}}
{{end}}
{{- if not .NodeConfig.NoFlannel }}
[plugins."io.containerd.grpc.v1.cri".cni]
bin_dir = "{{ .NodeConfig.AgentConfig.CNIBinDir }}"
conf_dir = "{{ .NodeConfig.AgentConfig.CNIConfDir }}"
{{end}}
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc]
runtime_type = "io.containerd.runc.v2"
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options]
SystemdCgroup = {{ .SystemdCgroup }}
{{ if .PrivateRegistryConfig }}
{{ if .PrivateRegistryConfig.Mirrors }}
[plugins."io.containerd.grpc.v1.cri".registry.mirrors]{{end}}
{{range $k, $v := .PrivateRegistryConfig.Mirrors }}
[plugins."io.containerd.grpc.v1.cri".registry.mirrors."{{$k}}"]
endpoint = [{{range $i, $j := $v.Endpoints}}{{if $i}}, {{end}}{{printf "%q" .}}{{end}}]
{{if $v.Rewrites}}
[plugins."io.containerd.grpc.v1.cri".registry.mirrors."{{$k}}".rewrite]
{{range $pattern, $replace := $v.Rewrites}}
"{{$pattern}}" = "{{$replace}}"
{{end}}
{{end}}
{{end}}
{{range $k, $v := .PrivateRegistryConfig.Configs }}
{{ if $v.Auth }}
[plugins."io.containerd.grpc.v1.cri".registry.configs."{{$k}}".auth]
{{ if $v.Auth.Username }}username = {{ printf "%q" $v.Auth.Username }}{{end}}
{{ if $v.Auth.Password }}password = {{ printf "%q" $v.Auth.Password }}{{end}}
{{ if $v.Auth.Auth }}auth = {{ printf "%q" $v.Auth.Auth }}{{end}}
{{ if $v.Auth.IdentityToken }}identitytoken = {{ printf "%q" $v.Auth.IdentityToken }}{{end}}
{{end}}
{{ if $v.TLS }}
[plugins."io.containerd.grpc.v1.cri".registry.configs."{{$k}}".tls]
{{ if $v.TLS.CAFile }}ca_file = "{{ $v.TLS.CAFile }}"{{end}}
{{ if $v.TLS.CertFile }}cert_file = "{{ $v.TLS.CertFile }}"{{end}}
{{ if $v.TLS.KeyFile }}key_file = "{{ $v.TLS.KeyFile }}"{{end}}
{{ if $v.TLS.InsecureSkipVerify }}insecure_skip_verify = true{{end}}
{{end}}
{{end}}
{{end}}
{{range $k, $v := .ExtraRuntimes}}
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes."{{$k}}"]
runtime_type = "{{$v.RuntimeType}}"
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes."{{$k}}".options]
BinaryName = "{{$v.BinaryName}}"
{{end}}
[plugins."io.containerd.tracing.processor.v1.otlp"]
endpoint = "host.k3d.internal:4317"
protocol = "grpc"
insecure = true
[plugins."io.containerd.internal.v1.tracing"]
sampling_ratio = 1.0
service_name = "containerd"
これらのファイルを作成したら、kubetracing フォルダ内でターミナルを開き、k3d を実行してクラスターを作成します。
このコマンドを実行する前に、[CURRENT_PATH] プレースホルダーを kubetracing フォルダの完全なパスに置き換えてください。
そのフォルダ内のターミナルで echo $PWD コマンドを実行するとパスを取得できます。
k3d cluster create tracingcluster \
--image=rancher/k3s:v1.27.1-k3s1 \
--volume '[CURRENT_PATH]/config.toml.tmpl:/var/lib/rancher/k3s/agent/etc/containerd/config.toml.tmpl@server:*' \
--volume '[CURRENT_PATH]/config:/etc/kube-tracing@server:*' \
--k3s-arg '--kube-apiserver-arg=tracing-config-file=/etc/kube-tracing/apiserver-tracing.yaml@server:*' \
--k3s-arg '--kube-apiserver-arg=feature-gates=APIServerTracing=true@server:*' \
--k3s-arg '--kubelet-arg=config=/etc/kube-tracing/kubelet-tracing.yaml@server:*'
このコマンドは、バージョン v1.27.1 の Kubernetes クラスターを作成し、マシン上の 3 つの Docker コンテナにセットアップします。
ここで kubectl cluster-info コマンドを実行すると、以下の出力が表示されます。
Kubernetes control plane is running at https://0.0.0.0:60503
CoreDNS is running at https://0.0.0.0:60503/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy
Metrics-server is running at https://0.0.0.0:60503/api/v1/namespaces/kube-system/services/https:metrics-server:https/proxy
オブザーバビリティ環境のログに戻ると、Kubernetes の内部オペレーションのスパンが OpenTelemetry Collector に送信されているのが確認できるはずです。 以下はその例です。
Span #90
Trace ID : 03a7bf9008d54f02bcd4f14aa5438202
Parent ID :
ID : d7a10873192f7066
Name : KubernetesAPI
Kind : SPAN_KIND_SERVER
Start time : 2023-05-18 01:51:44.954563708 +0000 UTC
End time : 2023-05-18 01:51:44.957555323 +0000 UTC
Status code : STATUS_CODE_UNSET
Status message :
Attributes:
-> net.transport: STRING(ip_tcp)
-> net.peer.ip: STRING(127.0.0.1)
-> net.peer.port: INT(54678)
-> net.host.ip: STRING(127.0.0.1)
-> net.host.port: INT(6443)
-> http.target: STRING(/api/v1/namespaces/kube-system/pods/helm-install-traefik-crd-8w4wd)
-> http.server_name: STRING(KubernetesAPI)
-> http.user_agent: STRING(k3s/v1.27.1+k3s1 (linux/amd64) kubernetes/bc5b42c)
-> http.scheme: STRING(https)
-> http.host: STRING(127.0.0.1:6443)
-> http.flavor: STRING(2)
-> http.method: STRING(GET)
-> http.wrote_bytes: INT(4724)
-> http.status_code: INT(200)
クラスターランタイムのテスト
オブザーバビリティ環境と Kubernetes クラスターのセットアップが完了したので、Kubernetes に対してコマンドを実行し、Jaeger でこれらのアクションのトレースを確認できます。
ブラウザを開き、http://localhost:16686/search にある Jaeger UI に移動します。
apiserver、containerd、kubelet サービスがトレースを公開していることが確認できます。

apiserver を選択し、「Find Traces」 をクリックします。
ここで Kubernetes コントロールプレーンからのトレースが確認できます。

kubectl を使って Kubernetes に対してサンプルコマンドを実行してみましょう。
たとえば echo の実行です。
$ kubectl run -it --rm --restart=Never --image=alpine echo-command -- echo hi
# 出力
# コマンドプロンプトが表示されない場合は、Enter キーを押してみてください。
# warning: couldn't attach to pod/echo-command, falling back to streaming logs: unable to upgrade connection: container echo-command not found in pod echo-command_default
# Hi
# pod "echo-command" deleted
次に、Jaeger を再度開き、kubelet サービス、オペレーション syncPod を選択し、タグ k8s.pod=default/echo-command を追加すると、この Pod に関連するスパンが表示されるはずです。

トレースを展開すると、この Pod を作成したオペレーションが確認できます。

まとめ
ベータ版であっても、 kubelet と apiserver の両方のトレースは、開発者が Kubernetes 内部で何が起きているかを理解し、問題のデバッグを始めるのに役立ちます。
これは、内部リソースを更新して Kubernetes に機能を追加する Kubernetes Operators のようなカスタムタスクを作成する開発者にとって有用です。
オブザーバビリティ分野のオープンソースツールの構築に注力するチームとして、OpenTelemetry コミュニティ全体を支援する機会は私たちにとって重要でした。 そのため、Kubernetes のコアエンジンからトレースを収集する新しい方法を研究していました。 Kubernetes が公開しているオブザーバビリティの現在のレベルを踏まえ、Kubernetes エンジンにおける分散トレーシングの現状に関心を持つ他の人々を支援するために、私たちの調査結果を公開したいと考えました。 Daniel Dias と Sebastian Choren は、OpenTelemetry を使って分散システムの開発とテストを行えるオープンソースツールである Tracetest に取り組んでいます。 Tracetest はあらゆる OTel 互換システムで動作し、トレースベースのテストを作成できます。 https://github.com/kubeshop/tracetest で確認してください。
この記事で使用した サンプルソース と セットアップ手順 は Tracetest リポジトリから入手できます。