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; later, at mount time, the node driver retrieves a volume token and mounts the volume for pods.

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

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: <region>

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. For provider: aws, blockAPI defaults to s3; for gcp, azure, and oci you must also set blockAPI (gcs, azure, or oci respectively) or the request is rejected 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: <region>
blockSize: 4Mi
compression: "true"
compressionAlgo: lz4
encryption: "true"
maxInodes: "1000000"

Mounting a volume created with encryption: "true" requires its encryption secret, at least 8 characters long. Add it as the secret field of the Secret named by node-publish-secret-name:

stringData:
adminAddr: <admin-addr>
token: <account-token>
secret: <encryption-secret>
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 the volume. 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, its volume tokens are removed, and the metadata server then deletes its files and blocks. The volume’s retention does not apply once it is retired, so a deleted claim’s data cannot be recovered or mounted at an earlier point in time. Deleting a claim whose volume is already gone succeeds rather than failing. See Retiring a Volume.

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.

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 blocks, not bytes. Each stored block uses one unit of the quota whether it is full or partly filled, and compression does not change the count, so many small files can reach the quota well before their combined size does. df computes its used figure from file sizes, so it can show less usage than the quota enforces.
  • 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 reporting an unlimited capacity — exabytes — for the claim would be worse than an error. Any volume without a storage quota 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 on any nodes can read and write simultaneously
ReadWriteOnceYesRead-write from a single node. Any number of pods on that node can use the volume at the same time (see the note below).
ReadWriteOncePodYesRead-write for a single pod in the cluster (see the note below)
ReadOnlyManyYesMultiple pods on any nodes with read-only access