はじめに
KubernetesのCustom Resource Definition (CRD) は、Kubernetes APIを拡張して独自のリソースを定義する強力な手段です。オペレーター開発ではCRDの設計が成功の鍵を握ります。しかし、一度リリースしたCRDは長期間にわたり互換性を保ちつつ進化させる必要があり、その設計は思った以上に難しいものです。「とりあえず動くCRD」を作ったあとに拡張や変更で苦労した経験があるエンジニアも多いでしょう。
本記事では、KubernetesのCRD設計におけるアンチパターンとベストプラクティスについて深掘りします。これはKubeCon EU 2025でChristian Schlotter氏 (Broadcom) とFabrizio Pandini氏 (VMware/Broadcom) が発表した「Kubernetes CRD Design for the Long Haul (長期運用を見据えたCRD設計)」 (Christian Schlotter and Fabrizio Pandini on KubeCon stage sharing anti… | Sean Schneeweiß)から得られた知見に基づいています。Cluster APIプロジェクトでの豊富な経験を踏まえた内容で、CRDの設計・進化に関する具体的なTIPSが満載でした。本記事ではそれらを日本語でカジュアルに紹介しつつ、筆者自身の補足や現場で役立つプロトリックも交えて解説します。
対象読者: 既にKubernetesオペレーター開発でCRDを扱っている中級~上級エンジニアの方を想定しています。基本的なKubebuilderの知識やCRDの使い方は理解している前提で、より「長く使える洗練されたAPI設計」を目指した内容です。
それでは、長期戦に耐えうるCRD設計のポイントを見ていきましょう。
よくあるCRD設計のアンチパターン
まず、開発者が陥りがちなCRD設計上のアンチパターン(避けるべき設計)を紹介します。**「あるある、やりがち!」**となるものがあるか、振り返りながら読んでみてください。
アンチパターン1: Kubernetes組み込みの型を直接埋め込む
症状: CRDのSpecやStatus内に、KubernetesのコアAPIで定義されているGo構造体(型)をそのまま利用することがあります。例えば、corev1.ObjectReference
やcorev1.PodSpec
などを直接埋め込んで再利用するケースです。
なぜやりがち?
既存の型を使えば一からフィールドを定義しなくて済み、重複も避けられるので「DRY原則に合致して良さそう」と感じます。また、Kubernetesの型なので信頼できると思いがちです。
問題点:
外部のAPI型を埋め込むと、バージョン依存の問題に悩まされます。他プロジェクトが管理する型は将来変更・拡張される可能性があり、それに引きずられてこちらのCRDのスキーマも意図せず変わってしまう恐れがありま (Christian Schlotter and Fabrizio Pandini on KubeCon stage sharing anti… | Sean Schneeweiß)】。例えば、Kubernetes本体がObjectReference
型を将来変更した場合(フィールド追加など)、自分のCRDでも同じ変更を受け入れるか、古いバージョンにとどめるかの判断を迫られます。いずれにせよ依存関係の更新とAPI互換性の維持が面倒になります。
また、組み込み型は汎用的すぎて自分のドメインにフィットしない場合があります。例えばcorev1.ObjectReference
にはname
やapiVersion
以外にも不要なフィールドが含まれますし、corev1.PodSpec
を埋め込めばPodに関する非常に多くのフィールドをユーザーにさらすことになり、コントローラー側で扱いきれない設定項目までAPIに露出してしまいます。
改善策:
「Copy, adapt and evolve」、つまり必要な部分だけ自前の型としてコピーして、用途に合わせてカスタマイズし、独立に進化できるようにしましょ (Christian Schlotter and Fabrizio Pandini on KubeCon stage sharing anti… | Sean Schneeweiß)】。具体的には、以下のようなアプローチです。
- 必要最低限のフィールドだけを持つ独自構造体を定義する。例えば、クラスタを参照するフィールドに
corev1.ObjectReference
を使う代わりに、自前でClusterRef
のようなstructを定義し、apiVersion
やkind
は固定でよいなら省略し、必要なname
とnamespace
だけ持たせるなど。 - または、Kubernetes組み込み型を埋め込むのではなく、参考にしつつ自前定義する。例えばPodSpec相当の設定を持たせたいなら、本当に必要なサブセットのみを自分のCRD用に再定義します。
こうすることで、自分たちのCRD APIのスキーマ管理権を自分で握ることができます。他プロジェクトのリリースサイクルや互換性方針に振り回されず、自分たちのペースでAPIを進化可能です。
例: Cluster APIでは、クラスタのインフラストラクチャ設定を指すためにCluster.Spec.InfrastructureRef
というフィールドがあります。この型定義は*corev1.ObjectReference
でし (cluster-api/api/v1beta1/cluster_types.go at main · kubernetes-sigs/cluster-api · GitHub)】。このようにcorev1の型を直接使っていますが、今から新規にAPIを設計するなら、例えば以下のようにする選択肢もあります:
// オブジェクト参照用の独自型を定義する例
type InfraRef struct {
APIGroup string `json:"apiGroup"`
Kind string `json:"kind"`
Name string `json:"name"`
Namespace string `json:"namespace,omitempty"`
}
上記は単なる例ですが、外部のObjectReferenceをそのまま使うのではなく、自前のInfraRef
型を用意することで、自分たちの文脈にあったフィールドだけを持たせています。こうしておけば、例えば将来参照先リソースを名前ではなく別の識別子で参照するよう変更したい場合でも、自分たちのInfraRef
型を拡張すれば対応できます。他方、corev1.ObjectReferenceに直接依存していると、その型に無い表現はできず、結局別フィールドを追加する羽目になったりと、一貫性が崩れてしまいます。
ただし補足:
後述するベストプラクティスにもありますが、「全ての組み込み型がNG」というわけではありません。Kubernetesの中でも特に安定しており意味も明確なメタデータ系の型(例: metav1.TypeMeta
やmetav1.ObjectMeta
、タイムスタンプのmetav1.Time
、標準的なCondition構造体など)は積極的に再利用してOKで (Christian Schlotter and Fabrizio Pandini on KubeCon stage sharing anti… | Sean Schneeweiß)】。これらはKubernetes全体で広く使われており変更も極力発生しないよう管理されています。実際、CRDのKind structにはObjectMetaなどを埋め込むのが定石です。それ以外のリソース固有のAPI型(PodSpecやVolume、ObjectReference等)は安易に内部で抱え込まない方が無難、というのが経験則です。
TIP: 既存型再利用の判断基準 – 迷ったら「その型はこの先も長期間フォーマットが変わらないと安心して言えるか?」と自問してみましょう。例えば、タイムスタンプの表現であるmetav1.Time
はISOフォーマットの文字列型でまず変わりません。一方、PodやServiceのSpecはKubernetesの進化とともに頻繁に拡張されています。将来にわたり変更が読めないものは自分でコントロールできる形にしておく方が吉です。
アンチパターン2: 複数のCRDで同じGo構造体を使い回す
症状: 複数のCustomResource間で、SpecやStatus用の構造体を共通化しているケースです。典型例は、「あるCRDのSpec構造体を別のCRDのSpecやテンプレート定義にそのまま再利用する」といったものです。Cluster APIプロジェクトでも、Machine
とMachineDeployment
でSpec(MachineSpec
)を共有する設計をしていました。
なぜやりがち?
一度定義した構造体を再利用すればコード重複が減り、一貫性も保てるように思えます。同じようなフィールドセットを持つリソースが複数ある場合、共通StructでDRYに書きたくなるのは自然な発想です。
問題点:
異なるリソースでライフサイクルや文脈が違うのに、APIを強制的に連動させてしまう点です。あるリソースでは意味を持つフィールドが、別のリソースでは無意味だったり不要だったりする場合があります。しかし構造体を共有してしまうと、どちらにもそのフィールドが現れてしまい、片方では使われないフィールドがAPIに露出してしまいます。
Cluster APIの例では、Machine
とMachineDeployment
の両方で使われるMachineSpec
構造体にproviderID
やclusterName
といったフィールドが含まれていまし (Allow user-controlled naming of Machines in Machine collections · Issue #10577 · kubernetes-sigs/cluster-api · GitHub)】。providerID
は実機マシンに割り振られる識別子ですが、MachineDeployment(複数マシンのテンプレート)には本来意味がない情報です。しかし構造体を共有した結果、MachineDeploymentのAPIに意味を成さないフィールドが紛れ込むことになりました。このように、共有したがために生じる不整合が発生しがちです。また、一方のリソースに新たな要件が出てフィールドを追加・変更したくても、もう一方への影響を考えて自由にできなくなります。まさに進化が連鎖的に拘束されてしまうので (Christian Schlotter and Fabrizio Pandini on KubeCon stage sharing anti… | Sean Schneeweiß)】。
開発者自身も後から「やっぱり共通化すべきじゃなかったかも…」と気付くことがあります。実際、Cluster APIのメンテナは「MachineSpecをMachineとMachineDeploymentで共有したのは良くなかったかもしれない」とコメントしていま (Allow user-controlled naming of Machines in Machine collections · Issue #10577 · kubernetes-sigs/cluster-api · GitHub)】。
改善策:
似ていても別物なら構造体定義も別にするのが安全です。多少コードが重複しても、それぞれ独立に進化できるメリットの方が大きいです。共通部分は後述のジェネリクスやコード生成で対応する手もありますが、APIスキーマとしては分離しておいた方が将来的に柔軟になります。
例えば、Cluster APIではMachineDeploymentのテンプレートspec.template.spec
でMachineSpecを使い回していましたが、もし専用のMachineTemplateSpecを定義しておけば、テンプレート用に意味のないフィールドを省いたスッキリした仕様にできたでしょう。このようにCRDごとに専用のSpec/Status structを定義することで、不必要なカップリングを減らせます。
TIP: APIは「将来別々の道を歩むかも?」を想像する – 目先では同じ内容でも、将来的に片方だけに追加要素が欲しくなるケースは多々あります。「今は偶然似ているだけ」かもしれないと考え、安易な共通化は避けましょう。どうしても共通化したい場合も、Goの埋め込みで共通フィールド集合を内包しつつ個別に拡張できるようにするといった工夫が考えられます。
アンチパターン3: 汎用的すぎるフィールド名を使う(定義不足のまま)
症状: value
やconfig
、parameters
、type
、info
など、一見して用途がはっきりしないあいまいな名前のフィールドをAPIに含めてしまうケースです。またはドメイン特有の用語だけれど利用者にとって馴染みがなく説明が無いようなフィールドも、結果的に「定義不足」で混乱を招くことがあります。
なぜやりがち?
設計時に「汎用的に拡張できるように」と考え、広く使えそうな名前を付けてしまうことがあります。例えばsettings
やoptions
のような名前にしておけば、何でも入れられて柔軟だろう…と安直に考えてしまうパターンです。また、自分たちのチーム内では通じる略語やコードネームをそのままフィールド名にしてしまい、十分な説明を書かない場合もあります。
問題点:
API利用者(CRを作成・編集するユーザーや他の開発者)から見ると、そのフィールドの意図や期待される値が分からないという事態になります。例えばtype
というフィールドがあったとして、「何のタイプ?どんな値を設定すれば?」と迷うでしょう。結局ドキュメントやソースコードを掘らないと理解できず、自己説明性の低いAPIになってしまいます。
特に汎用すぎる単語は避けるべきです。config
やoption
と名付けたばかりに、「設定を書くんだろうけど何の設定?フォーマットは?」と毎回迷わせてしまいます。また、そのフィールドの中身がさらに入れ子構造だったりするとブラックボックス化してしまい、スキーマ上はmap[string]interface{}
も同然でバリデーションも効かず、将来的な変更も困難です。
改善策:
フィールド名は出来る限り具体的にしましょう。値の単位や意味を名前に込めるだけでも違います。例えば時間を表すならtimeoutSeconds
のように単位まで含める(API全体で単位の付け方は統一)、種類を示すならdeploymentType
やvolumeType
のように具体化する、といった工夫です。KubernetesのAPI慣例では「フィールド名は宣言的(何を表すか)にし、命令的(どうするか)は避ける」とされています (community/contributors/devel/sig-architecture/api-conventions.md at master · kubernetes/community · GitHub)】。つまり、enableX
ではなくXEnabled
のように、状態や属性を表すように命名します。
もしどうしても抽象度の高い概念を扱うフィールドが必要なら、ドキュメント(APIリファレンスや注釈)で定義を明示してください。プロジェクト内に**用語集(Glossary)**を用意し、そのフィールド名や用語の意味を誰でも参照できるようにするのも有効で (Christian Schlotter and Fabrizio Pandini on KubeCon stage sharing anti… | Sean Schneeweiß)】。Cluster APIでも公式ドキュメントにGlossaryを設けており、Infrastructure Provider
やBootstrap Provider
など専門用語を丁寧に解説しています(例えば「ClusterClass」「Topology」といった概念も初見では分かりづらいですが、Glossaryのおかげで迷わずに済みます)。自分たちのCRD特有の言葉を使う場合は、ぜひ同様にチーム内/ユーザー向けの用語集を整備しましょう。
TIP: API定義を上から下まで声に出して読んでみる – 自分がユーザーになったつもりで、カスタムリソースYAMLのSpecを文章のように読んでみるテストをしてみましょう。例えば Cluster API の Cluster
リソースなら、「Clusterがほしい。そのspecにはtopologyがあって、その中のcontrolPlaneは3つのreplicasを持つ…」 といった具合で (Christian Schlotter and Fabrizio Pandini on KubeCon stage sharing anti… | Sean Schneeweiß)】。スラスラ読めてイメージできるならOKですが、「ここ曖昧で引っかかるな」という箇所があれば命名を見直すチャンスです。意外な気づきが得られますよ。
アンチパターン4: フィールド名をCamelCaseでつなげすぎる
症状: 単語をCamelCaseで継ぎ足した長いフィールド名が増えるケースです。例えばnodeDrainTimeout
やhttpProxyConfigJSON
のように、説明的ではあるものの一つのフィールド名に複数の概念が詰め込まれている状態です。
なぜやりがち?
「〇〇の△△に関する設定だから…じゃあ〇〇△△
って名前にしよう」という発想です。KubernetesのフィールドはCamelCase(JSONではcamelCase)で書くルールなので、複数単語をくっつけること自体は普通で (community/contributors/devel/sig-architecture/api-conventions.md at master · kubernetes/community · GitHub)】。しかし、本来はネストして表現すべきところを安易にフラットな名前で済ませてしまう場合があります。
問題点:
フィールド階層の構造が不明瞭になることです。本来、関連する設定項目はひとまとまりにした方が分かりやすいのに、CamelCaseで繋げただけだとパッと見て「どの部分がグループなのか」が見えません。また、名前が長くなりすぎて単純に読みにくくなります。例としてnodeDrainTimeout
とnodeDrainPollingInterval
など似たプレフィックスのフィールドが並ぶと、**「nodeDrain系のパラメータがいくつあるのか」**把握しづらいですし、将来さらにnodeDrainXxx
が増えたときにメンテナンス性が下がります。
改善策:
サブ構造体でグルーピングできるならすることを検討しましょ (Christian Schlotter and Fabrizio Pandini on KubeCon stage sharing anti… | Sean Schneeweiß)】。例えば、nodeDrainTimeout
とnodeDrainPollingInterval
という2つのフィールドがSpecにあるなら、nodeDrain
という一つのオブジェクト内にtimeout
とpollingInterval
を持たせる形にできます:
spec:
nodeDrain:
timeout: 30s
pollingInterval: 5s
上記の方が、Spec直下にnodeDrainTimeout: 30s
とnodeDrainPollingInterval: 5s
が別々にあるより論理的なまとまりが明示されます。**「ノード排出処理に関する設定一式」**と認識しやすく、今後新たな関連設定(例: nodeDrainForce
など)が増えてもnodeDrain
下に追加すれば済みます。CamelCaseで長々と名前を作るのは避け、フィールド階層を活用して表現力を上げるのがポイントです。
もちろん全てをネストしすぎると深い構造になりすぎるのでバランスは必要ですが、同じプレフィックスで始まる項目が3つ以上出てきたらネストを検討するくらいが目安でしょう。「CamelCaseの連結で対応」は手軽ですが、後から変更するのは非互換になるため困難です。最初から見通しの良い構造にしておくに越したことはありません。
アンチパターン5: GitOps下で扱いにくいリスト設計
症状: 配列(リスト)形式のフィールド定義に起因して、GitOps環境で差分管理やマージが難しくなるケースです。たとえばCRDのSpec内に配列で複数要素を持つが、その要素の識別子や順序の扱いが曖昧なために、Argo CDやFluxといったツール上で無用な差分や競合が発生する、といった問題です。
なぜやりがち?
Kubernetes APIではPodのcontainers
のように配列で複数要素を定義するパターンがあり、CRDでも「〇〇の一覧」はとりあえず[]
で定義しがちです。深く考えずにリストで持たせ、「必要なら名前プロパティで識別すればいいや」としてしまうこともあります。
問題点:
GitOpsではGit上のマニフェスト(YAML)と実際のクラスタ状態を同期させます。このとき、リスト項目の順序や増減の扱いで困ったことが起きることがあります。例えば、コントローラーがCRのSpec内のリストに要素を自動追加・削除するような挙動をすると、Git上の期望状態との差分が毎回発生してしまい収拾がつきません。また、単純にユーザーが順序を変えただけでも差分検出されますが、順序に意味が無い場合は「不要な差分」となります。
別の視点では、マージ戦略の問題もあります。Kubernetesではサーバサイドアプライやパッチ適用時のために、CRDのスキーマでx-kubernetes-list-type
やx-kubernetes-list-map-keys
といったプロパティでリストのマージ挙動を定義できます。しかしこれを適切に設定していないと、デフォルトでは配列全体がひとつのフィールドとして扱われ、要素ごとのマージができません。その結果、あるユーザーがリストの一部を変更するとリスト全体が入れ替わったとみなされて他の変更とコンフリクトしたり、GitOpsツールが差分を正しく検知できなかったりします。
改善策:
まず、本当に配列で持つ必要があるか検討しましょう。キーで一意に識別できる集合であれば、配列よりも**マップ(辞書)**のほうが扱いやすいことも多いです。たとえば「名前で識別される複数のコンポーネント設定」なら、components: {"frontend": {...}, "backend": {...}}
のようにマップで持てば順序の問題もなく、個別部分だけGitOpsで更新するのも容易です。
どうしても配列が適切な場合でも、要素にユニークキーとなるフィールドを持たせるか、またはCRDスキーマでlistMap
/listSet
として宣言することを検討してください。OpenAPIベースのCRD定義で例えば以下のように指定できます:
properties:
items:
type: array
x-kubernetes-list-type: map
x-kubernetes-list-map-keys: ["name"] # nameフィールドで一意識別
items:
type: object
properties:
name:
type: string
value:
type: string
上記のようにlist-type: map
かset
を設定すれば、サーバサイドアプライや差分計算時に要素単位で比較できま (Kubernetes 1.25: CustomResourceDefinition Validation Rules Graduate to Beta | Kubernetes)】。結果、順序の入れ替え程度では差分と見なされず、特定キーの要素だけ変更/追加/削除した場合も適切にマージできます。Argo CDなどのツールも、なるべくこうした情報を活用して差分比較するようになっています(Argo CDではCRDのpreservedUnknownFields: false
かつOpenAPIスキーマ定義がある場合、内容ベースで差分比較します)。
また、コントローラーがSpecのリストを勝手に書き換えないことも重要です。GitOpsでは基本的にSpecは人間(またはGitOpsツール)が真実の情報源(Single Source of Truth)となり、コントローラーの出力はStatusや別リソースで表現すべきです。もしどうしてもSpec相当の情報をコントローラーが生成する必要があるなら、その部分を別のCRとして扱う(たとえば子リソースを作成する)など、責務を分離しておくとGitOps的に安全です。
TIP: 差分に優しいAPI – GitOps運用を想定し、自分のCRDについて典型的な変更シナリオで差分がどう出るかを試してみましょう。リスト項目の追加・削除、順序変更、また複数人が同時に異なる項目を編集した場合などです。ツール上で無駄な差分が毎回出たり、マージ不能なコンフリクトが起きそうなら要改善です。スキーマでのマージキー設定や、フィールドの細分化・構造見直しで解決できないか検討してみてください。
長期運用のためのAPI進化と互換性ルール
CRDを長くメンテナンスしていくには、後方互換性を保ちつつスキーマ変更していく戦略も欠かせません。KubernetesのAPIポリシーにならい、以下のルールを心に留めておきましょ (Christian Schlotter and Fabrizio Pandini on KubeCon stage sharing anti… | Sean Schneeweiß)】:
- 破壊的変更(非互換変更)は最小限に: 基本的に既存フィールドの削除やリネーム、意味変更はクライアントに影響を与えるためNGです。どうしても必要な場合は、新しいAPIバージョン(例: v1beta1 -> v1beta2)へのマイナーバージョンアップ時に限定し、移行のための十分な周知期間を設けま (Christian Schlotter and Fabrizio Pandini on KubeCon stage sharing anti… | Sean Schneeweiß)】。
- 非推奨期間の確保: 古いフィールドやバージョンを廃止したい場合、いきなり削除せず**非推奨(deprecated)**マークを付けてしばらく残すのが筋です。Kubernetesプロジェクトでは「最低1年間 or 3リリースは非推奨のまま残す」というガイドラインがありま (Christian Schlotter and Fabrizio Pandini on KubeCon stage sharing anti… | Sean Schneeweiß)】。自分たちのプロジェクトでも、利用者が移行する猶予を十分に設けましょう。
-
新バージョンでの改良: CRDの
spec.versions
で複数バージョンをサポートし、v1alpha1→v1beta1→v1など段階的にスキーマを発展させるのも有効です。その際、同じリソースオブジェクトは全バージョンで相互変換できる(convertible)ように変換関数を実装します。これにより新旧クライアントどちらから見てもリソースを扱えるようにできます。 - フィールド追加は非破壊的: 新しい用途に対応するには、新フィールド追加やnullable許容など後方互換な手段で対処できないかまず考えます。例えば従来booleanであった設定を将来拡張したければ、新たにenum型の別フィールドを追加し、booleanはdeprecatedにする、といった方法です。安易に布石なくbool→string書き換えなどしないように注意です。
要は、「一度公開したAPIは簡単には変えられない」という心構えで設計・運用することです。だからこそ、上述してきたフィールド命名の慎重さやアンチパターン回避が効いてきます。最初から変えずに済む可能性を最大化するわけですね。
それでも変えたくなることはあります。その場合は上記ルールに従って段階的に、かつ互換性を壊すときはメジャーバージョンアップと周知徹底をセットで行いましょう。例えば「v1beta1でフィールドXを非推奨にしてv1beta2で削除、さらに1年後のv1正式版で完全削除」など計画を立てて進めます。
TIP: スキーマ差分チェック – コード上でCRDのGo structを編集した際には、controller-gen
などで生成されるOpenAPIスキーマのdiffを必ず確認しましょ (Christian Schlotter and Fabrizio Pandini on KubeCon stage sharing anti… | Sean Schneeweiß)】。意図しない差分(例えばoptionalをrequiredに変えてしまっている、enumに値を追加したけど既存クライアント大丈夫?など)に気付けます。レビュー時にもCRDのYAML差分を見るクセを付けると安心です。
CRD設計を洗練させるベストプラクティス集
アンチパターンを踏まえた上で、ここからはより良いCRD設計のためのベストプラクティスをいくつか紹介します。ちょっとした工夫でAPIの使い勝手と将来性が大きく向上しますので、ぜひ取り入れてみてください。
1. フィールド命名と用語の一貫性を保つ
すべてのフィールド名に意味を持たせるよう意識しましょう。「まあ通じるだろう」と付けた名前が後々ボディーブローのように効いてきます。命名はAPI設計で最も重要と言っても過言ではありませ (Christian Schlotter and Fabrizio Pandini on KubeCon stage sharing anti… | Sean Schneeweiß)】。
-
プロジェクト内で用語を統一: 例えば「クラスタ」という概念を
cluster
と呼ぶのかcl
と略すのか、一貫させます。あるCRDではcluster
, 別のCRDではcl
だと混乱のもとです。できればKubernetesコミュニティ標準の用語に沿うのが望ましいです(例: コントローラはcontroller、ノードはnodeなど)。 -
略語の乱用に注意: APIではなるべく略さず書く方が親切ですが、どうしても使う略語(CPU, TLSなど一般的なもの)は大文字小文字の規則にも気をつけま (community/contributors/devel/sig-architecture/api-conventions.md at master · kubernetes/community · GitHub)】。頭字語はフィールド先頭では小文字、それ以外ではパスカルケース内でそのまま、などKubernetesの慣例に従うと統一感が出ます(例:
httpProxy
、cpuCount
)。 - 用語集(Glossary)の整備: 前述の通り、独自の言葉や省略を使う場合はドキュメントで補完しましょ (Christian Schlotter and Fabrizio Pandini on KubeCon stage sharing anti… | Sean Schneeweiß)】。「このフィールドは○○とも言うがプロジェクト内では△△と呼ぶ」などしっかり明示すればユーザーフレンドリーです。例えばCluster APIのドキュメントには様々な略語(CAPI, CAPBKなど)が出てきますが、Glossaryがあるおかげで初心者も挫折しにくくなっています】
2. Kubernetes APIリントツールを活用する
人間の目だけでベストプラクティスに沿ったAPIを作るのは骨が折れます。そんなとき便利なのがKubernetes API Linter (通称 KAL) で (Christian Schlotter and Fabrizio Pandini on KubeCon stage sharing anti… | Sean Schneeweiß)】。KALはGoの構造体定義に対して典型的なミスや不適切な表現を検出し、警告してくれる静的解析ツールで (kube-api-linter/README.md at main · kubernetes-sigs/kube-api-linter · GitHub)】。
KALでチェックされるルールには例えば:
- フィールド名に禁止文字(大文字開始、アンダースコアなど)が使われていない (community/contributors/devel/sig-architecture/api-conventions.md at master · kubernetes/community · GitHub)】。
- 命名が一貫しているか、略語の用法は適切 (community/contributors/devel/sig-architecture/api-conventions.md at master · kubernetes/community · GitHub)】。
-
Map
やList
のフィールドにProperな型指定がされているか(例: マップはキーと値の型、リストは要素の型を明示すべき)。 -
metav1.Condition
の利用方法が慣例に沿っているか。 - APIバージョンの命名(v1alpha1→v1beta1→v1等)が正しい順序で使われているか。
などがあります。これらのルールは公式の**Kubernetes API Conventions(API設計規約)* (kube-api-linter/README.md at main · kubernetes-sigs/kube-api-linter · GitHub)】に基づいており、Kubernetesの洗練されたAPI群から抽出されたベストプラクティス集と言えます。
KALは現在golangci-lint
のプラグインとして提供されているので、プロジェクトに組み込めばコードを書いた段階で問題を検出できます。CIに組み込んでおけば、レビュー前に自動でフィードバックが得られて安心です。
プロTIP: lint結果は学びの宝庫 – KALのみならずlintツールの指摘は、「なぜそれが良くないのか?」を理解するチャンスです。例えば「フィールド名に
Spec
を含めるべきでない」と怒られたら、調べてみると「上位ですでにSpecであることが明白だから不要」等、理由があります。単にエラーを黙らせるのではなく、規約の背景知識も蓄えていきましょう。それがあなた自身の設計力向上につながります。
3. 他の成熟プロジェクトのAPI設計を参考にする
世の中には既に多くの優れたKubernetes拡張APIがあります。その中から良いデザインパターンを学ぶことも有益です。例えば:
- Cluster API: 複数リソース間の参照関係の設計(例: ClusterがInfrastructureClusterとControlPlaneを参照する)、Topologyという高度な抽象化の提供方法、Conditionの使い方など、参考になる部分が多いです。実際、本記事の元ネタであるKubeCon講演も「Cluster APIで得た教訓」がベースになっています。
- Crossplane: 各種クラウドリソースをKubernetes APIで表現するOSS。大量のCRDがありますが、Providersごとに共通の仕様を持たせつつ柔軟に差異を吸収しています。特にCompositionという仕組みではCRD同士を組み合わせて上位の抽象を作るという高度なことをしています。リソース名やフィールド設計も統一感があり、ドキュメントも充実しています。
- Argo CD / Flux: GitOps系ツールのCRD(ApplicationやKustomizationなど)は、実運用で何度も練られてきた経緯があります。特にArgo CDのApplicationSet CRDではv1beta1→v1で大きなスキーマ改善が行われました。そういったCHANGELOGを読むと、なぜ改善が必要だったのか知ることができます。
- Kubernetes公式リソース: カスタムではありませんが、ServiceやIngressなどよく練られたAPIが多数存在します。出来るだけそれらに似せる(ユーザーにとって直感的になる)よう意識するとよいでしょう。公式のAPI慣例ドキュメン (community/contributors/devel/sig-architecture/api-conventions.md at master · kubernetes/community · GitHub) (community/contributors/devel/sig-architecture/api-conventions.md at master · kubernetes/community · GitHub)】は必読です。
先人の知恵は積極的に取り入れましょう。ただし鵜呑みにしないことも大事です。他プロジェクトにも歴史的経緯や妥協があり、必ずしも全てが最善ではありません。「なぜこうしているのか?」を考え、自分のケースに適用可能か判断する批判的思考も忘れずに。
プロTIP: 実装者視点と利用者視点の両方を持つ – 他のプロジェクトを参考にするとき、自分がそのAPIを使うユーザーだと思って触ってみるのも効果的です。公式ドキュメントを読みながらサンプルマニフェストを書いてみて、「ここ分かりにくいな」と思えば自分のAPIでは改善しよう、とフィードバックできます。また逆に使いやすければお手本にできます。自分のプロジェクトに閉じこもらず、外の世界にある良いAPIに触れることでデザインの引き出しを増やしましょう。
おわりに
長期運用を見据えたKubernetes CRD設計のアンチパターンとベストプラクティスを紹介しました。内容を振り返ると:
- アンチパターン: 外部型の直埋め込み、構造体使い回し、曖昧なフィールド名、CamelCaseの乱用、リスト管理の不備… これらは将来の足かせになりま (Christian Schlotter and Fabrizio Pandini on KubeCon stage sharing anti… | Sean Schneeweiß)】。早めに摘み取りましょう。
- ベストプラクティス: 自前型でコントロール権を持つ、一貫した命名と用語管理(Glossary活用)、読みやすくグルーピングした構造、GitOpsを意識した設計、互換性ポリシーの遵守、Lintツール(KAL)導入、他プロジェクトからの学習… 攻めと守り両面での工夫が大切で (Christian Schlotter and Fabrizio Pandini on KubeCon stage sharing anti… | Sean Schneeweiß)】。
特に**「フィールドの名前一つ一つが重要」という点は心に刻んでおきたいです (Christian Schlotter and Fabrizio Pandini on KubeCon stage sharing anti… | Sean Schneeweiß)】。CRDというのは私たちが定義する一種の言語**です。いい加減に作れば利用者はそのツケを払わされ、丁寧に作れば永く愛されるでしょう。
最後に、冒頭で触れたKubeCon登壇者の言葉を借りれば、**「秘伝のCRDをシンプルで一貫性のあるみんなが快適に扱えるAPI型に変革」**するのが我々の目指すところで (KubeCon + CloudNativeCon Europe 2025: Kubernetes CRD Design for the Long Haul:...)】。本記事の内容が、皆さんのCRD設計を見直す一助になれば幸いです。
ぜひ自分のプロジェクトのCRDを見返して、改善できるところがないかチェックしてみてください。10年後でも通用するCRDを目指して、一緒に頑張りましょう!
参考資料:
- Kubernetes API conventions (公式API設計規約 (community/contributors/devel/sig-architecture/api-conventions.md at master · kubernetes/community · GitHub) (community/contributors/devel/sig-architecture/api-conventions.md at master · kubernetes/community · GitHub)】
- KubeCon EU 2025 「Kubernetes CRD Design for the Long Haul」講演まとめ(Sean Schneeweiß氏の投稿 (Christian Schlotter and Fabrizio Pandini on KubeCon stage sharing anti… | Sean Schneeweiß) (Christian Schlotter and Fabrizio Pandini on KubeCon stage sharing anti… | Sean Schneeweiß)】
- Kubernetes API Linter (KAL) GitHubリポジト (kube-api-linter/README.md at main · kubernetes-sigs/kube-api-linter · GitHub)】
- Cluster API公式ドキュメント(特にGlossaryや各種提案資料】
- Cluster API Issue: 構造体共有に関する議 (Allow user-controlled naming of Machines in Machine collections · Issue #10577 · kubernetes-sigs/cluster-api · GitHub)】
- Kubernetes Blog: "Introducing ClusterClass and Managed Topologies in Cluster API"(Cluster APIのTopology機能紹介 (KubeCon + CloudNativeCon Europe 2025: Kubernetes CRD Design for the Long Haul:...)】