可観測性 — 三つ(あるいは四つ)のシグナル
一言でいうと
モニタリングは「あらかじめ決めておいた質問に答えること」で、オブザーバビリティは「あらかじめ思いつかなかった質問にも答えられる性質」です。シグナルを分ける理由は、それぞれが答えられる質問が違うからです。
なぜ必要なのか
単一サーバーの時代は、ログファイル1つで足りました。マイクロサービスでは、リクエスト1つがサービス10個を通過し、そのうちどこが遅いのかを、ログだけからは知ることができません。しかもPodは死んでは新しく起動するので、ログを残した主体がすでに消えたあとで、調査を始めることになります。
そのため、「障害が起きたらサーバーに入って見る」という方式が成り立ちません。そもそも入るべきサーバーがないか、すでに入れ替わっているからです。データを外に出しておく必要があります。
どう動くのか
3本の柱、そして4本目
| シグナル | 答える質問 | コスト | 代表的なツール |
|---|---|---|---|
| メトリクス(Metrics) | 今どれくらい悪いか。いつからか | 安い (集計された数値) | Prometheus |
| ログ(Logs) | そのとき正確に何があったか | 中程度から高い | Loki、Elasticsearch |
| トレース(Traces) | このリクエストはどこで時間を使ったか | 高い (通常はサンプリング) | Jaeger、Tempo |
| プロファイル(Profiles) | このプロセスのCPU/メモリを誰が食っているか | 高い | Pyroscope |
調査の順序も、たいていはこの順です。メトリクスで異常を検知して範囲を絞り、トレースでどの区間かを見つけ、ログでその区間の事情を読みます。ログから探し始めると、干し草の山から針を探すことになります。
Kubernetesでの収集経路
- メトリクス: 各コンポーネントとアプリが
/metricsを公開 → Prometheusが定期的にpullします。 - ログ: コンテナのstdout/stderr → ノードのディスク上のファイル → DaemonSetのコレクターが読み取って送信します。
- トレース: アプリが計装されてOTLPでpush → コレクター → バックエンドという流れです。
ここでKCNAが指摘するポイントが1つあります。コンテナのログはstdout/stderrに送る必要があります。アプリが自分のファイルに書くと、Podが死ぬときに一緒に消え、コレクターも見られません。「ログはイベントストリーム」という12-factorの項目が、まさにこの話です。
OpenTelemetryが解く問題
以前は、バックエンドごとにSDKが違いました。Jaegerを使っていて別のものに変えると、アプリケーションコードを改めて修正しなければなりませんでした。OTelは計装のAPI/SDKと転送プロトコル(OTLP)を標準化して、この依存を断ち切りました。計装は一度だけ、バックエンドはコレクターの設定で切り替えます。
Kubernetesが最初から提供する観測の材料
- イベント:
kubectl describe podの下のほうです。スケジューリングの失敗、イメージpullの失敗、プローブの失敗など、コントロールプレーンの判断の根拠がここに残ります。ログを調べる前に見る場所です。 - ステータス(status):
.status.conditionsと.status.phaseです。 - metrics-server:
kubectl topが使う最小限のリソース使用量です。長期保管はしません。
信頼性の用語
- SLI: 実際に測定する指標です(例: 成功したリクエストの割合)。
- SLO: その指標の目標です(例: 30日間で99.9%)。
- SLA: その目標を守れなかったときの、契約上の賠償です。技術ではなく契約の概念です。
- エラーバジェット: SLOが99.9%なら、0.1%の分は壊れてもよいという予算です。この予算が残っていればデプロイをより速く、尽きれば安定化に集中するというように、意思決定の基準になります。
現場での姿
筆者のホームラボで、オブザーバビリティが劇的に現れた事例が2つあります。
1つ目は、CiliumのHubbleです。サイドカーを1つも注入せず、eBPFでカーネルから直接観測するのですが、実際の出力は次のとおりです。
07:50:38.064: ebpf-demo/client:47918 -> ebpf-demo/api:80 http-request FORWARDED (HTTP/1.1 GET http://api/)
07:50:38.223: ebpf-demo/client:47932 -> ebpf-demo/api:80 http-request DROPPED (HTTP/1.1 POST http://api/)
07:50:38.223: ebpf-demo/client:47932 <- ebpf-demo/api:80 http-response FORWARDED (HTTP/1.1 403 0ms POST)
メソッド・パス・レスポンスコード・遅延時間がすべて見え、ポリシーによるDROPPEDが明示されます。「なぜ遮断されたのか」を、推測ではなく記録でわかることが核心です。同じクラスターのnginxは、ポリシー適用前はGET 200 / POST 200でしたが、適用後はGET 200 / POST 403に分かれ、その変化がそのままフローログに残りました。
2つ目は、先ほど出てきたKubeVirtの事例です。コンポーネントの状態はすべてAllComponentsReadyなのに、VMは起動しませんでした。状態フィールドは「自分が起動した」と言うだけで、「自分がやる仕事が実際に行われている」とは言いません。そのため筆者は、クラスターの検証を状態の照会ではなく、実際の動作シナリオ14個(ノードReady、コントロールプレーンのコンポーネント、CNI、CoreDNS、ワーカーのスケジューリング、Pod間通信、サービスDNS + HTTP 200)で組み、통과 14 / 실패 0(韓国語の表示で、合格14件・失敗0件という意味です)を確認しました。オブザーバビリティの最後の形は、合成チェック(synthetic check)です。
次のクイズで確認すること
このコースの最後のクイズで、CNCFのエコシステムとオブザーバビリティをあわせて点検します。そのあとはKCSAです。ここまで学んだ構造を攻撃者の目でもう一度見るコースです。