Skip to main content

Kubernetes v1.37 'Garhwal' for Platform Operators: Storage Version Migration GA and a Stable Metrics API

9 min readDora NodaDora Noda
Share
On this page

Kubernetes v1.37, codenamed Garhwal, shipped on August 26, 2026 with 67 enhancements — 16 graduating to stable, 23 to beta, 27 entering alpha, and one deprecation. The headlines went to the flashy half of that list: gang scheduling, DRA updates, workload-aware scheduling for AI clusters. But if you operate your own fleet, the two graduations that actually change your next upgrade are the boring ones.

Here are the verdicts up front, so you can stop reading whenever you've got what you came for.

Storage Version Migration is now built into the control plane and enabled by default. The "touch every object" rewrite section of your upgrade runbook — the manual kubectl get / kubectl replace loop, or the out-of-tree kube-storage-version-migrator you deployed and forgot — can be replaced by a declarative API object with a status condition you can watch.

The resource Metrics API graduated to metrics.k8s.io/v1 with an identical surface to v1beta1. Nothing breaks and nothing must change today — but there are two verification steps to add to your runbook, because neither the HPA nor the current metrics-server release speaks v1 yet.

The rest of this post walks through each graduation, what concretely changes for a small self-hosted fleet, and the ordered checklist for a control plane that manages tenant clusters through its next chained upgrade.

Storage Version Migration, now built in and on by default

Every object in etcd is serialized in some API version — the resource's storage version. The catch that has bitten operators for years: changing a resource's storage version only affects new writes. Everything already sitting in etcd stays in the old serialization until something actively rewrites it through the API server. So when you promote a CRD and want to drop v1alpha1 from its served versions, or when you enable encryption at rest (or rotate encryption keys) and discover that existing objects are still stored unencrypted or under the old key, someone has to rewrite every affected object before the old version is truly gone.

Historically that someone was you, with a script. Cluster admins wrote kubectl get / kubectl replace loops over every instance of a resource, or deployed the out-of-tree kube-storage-version-migrator component to do the forcing. Both approaches worked, and both were tedious, hard to monitor, and easy to get subtly wrong — a half-migrated resource looks exactly like a migrated one until you drop the old version and something breaks.

v1.37 replaces all of that with a built-in API and controller. Creating a StorageVersionMigration object declares "rewrite every stored instance of this resource into the current storage version," and the in-tree StorageVersionMigrator controller does the work. Suppose you updated a CRD to use v1 as its storage version:

yaml
apiVersion: storagemigration.k8s.io/v1
kind: StorageVersionMigration
metadata:
  name: crontabs-migration
spec:
  resource:
    group: example.com
    resource: crontabs

Apply it, then watch the status the same way you'd watch any other controller-driven operation:

shell
kubectl get storageversionmigration.storagemigration.k8s.io/crontabs-migration -o yaml

A finished migration reports a Succeeded condition set to True. At that point every instance in storage is in the current storage version, and for CRDs you can narrow .status.storedVersions down to the preferred version. One sharp edge the upstream docs call out: if .status.storedVersions still lists the old version after a successful migration, the CRD itself was modified mid-migration, and you should retry before deprecating anything.

For a PaaS control plane, the practical win is bundling. Because the migration is a standard declarative object, CRD authors — including the Cluster API providers your fleet already runs — can ship the migration in the same manifest as the CRD upgrade. The runbook step changes from "run the rewrite script, then verify by sampling objects" to "apply the manifest, wait for Succeeded, then drop the old version." That is a genuine deletion of toil, not a relocation of it.

Two concrete places this lands in a self-hosted fleet's next upgrade. First, provider upgrades: CAPI infrastructure and bootstrap providers are CRD-heavy, and provider version bumps are exactly the moments when a storage version changes under you. Second, encryption: if your runbook has a key-rotation procedure that ends with "rewrite all secrets," that final step now has a first-class, monitorable primitive instead of a shell loop.

The metrics API grows up: metrics.k8s.io/v1

The resource Metrics API — the small API behind kubectl top and CPU/memory-based autoscaling — has been v1beta1 since Kubernetes v1.8, after an alpha debut in v1.6. It spent roughly a decade as the most production-proven beta API in the project: two resource types (NodeMetrics, and PodMetrics with a per-container breakdown), unchanged for years, consumed by every HPA in existence. v1.37 finally graduates it to metrics.k8s.io/v1.

The critical fact about this graduation: the v1 surface is identical to v1beta1. No renamed fields, no new fields, no change to what the CPU and memory numbers mean. This is an API-version graduation, not a behavior change. The stability guarantee is the feature — clients can now build against a version that carries Kubernetes' stable-API compatibility promise.

What changes operationally is less than you'd expect, and the exceptions matter more than the rule:

  • kubectl top already handles both. It prefers v1 when the cluster serves it and falls back to v1beta1 otherwise. No client upgrade needed.
  • The HPA controller speaks only v1beta1 in v1.37. Discovery-based selection between v1 and v1beta1 is planned but did not make this release. Whatever else you do, keep serving v1beta1 or autoscaling breaks.
  • No feature gate to flip. The API is served through the aggregation layer by your metrics implementation, typically metrics-server. For v1 to appear, your implementation must serve it and register a v1.metrics.k8s.io APIService.
  • Upstream recommends serving both versions during the transition, keeping v1beta1 available for older clients while v1 ramps.

And here is the awkward state worth knowing before you plan around v1: as of this writing, the reference implementation hasn't caught up. metrics-server v0.9.0 (July 2026) makes no mention of serving the stable version, and the project's current manifests still register only the v1beta1.metrics.k8s.io APIService. So upgrading your clusters to v1.37 does not by itself light up metrics.k8s.io/v1 — the endpoint appears when you deploy a metrics implementation that registers it. Expect that release to land separately, and until then this graduation is a promise your cluster is ready to keep, not a new endpoint you can call.

You can check what your cluster actually serves at any point with:

shell
kubectl get --raw /apis/metrics.k8s.io/ | jq .

and confirm the stable registration once your implementation supports it:

shell
kubectl get apiservice v1.metrics.k8s.io

So: does metrics-server config change? No — not today. There is nothing to reconfigure and no flag to set. The action item is verification-shaped, not config-shaped, which is exactly what the checklist below captures.

The fleet upgrade checklist

For a Cluster-API-managed fleet — a management cluster plus the tenant or workload clusters it reconciles — these two graduations slot into a chained upgrade in a specific order. Here is the runbook, assuming management cluster first:

  1. Upgrade the management cluster to v1.37 first. Storage Version Migration is enabled by default with no gate, so the controller is available immediately after the control-plane upgrade. Confirm the API exists: kubectl api-versions | grep storagemigration.
  2. Retire the out-of-tree migrator. If you deployed kube-storage-version-migrator, or your runbook carries a rewrite script for CRD storage-version changes, this is the upgrade where those go away. Replace the script step with "create the StorageVersionMigration object, wait for Succeeded."
  3. File migrations for provider CRD bumps in the same change. When a CAPI provider upgrade changes a CRD's storage version, ship the corresponding StorageVersionMigration object alongside the provider manifests — same PR, same apply — instead of treating the rewrite as a follow-up task. Then verify .status.storedVersions narrowed before anything downstream drops the old version.
  4. Fold key rotation into the same primitive. If your encryption-at-rest rotation procedure ends with a manual rewrite pass over secrets or other resources, convert that pass to a StorageVersionMigration object so rotation gets retries and a status condition instead of a hope and a shell loop.
  5. Verify the metrics API surface, but change nothing. After the upgrade, confirm /apis/metrics.k8s.io/ still serves v1beta1 (your HPA depends on it) and note whether v1 has appeared. Do not remove v1beta1, do not reconfigure metrics-server, and do not pin clients to v1 until the implementation you run registers it.
  6. Repeat per workload cluster, then audit clients. Once the fleet is on v1.37, inventory anything that talks to metrics.k8s.io directly — custom dashboards, scripts using kubectl get --raw, vendored client libraries — and confirm each tolerates the version list changing when v1 eventually appears. kubectl top already does; your hand-rolled scripts might not.

Steps 2 through 4 are the payoff: toil deleted. Steps 5 and 6 are the discipline: verify, don't assume.

What doesn't change (yet)

Worth stating plainly, because "two GAs in one release" invites over-eager cleanup:

  • No metrics-server config change is required. There is no new flag, no new chart value, no manifest edit that v1.37 demands of your metrics pipeline. When a metrics-server release serving v1 ships, the upgrade is deploying that release — and serving both versions while anything still reads v1beta1.
  • No feature gates for either graduation. SVM is on by default; the metrics v1 surface needs no gate. If your upgrade runbook has a "flip the new gates" step, neither of these belongs in it.
  • No v1beta1 removal timeline. The beta metrics API remains fully available in v1.37, and with the HPA itself still pinned to it, removal is not imminent. Treat v1beta1 as load-bearing until upstream says otherwise.
  • HPA version selection is still future work. Discovery-based v1/v1beta1 selection for the HPA controller is planned but absent from v1.37. The controller that consumes metrics most heavily is the last one to move — plan accordingly.

The quiet kind of release

Not every Kubernetes release earns its keep with new primitives. v1.37's headline features will matter to AI-heavy clusters — gang scheduling, DRA, smarter placement — but for the much larger population of teams running ordinary fleets on machines they own, the most valuable changes are two graduations that remove toil and formalize guarantees: stored-object rewrites become a declarative object with a status condition, and the metrics API your autoscaler has trusted for a decade finally carries a stable version string.

Upgrade the management cluster, delete the rewrite script, verify the metrics surface, and move on. That is the whole runbook — and that it fits in one paragraph is the point.

Running your own platform on Cluster API and owned hardware? Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on machines you own. 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