本記事は、[2024年夏季インターンシッププログラム](https://www.preferred.jp/ja/news/internship2024/)で勤務された安済翔真さんによる寄稿です。
## はじめに
京都大学工学部情報学科 B4 の安済翔真です。インターンでは、「Validating / Mutating の ポリシーテストのためのツール開発」のテーマで、PFN で使われている Validating Admission Policy(以下、VAP)のテストツール開発に取り組みました。開発したテストツールはOSSとして公開しています。
[https://github.com/pfnet/kaptest](https://github.com/pfnet/kaptest)
このブログでは、インターンシップの中で開発した、VAP のテストツールについて紹介します。
## PFN のクラスタ事情
PFN では、深層学習・AI ワークロード向けのクラウドサービス「Preferred Computing Platform」 (以下、PFCP)の開発を進めています。 PFCP は、PFN が開発したアクセラレータである MN-Core シリーズ を搭載したノードと GPU ノードから構成される Kubernetes クラスタであり、使いなれた Kubernetes のインターフェース・ツールを使用して効率的に利用できます。
PFCP はマルチテナント型で構成されており、複数のユーザが同一のクラスタを共用します。 ユーザが kubectl などを使用して任意のワークロードを作成できるため、他のテナントのユーザに影響を与えず適正な利用をしていただくために、ポリシー制御によるガードレールを厳密に運用する必要があります。
例えば、PFCP では以下のような制限を設けています。
- [Pod Security Standards](https://kubernetes.io/docs/concepts/security/pod-security-standards/) をベースにカスタムした独自のセキュリティポリシーに準拠すること
- 他のテナントのリソースに干渉しないこと
- PFCP の仕様のために必要な設定を満たすこと
上記の制限を実現するためには、Kubernetes APIの権限の制限では十分でないことがあります。そのため、従来 [Validating Admission Webhook](https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/#validatingadmissionwebhook) や [Mutating Admission Webhook](https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/#mutatingadmissionwebhook) が使われてきました。これらの機能を使うことで、リソースがetcdに保存される前に、リソースを検証 / 変更することができます。
一方で、Kubernetes コミュニティの SIG-API Machinery では、kube-apiserver に組み込みの [ValidatingAdmissionPolicy](https://kubernetes.io/docs/reference/access-authn-authz/validating-admission-policy/)(VAP) と [MutationAdmissionPolicy](https://github.com/kubernetes/enhancements/issues/3962)(MAP)の開発が進められています。 Common Expression Language(以下、CEL)を用いて簡単に記述でき、 Webhook サーバ の実装・構築が不要なため、運用負荷が下がると期待されています。 PFN でも Kubernetes v1.30 で VAP が GA になったため積極的に利用を進めています。
しかし、これらの機能には現状テストを書く機能がなく、実際に VAP をデプロイして Kubernetes のマニフェストを適用しないと VAP が正しく書かれているかを検証できません。
もし VAP の記述に誤りがあると、制限を逸脱した利用が可能となってしまうリスクがあります。そのため、クラスタやアドオンのバージョンアップ、VAP のルール更新の都度、網羅的な検証が必須です。しかし、 Kubernetes にマニフェストを順番に apply してテストケースを走らせるというのは大変で時間がかかります。
この課題を解決するために、静的解析ツールを用いて高速に動作する自動テストが必要でした。
## ValidatingAdmissionPolicy の機能説明
VAP では、以下のようにspec.validationsに Admit するオブジェクトの条件を CEL で記述することで、ポリシーを満たさないオブジェクトを Reject することができます。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | `apiVersion:` `admissionregistration.k8s.io/v1` `kind:` `ValidatingAdmissionPolicy` `metadata:` `name:` `"demo-policy.example.com"` `spec:` `failurePolicy:` `Fail` `matchConstraints:` `resourceRules:` `-` `apiGroups``:` `[``"apps"``]` `apiVersions:` `[``"v1"``]` `operations:` `[``"CREATE"``,` `"UPDATE"``]` `resources:` `[``"deployments"``]` `validations:` `-` `expression``:` `"object.spec.replicas <= 5"` |
| --- | --- |
以下のようにして、validations の中で使われるパラメータをポリシー定義から分離し、外から渡すこともできます。
1. パラメータとなるオブジェクトのマニフェストを記述
2. VAP のparamKindにパラメータとなるオブジェクトの kindとapiVersionを指定
3. ValidatingAdmissionPolicyBinding を記述し、VAP の評価時に注入したいパラメータオブジェクトと VAP を紐づけ
例えば、以下のReplicaLimitをパラメータとなるオブジェクトとして使う場合を考えます。
| 1 2 3 4 5 6 | `apiVersion:` `rules.example.com/v1` `kind:` `ReplicaLimit` `metadata:` `name:` `"replica-limit-test.example.com"` `namespace:` `"default"` `maxReplicas:` `3` |
| --- | --- |
このとき、VAP の paramKind には ReplicaLimit を指定します。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | `apiVersion:` `admissionregistration.k8s.io/v1` `kind:` `ValidatingAdmissionPolicy` `metadata:` `name:` `"replicalimit-policy.example.com"` `spec:` `failurePolicy:` `Fail` `paramKind:` `apiVersion:` `rules.example.com/v1` `kind:` `ReplicaLimit` `matchConstraints:` `resourceRules:` `-` `apiGroups``:` `[``"apps"``]` `apiVersions:` `[``"v1"``]` `operations:` `[``"CREATE"``,` `"UPDATE"``]` `resources:` `[``"deployments"``]` `validations:` `-` `expression``:` `"object.spec.replicas <= params.maxReplicas"` `reason:` `Invalid` |
| --- | --- |
ValidatingAdmissionPolicyBinding の policyName と paramRef にそれぞれ VAP とパラメータとなるオブジェクトの名前を記述することで、パラメータとなるオブジェクトと VAP を結びつけることができます。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | `apiVersion:` `admissionregistration.k8s.io/v1` `kind:` `ValidatingAdmissionPolicyBinding` `metadata:` `name:` `"replicalimit-binding-test.example.com"` `spec:` `policyName:` `"replicalimit-policy.example.com"` `validationActions:` `[``Deny``]` `paramRef:` `name:` `"replica-limit-test.example.com"` `namespace:` `"default"` `matchResources:` `namespaceSelector:` `matchLabels:` `environment:` `test` |
| --- | --- |
このパラメータ機能を用いることで、ポリシーを評価する際のコンテキストや条件をパラメータとして分離し、同じポリシーを再利用することが可能です。PFCPでは、ユーザが属する組織情報をパラメータとして渡すことで、同じポリシーを全組織で共用しています。
## 既存のツールの課題
VAPのテスト機能を提供するツールを調査したところ、2024年8月時点で調べた範囲では、本機能を提供するのは [Kyverno](https://kyverno.io/) だけでした。Kyverno は Validation と Mutation の Admission Webhook を提供する OSS ですが、[VAPで記述されたポリシーをテストする機能が部分的にサポートされています](https://kyverno.io/blog/2023/10/04/applying-validating-admission-policies-using-kyverno-cli/#testing-validatingadmissionpolicies-using-kyverno-test)。Kyvernoのテスト機能では、VAPと検査対象のオブジェクトのマニフェストを記述して、VAPの評価結果(Admit、 Deny、 Error)を検証することができます。
しかし、Kyverno のテスト機能には以下の機能が備わっていませんでした。
- paramKind が使われた VAP に対するテスト
- カスタムリソースに対する VAP のテスト
PFCP では paramKind やカスタムリソースを絡めた VAP を記述する必要があるため、これらの機能を具備したテストツールが必要です。
## ツールのインタフェースの検討
上記の課題を解決するべく、VAP を静的に解析して記述内容を検証するツールを開発しました。ツールの概要としては、VAP と検査対象のオブジェクトを入力として受け取り、そのオブジェクトが VAP に適合するかどうかを出力するというものです。
OSSとして公開し広く使って頂くためには、利用者にとって使いやすいインターフェースを具備することがとても重要です。ツールのインタフェースを決めるにあたって、主に以下の 2 点を検討しました。
- パラメータとなるオブジェクトをどのように指定するか
- オブジェクトの情報をどのように与えるか
### **パラメータとなるオブジェクトの指定方法**
前述のパラメータ機能を使用する際に、 パラメータとなるオブジェクトをテストツールにおいてどのように指定するかを検討しました。
候補として以下の 2 パターンを考えました。
- ValidatingAdmissionPolicyBinding を使って、パラメータとなるオブジェクトを選択する
- ValidatingAdmissionPolicyBinding を使わず、パラメータとなるオブジェクトを直接指定する
前者は、 Kubernetes で VAP とパラメータとなるオブジェクトを紐付ける方法を踏襲した方法になっています。しかし、以下の 2 点の理由から後者を選択しました。
- 今回開発するテストツールは、あくまで「VAP の定義が正しく書かれているか」を調べるツールであって、ValidatingAdmissionPolicyBinding の記述はこのテストツールのテスト対象に含まれていない。テスト条件を記述するために ValidatingAdmissionPolicyBinding を使用すると、記述量が増えて煩雑で分かりにくくなってしまう
- 今回ツールを開発するにあたって参考にした Kyverno CLI では、ValidatingAdmissionPolicyBinding を使わずに VAP だけでテスト可能な設計になっており、インターフェースがシンプルでわかりやすかった
### **オブジェクトの情報の与え方**
テストツールにおいて、検査対象のオブジェクトや、パラメータとなるオブジェクトの情報をどのように与えるかを検討しました。
候補として以下の 2 パターンを考えました。
- Kubernetes の struct を直接与える
- マニフェストを YAML ファイルに記述して、それを読み込む
それぞれのメリット・デメリットは以下の通りです。
| **パターン** | **メリット** | **デメリット** |
| --- | --- | --- |
| Kubernetes の struct を直接与える | Go 言語のライブラリとしてテストコードから直接呼び出すことができ、汎用性が高い | ユーザーが Kubernetes の struct を書く必要がある |
| YAML ファイルを読み込む | ・慣れ親しんだ YAML の形式でリソースを指定することができる ・kubectl get したリソースや IaC 管理しているyamlファイルを、そのままテストケースとして利用できる | ライブラリとして呼び出すことができず、汎用性が低い |
これらのメリット・デメリットを考慮した結果、テストツール内のコアモジュールでは入力に struct を受け取るようにし、CLI ツールとしては YAML ファイルを入力に受け取るようにしました。コアモジュールは外部モジュールとして公開されているため、一般のテストコードからも呼び出すことができます。
## ValidatingAdmissionPolicy をテストするテストツール kaptest
ツールの名称は kaptest としました。初めは VAP Testing Tool という名前を考えていましたが、将来的に[Mutating Admission Policy](https://github.com/kubernetes/enhancements/issues/3962)に対するテストもこのツールで行いたいと考えていたため、Kubernetes Admission Policy Test の略称である kaptest という名称を使うことにしました。
ツールの設計としては、先述の通り汎用性の観点から、コア機能と CLI ツールのための機能に分けることにしました。
### コアモジュールの使用例
コアモジュールは以下のインタフェースを持ちます。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 | `type` `ValidationParams ``struct` `{` `Object runtime.Object` `OldObject runtime.Object` `ParamObj runtime.Object` `NamespaceObj *corev1.Namespace` `UserInfo user.Info` `}` `type` `Validator ``interface` `{` `EvalMatchCondition(p ValidationParams) (*matchconditions.MatchResult, error)` `Validate(p ValidationParams) (*validating.ValidateResult, error)` `}` `func` `NewValidator(policy v1.ValidatingAdmissionPolicy) Validator` |
| --- | --- |
VAP をコンパイルして Validator を生成し、ValidationParams を与えて Validate を呼び出すことで、validations の評価を行うことができます。 既存のテストツールでは対応していなかった CRD や ParamKind、 UserInfo にも対応しています。UserInfo は、VAP の validationsの中で使われる request.UserInfo と対応しており、これを使うことで、リクエストを送信したユーザーの情報を用いたポリシーもテストできます。
また、VAP の matchCondition のテストもできるようになっています。VAP では matchCondition を使うことで、バリデーションの対象となるオブジェクトの条件を CEL の式で記述することができます。EvalMatchCondition では、VAP のmatchCondition にマッチするかを判定し、マッチしない場合はどの式にマッチしなかったかを返します。
### CLI ツールの使用例
CLI ツールは、YAML ファイルで記述された VAP と検査対象のオブジェクトを読み込み、コアモジュールを使って評価を行います。
テストケースのマニフェストを以下のように記述することで、VAP の評価結果を静的にテストすることができます。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | `validatingAdmissionPolicies:` `-` `policy.yaml` `resources:` `-` `resources.yaml` `testSuites:` `-` `policy``:` `simple-policy` `tests:` `-` `object``:` `kind:` `Deployment` `name:` `good-deployment` `expect:` `admit` `-` `object``:` `kind:` `Deployment` `name:` `bad-deployment` `expect:` `deny` `-` `policy``:` `error-policy` `tests:` `-` `object``:` `kind:` `Deployment` `name:` `good-deployment` `expect:` `error` |
| --- | --- |
## 実装したツールを使ってみました

PFN の社内クラスタで実際に使われている VAP に対して実際に kaptest を使用し、テストを行いながらリファクタリングすることで、その価値を体感してみました。今までは、VAP を修正する都度 kube-apiserver にリクエストを送って検証する必要があったため、リファクタリングが大変でした。今回開発したテストツールを使うことで、 ローカルで高速にテストを回しながら安全にリファクタリングを行うことができます。
ここでは、 has() を用いた VAP を [Optional Values](https://github.com/google/cel-spec/wiki/proposal-246) を用いた記述に置き換えてみます。
CEL は、null が設定されたオブジェクトの field を参照するとエラーになってしまうため、参照前に null check が必要です。nullable なフィールドでネストが深い要素を参照するときには、都度 has() を用いた null check を書く必要があります。
また、 VAP の validations には複数行の記述ができず、1行で書き切らなければなりません。可読性を高めるために variables で値を事前計算しておくことはできますが、こちらも1行しか書けません。 variables と validations それぞれで has() が多用され、簡単な処理でも見通しの悪い記述になってしまいます。
例えば、以下の例は、ingressClass が特定の値かどうかを調べる CEL の式になっているのですが、nullable なパラメータに対する検査をしているため has を使った複雑な式になっています。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 | `variables:` `-` `name``:` `annotations` `expression:` `"has(object.metadata.annotations) ? object.metadata.annotations : {}"` `-` `name``:` `ingressClassInAnn1` `expression:` `"'projectcontour.io/ingress.class' in variables.annotations ? variables.annotations['projectcontour.io/ingress.class'] : ''"` `-` `name``:` `ingressClassInAnn2` `expression:` `"'kubernetes.io/ingress.class' in variables.annotations ? variables.annotations['kubernetes.io/ingress.class'] : ''"` `-` `name``:` `isContourExternalIngressClass` `expression:` `>-` `has(object.spec.ingressClassName) && object.spec.ingressClassName == ``'contour-external'` `\|\| variables.ingressClassInAnn1 == ``'contour-external'` `\|\| variables.ingressClassInAnn2 == ``'contour-external'` |
| --- | --- | --- | --- | --- | --- |
この課題を解決するために、 CEL の [Optional Values](https://github.com/google/cel-spec/wiki/proposal-246) が有効です。Optional 型とメソッドを用いることで、シンプルな記述に置き換えることが可能です。Kubernetes v1.30 であれば Optional Values が利用できます。kaptest でテストケースを書き、テストしながら書き換えを進めたところ、以下のような簡潔な記述に書き直すことができました。
| 1 2 3 4 5 6 7 | `variables:` `-` `name``:` `isContourExternalIngressClass` `expression:` `>-` `object.spec.?ingressClassName == optional.of(``'contour-external'``)` `\|\| object.metadata.?annotations``[``'projectcontour.io/ingress.class'``]` `== optional.of(``'contour-external'``)` `\|\| object.metadata.?annotations``[``'kubernetes.io/ingress.class'``]` `== optional.of(``'contour-external'``)` |
| --- | --- | --- | --- | --- | --- |
## ツールを使ってみて改善した点
ツールを使ってみて、テストケースの設定ファイル作成が手間だと感じたため、設定ファイルを自動で作成するコマンドを開発しました。
複数の VAP のテストを1つの設定ファイルにまとめると、どのリソースがどの VAP のテストのためのものなのか分かりにくくなるため、 VAP ごとに設定ファイルを分けることが望ましいです。しかしその場合、設定ファイルが増えるため、その作成が手間という課題がありました。
この課題を解決するために、kaptest init で設定ファイルを自動生成できるようにしました。改善後の使用感については、ぜひ[公開したツール](https://github.com/pfnet/kaptest)を使って確かめてみてください。
## インターンの感想・謝辞
今回ツールを開発するにあたって、Kubernetesのコードリーディングなどを通してKubernetesへの理解を深めることができました。開発したツールはOSSとして公開することとなり、OSS開発における様々な知見を得ることができました。
最後に、インターン期間中にサポートしてくださったメンターの奥井さん、秋田さん、Cluster Servicesの方々に御礼申し上げます。
## メンターより
安済さんのメンターを務めました、PFN Cluster Services チームの奥井です。
Cluster Services チームでは例年 7 週間のサマーインターンを行っておりますが、安済さんには今年から新設した 2 週間インターンに従事いただきました。オリエンテーションと最終発表、クラスターデータセンター見学などを除くと実質 7 営業日しかなく、短い期間だったにも関わらず、集中して取り組みしっかり成果を出してくれました。ValidatingAdmissionPolicy 関連の Kubernetes コードリードから始まり、アップストリームのモジュールの効率的な活用、ユーザインターフェース設計、実装、CI/CD 整備など、開発の一連のサイクル全体に精力的に取り組んでくれました。ありがとうございます。
OSS 公開したツールについては、PFN 社内クラスターの ValidatingAdmissionPolicy の検証にすでに使い始めています。安済さんの取り組みのおかげで使い勝手の良いツールに仕上がったと思いますので、ValidatingAdmissionPolicy のテストを効率化したいとお考えの方はぜひ使ってみてください。フィードバックを頂けると嬉しいです!
PFN では、機械学習/深層学習そのものの研究だけでなく、それらを支えるクラスタの研究開発、構築、運用も行っています。OSS に対するコントリビューションや開発したツールの OSS 公開も、業務の一環として取り組めます。ご興味がある方、ご応募をお待ちしております。