インフラ●●●●○

Kubernetes

Kubernetes ・ くばねてぃす

多数のコンテナの配置・回復・増減を、宣言と現実の差を埋め続ける調整ループで自動化する基盤。

✦ 30秒で試す — 航海誰も手順を書いていないのに、落ちたサーバの仕事が引っ越すノード2にいたPodは、いつ別のノードに作り直される?予想してみる

概要

Kubernetes(クバネティス、略して k8s)は、多数のコンテナを複数のサーバにまたがって自動運用するための基盤(コンテナオーケストレータ)です。「このアプリのコンテナを3つ、常に動かしておいてほしい」という望ましい状態を設定ファイルで宣言すると、Kubernetes がコンテナの置き場所を決め、落ちたら立て直し、負荷が増えたら数を増やす — という運用を休まず続けます。

利用者が書くのは「どうやるか」ではなく「どうあるべきか」です。この宣言を受け取ったあとは、いくつもの小さな制御プログラムが、宣言と現実の食い違いを見つけては埋める作業を繰り返します。この「調整ループ」が Kubernetes の心臓で、スケーリングも障害からの回復も、同じループの結果として起きます。

2014年に Google がオープンソースとして公開し、2015年の 1.0 と同時に Linux Foundation 傘下の CNCF(Cloud Native Computing Foundation)へ寄贈されました。いまではコンテナを本番で動かすときの事実上の標準で、AWS(EKS)、Google Cloud(GKE)、Azure(AKS)といった主要なクラウドがどれもマネージドサービスとして提供しています。

なぜ生まれたか

コンテナは1個動かすだけなら簡単ですが、本番では話が変わります。数十〜数千のコンテナを複数のサーバに配り、どれかが異常終了したら再起動し、サーバごと壊れたら別のサーバに移し、リリースのたびに無停止で入れ替え、負荷に応じて数を調整する — これを人間が手順書とコマンドでこなすのは現実的ではありません。コンテナが普及するほど、「コンテナの群れを誰が面倒みるのか」が深刻な問題になりました。

Google はこの問題に10年早くぶつかっていました。社内では2000年代半ばから Borg というクラスタ管理システムが、検索や Gmail を含むほぼすべてのワークロードを数万台規模のマシンに詰め込んで動かしていました(2015年に論文で公開)。Borg で得た教訓 — ジョブを「宣言」として登録し、配置はスケジューラに任せ、マシンの故障は前提として扱う — を、社外でも使える形に作り直したのが Kubernetes です。Borg の開発者の一部がそのまま Kubernetes の設計に加わりました。

Kubernetes の答えの核は「宣言的運用」です。人間は手順ではなく状態だけを書き、システムが現実と宣言のずれを検出して直し続けます。手順は途中で失敗すると「どこまでやったか」が分からなくなりますが、状態の宣言は何度読み直しても同じ意味を持つので、途中で何が起きても最後は宣言どおりに収束します。障害対応・再配置・スケールという判断を機械に委ねられるようになったのは、この発想の転換のおかげです。

詳細

クラスタの構造 — コントロールプレーンとワーカーノード

Kubernetes は複数のマシンを束ねて1つの「クラスタ」として扱います。クラスタは、全体を管理するコントロールプレーンと、実際にコンテナが動くワーカーノード群から成ります。コントロールプレーンの中心は API サーバで、利用者の宣言も、各部品の報告も、すべてここを通ります。API サーバが受け取った状態は etcd という分散キーバリューストアに保存されます。etcd は3台や5台で構成され、合意アルゴリズムの Raft で過半数が同意した書き込みだけを確定させるため、1台が壊れてもクラスタの「記憶」は失われません。

Pod の置き場所を決めるのはスケジューラ、宣言と現実の差を埋めるのはコントローラ群(Deployment・ReplicaSet・ノードの監視など、種類ごとに別のループ)です。どれも互いに直接話さず、API サーバを watch(変更の購読)して、自分の担当する食い違いを見つけたら API サーバに書き込むだけです。各ワーカーノードでは kubelet というエージェントが動き、自分のノードに割り当てられた Pod を watch して、containerd などのコンテナランタイムに起動を頼みます。なお、このスケジューラは OS の CPU スケジューラとは別物ですが、「限られた資源に仕事をどう割り当てるか」を決めるという点で同じ発想です。

コントロールプレーン利用者kubectl applyスケジューラ置き場所を決めるコントローラ群差分を埋めるAPI サーバ唯一の窓口etcdRaft で合意して保存互いに直接話さずAPI サーバを watch する宣言を送るノード1ノード2ノード3kubeletkubeletkubeletランタイムcontainerdランタイムcontainerdランタイムcontainerdPodPodPodPodPodkubelet は自分のノードに割り当てられた Pod を watch し、ランタイムに起動させて状態を報告する
クラスタの構造 — 部品どうしは直接話さず、API サーバを通して宣言と現実を読み書きする

デプロイの最小単位は Pod で、1つ以上のコンテナを、同じネットワーク(同じ IP アドレス)と共有ボリュームの中にまとめたものです。通常は Pod を直接作らず、「この形の Pod を3つ維持せよ」という Deployment を宣言します。宣言がノード上のコンテナになるまでには、次のように複数のループが順にバトンを渡していきます。どの部品も相手を呼び出してはおらず、API サーバに書かれた変化を見て自分の仕事をしているだけです。

ノードのkubeletスケジューラコントローラAPIサーバ利用者ノードのkubeletスケジューラコントローラAPIサーバ利用者Deployment replicas=3 を送る変更の通知 watch足りない Pod を3つ作る 置き場所は未定未配置の Pod の通知Pod をノード2に割り当てる自分のノード宛ての Pod の通知ランタイムにコンテナを起動させるPod は Running と報告

調整ループ — 自己修復の正体

コントローラの仕事はどれも同じ形をしています。宣言された状態と現実の状態を見比べ、差があれば差を縮める行動を1つ取り、また見比べる。Pod が3つのはずが2つしかなければ1つ作る。4つあれば1つ消す。この単純なループが止まらずに回り続けることが、自己修復(セルフヒーリング)の正体です。ループは「何が起きたか」(イベント)ではなく「いまどうなっているか」(状態)を見て動くので、通知を取りこぼしても次に見比べたときに追いつけます。

差分なし

差分あり

現実の状態を観測

宣言された状態と比較

差分を埋める行動を1つ取る

面白いのは分業です。コントローラは「数が足りない」ことしか見ておらず、新しい Pod をどこに置くかは知りません。作られた Pod はいったん置き場所のない状態(Pending)になり、それをスケジューラが拾って、空きのあるノードに割り当てます。スケジューラが見るのは、各 Pod が宣言した資源の要求量(requests)とノードの空きで、実際の使用量ではありません。要求量を大きく書きすぎるとノードに空きがあるのに置けず、小さく書きすぎると詰め込みすぎて実行時に取り合いになります。

ノードごと壊れたときは、もう1段の待ち時間が入ります。ノードの kubelet から報告が途絶えると、ノード監視のコントローラが数十秒でそのノードを NotReady と判定しますが、そこにいた Pod はすぐには消されません。一時的なネットワークの揺らぎでいちいち作り直さないよう、既定では約5分待ってから Pod を退去させ、そこで初めて「数が足りない」ことになって別のノードに作り直されます。戻ってきたノードに既存の Pod を戻すこともしません。偏りを直すのは別の仕組み(descheduler など)の仕事です。

下の部品で確かめてください。Pod を押して止めると、コントローラが数の不足に気づいて作り、スケジューラが置き場所を決めます。ノードの電源を押してノードごと落とすと、そこにいた Pod は応答なしになり、待ち時間のあとで他のノードに作り直されます(部品では待ち時間を数秒に縮めています)。

⚡ 体験: 宣言と現実の差を埋める調整ループ
コントロールプレーン宣言(etcd に保存)Deployment webreplicas−3+見比べるコントローラ数の差を埋めるスケジューラ置き場所を決める未配置(Pending)ノード1Ready 1/4ノード2Ready 1/4ノード3Ready 1/4web-2nv稼働中web-jqf稼働中web-z5p稼働中
宣言3稼働中3置き場所待ち0応答なし0状態一致
観測 3 / 宣言 3 → 一致(何もしない)

宣言は「web を 3 つ」。金の Pod を押して消すか、ノード右上の ⏻ を押してノードごと落としてみてください。

サービス検出と負荷分散、スケーリング

Pod は頻繁に作り直され、そのたびに IP アドレスが変わります。そこで Kubernetes は Service という安定した窓口を用意し、その背後にいる Pod へリクエストを振り分けます。どの Pod を振り分け先に入れるかはヘルスチェック(Readiness プローブ)の結果で決まり、準備のできていない Pod には流しません。クラスタ内には DNS が組み込まれていて、Pod 同士は Service の名前で通信できます。外部からの HTTP トラフィックは Ingress(後継の Gateway API)で受け、実質的にロードバランサやリバースプロキシとして働きます。CPU 使用率などの指標に応じて Pod の数そのものを増減する Horizontal Pod Autoscaler も、「replicas の宣言を書き換えるコントローラ」にすぎません。スケーリングもまた、調整ループの上に載っています。

エコシステムと実務での位置づけ

宣言がすべて API サーバに集まる構造は、周辺の道具を作りやすくしました。パッケージ管理の Helm、Git リポジトリを宣言の唯一の情報源にしてクラスタを同期させる GitOps(Argo CD など)、監視の Prometheus、通信を横取りして制御するサービスメッシュと、その土台になるサイドカー。独自のリソースと独自のコントローラを足して Kubernetes を拡張する「オペレーター」も、同じ調整ループの型に従っています。インフラをコードで宣言する IaC と考え方が近く、両者を組み合わせてクラスタの外側まで宣言で管理するのが一般的です。マイクロサービスのように多数の独立したサービスを動かす構成では、Kubernetes が標準的な足場になっています。

障害対応も変わりました。以前は人間がアラートを受けて手で再起動していたものの多くが、システムが先に直し、人間は後から知る形になります。一方で、自動で直ることが問題を隠すこともあります。再起動を繰り返している Pod(CrashLoopBackOff)は、ループが回っているあいだは「数は足りている」ように見えるため、再起動の回数そのものを監視しておく必要があります。

その強力さは、学習と運用の重さと表裏一体です。クラスタ自体のバージョンアップ(おおむね年3回のマイナーリリースと、それぞれ約14か月のサポート期間)、ネットワークや権限の設定、etcd のバックアップなど、Kubernetes 自身の面倒をみる仕事が新たに生まれます。マネージドサービスを使ってもこの複雑さは半分しか消えません。小規模なシステムなら、コンテナを1台で動かす、PaaS やサーバレスに載せる、といった選択肢のほうが総コストで勝つことも多く、「本当にこの規模の自動化が必要か」を最初に問うのが、Kubernetes との正しい付き合い方です。

試問

画面には、宣言(「web の Pod をいくつ動かし続けるか」)と、宣言と現実の数の差を埋めるコントローラ、Pod の置き場所を決めるスケジューラ、そして Pod を載せる3台のワーカーノードがあります。宣言の数は − と + で変えられ、ノード右上の ⏻ でノードごと落とせます(もう一度押すと戻ります)。落ちたノードにいた Pod は、応答が途絶えてから猶予(ここでは4秒)を待って、別のノードに作り直されます。

✦ 命題挑戦中

ノードを1台落としたまま、宣言と現実が一致する範囲で、宣言する Pod の数をいちばん大きくせよ

  • ノードをちょうど1台だけ落としたままにする
  • 宣言した数の Pod がすべて稼働し、宣言と現実が一致している
  • その宣言が、これ以上1つ増やすと置き場所が足りなくなる数になっている
⚡ 体験: 1台落ちても支えきれる数を探す
コントロールプレーン宣言(etcd に保存)Deployment webreplicas−3+見比べるコントローラ数の差を埋めるスケジューラ置き場所を決める未配置(Pending)ノード1Ready 1/4ノード2Ready 1/4ノード3Ready 1/4web-2nv稼働中web-jqf稼働中web-z5p稼働中
宣言3稼働中3置き場所待ち0応答なし0状態一致
観測 3 / 宣言 3 → 一致(何もしない)

宣言は「web を 3 つ」。金の Pod を押して消すか、ノード右上の ⏻ を押してノードごと落としてみてください。