Provisioning with static storage configuration

version 470 From IBM Cloud PakĀ® foundational services version 4.7.0 and later, you can install foundational services with static storage configuration. Use static provisioning when you want complete control over a storage resource, including its exact size, type, and location, or in regulated environments where storage must be pre-approved, audited, and conformed to security policies.

Setting up static storage provisioning with a Network File System (NFS) server

  1. Create a Red Hat OpenShift Container Platform cluster.
  2. Collect the storage server and path.
    cat /etc/exports
    • The following example shows the default NFS server on an OpenShift Container Platform cluster:
      nfs:
          server: <server address> 000.00.00.000
          # Get the NFS server IP with the following command:
          # oc get svc nfs-server -n nfs-server -o jsonpath='{.spec.clusterIP}'
    • To collect the path, enter the path: /exports export path.
  3. Set up the NFS server.
    1. Create export directories for each persistent volume (PV).
      mkdir -p /exports/common-service-db-1
      mkdir -p /exports/common-service-db-1-wal
      mkdir -p /exports/common-service-db-2
      mkdir -p /exports/common-service-db-2-wal
    2. Set permissions.
      chmod 700 /exports/common-service-db-*
      chown -R 26:26 /exports/common-service-db-*
    3. Configure and reload exports.
      cat > /etc/exports << "EXPORTS"
      /exports *(rw,sync,fsid=0,crossmnt,no_subtree_check,no_root_squash,insecure)
      /exports/common-service-db-1 *(rw,sync,no_subtree_check,no_root_squash,insecure)
      /exports/common-service-db-1-wal *(rw,sync,no_subtree_check,no_root_squash,insecure)
      /exports/common-service-db-2 *(rw,sync,no_subtree_check,no_root_squash,insecure)
      /exports/common-service-db-2-wal *(rw,sync,no_subtree_check,no_root_squash,insecure)
      EXPORTS
      
      exportfs -ra
      • The fsid=0 value on the /exports path makes this path the Network File System version 4 (NFSv4) pseudo-root.
      • The crossmnt option allows for crossing into subdirectories.
      • Client paths automatically remove the /exports prefix.
  4. Predefine four PVs to support two common-service-db pods:
    • Predefine the common-service-db-1 PV with a data volume of 20 GiB.
    • Predefine the common-service-db-1-wal PV with a Write-Ahead Logging (WAL) volume of 10 GiB.
    • Predefine the common-service-db-2 PV with a data volume of 20 GiB.
    • Predefine the common-service-db-2-wal PV with a WAL volume of 10 GiB.
    The following example shows a production-ready PV configuration:
    apiVersion: v1
    kind: PersistentVolume
    metadata:
      name: common-service-db-1
    spec:
      capacity:
        storage: 20Gi
      accessModes:
        - ReadWriteOnce
      persistentVolumeReclaimPolicy: Retain
      nfs:
        server: <nfs-server address>  # for example, 000.00.00.000
        path: /common-service-db-1
      mountOptions:
        - hard
        - nfsvers=4.1
    ---
    apiVersion: v1
    kind: PersistentVolume
    metadata:
      name: common-service-db-1-wal
    spec:
      capacity:
        storage: 10Gi
      accessModes:
        - ReadWriteOnce
      persistentVolumeReclaimPolicy: Retain
      nfs:
        server: <nfs-server address>  # for example, 000.00.00.000
        path: /common-service-db-1-wal
      mountOptions:
        - hard
        - nfsvers=4.1
    ---
    apiVersion: v1
    kind: PersistentVolume
    metadata:
      name: common-service-db-2
    spec:
      capacity:
        storage: 20Gi
      accessModes:
        - ReadWriteOnce
      persistentVolumeReclaimPolicy: Retain
      nfs:
        server: <nfs-server address>  # for example, 000.00.00.000
        path: /common-service-db-2
      mountOptions:
        - hard
        - nfsvers=4.1
    ---
    apiVersion: v1
    kind: PersistentVolume
    metadata:
      name: common-service-db-2-wal
    spec:
      capacity:
        storage: 10Gi
      accessModes:
        - ReadWriteOnce
      persistentVolumeReclaimPolicy: Retain
      nfs:
        server: <nfs-server address>  # for example, 000.00.00.000
        path: /common-service-db-2-wal
      mountOptions:
        - hard
        - nfsvers=4.1
    • The following mount option configurations are available:
      • For databases, set the hard option so that NFS operations retry indefinitely on server failure.
      • Set the nfsvers=4.1 option to use NFSv4.1 for improved performance and security.
    • The following reclaim policy configurations are available:
      • For production, include the Retain option to preserve data after Persistent Volume Claim (PVC) deletion. This reclaim policy keeps the underlying storage even after the PVC is deleted so the data can be recovered or manually cleaned up.
      • Use the Delete option to automatically delete PV data when a PVC is deleted. Use this reclaim policy only when the underlying storage is disposable. Deleting the PVC permanently removes the backing volume.
    • The following NFS export option configurations are available:
      • The no_root_squash option allows root user access from NFS clients.
      • The insecure option allows connections from ports greater than 1024.
      • The no_subtree_check option improves reliability by disabling subtree checking.
      • The sync option helps ensure that data is written to the disk before writes are acknowledged.
    The following example shows a simple PV:
    kind: PersistentVolume
    apiVersion: v1
    metadata:
      name: static-storage-pv-11-new
    spec:
      capacity:
        storage: 10Gi
      nfs:
        server: 00.00.0.000
        path: /data
      accessModes:
        - ReadWriteOnce
      claimRef: # Optional: Bind to an existing PVC
        kind: PersistentVolumeClaim
        namespace: <namespace>
        name: <pvc-name>
      persistentVolumeReclaimPolicy: Delete
      volumeMode: Filesystem
    The following example shows a Rook Ceph PV:
    kind: PersistentVolume
    apiVersion: v1
    metadata:
      name: pv
    spec:
      capacity:
        storage: 10Gi
      csi:
        driver: <csi-driver.example.com>
        volumeHandle: <unique-volume-id>
        fsType: ext4
        volumeAttributes: # Optional: generic key-value attributes
          clusterID: rook-ceph
          imageFeatures: layering
          imageFormat: '2'
          imageName: <image-name>
          journalPool: replicapool
          pool: replicapool
          storage.kubernetes.io/csiProvisionerIdentity: <provisioner-identity>
        nodeStageSecretRef: # Optional: If the Container Storage Interface (CSI) driver requires secrets
          name: <csi-node-secret>
          namespace: <namespace>
        controllerExpandSecretRef:
          name: <csi-expand-secret>
          namespace: <namespace>
      accessModes:
        - ReadWriteOnce
      claimRef: # Optional: Bind to an existing PVC
        kind: PersistentVolumeClaim
        namespace: <namespace>
        name: <pvc-name>
      persistentVolumeReclaimPolicy: Delete
      volumeMode: Filesystem
    The following example shows an OpenShift Container Platform Data Foundation (ODF) PV:
    kind: PersistentVolume
    apiVersion: v1
    metadata:
      name: pv
    spec:
      capacity:
        storage: 10Gi
      csi:
        driver: <csi-driver.example.com>
        volumeHandle: <unique-volume-id>
        fsType: ext4
        volumeAttributes: # Optional: generic key-value attributes
          clusterID: openshift-storage
          imageFeatures: 'layering,deep-flatten,exclusive-lock,object-map,fast-diff'
          imageFormat: '2'
          imageName: <image-name>
          journalPool: ocs-storagecluster-cephblockpool
          pool: ocs-storagecluster-cephblockpool
          storage.kubernetes.io/csiProvisionerIdentity: <provisioner-identity>
        nodeStageSecretRef: # Optional: If the CSI driver requires secrets
          name: <csi-node-secret>
          namespace: <namespace>
        controllerExpandSecretRef:
          name: <csi-expand-secret>
          namespace: <namespace>
      accessModes:
        - ReadWriteOnce
      claimRef: # Optional: Bind to an existing PVC
        kind: PersistentVolumeClaim
        namespace: <namespace>
        name: <pvc-name>
      persistentVolumeReclaimPolicy: Delete
      storageClassName: ""
      volumeMode: Filesystem
    The following example shows a Portworx PV:
    apiVersion: v1
    kind: PersistentVolume
    metadata:
      name: testvol
    spec:
      capacity:
        storage: 10Gi
      claimRef: # Optional: Bind to an existing PVC
        apiVersion: v1
        kind: PersistentVolumeClaim
        name: <pvc-name>
        namespace: <namespace>
      accessModes:
        - ReadWriteOnce
      persistentVolumeReclaimPolicy: Retain
      portworxVolume:
        volumeID: "<px-volume-id>"
  5. Wait for the foundational services pods to run.
  6. Install ZenService. A storage class needs to be available to install ZenService. Install a storage class, such as Rook Ceph, NFS, ODF, or Portworx. Then, wait for the ZenService pods to run.

Setting up static storage provisioning with Rook Ceph

  1. Create a Red Hat OpenShift Container Platform cluster and install Rook Ceph.
  2. Collect rook-cephfs information.
    1. Run the following command to get the Ceph File System (CephFS) name:
      oc exec -n rook-ceph deploy/rook-ceph-tools -- ceph fs ls
      The output resembles the following example:
      name: myfs, metadata pool: myfs-metadata, data pools: [myfs-data0 ]
    2. Run one of the following commands to get Ceph monitor endpoints or the cluster ID.

      Get the Ceph monitor endpoints:

      oc get cm -n rook-ceph rook-ceph-mon-endpoints -o jsonpath='{.data.data}' | jq
      Get the cluster ID:
      oc get cephcluster -n rook-ceph rook-ceph -o jsonpath='{.status.ceph.fsid}'
    3. Run the following command to verify that the Container Storage Interface (CSI) secrets exist:
      oc get secret -n rook-ceph | grep csi-cephfs

      The rook-csi-cephfs-node and rook-csi-cephfs-provisioner secrets are expected.

  3. Create CephFS subvolumes.
    1. Access the Rook Ceph toolbox with the following command:
      oc exec -it -n rook-ceph deploy/rook-ceph-tools -- bash
    2. Create subvolumes for the common-service-db PostgreSQL cluster. First, set the file system name:
      export FS_NAME=myfs # The name is included in the output from the command to get the CephFS name.
      Create subvolumes for PostgreSQL data and WAL:
      ceph fs subvolume create ${FS_NAME} common-service-db-1
      ceph fs subvolume create ${FS_NAME} common-service-db-1-wal
      ceph fs subvolume create ${FS_NAME} common-service-db-2
      ceph fs subvolume create ${FS_NAME} common-service-db-2-wal
      Verify that the subvolumes were created:
      ceph fs subvolume ls ${FS_NAME}

      Get the subvolume paths and save them for PV creation:

      ceph fs subvolume getpath ${FS_NAME} common-service-db-1
      ceph fs subvolume getpath ${FS_NAME} common-service-db-1-wal
      ceph fs subvolume getpath ${FS_NAME} common-service-db-2
      ceph fs subvolume getpath ${FS_NAME} common-service-db-2-wal
      The output resembles the following example:
      /volumes/_nogroup/common-service-db-1/uuid-here
      /volumes/_nogroup/common-service-db-1-wal/uuid-here
      /volumes/_nogroup/common-service-db-2/uuid-here
      /volumes/_nogroup/common-service-db-2-wal/uuid-here
    3. Set size limits, called storage quotas, for subvolumes. Specify the size in bytes or with the proper unit, such as 21474836480 for 20 GB and 10737418240 for 10 GB.
      ceph fs subvolume resize ${FS_NAME} common-service-db-1 21474836480
      ceph fs subvolume resize ${FS_NAME} common-service-db-1-wal 10737418240
      ceph fs subvolume resize ${FS_NAME} common-service-db-2 21474836480
      ceph fs subvolume resize ${FS_NAME} common-service-db-2-wal 10737418240
      Verify that the quotas were set by checking the bytes_quota field:
      ceph fs subvolume info ${FS_NAME} common-service-db-1
      ceph fs subvolume info ${FS_NAME} common-service-db-1-wal
      ceph fs subvolume info ${FS_NAME} common-service-db-2
      ceph fs subvolume info ${FS_NAME} common-service-db-2-wal
      The output resembles the following example:
      bash-4.4$ ceph fs subvolume resize ${FS_NAME} common-service-db-1 21474836480
      [
          {
              "bytes_used": 0
          },
          {
              "bytes_quota": 21474836480
          },
          {
              "bytes_pcent": "0.00"
          }
      ]
      bash-4.4$ ceph fs subvolume info ${FS_NAME} common-service-db-1
      {
          "atime": "2026-05-22 19:47:06",
          "bytes_pcent": "0.00",
          "bytes_quota": 21474836480,
          "bytes_used": 0,
          "created_at": "2026-05-22 19:47:06",
          "ctime": "2026-05-22 19:52:44",
          "data_pool": "myfs-replicated",
          "features": [
              "snapshot-clone",
              "snapshot-autoprotect",
              "snapshot-retention"
          ],
          "gid": 0,
          "mode": 16877,
          "mon_addrs": [
              "172.30.151.57:6789",
              "172.30.18.59:6789",
              "172.30.7.232:6789"
          ],
          "mtime": "2026-05-22 19:47:06",
          "path": "/volumes/_nogroup/common-service-db-1/3691a72e-3932-4362-96ee-18d706539135",
          "pool_namespace": "",
          "state": "complete",
          "type": "subvolume",
          "uid": 0
      }
    4. Exit the toolbox with the exit command.
  4. Create a custom secret for static volumes so that the Rook operator doesn't overwrite the default rook-csi-cephfs-node secret.
    1. Get existing admin credentials from the secret that Rook manages:
      export ADMIN_ID=$(oc get secret rook-csi-cephfs-node -n rook-ceph -o jsonpath='{.data.adminID}' | base64 -d)
      export ADMIN_KEY=$(oc get secret rook-csi-cephfs-node -n rook-ceph -o jsonpath='{.data.adminKey}' | base64 -d)
    2. Display the values for verification:
      echo "Admin ID: ${ADMIN_ID}"
      echo "Admin Key: ${ADMIN_KEY}"
    3. Create a custom secret with all required fields:
      oc create secret generic cephfs-static-secret \
        --from-literal=userID="${ADMIN_ID}" \
        --from-literal=userKey="${ADMIN_KEY}" \
        --from-literal=adminID="${ADMIN_ID}" \
        --from-literal=adminKey="${ADMIN_KEY}" \
        -n rook-ceph
    4. Verify that the secret was created correctly:
      oc get secret cephfs-static-secret -n rook-ceph -o yaml
    The output resembles the following example:
    apiVersion: v1
    kind: Secret
    metadata:
      name: cephfs-static-secret
      namespace: rook-ceph
    type: Opaque
    data:
      adminID: Y3NpLWNlcGhmcy1ub2Rl
      adminKey: QVFCbFN1SnB1UlJzRUJBQTBmcXFyQ1ZRV3A2M0plS0Z5MUNMOVE9PQ==
      userID: Y3NpLWNlcGhmcy1ub2Rl
      userKey: QVFCbFN1SnB1UlJzRUJBQTBmcXFyQ1ZRV3A2M0plS0Z5MUNMOVE9PQ==
    • The Rook operator does not manage this custom secret.
    • This custom secret persists across pod restarts and operator reconciliations.
    • Use this secret in your PV definitions in the following step.
  5. Create a predefined PV pool.
    1. Create a YAML file with all PVs. Four PVs are defined to support two common-service-db pods that are running. Export the following variable:
      export DB1_PATH=$(oc exec -n ${NAMESPACE} deploy/rook-ceph-tools -- \
        ceph fs subvolume getpath ${FS_NAME} common-service-db-1)
      export DB1_WAL_PATH=$(oc exec -n ${NAMESPACE} deploy/rook-ceph-tools -- \
        ceph fs subvolume getpath ${FS_NAME} common-service-db-1-wal)
      export DB2_PATH=$(oc exec -n ${NAMESPACE} deploy/rook-ceph-tools -- \
        ceph fs subvolume getpath ${FS_NAME} common-service-db-2)
      export DB2_WAL_PATH=$(oc exec -n ${NAMESPACE} deploy/rook-ceph-tools -- \
        ceph fs subvolume getpath ${FS_NAME} common-service-db-2-wal)
        
      export STATIC_SECRET=cephfs-static-secret # This custom secret was created for static volumes.
      export NAMESPACE="rook-ceph"
      export FS_NAME="myfs"
      export CLUSTER_ID="rook-ceph"
        
    2. Create the following PV:
      apiVersion: v1
      kind: PersistentVolume
      metadata:
        name: common-service-db-1
      spec:
        capacity:
          storage: 20Gi
        accessModes:
          - ReadWriteOnce
        persistentVolumeReclaimPolicy: Retain
        csi:
          driver: rook-ceph.cephfs.csi.ceph.com
          nodeStageSecretRef:
            name: ${STATIC_SECRET}
            namespace: ${NAMESPACE}
          volumeAttributes:
            clusterID: ${CLUSTER_ID}
            fsName: ${FS_NAME}
            staticVolume: "true"
            rootPath: ${DB1_PATH}
          volumeHandle: common-service-db-1-handle
      ---
      apiVersion: v1
      kind: PersistentVolume
      metadata:
        name: common-service-db-1-wal
      spec:
        capacity:
          storage: 10Gi
        accessModes:
          - ReadWriteOnce
        persistentVolumeReclaimPolicy: Retain
        csi:
          driver: rook-ceph.cephfs.csi.ceph.com
          nodeStageSecretRef:
            name: ${STATIC_SECRET}
            namespace: ${NAMESPACE}
          volumeAttributes:
            clusterID: ${CLUSTER_ID}
            fsName: ${FS_NAME}
            staticVolume: "true"
            rootPath: ${DB1_WAL_PATH}
          volumeHandle: common-service-db-1-wal-handle
      ---
      apiVersion: v1
      kind: PersistentVolume
      metadata:
        name: common-service-db-2
      spec:
        capacity:
          storage: 20Gi
        accessModes:
          - ReadWriteOnce
        persistentVolumeReclaimPolicy: Retain
        csi:
          driver: rook-ceph.cephfs.csi.ceph.com
          nodeStageSecretRef:
            name: ${STATIC_SECRET}
            namespace: ${NAMESPACE}
          volumeAttributes:
            clusterID: ${CLUSTER_ID}
            fsName: ${FS_NAME}
            staticVolume: "true"
            rootPath: ${DB2_PATH}
          volumeHandle: common-service-db-2-handle
      ---
      apiVersion: v1
      kind: PersistentVolume
      metadata:
        name: common-service-db-2-wal
      spec:
        capacity:
          storage: 10Gi
        accessModes:
          - ReadWriteOnce
        persistentVolumeReclaimPolicy: Retain
        csi:
          driver: rook-ceph.cephfs.csi.ceph.com
          nodeStageSecretRef:
            name: ${STATIC_SECRET}
            namespace: ${NAMESPACE}
          volumeAttributes:
            clusterID: ${CLUSTER_ID}
            fsName: ${FS_NAME}
            staticVolume: "true"
            rootPath: ${DB2_WAL_PATH}
          volumeHandle: common-service-db-2-wal-handle
    3. Verify the PVs with the oc get pvcommand.

      The output resembles the following example:

      NAME                      CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS      CLAIM   STORAGECLASS   REASON   AGE
      common-service-db-1       20Gi       RWO            Retain           Available                                   1m
      common-service-db-1-wal   10Gi       RWO            Retain           Available                                   1m
      common-service-db-2       20Gi       RWO            Retain           Available                                   1m
      common-service-db-2-wal   10Gi       RWO            Retain           Available                                   1m
  6. Create PVCs.
    1. Create a YAML file with PVCs that bind to your static PVs.
      • The accessModes values must match the PV access modes.
      • The PVC storage request must either match or be less than the PV capacity.
      • The volumeName must match the PV name.
      • The storageClassName: "" empty string is required for static PV binding.
      apiVersion: v1
      kind: PersistentVolumeClaim
      metadata:
        name: common-service-db-1
        namespace: ibm-common-services  # Change this value to your target namespace.
      spec:
        accessModes:
          - ReadWriteOnce
        resources:
          requests:
            storage: 20Gi
        volumeName: common-service-db-1
        storageClassName: ""  # The empty string is required for static binding.
      ---
      apiVersion: v1
      kind: PersistentVolumeClaim
      metadata:
        name: common-service-db-1-wal
        namespace:  ibm-common-services
      spec:
        accessModes:
          - ReadWriteOnce
        resources:
          requests:
            storage: 10Gi
        volumeName: common-service-db-1-wal
        storageClassName: ""
      ---
      apiVersion: v1
      kind: PersistentVolumeClaim
      metadata:
        name: common-service-db-2
        namespace:  ibm-common-services
      spec:
        accessModes:
          - ReadWriteOnce
        resources:
          requests:
            storage: 20Gi
        volumeName: common-service-db-2
        storageClassName: ""
      ---
      apiVersion: v1
      kind: PersistentVolumeClaim
      metadata:
        name: common-service-db-2-wal
        namespace:  ibm-common-services
      spec:
        accessModes:
          - ReadWriteOnce
        resources:
          requests:
            storage: 10Gi
        volumeName: common-service-db-2-wal
        storageClassName: ""
    2. Verify the PVC binding. First, check the PVC status:
      oc get pvc -n  ibm-common-services
      In the output, all PVCs have the Bound status:
      # NAME                      STATUS   VOLUME                  CAPACITY   ACCESS MODES   STORAGECLASS   AGE
      # common-service-db-1       Bound    common-service-db-1     20Gi       RWO                           10s
      # common-service-db-1-wal   Bound    common-service-db-1-wal 10Gi       RWO                           10s
      # common-service-db-2       Bound    common-service-db-2     20Gi       RWO                           10s
      # common-service-db-2-wal   Bound    common-service-db-2-wal 10Gi       RWO                           10s
    3. Verify that the PV is bound to the PVC:
      oc get pv | grep common-service-db
  7. Install foundational services and wait for the foundational services pods to run.
  8. Install ZenService. A storage class must be available to install ZenService. Then, wait for the ZenService pods to run.