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

CRDとオペレータ

親を消したのに子が残った - 所有参照と削除の伝播

TT Labで続きを見る

目標

所有者参照の4つのフィールドがそれぞれ何を決めるかを、自分で作って確認し、--cascadeの3種類の違いをオブジェクトで証明したうえで、Terminatingで止まったオブジェクトを診断するレポートを作ります。

なぜ重要なのか

Operatorが作るリソースは、単独では生きていません。親が1つ消えたら、その下に付いているものも一緒に片付けられる必要がありますが、Kubernetesには、この作業を行う参照カウントがありません。その代わり、子が親を指すownerReferencesの1行があり、ガベージコレクターがその参照をたどって片付けます。そのため、参照を間違って書くと、故障が黙って発生します。uidが間違っていると、作ったばかりの子が数秒で消え、ネームスペースをまたぐと、警告イベントを1つだけ残して子が片付けられます。逆に、ファイナライザーは後片付けが終わるまで削除をつかまえておく仕組みなので、その後片付けをするコントローラーがないと、オブジェクトは永遠にTerminatingにとどまります。運用者が実際に出会う画面は、たいていこの2つのどちらかで、どちらも原因を突き止める手順が必要です。

ステップ

  1. /root/op-ownership/site-crd.yamlにCRD sites.own.labhub.ioを書いてください。グループown.labhub.io、kind Site、複数形sites、バージョンはv1を1つで、スキーマにはspec.region(string)だけを置きます。ネームスペースop-ownを作成し、/root/op-ownership/site-alpha.yamlでSite alpha(region kr-east)を適用した後、そのオブジェクトのmetadata.uidを/root/op-ownership/alpha-uid.txtに1行で保存してください。
  2. /root/op-ownership/alpha-good.yamlにConfigMap alpha-good(ネームスペースop-own)を書き、ownerReferencesにSite alphaを書いてください。apiVersion own.labhub.io/v1、kind Site、name alpha、uidはステップ1で得た実際の値です。/root/op-ownership/alpha-bad.yamlには、同じ内容のConfigMap alpha-badを書きますが、uidだけを00000000-0000-0000-0000-000000000000にします。両方を適用して、しばらく待った後、kubectl -n op-own get cmの出力を/root/op-ownership/gc-uid.txtに保存してください。
  3. ネームスペースop-own-remoteを作成し、/root/op-ownership/alpha-remote.yamlにConfigMap alpha-remoteを書いてください。ネームスペースはop-own-remoteなのに、ownerReferencesはop-ownにあるSite alphaを(実際のuidで)指します。適用した後、しばらく待ってから、kubectl -n op-own-remote get eventsの出力を/root/op-ownership/xns-event.txtに保存してください。
  4. /root/op-ownership/site-beta.yamlでSite beta(region kr-west)を作成し、その子としてConfigMap beta-1とbeta-2を/root/op-ownership/beta-children.yamlの1つのファイルに入れて(2つを---でつなぎます)、実際のuidで所有者参照をかけて適用してください。そのあと、kubectl -n op-own delete site beta --cascade=backgroundで削除し、子が消えるまで待った後、kubectl -n op-own get cmの出力を/root/op-ownership/cascade-background.txtに保存してください。
  5. /root/op-ownership/site-gamma.yamlでSite gamma(region jp-east)を作成し、子のConfigMap gamma-1・gamma-2を/root/op-ownership/gamma-children.yamlに入れて、実際のuidでかけて適用してください。kubectl -n op-own delete site gamma --cascade=orphanで削除した後、kubectl -n op-own get cm gamma-1 -o jsonpath='{.metadata.ownerReferences}'の結果を、/root/op-ownership/cascade-orphan.txtにgamma-1-ownerrefs=<값, 비어 있으면 none>(プレースホルダーは値(空ならnone)です)の1行で保存してください。
  6. /root/op-ownership/site-delta.yamlでSite delta(region us-west)を作成し、/root/op-ownership/delta-child.yamlでConfigMap delta-1を作成してください。所有者参照に、実際のuidとともにblockOwnerDeletion: trueを入れ、このConfigMap自身に、ファイナライザーown.labhub.io/holdを付けます。そのあと、kubectl -n op-own delete site delta --cascade=foreground --wait=falseを実行し、しばらく後に、kubectl -n op-own get site delta -o jsonを/root/op-ownership/foreground-stuck.jsonに保存してください。このSiteは、このラボが終わるまで、そのままにしておきます。
  7. /root/op-ownership/site-epsilon.yamlでSite epsilon(region eu-west)を、ファイナライザーown.labhub.io/drainとともに作成し、子のConfigMap epsilon-dataを実際のuidでかけて、/root/op-ownership/epsilon-child.yamlで適用してください。kubectl -n op-own delete site epsilon --wait=falseを実行した直後のオブジェクトを/root/op-ownership/finalizer-pending.jsonに保存し、後片付けの作業(子のConfigMapを直接削除する)を行った後、何を後片付けしたかを/root/op-ownership/epsilon-cleanup.txtに1行以上書いてください。最後に、ファイナライザーを外して、Siteが実際に消えるようにしてください。
  8. /root/op-ownership/stuck-report.shを作成してください。op-ownで、(1)deletionTimestampが刻まれたSiteをSTUCK Site/<이름> finalizers=<쉼표로 이은 목록>(プレースホルダーは名前と、カンマでつないだ一覧です)として、(2)blockOwnerDeletionがtrueの所有者参照を持つConfigMapをBLOCKER ConfigMap/<이름> owner=<종류>/<이름> finalizers=<목록>(プレースホルダーは順に、名前、種類と名前、一覧です)として出力し、2種類をまとめてソートして標準出力にだけ出力します。STUCKの行が1つでもあれば0ではないコードで、なければNONEを出力して0で終了する必要があります。出力を/root/op-ownership/stuck-report.txtに保存し、誰が何をなぜ止めているかを/root/op-ownership/diagnosis.txtに、人が読める文で書いてください(止めている子の名前と、そのファイナライザーの名前が必ず入っている必要があります)。

参考

親になる型を作る

/root/op-ownership/site-crd.yamlにCRD sites.own.labhub.ioを書いてください。グループown.labhub.io、kind Site、複数形sites、バージョンはv1を1つで、スキーマにはspec.region(string)だけを置きます。ネームスペースop-ownを作成し、/root/op-ownership/site-alpha.yamlでSite alpha(region kr-east)を適用した後、そのオブジェクトのmetadata.uidを/root/op-ownership/alpha-uid.txtに1行で保存してください。

所有者参照では、名前は人が読む値で、本当の鍵はuidです。同じ名前のオブジェクトが削除されて作り直されるとuidは変わりますが、この性質が、続くステップの核心になります。uidはkubectl get … -o jsonpathで取り出してください。

uidを間違えると、ガベージコレクターが子を削除する

/root/op-ownership/alpha-good.yamlにConfigMap alpha-good(ネームスペースop-own)を書き、ownerReferencesにSite alphaを書いてください。apiVersion own.labhub.io/v1、kind Site、name alpha、uidはステップ1で得た実際の値です。/root/op-ownership/alpha-bad.yamlには、同じ内容のConfigMap alpha-badを書きますが、uidだけを00000000-0000-0000-0000-000000000000にします。両方を適用して、しばらく待った後、kubectl -n op-own get cmの出力を/root/op-ownership/gc-uid.txtに保存してください。

ガベージコレクターは、参照の名前ではなくuidで親を探します。見つからなければ「親がすでに消えた子」とみなして片付けます。この動作のために、コントローラーがuidをキャッシュしておいて、親が作り直された後に使うと、作ったばかりの子がすぐに消えます。待つときは、固定のsleepよりも、消えるまで回る短いループのほうが安全です。

ネームスペースをまたぐ所有はない

ネームスペースop-own-remoteを作成し、/root/op-ownership/alpha-remote.yamlにConfigMap alpha-remoteを書いてください。ネームスペースはop-own-remoteなのに、ownerReferencesはop-ownにあるSite alphaを(実際のuidで)指します。適用した後、しばらく待ってから、kubectl -n op-own-remote get eventsの出力を/root/op-ownership/xns-event.txtに保存してください。

所有者参照は、同じネームスペースの中でしか結べず、クラスタースコープのオブジェクトだけが、ネームスペーススコープの子を持てます。ルールに違反しても、APIサーバーはapplyを止めません。あとでガベージコレクターが処理して、その痕跡をイベントとして残します。イベントの理由(REASON)の語を、そのまま読んでみてください。

backgroundは親から先に削除する

/root/op-ownership/site-beta.yamlでSite beta(region kr-west)を作成し、その子としてConfigMap beta-1とbeta-2を/root/op-ownership/beta-children.yamlの1つのファイルに入れて(2つを---でつなぎます)、実際のuidで所有者参照をかけて適用してください。そのあと、kubectl -n op-own delete site beta --cascade=backgroundで削除し、子が消えるまで待った後、kubectl -n op-own get cmの出力を/root/op-ownership/cascade-background.txtに保存してください。

backgroundはデフォルトです。APIサーバーが親をすぐに削除し、子の後片付けはガベージコレクターに任せます。そのため、コマンドはすぐに戻ってくるのに、子は少しの間残っています。この時間差が、「削除したのにまだ見える」という勘違いの出どころです。

orphanは子を残して、紐だけを切る

/root/op-ownership/site-gamma.yamlでSite gamma(region jp-east)を作成し、子のConfigMap gamma-1・gamma-2を/root/op-ownership/gamma-children.yamlに入れて、実際のuidでかけて適用してください。kubectl -n op-own delete site gamma --cascade=orphanで削除した後、kubectl -n op-own get cm gamma-1 -o jsonpath='{.metadata.ownerReferences}'の結果を、/root/op-ownership/cascade-orphan.txtにgamma-1-ownerrefs=<값, 비어 있으면 none>(プレースホルダーは値(空ならnone)です)の1行で保存してください。

orphanは、子を削除しません。その代わり、ガベージコレクターが子からその所有者参照を取り外します。外さないと、親のない参照が残って、次の後片付けのときに子が消えてしまうからです。Operatorを撤去しながら、そのリソースは残しておきたいときに使う方式です。

foregroundは子を待って止まる

/root/op-ownership/site-delta.yamlでSite delta(region us-west)を作成し、/root/op-ownership/delta-child.yamlでConfigMap delta-1を作成してください。所有者参照に、実際のuidとともにblockOwnerDeletion: trueを入れ、このConfigMap自身に、ファイナライザーown.labhub.io/holdを付けます。そのあと、kubectl -n op-own delete site delta --cascade=foreground --wait=falseを実行し、しばらく後に、kubectl -n op-own get site delta -o jsonを/root/op-ownership/foreground-stuck.jsonに保存してください。このSiteは、このラボが終わるまで、そのままにしておきます。

foregroundは、順序を逆にします。子がすべて消えてはじめて、親を削除します。その待機を表現するために、APIサーバーが親にファイナライザーを1つ付けますが、その名前が何なのかを、保存したJSONから探してみてください。子にファイナライザーがあると、その子が消えられないため、待機が終わりません。

ファイナライザーを外すまで、削除は終わらない

/root/op-ownership/site-epsilon.yamlでSite epsilon(region eu-west)を、ファイナライザーown.labhub.io/drainとともに作成し、子のConfigMap epsilon-dataを実際のuidでかけて、/root/op-ownership/epsilon-child.yamlで適用してください。kubectl -n op-own delete site epsilon --wait=falseを実行した直後のオブジェクトを/root/op-ownership/finalizer-pending.jsonに保存し、後片付けの作業(子のConfigMapを直接削除する)を行った後、何を後片付けしたかを/root/op-ownership/epsilon-cleanup.txtに1行以上書いてください。最後に、ファイナライザーを外して、Siteが実際に消えるようにしてください。

ファイナライザーが付いていると、削除リクエストはdeletionTimestampを刻むだけで止まります。オブジェクトは引き続き取得でき、修正もできますが、新しく作ることはできません。コントローラーがする仕事が、まさにこの区間です。外部のシステムを後片付けしてから、自分のファイナライザーを削除します。リストから要素を1つ外すには、kubectl patch --type=jsonのremove操作が便利です。

Terminatingで止まったものを診断する

/root/op-ownership/stuck-report.shを作成してください。op-ownで、(1)deletionTimestampが刻まれたSiteをSTUCK Site/<이름> finalizers=<쉼표로 이은 목록>(プレースホルダーは名前と、カンマでつないだ一覧です)として、(2)blockOwnerDeletionがtrueの所有者参照を持つConfigMapをBLOCKER ConfigMap/<이름> owner=<종류>/<이름> finalizers=<목록>(プレースホルダーは順に、名前、種類と名前、一覧です)として出力し、2種類をまとめてソートして標準出力にだけ出力します。STUCKの行が1つでもあれば0ではないコードで、なければNONEを出力して0で終了する必要があります。出力を/root/op-ownership/stuck-report.txtに保存し、誰が何をなぜ止めているかを/root/op-ownership/diagnosis.txtに、人が読める文で書いてください(止めている子の名前と、そのファイナライザーの名前が必ず入っている必要があります)。

診断の核心は、2つの一覧を並べて置くことです。止まった親と、その親をつかまえられる子です。年齢や時刻のように、見るたびに変わる値は出力に入れないでください。そうすれば、このレポートをファイルとして残しておいて、あとの結果と比べられます。