Kubernetes 1.37 ships storage version migration by default, retiring the hand-rolled scripts
On August 31, 2026, the storagemigration.k8s.io/v1 API and its controller reached general availability in Kubernetes 1.37. Platform teams can now deprecate old CRD versions and rotate at-rest encryption keys with a single declarative object, instead of hand-maintained kubectl get/replace scripts.
August 31, 2026. Michael Aspinwall, an engineer at Google, announced on the official blog that storage version migration (SVM) has graduated to general availability in Kubernetes 1.37. August 26, 2026. Release 1.37, codenamed Garhwal, shipped a few days earlier. The “last modified” stamp on the post confirms it is enabled by default across all v1.37 clusters. Why it matters: SVM lands on a problem every platform team has hit — how to evolve a resource’s storage schema without breaking what is already written.
The problem with stale storage versions
In Kubernetes, every stored resource is written using a specific storage version — a schema representation. The API only rewrites a resource when it is mutated. The direct consequence: when you want to change the storage version, existing objects stay frozen in their old format until they are rewritten.
The most common case is CRD promotion. When you move a custom resource from v1alpha1 to v1, new writes use v1, but objects already in the store remain serialized as v1alpha1. You cannot remove v1alpha1 from .status.storedVersions, nor drop its serving, until every single object has been rewritten. The same logic applies to encryption at rest and key rotation: existing resources stay encrypted under the old key until they are actively rewritten through the API server.
Until now, administrators had to make do with kubectl get / kubectl replace scripts, or by deploying the out-of-tree kube-storage-version-migrator component — approaches the post calls “tedious, error-prone, and difficult to monitor.”
How the built-in migration works
With 1.37, kicking off a migration is as simple as creating a declarative StorageVersionMigration object. The StorageVersionMigrator controller, now built into the control plane and enabled by default, watches these objects and automatically migrates existing resources to the default storage version for the API in question.
The manifest is minimal:
apiVersion: storagemigration.k8s.io/v1
kind: StorageVersionMigration
metadata:
name: crontabs-migration
spec:
resource:
group: example.com
resource: crontabs Apply it like any other resource:
kubectl apply -f crontabs-migration.yaml Watching and verifying a migration
The controller updates the object’s status as the migration progresses. Inspect it with:
kubectl get storageversionmigration.storagemigration.k8s.io/crontabs-migration -o yaml A successful migration reports a Succeeded condition set to True:
status:
conditions:
- type: Running
status: "False"
lastUpdateTime: "2026-08-02T10:05:00Z"
reason: StorageVersionMigrationInProgress
- type: Succeeded
status: "True"
lastUpdateTime: "2026-08-02T10:05:00Z"
reason: StorageVersionMigrationSucceeded Once Succeeded is reached, every instance of the resource is stored in the current version. For CRDs, .status.storedVersions should then be updated to contain only the preferred version. If it is not, the CRD changed during the migration — so you retry before deprecating the older version.
Bundling migration into CRD manifests
Because StorageVersionMigration is a standard declarative API, CRD authors can trigger the migration in the same manifest as their definition update. A single kubectl apply updates the CRD and migrates the existing objects:
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: crontabs.example.com
spec:
group: example.com
# Updated versions list, v1 with storage: true
---
apiVersion: storagemigration.k8s.io/v1
kind: StorageVersionMigration
metadata:
name: crontabs-migration
spec:
resource:
group: example.com
resource: crontabs That is the real paradigm shift: migration stops being operational improvisation and becomes a first-class object you version, monitor, and ship alongside the resource itself.
What it changes for platform teams
GA promotion does not change the metrics collected, but it changes what you are allowed to expect from the cluster. Three concrete benefits:
- Safe version deprecation: you remove v1alpha1 without risking objects still stored in the old schema.
- Real key rotation: at-rest encryption stops being aspirational, since migration forces the rewrite under the new key.
- No more hand-rolled scripts: no more
get/replaceloops to maintain, and no kube-storage-version-migrator to deploy.
The post invites feedback on the #sig-api-machinery Slack channel — a sign that the feature, while stable, is still in active adoption.
Why the built-in migration took so long
The need is not new. Storage version migration spent several releases in alpha, shepherded by SIG API Machinery, while teams leaned on kube-storage-version-migrator, an out-of-tree component maintained separately. The hard part is not writing one more object into etcd — it is doing so without breaking the consistency Kubernetes guarantees for its API.
When a controller rewrites objects in bulk, it walks every stored resource, decodes it in its old version, and re-encodes it in the new one. That rewrite must be idempotent, resumable after an interruption, and observable — three properties hand-rolled get/replace scripts never guaranteed. A crash mid-migration left objects half-migrated with nobody the wiser. The built-in controller brings exactly those guarantees: a per-object status, normalized conditions, and progress you can query at any time.
Adoption and precautions
GA status does not excuse you from method. Four points deserve an operator’s attention:
- One migration at a time: the controller processes objects sequentially; running several StorageVersionMigration objects in parallel against the same resource group muddies the status and can saturate the API server on very large fleets.
- Check
.status.storedVersionsafterward: if the CRD changed during the migration, the status will not update and you must retry — that is a designed case, not a failure. - Do not drop the old version too early: until the Succeeded condition is
True, removing v1alpha1 from serving breaks objects that have not yet been rewritten. - Treat migration as code: a StorageVersionMigration object gets versioned, reviewed, and shipped with the CRD — not applied by hand in an emergency.
The most concrete payoff, at bottom, is key rotation. etcd encryption at rest only protects data written after it is enabled; without migration, objects can sit encrypted under a compromised key for months. StorageVersionMigration forces their rewrite under the new key — turning a once-uncertain security operation into a declarative, verifiable step.
A worked example: retiring v1alpha1 safely
Consider a CRD that has served v1alpha1 since day one. You want to promote it to v1 and eventually remove the alpha. Before 1.37, the safe sequence was manual and fragile: flip the storage version to v1, then run a kubectl get/kubectl replace loop over every object to force re-serialization, eyeball the results, and only then prune v1alpha1 from .status.storedVersions. Skip a step and you leave objects stranded in a version the API server no longer serves — a classic cause of mysterious “resource not found” and decode errors during upgrades.
With the built-in controller, the sequence collapses to two declarative steps. Update the CRD so v1 becomes the storage version, and apply a StorageVersionMigration targeting that resource. The controller does the rewrite, surfaces progress in the object’s status, and flips Succeeded when every stored object is in v1. Only then do you prune the old version. What used to be an evening of scripting and manual verification becomes a reviewed manifest and a status you can assert on — the difference between a migration you hope worked and one you can prove worked.
The remaining sharp edges
Graduation does not mean the feature is finished — it means the API shape is frozen. Two edges remain for operators. First, the controller rewrites objects through the normal API machinery, so a migration over a huge resource class — hundreds of thousands of objects — still takes time and generates write load; schedule it against your etcd and API server capacity, not arbitrarily. Second, the built-in path only migrates what the API server can reach: objects for a CRD that has already been deleted cannot be rewritten, so prune responsibly and in the right order. Neither is a reason to avoid it; both are reasons to plan it.
Verdict
If you operate a 1.37 cluster, replace your migration scripts with the built-in controller: create a StorageVersionMigration for each CRD whose version you promote, and fold it into the CRD manifest. Less code to maintain, and an observable status end to end.
If you author CRDs, bundle a StorageVersionMigration object with every version bump. Migration becomes a shipped artifact, not a manual step that gets forgotten — and that is exactly what was missing for deprecating old versions to stop being a source of silent incidents.