Upgrade to Pro — share decks privately, control downloads, hide ads and more …

静的解析 x Kubernetes API Conventions = Kube API Li...

Avatar for sivchari sivchari
November 20, 2025
14

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

Avatar for sivchari

sivchari

November 20, 2025
Tweet

Transcript

  1. 静的解析 x Kubernetes API Conventions = Kube API Linter ~

    ベストプラクティスに準拠したカスタムリソースの作り方と運用 ~
  2. もくじ • Cluster API と Kube API Linter について •

    Kubernetes API Conventions と Kube API Linter • API 設計で考えること • 互換性を担保した API 移行方法
  3. 自己紹介 渋谷拓真 newmo株式会社 Go Conference メインオーガナイザー Individual Contributor • Kube

    API Linter Approver • Cluster API Reviewer • ArgoCD Member Kubernetes 2025 Contributor Award @sivchari
  4. • Cluster API (CAPI) ◦ Kubernetes SIGs プロジェクトの 1つ (SIG

    Cluster Lifecycle) ◦ CRD を使用して複数クラスタの作成、更新等の運用を容易にする
  5. • Management Cluster ◦ Workload Cluster を管理する ◦ CAPI が動く

    ◦ CAPI Provider も動く • Workload Cluster ◦ Management Cluster により管理 ◦ CAPI Provider が作成する実態
  6. • Bootstrap Provider ◦ cloud-init scriptの構築 ◦ 構築物のSecretへの保存 • Infrastructure

    Provider ◦ 各ベンダーが担当 ◦ Workload Cluster のリソース管理 ▪ VM (e.g. XXX Machine) ▪ ネットワーク • Control Plane Provider ◦ ライフサイクル管理 ◦ 証明書の生成
  7. • Kube API Linter (KAL) ◦ Kubernetes SIGs プロジェクトの 1つ

    (SIG App) ◦ Kubernetes API Conventions をベースに複数のルールセットを提供 KAL API
  8. 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: {}
  9. • Kubernetes API Conventions ◦ Kubernetes API の設計プラクティスがまとまっているドキュメント ◦ Kubernetes

    の設計をするなら読んだ方がいい ◦ Kubernetes の設計をするなら 本当に読んだ方がいい ◦ Kubernetes の設計をするなら 絶対に読んだ方がいい ◦ Kubernetes の設計をするなら お願いなので 読んでください
  10. • Optional vs. Required ◦ Kubernetes API は必ず Optional または

    Required になる ◦ Optional は以下のルールに従うべき ▪ +optional マーカーを持つ ▪ ポインタ型または built-in で nil を持つならそれを使う (map/slice) ▪ map/slice のゼロ値を見分けたい場合はポインタ型にする ▪ omitempty を持つ ◦ Required は以下のルールに従うべき ▪ 明示的に +required マーカーを持つ ▪ ゼロ値が有効であるなら例外的にポインタにしてもいい • 必須だけど空文字も有効な値として扱うなど
  11. 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 だからポインタ
  12. if ClusterSpec.ClusterNetwork != nil {} if ClusterSpec.FailureDomain != nil &&

    ClusterSpec.FailureDomain != “” {} Or If ptr.Deref(ClusterSpec.FailureDomain, “”) != “” {} nil に対する分岐と panic のリスク
  13. • Serialization of optional/required field ◦ optional と required どちらにするか決める

    • 具体的な例が参考になる ◦ bool は常にポインタにするべき ◦ 構造体をゼロ値にできる条件 ▪ 必須フィールドを持たない ▪ MinProperties がある • 若干整理されきってないところがある ◦ 複合型のゼロ値は *[]T とする ▪ omitzero で今はいいよね? ▪ 絶賛議論中
  14. 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なので空文字はエラー
  15. • optionalfields ルール ◦ +optional や omitempty, omitzero, kubebuilder marker

    を含めて静的解 析 ◦ SuggestedFix を搭載している ▪ golangci-lint -fix でコードを自動修正する ◦ 設定で警告のみにする等制御可能 Kube API Linter
  16. • Lists of named subobjects preferred over maps ◦ map

    型は使わないで同様の表現は struct で表現する ◦ JSON/YAML のキーがフィールドと一致する一貫性 ◦ +listType も使えるようになるため SSA を活用できる ports: - name: www containerPort:80 ports: www: containerPort:80 ✅ ❌
  17. • nomaps ルール ◦ map 型の使用を検出する ◦ 例外として map[string]string のみ許容することもできる

    ▪ Label/Annotation などは例外として認められているため Kube API Linter
  18. • 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
  19. • Spec はユーザーの要望 • k/k の API であっても外部 API の型を再利用しない

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

    • 型の共有はしない • 過剰な抽象化、具象化されたフィールド名を疑う
  21. • Spec はユーザーがその API のあるべき形を定義したもの • コントローラーによって Spec を書き換えることは絶対に NG

    • 型のゼロ値がコントローラーにとって有効かどうかを考える • デフォルト値を使ってもいいが、 default marker は使わないようにする ◦ +default:value=XXX and +kubebuilder:default:=XXX • ユーザーが意図する内容を一貫して保持できるようにする
  22. • ユーザーが意図する内容を一貫して保持できるようにする • Typed Client と Unstructured Client の互換性を考える //

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

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

    +optional //+kubebuilder:validation:MinLength:=1 Foo string `json:”foo,omitempty”` • Typed Client ◦ 空文字 は無効な値となる ◦ foo: “” は保存できない • Unstructured Client ◦ 空文字 は無効な値となる ◦ foo: “” は保存できない
  25. • Spec はユーザーの要望 • k/k の API であっても外部 API の型を再利用しない

    • 型の共有はしない • 過剰な抽象化、具象化されたフィールド名を疑う
  26. 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 }
  27. type ClusterSpec struct { // +optional ControlPlaneRef ContractVersionedObjectReference } •

    不要なフィールドは削除 • 必要に応じてコピー & ペースト Cluster API v1beta2 type ContractVersionedObjectReference struct { Kind Name APIGroup }
  28. • Spec はユーザーの要望 • k/k の API であっても外部 API の型を再利用しない

    • 型の共有はしない • 過剰な抽象化、具象化されたフィールド名を疑う
  29. type Machine struct { Spec MachineSpec } Cluster API type

    MachineTemplateSpec struct { Spec MachineSpec } type MachineSpec struct { Spec MachineSpec } type MachineDeploymentSpec struct { Template MachineTemplateSpec } // MachineSet と MachinePool も同様
  30. • Spec はユーザーの要望 • k/k の API であっても外部 API の型を再利用しない

    • 型の共有はしない • 過剰な抽象化、具象化されたフィールド名を疑う
  31. type MachineDeploymentSpec struct { RolloutAfter *metav1.Time Strategy *MachineDeploymentStrategy } •

    両方 Rollout に関係する • 利用者から見ると理解しづらい • RolloutStrategy に命名を変える? Cluster API v1beta1
  32. type MachineDeploymentSpec struct { Rollout MachineDeploymentRolloutSpec } type MachineDeploymentRolloutSpec struct {

    After metav1.Time Strategy MachineDeploymentRolloutStrategy } • Rollout でまとめる • 上から下に読めるか Cluster API v1beta2 Rollout は RollingUpdate で 5分後に実行する
  33. • 廃止するフィールドを Deprecated に集約して数バージョン保持する type ClusterStatus struct { Conditions Conditions

    } type Conditions []Condition type Condition struct {} Cluster API v1beta1 • Condition を独自定義 • metav1.Condition に移行したい ◦ API Conventions 推奨
  34. Cluster API v1beta2 type ClusterStatus struct { Conditions []metav1.Condition Deprecated

    *ClusterDeprecatedStatus } type ClusterDeprecatedStatus { V1Beta1 *ClusterV1Beta1DeprecatedStatus } type ClusterV1Beta1DeprecatedStatus { Conditions Conditions }
  35. • Kubernetes では conversion-gen というツールがある • 最新バージョンを +kubebuilder:storageversion とする •

    それ以外は +kubebuilder:deprecatedversion とする // v1beta1 // +kubebuilder:deprecatedversion type Cluster struct {} // v1beta2 // +kubebuilder:storageversion type Cluster struct {}
  36. // v1beta1 から v1beta2 への変換関数 func (src *Cluster) ConvertTo(dstRaw conversion.Hub)

    error // v1beta2 から v1beta1 への変換関数 func (dst *Cluster) ConvertFrom(srcRaw conversion.Hub) error • Spoke の定義をする
  37. 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 に保持する
  38. • 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}, })) }
  39. • 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) {}
  40. • 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{}, })) }
  41. • Deprecated するフィールドと代替フィールドの追加 (粒度細かく ) type ClusterStatus struct { Conditions

    []metav1.Condition Deprecated *ClusterDeprecatedStatus } type ClusterDeprecatedStatus { V1Beta1 *ClusterV1Beta1DeprecatedStatus } type ClusterV1Beta1DeprecatedStatus { Conditions Conditions }
  42. • 新しい API 構造の適用 ◦ 構造の変更、新規フィールドの追加 type MachineDeploymentSpec struct {

    RolloutAfter *metav1.Time Strategy *MachineDeploymentStrategy } type MachineDeploymentSpec struct { Rollout MachineDeploymentRolloutSpec } type MachineDeploymentRolloutSpec struct { After metav1.Time Strategy MachineDeploymentRolloutStrategy }
  43. まとめ • API Conventions を読もう • Kube API Linter を導入していい設計を目指そう

    • 互換性をシステムとして担保して API バージョンは移行しよう