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

CRDとオペレータ

シェルで調整ループを作る

TT Labで続きを見る

目標

CRの仕様を読んで下位リソースを合わせ、結果をstatusに書き戻す調整ループをシェルスクリプトで自分で実装し、2回目の実行が何も変えないことをオブジェクトのバージョンで証明します。

なぜ重要なのか

このラボで作るのは、コントローラーのバイナリではなく、reconcile関数の本質です。実際のコントローラーでも、reconcileに渡されるのはオブジェクトではなくキーだけで、何が変わったのかは教えてくれません。そのため、reconcileは常に「今ほしい状態は何で、実際は何か」からやり直します。この設計が強制するのが冪等性です。すでに合っている状態でもう一度書くと、新しいwatchイベントが生まれ、そのイベントがまたreconcileを呼んで、自分自身を無限にトリガーします。そのため、冪等性の本当の判定基準は「エラーが出なかった」ではなく、「下位オブジェクトのresourceVersionが変わっていない」ことです。もう1つ重要な区別が、エラーと再キューです。依存先がまだ準備できていないのは失敗ではないので、エラーとして返してはいけません。エラーは指数バックオフを積み上げ、ログを汚し、そのオブジェクトのリトライ間隔が長くなって、本当に問題が起きたときの反応が遅くなります。最後に、ファイナライザーは「削除=すぐに消える」という直感を壊す仕組みです。削除リクエストは削除時刻を刻むだけで、reconcileがもう一度呼ばれたその瞬間が、外部リソースを後片付けする最後のチャンスです。

ステップ

開始前の準備: ラボのPodはラボごとに新しく起動するため、前のラボのクラスターの状態は残っていません。kubectl get crd webservices.apps.labhub.ioの結果が空なら、CRDを書き直して適用し、kubectl create ns crd-labも実行してください。このラボでは、subresources.statusと、properties.statusの下のreplicas・observedGeneration・conditionsの定義が必ず必要で、specにはimage(required)、replicas(default 1)、tier(default dev)が必要です。

  1. まず、crd-labにWebService checkoutを作成してください(spec.image: nginx:1.27、spec.replicas: 3、spec.tier: dev)。そのあと、/root/op/reconcile/reconcile.shを作成し、chmod +xしてください。このスクリプトは、kubectl getでCRのspec(desired)と、下位のConfigMap checkout-desired(actual)を読み取り、/root/op/reconcile/out/observe.jsonに{"desired": {...}, "actual": {...}}の形で保存する必要があり、desired.imageはCRのspec.imageと正確に同じでなければなりません。
  2. 1回目の実行の判断を、/root/op/reconcile/out/decision-1.jsonに保存してください。actionはcreate、reasonはなぜそう判断したかを書いた文字列、targetは何についての判断かを示すもの(例: configmap/checkout-desired)を入れます。
  3. スクリプトが判断どおりに実行するようにしてください。crd-labにConfigMap checkout-desiredを作成しますが、data.imageはCRのspec.imageの値である必要があり、metadata.ownerReferences[0].uidはcheckoutの実際のuid、metadata.labelsにapp.kubernetes.io/managed-by: webservice-controllerがある必要があります。
  4. kubectl get cm checkout-desired -n crd-lab -o jsonpath='{.metadata.resourceVersion}'の値を/root/op/reconcile/out/rv-before.txtに保存し、スクリプトをもう一度実行した後、同じ値を/root/op/reconcile/out/rv-after.txtに保存してください。2つの値が同じである必要があり、2回目の判断は、/root/op/reconcile/out/decision-2.jsonにactionがnoopとして入っている必要があります。
  5. スクリプトがstatusを書き戻すようにしてください。checkoutのstatus.conditionsにtype: Ready、status: "True"の条件を入れ、status.observedGenerationをmetadata.generationと同じに、status.replicasをspec.replicasと同じにしてください。スクリプトの中には、--subresource=statusという文字列が実際に入っている必要があります。
  6. /root/op/reconcile/out/requeue.jsonを作成してください。actionはrequeue、after_secondsは0より大きい整数、is_errorはfalseです。そして、/root/op/reconcile/out/backoff-note.txtに、次の2つをそれぞれ1文以上で書いてください。リトライ間隔が指数的に伸びるバックオフがなぜ必要なのか、そして、「まだ準備ができていない」をすべてエラーとして処理するとエラーログが暴走し、1つのオブジェクトがキューを飢えさせてしまうという問題です。
  7. crd-labにWebService ephemeralを作成し、metadata.finalizersにwebservice.labhub.io/cleanupを入れてください。ステップ3と同じ方法で、子のConfigMap ephemeral-desiredも作成します。そのうえで、kubectl delete webservice ephemeral -n crd-lab --wait=falseで削除をリクエストし、その直後のオブジェクト全体を/root/op/reconcile/out/terminating.jsonに保存してください(このファイルには、metadata.deletionTimestampとmetadata.finalizersの両方が見えている必要があります)。そのあと、子のConfigMap ephemeral-desiredを削除して、何を後片付けしたかを/root/op/reconcile/out/cleanup.txtに書き、ファイナライザーを削除して、ephemeralが実際に消えるようにしてください。
  8. crd-labにWebService billingとsearchを追加で作成し(それぞれimageとreplicasを持たせる)、Reconcilerでcheckout・billing・searchの3つすべてを1周させた後、/root/op/reconcile/out/reconcile-report.jsonを作成してください。itemsは、各要素がnameとaction(create/update/noop/requeueのいずれか)を持つ配列で、summary.noopに何もしなかった件数を、convergedにtrueを入れます。3つのCRすべてで、status.observedGenerationがmetadata.generationと同じである必要があります。

参考

望ましい状態と実際の状態を読む

まず、crd-labにWebService checkoutを作成してください(spec.image: nginx:1.27、spec.replicas: 3、spec.tier: dev)。そのあと、/root/op/reconcile/reconcile.shを作成し、chmod +xしてください。このスクリプトは、kubectl getでCRのspec(desired)と、下位のConfigMap checkout-desired(actual)を読み取り、/root/op/reconcile/out/observe.jsonに{"desired": {...}, "actual": {...}}の形で保存する必要があり、desired.imageはCRのspec.imageと正確に同じでなければなりません。

reconcileの最初のステップは、判断ではなく読み取りです。CRのspecがdesired、下位のオブジェクトがactualです。まだ下位のオブジェクトがないなら、actualが空なのは正常で、スクリプトには実行権限が必要です。

比較して調整の判断を下す

1回目の実行の判断を、/root/op/reconcile/out/decision-1.jsonに保存してください。actionはcreate、reasonはなぜそう判断したかを書いた文字列、targetは何についての判断かを示すもの(例: configmap/checkout-desired)を入れます。

判断はactionだけで終わってはいけません。なぜそう判断したのかと、何についての判断なのかを一緒に残しておかないと、あとでログだけを見て追跡することができません。

判断どおりに下位リソースを作る

スクリプトが判断どおりに実行するようにしてください。crd-labにConfigMap checkout-desiredを作成しますが、data.imageはCRのspec.imageの値である必要があり、metadata.ownerReferences[0].uidはcheckoutの実際のuid、metadata.labelsにapp.kubernetes.io/managed-by: webservice-controllerがある必要があります。

値は手で書き写さずに、CRから読み取って入れてください。親とのつながりと、自分が作ったものであることを示す目印のラベルの、両方が必要です。

2回目の実行が何も変えないようにする

kubectl get cm checkout-desired -n crd-lab -o jsonpath='{.metadata.resourceVersion}'の値を/root/op/reconcile/out/rv-before.txtに保存し、スクリプトをもう一度実行した後、同じ値を/root/op/reconcile/out/rv-after.txtに保存してください。2つの値が同じである必要があり、2回目の判断は、/root/op/reconcile/out/decision-2.jsonにactionがnoopとして入っている必要があります。

冪等性の証拠は、ログではなくオブジェクトのバージョンです。2回目の実行の前後で、下位オブジェクトのresourceVersionをそれぞれ記録して比較してください。内容が同じなら、サーバーは書き直しません。

調整の結果をstatusに書き戻す

スクリプトがstatusを書き戻すようにしてください。checkoutのstatus.conditionsにtype: Ready、status: "True"の条件を入れ、status.observedGenerationをmetadata.generationと同じに、status.replicasをspec.replicasと同じにしてください。スクリプトの中には、--subresource=statusという文字列が実際に入っている必要があります。

statusは、specとは別のパスで書きます。そのパスを指定する文字列が、スクリプトの中に実際にある必要があります。処理した世代番号と、観測したレプリカ数も、あわせて合わせてください。

再キューの判断とバックオフを整理する

/root/op/reconcile/out/requeue.jsonを作成してください。actionはrequeue、after_secondsは0より大きい整数、is_errorはfalseです。そして、/root/op/reconcile/out/backoff-note.txtに、次の2つをそれぞれ1文以上で書いてください。リトライ間隔が指数的に伸びるバックオフがなぜ必要なのか、そして、「まだ準備ができていない」をすべてエラーとして処理するとエラーログが暴走し、1つのオブジェクトがキューを飢えさせてしまうという問題です。

依存先がまだないのは、失敗ではありません。いつ再び見るかを秒単位で書き、これがエラーではないことを明示してください。メモには、リトライ間隔が伸びる理由と、すべてをエラーとして処理したときに生じる問題を、それぞれ書く必要があります。

ファイナライザーで後片付けをしてから削除する

crd-labにWebService ephemeralを作成し、metadata.finalizersにwebservice.labhub.io/cleanupを入れてください。ステップ3と同じ方法で、子のConfigMap ephemeral-desiredも作成します。そのうえで、kubectl delete webservice ephemeral -n crd-lab --wait=falseで削除をリクエストし、その直後のオブジェクト全体を/root/op/reconcile/out/terminating.jsonに保存してください(このファイルには、metadata.deletionTimestampとmetadata.finalizersの両方が見えている必要があります)。そのあと、子のConfigMap ephemeral-desiredを削除して、何を後片付けしたかを/root/op/reconcile/out/cleanup.txtに書き、ファイナライザーを削除して、ephemeralが実際に消えるようにしてください。

削除リクエストは、即時の削除ではありません。削除時刻が刻まれた直後の姿を先に保存し、後片付けをしてからファイナライザーを外すと、オブジェクトが消えます。待たない削除のオプションがあります。

複数のCRを1周して、レポートを作る

crd-labにWebService billingとsearchを追加で作成し(それぞれimageとreplicasを持たせる)、Reconcilerでcheckout・billing・searchの3つすべてを1周させた後、/root/op/reconcile/out/reconcile-report.jsonを作成してください。itemsは、各要素がnameとaction(create/update/noop/requeueのいずれか)を持つ配列で、summary.noopに何もしなかった件数を、convergedにtrueを入れます。3つのCRすべてで、status.observedGenerationがmetadata.generationと同じである必要があります。

Reconcilerは、特定のCR専用ではありません。複数の対象を回りながらそれぞれの判断を集め、すべてが望ましい状態に到達したかを、1行で要約してください。すべての対象の世代番号が合っていてはじめて、収束したことになります。