Lab 35 - Static and dynamic storage

Lab 35 - Static and dynamic storage

Goal: replace hand-written PersistentVolumes with self-service: a CSI driver and a StorageClass. Chapter: PV, PVC and StorageClasses

On Day 2 (Lab 10) you wrote three static NFS PVs by hand. Check they are still there:

kubectl get pv                    # pv-storage1..3

Dynamic provisioning with the NFS CSI driver

Everything so far was static: you wrote a PV for every share, by hand. That does not scale - ten teams asking for volumes means ten PVs and a ticket each. A CSI driver plus a StorageClass turns that into self-service: the PVC asks, the driver creates.

Note

A word on names. The nfs: volume type you used above is the in-tree NFS plugin - compiled into Kubernetes itself. In-tree storage plugins are frozen and being removed; every driver now lives out of tree and speaks CSI. So there is no such thing as an “in-tree CSI driver”: what you install below, csi-driver-nfs, is the out-of-tree replacement for the in-tree plugin. Same NFS server, same exports - different plumbing, and it is the one with a future.

# 1 - install the driver (a DaemonSet + a controller Deployment, nothing exotic)
helm repo add csi-driver-nfs https://raw.githubusercontent.com/kubernetes-csi/csi-driver-nfs/master/charts
helm repo update
helm install csi-driver-nfs csi-driver-nfs/csi-driver-nfs \
  --namespace kube-system --version 4.12.0
kubectl get pods -n kube-system -l app.kubernetes.io/name=csi-driver-nfs -w   # Ctrl-C when Running
# 2 - what did that add?
kubectl get csidrivers
kubectl get ds,deploy -n kube-system | grep nfs

Look at the shape of it: a DaemonSet (the node plugin, which does the actual mounting - so it must be on every node, exactly as in Lab 09 (DaemonSets)) and a Deployment (the controller, which creates and deletes volumes). The nodes still need nfs-common, which you installed in step 1 - CSI does not replace the kernel mount, it drives it.

The StorageClass

# 3 -
cd ~
cat > sc-nfs.yaml <<'YAML'
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: nfs-csi
provisioner: nfs.csi.k8s.io
parameters:
  server: support01
  share: /storage3
reclaimPolicy: Retain
volumeBindingMode: Immediate
allowVolumeExpansion: true
mountOptions:
  - nfsvers=4.1
YAML
kubectl apply -f sc-nfs.yaml
kubectl get sc

Read it as a recipe the cluster can follow without you:

FieldMeaning
provisionerwhich CSI driver handles claims for this class
parametersdriver-specific: the NFS server and the parent share
reclaimPolicywhat happens to the PV when its PVC is deleted - Retain here
volumeBindingModeImmediate provisions at once; WaitForFirstConsumer waits for a Pod

The reclaim policy is set on the class, and every PV it creates inherits it. With Delete - the usual default - deleting a PVC deletes the data. Retain is what you want for anything you would be sad to lose.

Tip

Free the export first: /storage3 is about to become the parent directory the driver creates subdirectories in, so it must not be claimed by the static PV. kubectl delete pv pv-storage3 if it is still Available.

Self-service: a PVC with no PV

# 4 - remove the static PV for /storage3, then just ask
kubectl delete pv pv-storage3 --ignore-not-found

cat > pvc-dyn.yaml <<'YAML'
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: dyn-data
spec:
  accessModes: ["ReadWriteMany"]
  resources:
    requests:
      storage: 300Mi
  storageClassName: nfs-csi
YAML
kubectl apply -f pvc-dyn.yaml
kubectl get pvc dyn-data
kubectl get pv

No PV existed and now one does. Its name is pvc-<uuid>, its RECLAIM POLICY is Retain, and it was created by the driver - not by you. That is the whole difference between the first half of this lab and this one.

# 5 - where did it actually put the data?
PV=$(kubectl get pvc dyn-data -o jsonpath='{.spec.volumeName}')
echo $PV
kubectl get pv $PV -o jsonpath='{.spec.csi.volumeAttributes}{"\n"}'
ssh student@support01 'ls -l /storage3'

The driver created a subdirectory inside /storage3 named after the volume. One export, many volumes - that is how a single NFS share serves a whole cluster.

# 6 - use it
cat > dynpod.yaml <<'YAML'
apiVersion: v1
kind: Pod
metadata:
  name: dyn-writer
spec:
  containers:
    - name: box
      image: busybox:1.36
      command: ["sh", "-c", "echo \"dynamic volume, $(date)\" >> /data/log.txt; sleep 3600"]
      volumeMounts:
        - name: data
          mountPath: /data
  volumes:
    - name: data
      persistentVolumeClaim:
        claimName: dyn-data
YAML
kubectl apply -f dynpod.yaml
kubectl exec dyn-writer -- cat /data/log.txt

The Pod spec is identical to the static one. Applications never know or care whether the volume was provisioned by hand or by a driver - they reference a claim, and that is the point of the abstraction.

Retain in action: the data outlives the claim

# 7 - throw away the Pod *and* the claim
kubectl delete pod dyn-writer
kubectl delete pvc dyn-data
kubectl get pv

The PV is Released, not gone. With reclaimPolicy: Delete the driver would have deleted the subdirectory on the NFS server by now; with Retain it left everything alone and simply refuses to hand the volume to anybody else.

# 8 - prove the data is still on the server
ssh student@support01 'ls -l /storage3; find /storage3 -name log.txt -exec cat {} \;'
# 9 - hand the same volume to a new claim
kubectl patch pv $PV -p '{"spec":{"claimRef":null}}'
kubectl get pv $PV                       # Available again

cat > pvc-reuse.yaml <<YAML
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: dyn-data-v2
spec:
  accessModes: ["ReadWriteMany"]
  resources:
    requests:
      storage: 300Mi
  storageClassName: nfs-csi
  volumeName: $PV
YAML
kubectl apply -f pvc-reuse.yaml
kubectl get pvc dyn-data-v2

volumeName is what stops the provisioner from creating a new volume: the claim names the PV it wants, so this is a static binding to a dynamically created volume. Clearing claimRef is the manual step Retain deliberately forces on you - it is the moment a human confirms the old data may be reused.

# 10 - a new pod, the old data
sed 's/dyn-writer/dyn-reader/; s/claimName: dyn-data$/claimName: dyn-data-v2/' dynpod.yaml \
  | kubectl apply -f -
kubectl exec dyn-reader -- cat /data/log.txt      # the line from step 13 is there

Two lines now: one written before you deleted the claim, one after. The Pod is new, the PVC is new, the data survived both.

Static or dynamic?

Static PVStorageClass + CSI
Who creates the volumeyou, per volumethe driver, on demand
Scales to many teamsnoyes
Needs a drivernoyes
Good fora fixed appliance, an existing shareeverything else

Production clusters run both: a default StorageClass for self-service, plus a handful of hand-written PVs for storage that already exists and must be mounted exactly as it is.

Clean up

kubectl delete pod dyn-reader --ignore-not-found
kubectl delete pvc dyn-data-v2 --ignore-not-found
kubectl patch pv $PV -p '{"spec":{"claimRef":null}}' 2>/dev/null
kubectl delete pv $PV --ignore-not-found      # Retain means this is your job
ssh student@support01 'sudo rm -rf /storage3/pvc-*'
kubectl get pv,pvc
Warning

Retain means nobody cleans up after you. Released PVs and their data sit there until someone deletes them - which is the safety you asked for and the tidiness bill that comes with it.

Discovery

Infodiscover

Task A. Make nfs-csi the default StorageClass, then create a PVC without storageClassName. Expected: it binds.

kubectl patch sc nfs-csi -p '{"metadata":{"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}'
kubectl get sc                    # nfs-csi (default)
kubectl create -f - <<'YAML'
apiVersion: v1
kind: PersistentVolumeClaim
metadata: { name: no-class }
spec:
  accessModes: ["ReadWriteOnce"]
  resources: { requests: { storage: 50Mi } }
YAML
kubectl get pvc no-class
kubectl delete pvc no-class

Charts and operators that do not name a class now just work.

Keep

Leave the CSI driver and the nfs-csi class installed - Labs 36, 39, 41 and 42 need them.