Pod一つからワークロードコントローラまで
一言でいうと
Podはデプロイの単位ではなくスケジューリングの単位です。人が直接作る場面はほとんどなく、実際にはコントローラーが作ってくれます。どのコントローラーを選ぶかは、「この仕事はいつ終わるのか」で分かれます。
なぜ必要なのか
コンテナ1つが最小単位だとすると、ログ収集器やプロキシのような補助プロセスをどこに置くかが曖昧になります。同じイメージに詰め込むと、12-factorの「プロセス1つに関心事1つ」が崩れ、別のマシンに置くとlocalhostで通信できません。
Podはその間の答えです。ネットワークネームスペース(同じIP・ポート空間)とボリュームを共有するコンテナの束。そのため、サイドカーはlocalhostでアプリに接続でき、アプリとサイドカーは常に同じノードに一緒にスケジュールされます。
ただしPod自体は自分を復活させません。ノードが落ちると、その上のPodも一緒に消えます。だからPodの上にコントローラーが必要なのです。
どう動くのか
所有関係の連鎖
Deployment --(소유)--> ReplicaSet --(소유)--> Pod
Deploymentを作るとコントローラーマネージャーがReplicaSetを作り、ReplicaSetがPodを作ります。各オブジェクトのmetadata.ownerReferencesに親が記録され、このフィールドがガベージコレクションの根拠になります。Deploymentを削除すると、ownerReferencesをたどってReplicaSetとPodが連鎖的に片付けられます。
Deploymentをロールアウトすると、なぜReplicaSetが複数になるのでしょうか。以前のバージョンのReplicaSetを残しておくからです。そのためkubectl rollout undoが可能になります。元に戻すというのは、古いReplicaSetのreplicasをもう一度増やすことにほかなりません。
ワークロードコントローラーの選び方
| コントローラー | いつ使うか | 終わるか |
|---|---|---|
| Deployment | ステートレスなサービス。Web、API | 終わりません |
| StatefulSet | 安定した名前・順序・専用ストレージが必要なもの。DB、キュー | 終わりません |
| DaemonSet | ノードごとに1つずつ。ログ収集器、CNI、ノードエクスポーター | 終わりません |
| Job | 1回動いて終わる仕事。マイグレーション、バッチ | 終わります |
| CronJob | 決まった時刻にJobを作る | Jobが終わります |
核心となる判別式は「このプロセスは自分で終わるのか」です。終わる仕事にDeploymentを使うと、コンテナが終了するたびに再起動されて無限ループになります。そのため、JobのPodテンプレートはrestartPolicyにAlwaysを使えません。
DaemonSetにはreplicasフィールドがありません。個数を人が決めるのではなく、ノード数がそのまま個数だからです。ノードを追加すると、自動的に1つ増えます。
セルフヒーリングは魔法ではない
Podを削除すると再び作られるのは、ReplicaSetコントローラーが次のように動いているからです。
- 自分のセレクターに合うPodは、今何個ありますか。
- spec.replicasは何個ですか。
- 足りなければ作り、余っていれば削除します。
同じ名前のPodが戻ってくるのではなく、新しいPodが作られます。名前は毎回変わります。そのため、Podの名前に依存した設計は、いつも壊れます。
プローブ
- livenessProbe: 失敗するとコンテナを再起動します。「死んだので起動し直せ」という意味です。
- readinessProbe: 失敗するとServiceのエンドポイントから外します。「生きてはいるが、今はトラフィックを受けにくい」という意味です。
- startupProbe: 起動の遅いアプリのために、liveness/readinessの開始を遅らせます。
readinessを付けないと起動中のPodにトラフィックが突き刺さり、livenessを攻撃的に設定しすぎると、少し遅くなっただけのアプリが再起動を繰り返して状況が悪化します。
現場での姿
筆者のホームラボでは、Ciliumをインストールした直後にhubble-relayとhubble-uiのPodがPendingのままになりました。イベントは0/1 nodes are available: 1 node(s) had untolerated taint(s)でした。
原因は、コントローラーの種類の違いでした。コントロールプレーンノードにはnode-role.kubernetes.io/control-plane:NoScheduleのtaintが設定されていますが、hubbleの構成要素はDaemonSetではなくDeploymentなので、このtaintを許容しません。同じ時点でCoreDNSはうまく起動していましたが、それはCoreDNSがデフォルトでcontrol-planeのトレラレーションを持っているからでした。これはエラーではなく正常な動作で、ワーカーノードが参加するとすぐに解消しました。
もう1つ。同じクラスターを7ノードに拡張してGPUワーカーが4台になった際(RTX 3090 24GB、RTX 5090 32GB、RTX 4070 Laptop 8GBが2台)、Podがnvidia.com/gpu: 1だけを要求すると、32GBの5090が必要な学習が、8GBのノートPC用GPUに載ってしまうことがありました。Kubernetesから見れば、どちらも「GPU 1個」だからです。結局、gpu.homelab/tier=xlarge|large|smallのような意味ベースのラベルを自分で付け、nodeSelectorで選んで使うようにしました。リソース名が同じだからといって同じリソースではないこと、そしてそのギャップを埋めるのがラベルであることを示す事例です。
次のラボですること
次のラボで最初のPodを起動し、Deploymentを作ってスケールし、ReplicaSetのownerReferencesを自分で確認します。JobとCronJobとDaemonSetを1つずつ作り、最後にPodをわざと削除して、セルフヒーリングが実際に動くことを削除前後の一覧の比較で証明します。