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 段階でノードを選びます。

  1. フィルタリング (filtering)。この Pod を受け取れないノードをふるい落とします。リソースが足りないノード、nodeSelector の条件に合わないノード、耐えられない taint が付いたノードを除外します。
  2. スコアリング (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.27

nodeSelector は 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値が大きい / 小さい (整数)

NotInDoesNotExist が nodeSelector にはなかった否定条件です。これにより「このラベルがないノードにだけ」のような配置が可能になります。

nodeAffinity の例 #

次は disktypessd または 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 はその逆で「そうしたノードを 避けよ」という意味です。topologyKeykubernetes.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.27

labelSelector で「どの Pod を基準にするか」を選び、topologyKey で「どの単位で離すか」を定めます。ここでは同じ app=web の Pod どうしが同じノードを避けるので、ノードが 3 台未満なら余ったレプリカは Pending にとどまります。required の代わりに preferred を使えば、ノードが足りなくても一旦は同じノードに重ねて起動します。

preferred に変える場合も、同じ構造に重みを加えるだけです。

      affinity:
        podAntiAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
            - weight: 100
              podAffinityTerm:
                labelSelector:
                  matchLabels:
                    app: web
                topologyKey: kubernetes.io/hostname

preferred では labelSelectortopologyKeypodAffinityTerm の下へ 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-labels

describe 出力の 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/hostnamelabelSelector を自分の 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 がどのように追い出されるか を直接マニフェストで扱い、スケジューリングの残り半分を埋めます。

X