CNPA — クラウドネイティブプラットフォームエンジニアリングアソシエイト
KubernetesのAPIはなぜプラットフォームの共通語になったのか
一言でいうと
Kubernetesが勝ったのは、コンテナオーケストレーションの競争ではなく、API仕様の競争でした。宣言的なリソースと調整コントローラーという一対のパターンがコンテナの外にまで広がり、そのため、プラットフォームAPIを新しく作るときはCRDが標準の選択肢になりました。
なぜ必要なのか
プラットフォームチームが「サービスを1つ立ち上げる」ことを簡単にしようとすると、たいてい次のような道をたどります。
- Wikiに手順を書きます → 誰も最新の状態に保ちません。
- シェルスクリプトを作ります → 実行環境ごとに結果が違い、失敗すると途中の状態が残ります。
- 社内のWebアプリを作ります → 状態の保存、認証、監査、リトライ、並行制御をすべて自前で実装する必要があります。しかも、そのアプリが新たなSPOFになります。
3つ目を最後まで作ってみると、結局何を作り直しているのかに気づきます。状態ストア、楽観的同時実行制御、認証・認可、監査ログ、監視(watch)、そして調整ループ。どれもKubernetes APIサーバーがすでに持っているものばかりです。
そこで方向が逆になります。プラットフォームAPIを新しく作るのではなく、Kubernetes APIを拡張します。CRDを登録した瞬間に無料でついてくるものは、これだけあります。
- etcdに保存され、バージョンと
resourceVersionによる楽観的ロックがかかります - 既存のRBACがそのまま適用されます(
kubectl auth can-i create webservicesがすぐに動作します) - 監査ログに残ります
kubectl get/describe/edit、-o yaml、--watchがそのまま使えます- OpenAPIスキーマで不正な値を即座に拒否し、デフォルト値を補ってくれます
- GitOpsツールが、ほかのリソースとまったく同じように扱います
これが「Kubernetes APIがプラットフォームの共通言語だ」という言葉の実質です。新しい語彙(CRD)を定義しつつ、文法(API規約)は誰もがすでに知っているものを使うわけです。
どう動くのか
CRD+コントローラー=プラットフォームAPI
2つの部品が必要です。
CRDは語彙を定義します。group、version、kind、スコープ(NamespacedかClusterか)、そしてOpenAPI v3スキーマです。スキーマの役割は、思ったより大きいです。
spec:
versions:
- name: v1alpha1
served: true
storage: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
required: [image]
properties:
image: { type: string }
replicas: { type: integer, default: 2, minimum: 1, maximum: 10 }
public: { type: boolean, default: false }
requiredで、抜けているフィールドを即座に拒否します。minimum/maximumで範囲を強制します。replicas: 20を送ると、APIサーバーが人の読める文で拒否します。defaultはサーバーが補います。ユーザーが書かなければ、保存時点で値が入ります。これが「安全なデフォルト値」の最も安上がりな実装です。additionalPrinterColumnsで、kubectl getの出力に好きな列を表示します。小さく見えますが、開発者体験に大きく貢献します。
コントローラーは、その語彙に意味を与えます。WebServiceというCRを見て、Deployment、Service、HPA、NetworkPolicyを作ってくれる調整ループです。CRDだけあってコントローラーがなければ、そのCRは「構造が検証される設定ファイル」にとどまります。それはそれで役に立ちますが、プラットフォームAPIではありません。
スコープの選択も試験の常連です。Namespacedはテナントの境界の中に収まり、ネームスペースのRBACがそのまま効きます。Clusterスコープは名前が全体で共有されるため、テナント同士で名前が衝突し、RoleではなくClusterRoleでしか権限を与えられません。テナントが作るリソースは、ほぼ常にNamespacedであるべきです。
抽象化が漏れる瞬間
優れたプラットフォームAPIは「必要なことだけを尋ねます」。WebServiceは、イメージと、必要ならreplicas程度だけを尋ね、残り(ラベル規約、セキュリティコンテキスト、リソースのデフォルト値、オブザーバビリティ用のアノテーション、ネットワークポリシー)はコントローラーが埋めます。
ところが、必ず漏れる日が来ます。
- あるチームがサイドカーを付けなければならなくなります。
- 特定のノード(例: 32GB VRAMのGPU)にだけ載せなければならないワークロードが出てきます。
- 標準とは異なるプローブのパスを使うサービスが出てきます。
このとき「それはサポートしていません」と答えると、そのチームはプラットフォームを捨てて、生のYAMLに戻ります。一度出ていくと戻ってきません。だから、エスケープハッチ(escape hatch)を設計に最初から組み込んでおく必要があります。
| エスケープハッチ | 形態 | リスク |
|---|---|---|
| 部分オーバーライド | spec.podOverridesのような自由フィールド |
何でも入れられると、抽象化が無意味になります |
| 拡張ポイント | extraEnv、extraVolumes、nodeSelectorだけを許可 |
リストの管理コスト |
| レンダリング後の離脱 | 生成されたマニフェストをコピーして直接管理 | その後のプラットフォーム改善を受けられません |
バランスの取りどころは次のとおりです。エスケープハッチは必要ですが、それを使ったことが目に見える必要があります。オーバーライドを使ったサービスにラベルやステータス条件を残しておけば、プラットフォームチームは「この機能は5つのチームがオーバーライドで回避している」というシグナルを受け取り、正式な機能へとプロモーションできます。これが、プラットフォームを製品として回すフィードバックループです。
現場での姿
筆者のホームラボで、この問題がまさにそのままの形で現れました。GPU Feature Discoveryがノードにカード情報を自動でラベル付けします。RTX 3090(24576MB、ampere)、5090(32607MB、blackwell)、4070 Laptop(8188MB、ada-lovelace)は2枚です。ところが、Podがnvidia.com/gpu: 1だけを要求すると、32GBが必要な学習が8GBのノートPC用GPUに載ってしまうことがあります。Kubernetesにとっては、どちらも「GPU 1つ」だからです。
GFDが付けたgpu.memoryラベルは文字列なので、「24GB以上」のような比較セレクターは使えません。そこで、意味ベースのラベルを自分で付けました。gpu.homelab/tier=xlarge|large|smallとgpu.homelab/vram=32g|24g|8gです。これでワークロードは、nodeSelector: gpu.homelab/tier: xlargeで自分に合う等級を選べます。
この1行が、プラットフォームAPI設計の典型です。下層の物理的な事実(カードのモデル名、メモリのバイト数)をそのまま公開せず、ユーザーが判断できる語彙(tier)に翻訳しました。同時にエスケープハッチも残っています。本当に特定のカードが必要なら、GFDの元のラベルで直接セレクトできます。優れた抽象化とは、下の層を隠すものではなく、覆いつつ開けておくものです。
次のラボですること
CRDwebservices.platform.labhub.ioを実際のクラスターに作成し、スキーマ違反が即座に拒否されることと、デフォルト値がサーバーで補われることを自分の目で確認します。続いて、ネームスペースとResourceQuotaとLimitRangeでテナントの境界を引き、RBACでセルフサービスの権限を与えたうえで、ほかのテナントには手を出せないことをkubectl auth can-iで証明します。