Skip to main content

VolumeAttributesClass Went GA Promising Online Volume Resize. Here's What Actually Happens on a Self-Hosted Hetzner Fleet

8 min readDora NodaDora Noda
Share
On this page

A tenant's Postgres volume runs hot. IOPS pegged, query latency climbing, and the fix on most self-hosted Kubernetes platforms today is the same as it's been for a decade: provision a bigger disk, migrate the data, cut over, and hope the maintenance window is short enough that nobody notices. Kubernetes 1.34 shipped a feature explicitly built to kill that workflow — VolumeAttributesClass, promoted to General Availability in September 2025. Change a volume's performance tier with a kubectl patch, no detach, no recreate, no migration.

That's the pitch. Here's the part the GA announcement doesn't spell out: whether it works depends entirely on the CSI driver sitting underneath your PersistentVolumeClaim, and on a Cluster-API-managed Hetzner fleet — the exact stack a self-hosted PaaS actually runs on owned hardware — the honest answer in mid-2026 is partially, and only if you've already deployed the one storage layer that bothered to implement it. Below is the concrete inventory: which drivers support it, which don't, and the specific gap in the one that does that means "in-place resize" still isn't the zero-disruption promise the API's name implies.

How VolumeAttributesClass Actually Works

VolumeAttributesClass is a cluster-scoped resource, structurally similar to a StorageClass, but for parameters a CSI driver can change on a volume that already exists rather than ones fixed at provisioning time:

yaml
apiVersion: storage.k8s.io/v1
kind: VolumeAttributesClass
metadata:
  name: bronze-qos
driverName: rbd.csi.ceph.com
parameters:
  maxReadIops: "500"
  maxWriteIops: "500"

A PersistentVolumeClaim references one via spec.volumeAttributesClassName. To change tiers, you don't edit the class in place — the parameters inside a VolumeAttributesClass are themselves immutable — you create a new class (silver-qos, with higher limits) and patch the PVC to point at it. The control plane then tracks the transition in the PVC's status: currentVolumeAttributesClassName shows what's actually applied, and modifyVolumeStatus reports Pending, InProgress, or Infeasible while the change is in flight.

Underneath, that patch triggers a CSI ControllerModifyVolume remote procedure call — new to the spec, and the whole reason this feature needed driver-level buy-in rather than shipping as a pure Kubernetes API change. The GA release added one operationally important piece beta didn't have: explicit cancellation. If a requested change turns out to be infeasible (asking for more IOPS than the backend can give), the PVC can revert to its last known-good class instead of getting stuck half-applied.

All of that is real, shipped, and stable. The question a self-hosted platform actually has to answer isn't "does the Kubernetes API support this" — it's "does my storage layer implement the other half of the contract."

The Inventory: What Actually Implements ControllerModifyVolume

The VolumeAttributesClass KEP is explicit that the feature "can only be used with storage backed by Container Storage Interface, and only where the relevant CSI driver implements the ModifyVolume API." That's not a footnote — it's the entire ballgame. Kubernetes shipped the vocabulary; each driver has to ship the translation into its own backend's API. Here's where the three CSI drivers a Hetzner-based Cluster API fleet would realistically be running actually stand, checked directly against each project's source.

Hetzner's own hcloud-csi driver: unimplemented, and arguably moot. A search of the driver's codebase turns up zero references to MODIFY_VOLUME or ControllerModifyVolume anywhere — the capability simply isn't wired up. But there's a second, more fundamental reason this gap doesn't sting as much as it sounds: Hetzner Cloud Volumes don't have IOPS or throughput tiers to modify in the first place. They're flat-performance NVMe-backed block storage — the same tens-of-thousands of read IOPS regardless of volume size, no "upgrade to premium" SKU sitting behind an API call the way EBS's gp3-to-io2 jump is. VolumeAttributesClass exists to expose tiering a backend already has. Hetzner's own volumes don't have tiering to expose.

Longhorn — the CSI driver most self-hosted Kubernetes fleets actually default to — has no support either, and not even a tracking issue. Longhorn's repositories return zero hits for VolumeAttributesClass or ControllerModifyVolume. Longhorn does let you change a volume's replica count or data locality after creation, but that lives entirely in Longhorn's own CRDs and UI — it's not wired through the Kubernetes-native VolumeAttributesClass path a platform's control plane could drive declaratively alongside everything else it manages.

Ceph-CSI (the software-defined storage layer a fleet reaches for when it actually wants tunable QoS on owned hardware): partial support, and recent. This is the one real success story. ceph-csi PR #6274, merged June 12, 2026, implements cgroup v2 QoS enforcement for RBD volumes driven by VolumeAttributesClass — exactly the maxReadIops/maxWriteIops/maxReadBps/maxWriteBps parameters shown in the YAML above. Patch a PVC from a bronze-qos class to silver-qos, and ControllerModifyVolume correctly updates the RBD image's metadata with the new limits. It's real, it's merged, and it's the only one of the three that ships anything a fleet operator can actually point a tenant's database volume at today.

The Gap Nobody's Advertising

Here's where "GA" and "production-ready for a self-hosted fleet" stop being the same claim. A GitHub thread on that same ceph-csi issue, active as recently as July 23–24, 2026 — days before this post — surfaces a problem that only shows up once someone tries to actually rely on the feature in production:

"Now that #6274 has landed cgroup v2 QoS enforcement for kernel RBD (io.max written at NodePublishVolume time), there seems to be a gap: NodeModifyVolume is not implemented, so changing a PVC's VolumeAttributesClass at runtime does not update the running pod's cgroup limits."

Walk through what that means step by step, because it's the crux of the whole "no recreate needed" promise:

  1. An operator patches the tenant's PVC: volumeAttributesClassName: bronze-qossilver-qos.
  2. ControllerModifyVolume fires and correctly updates the RBD image's metadata on the Ceph cluster — maxReadIops goes from 500 to 1000. ✅
  3. The pod that's already running against that volume keeps enforcing its cgroup io.max limits from whenever it was last mounted — because those limits were written at NodePublishVolume time, and there's no NodeModifyVolume call in the CSI spec's current node-side contract to push the update to a live mount. ✅ at the storage layer, ✗ at the pod.

The follow-up comment in that same thread — "do we have NodeModifyVolume in the csi spec?" — is a maintainer asking a question that doesn't yet have a shipped answer. The controller-side half of VolumeAttributesClass works. The node-side half that would make a change take effect on a pod that's already running does not exist yet, on the one driver in this inventory that got far enough to expose the gap at all.

The Quota Piece a Multi-Tenant Platform Actually Needs

There's a second GA-era addition worth calling out, because it matters more to a multi-tenant PaaS than to a single-tenant cluster: ResourceQuota can now scope directly to VolumeAttributesClass via a scopeSelector targeting PersistentVolumeClaim.spec.volumeAttributesClassName. That closes an obvious hole — without it, nothing stops every tenant on a shared fleet from patching their own PVC onto the highest-IOPS class the operator ever created, turning a self-service tier upgrade into an unmetered claim on a shared Ceph cluster's total I/O budget.

With the quota scope wired up, a platform can cap how many PVCs across a namespace (or the whole fleet) are allowed to reference silver-qos or higher, the same way CPU and memory requests are already capped per tenant today. That's the missing piece that turns "here's a VolumeAttributesClass your tenant could theoretically self-serve into" into something a platform operator can actually expose as a paid tier without also exposing an uncapped blast radius on shared storage — a genuinely useful, easy-to-miss detail in the GA changelog next to the more visible cancellation semantics.

What This Actually Buys a Tenant Database Volume Today

None of this makes VolumeAttributesClass vaporware — it makes it a feature with a real, honest maturity level that's easy to overstate if you only read the GA announcement.

On a Hetzner-plus-Ceph-CSI fleet in July 2026, changing a tenant's storage tier is genuinely better than the old detach-recreate-reattach cycle: there's no data migration, no new PV, no window where the volume is unmounted while a copy happens elsewhere. That part of the promise is real and shipped. What isn't yet true is "the running pod immediately sees the new limits with zero disruption." A platform still needs a deliberate step — a rolling pod restart, or waiting for the next natural reschedule — to get the RBD image's updated metadata actually enforced at the cgroup level for a workload that's mid-flight. That's a controlled, scheduled restart instead of a data migration — a real improvement in blast radius, just not the fully online resize the feature's name suggests.

Practically, that puts a self-hosted platform's honest tenant-facing promise somewhere in between "instant, zero-downtime resize" and "the old manual migration": the tier change applies immediately at the storage layer, and takes full effect on the next controlled restart of the workload — which for a stateless-ish database sidecar pattern is a much smaller ask than the alternative, but still isn't nothing. Anyone promising tenants a fully transparent, mid-request IOPS bump today, on this stack, is promising something the CSI spec doesn't yet deliver.

For a platform's own roadmap, the actionable takeaway is narrower than "adopt VolumeAttributesClass": it's "adopt Ceph-CSI/Rook if tunable per-tenant storage QoS matters, track the NodeModifyVolume gap directly, and don't build a tenant-facing self-service tier-upgrade button that assumes the change is live before the next pod restart actually happens."


Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on machines you own. Storage-tier decisions like this one are exactly the kind of infrastructure nuance a self-hosted platform has to get right on behalf of every tenant it routes traffic and data for. Star the repo on GitHub or deploy your first app today.

Related articles

Run this on infrastructure you own

bex is the open-source, AI-native Render alternative — push a git repo and get a running HTTPS service on your own machines.

Get started with bex