Kube API LinterでKubernetes API規約に準拠したCRDを実装する
はじめに
KubernetesでCRDの元となるGo構造体を定義する際、Kubernetes API規約に準拠したコードを書くことは非常に重要です。しかし、この規約は広範囲に渡り、すべてのルールを把握して実装することは容易ではありません。
この記事では、Kubernetes API規約の概要と、それに準拠したコードを自動でチェックできるKube API Linterの使い方を紹介します。実際にKarpenterプロジェクトに適用した例も解説します。
Kubernetes API規約とは
Kubernetes API規約は、Kubernetesエコシステム全体で一貫性のあるAPIを提供するための設計ガイドラインです。この規約は、kubernetes/communityリポジトリで公開されており、以下のような重要な原則が定義されています。
主要な設計原則
1. spec/statusの分離
Kubernetesでは、リソースの望ましい状態(spec)と現在の状態(status)を明確に分離します。specフィールドはユーザーが定義する設定を含み、statusフィールドはシステムによって観測された現在の状態を表します。
type MyResource struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec MyResourceSpec `json:"spec,omitempty"`
Status MyResourceStatus `json:"status,omitempty"`
}
2. マップではなくリストを使用
API規約では、サブオブジェクトのマップを使用せず、名前フィールドを含むサブオブジェクトのリストを使用することが推奨されています。これにより、Server-Side Applyなどの機能との互換性が保たれます。
// 非推奨: マップの使用
type BadExample struct {
Items map[string]Item `json:"items"`
}
// 推奨: リストの使用
type GoodExample struct {
Items []Item `json:"items"`
}
type Item struct {
Name string `json:"name"`
Value string `json:"value"`
}
3. 型の使用規則
- すべての公開整数フィールドは、
intではなくint32またはint64を使用する必要があります - 期間を表す場合は、整数型ではなく
metav1.Durationを使用します - 条件を表す場合は、
metav1.Conditionのスライスを使用します
type MyResourceSpec struct {
Replicas int32 `json:"replicas"`
Timeout metav1.Duration `json:"timeout"`
}
type MyResourceStatus struct {
Conditions []metav1.Condition `json:"conditions,omitempty"`
}
4. JSONタグとマーカーの必須化
すべてのフィールドには適切なJSONタグが必要です。また、Server-Side Apply(SSA)をサポートするために、リストにはlistTypeマーカーを指定する必要があります。
type MyResourceSpec struct {
// +listType=map
// +listMapKey=name
Items []Item `json:"items,omitempty"`
}
これらの規約に従うことで、Kubernetes APIとの一貫性が保たれ、kubectl、Server-Side Apply、その他のKubernetesツールとの互換性が確保されます。
Kube API Linterとは
Kube API Linterは、Kubernetes API規約に準拠したコードを書くためのgolangci-lintプラグインです。このツールを使用することで、API定義のコードレビュー時に人手で確認していた項目を自動化できます。
Kube API Linterの主な機能
Kube API Linterは、以下のような項目をチェックします。
- jsontags: JSONタグの有無と形式
-
ssatags: Server-Side Apply用の
listTypeマーカー - commentstart: Godocコメントの形式
-
nodurations: 整数型ではなく
metav1.Durationの使用 - nomaps: マップではなくリスト型の使用
- arrayofstruct: 必須フィールドを持つ構造体の配列
-
conditions:
metav1.Conditionのスライスの使用 -
integers:
int32/int64の使用 - optionalorrequired: フィールドのoptional/requiredマーカー
- statusoptional: statusフィールドのoptional指定
- statussubresource: statusサブリソースの設定
これらのチェック項目により、Kubernetes API規約に準拠したCRDを実装できます。
セットアップ方法
Kube API Linterを使用するには、以下の手順でセットアップします。
1. golangci-lintのインストール
まず、golangci-lintをインストールします。
brew install golangci-lint
公式リポジトリ: https://github.com/golangci/golangci-lint
2. カスタム設定ファイルの作成
カスタムlinterを記述するために、.custom-gcl.ymlファイルを作成します。
version: v2.6.1
name: golangci-lint-kube-api-linter
destination: ./bin
plugins:
- module: 'sigs.k8s.io/kube-api-linter'
version: 'v0.0.0-20251106172841-33e0e4392883'
最新バージョンは、pkg.go.devで確認できます。
3. カスタムバイナリのビルド
以下のコマンドでカスタムバイナリをビルドします。
golangci-lint custom
このコマンドを実行すると、.custom-gcl.ymlのdestinationフィールドで指定された場所に、Kube API Linterが組み込まれたgolangci-lintバイナリが作成されます。
4. golangci-lint設定ファイルの作成
.golangci.ymlファイルを作成し、Kube API Linterを有効にします。
Kube API Linterのみを使用する場合
version: "2"
linters-settings:
custom:
kubeapilinter:
type: "module"
description: Kube API Linter lints Kube like APIs based on API conventions and best practices.
settings:
linters: {}
lintersConfig: {}
linters:
disable-all: true
enable:
- kubeapilinter
# 特定のパスでのみKube API Linterを実行する場合
issues:
exclude-rules:
- path-except: "api/*"
linters:
- kubeapilinter
特定のルールのみを有効にする場合
version: "2"
linters-settings:
custom:
kubeapilinter:
type: "module"
settings:
linters:
disable:
- "*"
enable:
- requiredfields
- statusoptional
- statussubresource
linters:
enable:
- kubeapilinter
他のlinterと併用する場合
既存の.golangci.ymlに以下の設定を追加します。
linters:
enable:
- asciicheck
- bidichk
- copyloopvar
# ... 既存のlinter
- kubeapilinter
linters-settings:
custom:
kubeapilinter:
type: "module"
description: Kube API Linter lints Kube like APIs based on API conventions and best practices.
settings:
linters: {}
lintersConfig: {}
5. 実行方法
カスタムバイナリを使用してlinterを実行します。
# 基本的な実行
./bin/golangci-lint-kube-api-linter run path/to/api/types
# 自動修正を適用する場合
./bin/golangci-lint-kube-api-linter run path/to/api/types --fix
--fixフラグを使用すると、修正可能な問題を自動的に修正できます。
実際に使ってみる
実際にKube API Linterを動かして、どのような問題が検出されるか確認してみます。今回は、Karpenterプロジェクトを例に使用します。
Karpenterプロジェクトへの適用
Karpenterは、Kubernetesクラスターのノードを自動的にプロビジョニングするオープンソースプロジェクトです。このプロジェクトには、/pkg/apis/配下にAPI定義が配置されています。
まず、前述の手順で.custom-gcl.ymlファイルを作成し、カスタムバイナリをビルドします。
次に、既存の.golangci.yamlに以下の設定を追加します。
version: "2"
run:
build-tags:
- test_performance
tests: true
timeout: 5m
linters:
enable:
- asciicheck
- bidichk
- copyloopvar
- errorlint
- gocyclo
- goheader
- gosec
- misspell
- nilerr
- revive
- staticcheck
- tparallel
- unconvert
- unparam
- kubeapilinter # 追加
disable:
- prealloc
linters-settings:
# 既存の設定...
custom:
kubeapilinter:
type: "module"
description: Kube API Linter lints Kube like APIs based on API conventions and best practices.
settings:
linters: {}
lintersConfig: {}
issues:
fix: true
# 以下、既存の設定...
linterの実行
Karpenterの/pkg/apis/配下を対象にlinterを実行します。
./bin/golangci-lint-kube-api-linter run ./pkg/apis/...
検出された問題
実行すると、以下のような問題が検出されました。
pkg/apis/v1/doc.go:1: : # sigs.k8s.io/karpenter/pkg/apis/v1
pkg/apis/v1/nodeclaim.go:117:10: cannot use ncr.Group (variable of type *string) as string value in struct literal
pkg/apis/v1/nodeclaim.go:118:10: cannot use ncr.Kind (variable of type *string) as string value in struct literal
pkg/apis/v1/nodepool.go:244:9: cannot use NodeClaimSpec{…} (value of struct type NodeClaimSpec) as *NodeClaimSpec value in struct literal
pkg/apis/v1/zz_generated.deepcopy.go:135:23: cannot use &out.Spec (value of type **NodeClaimSpec) as *NodeClaimSpec value in argument to in.Spec.DeepCopyInto
pkg/apis/v1/zz_generated.deepcopy.go:213:28: cannot use &out.Resources (value of type **ResourceRequirements) as *ResourceRequirements value in argument to in.Resources.DeepCopyInto
pkg/apis/v1/zz_generated.deepcopy.go:242:10: cannot use make(corev1.ResourceList, len(*in)) (value of map type "k8s.io/api/core/v1".ResourceList) as *"k8s.io/api/core/v1".ResourceList value in assignment
pkg/apis/v1/zz_generated.deepcopy.go:242:40: invalid argument: *in (variable of type *"k8s.io/api/core/v1".ResourceList) for built-in len
pkg/apis/v1/zz_generated.deepcopy.go:243:25: cannot range over *in (variable of type *"k8s.io/api/core/v1".ResourceList)
pkg/apis/v1/zz_generated.deepcopy.go:244:10: cannot index (*out) (variable of type *"k8s.io/api/core/v1".ResourceList)
pkg/apis/v1/zz_generated.deepcopy.go:249:40: invalid argument: *in (variable of type *"k8s.io/api/core/v1".ResourceList) for built-in len
pkg/apis/v1/zz_generated.deepcopy.go:249:40: too many errors (typecheck)
検出された問題は、主に以下のカテゴリに分類されます。
- jsontags (6件): JSONタグが不足しているフィールド
-
ssatags (5件): Server-Side Apply用の
listTypeマーカーが不足 - commentstart (11件): Godocコメントの形式が不正または欠落
-
nodurations (3件):
metav1.Durationの代わりに整数型を使用 - nomaps (6件): マップの代わりにリスト型を使用すべき
- arrayofstruct (3件): 必須フィールドがない構造体の配列
-
conditions (3件):
metav1.Conditionのスライスを使用すべき -
integers (1件):
int32/int64を使用すべき -
optionalorrequired (2件): フィールドを
optional/requiredとしてマークすべき
検出された問題の詳細
各カテゴリの問題について、具体例を見ていきます。
1. jsontags - JSONタグの不足
Kubernetes APIでは、すべての公開フィールドに適切なJSONタグが必要です。
// 問題のあるコード
type MySpec struct {
Replicas int32 // JSONタグがない
}
// 修正後
type MySpec struct {
Replicas int32 `json:"replicas"`
}
2. ssatags - Server-Side Apply用マーカー
リスト型のフィールドには、Server-Side Applyが正しく動作するようにlistTypeマーカーを指定する必要があります。
// 問題のあるコード
type MySpec struct {
Items []Item `json:"items"`
}
// 修正後
type MySpec struct {
// +listType=map
// +listMapKey=name
Items []Item `json:"items"`
}
listTypeには、以下の値を指定できます。
-
atomic: リスト全体を1つの単位として扱います -
set: 重複のない要素のセットとして扱います -
map: キーで識別される要素のマップとして扱います
3. commentstart - Godocコメント形式
公開型とフィールドのGodocコメントは、型名またはフィールド名で始まる必要があります。
// 問題のあるコード
// This is my resource
type MyResource struct {}
// 修正後
// MyResource は、カスタムリソースを表します。
type MyResource struct {}
4. nodurations - metav1.Durationの使用
期間を表すフィールドには、整数型ではなくmetav1.Durationを使用します。
// 問題のあるコード
type MySpec struct {
TimeoutSeconds int32 `json:"timeoutSeconds"`
}
// 修正後
type MySpec struct {
Timeout metav1.Duration `json:"timeout"`
}
5. nomaps - リスト型の使用
マップの代わりに、名前フィールドを持つ構造体のリストを使用します。
// 問題のあるコード
type MySpec struct {
Settings map[string]string `json:"settings"`
}
// 修正後
type MySpec struct {
// +listType=map
// +listMapKey=name
Settings []Setting `json:"settings"`
}
type Setting struct {
Name string `json:"name"`
Value string `json:"value"`
}
6. conditions - metav1.Conditionの使用
ステータスの条件を表す場合は、metav1.Conditionのスライスを使用します。
// 問題のあるコード
type MyStatus struct {
Ready bool `json:"ready"`
Error string `json:"error,omitempty"`
}
// 修正後
type MyStatus struct {
// +listType=map
// +listMapKey=type
Conditions []metav1.Condition `json:"conditions,omitempty"`
}
metav1.Conditionを使用することで、以下のような標準的な条件表現が可能になります。
condition := metav1.Condition{
Type: "Ready",
Status: metav1.ConditionTrue,
LastTransitionTime: metav1.Now(),
Reason: "SuccessfullyReconciled",
Message: "Resource is ready",
}
7. integers - 適切な整数型の使用
公開整数フィールドには、プラットフォーム依存のintではなく、int32またはint64を使用します。
// 問題のあるコード
type MySpec struct {
Replicas int `json:"replicas"`
}
// 修正後
type MySpec struct {
Replicas int32 `json:"replicas"`
}
8. optionalorrequired - フィールドマーカー
フィールドが必須かオプショナルかを明示的にマークします。
// 修正後
type MySpec struct {
// +required
Name string `json:"name"`
// +optional
Description string `json:"description,omitempty"`
}
まとめ
Kubernetes API規約は広範囲に渡りますが、Kube API Linterを活用することで、規約に準拠したCRDを効率的に実装できます。CRDを実装する際は、ぜひこのツールを活用してみてください。
Discussion