保存されているものと見せているものは違う
一言でいうと
CRDの複数のバージョンは、同じバイト列を別の窓で見せる仕組みであり、ストレージバージョンを上げることと、実際に移すことは別の作業です。その2つをつなぐ記録がstatus.storedVersionsです。
なぜこの手順が必要なのか
APIは、一度公開すると元に戻しにくいものです。ユーザーがGitにコミットしたマニフェストがあり、etcdには、その表現で保存されたオブジェクトが溜まっています。そのため、フィールドを変える必要があるときにするのは「直す」ことではなく、「新しいバージョンを出す」ことです。
ここで、1つのことを必ず分けて考える必要があります。提供(served)と保存(storage)は別の軸です。
| フラグ | 意味 | いくつまで |
|---|---|---|
served |
このバージョンでリクエストを受け付けるか | 複数 |
storage |
このバージョンの表現でetcdに書くか | ちょうど1つ |
保存は、1つの表現でしか行われません。そのため、v1alpha1で作ったオブジェクトをv1beta1で取得すると、保存されたバイト列を取り出して、リクエストされたバージョンに変換して返します。変換の方式を決めるのがspec.conversion.strategyで、何も書かなければNoneです。
Noneは変換しません。apiVersionの表記だけを書き換えて、そのまま見せます。フィールド名が2つのバージョンで同じときにしか使えない戦略であり、だから危険です。新しいバージョンが追加したフィールドにデフォルト値があっても、その値は見えません。デフォルト値は保存するときに埋められるのに、このオブジェクトはまだ古い表現で保存されているからです。画面にはフィールドがないように見え、その状態をそのまま信じたコントローラーが、誤った判断を下します。
どう動くのか
移行の手順は4つのステップで、順序がすべてです。
1. 새 버전을 served 로 추가한다 (옛 버전도 그대로 제공)
2. storage 를 새 버전으로 옮긴다 (앞으로 저장할 표현만 바뀐다)
3. 기존 오브젝트를 전부 한 번씩 다시 쓴다 (여기서 실제 이전이 일어난다)
4. status.storedVersions 를 정리하고
옛 버전을 served: false → 제거한다
このコードブロックの韓国語の4つの項目は、順に、新しいバージョンをservedとして追加する(古いバージョンもそのまま提供する)、storageを新しいバージョンに移す(今後保存する表現だけが変わる)、既存のオブジェクトをすべて1回ずつ書き直す(ここで実際の移行が起きる)、status.storedVersionsを整理して古いバージョンをserved: falseにしてから削除する、という意味です。
3つ目が、この手順の心臓部です。2つ目を行った瞬間、status.storedVersionsには、古いバージョンと新しいバージョンが両方入ることになります。そのリストは、「このCRDでこれまでに実際に保存されたことのある表現」という意味です。オブジェクトを書き直すと新しい表現で保存されますが、APIサーバーは、古い表現で保存されたものがまだ残っているかどうかを数えてくれません。そのため、4つ目の整理は、人が「書き直しを終えた」と宣言する行為です。
その宣言をしないと、APIサーバーが最後の扉に鍵をかけておきます。storedVersionsに残っているバージョンをspec.versionsから外そうとするとリクエストが拒否され、エラーがどのフィールドのせいなのかも、そのまま教えてくれます。この安全装置がなかったら、古いスキーマを削除した瞬間に、その表現で保存されたオブジェクトが読めなくなります。
バージョンを下げるときに使う2つのフィールドも、一緒に知っておくとよいです。deprecated: trueを付けると、そのバージョンでリクエストしたときにクライアントが警告を受け取り、deprecationWarningで文を自分で決められます。警告はリクエストを妨げません。人々が移行する時間を与える仕組みです。
現場での姿
1つ目は、書き直しを飛ばした事故です。最もよくある形は、こうです。ストレージバージョンを上げて、しばらく何の問題もなく(新しく作ったものは新しい表現なので)、数か月後の整理作業で古いバージョンを削除します。その瞬間、古いオブジェクトが一覧から消えます。ストレージにはそのまま残っているのに、読み取るためのスキーマがなくなったからです。storedVersionsを先に手で消して、安全装置を通り抜けてしまった場合が、特に危険です。
2つ目は、静かに間違うコントローラーです。strategy: NoneのCRDで、新しいフィールドにデフォルト値を入れ、コントローラーがそのフィールドを読んで動作を変えるように組んでおきます。新しく作ったオブジェクトにはデフォルト値が埋まっているのに、古いオブジェクトは空です。コントローラーは、同じ種類のオブジェクトを2通りに扱うことになり、その違いは、作られた時点にだけかかっています。書き直しは、この問題も一緒になくします。
3つ目は、オブジェクトが多いときの実務です。オブジェクトが数万個あるなら、書き直しを一度に回しません。1件ごとにetcdへの書き込みであり、watchを見ているすべてのコントローラーがイベントを受け取るからです。普通は、分けて、使用量が少ない時間に、進み具合を数えながら回します。Kubernetesには、この作業を代わりに行ってくれるStorageVersionMigrationという別のAPIがあります(公式の案内)。ただし、手順の骨格は今学んでいるものと同じなので、手で一周してみた人だけが、そのツールが何を代わりにやってくれるのかがわかります。このラボでは、ステップごとに何が変わるのかを、自分で確認します。
このラボ環境の限界
ラボのPodには、Webhookサーバーを起動する手段がありません。そのため、conversion.strategy: Webhookは扱わず、Noneの範囲の中でだけ確認します。言い換えれば、フィールド名が変わる移行は、この環境では試せません。その代わり、Noneが正確に何をして何をしないのかを、オブジェクトで証明します。書き直す前は新しいフィールドのデフォルト値が埋まらず、書き直した後は埋まります。この違いが、移行が実際に起きたという証拠になります。
次のラボですること
2つのバージョンを持つCRDを作って、非推奨の警告を受け取ってみたうえで、ストレージバージョンを上げ、すべてのオブジェクトを書き直して、デフォルト値が埋まることで移行を証明します。そのあと、古いバージョンを削除しようとして、APIサーバーに阻止されるのを見て、正しい順序で記録を整理した後、古い窓を閉じます。最後に、移行が終わったかどうかを一度に判定するチェックスクリプトを作ります。