Ubuntu 26.04

Kubernetes : Dynamic Volume Provisioning (NFS)2026/05/14

 

To use Dynamic Volume Provisioning feature when using Persistent Storage, it's possible to create PV (Persistent Volume) dynamically without creating PV manually by Cluster Administrator when created PVC (Persistent Volume Claim) by users.

This example is based on the environment like follows.

+----------------------+   +----------------------+
|  [ ctrl.srv.world ]  |   |   [ dlp.srv.world ]  |
|     Manager Node     |   |     Control Plane    |
+-----------+----------+   +-----------+----------+
        eth0|10.0.0.25             eth0|10.0.0.30
            |                          |
------------+--------------------------+-----------
            |                          |
        eth0|10.0.0.51             eth0|10.0.0.52
+-----------+----------+   +-----------+----------+
| [ node01.srv.world ] |   | [ node02.srv.world ] |
|     Worker Node#1    |   |     Worker Node#2    |
+----------------------+   +----------------------+

 

For example, configure dynamic volume provisioning with NFS provisioner that NFS storage is provided from [nfs.srv.world (10.0.0.35)].

[1]

Run NFS Server, refer to here.
On this example, configure [/home/nfsshare] directory as NFS share.

[2]

Worker Nodes need to be able to mount NFS share on NFS server.

[3] Install NFS Client Provisioner with Helm.
ubuntu@ctrl:~$
helm repo add nfs-subdir-external-provisioner https://kubernetes-sigs.github.io/nfs-subdir-external-provisioner/
# nfs.server = (NFS server's hostname or IP address)
# nfs.path = (NFS share Path)

ubuntu@ctrl:~$
helm install nfs-client -n kube-system --set nfs.server=10.0.0.35 --set nfs.path=/home/nfsshare nfs-subdir-external-provisioner/nfs-subdir-external-provisioner
NAME: nfs-client
LAST DEPLOYED: Thu May 14 00:46:58 2026
NAMESPACE: kube-system
STATUS: deployed
REVISION: 1
TEST SUITE: None

ubuntu@ctrl:~$
kubectl get pods -n kube-system

NAME                                                          READY   STATUS    RESTARTS      AGE
coredns-589f44dc88-69rnb                                      1/1     Running   1 (51m ago)   44h
coredns-589f44dc88-lcgqc                                      1/1     Running   1 (51m ago)   44h
etcd-dlp.srv.world                                            1/1     Running   1 (51m ago)   44h
kube-apiserver-dlp.srv.world                                  1/1     Running   1 (51m ago)   44h
kube-controller-manager-dlp.srv.world                         1/1     Running   1 (51m ago)   44h
kube-proxy-758rn                                              1/1     Running   1 (51m ago)   44h
kube-proxy-zblkn                                              1/1     Running   1 (50m ago)   44h
kube-proxy-zkvbq                                              1/1     Running   1 (50m ago)   44h
kube-scheduler-dlp.srv.world                                  1/1     Running   1 (51m ago)   44h
metrics-server-54bf87486c-7qctk                               1/1     Running   0             14m
nfs-client-nfs-subdir-external-provisioner-578cb67886-4tqwp   1/1     Running   0             21s
[4] This is an example to use dynamic volume provisioning by a Pod.
ubuntu@ctrl:~$
kubectl get pv

No resources found in default namespace.
ubuntu@ctrl:~$
kubectl get pvc

No resources found in default namespace.
ubuntu@ctrl:~$
kubectl get storageclass

NAME         PROVISIONER                                                RECLAIMPOLICY   VOLUMEBINDINGMODE   ALLOWVOLUMEEXPANSION   AGE
nfs-client   cluster.local/nfs-client-nfs-subdir-external-provisioner   Delete          Immediate           true                   65s

# create PVC

ubuntu@ctrl:~$
vi my-pvc.yml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: my-provisioner
spec:
  accessModes:
    - ReadWriteOnce
  # specify StorageClass name
  storageClassName: nfs-client
  resources:
    requests:
      # volume size
      storage: 5Gi

ubuntu@ctrl:~$
kubectl apply -f my-pvc.yml

persistentvolumeclaim/my-provisioner created
ubuntu@ctrl:~$
kubectl get pvc

NAME             STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   VOLUMEATTRIBUTESASS   AGE
my-provisioner   Bound    pvc-fbe36a32-3557-44a7-b805-4e6cf3c408cc   5Gi        RWO            nfs-client     <unset>               4s

# PV is generated dynamically

ubuntu@ctrl:~$
kubectl get pv

NAME                                       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                    STORAGECLASS   VOLUMEATTRIBUTESCLASS   REASON   AGE
pvc-fbe36a32-3557-44a7-b805-4e6cf3c408cc   5Gi        RWO            Delete           Bound    default/my-provisioner   nfs-client     <unset>                          25s

ubuntu@ctrl:~$
vi my-pod.yml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-nginx
spec:
  selector:
    matchLabels:
      run: my-nginx
  replicas: 1
  template:
    metadata:
      labels:
        run: my-nginx
    spec:
      containers:
      - name: my-nginx
        image: nginx
        ports:
        - containerPort: 80
        volumeMounts:
        - mountPath: /usr/share/nginx/html
          name: nginx-pvc
      volumes:
        - name: nginx-pvc
          persistentVolumeClaim:
            # PVC name you created
            claimName: my-provisioner

ubuntu@ctrl:~$
kubectl apply -f my-pod.yml

deployment.apps/my-nginx created
ubuntu@ctrl:~$
kubectl get pods

NAME                        READY   STATUS    RESTARTS   AGE
my-nginx-845f6d67c5-w5zkt   1/1     Running   0          6s

ubuntu@ctrl:~$
kubectl exec my-nginx-845f6d67c5-w5zkt -- df /usr/share/nginx/html

Filesystem                                                                               1K-blocks  Used Available Use% Mounted on
10.0.0.35:/home/nfsshare/default-my-provisioner-pvc-fbe36a32-3557-44a7-b805-4e6cf3c408cc 164028416  2048 155621376   1% /usr/share/nginx/html

# verify accessing to create test index file

ubuntu@ctrl:~$
echo "Nginx Index" > index.html

ubuntu@ctrl:~$
kubectl cp index.html my-nginx-845f6d67c5-w5zkt:/usr/share/nginx/html/index.html

ubuntu@ctrl:~$
kubectl expose deployment my-nginx --type="NodePort" --port 80

ubuntu@ctrl:~$
kubectl port-forward service/my-nginx --address 127.0.0.1 8082:80 &

ubuntu@ctrl:~$
curl localhost:8082

Handling connection for 8082
Nginx Index
# when removing, to remove PVC, then PV is also removed dynamically

ubuntu@ctrl:~$
kubectl delete deployment my-nginx

deployment.apps "my-nginx" deleted
ubuntu@ctrl:~$
kubectl delete pvc my-provisioner

persistentvolumeclaim "my-provisioner" deleted
ubuntu@ctrl:~$
kubectl get pv

No resources found
[5] To use StatefulSet, it's possible to specify [volumeClaimTemplates].
ubuntu@ctrl:~$
kubectl get pv

No resources found in default namespace.
ubuntu@ctrl:~$
kubectl get pvc

No resources found in default namespace.
ubuntu@ctrl:~$
kubectl get storageclass

NAME         PROVISIONER                                                RECLAIMPOLICY   VOLUMEBINDINGMODE   ALLOWVOLUMEEXPANSION   AGE
nfs-client   cluster.local/nfs-client-nfs-subdir-external-provisioner   Delete          Immediate           true                   4m57s

ubuntu@ctrl:~$
vi statefulset.yml
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: my-mginx
spec:
  serviceName: my-mginx
  replicas: 1
  selector:
    matchLabels:
      app: my-mginx
  template:
    metadata:
      labels:
        app: my-mginx
    spec:
      containers:
      - name: my-mginx
        image: nginx
        volumeMounts:
        - name: data
          mountPath: /usr/share/nginx/html
  volumeClaimTemplates:
  - metadata:
      name: data
    spec:
      # specify StorageClass name
      storageClassName: nfs-client
      accessModes: [ "ReadWriteOnce" ]
      resources:
        requests:
          storage: 5Gi

ubuntu@ctrl:~$
kubectl apply -f statefulset.yml

statefulset.apps/my-mginx created
ubuntu@ctrl:~$
kubectl get statefulset

NAME       READY   AGE
my-mginx   1/1     7s

ubuntu@ctrl:~$
kubectl get pods

NAME         READY   STATUS    RESTARTS   AGE
my-mginx-0   1/1     Running   0          15s

ubuntu@ctrl:~$
kubectl get pvc

NAME              STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   VOLUMEATTRIBUTESCLASS   AGE
data-my-mginx-0   Bound    pvc-65b069b1-bf8a-4191-869b-a7ce9986c4d1   5Gi        RWO            nfs-client     <unset>                 19s

ubuntu@ctrl:~$
kubectl get pv

NAME                                       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                     STORAGECLASS   VOLUMEATTRIBUTESCLASS   REASON   AGE
pvc-65b069b1-bf8a-4191-869b-a7ce9986c4d1   5Gi        RWO            Delete           Bound    default/data-my-mginx-0   nfs-client     <unset>                          35s
Matched Content