TT Lab
はじめる
学ぶ 学習パス コース

CNPE — クラウドネイティブプラットフォームエンジニア

CRDは機能ではなく契約です

TT Labで続きを見る

一言でいうと

CRDは、新しい種類のオブジェクトをAPIに登録します。プラットフォームの契約を完成させるには、受け取る値、許可する変更、サポートするバージョン、ステータスを書き込む主体まで決める必要があります。オブジェクトの作成に成功したことは、サービスの準備完了とは違います。

なぜ必要なのか

次は練習の状況です。チームが、データベースを申請するAppClaimを作りました。kubectl applyは成功しましたが、接続先のアドレスがありません。APIはリクエストを保存しただけで、実際にデータベースを作るコントローラーは、まだありません。ここでCRDをもう一度適用しても、解決にはなりません。受付と調整のどちらの段階が抜けているのかを、まず切り分ける必要があります。

CRDそのものは、ワークロードを作成しません。コントローラーを組み合わせて初めて、ユーザーが望む状態を実際の状態に合わせる動作が生まれます。このラボはCRD・検証・RBAC・クォータを扱い、AppClaimコントローラーの実装は含みません。公式Custom Resourcesの概念

どう動くのか

スキーマは形を、CELは関係を検査します

型がintegerだという事実だけでは、replicas <= maxReplicasは保証されません。たとえば9と4は、それぞれ整数ですが、申請した数が上限より多くなっています。2つのフィールドを含むspecスキーマの位置に、以下のルールを置きます。これはCRD全体ではなく、その位置に入れる断片です。

type: object
required: [tier, replicas, maxReplicas]
properties:
  tier:
    type: string
    enum: [bronze, silver, gold]
  replicas: {type: integer, minimum: 1}
  maxReplicas: {type: integer, minimum: 1}
x-kubernetes-validations:
  - rule: "self.replicas <= self.maxReplicas"
    message: "replicas는 maxReplicas 이하여야 합니다"

requiredはその位置のフィールドの欠落を、enumは許可した値の集合を、CELは2つの値の関係を検査します。spec自体も必ず存在しなければならないAPIなら、上位のオブジェクトスキーマにもrequired: [spec]を宣言する必要があります。下位のrequiredが、上位のオブジェクトまで必須にするわけではありません。公式CRDスキーマと検証

不変ルールの位置が比較範囲を決めます

デフォルトの遷移ルールで、oldSelfは対応する以前の値を意味します。今回のラボのようにoptionalOldSelfを使わないルールは、作成時に以前の値がないため、スキップされます。specの位置のself.tier == oldSelf.tierは、tierだけを比較します。同じ位置でself == oldSelfを使うと、spec全体が同じでなければならないため、正常なreplicasの変更まで止めてしまいます。逆に、tierフィールドそのものに置いたself == oldSelfは、tierだけを比較します。文字列を暗記するよりも、ルールが付いている位置を確認してください。公式CEL遷移ルールの説明

不変性は、CELだけで実装できる概念ではありません。ここではCRDに組み込まれたCELを選びましたが、別途admissionの検証を使う設計もあります。また、最新のKubernetesのoptionalOldSelf: trueは、以前の値がない場合でもルールを評価し、oldSelfをOptional型に変えます。したがって、「oldSelfがあれば、作成のときには常に実行されない」と一般化すると間違いです。optionalOldSelfの条件と動作

サポートするバージョンと保存バージョンは、別々の約束です

項目 意味 これだけでは保証しないこと
served そのバージョンのAPIパスを提供する ほかのバージョンと同じ検証ルール
storage 新しい書き込みに使う保存バージョン。正確に1つ 既存オブジェクトの一括変換の完了
conversion バージョン間の表現の変換方式 外部サービスの作成や、ポリシー検証の代替

2つのバージョンを読めたからといって、オブジェクトが2つできるわけではありません。同じnamespace/nameのオブジェクトを、別のAPI表現で読んでいるのです。保存バージョンを変更しても、既存の保存オブジェクトがすべて自動で書き直されるわけではありません。旧バージョンの削除は、クライアントの移行、保存データの移行、status.storedVersionsの整理まで確認する作業です。古いバージョンを無条件で永久に提供するのも、新しいバージョンの追加と同時に止めるのも、正解ではありません。

デフォルトのconversion.strategy: Noneは、フィールド名の変更を実装してくれません。sizeをcapacityに変える契約なら、別途、変換の設計が必要です。旧バージョンのAPIが開いている間は、そのバージョンでも、正常な入力と禁止された入力を確認してください。公式のバージョン管理と削除手順

statusは別オブジェクトではなく、別の書き込み経路です

subresources.status: {}を有効にすると、通常のオブジェクトに対するPOST・PUT・PATCHは、statusの変更を無視します。/statusに送る変更は、status以外の変更を無視します。この分離は、望む値と観測した値を誰が書くかを分けるための仕組みです。statusが自動で埋まることや、「Ready」という文言が真実であることを検証してくれるわけではありません。公式statusサブリソースの契約

したがって、ユーザーがspecを変更する権限と、コントローラーがstatusを変更する権限は、別々に設計します。additionalPrinterColumnsは、人が素早く確認できるように値を表示するだけで、その値を計算するコントローラーの代わりにはなりません。

現場での姿

LabHubの既存のラボを実際のk3s APIで確認したところ、v1はreplicas=9, maxReplicas=4とtier=platinumを拒否しましたが、v1alpha1は同じリクエストを許可しました。保存バージョンがv1だからという理由で、v1のルールだけを要求していたことが原因でした。2つのservedバージョンのスキーマと実際のリクエストを合わせて検査するように直しました。

この事例の核心的な問いは、「ルールが1か所にあるか」ではなく、「ユーザーが利用できるすべてのAPI経路が、同じ契約を守っているか」です。検証の一覧には、バージョンごとの正常な作成、誤った容量、未定義のtier、tierの変更の拒否、replicasの変更の許可を、並べて置きます。拒否だけを確認すると、すべてのリクエストを止めてしまう誤ったルールを見逃します。

次のラボですること

2つのバージョンのAppClaimの契約を定義し、直接サーバーのdry-runリクエストを送ります。正常な値は通過し、誤った値は該当フィールドの検証エラーで拒否されるかを確認してください。コマンドが失敗したというだけで、検証が成功したと言わないでください。次の理論では、これとは別の、主体の権限と払い出し数の上限を扱います。