This is the second article in my series about PX-CSI. In the previous article, we covered what PX-CSI is, its architecture, and how to install it on a Kubernetes cluster. Now that PX-CSI is running, let’s see how we can provision storage for our applications.
Introduction
At the center of everything in PX-CSI sits one Kubernetes object: the StorageClass.This is where you tell PX-CSI which backend (FlashArray, FlashArray File, FlashBlade) and options to use, and it’s chat a PersistentVolumeClaim references to get a volume provisioned automatically. This article walks through the StorageClass parameters in detail, then covers both ways to get a volume into Kubernetes: dynamic provisioning (PX-CSI creates a new volume) and static provisioning (PX-CSI import an existing volume from the array).
The storageClass
PX-CSI uses standard Kubernetes StorageClass resources, with a set of PX-CSI-specific parameters. The provisioner is always the same:
provisioner: pxd.portworx.com
| Field | Purpose |
reclaimPolicy | Delete or Retain – what happens to the colume when the PVC is released |
volumeBindingMode | Immediate or WaitForFirstConsumer (required for topology-aware provisioning) |
allowVolumeExpansion | true / false – whether the volume can be resized later |
mountOptions | NFS options for NFS-backed classes (nfsvers=4.1, tcp) |
parameters.backend | pure_block (FlashArray block volumes), pure_fa_file (FlashArray File), or pure_file (FlashBlade File) |
FlashArray block volumes
The simplest case – a block storageClass for FlashArray. This storageClass is provisioned automatically by the PX operator:
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: px-fa-direct-access
provisioner: pxd.portworx.com
allowVolumeExpansion: true
parameters:
backend: pure_block
reclaimPolicy: Delete
volumeBindingMode: Immediate
Useful optional parameters for block volumes:
parameters.max_iops— QoS IOPS cap (100 to 100,000,000)parameters.max_bandwidth— QoS bandwidth cap (10G,500M, etc.)parameters.csi.storage.k8s.io/fstype— file system to create (ext4,xfs, …)parameters.secure—trueto enable volume encryption (1 encryption key for the whole cluster)parameters.pure_fa_pod_name— required for ActiveCluster stretched pods and for secure multi-tenancy (realms); we’ll use this in a later article
FlashBlade filesystems
FlashBlade file systems use their own, distinct backend: pure_file — don’t confuse it with pure_fa_file, which is FlashArray File Services and gets its own dedicated section further down this article.
PX operator provision 2x StorageClass:
- px-fb-direct-access-nfsv3
- px-fb-direct-access-nfsv4
Below the details for those 2 storageClass
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: px-fb-direct-access-nfsv3
provisioner: pxd.portworx.com
parameters:
backend: pure_file
pure_export_rules: '*(rw)'
mountOptions:
- nfsvers=3
- tcp
reclaimPolicy: Delete
volumeBindingMode: Immediate
allowVolumeExpansion: true
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: px-fb-direct-access-nfsv4
provisioner: pxd.portworx.com
parameters:
backend: pure_file
pure_export_rules: '*(rw)'
mountOptions:
- nfsvers=4.1
- tcp
reclaimPolicy: Delete
volumeBindingMode: Immediate
allowVolumeExpansion: true
As you can see, the only difference reside in the mountOptions, specifying the NFS version to use.
Key parameters worth knowing about, specific to FlashBlade:
pure_export_rules— NFS export rule, e.g.*(rw)or*(rw,no_root_squash); don’t combine withpure_nfs_policybelowpure_nfs_policy/pure_nfs_server— required together for secure multi-tenancy and multi-server configurations; server must exist on the FlashBlade array (and, for secure multi-tenancy, inside theRealmdeclared inpure.json), if the NFS policy doesn’t exist PX-CSI creates one with default valuespure_nfs_export_rules_client/pure_nfs_export_rules_access— client selector (IP, subnet, hostname, or*) and access mode (no-squash,root-squash,all-squash) for NFS exports when using multi-server or secure multi-tenancypure_fb_hard_limit_enabled,pure_fb_snapshot_directory_enabled,pure_fb_nfsv3_enabled,pure_fb_nfsv4_1_enabled,pure_fb_fast_remove_directory_enabled— booleans controlling quota enforcement, the snapshot directory, NFS protocol versions, and automatic directory cleanup on PVC deletion
If traffic encryption is required, you can add the following mountOptions – xprtsec=tlstogether withj a pure_nfs_policy that references a TLS-enabled export rule (requires Purity 4.6.0+ on FlashBlade, plus nfs-utils, tlshd, and openssl on worker nodes)
Dynamic Provisioning
With dynamic provisioning, volumes are created automatically the moment a matching PVC is requested — no manual volume creation.
The workflow is always the same, regardless of backend:
- Create a
StorageClasswith backend parameters (as above). - Create a
PersistentVolumeClaimthat references theStorageClass. - PX-CSI provisions a new volume (or file system) on the array.
- Kubernetes mounts the resulting PVC into your pod.
Example PVC referencing the default FlashArray block class:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: pure-claim-block
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 20Gi
storageClassName: px-fa-direct-access
vlamy@k8s-vla-master:~$ kubectl get pvc pure-claim-block
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS VOLUMEATTRIBUTESCLASS AGE
pure-claim-block Bound pvc-30da850a-bd9e-4249-b86a-4b8d6c773b27 20Gi RWO px-fa-direct-access <unset> 119s
The following volume has been created on my FlashArray by PX-CSI :

Static Provisioning: Importing existing volumes
Sometimes the volume already exists — created outside Kubernetes. Static provisioning lets you import it instead of creating a new one, using PVC annotations. This can be very useful to migrate workloads.
The two annotations that matter:
portworx.io/pure-volume-name(required) — the name of the existing volume on the array:- FlashArray block volumes:
<realm>::<pod_name>::<volume_name>(realm and pod name are optional, only needed if the volume lives in one) - FlashArray File Services:
<pod_name>::<directory_name>(pod name optional) - FlashBlade file systems:
filesystem_name
- FlashArray block volumes:
portworx.io/pure-array-id(optional but important) — the ID of the specific array to import from, when multiple arrays are connected to the cluster
This is the definition of a PVC that will import an existing volume:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: imported-pvc
annotations:
portworx.io/pure-volume-name: "my-existing-volume"
portworx.io/pure-array-id: "<array-id>"
spec:
storageClassName: px-fa-direct-access
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 50Gi # must match the existing volume's size
Important: if a volume with the same name exists on multiple arrays and you skip
portworx.io/pure-array-id, PX-CSI may import it from any of them. Always set the array ID explicitly when more than one array is connected.
PX-CSI with FlashArray File Services (FA-File)
Block volumes cover the classic “one PVC, one pod” case, but plenty of applications need several pods reading and writing the same volume at once — shared configuration, a content directory served by multiple web front-ends, shared caches, and so on. That’s exactly what ReadWriteMany (RWX) is for, and on FlashArray it’s delivered through FlashArray File Services, an NFS layer PX-CSI provisions through the pure_fa_file backend.
This section walks through the full path: preparing FlashArray, creating the StorageClass with every relevant option — including two capabilities that landed in PX-CSI 26.3.0, multi-server support and automatic PVC size enforcement — and deploying a small nginx application with three pods sharing one PVC.
Requirements on FlashArray
Before PX-CSI can provision anything on the FA-File backend, some configuration must be done on the FlashArray (this is a one-time setup per array)
- Ensure FlashArray File Services are enabled on your FlashArray
- Configure a VIF (virtual interface) dedicated to file services traffic
- Create a File System – this is the top-level container that will host the managed directories created by PX-CSI for each PVC. The name of this File System will be used in the
storageClass - Create an NFS policy – required before PX-CSI can create any export. FlashArray ships a default nfs-simple policy with user mapping enabled, which is usually not what you want for a Kubernetes workload:
- With user mapping enabled and
AUTH_SYSsecurity (the default), anonymous users get mapped to UID/GID65534, which tends to cause permission errors when a pod’ssecurityContext.fsGroupdoesn’t match. - For a straightforward setup, either disable user mapping on
nfs-simple, or create a new policy with user mapping disabled andno_root_squashaccess.
- With user mapping enabled and
- (Optional) Create additional NFS server objects if you plan to use multi-server — see below. Purity 6.8.7+ is required for this.
For the purpose of this lab, I will create a dedicated NFS policy, “nfs-no-squash”
Once this is done, add the FlashArray’s NFSEndPoint to the pure.json file used to create the px-pure-secret (covered in the first article of this series):
{
"FlashArrays": [
{
"MgmtEndPoint": "<fa-management-endpoint>",
"APIToken": "<fa-api-token>",
"NFSEndPoint": "<nfs-endpoint-of-fa>"
}
]
}
NFSEndPointcan be a hostname, FQDN or IP address — if you use a hostname or FQDN, every Kubernetes node needs to resolve it through its own DNS configuration.
StorageClass for FA-File, with all the options
The backend type for FlashArray File Services is pure_fa_file. Here’s a StorageClass using the full set of relevant parameters:
kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
name: fa-file-sc
provisioner: pxd.portworx.com
parameters:
backend: "pure_fa_file" # required — FlashArray File Services backend
pure_nfs_policy: "nfs-no-squash" # required — must already exist on FlashArray
pure_fa_file_system: "FTF-filesystem" # required — must already exist on FlashArray
pure_nfs_endpoint: "<nfs-endpoint-of-fa>" # optional — overrides pure.json's NFSEndPoint
# pure_nfs_server: "<nfs-server-name>" # optional — target a specific NFS server object instead of _array_server (PX-CSI 26.3.0+, Purity 6.8.7+)
# pure_quota_policy: "100g_policy" # optional — pins a fixed quota policy; overrides automatic per-PVC enforcement (see below)
mountOptions:
- nfsvers=4.1 # PX-CSI defaults to 4.1 if omitted; the array's NFS policy version takes precedence if set
- proto=tcp # UDP is not supported
# - sec=krb5 # optional Kerberos auth: krb5 (auth only), krb5i (+integrity), krb5p (+encryption)
# - xprtsec=tls # optional NFS over TLS (Purity 6.10.6+, requires nfs-utils/tlshd/openssl on nodes)
reclaimPolicy: Delete
volumeBindingMode: Immediate # use WaitForFirstConsumer if combined with allowedTopologies
allowVolumeExpansion: true
Multi-server support (new in PX-CSI 26.3.0)
Until PX-CSI 26.3.0, every FA-File mount went through the array’s default _array_server, whatever StorageClass you used. As of 26.3.0, you can target a specific NFS server per StorageClass using the new pure_nfs_server parameter — the same pattern FlashBlade has had since 26.2.0:
parameters:
backend: "pure_fa_file"
pure_nfs_policy: "nfs-no-squash"
pure_fa_file_system: "FTF-filesystem"
pure_nfs_server: "fa-nfs-server-tenant-a" # server object name, not an IP address
This is useful for isolating data access per application, team, or tenant on a per-StorageClass basis — each one resolves to a different NFS server object on the array instead of sharing the same default endpoint. Requires PX-CSI 26.3.0+ and Purity 6.8.7+; the server object must already exist on FlashArray.
Automatic PVC size enforcement (new in PX-CSI 26.3.0)
Before 26.3.0, an FA-File PVC’s resources.requests.storage was little more than a label — nothing actually stopped an application from writing past it, since the directory could grow to the full capacity of the file system unless you manually attached a pure_quota_policy. PX-CSI 26.3.0 fixes this by default: for every FA-File PVC, it now automatically creates (or reuses) a quota policy sized to match the request, and attaches it to the managed directory.
The naming convention rounds the requested size up to the next whole GiB and names the policy accordingly, so PVCs that round to the same size share one policy:
| Requested size | Rounded (ceil) | Policy name | Enforced limit |
| 1 GiB | 1 GiB | px_auto_1gb | 1 GiB |
| 1.5 GiB | 2 GiB | px_auto_2gb | 2 GiB |
| 2 GiB | 2 GiB | px_auto_2gb | 2 Gib |
PX-CSI keeps this in sync automatically: expanding a PVC swaps in the policy for the new size and removes the old one if nothing else uses it, and deleting a PVC removes its auto-created policy under the same condition.
To be noticed: It’s controlled by the ENABLE_AUTO_CREATE_QUOTA_POLICY environment variable on the StorageCluster, enabled by default. To disable it cluster-wide:
apiVersion: core.libopenstorage.org/v1
kind: StorageCluster
metadata:
name: px-cluster
namespace: portworx
spec:
env:
- name: ENABLE_AUTO_CREATE_QUOTA_POLICY
value: "false"
Setting pure_quota_policy explicitly in the StorageClass overrides auto-creation — PX-CSI then validates the requested PVC size against that fixed policy instead of generating a px_auto_<size>gb one. Use this if you want several PVCs sharing one deliberately-sized policy rather than one policy per size tier.
Creating an RWX PVC
Use the following YAML file to create an RWX PVC using the fa-file-sc storageClass
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: pure-claim-fa
labels:
app: nginx
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 20Gi
storageClassName: fa-file-sc
Demo: 3 nginx pods sharing the same PVC
To actually see the RWX behavior in action, let’s deploy nginx with 3 replicas, all mounting the same pure-claim-fa PVC, using the following YAML file:
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-rwx-demo
labels:
app: nginx
spec:
replicas: 3
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx
ports:
- containerPort: 80
volumeMounts:
- name: pure-vol
mountPath: /usr/share/nginx/html
volumes:
- name: pure-vol
persistentVolumeClaim:
claimName: pure-claim-fa
vlamy@k8s-vla-master:~/px-csi$ k apply -f nginx-deployment.yaml
vlamy@k8s-vla-master:~/px-csi$ k get pods -l app=nginx -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
nginx-rwx-demo-86cd998748-llml7 1/1 Running 0 18s 192.168.107.190 k8s-vla-node03 <none> <none>
nginx-rwx-demo-86cd998748-qfftl 1/1 Running 0 18s 192.168.5.40 k8s-vla-node02 <none> <none>
nginx-rwx-demo-86cd998748-tzqbd 1/1 Running 0 18s 192.168.100.81 k8s-vla-node01 <none> <none>
Three pods, scheduled across three different nodes, all with the same FA-File volume mounted at /usr/share/nginx/html. To prove they’re really sharing the same backing storage, write a file from one pod and read it back from another:
bash
vlamy@k8s-vla-master:~/px-csi$ k exec nginx-rwx-demo-86cd998748-llml7 -- sh -c 'echo "hello from pod 1" > /usr/share/nginx/html/index.html'
vlamy@k8s-vla-master:~/px-csi$ k exec nginx-rwx-demo-86cd998748-qfftl -- cat /usr/share/nginx/html/index.html
Hello from pod 1
vlamy@k8s-vla-master:~/px-csi$ k exec nginx-rwx-demo-86cd998748-tzqbd -- cat /usr/share/nginx/html/index.html
Hello from pod 1
Both the second and third pods immediately see the file written by the first one — the RWX volume is shared between the 3 pods. This is the pattern to reuse anytime you need multiple pods (a Deployment, but also a StatefulSet or a DaemonSet) reading and writing a common data set on FlashArray.
Since this PVC requested 20Gi, PX-CSI will have auto-created and attached a px_auto_20gb quota policy behind the scenes — you can confirm the mount actually reports and enforces that size from inside any of the three pods:
bash
vlamy@k8s-vla-master:~/px-csi$ k exec nginx-rwx-demo-86cd998748-llml7 -- df -h /usr/share/nginx/html
Filesystem Size Used Avail Use% Mounted on
10.239.112.27:/px_2c8e50e7-pvc-c325038d-87f9-4d12-877c-279e8e16d078 20G 1.0M 20G 1% /usr/share/nginx/html
This is a screenshot of the PX-CSI generated Quota policy on the FlashArray:

Static provisioning: importing existing volumes
Sometimes the volume already exists — created outside Kubernetes. Static provisioning lets you import it instead of creating a new one, using PVC annotations rather than a dataSource. This can be very useful when migrating data.
The two annotations that matter:
portworx.io/pure-volume-name(required) — the name of the existing volume on the array:- FlashArray block volumes:
<realm>::<pod_name>::<volume_name>(realm and pod name are optional, only needed if the volume lives in one) - FlashArray File Services:
<pod_name>::<directory_name>(pod name optional) - FlashBlade file systems:
filesystem_name
- FlashArray block volumes:
portworx.io/pure-array-id(optional but important) — the ID of the specific array to import from, when multiple arrays are connected to the cluster
To retrieve the array-id, launch the following command:
vlamy@k8s-vla-master:~/px-csi$ px-csi list backend
Name ID Endpoint Type NFS Endpoint Realm Vlan API Token
baie6 33977677-95fe-4a4a-8685-4172b1b4f754 10.239.112.162 FlashArray 10.239.112.26 Valid
To import the volume “my-existing-volume” from Array “baie6”, create the following PVC (the size of the new PVC must exactly match the existing volume’s size.
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: imported-pvc
annotations:
portworx.io/pure-volume-name: "my-existing-volume"
portworx.io/pure-array-id: "33977677-95fe-4a4a-8685-4172b1b4f754"
spec:
storageClassName: px-fa-direct-access
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 50Gi # must match the existing volume's size
Important: if a volume with the same name exists on multiple arrays and you skip
portworx.io/pure-array-id, PX-CSI may import it from any of them. Always set the array ID explicitly when more than one array is connected.
What’s next
Now that we can get volumes into Kubernetes — dynamically or statically — the next article covers what happens after day one: resizing PVCs as applications grow, cloning volumes for dev/test, and taking application-consistent snapshots (including the newer VolumeGroupSnapshot for multi-PVC applications) with restore.
This article is part of a series on PX-CSI. Previous: PX-CSI – Introduction & Architecture. Next: Managing the Volume Lifecycle.
This article was drafted with the help of Claude (Anthropic’s AI assistant) from my own notes and lab testing — the hands-on work, demos, and technical validation against the official PX-CSI documentation are all mine.