Slide 1

Slide 1 text

静的解析 x Kubernetes API Conventions = Kube API Linter ~ ベストプラクティスに準拠したカスタムリソースの作り方と運用 ~

Slide 2

Slide 2 text

もくじ ● Cluster API と Kube API Linter について ● Kubernetes API Conventions と Kube API Linter ● API 設計で考えること ● 互換性を担保した API 移行方法

Slide 3

Slide 3 text

自己紹介 渋谷拓真 newmo株式会社 Go Conference メインオーガナイザー Individual Contributor ● Kube API Linter Approver ● Cluster API Reviewer ● ArgoCD Member Kubernetes 2025 Contributor Award @sivchari

Slide 4

Slide 4 text

Cluster API と Kube API Linter について

Slide 5

Slide 5 text

● Cluster API (CAPI) ○ Kubernetes SIGs プロジェクトの 1つ (SIG Cluster Lifecycle) ○ CRD を使用して複数クラスタの作成、更新等の運用を容易にする

Slide 6

Slide 6 text

● Management Cluster ○ Workload Cluster を管理する ○ CAPI が動く ○ CAPI Provider も動く ● Workload Cluster ○ Management Cluster により管理 ○ CAPI Provider が作成する実態

Slide 7

Slide 7 text

● Bootstrap Provider ○ cloud-init scriptの構築 ○ 構築物のSecretへの保存 ● Infrastructure Provider ○ 各ベンダーが担当 ○ Workload Cluster のリソース管理 ■ VM (e.g. XXX Machine) ■ ネットワーク ● Control Plane Provider ○ ライフサイクル管理 ○ 証明書の生成

Slide 8

Slide 8 text

● Kube API Linter (KAL) ○ Kubernetes SIGs プロジェクトの 1つ (SIG App) ○ Kubernetes API Conventions をベースに複数のルールセットを提供 KAL API

Slide 9

Slide 9 text

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: {}

Slide 10

Slide 10 text

● 全体で 28 のルールセットがある ● API Conventions は重要だが複雑 ○ 全て把握しレビューはきつい Kube API Linter

Slide 11

Slide 11 text

Kubernetes API Conventions と Kube API Linter

Slide 12

Slide 12 text

● Kubernetes API Conventions ○ Kubernetes API の設計プラクティスがまとまっているドキュメント ○ Kubernetes の設計をするなら読んだ方がいい ○ Kubernetes の設計をするなら 本当に読んだ方がいい ○ Kubernetes の設計をするなら 絶対に読んだ方がいい ○ Kubernetes の設計をするなら お願いなので 読んでください

Slide 13

Slide 13 text

● Optional vs. Required ○ Kubernetes API は必ず Optional または Required になる ○ Optional は以下のルールに従うべき ■ +optional マーカーを持つ ■ ポインタ型または built-in で nil を持つならそれを使う (map/slice) ■ map/slice のゼロ値を見分けたい場合はポインタ型にする ■ omitempty を持つ ○ Required は以下のルールに従うべき ■ 明示的に +required マーカーを持つ ■ ゼロ値が有効であるなら例外的にポインタにしてもいい ● 必須だけど空文字も有効な値として扱うなど

Slide 14

Slide 14 text

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 だからポインタ

Slide 15

Slide 15 text

if ClusterSpec.ClusterNetwork != nil {} if ClusterSpec.FailureDomain != nil && ClusterSpec.FailureDomain != “” {} Or If ptr.Deref(ClusterSpec.FailureDomain, “”) != “” {} nil に対する分岐と panic のリスク

Slide 16

Slide 16 text

● Serialization of optional/required field ○ optional と required どちらにするか決める ● 具体的な例が参考になる ○ bool は常にポインタにするべき ○ 構造体をゼロ値にできる条件 ■ 必須フィールドを持たない ■ MinProperties がある ● 若干整理されきってないところがある ○ 複合型のゼロ値は *[]T とする ■ omitzero で今はいいよね? ■ 絶賛議論中

Slide 17

Slide 17 text

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なので空文字はエラー

Slide 18

Slide 18 text

● optionalfields ルール ○ +optional や omitempty, omitzero, kubebuilder marker を含めて静的解 析 ○ SuggestedFix を搭載している ■ golangci-lint -fix でコードを自動修正する ○ 設定で警告のみにする等制御可能 Kube API Linter

Slide 19

Slide 19 text

● Lists of named subobjects preferred over maps ○ map 型は使わないで同様の表現は struct で表現する ○ JSON/YAML のキーがフィールドと一致する一貫性 ○ +listType も使えるようになるため SSA を活用できる ports: - name: www containerPort:80 ports: www: containerPort:80 ✅ ❌

Slide 20

Slide 20 text

● nomaps ルール ○ map 型の使用を検出する ○ 例外として map[string]string のみ許容することもできる ■ Label/Annotation などは例外として認められているため Kube API Linter

Slide 21

Slide 21 text

● リストを扱うのであれば必ず +listType をつける ● デフォルトだと atomic のためリスト全体を置き換える ● 意図していない場合永遠に Reconciliation Loop が走るかも X Controller Y Controller fieldA: XXX fieldB: YYY fieldC: ZZZ

Slide 22

Slide 22 text

● 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

Slide 23

Slide 23 text

API 設計で考えること

Slide 24

Slide 24 text

● Spec はユーザーの要望 ● k/k の API であっても外部 API の型を再利用しない ● 型の共有はしない ● 過剰な抽象化、具象化されたフィールド名を疑う

Slide 25

Slide 25 text

● Spec はユーザーの要望 ● k/k の API であっても外部 API の型を再利用しない ● 型の共有はしない ● 過剰な抽象化、具象化されたフィールド名を疑う

Slide 26

Slide 26 text

● Spec はユーザーがその API のあるべき形を定義したもの ● コントローラーによって Spec を書き換えることは絶対に NG ● 型のゼロ値がコントローラーにとって有効かどうかを考える ● デフォルト値を使ってもいいが、 default marker は使わないようにする ○ +default:value=XXX and +kubebuilder:default:=XXX ● ユーザーが意図する内容を一貫して保持できるようにする

Slide 27

Slide 27 text

● ユーザーが意図する内容を一貫して保持できるようにする ● Typed Client と Unstructured Client の互換性を考える // +optional Foo string `json:”foo,omitempty”` ● Typed Client ○ 空文字 は無効な値となる ○ foo: “” は保存されない ● Unstructured Client ○ 空文字 は有効な値となる ○ foo: “” は保存できる ● Typed/Unstructured のマーシャリン グの不整合

Slide 28

Slide 28 text

● ユーザーが意図する内容を一貫して保持できるようにする ● Typed Client と Unstructured Client の互換性を考える // +optional Foo *string `json:”foo,omitempty”` ● Typed Client ○ 空文字 は有効な値となる ○ foo: “” は保存できる ● Unstructured Client ○ 空文字 は有効な値となる ○ foo: “” は保存できる

Slide 29

Slide 29 text

● ユーザーが意図する内容を一貫して保持できるようにする ● Typed Client と Unstructured Client の互換性を考える // +optional //+kubebuilder:validation:MinLength:=1 Foo string `json:”foo,omitempty”` ● Typed Client ○ 空文字 は無効な値となる ○ foo: “” は保存できない ● Unstructured Client ○ 空文字 は無効な値となる ○ foo: “” は保存できない

Slide 30

Slide 30 text

● Spec はユーザーの要望 ● k/k の API であっても外部 API の型を再利用しない ● 型の共有はしない ● 過剰な抽象化、具象化されたフィールド名を疑う

Slide 31

Slide 31 text

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 }

Slide 32

Slide 32 text

type ClusterSpec struct { // +optional ControlPlaneRef ContractVersionedObjectReference } ● 不要なフィールドは削除 ● 必要に応じてコピー & ペースト Cluster API v1beta2 type ContractVersionedObjectReference struct { Kind Name APIGroup }

Slide 33

Slide 33 text

● Spec はユーザーの要望 ● k/k の API であっても外部 API の型を再利用しない ● 型の共有はしない ● 過剰な抽象化、具象化されたフィールド名を疑う

Slide 34

Slide 34 text

type Machine struct { Spec MachineSpec } Cluster API type MachineTemplateSpec struct { Spec MachineSpec } type MachineSpec struct { Spec MachineSpec } type MachineDeploymentSpec struct { Template MachineTemplateSpec } // MachineSet と MachinePool も同様

Slide 35

Slide 35 text

● Spec はユーザーの要望 ● k/k の API であっても外部 API の型を再利用しない ● 型の共有はしない ● 過剰な抽象化、具象化されたフィールド名を疑う

Slide 36

Slide 36 text

type MachineDeploymentSpec struct { RolloutAfter *metav1.Time Strategy *MachineDeploymentStrategy } ● 両方 Rollout に関係する ● 利用者から見ると理解しづらい ● RolloutStrategy に命名を変える? Cluster API v1beta1

Slide 37

Slide 37 text

type MachineDeploymentSpec struct { Rollout MachineDeploymentRolloutSpec } type MachineDeploymentRolloutSpec struct { After metav1.Time Strategy MachineDeploymentRolloutStrategy } ● Rollout でまとめる ● 上から下に読めるか Cluster API v1beta2 Rollout は RollingUpdate で 5分後に実行する

Slide 38

Slide 38 text

互換性を担保した API 移行方法

Slide 39

Slide 39 text

● どういう時に移行するか ● Deprecated の活用 そして Fuzzing と Conversion Test ● 移行の具体的なステップ

Slide 40

Slide 40 text

● どういう時に移行するか ● Deprecated の活用 そして Fuzzing と Conversion Test ● 移行の具体的なステップ

Slide 41

Slide 41 text

● 公開した API は例え自分たちにしか使用されていない場合でも責任を持つ ● API の設計が全て、実装はあとからでもどうにでもなる ● 型や構造の変化は API を変更する必要があることを認識する ● 互換性の責任 ( v1beta1 ↔ v1beta2) を持つ

Slide 42

Slide 42 text

● どういう時に移行するか ● Deprecated の活用 そして Fuzzing と Conversion Test ● 移行の具体的なステップ

Slide 43

Slide 43 text

● 廃止するフィールドを Deprecated に集約して数バージョン保持する type ClusterStatus struct { Conditions Conditions } type Conditions []Condition type Condition struct {} Cluster API v1beta1 ● Condition を独自定義 ● metav1.Condition に移行したい ○ API Conventions 推奨

Slide 44

Slide 44 text

Cluster API v1beta2 type ClusterStatus struct { Conditions []metav1.Condition Deprecated *ClusterDeprecatedStatus } type ClusterDeprecatedStatus { V1Beta1 *ClusterV1Beta1DeprecatedStatus } type ClusterV1Beta1DeprecatedStatus { Conditions Conditions }

Slide 45

Slide 45 text

● Kubernetes では conversion-gen というツールがある ● 最新バージョンを +kubebuilder:storageversion とする ● それ以外は +kubebuilder:deprecatedversion とする // v1beta1 // +kubebuilder:deprecatedversion type Cluster struct {} // v1beta2 // +kubebuilder:storageversion type Cluster struct {}

Slide 46

Slide 46 text

● conversion-gen は Hub / Spoke というメソッドを用いて相互変換する ○ バージョン間変換を直接定義していくとパターン数が増える ○ Hub を用意することでそのバージョンを中心として変換を行う

Slide 47

Slide 47 text

● Hub の定義をする func (*Cluster) Hub() {}

Slide 48

Slide 48 text

// v1beta1 から v1beta2 への変換関数 func (src *Cluster) ConvertTo(dstRaw conversion.Hub) error // v1beta2 から v1beta1 への変換関数 func (dst *Cluster) ConvertFrom(srcRaw conversion.Hub) error ● Spoke の定義をする

Slide 49

Slide 49 text

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 に保持する

Slide 50

Slide 50 text

● 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}, })) }

Slide 51

Slide 51 text

● 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) {}

Slide 52

Slide 52 text

● どういう時に移行するか ● Deprecated の活用 そして Fuzzing と Conversion Test ● 移行の具体的なステップ

Slide 53

Slide 53 text

● 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{}, })) }

Slide 54

Slide 54 text

● Deprecated するフィールドと代替フィールドの追加 (粒度細かく ) type ClusterStatus struct { Conditions []metav1.Condition Deprecated *ClusterDeprecatedStatus } type ClusterDeprecatedStatus { V1Beta1 *ClusterV1Beta1DeprecatedStatus } type ClusterV1Beta1DeprecatedStatus { Conditions Conditions }

Slide 55

Slide 55 text

● 新しい API 構造の適用 ○ 構造の変更、新規フィールドの追加 type MachineDeploymentSpec struct { RolloutAfter *metav1.Time Strategy *MachineDeploymentStrategy } type MachineDeploymentSpec struct { Rollout MachineDeploymentRolloutSpec } type MachineDeploymentRolloutSpec struct { After metav1.Time Strategy MachineDeploymentRolloutStrategy }

Slide 56

Slide 56 text

● Deprecated フィールドを実装から全て消す if c.Status.Deprecated.XXXV1Beta1 != nil { // do something } if c.Status.XXX != nil { // do something }

Slide 57

Slide 57 text

まとめ ● API Conventions を読もう ● Kube API Linter を導入していい設計を目指そう ● 互換性をシステムとして担保して API バージョンは移行しよう