Capabilities はどこから来るのか、そして crds がリリースの外にある理由
一言でいうと
.Capabilitiesの値は、コマンドごとに取得元が違い、crds/は、テンプレートでもreleaseのマニフェストでもない、別のライフサイクルを持ちます。
なぜ必要なのか
チャート1つで複数のクラスターをサポートし始めると、すぐにこんなコードが入ってきます。
{{- if .Capabilities.APIVersions.Has "policy/v1/PodDisruptionBudget" }}
apiVersion: policy/v1
{{- else }}
apiVersion: policy/v1beta1
{{- end }}
古いクラスターにはpolicy/v1がないので、ベータバージョンを使う、という分岐です。うまく動いているように見えますが、CIでhelm templateでレンダリング検査を実行すると、常にelse側だけが出ます。一方、実際のデプロイでは、if側が出ます。同じチャート、同じ値なのに、結果が違います。
理由は、Capabilitiesの情報の取得元が違うからです。
| コマンド | クラスターに問い合わせるか | KubeVersion |
|---|---|---|
helm template |
いいえ | Helmに埋め込まれたデフォルト値 |
helm template --validate |
はい | 実際のクラスター |
helm install --dry-run |
はい | 実際のクラスター |
helm install --dry-run=server |
はい | 実際のクラスター |
helm install |
はい | 実際のクラスター |
helm templateだけがオフラインです。そして、そのデフォルトのCapabilitiesのリストはとても小さく、実際のクラスターなら当然あるはずのnetworking.k8s.io/v1/Ingressさえ、ないと出ます。これを知らないと、「レンダリングしてみたら、Ingressの分岐が通らない」と言って、チャートを直し始めます。
クラスターなしで分岐をテストするには、Helmに嘘をつかせれば構いません。--kube-version 1.21.0はバージョンを、--api-versions "demo.labhub.io/v1/Widget"はAPIのリストを、付け加えます。付け加えるという点が重要です。デフォルトのリストを置き換えるのではなく、足します。
crdsディレクトリは5つの点で違う
CRDには、鶏と卵の問題があります。CRDが定義するユーザーリソースを、同じチャートが一緒に作ろうとすると、CRDが先にクラスターに入っている必要があります。Helmはこれを、crds/という専用のディレクトリで解決します。そのディレクトリは、普通のテンプレートと、5つの点で違います。
- テンプレートエンジンを通りません。波括弧を書いても、ただの文字です。そのため、CRDを値で条件分岐できません。
- デフォルトのレンダリング結果には出てきません。
helm template --include-crdsを渡す必要があります。 - インストールのとき、ほかのすべてより先に入ります。そのため、同じチャートのテンプレートが、そのCRDを使うオブジェクトを一緒に作れ、最初のインストールでも
.Capabilities.APIVersions.Hasが真になります。 - アップグレードでは手を付けません。チャートの
crds/を直してhelm upgradeを実行しても、クラスターのCRDはそのままです。公式ドキュメントがこれを制限として明記し、CRDの更新は人が直接行うよう案内しています。 - releaseを削除しても残ります。CRDを削除すると、それで作られたすべてのユーザーリソースが一緒に消えるので、Helmはこの判断を人に任せます。
helm get manifestでreleaseのマニフェストを取り出してみると、CRDがありません。releaseがCRDを所有していないという意味で、上の4つ目と5つ目が、ここから導かれます。
代案: テンプレートにCRDを置く方式
crds/の制約(アップグレードで更新されない、条件分岐できない)が困るなら、CRDをtemplates/に置くという選択肢があります。そうすれば、普通のオブジェクトになって、アップグレードで更新され、条件も掛けられます。その代わり、releaseがCRDを所有することになり、releaseを削除するとCRDとそのリソースがすべて消えます。そして、複数のreleaseが同じCRDを使うと、所有権が重なって衝突します。
実務でよく使われる境界は、こうです。CRDをオペレーター(operator)チャートとは別の、分離したチャートにして、クラスター管理者が一度だけインストールし、アプリケーションチャートは、.Capabilities.APIVersions.Hasで存在を確認するだけにします。大きなプロジェクトが、<이름>-crds(プレースホルダーは名前です)チャートを別に出す理由が、これです。
現場での姿
最も多く起きる事故は、「CRDを直してアップグレードしたのに、新しいフィールドが効かない」というものです。helm historyにはリビジョンが積まれ、デプロイは成功として表示されるのに、クラスターのCRDは、古いスキーマのままです。新しいフィールドを使ったユーザーリソースは、スキーマにないフィールドとして扱われ、黙って切り落とされます。この組み合わせが特に悪いのは、どこにも失敗が報告されない点です。デプロイパイプラインに、CRDを別にkubectl applyするステップを置くか、CRD専用のチャートを別に置くのが、定石的な対応です。
2つ目によくあるのは、CIのレンダリング検査が、本番と違う結果を出すことです。helm templateだけを実行すると、Capabilitiesの分岐が常に片側に固定されます。CIで実際のクラスターを使えるなら、--validateや--dry-run=serverを使い、使えないなら、--kube-versionと--api-versionsで対象のクラスターを模倣して、両方のケースをレンダリングしてみるほうが、安全です。
次のラボですること
Capabilitiesをそのまま出力するチャートを作って、オフラインのレンダリングの値を確認し、--kube-versionと--api-versionsで、別のクラスターのふりをしてレンダリングします。Capabilitiesに応じて、ユーザーリソースを入れたり外したりする分岐を作り、crds/にCRDを置いたあと、実際のkwokクラスターにインストールして、マニフェストにCRDがないことを確認します。最後に、CRDを直してアップグレードしてもクラスターがそのままであることを自分で確認し、--validateと--dry-run=serverが、オフラインのレンダリングとどう分かれるかを整理します。