Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
静的解析 x Kubernetes API Conventions = Kube API Li...
Search
sivchari
November 20, 2025
200
0
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
静的解析 x Kubernetes API Conventions = Kube API Linter ~ ベストプラクティスに準拠したカスタムリソースの作り方と運用 ~
sivchari
November 20, 2025
More Decks by sivchari
See All by sivchari
govalid ~ Type-safe validation tool ~
sivchari
0
130
Go1.25 リリースパーティ ~ nil pointer bug ~
sivchari
0
120
Google Developer Group - DevFest Tokyo 2025
sivchari
0
130
Who tests the Tests ?
sivchari
0
150
Go 1.26 リリースパーティ
sivchari
0
200
What's GOCACHEPROG ?
sivchari
1
560
gh_extensionsによる快適なOSS生活.pdf
sivchari
0
160
Visualization Go scheduler by gosched-simulator
sivchari
1
650
protoc pluginのはじめかた
sivchari
0
160
Featured
See All Featured
Raft: Consensus for Rubyists
vanstee
141
7.5k
The agentic SEO stack - context over prompts
schlessera
0
800
No one is an island. Learnings from fostering a developers community.
thoeni
21
3.7k
Practical Tips for Bootstrapping Information Extraction Pipelines
honnibal
25
1.9k
Ethics towards AI in product and experience design
skipperchong
2
300
エンジニアに許された特別な時間の終わり
watany
107
250k
Mind Mapping
helmedeiros
PRO
1
240
Odyssey Design
rkendrick25
PRO
2
690
How STYLIGHT went responsive
nonsquared
100
6.2k
Self-Hosted WebAssembly Runtime for Runtime-Neutral Checkpoint/Restore in Edge–Cloud Continuum
chikuwait
0
570
A designer walks into a library…
pauljervisheath
211
24k
Optimizing for Happiness
mojombo
378
71k
Transcript
静的解析 x Kubernetes API Conventions = Kube API Linter ~
ベストプラクティスに準拠したカスタムリソースの作り方と運用 ~
もくじ • Cluster API と Kube API Linter について •
Kubernetes API Conventions と Kube API Linter • API 設計で考えること • 互換性を担保した API 移行方法
自己紹介 渋谷拓真 newmo株式会社 Go Conference メインオーガナイザー Individual Contributor • Kube
API Linter Approver • Cluster API Reviewer • ArgoCD Member Kubernetes 2025 Contributor Award @sivchari
Cluster API と Kube API Linter について
• Cluster API (CAPI) ◦ Kubernetes SIGs プロジェクトの 1つ (SIG
Cluster Lifecycle) ◦ CRD を使用して複数クラスタの作成、更新等の運用を容易にする
• Management Cluster ◦ Workload Cluster を管理する ◦ CAPI が動く
◦ CAPI Provider も動く • Workload Cluster ◦ Management Cluster により管理 ◦ CAPI Provider が作成する実態
• Bootstrap Provider ◦ cloud-init scriptの構築 ◦ 構築物のSecretへの保存 • Infrastructure
Provider ◦ 各ベンダーが担当 ◦ Workload Cluster のリソース管理 ▪ VM (e.g. XXX Machine) ▪ ネットワーク • Control Plane Provider ◦ ライフサイクル管理 ◦ 証明書の生成
• Kube API Linter (KAL) ◦ Kubernetes SIGs プロジェクトの 1つ
(SIG App) ◦ Kubernetes API Conventions をベースに複数のルールセットを提供 KAL API
version: v2.X.X name: gcl-kal destination: ./bin plugins: - module: sigs.k8s.io/kube-api-linter
- version: v0.0.0-YYYYMMDDHHMMSS-commithash .custom-gcl.yaml .golangci.yaml linters-settings: custom: kubeapilinter: type: module settings: linters: {} lintersConfig: {}
• 全体で 28 のルールセットがある • API Conventions は重要だが複雑 ◦ 全て把握しレビューはきつい
Kube API Linter
Kubernetes API Conventions と Kube API Linter
• Kubernetes API Conventions ◦ Kubernetes API の設計プラクティスがまとまっているドキュメント ◦ Kubernetes
の設計をするなら読んだ方がいい ◦ Kubernetes の設計をするなら 本当に読んだ方がいい ◦ Kubernetes の設計をするなら 絶対に読んだ方がいい ◦ Kubernetes の設計をするなら お願いなので 読んでください
• Optional vs. Required ◦ Kubernetes API は必ず Optional または
Required になる ◦ Optional は以下のルールに従うべき ▪ +optional マーカーを持つ ▪ ポインタ型または built-in で nil を持つならそれを使う (map/slice) ▪ map/slice のゼロ値を見分けたい場合はポインタ型にする ▪ omitempty を持つ ◦ Required は以下のルールに従うべき ▪ 明示的に +required マーカーを持つ ▪ ゼロ値が有効であるなら例外的にポインタにしてもいい • 必須だけど空文字も有効な値として扱うなど
Cluster API v1beta1 type ClusterSpec struct { // +optional ClusterNetwork
*ClusterNetwork `json:"clusterNetwork,omitempty"` // +optional // +kubebuilder:validation:MinLength=1 // +kubebuilder:validation:MaxLength=256 FailureDomain *string `json:"failureDomain,omitempty"` } optional だからポインタ optional だからポインタ
if ClusterSpec.ClusterNetwork != nil {} if ClusterSpec.FailureDomain != nil &&
ClusterSpec.FailureDomain != “” {} Or If ptr.Deref(ClusterSpec.FailureDomain, “”) != “” {} nil に対する分岐と panic のリスク
• Serialization of optional/required field ◦ optional と required どちらにするか決める
• 具体的な例が参考になる ◦ bool は常にポインタにするべき ◦ 構造体をゼロ値にできる条件 ▪ 必須フィールドを持たない ▪ MinProperties がある • 若干整理されきってないところがある ◦ 複合型のゼロ値は *[]T とする ▪ omitzero で今はいいよね? ▪ 絶賛議論中
Cluster API v1beta2 type ClusterSpec struct { // +optional ClusterNetwork
ClusterNetwork `json:"clusterNetwork,omitempty,omitzero"` // +optional // +kubebuilder:validation:MinLength=1 // +kubebuilder:validation:MaxLength=256 FailureDomain string `json:"failureDomain,omitempty"` } 全部optionalならomitzeroを使用すれば OK MinLength=1なので空文字はエラー
• optionalfields ルール ◦ +optional や omitempty, omitzero, kubebuilder marker
を含めて静的解 析 ◦ SuggestedFix を搭載している ▪ golangci-lint -fix でコードを自動修正する ◦ 設定で警告のみにする等制御可能 Kube API Linter
• Lists of named subobjects preferred over maps ◦ map
型は使わないで同様の表現は struct で表現する ◦ JSON/YAML のキーがフィールドと一致する一貫性 ◦ +listType も使えるようになるため SSA を活用できる ports: - name: www containerPort:80 ports: www: containerPort:80 ✅ ❌
• nomaps ルール ◦ map 型の使用を検出する ◦ 例外として map[string]string のみ許容することもできる
▪ Label/Annotation などは例外として認められているため Kube API Linter
• リストを扱うのであれば必ず +listType をつける • デフォルトだと atomic のためリスト全体を置き換える • 意図していない場合永遠に
Reconciliation Loop が走るかも X Controller Y Controller fieldA: XXX fieldB: YYY fieldC: ZZZ
• ssatags ルール ◦ リストフィールドで +listType を保持していないと報告 ◦ []byte への
+listType を報告 ◦ +listType=map が built-in (e.g. []string) のスライスについていると報告 ◦ +listType=map が +listMapKey を持っていないと報告 ◦ +listMapKey に存在しないフィールドを指定すると報告 ◦ +listType=set が struct についていると報告 ▪ struct を set で使用することはできない atomic のみ可能 ▪ +listType=atomic で良く多くのケースは +listType=map を推奨 Kube API Linter
API 設計で考えること
• Spec はユーザーの要望 • k/k の API であっても外部 API の型を再利用しない
• 型の共有はしない • 過剰な抽象化、具象化されたフィールド名を疑う
• Spec はユーザーの要望 • k/k の API であっても外部 API の型を再利用しない
• 型の共有はしない • 過剰な抽象化、具象化されたフィールド名を疑う
• Spec はユーザーがその API のあるべき形を定義したもの • コントローラーによって Spec を書き換えることは絶対に NG
• 型のゼロ値がコントローラーにとって有効かどうかを考える • デフォルト値を使ってもいいが、 default marker は使わないようにする ◦ +default:value=XXX and +kubebuilder:default:=XXX • ユーザーが意図する内容を一貫して保持できるようにする
• ユーザーが意図する内容を一貫して保持できるようにする • Typed Client と Unstructured Client の互換性を考える //
+optional Foo string `json:”foo,omitempty”` • Typed Client ◦ 空文字 は無効な値となる ◦ foo: “” は保存されない • Unstructured Client ◦ 空文字 は有効な値となる ◦ foo: “” は保存できる • Typed/Unstructured のマーシャリン グの不整合
• ユーザーが意図する内容を一貫して保持できるようにする • Typed Client と Unstructured Client の互換性を考える //
+optional Foo *string `json:”foo,omitempty”` • Typed Client ◦ 空文字 は有効な値となる ◦ foo: “” は保存できる • Unstructured Client ◦ 空文字 は有効な値となる ◦ foo: “” は保存できる
• ユーザーが意図する内容を一貫して保持できるようにする • Typed Client と Unstructured Client の互換性を考える //
+optional //+kubebuilder:validation:MinLength:=1 Foo string `json:”foo,omitempty”` • Typed Client ◦ 空文字 は無効な値となる ◦ foo: “” は保存できない • Unstructured Client ◦ 空文字 は無効な値となる ◦ foo: “” は保存できない
• Spec はユーザーの要望 • k/k の API であっても外部 API の型を再利用しない
• 型の共有はしない • 過剰な抽象化、具象化されたフィールド名を疑う
type ClusterSpec struct { // +optional ControlPlaneRef *corev1.ObjectReference } •
API の互換性 ◦ ObjectReference の変更 ◦ バージョン追従 • 過剰な Spec ◦ CAPI が使いたい ▪ Kind ▪ Name ▪ APIVersion Cluster API v1beta1 type ObjectReference struct { Kind Namespace Name UID APIVersion ResourceVersion FieldPath }
type ClusterSpec struct { // +optional ControlPlaneRef ContractVersionedObjectReference } •
不要なフィールドは削除 • 必要に応じてコピー & ペースト Cluster API v1beta2 type ContractVersionedObjectReference struct { Kind Name APIGroup }
• Spec はユーザーの要望 • k/k の API であっても外部 API の型を再利用しない
• 型の共有はしない • 過剰な抽象化、具象化されたフィールド名を疑う
type Machine struct { Spec MachineSpec } Cluster API type
MachineTemplateSpec struct { Spec MachineSpec } type MachineSpec struct { Spec MachineSpec } type MachineDeploymentSpec struct { Template MachineTemplateSpec } // MachineSet と MachinePool も同様
• Spec はユーザーの要望 • k/k の API であっても外部 API の型を再利用しない
• 型の共有はしない • 過剰な抽象化、具象化されたフィールド名を疑う
type MachineDeploymentSpec struct { RolloutAfter *metav1.Time Strategy *MachineDeploymentStrategy } •
両方 Rollout に関係する • 利用者から見ると理解しづらい • RolloutStrategy に命名を変える? Cluster API v1beta1
type MachineDeploymentSpec struct { Rollout MachineDeploymentRolloutSpec } type MachineDeploymentRolloutSpec struct {
After metav1.Time Strategy MachineDeploymentRolloutStrategy } • Rollout でまとめる • 上から下に読めるか Cluster API v1beta2 Rollout は RollingUpdate で 5分後に実行する
互換性を担保した API 移行方法
• どういう時に移行するか • Deprecated の活用 そして Fuzzing と Conversion Test
• 移行の具体的なステップ
• どういう時に移行するか • Deprecated の活用 そして Fuzzing と Conversion Test
• 移行の具体的なステップ
• 公開した API は例え自分たちにしか使用されていない場合でも責任を持つ • API の設計が全て、実装はあとからでもどうにでもなる • 型や構造の変化は API
を変更する必要があることを認識する • 互換性の責任 ( v1beta1 ↔ v1beta2) を持つ
• どういう時に移行するか • Deprecated の活用 そして Fuzzing と Conversion Test
• 移行の具体的なステップ
• 廃止するフィールドを Deprecated に集約して数バージョン保持する type ClusterStatus struct { Conditions Conditions
} type Conditions []Condition type Condition struct {} Cluster API v1beta1 • Condition を独自定義 • metav1.Condition に移行したい ◦ API Conventions 推奨
Cluster API v1beta2 type ClusterStatus struct { Conditions []metav1.Condition Deprecated
*ClusterDeprecatedStatus } type ClusterDeprecatedStatus { V1Beta1 *ClusterV1Beta1DeprecatedStatus } type ClusterV1Beta1DeprecatedStatus { Conditions Conditions }
• Kubernetes では conversion-gen というツールがある • 最新バージョンを +kubebuilder:storageversion とする •
それ以外は +kubebuilder:deprecatedversion とする // v1beta1 // +kubebuilder:deprecatedversion type Cluster struct {} // v1beta2 // +kubebuilder:storageversion type Cluster struct {}
• conversion-gen は Hub / Spoke というメソッドを用いて相互変換する ◦ バージョン間変換を直接定義していくとパターン数が増える ◦
Hub を用意することでそのバージョンを中心として変換を行う
• Hub の定義をする func (*Cluster) Hub() {}
// v1beta1 から v1beta2 への変換関数 func (src *Cluster) ConvertTo(dstRaw conversion.Hub)
error // v1beta2 から v1beta1 への変換関数 func (dst *Cluster) ConvertFrom(srcRaw conversion.Hub) error • Spoke の定義をする
func (src *Cluster) ConvertTo(dstRaw conversion.Hub) error { restored := &clusterv1.Cluster{}
ok, err := utilconversion.UnmarshalData(src, restored) if err != nil { return err } } func (dst *Cluster) ConvertFrom(srcRaw conversion.Hub) error { return utilconversion.MarshalData(src, dst) } • utilconversion を使用して存在しないフィールドは Annotation に保持する
• Fuzzing test を書く • github.com/sivchari/utilconversion もあるよ func TestFuzzyConversion(t *testing.T)
{ t.Run("for Cluster", utilconversion.FuzzTestFunc(utilconversion.FuzzTestFuncInput{ Hub: &clusterv1.Cluster{}, Spoke: &Cluster{}, FuzzerFuncs: []fuzzer.FuzzerFuncs{ClusterFuzzFuncs}, })) }
• Fuzzing test を書く (CAPI だと1万回実行) spokeBefore := input.Spoke.DeepCopyObject().(conversion.Convertible) fuzzer.Fill(spokeBefore)
hubCopy := input.Hub.DeepCopyObject().(conversion.Hub) g.Expect(spokeBefore.ConvertTo(hubCopy)).To(gomega.Succeed()) spokeAfter := input.Spoke.DeepCopyObject().(conversion.Convertible) g.Expect(spokeAfter.ConvertFrom(hubCopy)).To(gomega.Succeed()) if !apiequality.Semantic.DeepEqual(spokeBefore, spokeAfter) {}
• どういう時に移行するか • Deprecated の活用 そして Fuzzing と Conversion Test
• 移行の具体的なステップ
• API を丸ごとコピーしてリネーム & Convertion test の用意 ◦ v1beta1 をコピーして
v1beta2 にリネーム ◦ v1beta1 を deprecatedversion 、v1beta2 を stroaged version ◦ v1beta1 に ConvertTo / ConvertFrom を用意 func TestFuzzyConversion(t *testing.T) { t.Run("for Cluster", utilconversion.FuzzTestFunc(utilconversion.FuzzTestFuncInput{ Hub: &clusterv1.Cluster{}, Spoke: &Cluster{}, })) }
• Deprecated するフィールドと代替フィールドの追加 (粒度細かく ) type ClusterStatus struct { Conditions
[]metav1.Condition Deprecated *ClusterDeprecatedStatus } type ClusterDeprecatedStatus { V1Beta1 *ClusterV1Beta1DeprecatedStatus } type ClusterV1Beta1DeprecatedStatus { Conditions Conditions }
• 新しい API 構造の適用 ◦ 構造の変更、新規フィールドの追加 type MachineDeploymentSpec struct {
RolloutAfter *metav1.Time Strategy *MachineDeploymentStrategy } type MachineDeploymentSpec struct { Rollout MachineDeploymentRolloutSpec } type MachineDeploymentRolloutSpec struct { After metav1.Time Strategy MachineDeploymentRolloutStrategy }
• Deprecated フィールドを実装から全て消す if c.Status.Deprecated.XXXV1Beta1 != nil { // do
something } if c.Status.XXX != nil { // do something }
まとめ • API Conventions を読もう • Kube API Linter を導入していい設計を目指そう
• 互換性をシステムとして担保して API バージョンは移行しよう