はじめに
CRD の古い API バージョン (v1alpha1 など) を .status.storedVersions から外したい、あるいは暗号化キーをローテーションしたいと思っても、etcd に古いスキーマ・古い鍵のまま眠っているオブジェクトがある限り、それはできません。オブジェクトは一度書き込まれると、次に誰かが更新するまで古いストレージバージョンのまま etcd に残り続けるためです。
この「古いストレージバージョンのまま眠っているオブジェクトを、明示的に書き戻して最新化する」作業がストレージバージョンマイグレーションです。
Historically, cluster administrators and CRD authors had to rely on manual
kubectl get/kubectl replacescripts, or to deploy the out-of-treekube-storage-version-migratorcomponent 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 で
StorageVersionMigratorfeature 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/viewClusterRole に含まれていません。 クラスタ管理者以外がこの機能を使うには、専用の 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"}
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 をメンテナンスしている人には地味に嬉しい機能だと思います。
参考
本文で登場する順に並べています。
参考リンク
- Kubernetes v1.37: Storage Version Migration Enabled by Default
- Storage Versions
- Migrate Kubernetes Objects Using Storage Version Migration
- KEP-4192: Move Storage Version Migrator in-tree
- StorageVersionMigration API リファレンス
- feature-gates リファレンス (StorageVersionMigrator)
その他の参考資料
本文中では引用していませんが、あわせて参考になるスライドです。