Skip to content

Dynamic Provisioning (Enterprise)

Dynamic provisioning allows Kubernetes to automatically create a new flexFS volume when a PersistentVolumeClaim is submitted. The CSI controller calls the admin server REST API to create the volume, retrieves a volume token, and makes it available to pods.

  • The flexFS CSI driver is installed
  • An Enterprise admin server is running and reachable from the cluster
  • You have an account token with volume creation permissions

The CSI driver needs the admin server address and an account token to create volumes. Store these in a Kubernetes Secret:

apiVersion: v1
kind: Secret
metadata:
name: flexfs-secret
namespace: default
stringData:
adminAddr: <ADMIN-ADDR>
token: <ACCOUNT-TOKEN>

Replace <ADMIN-ADDR> with the address of your admin server (for example, admin.example.com:443) and <ACCOUNT-TOKEN> with a valid account token.

Terminal window
kubectl apply -f secret.yaml

The StorageClass tells Kubernetes to use the flexFS CSI provisioner and specifies volume creation parameters:

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: flexfs-dynamic
provisioner: csi.flexfs.io
allowVolumeExpansion: true
parameters:
csi.storage.k8s.io/provisioner-secret-namespace: default
csi.storage.k8s.io/provisioner-secret-name: flexfs-secret
csi.storage.k8s.io/node-publish-secret-namespace: default
csi.storage.k8s.io/node-publish-secret-name: flexfs-secret
csi.storage.k8s.io/controller-expand-secret-namespace: default
csi.storage.k8s.io/controller-expand-secret-name: flexfs-secret
provider: aws
region: us-east-1

provider and region are not optional decoration: the admin server needs them (or an explicit metaStore + blockStore) to place the volume, and rejects a request carrying neither with a 400. See StorageClass parameters for the full list.

You can add optional parameters to control volume settings at creation time. See the StorageClass parameters reference for all options.

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: flexfs-encrypted
provisioner: csi.flexfs.io
allowVolumeExpansion: true
parameters:
csi.storage.k8s.io/provisioner-secret-namespace: default
csi.storage.k8s.io/provisioner-secret-name: flexfs-secret
csi.storage.k8s.io/node-publish-secret-namespace: default
csi.storage.k8s.io/node-publish-secret-name: flexfs-secret
csi.storage.k8s.io/controller-expand-secret-namespace: default
csi.storage.k8s.io/controller-expand-secret-name: flexfs-secret
provider: aws
region: us-east-1
blockSize: 4Mi
compression: "true"
compressionAlgo: lz4
encryption: "true"
maxInodes: "1000000"
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: flexfs-dynamic
namespace: default
spec:
storageClassName: flexfs-dynamic
accessModes:
- ReadWriteMany
resources:
requests:
storage: 100Gi
Terminal window
kubectl apply -f dynamic-volume.yaml

Once the PVC is created, the CSI provisioner will:

  1. Call CreateVolume on the flexFS CSI controller
  2. The controller sends a POST /v1/volumes request to the admin server to create a new volume
  3. A PersistentVolume is automatically created and bound to the PVC
apiVersion: v1
kind: Pod
metadata:
name: my-app
spec:
containers:
- name: app
image: ubuntu:latest
command: ["sleep", "infinity"]
volumeMounts:
- name: data
mountPath: /data
volumes:
- name: data
persistentVolumeClaim:
claimName: flexfs-dynamic

One volume per claim. The provisioner names each volume pvc-<PVC UID>, so every claim gets its own flexFS volume. Volumes are never reused across claims: delete a PVC and create another with the same name and you get a new UID, and therefore a new, empty volume. Binding to an existing volume is what static provisioning is for.

Retries adopt rather than duplicate. If provisioning is retried after a lost response, the admin server returns the volume it already created for that name instead of making a second one. If a volume of that name exists but has a smaller quota than the claim asks for, the driver reports ALREADY_EXISTS rather than binding the claim to something too small.

Deletion retires; cleanup follows. When the claim is deleted and the reclaim policy is Delete, the driver calls DELETE /v1/volumes/for-name/<volume-name>, which retires the volume: it is marked retired and its volume tokens are removed, but its data is still there. Block data is removed later, in a separate cleanup phase performed by the metadata server and bounded by the volume’s retention. So data outlives the claim for the retention window — which is also what makes point-in-time mounts of a deleted volume possible while it lasts. Deleting a claim whose volume is already gone succeeds rather than failing.

With reclaimPolicy: Retain, the driver is never asked to delete anything: the volume, its tokens, and its data stay until someone retires them with configure.flexfs delete volume.

A retired name is taken until cleanup finishes. Creating a volume whose name still belongs to a retired one is refused with a conflict rather than handing the retired volume back, since its data is queued for removal and would be emptied underneath the new claim. Dynamically provisioned names are derived from the claim’s UID, so this only arises when creating volumes with explicit names — wait for the retention period to elapse, or choose another name.

The claim’s resources.requests.storage is enforced on the created volume, not merely recorded (if the claim also sets resources.limits.storage, the limit is what the quota is set to, since that is the ceiling the claim says must not be exceeded):

  • It is rounded up to a whole block, so a 1 GiB claim on a 4 MiB block size becomes 256 blocks.
  • df inside the pod reports the quota, and writing past it fails with ENOSPC.
  • The limit counts stored blocks, after compression. Compressible data therefore fits more logical bytes than the claim asked for, and the quota is a cap on what the volume consumes rather than a promise about file sizes.
  • Because the limit is checked when blocks are recorded rather than when a write is buffered, a burst already in flight can overshoot slightly before subsequent writes start failing.
  • maxInodes on the StorageClass caps inodes; it is unlimited unless set, since a claim expresses only bytes.

Growing a volume. Set allowVolumeExpansion: true on the StorageClass and edit the claim’s request. The csi-resizer sidecar calls the driver, which raises the volume’s quota. The new limit is recorded at once, but a mount that is already running picks it up on its next volume-settings refresh — about a minute in practice — so for up to that long an already-full volume keeps returning ENOSPC and df keeps showing the old size. No remount or pod restart is needed; it simply is not instant.

Expansion only ever raises a quota. Kubernetes refuses to shrink a claim below its recorded capacity, so a dynamically provisioned volume’s limit is whatever its claim last asked for.

One request is refused: expanding a volume that has no quota. A volume with no limit already permits more than any request, so there is nothing to raise, and recording the unlimited sentinel — exabytes — as the claim’s capacity would be worse than an error. Any volume whose max_blocks is 0 is in this state, including one bound through a static PV. Set a limit deliberately with configure.flexfs update volume --maxBlocks/--maxInodes if you want one.

A pre-existing volume bound through a static PV is not affected by any of this: its PV capacity is informational, and quotas are set with configure.flexfs update volume --maxBlocks/--maxInodes.

FlexFS supports the following CSI access modes:

Access modeSupportedDescription
ReadWriteManyYesMultiple pods can read and write simultaneously
ReadWriteOnceYesSingle pod read-write access
ReadOnlyManyYesMultiple pods with read-only access