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

CRDとオペレータ

スキーマ・サブリソース・バージョン — CRDの三つの軸

TT Labで続きを見る

一言でいうと

CRDの作成はフィールドの一覧を書く作業ではなく、検証をどこまでAPIサーバーに任せられるかを設計する作業です。

なぜ必要なのか

コントローラーのコードにif spec.Replicas < 1 { return error }のような検査が溜まり始めると、2つの問題が生じます。1つ目は、その検査が動くタイミングが遅すぎることです。誤ったオブジェクトはすでにetcdに保存されていて、ユーザーはkubectl applyが成功したと信じています。2つ目は、そのルールがどこにもドキュメント化されないことです。ユーザーは、コントローラーのログを読んではじめて何が間違っていたかがわかります。

スキーマに移すと、正反対になります。kubectl applyがその場で失敗し、エラーメッセージがどのフィールドがなぜだめなのかを教えてくれて、さらにenum型なら許可される値の一覧まで一緒に教えてくれます。そして、kubectl explain webservice.specがそのままドキュメントになります。

どう動くのか

CRDを支える柱は3つです。

1) structural schemaは、Kubernetes 1.16以降、すべてのCRDで、すべてのフィールドの型をOpenAPI v3で明示する必要があるという条件です。この条件が整ってはじめて、pruning、デフォルト値の注入、サーバーサイドapply、CEL検証が動作します。ここで、3つをはっきり区別する必要があります。

分類 意味 どんなときに使うか
required 空なら拒否 妥当なデフォルト値がない、核心的な識別子
default 空ならAPIサーバーが埋める ほとんどの人が同じ値を使うフィールド
何も付けない 空でも許可し、埋めない 本当に任意の機能

デフォルト値を与えられるフィールドをあえてrequiredにすると、ユーザーが毎回同じ値を書くボイラープレートが生まれ、あとで任意のフィールドに下げることも難しくなります。逆に、イメージのように誤ったデフォルト値が危険なフィールドは、明示的に拒否するほうがよいです。

pruningは標準の動作です。スキーマにないフィールドは、保存される前に切り落とされます。タイプミスしたフィールドが黙って消えるのは最初は戸惑いますが、これが「スキーマがそのまま契約」であることを強制する仕組みです。任意のキーと値を受け取る必要がある箇所には、x-kubernetes-preserve-unknown-fields: trueで例外を開けますが、その箇所だけを最小限に開ける必要があります。乱用すると、structural schemaの利点をまるごと失います。

2)サブリソースは、statusサブリソースを有効にすると、specとstatusが別々のエンドポイントになります。ユーザーはspecだけを、コントローラーはstatusだけを書きます。ここから、決定的な性質が1つ導かれます。statusを書いてもmetadata.generationが上がりません。そのおかげで、コントローラーは「ユーザーがspecを変更した」ことと「自分が今statusを書いた」ことを区別でき、これが無限reconcileを防ぐ土台になります。

scaleサブリソースは、specReplicasPath、statusReplicasPath、labelSelectorPathの3つのパスを伝えるだけで、kubectl scaleとHPAを自分の型につなげてくれます。セレクターのパスが必要なのは、HPAがそのセレクターでPodを数えるからです。

3)バージョンは、1つのCRDが複数のバージョンを同時に提供でき、各バージョンが2つのフラグを持つという仕組みです。

保存は1つの表現でしか行われないため、すべてのバージョンは互いに無損失で変換できる必要があります。そして、status.storedVersionsは「これまでにこのCRDで保存されたことのあるバージョン」を記録します。ストレージバージョンを変更した後、既存のオブジェクトを再保存しないと、このリストに古いバージョンが残ります。その状態で古いスキーマを削除すると、保存されているオブジェクトを読めなくなります。バージョン削除事故の大部分は、この再保存のステップを飛ばしたことから起きます。

なお、このラボ環境にはWebhookのエンドポイントを提供する手段がないため、conversion webhookは扱いません。代わりに、strategy: Noneの状態で複数のバージョンを同時に提供しながら、ストレージバージョンのルールを確認します。

現場での姿

1つ目は、CRDの名前のルールでつまずく初日です。metadata.nameは必ず<복수형>.<그룹>(プレースホルダーは複数形とグループです)の形でなければなりません。webservicesとだけ書くと、APIサーバーが拒否します。フィクスチャとして渡した壊れたCRDが、まさにそのケースです。このルールがあるのは、CRDそのものがクラスタースコープで、名前がそのままグローバルで一意なキーになるからです。

2つ目は、カスタムカラムが運用の質を変えることです。kubectl get webservicesと打ったときにNAMEとAGEしか出なければ、誰もそのコマンドを使いません。運用者が障害対応中に知りたい2、3個の項目をカラムとして載せておくと、そのコマンド1つがダッシュボードになります。

3つ目は、enumはドキュメントであることです。enumを入れると、拒否メッセージに許可リストが一緒に表示されます。ユーザーはWikiを探し回らなくても、エラーメッセージを読むだけで答えがわかります。検証をスキーマに移す本当の利点は、拒否そのものではなく、この案内です。

次のラボですること

2つのラボが続きます。最初のラボでは、apps.labhub.io/v1のWebServiceをゼロから作成します。名前のルール、スキーマ、カスタムカラム、statusとscaleのサブリソース、そしてv1alpha1とv1の2つのバージョンです。2つ目のラボでは、わざとルールに違反したリソースを投入して、APIサーバーがどんな文言で拒否するかを集め、デフォルト値の注入とpruningを目で確認したうえで、CELルールでフィールド間の制約までスキーマに入れて、検証マトリクスを作ります。