古いバージョンを消したらオブジェクトが消えた - 保存バージョン移行の手順
目標
2つのバージョンを持つCRDを作ってストレージバージョンを上げ、すべてのオブジェクトを書き直したうえでstatus.storedVersionsを整理し、古いバージョンを下げる移行の手順を、最初から最後まで歩いてみます。途中でAPIサーバーがどこで止めてくれるかも、自分で確認します。
なぜ重要なのか
CRDはコードではなくAPI契約です。フィールドを1つ変えるには、新しいバージョンを出し、古いバージョンを使っていた人たちが移行する時間を与えてはじめて、古いものを削除できます。このとき、人々が最もよく抜かすのが、すでに保存されたオブジェクトを書き直す作業です。ストレージバージョンを変えるのは「今後保存する表現」だけを変える作業なので、すでにetcdに入っているバイト列は、そのまま古い表現です。その状態で古いスキーマを削除すると、保存されたオブジェクトを読む方法がなくなります。status.storedVersionsは、まさにその事故を防ぐための記録であり、APIサーバーは、このリストに残っているバージョンを外そうとする試みを、実際に拒否します。このラボでは、その安全装置に一度ぶつかってみたうえで、正しい順序で通過します。
ステップ
/root/crd-version/tunnel-crd.yamlにCRDtunnels.net.labhub.ioを書いてください。グループnet.labhub.io、kindTunnel、複数形tunnels、バージョンは2つです。v1alpha1はserved: true・storage: trueで、deprecated: trueとdeprecationWarningを付け、スキーマにはspec.endpoint(string)・spec.port(integer)だけを置きます。v1beta1はserved: true・storage: falseで、この2つのフィールドに加えてspec.mtu(integer、default: 1400)を置きます。適用した後、2つのバージョンの状態を/root/crd-version/versions-initial.txtに、<버전> served=<참거짓> storage=<참거짓>(プレースホルダーはバージョンとtrueまたはfalseです)の2行で保存してください。- ネームスペース
crd-versionを作成し、/root/crd-version/tunnel-east.yaml(名前t-east、endpoint10.30.0.11、port 4789)と/root/crd-version/tunnel-west.yaml(名前t-west、endpoint10.30.0.12、port 4789)をv1alpha1で適用してください。適用するときに返ってくる警告を標準エラー出力まで受け取って/root/crd-version/deprecation-warning.txtに保存し、その時点のstatus.storedVersionsを/root/crd-version/stored-initial.txtに1行で保存してください。 - 同じオブジェクトを、新しいバージョンの窓で読んでください。
kubectl -n crd-version get tunnels.v1beta1.net.labhub.io t-east -o yamlの出力を、/root/crd-version/as-v1beta1.yamlに保存します。そして、/root/crd-version/conversion-note.txtに2行を書いてください。apiVersion=<읽은 apiVersion>とmtu=<spec.mtu 값, 없으면 none>です(プレースホルダーは順に、読み取ったapiVersionと、spec.mtuの値(なければnone)です)。 /root/crd-version/tunnel-crd-beta-storage.yamlに、ステップ1のCRDをコピーして、v1alpha1のstorageをfalseに、v1beta1のstorageをtrueに変えて適用してください。適用した後、status.storedVersionsを/root/crd-version/stored-after-flip.txtに1行で保存してください。- ストレージバージョンを実際に移してください。
crd-versionのすべてのTunnelをv1beta1で読んで、そのまま書き直せばよいです(kubectl get … -o json | kubectl replace -f -)。2つのオブジェクトをどちらも書き直した後、kubectl -n crd-version get tunnels.v1beta1.net.labhub.io -o yamlの出力を/root/crd-version/after-rewrite.yamlに保存してください。 - 古いバージョンを
spec.versionsから外してみてください。kubectl patch crd tunnels.net.labhub.io --type=json -p '[{"op":"remove","path":"/spec/versions/0"}]'です。コマンドの出力と終了コードを/root/crd-version/remove-blocked.txtに集めてください。1行目はremove-rc=<종료 코드>(プレースホルダーは終了コードです)で、その下にサーバーが出した文をそのまま貼り付けます。 - これですべてのオブジェクトがv1beta1で保存されているので、記録を整理してください。
kubectl patch crd tunnels.net.labhub.io --subresource=status --type=mergeで、status.storedVersionsを["v1beta1"]の1つにします。整理した後の値を、/root/crd-version/stored-pruned.txtに1行で保存してください。 /root/crd-version/tunnel-crd-retire.yamlに、ステップ4のCRDをコピーして、v1alpha1のservedをfalseに変えて適用してください。そのあと、kubectl -n crd-version get tunnels.v1alpha1.net.labhub.io t-eastを実行して、結果を/root/crd-version/retired.txtに集めてください。1行目はget-rc=<종료 코드>(プレースホルダーは終了コードです)で、その下にサーバーの文を貼り付けます。最後に、/root/crd-version/version-check.shを作成してください。このCRDのserved・storage・storedを1行ずつ<이름>=<값>(プレースホルダーは名前と値です)として出力し、storedVersionsにストレージバージョンではない値があればPENDING <버전>(プレースホルダーはバージョンです)を出力して0ではないコードで、なければMIGRATEDを出力して0で終了する必要があります。その出力を/root/crd-version/version-check.txtに保存してください。
参考
servedはそのバージョンでリクエストを受け付けるかどうか、storageはそのバージョンで保存するかどうかです。storageはちょうど1つです。- 特定のバージョンで取得するには、
kubectl get <복수형>.<버전>.<그룹> <이름>(プレースホルダーは複数形、バージョン、グループ、名前です)の形を使います。 status.storedVersionsはstatusの窓にあるため、kubectl patch … --subresource=statusで修正します。- この環境にはWebhookサーバーを起動する手段がないため、
conversion.strategyはNoneまでしか扱いません。 - よくあるミス: ストレージバージョンだけを変えて、既存のオブジェクトを書き直さないことです。
- よくあるミス: storedVersionsを先に手で消して、安全装置を無力化してしまうことです。
- 参考: https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definition-versioning/
- 参考: https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/
1つの型に2つのバージョンを同時に提供する
/root/crd-version/tunnel-crd.yamlにCRD tunnels.net.labhub.ioを書いてください。グループnet.labhub.io、kind Tunnel、複数形tunnels、バージョンは2つです。v1alpha1はserved: true・storage: trueで、deprecated: trueとdeprecationWarningを付け、スキーマにはspec.endpoint(string)・spec.port(integer)だけを置きます。v1beta1はserved: true・storage: falseで、この2つのフィールドに加えてspec.mtu(integer、default: 1400)を置きます。適用した後、2つのバージョンの状態を/root/crd-version/versions-initial.txtに、<버전> served=<참거짓> storage=<참거짓>(プレースホルダーはバージョンとtrueまたはfalseです)の2行で保存してください。
1つのCRDの中のバージョンは、同じオブジェクトを別の窓で見ているものなので、保存はたった1つの表現でしか行われません。そのため、storageがtrueのバージョンはちょうど1つでなければならず、残りはservedだけを有効にして、読み書きの窓として残します。deprecationWarningは、そのバージョンでリクエストしたときに、クライアントに返る文です。
非推奨のバージョンで作ると、警告が返ってくる
ネームスペースcrd-versionを作成し、/root/crd-version/tunnel-east.yaml(名前t-east、endpoint 10.30.0.11、port 4789)と/root/crd-version/tunnel-west.yaml(名前t-west、endpoint 10.30.0.12、port 4789)をv1alpha1で適用してください。適用するときに返ってくる警告を標準エラー出力まで受け取って/root/crd-version/deprecation-warning.txtに保存し、その時点のstatus.storedVersionsを/root/crd-version/stored-initial.txtに1行で保存してください。
非推奨の警告は、標準出力ではなく標準エラー出力に出ます。storedVersionsはspecではなくstatusにある記録で、「このCRDでこれまでに実際に保存されたことのあるバージョン」を意味します。まだ1つのバージョンでしか保存していないなら、何が入っているか予想してみてください。
新しいバージョンで読むと、何が変わるのか
同じオブジェクトを、新しいバージョンの窓で読んでください。kubectl -n crd-version get tunnels.v1beta1.net.labhub.io t-east -o yamlの出力を、/root/crd-version/as-v1beta1.yamlに保存します。そして、/root/crd-version/conversion-note.txtに2行を書いてください。apiVersion=<읽은 apiVersion>とmtu=<spec.mtu 값, 없으면 none>です(プレースホルダーは順に、読み取ったapiVersionと、spec.mtuの値(なければnone)です)。
conversion.strategyを別に書かなければNoneで、Noneは保存されたバイト列をそのままにして、apiVersionの表記だけを書き換えて見せます。v1beta1のスキーマにはデフォルト値があるフィールドがありますが、デフォルト値は「保存するとき」に埋められます。このオブジェクトが、まだどのバージョンで保存されているか、考えてみてください。
ストレージバージョンを上げる
/root/crd-version/tunnel-crd-beta-storage.yamlに、ステップ1のCRDをコピーして、v1alpha1のstorageをfalseに、v1beta1のstorageをtrueに変えて適用してください。適用した後、status.storedVersionsを/root/crd-version/stored-after-flip.txtに1行で保存してください。
この1行を変えると、「今後保存するときに使う表現」だけが変わります。すでにetcdに入っているオブジェクトのバイト列は、そのままです。そのため、storedVersionsは減らず、むしろ増えます。そのリストが何を意味するのか、もう一度読んでみてください。
すべてのオブジェクトを1回ずつ書き直す
ストレージバージョンを実際に移してください。crd-versionのすべてのTunnelをv1beta1で読んで、そのまま書き直せばよいです(kubectl get … -o json | kubectl replace -f -)。2つのオブジェクトをどちらも書き直した後、kubectl -n crd-version get tunnels.v1beta1.net.labhub.io -o yamlの出力を/root/crd-version/after-rewrite.yamlに保存してください。
書き直しが本当に起きたかどうかは、新しいバージョンにだけあるデフォルト値が証拠になります。ストレージバージョンで書く瞬間に、APIサーバーがそのフィールドを埋めるからです。オブジェクトが数千個ある実際の運用では、この作業を一度に回さず、分けて回します。毎回etcdへの書き込みが発生するからです。
まだ削除できないと、APIサーバーが止める
古いバージョンをspec.versionsから外してみてください。kubectl patch crd tunnels.net.labhub.io --type=json -p '[{"op":"remove","path":"/spec/versions/0"}]'です。コマンドの出力と終了コードを/root/crd-version/remove-blocked.txtに集めてください。1行目はremove-rc=<종료 코드>(プレースホルダーは終了コードです)で、その下にサーバーが出した文をそのまま貼り付けます。
この拒否は小言ではなく、安全装置です。リストに残っているバージョンのスキーマを削除すると、その表現で保存されたオブジェクトを読む方法がなくなります。エラーの文がどのフィールドを指しているか、正確に読んでみてください。直すべき場所が、そこに書かれています。
記録を整理してはじめて、扉が開く
これですべてのオブジェクトがv1beta1で保存されているので、記録を整理してください。kubectl patch crd tunnels.net.labhub.io --subresource=status --type=mergeで、status.storedVersionsを["v1beta1"]の1つにします。整理した後の値を、/root/crd-version/stored-pruned.txtに1行で保存してください。
この整理は、人が「書き直しを終えた」と宣言する行為です。APIサーバーは、書き直しを代わりに数えてはくれません。そのため、ステップ5を飛ばしてここから始めると、そのまま事故になります。statusは別の窓なので、普通のpatchでは届きません。
古い窓を閉じて、移行の完了を証明する
/root/crd-version/tunnel-crd-retire.yamlに、ステップ4のCRDをコピーして、v1alpha1のservedをfalseに変えて適用してください。そのあと、kubectl -n crd-version get tunnels.v1alpha1.net.labhub.io t-eastを実行して、結果を/root/crd-version/retired.txtに集めてください。1行目はget-rc=<종료 코드>(プレースホルダーは終了コードです)で、その下にサーバーの文を貼り付けます。最後に、/root/crd-version/version-check.shを作成してください。このCRDのserved・storage・storedを1行ずつ<이름>=<값>(プレースホルダーは名前と値です)として出力し、storedVersionsにストレージバージョンではない値があればPENDING <버전>(プレースホルダーはバージョンです)を出力して0ではないコードで、なければMIGRATEDを出力して0で終了する必要があります。その出力を/root/crd-version/version-check.txtに保存してください。
servedをオフにすると、そのバージョンのエンドポイント自体が消えるため、古いapiVersionで送るリクエストは、リソースが見つからないという応答を受け取ります。保存されたオブジェクトは何事もなくあるのに、その窓からだけ見えないのです。チェックスクリプトは標準出力にだけ書き、ファイルを直接触らないようにしてください。そうすれば、何回実行しても同じ答えが出ます。