新卒インフラエンジニア向け「Kubernetes 実践教科書 ② CKA クラスタ構築・運用編」(全16回)の第7回です。前回はクラスタ全体を v1.36 へアップグレードし、「止めずに育てる」運用の第一歩を踏み出しました。本回のテーマはスケジューリングです。ここまで Pod は「クラスタのどこかのノードに載る」で済ませてきましたが、本番では「この Pod はこのノードに載せたい」「このノードには載せたくない」を意図どおりに制御する場面が必ず出てきます。本回では、スケジューラを操る 4 つの道具(nodeSelector / nodeAffinity / podAffinity・podAntiAffinity / taint・toleration)と PriorityClass を使い分けます。あわせて、第1巻で学んだ D2(マルチコンテナ Pod・rollout/undo)を CKA の実技形式で手早く再確認します。いずれも CKA のドメイン D2「Workloads and Scheduling」で頻出です。
本回の操作はすべて作業端末 k8s-ops(192.168.1.122)から kubectl で行います。プロンプトの $ は一般ユーザー developer を表します。演習で使うノードは Workload Node の k8s-wl-01・k8s-wl-02 が中心です。
本回で作成する YAML マニフェストは、k8s-ops の developer のホームディレクトリ配下に専用の作業ディレクトリを作ってまとめておきます。作成するファイルの所有者は developer で、sudo は使いません(クラスタ操作は developer の kubeconfig で完結します)。最初にディレクトリを作って移動しておきます。以降の「〇〇.yaml として保存します」は、すべてこのディレクトリ内に作る前提です。
実行コマンド(k8s-ops・developer):
$ mkdir -p ~/scheduling && cd ~/scheduling
目次
今ここマップ(全 16 回中の現在地)
本シリーズは 6 部構成です。現在地は第2部「ワークロード管理」の第7回です。
- 第1部 クラスタ構築(第1〜5回)
- 第2部 ワークロード管理(第6〜8回)← 今ここ
- 第3部 ネットワーク(第9〜11回)
- 第4部 ストレージ(第12回)
- 第5部 監視・運用(第13〜14回)
- 第6部 トラブルシュート(第15〜16回)
- Kubernetes v1.36.2(第6回でアップグレード済み)
- kubectl v1.35.6(作業端末・apiserver とは ±1 マイナー以内で動作)
- AlmaLinux 10.2
- CKA 試験は本稿執筆時点で v1.35 ですが、本回で扱うスケジューリングの API(
affinity・tolerations・PriorityClass)はマイナー番号が変わっても同じ - 確認日 2026-07-12
この回のゴール
本回を終えると、次のことができるようになります。到達できたかは記事末の「やってみよう」と「理解度チェック」で確認します。
nodeSelectorとnodeAffinityの違い・使い分けを説明できるpodAffinity/podAntiAffinityで Pod の同居・分散を制御できるtaint(ノード側の拒否)とtoleration(Pod 側の許容)の対応関係を理解するPriorityClassで重要な Pod を優先スケジューリングできる- 第1巻で学んだ D2(マルチコンテナ Pod・rollout/undo)を CKA 形式で再確認できる
スケジューラの仕事(おさらい)
道具を使う前に、誰がどう配置を決めているかを押さえます。新しい Pod が作られると、kube-scheduler が「まだノードの決まっていない Pod(Pending)」を見つけ、2 段階で載せ先を選びます。
- フィルタリング:そもそも載せられるノードを絞り込む(リソース不足・taint・nodeSelector 不一致などを除外)。
- スコアリング:残った候補に点数を付け、最も高いノードに載せる。
私たちがこの判断に介入する手段は、大きく2 つの側面に分かれます。ノード側に「印」を付ける手段(label と taint)と、Pod 側に「希望」を書く手段(nodeSelector・affinity・toleration)です。この対応関係を最初に地図として持っておくと、以降の道具が整理して理解できます。

nodeSelector と nodeAffinity
まず「この Pod をこのノードに載せたい」を指定する手段です。もっとも単純なのが nodeSelector。ノードにラベルを付け、Pod にそのラベルとの完全一致を書きます。例として wl-01 に「SSD を積んだノード」という意味の disktype=ssd ラベルを付けます。
実行コマンド(k8s-ops・developer):
$ kubectl label node k8s-wl-01 disktype=ssd
実行結果:
node/k8s-wl-01 labeled
次に、このラベルを持つノードにだけ載る Pod を作ります。マニフェストを nodeselector-pod.yaml として保存します。
apiVersion: v1
kind: Pod
metadata:
name: nginx-ssd
spec:
nodeSelector:
disktype: ssd
containers:
- name: nginx
image: nginx:1.27-alpine
実行コマンド(k8s-ops・developer):
$ kubectl apply -f nodeselector-pod.yaml
$ kubectl get pod nginx-ssd -o wide
実行結果:
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
nginx-ssd 1/1 Running 0 12s 10.244.215.216 k8s-wl-01 <none> <none>
ラベルを付けた wl-01 に載りました。nodeSelector は完全一致だけで、範囲指定や「A または B」のような柔軟な条件は書けません。そこで登場するのが nodeAffinity です。演算子(In / NotIn / Exists など)で条件を柔軟に書け、さらに「必須」と「できれば」を区別できます。次のマニフェストを nodeaffinity-pod.yaml として保存します。
apiVersion: v1
kind: Pod
metadata:
name: nginx-affinity
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: disktype
operator: In
values:
- ssd
- nvme
containers:
- name: nginx
image: nginx:1.27-alpine
requiredDuringSchedulingIgnoredDuringExecution は「スケジュール時に必須(満たすノードが無ければ Pending)」を意味します。長い名前ですが、前半 requiredDuringScheduling=載せるときは必須、後半 IgnoredDuringExecution=いったん載った後にノードのラベルが変わっても追い出さない、と読みます。「できれば」で済ませたいときは preferredDuringSchedulingIgnoredDuringExecution を使い、条件に weight(重み)を付けます。上の例は「disktype が ssd か nvme のノード」という条件で、wl-01 に載ります。
実行コマンド(k8s-ops・developer):
$ kubectl apply -f nodeaffinity-pod.yaml
$ kubectl get pod nginx-affinity -o wide
実行結果:
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
nginx-affinity 1/1 Running 0 10s 10.244.215.217 k8s-wl-01 <none> <none>
確認できたら、この 2 つの Pod と、付けたラベルを片付けておきます。ラベルの削除は、キー名の末尾に -(マイナス)を付けて行います。
$ kubectl delete pod nginx-ssd nginx-affinity
$ kubectl label node k8s-wl-01 disktype-
podAffinity と podAntiAffinity
ここまでは「Pod とノード」の関係でした。次は「Pod と Pod」の関係です。podAffinity は「あの Pod と同じノードに載せたい」(例:キャッシュを使う側の近くに置く)、podAntiAffinity は「同じ役割の Pod を別のノードに散らしたい」(可用性を上げる)ときに使います。どちらも「どの単位で同居・分散を判断するか」を topologyKey で指定します。ノード単位で散らすなら kubernetes.io/hostname です。
可用性の実感が大きい podAntiAffinity を試します。同じラベルを持つ Pod を同じノードに載せないルールにして、3 レプリカの Deployment を作ります。マニフェストを antiaffinity-deploy.yaml として保存します。
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: nginx
image: nginx:1.27-alpine
実行コマンド(k8s-ops・developer):
$ kubectl apply -f antiaffinity-deploy.yaml
$ kubectl get pods -l app=web -o wide
実行結果:
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
web-55f5476fbd-qdxsj 1/1 Running 0 8s 10.244.215.218 k8s-wl-01 <none> <none>
web-55f5476fbd-lv8r6 1/1 Running 0 8s 10.244.119.147 k8s-wl-02 <none> <none>
web-55f5476fbd-d9zb8 0/1 Pending 0 8s <none> <none> <none> <none>
2 つは wl-01・wl-02 に 1 つずつ分散し、3 つ目は Pending になりました。「同じ app: web の Pod がいるノードには載せない」という必須ルールのため、Workload Node が 2 つしかない今、3 つ目の載せ先が無いのです。Pending の理由を describe の Events で確認します。
実行コマンド(k8s-ops・developer):
$ kubectl describe pod -l app=web | grep -A3 Events:
実行結果(Pending の Pod の Events 抜粋):
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Warning FailedScheduling 7s default-scheduler 0/5 nodes are available: 2 node(s) didn't match pod anti-affinity rules, 3 node(s) had untolerated taint(s). no new claims to deallocate, preemption: 0/5 nodes are available: 2 No preemption victims found for incoming pod, 3 Preemption is not helpful for scheduling.
Events が理由をそのまま教えてくれます。2 node(s) didn't match pod anti-affinity rules(=すでに web が載っている wl-01・wl-02)と 3 node(s) had untolerated taint(s)(=Control Plane の 3 台)で、載せ先がありません。継承している fanclub-api の backend は 1 レプリカです。複数レプリカにしたアプリへ同じ手法で podAntiAffinity を付ければ、レプリカが別ノードへ分散して可用性が上がります(本回では chart は変更しません。なお fanclub の backend を増やせる数には上限があり、これは第8回の ResourceQuota で扱います)。確認できたら片付けます。
$ kubectl delete deploy web
taint と toleration
ここまではノードに「印(label)」を付けて引き寄せる手段でした。逆に、ノードに「立ち入り制限」を付けてPod を寄せ付けないのが taint です。そして、その制限を持つノードにあえて載ってよいという許可証を Pod 側に付けるのが toleration です。taint と toleration は必ずペアで働きます。まず wl-01 に「バッチ専用」という意味の taint を付けます。effect は NoSchedule(新規は載せない)・PreferNoSchedule(できれば載せない)・NoExecute(既存も追い出す)の 3 種です。
実行コマンド(k8s-ops・developer):
$ kubectl taint nodes k8s-wl-01 dedicated=batch:NoSchedule
実行結果:
node/k8s-wl-01 tainted
この状態で、toleration を持たない普通の Pod を 3 つ作ってみます。wl-01 は taint で拒否、Control Plane も既定 taint で拒否のため、すべて wl-02 に載るはずです。
実行コマンド(k8s-ops・developer):
$ kubectl create deployment plain --image=nginx:1.27-alpine --replicas=3
$ kubectl get pods -l app=plain -o wide
実行結果:
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
plain-6c6f79cf84-kxcvr 1/1 Running 0 8s 10.244.119.149 k8s-wl-02 <none> <none>
plain-6c6f79cf84-lk4wp 1/1 Running 0 8s 10.244.119.150 k8s-wl-02 <none> <none>
plain-6c6f79cf84-x99gs 1/1 Running 0 8s 10.244.119.148 k8s-wl-02 <none> <none>
3 つとも wl-02 に載りました。wl-01 は taint で避けられています。次に、この taint を許容する toleration を持ち、かつ nodeSelector で wl-01 を指名した Pod を作ります。toleration があれば taint のあるノードにも載れることを確かめます。マニフェストを toleration-pod.yaml として保存します。
apiVersion: v1
kind: Pod
metadata:
name: batch-job
spec:
nodeSelector:
kubernetes.io/hostname: k8s-wl-01
tolerations:
- key: dedicated
operator: Equal
value: batch
effect: NoSchedule
containers:
- name: nginx
image: nginx:1.27-alpine
実行コマンド(k8s-ops・developer):
$ kubectl apply -f toleration-pod.yaml
$ kubectl get pod batch-job -o wide
実行結果:
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
batch-job 1/1 Running 0 6s 10.244.215.219 k8s-wl-01 <none> <none>
toleration を持つ Pod は、taint のある wl-01 に載りました。もし toleration を外して wl-01 を指名すると、taint に阻まれて Pending になります(toleration が「許可証」として効いていることの裏返しです)。この taint と toleration の仕組みは、身近なところで既に使われています。Control Plane に通常の Pod が載らないのは、第2回の kubeadm init が各 Control Plane Node に node-role.kubernetes.io/control-plane:NoSchedule という taint を付けているからです。前回のアップグレードで Pending Pod の Events に出た untolerated taint(s) も、これを指していました。確認できたら演習用リソースを片付け、wl-01 の taint も外します。
実行コマンド(k8s-ops・developer):
$ kubectl delete deployment plain
$ kubectl delete pod batch-job
$ kubectl taint nodes k8s-wl-01 dedicated=batch:NoSchedule-
taint の削除は、付けたときの指定の末尾に -(マイナス)を付けます(ラベルの削除と同じ流儀です)。これで wl-01 は通常どおりスケジュール可能に戻ります。
PriorityClass(優先スケジューリング)
ノードのリソースが逼迫したとき、「重要な Pod を優先して載せたい」ことがあります。これを実現するのが PriorityClass です。優先度の値(value)を定義し、Pod に priorityClassName で紐付けます。値が大きいほど優先され、リソースが足りないときはスケジューラが低優先の Pod を追い出して(preemption)高優先の Pod を載せます。まず PriorityClass を定義します。マニフェストを priorityclass.yaml として保存します。
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: high-priority
value: 1000000
globalDefault: false
description: "重要なワークロード用の高優先度クラス"
globalDefault: false は「このクラスをクラスタ全体の既定にはしない」という意味です(true にすると priorityClassName を指定しない全 Pod の既定優先度になるため、通常は false)。適用して、Pod から参照します。
実行コマンド(k8s-ops・developer):
$ kubectl apply -f priorityclass.yaml
$ kubectl get priorityclass high-priority
実行結果:
NAME VALUE GLOBAL-DEFAULT AGE PREEMPTIONPOLICY
high-priority 1000000 false 5s PreemptLowerPriority
Pod 側は spec.priorityClassName にクラス名を書くだけです。たとえば重要なアプリの Pod なら、次のように 1 行を加えます。
apiVersion: v1
kind: Pod
metadata:
name: important-app
spec:
priorityClassName: high-priority
containers:
- name: nginx
image: nginx:1.27-alpine
リソースが十分なうちは優先度の差は表に出ませんが、逼迫時には低優先の Pod が preempt(追い出し)され、この Pod が優先して載ります。CKA では「重要な Pod に PriorityClass を定義して付ける」までが問われることが多く、preemption の作り込みまでは求められません。確認できたら、定義した PriorityClass を片付けます(上の important-app は例示で、適用していなければ削除は不要です)。
$ kubectl delete priorityclass high-priority
D2 再確認ドリル(第1巻の要点を CKA 形式で)
ここからは、第1巻(CKAD)で習得済みの D2 の要点を、CKA の実技形式で手早く再確認します。教え直しではなく「手が覚えているか」の確認なので、分量は控えめにします。
ドリル1:マルチコンテナ Pod
1 つの Pod に複数のコンテナを入れ、初期化用の initContainer と本体を組み合わせます。initContainer は本体より先に、完了するまで実行されます。マニフェストを multi-pod.yaml として保存します。
apiVersion: v1
kind: Pod
metadata:
name: multi
spec:
initContainers:
- name: init-wait
image: busybox:1.37
command: ['sh', '-c', 'echo init done']
containers:
- name: app
image: nginx:1.27-alpine
実行コマンド(k8s-ops・developer):
$ kubectl apply -f multi-pod.yaml
$ kubectl get pod multi
実行結果:
NAME READY STATUS RESTARTS AGE
multi 1/1 Running 0 18s
initContainer が完了してから本体の nginx が起動し、1/1 Running になりました。「1 つの Pod = 複数コンテナで、ネットワークとボリュームを共有する」という第1巻の要点はここでも同じです。確認できたら kubectl delete pod multi で片付けます。
ドリル2:Deployment の rollout と undo
ロールアウトとロールバックを、使い捨ての Deployment で確認します。CKA で頻出の「更新 → 状態確認 → 差し戻し」を体で戻します。まず default 名前空間に nginx の Deployment を 2 レプリカで作ります。
実行コマンド(k8s-ops・developer):
$ kubectl create deployment rollout-demo --image=nginx:1.27-alpine --replicas=2
次に、イメージをわざと存在しないタグに更新して不具合を起こし、rollout の状態を見ます。コンテナ名は Deployment 名から自動で nginx になっています。
実行コマンド(k8s-ops・developer):
$ kubectl set image deployment/rollout-demo nginx=nginx:9.99-does-not-exist
$ kubectl rollout status deployment/rollout-demo --timeout=20s
実行結果:
deployment.apps/rollout-demo image updated
Waiting for deployment "rollout-demo" rollout to finish: 1 out of 2 new replicas have been updated...
error: timed out waiting for the condition
rollout は 20 秒待っても完了せず、タイムアウトしました。新しい Pod が存在しないタグを取得できず ImagePullBackOff で止まっているためです。既定の RollingUpdate は、レプリカ 2 だと maxUnavailable が 0 に丸められ、新しい Pod が正常になるまで古い Pod を残します。つまり更新に失敗しても稼働は続きます。Pod の状態を見てみます。
実行コマンド(k8s-ops・developer):
$ kubectl get pods -l app=rollout-demo -o wide
実行結果:
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
rollout-demo-6b9fbcdf85-vq7ss 1/1 Running 0 21s 10.244.119.152 k8s-wl-02 <none> <none>
rollout-demo-6b9fbcdf85-x4lz8 1/1 Running 0 21s 10.244.215.220 k8s-wl-01 <none> <none>
rollout-demo-7bcdf8bfb4-x4ll4 0/1 ImagePullBackOff 0 20s 10.244.119.153 k8s-wl-02 <none> <none>
古い 2 つは Running、新しい 1 つが ImagePullBackOff です。この「更新が刺さった」状態を、直前の世代へ戻して復旧します。
実行コマンド(k8s-ops・developer):
$ kubectl rollout undo deployment/rollout-demo
$ kubectl rollout history deployment/rollout-demo
実行結果:
deployment.apps/rollout-demo rolled back
deployment.apps/rollout-demo
REVISION CHANGE-CAUSE
2 <none>
3 <none>
rollout undo で正常な世代へ戻り、ImagePullBackOff だった Pod は片付きました。rollout history の REVISION は、更新と戻しで番号が進みます。障害時に「まず直前へ戻す」という初動が、コマンド 1 つで取れることを確認しました。実運用でも、fanclub-backend のようなアプリを同じ手順で更新・切り戻しできます。確認できたら Deployment を削除します。
$ kubectl delete deployment rollout-demo
ドリル3:imperative での時短
CKA は時間との勝負です。YAML を一から書かず、--dry-run=client -o yaml で雛形を生成して手直しするのが定石です。ConfigMap を例に、雛形だけを出力します(--dry-run=client なので実際には作りません)。
実行コマンド(k8s-ops・developer):
$ kubectl create configmap demo --from-literal=color=blue --dry-run=client -o yaml
実行結果:
apiVersion: v1
data:
color: blue
kind: ConfigMap
metadata:
name: demo
この出力を > demo.yaml でファイルに保存して手直しすれば、一から書くより速く正確です。同様に kubectl set image・kubectl scale・kubectl expose なども、YAML を書かずに素早く操作できる時短コマンドです。
やってみよう
nodeSelector でノードを指名する
wl-01 に disktype=ssd ラベルを付け、nodeSelector: {disktype: ssd} を持つ Pod を作り、kubectl get pod -o wide で wl-01 に載ることを確認してください。確認後はラベルと Pod を片付けます。
podAntiAffinity で分散させる
本文の web Deployment(replicas=3・podAntiAffinity required・topologyKey: kubernetes.io/hostname)を作り、2 つが wl-01・wl-02 に分散して 3 つ目が Pending になることを -o wide で確認してください。describe の Events に didn't match pod anti-affinity rules が出ることも読み取ってください。確認後は delete します。
taint と toleration
wl-01 に dedicated=batch:NoSchedule の taint を付け、toleration を持たない Pod が wl-01 を避けること、toleration を付けた Pod は wl-01 に載れることを確認してください。確認後は taint を - 付きで削除します。
rollout と undo(D2 再確認)
使い捨ての Deployment(kubectl create deployment rollout-demo --image=nginx:1.27-alpine --replicas=2)を作り、kubectl set image で存在しないタグに更新 → kubectl rollout status がタイムアウトし新 Pod が ImagePullBackOff になること → kubectl rollout undo で復旧 → kubectl rollout history で世代を確認してください。確認後は Deployment を削除します。
まとめ
本回では、スケジューリングを意図どおりに操る道具を一通り扱いました。ノードに引き寄せる nodeSelector / nodeAffinity、Pod 同士の同居・分散を決める podAffinity / podAntiAffinity、ノードから遠ざける taint とその許可証 toleration、そして優先度を決める PriorityClass。あわせて、第1巻で学んだ D2(マルチコンテナ Pod・rollout/undo・imperative 時短)を CKA の実技形式で再確認しました。「どの Pod をどのノードに、どう載せるか」を自分で決められるようになったことで、可用性やリソース配分を設計できる土台ができました。
理解度チェック(○×形式・全 9 問)
次の各文が正しいか(○)誤りか(×)を判断してください。下の「解答と解説」を開くと答え合わせができます。
nodeSelectorはラベルの完全一致のみで、範囲や「A または B」のような条件は書けない。podAntiAffinityは同じ役割の Pod を同じノードに集める仕組みである。taintはノード側に付け、tolerationは Pod 側に付ける。requiredDuringSchedulingIgnoredDuringExecutionの条件を満たすノードが無い場合、Pod は Pending になる。- Control Plane に通常の Pod が載らないのは、
node-role.kubernetes.io/control-plane:NoScheduleの taint があるためである。 - taint の削除は、付けたときの指定の末尾に
-を付けて行う。 PriorityClassのglobalDefault: trueは、priorityClassName を指定しない Pod の既定優先度になる。- 1 つの Pod に複数のコンテナを入れることはできない。
kubectl rollout undoで直前の世代へ戻せる。
解答と解説
1=○(完全一致のみ。柔軟な条件は nodeAffinity)/2=×(散らす仕組み。集めるのは podAffinity)/3=○(taint=ノード側・toleration=Pod 側)/4=○(required は満たせなければ Pending)/5=○(既定 taint による)/6=○(末尾に -)/7=○(globalDefault:true は未指定 Pod の既定になる)/8=×(マルチコンテナ Pod は可能)/9=○(直前世代へ戻せる)
次回予告
次回・第8回では、マルチテナント統制に踏み込みます。RBAC(誰が何を操作できるか)を管理者視点で正式に実装し、ResourceQuota / LimitRange(名前空間ごとの資源上限)と HPA(負荷に応じた自動スケール)を組み合わせて、複数チームが 1 つのクラスタを安全に共用する仕組みを作ります。
