💡 Usage
📜 Table of Contents
- Create a StorageClass
- Create a PersistentVolumeClaim
- Consume it from a pod
- Share one volume between pods
- Access modes
- Mount options
- Reclaim policy
- Inspecting a volume
- Volume expansion
- What is not supported
- Connection count and endpoint spreading
1. Create a StorageClass
The driver does not ship a StorageClass. There is no default class and no template in the Helm chart, because every class has to name a Storage Space that only you know about. Create one before you create a PVC.
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: linode-nfs
provisioner: linodenfs.csi.linode.com
parameters:
# Exactly one of space-id or space-label is required.
linodenfs.csi.linode.com/space-id: "42"
reclaimPolicy: Delete
allowVolumeExpansion: true
volumeBindingMode: Immediate
Or reference the space by label instead of ID:
parameters:
linodenfs.csi.linode.com/space-label: "my-cluster-space"
Referencing by label costs one extra API call per CreateVolume (an exact-match list), so referencing by ID is marginally cheaper.
To make it the cluster default:
metadata:
name: linode-nfs
annotations:
storageclass.kubernetes.io/is-default-class: "true"
See StorageClass parameters for tags, root squash, and the exact validation rules.
2. Create a PersistentVolumeClaim
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: nfs-pvc
spec:
accessModes:
- ReadWriteMany
storageClassName: linode-nfs
resources:
requests:
storage: 10Gi
kubectl apply -f nfs-pvc.yaml
kubectl get pvc nfs-pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
nfs-pvc Bound pvc-a1b2c3d4-5e6f-7890-abcd-ef1234567890 10Gi RWX linode-nfs 35s
Behind the bound PVC, the controller has created an NFS filesystem in your Storage Space labelled after the PV name (lowercased, truncated to 63 bytes). The driver neither validates nor changes the Space access policy, so a bound PVC does not prove the cluster can reach it.
If the Space policy is disabled, not active, or does not admit the cluster VPC, the PVC still binds but pods using it stay ContainerCreating with a mount.nfs4 error. Ask the Space owner to fix the policy; the kubelet retries the mount once it is corrected. See Mount times out or is denied.
Provisioning waits on the Linode API, so first-time creation is not instant. The driver allows up to 5 minutes per wait before returning DeadlineExceeded and letting csi-provisioner retry.
3. Consume it from a pod
apiVersion: v1
kind: Pod
metadata:
name: nfs-app
spec:
containers:
- name: app
image: busybox
command: ["/bin/sh", "-c", "while true; do date >> /data/out.txt; sleep 5; done"]
volumeMounts:
- name: data
mountPath: /data
volumes:
- name: data
persistentVolumeClaim:
claimName: nfs-pvc
kubectl apply -f nfs-app.yaml
kubectl exec nfs-app -- tail -n 3 /data/out.txt
After scheduling, the sequence depends on accessPolicy.mode:
- Authorize (
nodemode only).csi-attachercallsControllerPublishVolume, which adds that node’s Linode to the filesystem’s Linode ACL and waits for the access policy to become active. There is no block device involved. - Stage. The node plugin mounts the filesystem over NFSv4 at the kubelet staging path, once per node. It prefers the FQDN mount source and uses an IP source when the FQDN is absent.
- Publish. The node plugin bind-mounts the staging path into the pod’s target path.
The default space mode skips step 1. The Space access policy authorizes cluster connectivity, and Kubernetes creates no VolumeAttachment.
4. Share one volume between pods
This is the whole point of file storage. One PVC, many pods, on many nodes, all writing:
apiVersion: apps/v1
kind: Deployment
metadata:
name: nfs-writers
spec:
replicas: 3
selector:
matchLabels:
app: nfs-writers
template:
metadata:
labels:
app: nfs-writers
spec:
containers:
- name: writer
image: busybox
command:
- /bin/sh
- -c
- while true; do echo "$(hostname) $(date)" >> /data/shared.log; sleep 5; done
volumeMounts:
- name: data
mountPath: /data
volumes:
- name: data
persistentVolumeClaim:
claimName: nfs-pvc
kubectl exec deploy/nfs-writers -- tail -n 10 /data/shared.log
Every replica writes to the same filesystem. In the default space mode there is no per-node ACL and no VolumeAttachment; the Space access policy that the Space owner configured admits the cluster VPC, which covers every node. In node mode, each node the replicas land on also gets its Linode added to the filesystem’s ACL on first attach.
Pods sharing a volume on one node do not serialize behind each other: NodePublishVolume locks the pod target path, not the volume ID, so concurrent pod starts proceed in parallel. The one-per-node NFS mount is shared and staged only once.
🔓 Access modes
The driver accepts every mount access mode the CSI spec defines:
| Access mode | Short | Accepted |
|---|---|---|
ReadWriteMany | RWX | ✅ |
ReadOnlyMany | ROX | ✅ |
ReadWriteOnce | RWO | ✅ |
ReadWriteOncePod | RWOP | ✅ |
ReadWriteMany is the mode that reflects what the backend actually does. The narrower modes are accepted and honored by Kubernetes’ own scheduling and binding rules, but nothing in the driver or the NFS backend enforces single-node exclusivity for them, so do not rely on ReadWriteOnce as a data-safety mechanism here.
ReadOnlyMany publishes each pod’s bind mount with ro, even when the Pod omits readOnly: true. CSI SINGLE_NODE_READER_ONLY requests also publish with ro. NFS staging and backend export policy are unchanged.
Requesting a raw block volume (volumeMode: Block) fails with InvalidArgument. This is a filesystem, not a device.
⚓ Mount options
Mount flags on the StorageClass are passed through to both the NFS mount and the pod bind-mount:
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: linode-nfs-tuned
provisioner: linodenfs.csi.linode.com
parameters:
linodenfs.csi.linode.com/space-id: "42"
mountOptions:
- hard
- noatime
- rsize=1048576
- wsize=1048576
- timeo=600
- retrans=2
reclaimPolicy: Delete
Notes:
- The filesystem type is always
nfs4. There is nofsTypeparameter, and settingcsi.storage.k8s.io/fstypehas no effect. NodeStageVolumepasses these flags to the NFS mount.NodePublishVolumepasses them again alongsidebind, and addsrofor an explicit read-only request or a reader-only CSI access mode.
For NFS connection count and multipath options, see Connection count and endpoint spreading at the end of this page.
Mount options are copied to the PV when it is provisioned. Editing the StorageClass does not change options on existing PVs; newly provisioned volumes use the updated options. See StorageClass parameters.
🔄 Reclaim policy
Standard Kubernetes semantics apply, and the choice matters more than usual because the driver cannot list volumes for you.
reclaimPolicy | On PVC deletion |
|---|---|
Delete | The PV is removed and DeleteVolume deletes the NFS filesystem |
Retain | The PV stays Released and the NFS filesystem stays in your Storage Space |
With Retain, cleanup is manual. Since ListVolumes is not implemented, the driver will not help you find orphans later; the filesystem label carries the old PV name, which is your only breadcrumb. Delete retained filesystems through the Cloud Manager or the API when you are done with them.
🔍 Inspecting a volume
The PV’s volumeHandle and volumeAttributes tell you exactly which backend object a claim maps to:
kubectl get pv -o custom-columns=\
NAME:.metadata.name,\
HANDLE:.spec.csi.volumeHandle,\
TARGET:.spec.csi.volumeAttributes.mount-target,\
REGION:.spec.csi.volumeAttributes.region
NAME HANDLE TARGET REGION
pvc-a1b2c3d4-5e6f-7890-abcd-ef1234567890 42/1337 fs-1337.us-ord.nfs.linode.com us-ord
The handle is {space_id}/{filesystem_id}. The filesystem access policy depends on the configured mode:
curl -sS \
-H "Authorization: Bearer ${LINODE_API_TOKEN}" \
https://api.linode.com/v4beta/nfs/spaces/42/filesystems/1337/access-policy
In space mode, an explicit filesystem-root-squash setting produces an enabled policy with an empty linode_acl; access continues through the Space’s VPC policy. Omitting the setting or leaving it blank leaves the filesystem policy unchanged. In node mode, the driver maintains the ACL through ControllerPublishVolume and ControllerUnpublishVolume.
📏 Volume expansion
Set allowVolumeExpansion: true in the StorageClass to allow PVC capacity increases. The controller expands the filesystem’s root quota online; no node-side resize is needed. Equal-size retries succeed without changing the quota, while requests smaller than the current quota are rejected. The Linode API enforces the maximum customer-manageable capacity, and the driver returns API rejections to Kubernetes.
🚫 What is not supported
| Operation | Status |
|---|---|
Volume cloning (dataSource of kind PersistentVolumeClaim) | Rejected with unsupported volume content source |
| Raw block volumes | Rejected. Mount capabilities only |
GetCapacity | Not implemented, so storage-capacity-aware scheduling has nothing to consume |
| Listing volumes | ListVolumes is not implemented and the capability is not advertised, so the external-provisioner never calls it |
🔌 Connection count and endpoint spreading
Use StorageClass.mountOptions to pass connection and multipath options to the NFS client on each worker node. For example, add these entries to the class:
mountOptions:
- hard
- nconnect=8
- remoteports=dns
- pconnect=1
These options control different parts of the connection layout:
nconnect=<N>requests<N>connections for the mount. It controls the total connection count; it does not by itself spread connections across different server IP addresses.8is an example, not a universal recommendation or driver limit. Availability and accepted values depend on the node’s Linux kernel and NFS client.remoteports=dnstells a compatible NFS client to use the IP addresses returned when it resolves the mount target as remote endpoints. Multipath can spread connections only when the node’s resolver returns multiple distinct addresses.pconnect=<N>controls how many connections are grouped into each NFS client context. Withnconnect=8,pconnect=1, the client can use up to eight remote addresses. Withnconnect=8,pconnect=2, it groups connections in pairs and can use at most four remote addresses. The actual spread also depends on the addresses returned by DNS and the client implementation.
remoteports and pconnect are not portable Linux NFS mount options. They require a compatible NFS client on every node where the volume may be mounted. The CSI driver does not install or configure that client. An unsupported option causes the node mount to fail. Check the node client’s documentation for supported NFS versions, kernel requirements, and option limits before using them.
Without an FQDN, the node mounts the first usable IPv6 source returned by the API. IPv4 and IPv4-mapped IPv6 addresses are ignored. If the mount options include remoteports=dns, the node replaces it with the usable IPv6 addresses joined by ~. It removes export paths and IPv6 brackets from this list. For example, two IPv6 targets produce remoteports=2001:db8::1~2001:db8::2.
The driver preserves explicit remoteports values, nconnect, and pconnect. It does not add multipath options when none are requested. FQDN-based mounts retain their existing options, including remoteports=dns.
For FQDN-based mounts, the driver uses the worker node’s resolver, not CoreDNS. Check the mount target’s answers from a worker node; one returned address cannot provide endpoint spreading. After mounting, inspect the effective NFS mount options on that node:
findmnt -t nfs4 -o SOURCE,TARGET,OPTIONS
That confirms the mount options, not how many connections reached each endpoint. Use the node client’s diagnostics or inspect its established NFS connections to verify the actual distribution. In one CSI test with a compatible node client, nconnect=8,remoteports=dns,pconnect=1 produced eight TCP connections across eight distinct IPv6 endpoints. This is an observed connection layout, not a performance guarantee; benchmark with your workload and node configuration.