Deploy Storage Provisioner on a Bare-Metal Kubernetes Cluster¶
Overview¶
A StorageClass is a Kubernetes API object that defines how dynamic volume provisioning works — which provisioner handles it, the reclaim policy, and the binding mode. A StorageClass cannot provision itself; a provisioner (an external controller deployed into the cluster) must exist first.
| Cluster Type | Default StorageClass | Action Required |
|---|---|---|
| minikube / kind | standard | None — ships pre-installed |
| kubeadm (bare-metal) | None | Deploy local-path-provisioner |
| EKS (AWS) | gp2 / gp3 | None — use gp3 in PVC spec |
kubeadm clusters have no StorageClass by default
A PVC deployed on a kubeadm cluster without a provisioner will stay in Pending indefinitely. No error is shown — it silently waits for a provisioner that never comes.
Prerequisites¶
- A running kubeadm-bootstrapped cluster
kubectlconfigured to communicate with the cluster- All nodes show
Readystatus:
Step 1 — Deploy the Local Path Provisioner¶
kubectl apply -f https://raw.githubusercontent.com/rancher/local-path-provisioner/v0.0.35/deploy/local-path-storage.yaml
This creates the local-path-storage namespace, deploys the provisioner controller, and automatically registers a StorageClass named local-path.
Verify both the provisioner pod and the StorageClass are ready:
Expected output:
NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE DEFAULT
local-path rancher.io/local-path Delete WaitForFirstConsumer
WaitForFirstConsumer is expected behavior
The local-path StorageClass uses WaitForFirstConsumer binding mode. A PersistentVolume is not created until a Pod that consumes the PVC is scheduled to a node. The PVC will show Pending until then — this is normal.
Step 2 — (Optional) Set local-path as the Cluster Default¶
Perform this step only if workloads should use local-path automatically when no storageClassName is specified in a PVC.
kubectl patch storageclass local-path \
-p '{"metadata": {"annotations": {"storageclass.kubernetes.io/is-default-class": "true"}}}'
Verify:
The local-path entry should now show (default) in the NAME column.
Tip
Only one StorageClass should be marked as default per cluster. If another StorageClass is already marked default, remove its annotation first:
Step 3 — Use local-path in a New PVC¶
Specify storageClassName: local-path explicitly in the PVC manifest:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: <pvc-name>
namespace: <namespace>
spec:
storageClassName: local-path
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
Apply it:
Step 4 — Fix a PVC Stuck in Pending (Existing Deployments)¶
storageClassName is immutable on a PVC
Once a PVC is created, spec.storageClassName cannot be patched or updated in place — Kubernetes will reject the request. The only way to fix a PVC with a wrong or missing StorageClass is to delete and recreate it.
Option A — Imperative (delete and recreate with correct class)¶
# 1. Export the existing PVC spec
kubectl get pvc <pvc-name> -n <namespace> -o yaml > pvc-backup.yaml
# 2. Delete the stuck PVC
kubectl delete pvc <pvc-name> -n <namespace>
# 3. Edit pvc-backup.yaml — update storageClassName to local-path,
# and remove the status block and auto-generated metadata fields
# (resourceVersion, uid, creationTimestamp, annotations added by k8s)
# 4. Recreate the PVC
kubectl apply -f pvc-backup.yaml
Option B — Declarative (Kustomize strategic merge patch)¶
A strategic merge patch updates specific fields in an existing manifest without replacing the entire object. Create a patch file that overrides only storageClassName:
# overlays/<environment>/patch-storageclass.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: <pvc-name>
namespace: <namespace>
spec:
storageClassName: local-path
Reference it in kustomization.yaml:
Apply the overlay:
When to use which option
- Use Option A when you need to fix a PVC immediately on a live cluster without a Kustomize setup.
- Use Option B when managing multi-environment deployments with Kustomize — the patch file is committed to Git and applied consistently across environments (bare-metal uses
local-path, EKS usesgp3).
Verify Final State¶
Expected: