PX-CSI – Provisioning Storage: StorageClass, Dynamic & Static Provisioning


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

FieldPurpose
reclaimPolicyDelete or Retain – what happens to the colume when the PVC is released
volumeBindingModeImmediate or WaitForFirstConsumer (required for topology-aware provisioning)
allowVolumeExpansiontrue / false – whether the volume can be resized later
mountOptionsNFS options for NFS-backed classes (nfsvers=4.1, tcp)
parameters.backendpure_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 — true to 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 with pure_nfs_policy below
  • pure_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 the Realm declared in pure.json), if the NFS policy doesn’t exist PX-CSI creates one with default values
  • pure_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-tenancy
  • pure_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:

  1. Create a StorageClass with backend parameters (as above).
  2. Create a PersistentVolumeClaim that references the StorageClass.
  3. PX-CSI provisions a new volume (or file system) on the array.
  4. 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
  • 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)

  1. Ensure FlashArray File Services are enabled on your FlashArray
  2. Configure a VIF (virtual interface) dedicated to file services traffic
  3. 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
  4. 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_SYS security (the default), anonymous users get mapped to UID/GID 65534, which tends to cause permission errors when a pod’s securityContext.fsGroup doesn’t match.
    • For a straightforward setup, either disable user mapping on nfs-simple, or create a new policy with user mapping disabled and no_root_squash access.
  5. (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>"
    }
  ]
}

NFSEndPoint can 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 sizeRounded (ceil)Policy nameEnforced limit
1 GiB1 GiBpx_auto_1gb1 GiB
1.5 GiB2 GiBpx_auto_2gb2 GiB
2 GiB2 GiBpx_auto_2gb2 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
  • 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.

Leave a Comment