CR一枚から下位リソースと状態を作る
目標
CR 1枚を入力にして下位リソースを作り、その結果をstatusに書き戻して、kubectl getの1行がデプロイの状態を語ってくれる構造を、手を動かして完成させます。
なぜ重要なのか
CRをデプロイのインターフェースとして使うときに守るべき規律は、4つあります。1つ目は、specはユーザーが書き、statusはコントローラーが書くことです。コントローラーがspecに手を入れると、Gitリポジトリとクラスターがずれて、次の同期がそれを消します。2つ目は、statusを必ず別のパスで書くことです。サブリソースが有効なのに通常のupdateで書くと、statusが黙って無視されるため、「確かに書いたのに反映されない」という症状として現れます。3つ目は、子に所有者参照を付けることです。親が削除されたときにガベージコレクターが子を自動的に片付けてくれて、コントローラーは「自分が作ったもの」だけを管理すればよいので、クリーンアップのロジックが単純になります。このとき、つなぎ目は名前ではなくuidです。同じ名前の親を削除して作り直すとuidが変わり、古いuidを指している子はすぐに回収されます。4つ目は、observedGenerationで時間差を可視化することです。この値がmetadata.generationより小さければ「statusはまだ古い仕様を基準にしている」という意味で、この1組がないと、ユーザーはstatusを信じてよいのかわかりません。
ステップ
開始前の準備: ラボのPodはラボごとに新しく起動するため、前のラボのクラスターの状態は残っていません。kubectl get crd webservices.apps.labhub.ioの結果が空なら、CRDを書き直して適用し、kubectl create ns crd-labも実行してください。このラボには、v1のadditionalPrinterColumns(Image/Replicas/Tier/Age)、subresources.status、subresources.scale(.spec.replicas/.status.replicas/.status.selector)、そしてproperties.statusの下のreplicas・selector・observedGeneration・conditionsの定義がすべて必要です。statusのスキーマがないと、patchしてもpruningで切り落とされます。
/root/crd/deploy/minimal.yamlに、metadata.name: minimal(ネームスペースcrd-lab)で、spec.image: nginx:1.27だけがあるWebServiceを書いて適用してください。spec.replicasは書かないでください。保存されたオブジェクトのspec.replicasが1になっている必要があります。/root/crd/deploy/storefront.yamlに、metadata.name: storefront、metadata.labels.tier: prod、spec.image: nginx:1.27、spec.replicas: 4、spec.tier: prodのWebServiceを書いて適用してください。crd-labにConfigMapstorefront-configを作成し、metadata.ownerReferences[0]に、apiVersion: apps.labhub.io/v1、kind: WebService、name: storefront、controller: true、そしてuidにはkubectl get webservice storefront -n crd-lab -o jsonpath='{.metadata.uid}'で取得した実際の値を入れてください。storefrontのstatusに、replicas: 4とselector: app=storefrontを書いてください。必ず--subresource=statusを付けたpatchで書く必要があり、使用したコマンドライン全体を/root/crd/deploy/out/status-patch.txtに保存してください。- 同じstatusに
conditionsを追加してください。type: Ready、status: "True"、reason: AllReplicasReady、messageは自由な文、lastTransitionTimeはdate -u +%Y-%m-%dT%H:%M:%SZの形式のRFC3339の時刻です。あわせて、status.observedGenerationをmetadata.generationと同じ値にしてください。 /opt/lab/fixtures/crd/sample-cr.yamlをcrd-labに適用して、sampleを追加してください(これでWebServiceが3つになります)。そのうえで、kubectl label webservice minimal -n crd-lab tier=devでラベルを付け、kubectl get webservice -n crd-lab -l tier=prodの出力を/root/crd/deploy/out/selected.txtに保存してください。このファイルにはstorefrontがあってminimalがない必要があり、tier=prodで絞り込まれるのはちょうど1つでなければなりません。kubectl get webservice -n crd-lab -o custom-columns=NAME:.metadata.name,IMAGE:.spec.image,TIER:.spec.tierの出力を/root/crd/deploy/out/columns.txtに保存してください。ヘッダー1行にリソース3行以上、合計4行以上である必要があります。crd-labにConfigMapstorefront-desiredを作成してください。data.image、data.replicas、data.tierの3つのキーの値は、storefrontのspecから読み取った値と文字列として正確に同じである必要があり、ステップ3と同じ方法でstorefrontを所有者に指定してください。このステップの後も、status.observedGenerationはmetadata.generationと同じである必要があります。
参考
- ラボのPodはラボごとに新しく起動するため、前のラボのクラスターの状態は残っていません。それでも、宣言をファイルとして残しておけば、どのPodでも同じ状態を再現できます。これが宣言的であることの実質的な利点です。
- statusの書き込みの例:
kubectl patch webservice storefront -n crd-lab --subresource=status --type=merge -p '{"status":{"replicas":4}}' - ConfigMapの
dataの値は常に文字列です。数値の4は"4"と書く必要があり、そうしないと適用が拒否されます。 - 所有者参照を入れたオブジェクトは、
kubectl applyの代わりにファイルを作って適用するほうが楽です。uidをシェル変数で受け取って、マニフェストに差し込んでください。 - よくあるミス1: ステップ4で、
--subresource=statusなしでpatchしてしまうことです。サブリソースが有効だとstatusが黙って無視され、エラーが何も出ないまま値が保存されません。 - よくあるミス2: ステップ3で、uidではなく名前だけを合わせてしまうことです。名前が同じでもuidが違えば、ガベージコレクターはその子を孤児とみなして、すぐに削除します。
- よくあるミス3: ステップ8で、specを再度修正してgenerationが上がった後に、observedGenerationを更新しないことです。
最小の仕様でCRを作る
/root/crd/deploy/minimal.yamlに、metadata.name: minimal(ネームスペースcrd-lab)で、spec.image: nginx:1.27だけがあるWebServiceを書いて適用してください。spec.replicasは書かないでください。保存されたオブジェクトのspec.replicasが1になっている必要があります。
必須フィールドを1つだけ書きます。残りを書くと、デフォルト値が埋められたのかどうか確認できなくなるため、空のままにしてください。
全体の仕様を埋めたCRを作る
/root/crd/deploy/storefront.yamlに、metadata.name: storefront、metadata.labels.tier: prod、spec.image: nginx:1.27、spec.replicas: 4、spec.tier: prodのWebServiceを書いて適用してください。
specの値とmetadataのラベルは、別の場所です。前者はコントローラーが読む意図で、後者はセレクターで選ぶための索引です。両方が必要です。
所有者参照で子を結び付ける
crd-labにConfigMap storefront-configを作成し、metadata.ownerReferences[0]に、apiVersion: apps.labhub.io/v1、kind: WebService、name: storefront、controller: true、そしてuidにはkubectl get webservice storefront -n crd-lab -o jsonpath='{.metadata.uid}'で取得した実際の値を入れてください。
所有者参照は、名前ではなくuidでつながります。先に親のuidを取得して、その値を入れてください。apiVersionはグループとバージョンを一緒に書き、主コントローラーであることを示すブール値のフィールドも必要です。
statusサブリソースに観測値を書く
storefrontのstatusに、replicas: 4とselector: app=storefrontを書いてください。必ず--subresource=statusを付けたpatchで書く必要があり、使用したコマンドライン全体を/root/crd/deploy/out/status-patch.txtに保存してください。
通常のpatchでは、statusは無視されます。status専用のパスを指定するオプションがあり、そのオプションを使ったコマンドそのものをファイルに残す必要があります。セレクターは、키=값(プレースホルダーはキーと値です)の形の文字列です。
標準のconditionsとobservedGenerationを埋める
同じstatusにconditionsを追加してください。type: Ready、status: "True"、reason: AllReplicasReady、messageは自由な文、lastTransitionTimeはdate -u +%Y-%m-%dT%H:%M:%SZの形式のRFC3339の時刻です。あわせて、status.observedGenerationをmetadata.generationと同じ値にしてください。
Ready条件には、機械が読む理由コードと、人が読む説明の両方が必要です。時刻はRFC3339の形式でなければならず、処理した世代番号はmetadataの値と同じである必要があります。
ラベルセレクターでCRを選ぶ
/opt/lab/fixtures/crd/sample-cr.yamlをcrd-labに適用して、sampleを追加してください(これでWebServiceが3つになります)。そのうえで、kubectl label webservice minimal -n crd-lab tier=devでラベルを付け、kubectl get webservice -n crd-lab -l tier=prodの出力を/root/crd/deploy/out/selected.txtに保存してください。このファイルにはstorefrontがあってminimalがない必要があり、tier=prodで絞り込まれるのはちょうど1つでなければなりません。
specの値ではなく、metadataのラベルで絞り込まれます。prodで絞り込まれるものがちょうど1つになるように、ほかのCRには別の値を付けてください。
custom-columnsで必要なフィールドだけを取り出す
kubectl get webservice -n crd-lab -o custom-columns=NAME:.metadata.name,IMAGE:.spec.image,TIER:.spec.tierの出力を/root/crd/deploy/out/columns.txtに保存してください。ヘッダー1行にリソース3行以上、合計4行以上である必要があります。
CRDに定義したカラムとは別に、参照するときにカラムを直接指定できます。머리글:JSON경로(プレースホルダーはヘッダーとJSONパスです)をカンマでつなぎます。
CRの仕様から望ましい状態のオブジェクトを作る
crd-labにConfigMap storefront-desiredを作成してください。data.image、data.replicas、data.tierの3つのキーの値は、storefrontのspecから読み取った値と文字列として正確に同じである必要があり、ステップ3と同じ方法でstorefrontを所有者に指定してください。このステップの後も、status.observedGenerationはmetadata.generationと同じである必要があります。
値を手で書き写さずに、CRから読み取ってそのまま入れてください。3つの値がCRと1つでも違ってはならず、親とのつながりも必要です。specを変更した場合は、処理した世代番号も合わせ直してください。