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
250
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
go test を速くする
sivchari
3
1.1k
govalid ~ Type-safe validation tool ~
sivchari
0
170
Go1.25 リリースパーティ ~ nil pointer bug ~
sivchari
0
170
Google Developer Group - DevFest Tokyo 2025
sivchari
0
170
Who tests the Tests ?
sivchari
0
210
Go 1.26 リリースパーティ
sivchari
0
250
What's GOCACHEPROG ?
sivchari
1
610
gh_extensionsによる快適なOSS生活.pdf
sivchari
0
200
Visualization Go scheduler by gosched-simulator
sivchari
1
700
Featured
See All Featured
Navigating Weather and Climate Data
rabernat
0
490
Design of three-dimensional binary manipulators for pick-and-place task avoiding obstacles (IECON2024)
konakalab
0
550
How to Think Like a Performance Engineer
csswizardry
28
2.7k
Building a A Zero-Code AI SEO Workflow
portentint
PRO
0
690
The World Runs on Bad Software
bkeepers
PRO
72
12k
Why Our Code Smells
bkeepers
PRO
340
58k
The Pragmatic Product Professional
lauravandoore
37
7.4k
How to Align SEO within the Product Triangle To Get Buy-In & Support - #RIMC
aleyda
2
1.8k
The Success of Rails: Ensuring Growth for the Next 100 Years
eileencodes
47
8.3k
Performance Is Good for Brains [We Love Speed 2024]
tammyeverts
12
1.8k
Claude Code どこまでも/ Claude Code Everywhere
nwiizo
67
57k
Kristin Tynski - Automating Marketing Tasks With AI
techseoconnect
PRO
0
480
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 バージョンは移行しよう