Kubernetesカスタムリソースの設計: リソース参照
Kubernetesでカスタムリソースを作っているとリソースからリソースへの参照 (リソース参照) を作りたい場合があります。例えばIngressのServiceへの参照のようなものです。
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: minimal-ingress
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
ingressClassName: nginx-example
rules:
- http:
paths:
- path: /testpath
pathType: Prefix
backend:
service: # <= 参照
name: test
port:
number: 80
他にもLabel Selectorはある種のリソース参照です。例えばServiceはLabel SelectorでPodを参照しています。
apiVersion: v1
kind: Service
metadata:
name: nginx-service
spec:
selector: # <= 参照
app.kubernetes.io/name: proxy
ports:
- name: name-of-service-port
protocol: TCP
port: 80
targetPort: http-web-svc
Kubernetesのカスタムリソースにおけるリソース参照は便利であったり必要なこともありますが、コントローラの実装は確実に複雑になります。そのため、使いどころを見極めることが必要です。ちょうど最近リソース参照を使うか使わないかの判断が必要な場面があり、少しまとまった知見が得られたので、備忘録も兼ねて書き残します。
そもそもリソース参照は避けるべき
そもそも可能であればリソース間参照は作らないほうがいいと自分は考えます。コントローラの実装が複雑になるからです。根本的な問題はKubernetesのカスタムリソースにおいてリソース参照はOwner Referenceなどの特殊な場合を除いて、実質的にサポートされていないということです。Kube API Serverから見ればリソース名を参照しているフィールドやLabel Selectorなどはただの文字列フィールドで、Kube API Serverはそこに参照が存在することを知りません。
そのため、RDBのForeign Keyのように参照が切れる操作を弾いたりすることはありませんし、リソースの作成時に参照先が存在しなくてもリソースの作成は成功します。したがって、そのようなケースに対処するのはコントローラの責任です。また、参照先リソースが更新されたり削除された際、参照元リソースをWatchしていてもイベントは発火されませんので、コントローラは参照先リソースの変更も含めてWatchをしてReconcileをする必要があります。
もし可能であればリソース参照を使うのを避けて、参照先リソースの内容を参照元にインラインで書けるようにすることを検討するべきです。そうすれば上に挙げたような複雑性を回避できます。
リソース参照を使う意味があるケース
自分の管理下にないリソースを参照する場合
ではリソース参照を使う必要があるケースとはどのような場合でしょうか?わかりやすいケースはKubernetesのビルトインリソースや、自分以外の誰かの管理下にあるリソースを参照する場合はリソース参照を使わざるを得ません。中でもSecretに対する参照はパスワードや証明書を入力に取るようなリソースの場合、なかなか避けることができません。
参照元と参照先のRBACの権限の分離が必要な場合
参照元リソースと参照先リソースに対するRBACの権限を分けたい場合にリソースを分離してリソース参照を使う場合があります。代表的な例はSecretです。Kubernetesのドキュメント にもある通り、Secretはデフォルトでは暗号化なしでディスクに保存されます。これは一見Secretという名前の意味に反しているように思えますが、Podなどの参照元リソースとRBACレベルで権限を分けることによって安全に取り扱うことができるとしています。このようなセキュリティの観点から設定の権限を分離することが望ましい場合はCRDの設定の一部を別のリソースに分離することに意味があります。
似たようなケースで参照元と参照先のリソースを作るユーザの属性 (ペルソナ) が違うという場合にRBACの権限を分ける場合があります。GatewayAPI ではInfrastructure Providers, Cluster Operators, Application Developersという3つのペルソナを定義していて、それぞれGateway Class, Gateway, XXXRouteと作成するリソースの分担があり、RBACの権限もこの分担に従うことが想定されています。リソースの設計段階で明確なペルソナが決まっている場合はリソース参照を使ってこのような分離をすることが有効な場合もあります。
設定の重複を無くしたい場合
設定の重複を排除するために複数のリソースが1つのリソースを参照するような関係を作りたい時にリソース参照を使うことがあります。これは上に挙げた例とは違い、インライン化によって確実に代替可能で、費用対効果によってはインライン化を選択するべきです。重複を減らすモチベーションがユーザのタイプ量を減らすためということであれば、それは必要ないかもしれません。Kubernetes APIは非常に自動化と相性が良く、重複した設定をたくさん書くことがユーザにとって負荷にならないかもしれないからです。
重複があることによってリソースサイズが大きくなり、etcdのディスク容量やUpdate時のネットワークの帯域、Client側のキャッシュの容量などを圧迫することを懸念するのであれば、Updateの頻度や想定されうるリソースの数などを見積もってベンチマークをするべきです。おそらくほとんどの場合は杞憂でしょう。少し重複の話とはずれますが、リソースの肥大化の弊害は確かに存在していて、これはKubernetesがEndpointsをEndpointSliceに移行している理由の一つです。しかし、Googleの発表 の中で出ている数字は5000ノードのクラスタで1MBの大きさのEndpointsにUpdateが発生した場合、トータル5GBのトラフィックが発生するというものです。やはりこれは決して一般的な環境で発生する問題ではなく、Googleスケールの話であると受け止めるべきでしょう。
決断を先延ばしにすることもできる
リソース参照を使うべきかをすぐに判断できない場合、まずはインラインで設定を書くスタイルで始めて、必要に応じてリソース参照を使うスタイルに移行することもできます。その際にAPIの破壊的変更は必要ありません。インラインで設定を書いていたところにOptionalなリソース参照のフィールドを定義して、リソース参照が空でなければ参照されているリソースの側の設定を使い、リソース参照が空であればインラインの設定を使うようにコントローラ側を書き換えれば済みます。
設定重複によるリソースの肥大化や、ペルソナの分離などはリソースの設計段階では想定が難しいことがあります。GatewayAPIは素晴らしい前例ですが、それでさえ長年のIngressの運用経験を元にペルソナの定義に至っているので、おそらく実際に運用をせずにいきなり特定のスケールやペルソナなどを想定しても実際に使えるものにはならないでしょう。それよりはまずはコントローラの実装のシンプルさを取って、後から必要に応じて仕様を拡張する方法を取ったほうがいいのではないかと思います。
まとめ
Kubernetesのカスタムリソースにおけるリソース参照に関してPros/Consや導入の判断軸などに関してまとめました。どなたかの参考になれば幸いです。
Discussion