← All TILs · kubernetes

RKE2 ships no default StorageClass, and a PVC will sit Pending forever

kubernetes - 2026-09-18

Seventh entry in the RKE2/Kubernetes series. The CNI entry covered something RKE2 bundles and makes permanent; this one covers the opposite — a thing it deliberately doesn't bundle at all, where the failure mode is silence rather than an error.

The symptom

Apply a PersistentVolumeClaim to a fresh single-node RKE2 cluster:

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: data
spec:
  accessModes: ["ReadWriteOnce"]
  resources:
    requests:
      storage: 1Gi
kubectl get pvc
# NAME   STATUS    VOLUME   CAPACITY   ACCESS MODES   STORAGECLASS   AGE
# data   Pending                                                     3m

Pending, with an empty STORAGECLASS column, indefinitely — and any Pod mounting it is Pending too, because the scheduler won't place a Pod whose volume can't be satisfied. Nothing errors out, nothing times out. There's simply no provisioner listening, and nothing in the cluster is obliged to tell you that.

RKE2 genuinely ships nothing here

This is worth stating carefully because it's easy to assume otherwise, and the assumption is the trap. Three independent checks, all pointing the same way:

The k3s assumption that causes this

If this surprises you, it's probably because k3s behaves the opposite way, and the two get mentally merged. k3s ships manifests/local-storage.yaml — Rancher's local-path-provisioner — and that manifest carries:

annotations:
  storageclass.kubernetes.io/is-default-class: "true"

That annotation is the entire difference. A PVC that names no storageClassName gets the default class if one is marked default, and gets nothing at all if none is. On k3s a bare PVC binds within seconds; on RKE2 the identical YAML hangs forever.

sequenceDiagram
    actor You
    participant API as kube-apiserver
    participant Prov as Dynamic provisioner

    You->>API: kubectl apply -f pvc.yaml
    API->>API: no storageClassName set, look for a default StorageClass
    alt A StorageClass is annotated is-default-class (k3s, or RKE2 after you install one)
        API->>Prov: provision a volume for this claim
        Prov-->>API: PV created and bound
        API-->>You: PVC Bound
    else No default StorageClass exists (stock RKE2)
        API-->>You: PVC stays Pending, no error, no timeout
    end

What to actually install

Checking first is one command, and an empty result is the whole diagnosis:

kubectl get storageclass
# No resources found

From there it depends on what the cluster is for:

Whichever you pick, mark it default explicitly unless you want every PVC to name its class:

kubectl patch storageclass local-path \
  -p '{"metadata":{"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}'

The general shape of this

Both of the last two entries are the same lesson from opposite directions. The CNI is bundled, opinionated, and permanent. Storage is absent, unopinionated, and entirely yours. "Enterprise-ready distribution" in RKE2's own framing turns out to mean fewer defaults, not more — it declines to guess where guessing wrong would be expensive, and the cost of that is failure modes that look like nothing happening.

Where this series goes next

Ingress — which is bundled, and which just changed its default underneath everyone.

Created 2026-09-18T02:28:41+02:00 · Edit