Certified Kubernetes Administrator (CKA) #13 Scheduling 1: nodeSelector、nodeAffinity、podAffinity/antiAffinity
#12 ConfigMap と Secret の深掘り まででワークロードとその設定を扱ったなら、この記事からは そのワークロードをどのノードに置くか を制御します。デフォルトでは kube-scheduler が適当なノードを自分で選びます。しかし運用では「GPU が付いたノードにだけ」「同じアベイラビリティゾーンのキャッシュの隣に」「レプリカは互いに別のノードへ散らして」といった要求が絶えず生まれます。こうした配置の意図をマニフェストで表現するのがスケジューリングです。
この #13 では nodeSelector、nodeAffinity、podAffinity/podAntiAffinity の 4 つを扱います。すべて「Pod がどのノードを好むか」を表現する道具です。次の #14 で扱う taints/tolerations が逆に「ノードがどの Pod を押し出すか」を扱うので、2 つの記事を合わせて見るとスケジューリングの両面が完成します。
スケジューラは何をするのか #
まず大きな絵をつかみます。Pod を作ると、そのマニフェストの nodeName フィールドは空のままです。kube-scheduler はノードが指定されていない Pod を見つけると、2 段階でノードを選びます。
- フィルタリング (filtering)。この Pod を受け取れないノードをふるい落とします。リソースが足りないノード、nodeSelector の条件に合わないノード、耐えられない taint が付いたノードを除外します。
- スコアリング (scoring)。残った候補ノードに点数を付け、最も高いノードを選びます。preferred ルールの重み、リソースの余裕、イメージキャッシュの有無などが点数に反映されます。
スケジューラがノードを決めると、その結果を Pod の nodeName に書き込みます (これをバインディングと呼びます)。そのノードの kubelet が自分宛にバインドされた Pod を見てコンテナを起動します。この記事で扱う道具はすべて、このフィルタリングとスコアリングの段階に介入する 方法です。
配置の意図を表現する道具を強さの順に整理すると、次のようになります。
| 道具 | 基準 | 強制力 |
|---|---|---|
| nodeSelector | ノードラベル | 強制 (合わなければ Pending) |
| nodeAffinity (required) | ノードラベル | 強制 (合わなければ Pending) |
| nodeAffinity (preferred) | ノードラベル | 選好 (点数のみ、合わなくても配置) |
| podAffinity / podAntiAffinity | 他の Pod の位置 | required と preferred のどちらも可能 |
nodeSelector: ラベルの単純マッチ #
最も単純な道具です。Pod の spec.nodeSelector にラベルのキーと値を書くと、そのラベルを すべて 持つノードにだけ配置されます。条件を満たすノードが 1 つもなければ、Pod は Pending にとどまります。
まずノードにラベルを付けます。
# ノードにラベルを付与
k label node node01 disktype=ssd
# ラベルの確認
k get nodes --show-labels
k get nodes -l disktype=ssd次に Pod でそのラベルを指定します。
apiVersion: v1
kind: Pod
metadata:
name: web
spec:
nodeSelector:
disktype: ssd
containers:
- name: web
image: nginx:1.27nodeSelector は AND マッチのみ です。複数のキーを書くとそのすべてを持つノードでなければならず、「2 つのうちどちらか」や「この値ではない」といった条件は表現できません。そのような表現力が必要なら nodeAffinity へ移ります。
nodeAffinity: required と preferred #
nodeAffinity は nodeSelector の拡張版です。同じ「ノードラベルで選ぶ」という目的ですが、演算子と強制力を細かく表現できます。2 種類があります。
- requiredDuringSchedulingIgnoredDuringExecution。強制ルールです。条件を満たすノードがなければ Pod は Pending にとどまります。nodeSelector と同じ強制力でありながら演算子を使えます。
- preferredDuringSchedulingIgnoredDuringExecution。選好ルールです。条件を満たすノードに加点しますが、そうしたノードがなくても別のノードにそのまま配置します。
名前が長い理由は、2 つの部分に分けて読めばわかります。前の DuringScheduling はスケジューリング時点でこのルールを見るという意味で、後ろの IgnoredDuringExecution はすでに起動している Pod は後でノードラベルが変わっても追い出さないという意味です。
演算子 #
nodeAffinity の matchExpressions で使う演算子です。
| 演算子 | 意味 |
|---|---|
In | 値がリストの中にある |
NotIn | 値がリストの中にない |
Exists | キーが存在する (値は無関係) |
DoesNotExist | キーが存在しない |
Gt / Lt | 値が大きい / 小さい (整数) |
NotIn と DoesNotExist が nodeSelector にはなかった否定条件です。これにより「このラベルがないノードにだけ」のような配置が可能になります。
nodeAffinity の例 #
次は disktype が ssd または nvme のノードに 必ず 配置し、そのうち zone=ap-northeast-1a のノードを 選好する 例です。
apiVersion: v1
kind: Pod
metadata:
name: db
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: disktype
operator: In
values:
- ssd
- nvme
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 50
preference:
matchExpressions:
- key: zone
operator: In
values:
- ap-northeast-1a
containers:
- name: db
image: postgres:16構造で混同しやすい 2 か所を押さえます。
- required は
nodeSelectorTermsのリスト です。リストの項目どうしは OR で結ばれます。1 つの項目の中の複数のmatchExpressionsは AND で結ばれます。 - preferred は重みを持つリスト です。各項目に
weight(1〜100) が付き、満たすノードはその重み分だけ点数を多く受け取ります。複数の preferred ルールを満たすと重みが合算されます。
podAffinity と podAntiAffinity: 他の Pod を基準に #
nodeAffinity が ノードラベル を基準にするなら、podAffinity と podAntiAffinity は すでに起動している他の Pod の位置 を基準にします。
- podAffinity。特定のラベルを持つ Pod の 近く に置きます。たとえばアプリ Pod をキャッシュ Pod と同じノードに付けてネットワーク遅延を減らします。
- podAntiAffinity。特定のラベルを持つ Pod から 離して 置きます。たとえば同じ Deployment のレプリカを互いに別のノードへ散らし、1 つのノード障害が全体を切らないようにします。
topologyKey が「同じ場所」を定義する #
podAffinity の核心は topologyKey です。「同じノード」なのか「同じアベイラビリティゾーン」なのかという「近さの単位」をノードラベルのキーで定めます。
| topologyKey | 「同じ場所」の意味 |
|---|---|
kubernetes.io/hostname | 同じノード |
topology.kubernetes.io/zone | 同じアベイラビリティゾーン |
topology.kubernetes.io/region | 同じリージョン |
動作はこう読みます。podAffinity は「ラベルが合う Pod が起動しているノードと 同じ topologyKey 値 を持つノードに私を配置せよ」という意味で、podAntiAffinity はその逆で「そうしたノードを 避けよ」という意味です。topologyKey を kubernetes.io/hostname にすると「同じノード/別のノード」の単位になり、zone にすると「同じゾーン/別のゾーン」の単位になります。
podAntiAffinity の例: レプリカをノードごとに 1 つずつ #
次は app=web ラベルを持つ Pod どうしを 互いに別のノードに 置く例です。Deployment の Pod テンプレートに入れると、レプリカが 1 つのノードに集まりません。
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
spec:
replicas: 3
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchLabels:
app: web
topologyKey: kubernetes.io/hostname
containers:
- name: web
image: nginx:1.27labelSelector で「どの Pod を基準にするか」を選び、topologyKey で「どの単位で離すか」を定めます。ここでは同じ app=web の Pod どうしが同じノードを避けるので、ノードが 3 台未満なら余ったレプリカは Pending にとどまります。required の代わりに preferred を使えば、ノードが足りなくても一旦は同じノードに重ねて起動します。
preferred に変える場合も、同じ構造に重みを加えるだけです。
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchLabels:
app: web
topologyKey: kubernetes.io/hostnamepreferred では labelSelector と topologyKey が podAffinityTerm の下へ 1 段入り、その上に weight が付くという点だけ覚えておけば十分です。
手動配置: nodeName でスケジューラを迂回 #
ここまでの道具はすべてスケジューラにヒントを与えるだけで、最終決定はスケジューラが行います。一方、Pod に spec.nodeName を直接書くと、スケジューラを完全に飛ばして そのノードへ直ちにバインドされます。
apiVersion: v1
kind: Pod
metadata:
name: pinned
spec:
nodeName: node01
containers:
- name: app
image: nginx:1.27この方式はフィルタリングもスコアリングも通らないので、そのノードにリソースがなくても taint があっても強制的にバインドされます。結果としてノードが受け取れなければ Pod が永遠に起動しない危険があり、実務ではほとんど使いません。ただし スケジューラ自体が落ちた状況で control plane コンポーネントを起動しなければならないとき のような例外では有用です。static Pod がまさにこの方式で、kubelet がマニフェストディレクトリの Pod をスケジューラなしで直接起動します。
デバッグ: なぜ Pending にとどまるのか #
affinity ルールはきつく掛けすぎると Pod が Pending に閉じ込められやすくなります。原因は describe ですぐに見えます。
# Pending の原因を確認
k describe pod web
# ノードラベルが条件と合うか再確認
k get nodes --show-labelsdescribe 出力の Events にスケジューラのメッセージが出ます。nodeAffinity が合わなければ didn't match Pod's node affinity/selector、podAntiAffinity でノードが足りなければ didn't match pod anti-affinity rules のような文言が出ます。この文言を読むだけで、どのルールが足を引っ張っているかわかります。
試験ポイント #
CKA 試験でスケジューリングは Workloads and Scheduling ドメイン (15%) の一軸です。この記事の範囲でよく出る作業を整理します。
- ノードラベルの付与。
k label node <ノード> <キー>=<値>を手に覚えること。問題が要求するラベルを先に付けないと nodeSelector/nodeAffinity が動きません。 - nodeSelector vs nodeAffinity の区別。「このラベルがあるノード」は nodeSelector で十分で、「2 つのうちどちらか」または「このラベルがないノード」は nodeAffinity の
In/NotIn/Existsが必要です。 - required と preferred の強制力の違い。問題が「必ず」と言えば required、「可能なら」と言えば preferred です。preferred には
weightが必須です。 - podAntiAffinity でレプリカを散らす。
topologyKey: kubernetes.io/hostnameとlabelSelectorを自分の Pod ラベルで掛けるパターンを覚えておくこと。レプリカ数がノード数より多く required なら一部が Pending にとどまる点を覚えておきます。 - YAML 構造の落とし穴。required nodeAffinity は
nodeSelectorTerms、preferred はweight+preference、preferred podAffinity はweight+podAffinityTermです。この 3 つの形のインデントの違いが最もよくある失敗です。 - Pending デバッグ。
k describe podの Events 1 行でどのルールが原因かを即座に判断します。
試験では affinity YAML を手で一から書く時間はもったいないです。kubectl create deployment ... $do で骨組みを作ってから affinity ブロックだけ差し込み、公式ドキュメントの Assigning Pods to Nodes の例をコピーして値だけ変えるのが最も速い方法です。
まとめ #
この記事で押さえたこと:
- スケジューラはフィルタリングとスコアリングの 2 段階 でノードを選んだ後、Pod の
nodeNameに結果をバインドします。 - nodeSelector。ノードラベルの AND マッチ。最も単純で強制。表現力が足りなければ nodeAffinity へ移ります。
- nodeAffinity。required (強制) と preferred (選好+重み)。
In/NotIn/Existsなどの演算子で否定条件まで表現します。 - podAffinity/podAntiAffinity。他の Pod の位置を基準に、
topologyKey単位で同じ場所に付けたり別の場所へ散らしたりします。 - nodeName。スケジューラを迂回する手動配置。static Pod の動作方式ですが一般のワークロードには勧めません。
- デバッグ。Pending なら
k describe podの Events でどのルールが塞いでいるかを確認します。
次へ — Scheduling 2 #
この記事の道具はすべて「Pod がどのノードを好むか」を表現しました。次の #14 Scheduling 2: Taints/tolerations、Priority/PriorityClass、preemption では逆方向を扱います。taints/tolerations で ノードがどの Pod を押し出すか、PriorityClass で 資源が足りないときどの Pod が先に席を取るか、preemption で 低い優先度の Pod がどのように追い出されるか を直接マニフェストで扱い、スケジューリングの残り半分を埋めます。