CNPA — クラウドネイティブプラットフォームエンジニアリングアソシエイト
削除したクォータが数秒後に復活した
目標
本物のk3s上で、TeamSpaceというプラットフォームAPIに小さなコントローラーを自分で取り付けます。ユーザーがTeamSpaceを1つ作るとネームスペースとクォータが連動して作られ、 誰かが手で変更しても元に戻り、削除するときは後始末を終えてから消えることを、ステップごとに値で確認します。
なぜ重要なのか
CRDはAPIサーバーに新しい名詞を登録するだけで、その名詞に意味を与えるのはコントローラーです。Kubernetesのコントローラーは、望ましい状態(spec)と実際の状態を 絶えず比較して差を縮める調整ループであり、Operatorパターンは、そのループに特定ドメインの運用知識を盛り込む方式です。プラットフォームチームがセルフサービスAPIを 作るときにこの構造に従えば、保存・認可・監査・watchはAPIサーバーが提供し、チームは調整ロジックだけを書けば済みます。 調整はイベントではなく現在の状態を基準に回るため、見逃した通知やコントローラーの再起動があっても最終的に収束し、ユーザーはstatusのobservedGenerationで 「自分の変更が反映されたか」を読み取ります。このラボは、その性質を1つずつ壊しながら確認します。
ステップ
/root/cnpa-op/crd.yamlにCRDteamspaces.platform.labhub.ioを作成して適用してください。グループはplatform.labhub.io、scope: Cluster、kindはTeamSpace(pluralteamspaces)、バージョンv1alpha1でsubresources.statusを有効にし、スキーマはspec.pods(integer、必須、1..50)・spec.cpu(string、デフォルト値"1")・status.observedGeneration(integer)・status.namespace(string)です。次に/root/cnpa-op/alpha.yamlでTeamSpacealpha(spec.pods: 4)を作成し、/root/cnpa-op/before.jsonにuid(alphaのuid)とnamespace_exists(ネームスペースteam-alphaが存在するか、ブール値)を書いてください。- alphaに対して3つの操作を順に行い、そのたびに
metadata.generationを読み取ってください。① ラベルowner=platformの追加、② メインリソースのエンドポイントへ{"status":{"observedGeneration":7}}をmergeパッチ、③spec.podsを6に変更。/root/cnpa-op/generation.jsonにuid、gen_initial(開始時の値)、gen_after_label、gen_after_spec(③のあと)、status_via_main(②の直後のalphaの.statusの値、なければnull)を書きます。 /root/cnpa-op/reconcile.sh(実行権限付き)を作成してください。1回実行すると、すべてのTeamSpace<이름>(プレースホルダーはTeamSpaceの名前です)について、ネームスペースteam-<이름>(ラベルplatform.labhub.io/teamspace: <이름>、そのTeamSpaceを指すownerReference付き)と、その中のResourceQuotateam-quota(podsはspec.pods、requests.cpuはspec.cpu)を合わせ、statusサブリソースにobservedGeneration(現在のgeneration)とnamespaceを記録します。1回実行したあと、/root/cnpa-op/reconcile.jsonにquota_uid(team-alphaのteam-quotaのuid)、namespace_owner_uid(team-alphaのownerReferenceのuid)、second_pass_rv_changed(もう一度実行したとき、クォータのresourceVersionが変わったか、ブール値)を書いてください。/etc/systemd/system/cnpa-op.serviceを作成して、reconcile.shを3秒間隔で延々と実行させ(Restart=always)、enableして開始してください。新しいTeamSpaceを作成すると、人が何もしなくても、数秒以内にネームスペースとクォータが作られる必要があります。- コントローラーが動いている状態で、
team-alphaのteam-quotaを手で削除し、新しいuidで復活するまでにかかった秒数を測ってください。続いて、復活したクォータのpodsを99にパッチして、再び6に戻るかを確認してください。/root/cnpa-op/drift.jsonにdeleted_uid、restored_uid、restore_seconds(整数)、edited_to(99)、reverted_to(戻った値、整数)を書きます。 reconcile.shを修正して、finalizerplatform.labhub.io/cleanupを扱うようにしてください。削除要求のないTeamSpaceにはこのfinalizerを付け(ほかのfinalizerは保持)、deletionTimestampが付いたTeamSpaceは/root/cnpa-op/archive/<이름>.json(プレースホルダーはTeamSpaceの名前です)にname・uid・podsを残したあと、finalizerを外します。その後、TeamSpacebeta(pods: 2、cpu: "500m")を作成し、ネームスペースができたのを確認してから削除してください。/root/cnpa-op/cleanup.jsonにuid(betaのuid)、blocked_seen(削除直後にbetaがdeletionTimestampを持ったまままだ残っているのを確認できたか、ブール値)、namespace_gone(完了後にteam-betaが消えたか、ブール値)を書きます。cnpa-op.serviceを停止し、alphaのspec.podsを8に変更してください。8秒待ってから、alphaのmetadata.generation・status.observedGenerationとクォータのpodsを読み取り、サービスを再起動して両者が一致するまで待ってください。/root/cnpa-op/catchup.jsonにgeneration、observed_while_stopped、quota_pods_while_stopped、observed_after_start、quota_pods_after_start(クォータの値は文字列のまま)を書きます。/root/cnpa-op/report.jsonにcrd_alone_created_namespace(ステップ1)、status_bumps_generation(statusの書き込みでgenerationが上がったか。ステップ2、または現在のalphaで確認)、restore_seconds(ステップ5)、finalizer(名前)、archived(archiveディレクトリのbeta.jsonに書かれたuid)、generations_behind_while_stopped(ステップ7のgenerationとobserved_while_stoppedの差)、trigger(levelまたはedgeのうち、このコントローラーが従う方式)を書いてください。
参考
- VM内にk3sがあり、
kubectl、jq、python3、systemctlが使えます。コンテナイメージは使いません。 - statusの書き込み:
kubectl patch teamspace <이름> --subresource=status --type merge -p '{"status":{...}}'(プレースホルダーはTeamSpaceの名前です)。 - よくある間違い: statusをメインリソースのエンドポイントにパッチしてしまうことです。statusサブリソースがある場合、サーバーは黙って捨て、
patched (no change)とだけ出力します。 - よくある間違い: finalizerを付けるパッチが、既存のリストを丸ごと上書きしてしまうことです。ほかのコントローラーのfinalizerが消えます。
- よくある間違い: コントローラーが止まったままラボを終えることです。ステップ4・5・6・7の採点ツールは、コントローラーが動いていないと通過しません。
- Controllers・Operator pattern・CRD status subresource・Finalizers・Owners and Dependents・CNCF Platforms White Paper
型を登録するだけでは何も起きない
/root/cnpa-op/crd.yamlにCRD teamspaces.platform.labhub.ioを作成して適用してください。グループはplatform.labhub.io、scope: Cluster、kindはTeamSpace(plural teamspaces)、バージョンv1alpha1でsubresources.statusを有効にし、スキーマはspec.pods(integer、必須、1..50)・spec.cpu(string、デフォルト値"1")・status.observedGeneration(integer)・status.namespace(string)です。次に/root/cnpa-op/alpha.yamlでTeamSpace alpha(spec.pods: 4)を作成し、/root/cnpa-op/before.jsonにuid(alphaのuid)とnamespace_exists(ネームスペースteam-alphaが存在するか、ブール値)を書いてください。
CRDはAPIサーバーに新しい名詞を知らせるだけです。その名詞を見て何かを作る側は、まだ誰もいません。ネームスペースが存在するかは、kubectl get nsの終了コードでわかります。
何がgenerationを上げるのか
alphaに対して3つの操作を順に行い、そのたびにmetadata.generationを読み取ってください。① ラベルowner=platformの追加、② メインリソースのエンドポイントへ{"status":{"observedGeneration":7}}をmergeパッチ、③ spec.podsを6に変更。/root/cnpa-op/generation.jsonにuid、gen_initial(開始時の値)、gen_after_label、gen_after_spec(③のあと)、status_via_main(②の直後のalphaの.statusの値、なければnull)を書きます。
generationは、specが変わったときだけAPIサーバーが上げる数字です。メタデータだけが変わるとresourceVersionは上がりますが、generationはそのままです。statusサブリソースを有効にしたCRDでは、メインリソースに送ったstatusの変更をサーバーが捨てます。そのときkubectlが何と出力するかを見てください。
何度回しても同じ結果になる
/root/cnpa-op/reconcile.sh(実行権限付き)を作成してください。1回実行すると、すべてのTeamSpace <이름>(プレースホルダーはTeamSpaceの名前です)について、ネームスペースteam-<이름>(ラベルplatform.labhub.io/teamspace: <이름>、そのTeamSpaceを指すownerReference付き)と、その中のResourceQuota team-quota(podsはspec.pods、requests.cpuはspec.cpu)を合わせ、statusサブリソースにobservedGeneration(現在のgeneration)とnamespaceを記録します。1回実行したあと、/root/cnpa-op/reconcile.jsonにquota_uid(team-alphaのteam-quotaのuid)、namespace_owner_uid(team-alphaのownerReferenceのuid)、second_pass_rv_changed(もう一度実行したとき、クォータのresourceVersionが変わったか、ブール値)を書いてください。
調整は「何が変わったか」ではなく、「いま望ましい状態と実際の状態が同じか」を見ます。だから、何度回しても結果が同じでなければなりません。kubectl applyは、内容が同じなら書き込みません。statusはkubectl patch --subresource=statusで書きます。採点ツールは、このスクリプトをさらに2回実行して、何も書き込まないかを確認します。
一度ではなく、回り続ける
/etc/systemd/system/cnpa-op.serviceを作成して、reconcile.shを3秒間隔で延々と実行させ(Restart=always)、enableして開始してください。新しいTeamSpaceを作成すると、人が何もしなくても、数秒以内にネームスペースとクォータが作られる必要があります。
ExecStartにwhile true; do ...; sleep 3; doneのループをbash -cで入れればよいです。採点ツールは一時的なTeamSpaceを1つ作って、ネームスペースが自動でできるかを確認し、削除します。
削除したクォータが数秒後に復活する
コントローラーが動いている状態で、team-alphaのteam-quotaを手で削除し、新しいuidで復活するまでにかかった秒数を測ってください。続いて、復活したクォータのpodsを99にパッチして、再び6に戻るかを確認してください。/root/cnpa-op/drift.jsonにdeleted_uid、restored_uid、restore_seconds(整数)、edited_to(99)、reverted_to(戻った値、整数)を書きます。
コントローラーは、誰が削除したのかを知りません。次の周回で、望ましい状態と実際の状態を比較するだけです。そのため、復元されたクォータは、名前は同じでも新しいオブジェクトです。ステップ2でspec.podsをいくつに変更したかを思い出してください。
削除する前にやるべきことがある
reconcile.shを修正して、finalizer platform.labhub.io/cleanupを扱うようにしてください。削除要求のないTeamSpaceにはこのfinalizerを付け(ほかのfinalizerは保持)、deletionTimestampが付いたTeamSpaceは/root/cnpa-op/archive/<이름>.json(プレースホルダーはTeamSpaceの名前です)にname・uid・podsを残したあと、finalizerを外します。その後、TeamSpace beta(pods: 2、cpu: "500m")を作成し、ネームスペースができたのを確認してから削除してください。/root/cnpa-op/cleanup.jsonにuid(betaのuid)、blocked_seen(削除直後にbetaがdeletionTimestampを持ったまままだ残っているのを確認できたか、ブール値)、namespace_gone(完了後にteam-betaが消えたか、ブール値)を書きます。
finalizerのあるオブジェクトにdeleteを送ると、APIサーバーは削除せず、deletionTimestampだけを付けます。リストが空になって初めて実際に消えます。ネームスペースはownerReferenceをたどってガベージコレクターが削除するため、コントローラーが直接削除する必要はありません。クラスター外の記録のように、所有関係で表現できないものだけをfinalizerで片付けます。削除直後の状態を見るには、kubectl delete --wait=falseで要求だけを送ってすぐに読み取ってください。採点ツールは、一時的なTeamSpaceをもう1つ作って削除してみます。
停止中の変更を見逃さない
cnpa-op.serviceを停止し、alphaのspec.podsを8に変更してください。8秒待ってから、alphaのmetadata.generation・status.observedGenerationとクォータのpodsを読み取り、サービスを再起動して両者が一致するまで待ってください。/root/cnpa-op/catchup.jsonにgeneration、observed_while_stopped、quota_pods_while_stopped、observed_after_start、quota_pods_after_start(クォータの値は文字列のまま)を書きます。
このコントローラーは、変更イベントを受け取って処理することはしません。毎周回で現在の状態を読むだけなので、停止している間に何が何回変わったかを知らなくてもかまいません。observedGenerationがgenerationより小さいことは「まだこのspecを反映できていない」というシグナルであり、ユーザーがコントローラーの進み具合を読み取る標準的な方法です。
コントローラーがやったことを値として残す
/root/cnpa-op/report.jsonにcrd_alone_created_namespace(ステップ1)、status_bumps_generation(statusの書き込みでgenerationが上がったか。ステップ2、または現在のalphaで確認)、restore_seconds(ステップ5)、finalizer(名前)、archived(archiveディレクトリのbeta.jsonに書かれたuid)、generations_behind_while_stopped(ステップ7のgenerationとobserved_while_stoppedの差)、trigger(levelまたはedgeのうち、このコントローラーが従う方式)を書いてください。
前のステップで残したJSONを読んで計算します。このコントローラーは、変更イベント1つ1つには反応せず、毎回現在の状態全体を読んで合わせます。採点ツールは、同じファイルとクラスターをもう一度照合します。