PersistentVolume と Pod の NodeAffinity はどっちが優先されるの

この記事はなむゆの個人ブログにもマルチポストしてあります。
https://namyusql.hatenablog.com/entry/2022-02-14-kubernetes-nodeaffinity

はじめに結論

Q PersistentVolume と Pod、どちらでも NodeAffinity が設定できますが、これらで互いに矛盾するような設定をすると、どのように Pod がスケジューリングされるのでしょうか?

A node affinity conflict のエラーであるとしてスケジューリングされなくなります。

説明

Kubernetes には NodeAffinity という概念があります。これは、その Pod がどの Node に配置されるか(スケジューリングされるか)をコントロールするために使われるものです。
Node 毎に label を設定し、その label が付けられた Node を対象とした NodeAffinity を Pod に設定してやることで、その Pod が展開される先の Node を指定したり、優先順位付けを行ってやることができます。
この NodeAffinity は、アプリケーションの本体である Pod に対して直接設定してやることもできますが、他にも設定できるリソースがあります。
PersistentVolume がその一つです。
PersistentVolume は Pod で作成されたデータを永続化するための概念で、Node として動作しているマシン内や外部のファイルシステム等にそのデータを保持する場所を定義し、Pod 内の特定のディレクトリと繋ぎ合わせます。
この中で特に、Node 内部にデータを保存する場合、どの Node に対してその Pod を展開するかが重要になってきます。
なぜなら、複数の Node がある環境の場合、Pod が配置された Node のマシン上にデータを保存すると言っても Pod がどの Node に配置されるか分からないため、Pod から作成されたデータがどこの Node に保存されるかも分からなくなってしまうためです。
そのため、PersistentVolume というリソースでも NodeAffinity を指定することができ、この PersistentVolume を使用することが指定されている Pod は使用している PersistentVolume の NodeAffinity に従って Node に配置されます。
ということで、NodeAffinity は Pod そのものと、PersistentVolume の両方で設定することができます。
では、これらで互いに矛盾するような NodeAffinity を設定すると、どちらが優先されるのかというのが今回の話の趣旨になります。

検証方法

まず最初に、複数の Node がある Kubernetes クラスターを用意します。
スクラッチで用意してもいいのですが、自分の場合今回は Azure に頼って AKS で簡単に二つ Node がある環境を用意しています。

次に、作成した二つの Node にそれぞれ別の label を設定します。

kubectl label node <ひとつめのNodeの名前> nodeName=A
kubectl label node <ふたつめのNodeの名前> nodeName=B

続いて、各種マニフェストファイルを用意します。
0_pv.yaml

apiVersion: v1
kind: PersistentVolume
metadata:
  name: local-pv
spec:
  capacity:
    storage: 1Gi
  volumeMode: Filesystem
  accessModes:
    - ReadWriteMany
  persistentVolumeReclaimPolicy: Retain
  storageClassName: local
  local:
    path: /
  nodeAffinity:
    required:
      nodeSelectorTerms:
        - matchExpressions:
            - key: nodeName
              operator: In
              values:
                - A

1_pvc.yaml

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: local-pvc
spec:
  accessModes:
    - ReadWriteMany
  volumeMode: Filesystem
  resources:
    requests:
      storage: 1Gi
  storageClassName: local

2_deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  creationTimestamp: null
  labels:
    app: prefer-b-deployment
  name: prefer-b-deployment
spec:
  replicas: 10
  selector:
    matchLabels:
      app: prefer-b-deployment
  strategy: {}
  template:
    metadata:
      creationTimestamp: null
      labels:
        app: prefer-b-deployment
    spec:
      containers:
        - command:
            - /bin/sh
            - -c
            - sleep 1000
          image: busybox
          name: busybox
          resources:
            requests:
              cpu: 50m
              memory: 50Mi
            limits:
              cpu: 50m
              memory: 50Mi
          volumeMounts:
            - mountPath: "/path"
              name: mypd
      volumes:
        - name: mypd
          persistentVolumeClaim:
            claimName: local-pvc
      affinity:
        nodeAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
              - matchExpressions:
                  - key: nodeName
                    operator: In
                    values:
                      - B
status: {}

ここで確認しておきたいのは、0_pv.yaml と 2_deployment.yaml に設定した nodeAffinity の値です。
両方にそれぞれrequiredrequiredDuringSchedulingIgnoredDuringExecutionとして Node を指定するパラメータを設定しています。
Node にはそれぞれ nodeName として A,B という label を割り振っているので、PersistentVolume と Pod では別の Node に割り当てられようとしています。
requiredDuringSchedulingIgnoredDuringExecutionの別のパラメータとしては preferredDuringSchedulingIgnoredDuringExecutionというものがあるのですが、こちらはrequiredではなくprefferedなので、required 程優先はされません。
そのため今回はpreferredDuringSchedulingIgnoredDuringExecutionの方を使用しています。

続いて、マニフェストを展開します。

kubectl apply -f 0_pv.yaml
kubectl apply -f 1_pvc.yaml
kubectl apply -f 2_deployment.yaml

マニフェストを展開したら、しばらくして pod の様子を確認します。

kubectl get pod

結果はどうでしょうか。例としては以下のように、全部の Pod が Pending の状態になっているはずです。

NAME                                   READY   STATUS    RESTARTS   AGE
prefer-b-deployment-7479ffc7f8-2hpdc   0/1     Pending   0          9h
prefer-b-deployment-7479ffc7f8-5cgqq   0/1     Pending   0          9h
prefer-b-deployment-7479ffc7f8-5fp6m   0/1     Pending   0          9h
prefer-b-deployment-7479ffc7f8-9bllh   0/1     Pending   0          9h
prefer-b-deployment-7479ffc7f8-gxnrp   0/1     Pending   0          9h
prefer-b-deployment-7479ffc7f8-r5p8g   0/1     Pending   0          9h
prefer-b-deployment-7479ffc7f8-rmnkd   0/1     Pending   0          9h
prefer-b-deployment-7479ffc7f8-tjxdz   0/1     Pending   0          9h
prefer-b-deployment-7479ffc7f8-tqbff   0/1     Pending   0          9h
prefer-b-deployment-7479ffc7f8-xvs7c   0/1     Pending   0          9h

個々の Pod の状態はどのようになっているのか確認しましょう。
このうちの一個の Pod の名前を指定して、様子を見てみましょう。

kubectl describe pod prefer-b-deployment-7479ffc7f8-2hpdc

出力は分量が多いので省略しますが、最後の Event の部分が以下のようになっているはずです。

Events:
  Type     Reason            Age   From               Message
  ----     ------            ----  ----               -------
  Warning  FailedScheduling  62m   default-scheduler  0/2 nodes are available: 1 node(s) didn't match Pod's node affinity/selector, 1 node(s) had volume node affinity conflict.

0/2 nodes are available: 1 node(s) didn't match Pod's node affinity/selector, 1 node(s) had volume node affinity conflict.とあるとおり、Pod の NodeAffinity と PersistentVolume の NodeAffinity との間で Conflict が起きているためスケジューリングできる Node がないとしてエラーとなることが確認できます。

おわりに

Pod のスケジューリングの仕組みは組み合わせによってはこのようなエラーが出ることもあるので、node の conflict のエラーが起きたときに疑うべき要因の一つになるかもなと思った回でした。

参考