Blog

2024.10.11

Engineering

Kubernetes Mutating Admission Policyの調査、検証

Kazuki Suda

本記事は、2024年夏季インターンシッププログラムで勤務された大川快さんによる寄稿です。

はじめに

東京工業大学情報理工学院情報工学系D2の大川快と申します。PFN2024 夏季国内インターンシップに参加し、Cluster ServicesチームでKubernetesで今後導入予定の新機能であるMutating Admission Policyの調査、検証を行いました。 Mutating Admission Policyはユーザー定義のルールによるKubernetes APIへのリクエストの書き換えをWebhookを用いずに実現する機能です。本記事ではMutating Admission Policyの説明や Admission Webhook からの置き換え例などを解説します。なおMutating Admission Policyは現時点(2024年9月)では、Pull Request(https://github.com/kubernetes/kubernetes/pull/126497)がUpstreamにマージされていないため仕様が変更される可能性があります。

Admission WebhookによるValidationおよびMutation

 kube-apiserverのAdmission ControllerはKubernetes APIへのリクエストによってオブジェクトが永続化される前に、リクエストの内容を検証(Validation)したり、内容の変更(Mutation)したりします[1,2]。

 kube-apiserverは事前にコンパイルされるため、Kubernetesオブジェクトによって設定できるValidationやMutationのルールには限りがあります。複雑なルールを適用したい場合はAdmission Webhookを用いて外部のサーバーで処理を行います。Admission Webhookを用いることでValidationやMutationの柔軟性を高めることができますが、WebhookによるオーバーヘッドやWebhookを処理するサーバーの開発や運用、保守のコストが発生するという問題があります。

Validating Admission PolicyとMutating Admission Policy

上記の問題を解決するため、Validating Admission PolicyとMutating Admission Policyが提案されました[3]。これらはCEL(Common Expression Language)と呼ばれる式言語を用いて、ValidationやMutationのルールを定義できます。この機能によりWebhookサーバーなしに複雑なルールを適用できます。Validating Admission PolicyはKubernetes v1.30でGAしましたが、Mutating Admission Policyについてはまだ実装されておらず、v1.32でアルファでの追加が目指されています。インターン開始時点で、MutatingAdmissionPolicyを利用可能にしたPull Request(#126497)が出されており、このPull RequestからKubernetesをビルドすることで先んじてMutating Admission Policyを試用できます。本記事ではこのPull Requestの使用してAdmission Webhookを用いて行っているMutationをMutating Admission Policyで置き換えられるか以下の3つのケースについて調査、検証した結果を紹介します。

  • 環境変数 NVIDIA_VISIBLE_DEVICESnone に強制 
  • RDMAジョブの設定の自動化 
  • 拡張リソースをlabelsに付与 

補足: インターン後に本Pull RequestはマージされずにCloseされており、Pull Request #127134 でMutating Admission Policyの実装作業が行われています。

環境変数 NVIDIA_VISIBLE_DEVICESnone に強制

 PFNではオンプレミスの計算機クラスタを運用しており、NVIDIA製GPUを搭載したNodeが存在します。NVIDIA GPUを利用する場合には、以下のように設定することでGPUのリソースを要求できます。

apiVersion: v1
kind: Pod
...
spec:
  containers:
  - resources:
      limits:
        nvidia.com/gpu: 1
    ...

 しかし、NVIDIAのdevice pluginの仕様により、nvidia.com/gpu: 0 を設定したりGPUをリクエストしない場合に、コンテナの中からNodeに搭載されているGPUが意図せずすべて見えてしまう問題があります[7]。PFNでは、この問題に対処するため、GPUがリクエストされていない場合には強制的に環境変数 NVIDIA_VISIBLE_DEVICES none に設定することで、コンテナからGPUが見えないようにしています[8]。 これを実現するためにAdmission Webhookを用いて、envの設定を行っています。例えば、以下のようなマニフェストがあった場合

apiVersion: v1
kind: Pod
...
spec:
  containers:
  - resources:
      limits:
        nvidia.com/gpu: 0

次のように変更することでコンテナからGPUが見えないようにします。

apiVersion: v1
kind: Pod
...
spec:
  containers:
  - resources:
      limits:
        nvidia.com/gpu: 0
    env:
    - name: NVIDIA_VISIBLE_DEVICES
      value: none

また .spec.containers[x].resources.limits.['nvidia.com/gpu'] が設定されていなかった場合も同様です。

Mutating Admission Policy

  マニフェストを自動で書き換えるためにMutating Admission Policyを用いた設定を行います。Mutating Admission Policyを適用するにはMutatingAdmissionPolicyオブジェクトとMutatingAdmissionPolicyBindingオブジェクトの二つを作成する必要があります。MutatingAdmissionPolicyオブジェクトはCEL(Common Expression Language)と呼ばれるGoogleが開発した式言語[9]を用いて定義されたMutationのルールを持ちます。このCELで定義された内容に基づいてMutationが行われます。この処理はkube-apiserver内で行われるため、Webhookが不要になります。

MutatingAdmissionPolicyオブジェクトのマニフェストファイル

 マニフェストファイルの例を使って、MutatingAdmissionPolicyオブジェクトとMutatingAdmissionPolicyBindingオブジェクトをどのように設定するのかを説明します。MutatingAdmissionPolicyオブジェクト定義するマニフェストファイルは以下のようになります。

apiVersion: admissionregistration.k8s.io/v1alpha1
kind: MutatingAdmissionPolicy
metadata:
  name: nvidia-visible-devices-policy.example.com
spec:
  matchConstraints:
    resourceRules:
    - apiGroups:   [""]
      apiVersions: ["v1"]
      operations:  ["CREATE"]
      resources:   ["pods"]
  failurePolicy: Fail
  mutations:
  - patchType: ApplyConfiguration
    reinvocationPolicy: IfNeeded
    expression: >-
     Object {
       spec: Object.spec{
         containers: object.spec.containers.filter(
           ct, quantity(ct.resources.?limits['nvidia.com/gpu'].orValue("0")).asInteger() == 0
         ).map(
           ct,
           Object.spec.containers.item {
             name: ct.name,
             env: [
               Object.spec.containers.item.env.item {
                 name: 'NVIDIA_VISIBLE_DEVICES',
                 value: 'none',
               }
             ]
           }
         ),
         initContainers: object.spec.?initContainers.orValue([]).filter(
           ct, quantity(ct.resources.?limits['nvidia.com/gpu'].orValue("0")).asInteger() == 0
         ).map(
           ct,
           Object.spec.containers.item {
             name: ct.name,
             env: [
               Object.spec.containers.item.env.item {
                 name: 'NVIDIA_VISIBLE_DEVICES',
                 value: 'none',
               }
             ]
           }
         )
       }
     }

 このマニフェストファイルについて説明します。

 .spec.matchConstraints によりPolicyを適用する範囲を設定できます。この例ではPodを作成する際にPolicyが適用されます。

  .spec.mutations ではMutationのルールを定義します。.spec.mutations.patchType には、CELで定義されたオブジェクトとオリジナルのオブジェクトをどのようにマージするかを設定します。
 また .spec.mutations.expression にMutationを定義するCELを記述します。

CELで利用可能な変数

 KubernetesのCEL内では object や  paramsvariables 等の変数を利用できます。

 objectにはMutationされる前のオブジェクトの値が格納されています。例えばオブジェクトのnameを取得したい場合は、object.metadata.name で参照できます。 params はMutatingAdmissionPolicyBindingで設定したパラメータの値を参照できます。こちらについては次の例で説明します。variables は MutatingAdmissionPolicyの .spec.variables で設定したCELの評価値を参照することできます。ただし variables については 2024年9月時点では正常に動作せず、実際に利用できませんでした。

nvidia.com/gpu: 0 もしくは nvidia.com/gpu が設定されていないコンテナの判定

 NVIDIA_VISIBLE_DEVICESnone に設定すべきコンテナは次のように表現できます。

object.spec.containers.filter(
 ct,
 quantity(ct.resources.?limits['nvidia.com/gpu'].orValue("0")).asInteger() == 0
)

ここで filter() はCELのlistやmapに対し、条件に一致する要素だけ取り出す関数です。第一引数が、走査する要素を表し、第二引数が条件式を表しています。また、?limits はOptionalであることを表し、入力されたオブジェクトの limits が設定されていない場合は orValue() に渡された値で評価されるようになります。また xxx.?limits['nvidia.com/gpu'] は xxx.?limits[?'nvidia.com/gpu']と等価なので limits は設定されているが nvidia.com/gpu は設定されていないというケースでも orValue()  に渡された値が評価されます。quantity().spec.containers[x].resources.limits.xxx に指定される値を数値に変換する関数です。このようにしてnvidia.com/gpu: 0 もしくは nvidia.com/gpu が設定されていないコンテナを取り出すことができます。

NVIDIA_VISIBLE_DEVICESnone を設定

次のようにしてNVIDIA_VISIBLE_DEVICES を強制的に none にします。

.map(
 ct,
 Object.spec.containers.item {
   name: ct.name,
   env: [
     Object.spec.containers.item.env.item {
       name: 'NVIDIA_VISIBLE_DEVICES',
       value: 'none',
     }
   ]
 }
)

ここで map() はCELのlistやmapの各要素を変換する関数です。この例ではコンテナを表すオブジェクトに env を指定します。ここで注意が必要なのは、env の他にnameも指定する必要があります。これはnameが一致するObject.spec.containersの要素にCELで定義したオブジェクトをマージするためです。

MutatingAdmissionPolicyBindingオブジェクトのマニフェストファイル

 MutatingAdmissionPolicyオブジェクトに加え、MutatingAdmissionPolicyBindingオブジェクトを作成する必要があります。 MutatingAdmissionPolicyBindingを用いることでMutatingAdmissionPolicyの範囲を制限したり、パラメーターを与えたりします。MutatingAdmissionPolicyBindingオブジェクトがあることにより、適用条件やパラメータの異なるMutatingAdmissionPolicyオブジェクトを複数作成することなく定義できるようになります。

 以下が、MutatingAdmissionPolicyBindingオブジェクトのマニフェストファイルです。

apiVersion: admissionregistration.k8s.io/v1alpha1
kind: MutatingAdmissionPolicyBinding
metadata:
  name: nvidia-visible-devices-binding.example.com
spec:
  policyName: "nvidia-visible-devices-policy.example.com"
  matchResources:
    namespaceSelector:
      matchExpressions:
      - key: kubernetes.io/metadata.name
        operator: NotIn
        values: [ "nvidia-gpu"]

 このマニフェストファイルについて説明します。

 .spec.policyName はBindするMutatingAdmissionPolicyオブジェクトのnameを指定します。

 .spec.matchResources はPolicyを適用する範囲を指定します。この例ではKubernetesの namespaceSelectorを用いて namespaceが 'nvidia-gpu' でないときにPolicyを適用するように設定しています。ほかの指定方法は次のコマンドで調べることができます。

kubectl explain mutatingadmissionpolicybinding.spec.matchResources

Mutating Admission Policyの適用および動作確認

 上で示したマニフェストでMutaingが行われるか動作確認を行います。MutatingAdmissionPolicyオブジェクトのマニフェストファイルをpolicy.yaml、MutatingAdmissionPolicyBindingオブジェクトのマニフェストファイルをbinding.yamlとします。
 Mutating Admission Policyを適用するには以下のコマンドを実行します。

kubectl apply -f policy.yaml -f binding.yaml

 続いて、Podを作成します。以下のマニフェストファイルを作成します。(ファイル名はpod.yamlとします。)

apiVersion: v1
kind: Pod
metadata:
 name: gpu-zero
spec:
 containers:
 - name: main
   image: busybox
   resources:
     limits:
       nvidia.com/gpu: 0

 以下のコマンドでPodを作成します。

kubectl apply -f pod.yaml

 このPodに対して正しくMutationが行われたかを以下のコマンドで確認します。

kubectl get pod gpu-zero -o "jsonpath={.spec.containers[0].env}" | jq -r .

実行結果は以下のようになり、NVIDIA_VISIBLE_DEVICESnone に設定できていることがわかります。

[
 {
   "name": "NVIDIA_VISIBLE_DEVICES",
   "value": "none"
 }
]

RDMAジョブの設定の自動化 

 PFNのクラスタで RDMA で使った複数ノードでのジョブを実行するときには SR-IOV を使ったネットワークを Pod で使えるようにするために複数のリソースやアノテーションをまとめてつける必要がありますが、現状ではユーザーがマニフェストに決まった記述をマニュアルで書いてます。この設定を自動化し、クラスタに詳しくない人でも扱えるようにするため、自動でリソースの設定やアノテーションをMutating Admission Policyを用いて行います。

実現したいこと

 以下のように preferred.jp/rdma: 1 が設定されていた場合に

apiVersion: v1
kind: Pod
...
spec:
 containers:
 - resources:
     limits:
       preferred.jp/rdma: 1
   ...

該当するコンテナに preferred.jp/net[1-4]: 1 がというリソースのリミットを追加し、.metadata.annotations['k8s.v1.cni.cncf.io/networks'] に  networking-rdma/net1, networking-rdma/net2, networking-rdma/net3, networking-rdma/net4 を設定します。期待されるMutationの結果は以下の通りです。

apiVersion: v1
kind: Pod
metadata:
  annotations:
    k8s.v1.cni.cncf.io/networks: "networking-rdma/net1, networking-rdma/net2, networking-rdma/net3, networking-rdma/net4"
  ...
spec:
  containers:
  - resources:
      limits:
        preferred.jp/rdma: 1
        preferred.jp/net1: 1
        preferred.jp/net2: 1
        preferred.jp/net3: 1
        preferred.jp/net4: 1
    ...

マニフェストファイルの例

MutatingAdmissionPolicyオブジェクトとMutatingAdmissionPolicyBindingオブジェクトの例を以下に示します。

apiVersion: admissionregistration.k8s.io/v1alpha1
kind: MutatingAdmissionPolicy
metadata:
  name: rdma-resources-and-annotations-inserting.example.com
spec:
  paramKind:
    apiVersion: mutations.example.com/v1
    kind: RdmaResourcesAndAnnotationsInsertingParam 
  matchConstraints:
    resourceRules:
    - apiGroups:   [""]
      apiVersions: ["v1"]
      operations:  ["CREATE"]
      resources:   ["pods"]
  failurePolicy: Fail
  mutations:
  - patchType: ApplyConfiguration
    reinvocationPolicy: IfNeeded
    expression: >-
     Object {
        metadata: Object.metadata{
          annotations: object.spec.containers.exists(
           ct,
           quantity(
              ct.resources.?limits["preferred.jp/rdma"].orValue("0")
            ).asInteger() == 1
         ) 
         ? { 
            "k8s.v1.cni.cncf.io/networks": object.metadata.?annotations["k8s.v1.cni.cncf.io/networks"].orValue(params.annotations) 
            } : {}
       },
        spec: Object.spec{
          containers: object.spec.containers.filter(
           ct,
           quantity(
              ct.resources.?limits["preferred.jp/rdma"].orValue("0")
            ).asInteger() == 1
         ).map(
           ct,
           Object.spec.containers.item {
              name: ct.name,
              resources: {
                "limits": params.nicLimits
             }
           }
         )
       }
     }
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: rdma-resources-and-annotations-inserting-params.mutations.example.com
spec:
  group: mutations.example.com
  names:
    kind: RdmaResourcesAndAnnotationsInsertingParam 
    listKind: RdmaResourcesAndAnnotationsInsertingParamList
    plural: rdma-resources-and-annotations-inserting-params
    singular: rdma-resources-and-annotations-inserting-param
  scope: Namespaced
  versions:
  - name: v1
    served: true
    storage: true
    schema:
      openAPIV3Schema:
        type: object
        properties:
          nicLimits:
            type: object
            properties:
             preferred.jp/net1: 
                type: integer
             preferred.jp/net2: 
                type: integer
             preferred.jp/net3: 
                type: integer
             preferred.jp/net4: 
                type: integer
          annotations:
            type: string
---
apiVersion: mutations.example.com/v1
kind: RdmaResourcesAndAnnotationsInsertingParam 
metadata: 
  name: rdma-resources-and-annotations-inserting-param.example.com
  namespace: default
nicLimits:
 preferred.jp/net1: 1
 preferred.jp/net2: 1
 preferred.jp/net3: 1
 preferred.jp/net4: 1
annotations: "networking-rdma/net1 networking-rdma/net2 networking-rdma/net3 networking-rdma/net4"
---
apiVersion: admissionregistration.k8s.io/v1alpha1
kind: MutatingAdmissionPolicyBinding
metadata:
  name: rdma-resources-and-annotations-inserting-binding.example.com
spec:
  policyName: rdma-resources-and-annotations-inserting.example.com
  paramRef:
   name: rdma-resources-and-annotations-inserting-param.example.com
   namespace: default

 この例の特徴はMutatingAdmissionPolicyBindingオブジェクトを用いてパラメータを設定している点です。 MutatingAdmissionPolicyBinding spec.paramRef でMutatingAdmissionPolicyに渡すパラメータを設定しています。この例ではCRDを用いてRdmaResourcesAndAnnotationsInsertingParamオブジェクトを作成し、それをパラメータとしています。MutatingAdmissionPolicyBinding .spec.paramRef.name にパラメータのオブジェクトのnameを指定しています。

 続いてMutatingAdmissionPolicyオブジェクトの設定を見てみます。 MutatingAdmissionPolicy .spec.paramKind で受け取るパラメータのapiVersionkindを設定しています。受け取ったパラメータは以下のようにCEL内の変数 params で用いて参照できます。

           Object.spec.containers.item {
             name: ct.name,
             resources: {
               "limits": params.nicLimits
             }
           }

 また、この例ではexist()filter()に同じ条件を渡しているので、variablesを使うことで記述量を削減できます。

拡張リソースをlabelsに付与

  PFNのクラスタでは、GPUやMN-Coreを拡張リソースとして扱っており、Validationや管理を行う上でPodが拡張リソースを用いているかの判定が必要な場合があります。その際、Podが利用している拡張リソースを .metadata.labels に持たせておくと、KubernetesのlabelSelector等を用いて容易に判定ができます。具体的に以下のようなPodのマニフェストファイルがあった場合、

apiversion: v1
kind: pod
...
spec:
  initcontainers:
  - resources:
      limits:
        nvidia.com/gpu: 1
    ...
  containers:
  - resources:
      limits:
        cpu: 200m
        preferred.jp/mncore: 1
    ...
  - resources:
      limits:
        preferred.jp/mncore: 1
        preferred.jp/with-binarysi: 1mi
    ...

以下のようにMutationしたいです。

apiversion: v1
kind: pod
metadata:
  labels:
    pod-resource.preferred.jp/preferred.jp--mncore: ""
    pod-resource.preferred.jp/preferred.jp--with-binarysi: ""
    pod-resource.preferred.jp/nvidia.com--gpu: ""
...

CELの限界

 しかし次のような理由から、Mutating Admission Policyを用いてこのMutationを実現することは難しいのではないかという結論に至りました。

  • list から mapを作成できない。 
    • .spec.containersはlist型なのに対し、.metadata.labelsはmap型
    • listの要素をkeyとしてmapを構築する方法がない。
  • 配列のconcatenateができない
    • .spec.containers は各要素が複数の拡張リソースを持つことがあるので二重の配列になる。
    • これらをまとめる concatenate の処理が必要だが、そのような機能がない

CELのKubernetes拡張に自前の実装を追加

  KubernetesではCEL上で使える拡張ライブラリがあります。このライブラリに上記の処理を行う関数を追加しました。具体的には

  • listToMap(x,y)
    • list x の要素をkey, list y の要素をvalueとしたmapを返す
  • e.sum()
    • listを要素に持つlist e をconcatenateして平滑化したlistを返す

の二つを追加しました。

 また、ExtendedResourseかどうかの判定はv1.31から導入されるformatというライブラリを用いれば可能ですが、CELが複雑になるため直接判定できる関数を追加しました。

 CELの拡張ライブラリはKubernetesのソースコードの /staging/src/k8s.io/apiserver/pkg/cel/library/ 以下で実装されています(https://github.com/kubernetes/kubernetes/tree/v1.31.0/staging/src/k8s.io/apiserver/pkg/cel/library/)。ここに拡張する関数を実装、追加しました。拡張した関数の実装例は以下の通りです。

func listToMap(keyList ref.Val, valueList ref.Val) ref.Val {
	goMap := make(map[string]any)

	keyLister, keyOk := keyList.(traits.Lister)
	valueLister, valueOk := valueList.(traits.Lister)

	if !keyOk {
		return types.MaybeNoSuchOverloadErr(keyList)
	}
	if !valueOk {
		return types.MaybeNoSuchOverloadErr(valueList)
	}

	keySz := keyLister.Size().(types.Int)
	valueSz := valueLister.Size().(types.Int)

	var sz = keySz
	if sz > valueSz {
		sz = valueSz
	}

	for i := types.Int(0); i < sz; i++ {
		// type(key) == any
		key := keyLister.Get(types.Int(i)).Value()
		// type(key) == any
		value := valueLister.Get(types.Int(i)).Value()

		keyStr, keyStrOk := key.(string)

		if !keyStrOk {
			return types.MaybeNoSuchOverloadErr(keyList)
		}

		goMap[keyStr] = value
	}

	return types.NewStringInterfaceMap(types.DefaultTypeAdapter, goMap)
}

手元のKubernetesクラスタに新たに追加した関数についてはPull Requestを出したいと考えています。

自前実装を用いた例

apiVersion: admissionregistration.k8s.io/v1alpha1
kind: MutatingAdmissionPolicy
metadata:
  name: extended-resource-labeling.example.com
spec:
  matchConstraints:
    resourceRules:
    - apiGroups:   [""]
      apiVersions: ["v1"]
      operations:  ["CREATE"]
      resources:   ["pods"]
  failurePolicy: Fail
  mutations:
  - patchType: ApplyConfiguration
    reinvocationPolicy: IfNeeded
    expression: >-
      Object {
        metadata: Object.metadata {
          labels: listToMap(
            (object.spec.containers + object.spec.?initContainers.orValue([])).map(
              ct, 
              ct.resources.?limits.orValue({}).filter( 
                k,
                resourceName(k).isExtended()
              ).filter(
                k,
                quantity(ct.resources.limits[k]).compareTo(quantity("0")) != 0
              ).map(
                k, 
                k.replace('/', '--', -1)
              )
            ).sum(),
            (object.spec.containers + object.spec.?initContainers.orValue([])).map(
              ct, 
              ct.resources.?limits.orValue({}).filter(
                k,
                resourceName(k).isExtended()
              ).filter(
                k,
                quantity(ct.resources.limits[k]).compareTo(quantity("0")) != 0
              ).map(
                k, 
                ""
              )
            ).sum()
          )
        }
      }

Mutating Admission Policyの所感

 今回、Mutating Admission Policyを試してみましたが、Webhookを利用せずにユーザー定義のMutationが行えることがわかりました。 サーバーの実装コストやCI/CDの環境構築などの運用コストが無くなることは大きなメリットです。

 一方でCELの機能が限定的であるということがわかりました。CELでは停止性を保証するなどの理由から非チューリング完全になっており、機能が少ない印象を受けました(「拡張リソースをlabelsに付与」の例)。また、関数定義ができないため、記述が冗長になりがちです。そのため、数行では書けないような、複雑な処理は従来通りWebhookを用いた方が良いケースもありそうです。

インターンの感想

 本インターンに参加するまでKubernetesをほとんど触ったことがなかったので、Kubernetesの機能や思想について知ることができ、大変勉強になりました。また普段研究室で管理しているGPUクラスタとPFNの計算基盤の違いを理解できました。特にSlurm等のJobスケジューラーとKubernetesの違いについて知ることができ大変貴重な経験ができました。

メンターの須田さん、清水さん、 クラスターサービスの皆さん、大変お世話になりました!ありがとうございました!

メンターより

大川さんのメンターを担当しました Cluster Services チームの清水、須田です。

Cluster Services チームのインターンは昨年までは7週間の期間で行っていましたが、今年は期間を短くし2週間で実施しました。この2週間の中にオリエンテーション、クラスタを設置しているデータセンターへの見学、最終発表などがありインターンのテーマに取り組むことができる実施的な日数がさらに少ない状況でもしっかりと成果を出してくれました。

インターンに参加するまでは Kubernetes を触った経験はほぼなかったとのことだったので、短い期間の中で Kubernetes そのものを理解するところから始まり、インターンのテーマであった Mutatating Admission Policy という現在進行形で開発が進んでいる新機能の仕様や実装を調査するのは大変だったと思いますがスムーズに作業を進めていました。既存の Admission Webhook で行われている mutation を置き換えた時にどのようになるかの検討からは、 Mutating Admission Policy の使いやすいところや逆にまだ使いにくいところを明らかにしてくれ、今後の機能の置き換えを行う際に参考になる知見が得られました。ありがとうございます。

付録: Kindを用いた検証環境の構築

今回の検証はPull Request(#126497)のKubernetesをソースからビルドし、Kind上で検証を行いました。

kind build node-image

Mutating Admission Policyを有効にするには以下のような設定(cluster.yaml)を加えてclusterを作成します。

apiVersion: kind.x-k8s.io/v1alpha4
kind: Cluster
nodes:
- role: control-plane
  image: kindest/node:latest
featureGates:
  MutatingAdmissionPolicy: true
runtimeConfig:
  api/alpha: "true"

以下のコマンドでクラスター作成できます。

kind create cluster --config cluster.yaml

以下のコマンドでAPIが有効になっているか確認できます。

kubectl api-resources | grep mutatingadmission

付録: CELのTips

Optional

 Kubernetes v1.29からCELのOptionalが利用できます。従来はmapやlistの要素が存在するかわからない場合に has() を多用する必要がありました。 例えば  .spec.containers[x].resources.limits.['nvidia.com/gpu'] が設定されていない、もしくは "0" と判定する場合次のような長い評価式が必要です。

!(has(ct.resources.limits) && "nvidia.com/gpu" in ct.resources.limits) || quantity(ct.resources.limits['nvidia.com/gpu']).asInteger() == 0

limits'nvidia.com/gpu' が存在するかわからないため、has()in を用いて何度も評価を行う必要があります。これをOptionalを用いると以下のように短く書くことができます。

quantity(ct.resources.?limits['nvidia.com/gpu'].orValue("0")).asInteger() == 0

?はOptionalであることを表します。ct.resourceslimitsを持つ場合は optional.of(ct.resources.limits)がそうでない場合はoptional.none()と評価されます。また一度?をつけた場合、下位の要素もOptionalとして評価されるため、limitsはあるが'nvidia.com/gpu' はないという状況でも、エラーは発生せずOptional型として評価されます。 Optionalのメンバ関数.orValue()はOptional型の変数が値を持つ場合はその値を、optional.none()の場合は渡された引数を返します。このようにすることで冗長な記述をさけることができます。

付録: variables, matchCondtionsを用いた場合の例

apiVersion: admissionregistration.k8s.io/v1alpha1
kind: MutatingAdmissionPolicy
metadata:
  name: rdma-resources-and-annotations-inserting.example.com
spec:
  paramKind:
    apiVersion: mutations.example.com/v1
    kind: RdmaResourcesAndAnnotationsInsertingParam 
  matchConstraints:
    resourceRules:
    - apiGroups:   [""]
      apiVersions: ["v1"]
      operations:  ["CREATE"]
      resources:   ["pods"]
  variables:
    - name: containersWithLimits
      expression: >-
        object.spec.containers.filter(
          ct,
          quantity(ct.resources.?limits["preferred.jp/rdma"].orValue("0")).asInteger() == 1
        )
    - name: isContainerWithLimitsExists
      expression: >-
        variables.containersWithLimits.size() > 0
  matchConditions:
    - name: has-rdma-resources-limits
      expression: >-
          variables.isContainerWithLimitsExists
  failurePolicy: Fail
  mutations:
  - patchType: ApplyConfiguration
    reinvocationPolicy: IfNeeded
    expression: >-
      Object {
        metadata: Object.metadata{
          annotations: { 
            "k8s.v1.cni.cncf.io/networks": object.metadata.?annotations["k8s.v1.cni.cncf.io/networks"].orValue(params.annotations) 
          }
        },
        spec: Object.spec{
          containers: variables.containersWithLimits.map(
            ct,
            Object.spec.containers.item {
              name: ct.name,
              resources: {
                "limits": params.nicLimits
              }
            }
          )
        }
      }

 未検証ではありますが、variablesとmatchCondtionsを用いてRDMAジョブの設定の自動化を行う例です。RDMAに関するlimitsを持つコンテナをvariablesで持つことで判定処理の記述を減らしたり、matchConditionsでそのようなコンテナが存在するかを条件にすることで、ロジックを簡略化できます。

参考文献

  • Twitter
  • Facebook