CentOS Stream 10: Dynamic Provisioning (NFS)
Configure dynamic volume provisioning using NFS as the storage backend in Kubernetes on CentOS Stream 10.
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)].
Step 1
Run NFS Server.
On this example, configure [/home/nfsshare] directory as NFS share.
Step 2
Worker Nodes need to be able to mount NFS share on NFS server.
Step 3
Install NFS Client Provisioner with Helm.
[cent@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)
[cent@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: Tue May 20 13:28:56 2025
NAMESPACE: kube-system
STATUS: deployed
REVISION: 1
TEST SUITE: None
[cent@ctrl ~]$ kubectl get deployment -n kube-system
NAME READY UP-TO-DATE AVAILABLE AGE
calico-kube-controllers 1/1 1 1 3h49m
coredns 2/2 2 2 3h50m
metrics-server 1/1 1 1 9m31s
nfs-client-nfs-subdir-external-provisioner 1/1 1 1 19s
Step 4
This is an example to use dynamic volume provisioning by a Pod.
[cent@ctrl ~]$ kubectl get pv
No resources found in default namespace.
[cent@ctrl ~]$ kubectl get pvc
No resources found in default namespace.
[cent@ctrl ~]$ kubectl get storageclass
NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE
nfs-client cluster.local/nfs-client-nfs-subdir-external-provisioner Delete Immediate true 57s
create PVC
[cent@ctrl ~]$ vi my-pvc.yml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: my-provisioner
spec:
accessModes:
- ReadWriteOnce
<span class="color2"># specify StorageClass name</span>
storageClassName: nfs-client
resources:
requests:
<span class="color2"># volume size</span>
storage: 5Gi
[cent@ctrl ~]$ kubectl apply -f my-pvc.yml
persistentvolumeclaim/my-provisioner created
[cent@ctrl ~]$ kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS VOLUMEATTRIBUTESCLASS AGE
my-provisioner Bound pvc-b21cbb22-e9e8-4b58-abfc-320b0584db71 5Gi RWO nfs-client <unset> 4s
PV is generated dynamically
[cent@ctrl ~]$ kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS VOLUMEATTRIBUTESCLASS REASON AGE
pvc-b21cbb22-e9e8-4b58-abfc-320b0584db71 5Gi RWO Delete Bound default/my-provisioner nfs-client <unset> 33s
[cent@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:
<span class="color2"># PVC name you created</span>
claimName: my-provisioner
[cent@ctrl ~]$ kubectl apply -f my-pod.yml
deployment.apps/my-nginx created
[cent@ctrl ~]$ kubectl get pods
NAME READY STATUS RESTARTS AGE
my-nginx-6b6dcc5896-szlgv 1/1 Running 0 8s
[cent@ctrl ~]$ kubectl exec my-nginx-6b6dcc5896-szlgv -- df /usr/share/nginx/html
Filesystem 1K-blocks Used Available Use% Mounted on
10.0.0.35:/home/nfsshare/default-my-provisioner-pvc-b21cbb22-e9e8-4b58-abfc-320b0584db71 164028416 0 155623424 0% /usr/share/nginx/html
verify accessing to create test index file
[cent@ctrl ~]$ echo "Nginx Index" > index.html
[cent@ctrl ~]$ kubectl cp index.html my-nginx-6b6dcc5896-szlgv:/usr/share/nginx/html/index.html
[cent@ctrl ~]$ kubectl expose deployment my-nginx --type="NodePort" --port 80
[cent@ctrl ~]$ kubectl port-forward service/my-nginx --address 127.0.0.1 8082:80 &
[cent@ctrl ~]$ curl localhost:8082
Handling connection for 8082
Nginx Index
when removing, to remove PVC, then PV is also removed dynamically
[cent@ctrl ~]$ kubectl delete deployment my-nginx
deployment.apps "my-nginx" deleted
[cent@ctrl ~]$ kubectl delete pvc my-provisioner
persistentvolumeclaim "my-provisioner" deleted
[cent@ctrl ~]$ kubectl get pv
No resources found
Step 5
To use StatefulSet, it's possible to specify [volumeClaimTemplates].
[cent@ctrl ~]$ kubectl get pv
No resources found in default namespace.
[cent@ctrl ~]$ kubectl get pvc
No resources found in default namespace.
[cent@ctrl ~]$ kubectl get storageclass
NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE
nfs-client cluster.local/nfs-client-nfs-subdir-external-provisioner Delete Immediate true 5m15s
[cent@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:
<span class="color2"># specify StorageClass name</span>
storageClassName: nfs-client
accessModes: [ "ReadWriteOnce" ]
resources:
requests:
storage: 5Gi
[cent@ctrl ~]$ kubectl apply -f statefulset.yml
statefulset.apps/my-mginx created
[cent@ctrl ~]$ kubectl get statefulset
NAME READY AGE
my-mginx 1/1 8s
[cent@ctrl ~]$ kubectl get pods
NAME READY STATUS RESTARTS AGE
my-mginx-0 1/1 Running 0 21s
[cent@ctrl ~]$ kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS VOLUMEATTRIBUTESCLASS AGE
data-my-mginx-0 Bound pvc-d68145c1-8a77-4ccb-88f4-1741cb2317f9 5Gi RWO nfs-client <unset> 34s
[cent@ctrl ~]$ kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS VOLUMEATTRIBUTESCLASS REASON AGE
pvc-d68145c1-8a77-4ccb-88f4-1741cb2317f9 5Gi RWO Delete Bound default/data-my-mginx-0 nfs-client <unset> 50s