💡 Usage

📜 Table of Contents

  1. Create a StorageClass
  2. Create a PersistentVolumeClaim
  3. Consume it from a pod
  4. Share one volume between pods
  5. Access modes
  6. Mount options
  7. Reclaim policy
  8. Inspecting a volume
  9. Volume expansion
  10. What is not supported
  11. 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:

  1. Authorize (node mode only). csi-attacher calls ControllerPublishVolume, 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.
  2. 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.
  3. 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 no fsType parameter, and setting csi.storage.k8s.io/fstype has no effect.
  • NodeStageVolume passes these flags to the NFS mount. NodePublishVolume passes them again alongside bind, and adds ro for 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. 8 is an example, not a universal recommendation or driver limit. Availability and accepted values depend on the node’s Linux kernel and NFS client.
  • remoteports=dns tells 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. With nconnect=8,pconnect=1, the client can use up to eight remote addresses. With nconnect=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.


This site uses Just the Docs, a documentation theme for Jekyll.