3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Kubernetes 1.37でデフォルトで有効になったStorage Version Migrationについて

3
Last updated at Posted at 2026-10-05

はじめに

CRD の古い API バージョン (v1alpha1 など) を .status.storedVersions から外したい、あるいは暗号化キーをローテーションしたいと思っても、etcd に古いスキーマ・古い鍵のまま眠っているオブジェクトがある限り、それはできません。オブジェクトは一度書き込まれると、次に誰かが更新するまで古いストレージバージョンのまま etcd に残り続けるためです。

この「古いストレージバージョンのまま眠っているオブジェクトを、明示的に書き戻して最新化する」作業がストレージバージョンマイグレーションです。

Historically, cluster administrators and CRD authors had to rely on manual kubectl get / kubectl replace scripts, or to deploy the out-of-tree kube-storage-version-migrator component to force re-writes. These approaches were often tedious, error-prone, and difficult to monitor.

(参考: Kubernetes v1.37: Storage Version Migration Enabled by Default)

Kubernetes 1.37 でこの機能がクラスタ標準機能 (in-tree) として GA (General Availability) に昇格し、追加のデプロイなしに全クラスタでデフォルト有効になりました。この記事では、ストレージバージョンとは何か、なぜマイグレーションが必要なのか、1.37 の標準機能としての使い方と RBAC、そしてビルトインリソースとカスタムリソースそれぞれのユースケースを整理します。

対象コンポーネントとバージョン

コンポーネント バージョン リポジトリ 本記事での役割
Kubernetes v1.37.1 https://github.com/kubernetes/kubernetes StorageVersionMigrator feature gate (GA / デフォルト有効)。kube-controller-manager 内の SVM 関連コントローラ、storagemigration.k8s.io/v1 API

TL;DR

  • ストレージバージョンマイグレーションは、etcd に眠る「古いスキーマ・古い鍵のまま」のオブジェクトを、無変更の update/patch で書き戻し、最新のストレージバージョンに揃える作業です。 CRD の旧バージョン廃止や暗号化キーのローテーションの前提条件になります。
  • Kubernetes 1.37 で StorageVersionMigrator feature gate が GA になり、デフォルトで有効です。 追加のアドオンをデプロイしなくても、storagemigration.k8s.io/v1 の StorageVersionMigration リソースを作るだけでマイグレーションを開始できます。
  • 実体は kube-controller-manager 内の 3 つのコントローラです。 オブジェクトを実際に書き戻す SVM コントローラ、GC キャッシュの鮮度を確認するための resource version コントローラ、CRD の status.storedVersions を後始末する migration runner コントローラに分かれています。
  • storagemigration.k8s.io は既定の admin/edit/view ClusterRole に含まれていません。 クラスタ管理者以外がこの機能を使うには、専用の RBAC を用意する必要があります。
  • ビルトインリソースのマイグレーションと CRD のマイグレーションでは、コントローラの後処理が異なります。 CRD の場合だけ、マイグレーション完了後に CRD の status.storedVersions から旧バージョンを取り除く処理が追加で走ります。

storage-version とは、storage-version-migration する必要性

Kubernetes の API サーバーは、オブジェクトを etcd (相当のバックエンド) に保存する際、特定のバージョンでシリアライズします。この「実際に etcd 上でどう符号化されているか」を指す概念が storage version です。

The Kubernetes API server stores objects, relying on an etcd-compatible backing store (often, the backing storage is etcd itself). Each object is serialized using a particular version of that API type; for example, the v1 representation of a ConfigMap. Kubernetes uses the term storage version to describe how an object is stored in your cluster.

(参考: Storage Versions)

storage version は、クライアントが kubectl get deployment -o yaml などで受け取る API 表現とは独立した概念です。API サーバーは複数バージョン間の変換を担うため、クライアントからは v1 と v2 のどちらでオブジェクトを操作しても違いは見えませんが、その裏で「実際に 1 つだけ存在する etcd 上のバイト列」が古いバージョンのまま固定されていることがあります。

Reads from the API Server will convert the stored data to the API representation of the object. This makes it so that old storage versions can sit indefinitely as long as no updates occur to the object. Writes, on the other hand, will convert the stored object to the new representation upon update.

(参考: Storage version to resource mapping)

ここで問題になるのが、「更新が起きない限り古いバージョンのまま残り続ける」という性質です。ビルトインリソースは storage version が API バージョンと直接結び付いていないため意識する場面は少ないですが、CRD は事情が異なります。CRD では spec.versions[].storage: true で指定したバージョンのスキーマが、そのままオブジェクトの符号化として使われます。

for custom resources, a certain version of the resource must be set as the storage version. The schema defined by that specific version of the custom resource will be used as the encoding of the resource in the storage layer.

(参考: Storage versions for custom resources)

CRD の v1alpha1 を v1 に昇格させて storage: true を v1 へ切り替えても、それ以前に作られたオブジェクトは更新されるまで v1alpha1 のバイト列のまま etcd に残ります。この状態で CRD の status.storedVersions から v1alpha1 を削除してしまうと、そのオブジェクトは読み書きできなくなります。暗号化キーのローテーションでも同様で、鍵を切り替えても再書き込みが起きるまでは旧鍵で暗号化されたデータが残り、旧鍵を廃止できません。

Another important issue is the use of encryption keys [...] Since a resource must be actively in use to update the storage version, when a key rotation is done, both the old encryption key and the new encryption key must remain in use until the administrator is sure all objects have been written to at least once.

(参考: Migrating to a different storage version)

ストレージバージョンマイグレーションは、この「全オブジェクトが少なくとも一度は書き戻された」ことを保証するための仕組みです。対象リソースの全オブジェクトに対して無変更の update/patch を発行し、API サーバーに最新のストレージバージョンで再エンコードさせます。

どのような運用でこの機能が重要になるか

この必要性の大きさは、クラスタの運用形態によって変わります。長期間 in-place でマイナーバージョンを積み重ねてアップグレードしているクラスタや、独自の CRD を持つカスタムコントローラーを運用している場合は、アップグレードを重ねるたびに古い storage version やローテーション前の暗号化キーが etcd に蓄積していくため、このマイグレーションの重要性が高くなります。

一方、クラスタ自体をアップグレードごとに新規作成し、クラスタレベルで Blue/Green 的にカットオーバーする運用では、必要性が薄れる場合があります。GitOps などで宣言的にオブジェクトを新クラスタへ再作成するなら、常に最新の storage version で書き込まれるため、マイグレーションは基本的に不要です。

StorageVersionMigration の使い方、参考の YAML と RBAC のサンプル

1.37 では、StorageVersionMigration オブジェクトの作成はユーザー側の責任になっています。discovery document の変化を自動検知してマイグレーションを開始する仕組みは、in-tree 化にあたって対象外とされました。

Move the existing SVM controller logic in-tree into KCM from its original source [...] Automatic storage version migration will be deferred to the user.

(参考: KEP-4192: Move Storage Version Migrator in-tree)

StorageVersionMigrator feature gate は 1.30 で Alpha (デフォルト無効)、1.35 で Beta (デフォルト無効) を経て、1.37 で GA としてデフォルト有効になりました。API 自体も v1beta1 (1.35 で導入) から v1 (1.37 で導入、v1beta1 は同時に deprecated) へ昇格しています。

StorageVersionMigrator: {
    {Version: version.MustParse("1.30"), Default: false, PreRelease: featuregate.Alpha},
    {Version: version.MustParse("1.35"), Default: false, PreRelease: featuregate.Beta},
    {Version: version.MustParse("1.37"), Default: true, PreRelease: featuregate.GA},
},

(参考: kube_features.go#L2205-L2209、storagemigration/v1beta1/types.go#L26-L27)

つまり 1.37 での「使い方」は、アドオンをデプロイすることではなく、storagemigration.k8s.io/v1 の StorageVersionMigration オブジェクトを自分で kubectl apply することです。

apiVersion: storagemigration.k8s.io/v1
kind: StorageVersionMigration
metadata:
  name: secrets-migration
spec:
  resource:
    group: ""
    resource: secrets

(参考: Migrate Kubernetes Objects Using Storage Version Migration)

進捗は .status.conditions の Running/Succeeded/Failed で確認します。

kubectl wait --for=condition=Succeeded storageversionmigration.storagemigration.k8s.io/secrets-migration
status:
  conditions:
  - type: Running
    status: "False"
    lastUpdateTime: "2024-03-12T20:29:46Z"
    reason: StorageVersionMigrationInProgress
  - type: Succeeded
    status: "True"
    lastUpdateTime: "2024-03-12T20:29:46Z"
    reason: StorageVersionMigrationSucceeded
  resourceVersion: "84"

(参考: Migrate Kubernetes Objects Using Storage Version Migration)

RBAC のサンプル

コントローラ側の RBAC は kube-controller-manager 用の system:controller:storage-version-migrator-controller ClusterRole として組み込み済みで、StorageVersionMigrator feature gate が有効なときだけ作成されます。

if utilfeature.DefaultFeatureGate.Enabled(features.StorageVersionMigrator) {
    addControllerRole(&controllerRoles, &controllerRoleBindings, rbacv1.ClusterRole{
        ObjectMeta: metav1.ObjectMeta{
            Name: saRolePrefix + "storage-version-migrator-controller",
        },
        Rules: []rbacv1.PolicyRule{
            // need list to get current RV for any resource
            // need patch for any resource to perform migration
            rbacv1helpers.NewRule("list", "patch").Groups("*").Resources("*").RuleOrDie(),
            rbacv1helpers.NewRule("get").Groups("apiextensions.k8s.io").Resources("customresourcedefinitions").RuleOrDie(),
            rbacv1helpers.NewRule("patch").Groups("apiextensions.k8s.io").Resources("customresourcedefinitions/status").RuleOrDie(),
            rbacv1helpers.NewRule("update").Groups(storageVersionMigrationGroup).Resources("storageversionmigrations/status").RuleOrDie(),
        },
    })
}

(参考: controller_policy.go#L577-L591)

list/patch しか許可していない点が目を引きますが、これはコントローラが任意リソースを直接 watch していないためです。実際のオブジェクト一覧は kube-controller-manager の Garbage Collector が既に持っている GC キャッシュ (各リソースの watch は GC 自身の RBAC で確保済み) を再利用し、SVM コントローラは「一貫性チェック用の 1 回の list」と「書き戻しのための patch」だけを追加で必要とします。

resourceMonitor, hasSynced, errMonitor := svmc.dependencyGraphBuilder.GetMonitor(monCtx, *gvr)
// ...
allObjects := resourceMonitor.Store.List()

(参考: storageversionmigrator.go#L239-L284)

一方、ユーザー側 (StorageVersionMigration オブジェクトを作成する側) の RBAC は既定の admin/edit/view ClusterRole に含まれていません。storagemigration.k8s.io という API グループ定数は、bootstrap policy 全体を見てもコントローラ用の ClusterRole 以外に登場しません。

(参考: policy.go#L69、controller_policy.go#L588)

クラスタ管理者以外のオペレータやチームにマイグレーションの実行を任せたい場合は、次のような専用の ClusterRole を用意する必要があります。

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: storage-version-migration-editor
rules:
- apiGroups: ["storagemigration.k8s.io"]
  resources: ["storageversionmigrations"]
  verbs: ["create", "get", "list", "watch", "delete"]
- apiGroups: ["storagemigration.k8s.io"]
  resources: ["storageversionmigrations/status"]
  verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: storage-version-migration-editor-binding
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: storage-version-migration-editor
subjects:
- kind: Group
  name: platform-operators
  apiGroup: rbac.authorization.k8s.io

StorageVersionMigration はクラスタスコープのリソースなので、Role/RoleBinding ではなく ClusterRole/ClusterRoleBinding で権限を渡す必要がある点に注意が必要です。

Storage Version Migration のユースケース

StorageVersionMigration の典型的なユースケースは、暗号化キーのローテーション後に secrets のようなビルトインリソースを再暗号化する場合と、CRD の新しいバージョンを storage version に昇格させて古いバージョンの CR を書き戻す場合の 2 つに大別できます。StorageVersionMigration オブジェクトの spec.resource に指定する group/resource が違うだけで、操作方法自体は共通ですが、実装上の違いは、kube-controller-manager 側で動く 3 つのコントローラのうち、CRD の後処理を担当する 1 つが CRD の場合だけ追加で動作する点にあります。

return newControllerLoop(concurrentRun(
    svm.NewSVMController(...).Run,
    svm.NewResourceVersionController(...).Run,
    svm.NewCustomResourceController(...).Run,
), controllerName), nil

(参考: storageversionmigrator.go#L93-L117)

  • SVM コントローラ: 対象オブジェクトを 1 件ずつ patch して書き戻す、マイグレーション本体。
  • resource version コントローラ: マイグレーション開始前に「存在しない namespace への list」で現在のリソースバージョンを取得し、.status.resourceVersion に記録する。GC キャッシュがこの値より新しくなるまで待ってから本体の migration を走らせるための、鮮度チェック用のチェックポイントです。
  • migration runner コントローラ (NewCustomResourceController): 同じ GroupResource に対して複数の StorageVersionMigration が同時に走らないよう調停し、対象が CRD であれば CRD 側の状態を更新する。

ユースケース1: ビルトインリソースの場合 (secrets の再暗号化)

暗号化キーをローテーションした後、既存の Secret を新しい鍵で再暗号化したい場合を例にします。まず、KMS provider などで at rest 暗号化を設定した状態で Secret を作成します。

EncryptionConfiguration(apiVersion: apiserver.config.k8s.io/v1)は、kubectl apply で作成する通常の Kubernetes リソースではありません。secrets など、どのリソースを、どの暗号化プロバイダ (aescbc、kms など) と鍵で暗号化するかを定義する設定ファイルで、kube-apiserver に --encryption-provider-config フラグで渡して読み込ませます。apiserver.config.k8s.io は kube-apiserver 本体の内部設定 API 群 (AdmissionConfiguration、AuthenticationConfiguration など) が属するグループです。

const GroupName = "apiserver.config.k8s.io"
var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1"}

(参考: register.go、Encrypting Confidential Data at Rest)

kind: EncryptionConfiguration
apiVersion: apiserver.config.k8s.io/v1
resources:
- resources:
  - secrets
  providers:
  - aescbc:
      keys:
      - name: key1
        secret: c2VjcmV0IGlzIHNlY3VyZQ==
kubectl create secret generic my-secret --from-literal=key1=supersecret

この時点で etcd 上の my-secret は k8s:enc:aescbc:v1:key1 というプレフィックス付きで、key1 により暗号化されています。次に、暗号化キーをローテーションするため、新しい鍵 key2 を優先鍵として追加し、key1 は復号用に残す形で EncryptionConfiguration を更新します。

kind: EncryptionConfiguration
apiVersion: apiserver.config.k8s.io/v1
resources:
- resources:
  - secrets
  providers:
  - aescbc:
      keys:
      - name: key2
        secret: c2VjcmV0IGlzIHNlY3VyZSwgaXMgaXQ/
  - aescbc:
      keys:
      - name: key1
        secret: c2VjcmV0IGlzIHNlY3VyZQ==

--encryption-provider-config-automatic-reload を有効にしておけば、kube-apiserver はこの設定ファイルの変更を自動で再読み込みします。しかし、これだけでは新規作成・更新されるオブジェクトが key2 で暗号化されるようになるだけで、既存の my-secret は更新されるまで key1 のまま etcd に残ります。ここで StorageVersionMigration を使い、secrets を明示的に書き戻します。spec.resource にはビルトインリソースの group/resource を指定します (ビルトインリソースの group は "" = コアグループ)。使う YAML と進捗確認の方法は、前述の「使い方」の章で示した secrets-migration の例と同じです。

kubectl apply -f migrate-secret.yaml
kubectl wait --for=condition=Succeeded storageversionmigration.storagemigration.k8s.io/secrets-migration

マイグレーションが Succeeded になった後、my-secret を etcd から直接確認すると、プレフィックスが k8s:enc:aescbc:v1:key2 に変わっていることを確認できます。

(参考: Re-encrypt Kubernetes secrets using storage version migration)

secrets のようなビルトインリソースには対応する CRD オブジェクトが存在しないため、migration runner コントローラは CRD 検索が空振りしてそのまま抜けます。

func (crc *MigrationRunnerController) crdForGroupResource(ctx context.Context, gr metav1.GroupResource) (*apiextensionsv1.CustomResourceDefinition, bool, error) {
    crdName := fmt.Sprintf("%s.%s", gr.Resource, gr.Group)
    crd, err := crc.crdClient.Get(ctx, crdName, metav1.GetOptions{})
    if apierrors.IsNotFound(err) {
        return nil, false, nil
    }
    // ...
}

(参考: migrationrunner.go#L417-L427)

つまりビルトインリソースのマイグレーションは、SVM コントローラによる「オブジェクトの patch」と、.status.conditions の Succeeded/Failed 更新だけで完結します。暗号化キーのローテーション後に secrets を再暗号化する用途は、まさにこのケースです。

ユースケース2: カスタムリソース (CRD) の場合 (crontabs の storage version 昇格)

CRD の新しいバージョンを storage version に昇格させ、既存の CR を書き戻したい場合を例にします。次の crontabs.example.com CRD は v1beta1 を storage version としています (v1 は served: true ですが storage: false で、time フィールドは storage には反映されません)。

apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: crontabs.example.com
spec:
  group: example.com
  versions:
  - name: v1beta1
    served: true
    storage: true
    schema:
      openAPIV3Schema:
        type: object
        properties:
          host:
            type: string
          port:
            type: string
  - name: v1
    served: true
    storage: false
    schema:
      openAPIV3Schema:
        type: object
        properties:
          host:
            type: string
          port:
            type: string
          time:
            type: string
  conversion:
    strategy: None
  scope: Namespaced
  names:
    plural: crontabs
    singular: crontab
    kind: CronTab
    shortNames:
    - ct

(参考: Storage versions for custom resources)

v1 を新しい storage version に昇格させるには、storage: true/storage: false を入れ替えて CRD を更新します。

apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: crontabs.example.com
spec:
  group: example.com
  versions:
  - name: v1beta1
    served: true
    storage: false   # true から変更
    schema:
      # ...
  - name: v1
    served: true
    storage: true    # false から変更
    schema:
      # ...
  conversion:
    strategy: None
  scope: Namespaced
  names:
    plural: crontabs
    singular: crontab
    kind: CronTab
    shortNames:
    - ct

この更新だけでは、既存の crontabs オブジェクトは更新されるまで v1beta1 のバイト列のまま etcd に残ります。ここで CRD の spec.resource に group/resource (plural 名) を指定した StorageVersionMigration を作成し、既存オブジェクトを v1 へ書き戻します。

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

(参考: Kubernetes v1.37: Storage Version Migration Enabled by Default)

CRD 作者は、この StorageVersionMigration を CRD 更新後に単発で kubectl apply するだけでなく、CRD 本体の manifest と同じファイルに含めて一緒に適用することもできます。

apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: crontabs.example.com
spec:
  group: example.com
  # Updated versions list where v1 has storage: true
  ...
---
apiVersion: storagemigration.k8s.io/v1
kind: StorageVersionMigration
metadata:
  name: crontabs-migration
spec:
  resource:
    group: example.com
    resource: crontabs

(参考: Including migrations in your CRD manifests)

CRD が対象の場合は、migration runner コントローラが追加で 2 つの仕事をします。

1つ目は、マイグレーション実行中に CRD 側へ Migrating 条件を立てることです。

applyConfig := applyconfigurationapiextensionsv1.CustomResourceDefinition(crd.Name).
    WithResourceVersion(crd.ResourceVersion).
    WithStatus(applyconfigurationapiextensionsv1.CustomResourceDefinitionStatus().
        WithConditions(applyconfigurationapiextensionsv1.CustomResourceDefinitionCondition().
            WithType(apiextensionsv1.StorageMigrating).
            WithStatus(apiextensionsv1.ConditionTrue).
            // ...
            WithObservedGeneration(crd.Generation)))

(参考: migrationrunner.go#L241-L276)

2つ目は、マイグレーション成功後に CRD の status.storedVersions から古いバージョンを取り除くことです。ここで CRD の generation が記録しておいた値と一致しているかを確認しており、一致していれば storedVersions を現在の storage version 1 つだけに絞り込みます。マイグレーション中に CRD 自体が更新されていた場合は、storedVersions の書き換えをスキップして安全側に倒します。

migratingCond := apihelpers.FindCRDCondition(crd, apiextensionsv1.StorageMigrating)
var canUpdateStoredVersions bool
if migratingCond != nil && migratingCond.ObservedGeneration == crd.Generation {
    canUpdateStoredVersions = true
}
// ...
if canUpdateStoredVersions {
    // Wipe out all stored versions that we have migrated off of if nothing else
    // has transpired while the migration was in progress.
    for _, version := range crd.Spec.Versions {
        if version.Storage {
            applyConfig.Status.WithStoredVersions(version.Name)
            break
        }
    }
}

(参考: migrationrunner.go#L358-L407)

公式ドキュメントの CRD マイグレーションの例でも、成功後は対象 CRD の status.storedVersions が新しい storage version だけに絞られていることを確認する手順になっています (下記は元の例の testcrds/v2 を、ここまでの crontabs/v1 の例に合わせて示したものです)。

status:
  acceptedNames:
    kind: CronTab
    plural: crontabs
  conditions:
    - type: Established
      status: "True"
  storedVersions:
    - v1

(参考: Update the preferred storage schema of a CRD)

この storedVersions の後始末があるかどうかが、ビルトインリソースのマイグレーションと CRD のマイグレーションの実質的な違いです。CRD の場合、マイグレーションが成功して初めて、spec.versions から古いバージョンの定義を安全に削除できるようになります。

おわりに

昔からみんな欲しかったけど、特に作る人もいなかったのでなかった機能が念願の GA になりました。
クラスタを長期運用したり、カスタムコントローラーを作って対応する CRD をメンテナンスしている人には地味に嬉しい機能だと思います。

参考

本文で登場する順に並べています。

参考リンク

その他の参考資料

本文中では引用していませんが、あわせて参考になるスライドです。

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?