<h2>Scenario 1:</h2>
<p>I have 3 local-persistent-volumes provisioned, each pv is mounted on different node:</p>
<ul>
<li>10.30.18.10</li>
<li>10.30.18.11</li>
<li>10.30.18.12</li>
</ul>
<p>When I start my app with 3 replicas using:</p>
<pre><code>kind: StatefulSet
metadata:
  name: my-db
spec:
  replicas: 3
...
...
  volumeClaimTemplates:
  - metadata:
      name: my-local-vol
    spec:
      accessModes: [ &quot;ReadWriteOnce&quot; ]
      storageClassName: &quot;my-local-sc&quot;
      resources:
        requests:
          storage: 10Gi
</code></pre>
<p>Then I notice pods and pvs are on the same host:</p>
<ul>
<li>pod1 with ip <code>10.30.18.10</code> has claimed the pv that is mounted on <code>10.30.18.10</code></li>
<li>pod2 with ip <code>10.30.18.11</code> has claimed the pv that is mounted on <code>10.30.18.11</code></li>
<li>pod3 with ip <code>10.30.18.12</code> has claimed the pv that is mounted on <code>10.30.18.12</code></li>
</ul>
<p>(whats not happening is: pod1 with ip <code>10.30.18.10</code> has claimed the pv that is mounted on different node <code>10.30.18.12</code> etc)</p>
<p>The only common config between pv and pvc is <code>storageClassName</code>, so I didn't configure this behavior.</p>
<p><strong>Question:</strong>
So, who is responsible for this magic? Kubernetes scheduler? Kubernetes provisioner?</p>
<hr />
<h2>Scenario 2:</h2>
<p>I have 3 local-persistent-volumes provisioned:</p>
<ul>
<li>pv1 has capacity.storage of 10Gi</li>
<li>pv2 has capacity.storage of 100Gi</li>
<li>pv3 has capacity.storage of 100Gi</li>
</ul>
<p>Now, I start my app with 1 replica</p>
<pre><code>kind: StatefulSet
metadata:
  name: my-db
spec:
  replicas: 1
...
...
  volumeClaimTemplates:
  - metadata:
      name: my-local-vol
    spec:
      accessModes: [ &quot;ReadWriteOnce&quot; ]
      storageClassName: &quot;my-local-sc&quot;
      resources:
        requests:
          storage: 10Gi
</code></pre>
<p>I want to ensure that this StatefulSet <em>always</em> claim pv1 (10Gi) even this is on a different node, and don't claim pv2 (100Gi) and pv3 (100Gi)</p>
<p><strong>Question:</strong></p>
<p>Does this happen automatically?</p>
<p>How do I ensure the desired behavior? Should I use a separate storageClassName to ensure this?</p>
<p>What is the PersistentVolumeClaim policy? Where can I find more info?</p>
<hr />
<p>EDIT:</p>
<p>yml used for StorageClass:</p>
<pre><code>kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
  name: my-local-pv
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: WaitForFirstConsumer
</code></pre>

Sida Zhou
<p>With local Persistent Volumes, this is the expected behaviour. Let me try to explain what happens when using local storage.</p>
<p>The usual setup for local storage on a cluster is the following:</p>
<ul>
<li>A local storage class, configured to be <code>WaitForFirstConsumer</code></li>
<li>A series of local persistent volumes, linked to the local storage class</li>
</ul>
<p>And this is all well documented with examples in the official documentation: <a href="https://kubernetes.io/docs/concepts/storage/volumes/#local" rel="nofollow noreferrer">https://kubernetes.io/docs/concepts/storage/volumes/#local</a></p>
<p>With this done, Persistent Volume Claims can request storage from the local storage class and StatefulSets can have a <code>volumeClaimTemplate</code> which requests storage of the local storage class.</p>
<hr />
<p>Let me take as example your StatefulSet with 3 replicas, each one requires local storage with the <code>volumeClaimTemplate</code>.</p>
<ul>
<li><p>When the Pods are first created, they request a storage of the required <code>storageClass</code>. For example your <code>my-local-sc</code></p>
</li>
<li><p>Since this storage class is manually created and does <strong>not</strong> support dynamically provisioning of new PVs (like, for example, Ceph or similar) it is checked if a PV attached to the storage class is <code>available</code> to be bound.</p>
</li>
<li><p>If a PV is selected, it is bound to the newly created PVC (and from now, can be used only with that particular PV, since it is now <code>Bound</code>)</p>
</li>
<li><p>Since the PV is of type <code>local</code>, the PV has a <code>nodeAffinity</code> required which selects a node.</p>
</li>
<li><p>This <strong>force</strong> the Pod, now bound to that PV, to be <strong>scheduled only on that particular node</strong>.</p>
</li>
</ul>
<p>This is why each Pod was scheduled on the same node of the bounded persistent volume. And this means that the Pod is restricted to run on that node only.</p>
<p>You can test this easily by draining / cordoning one of the nodes and then trying to restart the Pod bound to the PV available on that particular node. What you should see is that the Pod will <strong>not</strong> start, as the PV is restricted from its <code>nodeAffinity</code> and the node is not available.</p>
<hr />
<p>Once each Pod of the StatefulSet is bound to a PV, that Pod will be scheduled only on a specific node.. Pods will not change the PV that they are using, unless the PVC is removed (which will force the Pod to request again a new PV to bound)</p>
<p>Since local storage is handled manually, PV which were bounded and have the related PVC removed from the cluster, enter in <code>Released</code> state and cannot be claimed anymore, they must be handled by someone.. maybe deleting them and then recreating new ones at the same location (and maybe cleaning the filesystem as well, depending on the situation)</p>
<p>This means that local storage is OK to be used only:</p>
<ul>
<li><p>If HA is not a problem.. for example, I don't care if my app is blocked by a single node not working</p>
</li>
<li><p>If HA is handled directly by the app itself. For example, a StatefulSet with 3 Pods like a multi-primary database (Galera, Clickhouse, Percona for examples) or ElasticSearch or Kafka, Zookeeper or something like that.. all will handle the HA on their own as they can resist one of their nodes being down as long as there's quorum.</p>
</li>
</ul>
<hr />
<p><strong>UPDATE</strong></p>
<p>Regarding the Scenario 2 of your question. Let's say you have multiple Available PVs and a single Pod which starts and wants to Bound to one of them. This is a normal behaviour and the control plane would select one of those PVs on its own (if they match with the requests in Claim)</p>
<p>There's a specific way to pre-bind a PV and a PVC, so that they will always bind together. This is described in the docs as &quot;<strong>reserving a PV</strong>&quot;: <a href="https://kubernetes.io/docs/concepts/storage/persistent-volumes/#reserving-a-persistentvolume" rel="nofollow noreferrer">https://kubernetes.io/docs/concepts/storage/persistent-volumes/#reserving-a-persistentvolume</a></p>
<p>But the problem is that this cannot be applied to olume claim templates, as it requires the claim to be created manually with special properties.</p>
<p>The volume claim template tho, as a selector field which can be used to restrict the selection of a PV based on labels. It can be seen in the API specs ( <a href="https://v1-18.docs.kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#persistentvolumeclaimspec-v1-core" rel="nofollow noreferrer">https://v1-18.docs.kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#persistentvolumeclaimspec-v1-core</a> )</p>
<p>When you create a PV, you label it with what you want.. for example you could label it like the following:</p>
<pre><code>apiVersion: v1
kind: PersistentVolume
metadata:
  name: example-small-pv
  labels:
    size-category: small
spec:
  capacity:
    storage: 10Gi
  volumeMode: Filesystem
  accessModes:
  - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  storageClassName: local-storage
  local:
    path: /mnt/disks/ssd1
  nodeAffinity:
    required:
      nodeSelectorTerms:
      - matchExpressions:
        - key: kubernetes.io/hostname
          operator: In
          values:
          - example-node-1
---
apiVersion: v1
kind: PersistentVolume
metadata:
  name: example-big-pv
  labels:
    size-category: big
spec:
  capacity:
    storage: 100Gi
  volumeMode: Filesystem
  accessModes:
  - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  storageClassName: local-storage
  local:
    path: /mnt/disks/ssd1
  nodeAffinity:
    required:
      nodeSelectorTerms:
      - matchExpressions:
        - key: kubernetes.io/hostname
          operator: In
          values:
          - example-node-2
</code></pre>
<p>And then the claim template can select a category of volumes based on the label. Or maybe it doesn't care so it doesn't specify <code>selector</code> and can use all of them (provided that the size is enough for its claim request)</p>
<p>This could be useful.. but it's not the only way to select or restrict which PVs can be selected, because when the PV is first bound, if the storage class is WaitForFirstConsumer, the following is also applied:</p>
<blockquote>
<p>Delaying volume binding ensures that the PersistentVolumeClaim binding
decision will also be evaluated with any other node constraints the
Pod may have, such as node resource requirements, node selectors, Pod
affinity, and Pod anti-affinity.</p>
</blockquote>
<p>Which means that if the Pod has a node affinity to one of the nodes, it will select for sure a PV on that node (if the local storage class used is WaitForFirstConsumer)</p>
<hr />
<p>Last, let me quote the offical documentation for things that I think they could answer your questions:</p>
<p>From <a href="https://kubernetes.io/docs/concepts/storage/persistent-volumes/" rel="nofollow noreferrer">https://kubernetes.io/docs/concepts/storage/persistent-volumes/</a></p>
<blockquote>
<p>A user creates, or in the case of dynamic provisioning, has already
created, a PersistentVolumeClaim with a specific amount of storage
requested and with certain access modes. A control loop in the master
watches for new PVCs, finds a matching PV (if possible), and binds
them together. If a PV was dynamically provisioned for a new PVC, the
loop will always bind that PV to the PVC. Otherwise, the user will
always get at least what they asked for, but the volume may be in
excess of what was requested. Once bound, PersistentVolumeClaim binds
are exclusive, regardless of how they were bound. A PVC to PV binding
is a one-to-one mapping, using a ClaimRef which is a bi-directional
binding between the PersistentVolume and the PersistentVolumeClaim.</p>
<p>Claims will remain unbound indefinitely if a matching volume does not
exist. Claims will be bound as matching volumes become available. For
example, a cluster provisioned with many 50Gi PVs would not match a
PVC requesting 100Gi. The PVC can be bound when a 100Gi PV is added to
the cluster.</p>
</blockquote>
<p>From <a href="https://kubernetes.io/docs/concepts/storage/volumes/#local" rel="nofollow noreferrer">https://kubernetes.io/docs/concepts/storage/volumes/#local</a></p>
<blockquote>
<p>Compared to hostPath volumes, local volumes are used in a durable and
portable manner without manually scheduling pods to nodes. The system
is aware of the volume's node constraints by looking at the node
affinity on the PersistentVolume.</p>
<p>However, local volumes are subject to the availability of the
underlying node and are not suitable for all applications. If a node
becomes unhealthy, then the local volume becomes inaccessible by the
pod. The pod using this volume is unable to run. Applications using
local volumes must be able to tolerate this reduced availability, as
well as potential data loss, depending on the durability
characteristics of the underlying disk.</p>
</blockquote>
