Validating Admission Policyを試してみた

# Validating Admission Policyとは？

[Validating Admission Policy](https://kubernetes.io/docs/reference/access-authn-authz/validating-admission-policy/)はValidating Admission Webhookを一部代替できる機能です。Kubernetes v1.26からalpha機能として利用可能になりました。
Validating Admission PolicyはCommon Expression Language (CEL)を用いてValidationのルールを定義します。CELで表現できる範囲に限られますが、Admission Webhookと異なりkube-apiserver内で評価されるため、Admission Webhookとkube-apiserver間のネットワークの問題に影響を考えなくて良いのは大きなメリットでしょう。

## Validating Admission Policyの有効化

Validating Admission Policyは新たに追加されたKubernetes APIである`admissionregistration.k8s.io/v1alpha1`を用いて設定します。これはKubernetes v1.26時点ではデフォルト無効となっているので、RuntimeConfigで有効化する必要があります。またValidating Admission Policy自体もデフォルト無効となっているため、Feature Gatesを有効化する必要があります。

kindの場合以下のような設定ファイルで、Validating Admission Policyが利用できるクラスタが作成できます。

```yaml
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
featureGates:
  "ValidatingAdmissionPolicy": true
runtimeConfig:
  "admissionregistration.k8s.io/v1alpha1": true
nodes:
- role: control-plane
  image: kindest/node:v1.26.0
```

## Validating Admission Policyを試す

Validating Admission Policyは最低限、ValidatingAdmissionPolicyとValidatingAdmissionPolicyBindingの2つリソースが必要となります。ValidatingAdmissionPolicyは以下のように具体的なポリシーを定義します。これはDeploymentの名前が`foo`から始まらないといけないというポリシーです。

```yaml
apiVersion: admissionregistration.k8s.io/v1alpha1
kind: ValidatingAdmissionPolicy
metadata:
  name: "demo-policy.uesyn.dev"
spec:
  matchConstraints:
    resourceRules:
    - apiGroups:   ["apps"]
      apiVersions: ["v1"]
      operations:  ["CREATE", "UPDATE"]
      resources:   ["deployments"]
  validations:
  - expression: object.metadata.name.startsWith("foo")
```

上記のValidatingAdmissionPolicyは定義するだけでは利用できません。以下のようにValidatingAdmissionPolicyBindingが必要となります。具体的に以下のようなリソースを作成すると、`default` namespaceへ対象を絞ることができます。

```yaml
apiVersion: admissionregistration.k8s.io/v1alpha1
kind: ValidatingAdmissionPolicyBinding
metadata:
  name: "demo-policy-binding.uesyn.dev"
spec:
  policyName: "demo-policy.uesyn.dev"
  matchResources:
    namespaceSelector:
      matchLabels:
        kubernetes.io/metadata.name: "default"
```

上記をクラスタへapplyし、Deploymentを作成してみます。

```sh
# これはValidationされ、許可される
$ kubectl --namespace default create deployment --image=nginx foo-deployment
deployment.apps/foo-deployment created

# これはValidationされ、拒否される
$ kubectl --namespace default create deployment --image=nginx bar-deployment
error: failed to create deployment: deployments.apps "bar-deployment" is forbidden: Validat
ingAdmissionPolicy 'demo-policy.uesyn.dev' with binding 'demo-policy-binding.uesyn.dev' den
ied request: failed expression: object.metadata.name.startsWith("foo")

# これはdefault namespaceではないのでValidationされない
$ kubectl --namespace kube-public create deployment --image=nginx bar-deployment
deployment.apps/bar-deployment created
```

ちなみにこのValidatingAdmissionPolicyの`.spec.matchConstraints`とValidatingAdmissionPolicyBindingの`.spec.matchResources`は名前は異なりますが、中身の指定方法は全く同じです。[^1] これらの積集合がValidationの対象となるようで、もし積集合がない場合はValidationが実施されないので注意が必要です。

[^1]: どちらも同じ[MatchResources](https://github.com/kubernetes/kubernetes/blob/b46a3f887ca979b1a5d14fd39cb1af43e7e5d12d/staging/src/k8s.io/api/admissionregistration/v1alpha1/types.go#L260-L345)構造体を利用しています。

## パラメータを利用する

Validating Admission Policyでは、Policyへ外部から値を取得するためのパラメータを利用できます。Kubernetesのオブジェクトの値を参照でき、NativeリソースだけでCustom Resourceの値も参照できる様です。パラメータを利用するには、まずどの種類のKubernetesリソースの値を参照するかをValidatingAdmissionPolicyの`spec.paramKind`で定義します。
先ほどの例はnameが`foo`以外から始まるDeploymentの作成を拒否するものでしたが、その`foo`をパラメータを用いて変更できるようにします。今回は手軽にConfigMapから値を取る様にします。パラメータは`params`変数でアクセスできます。今回はConfigMapのdataへ`prefix`というキーに設定された値を参照する設定です。

```yaml
apiVersion: admissionregistration.k8s.io/v1alpha1
kind: ValidatingAdmissionPolicy
metadata:
  name: demo-policy.uesyn.dev
spec:
  paramKind:
    apiVersion: v1
    kind: ConfigMap
  matchConstraints:
    resourceRules:
    - apiGroups:   ["apps"]
      apiVersions: ["v1"]
      operations:  ["CREATE", "UPDATE"]
      resources:   ["deployments"]
  validations:
  - expression: object.metadata.name.startsWith(params.data.prefix)
```

具体的にどのリソースを参照するかは、ValidatingAdmissionPolicyBindingリソースの`spec.paramRef`で指定します。Cluster Scopedなリソースの場合はnamespaceの指定は不要です。

```yaml
apiVersion: admissionregistration.k8s.io/v1alpha1
kind: ValidatingAdmissionPolicyBinding
metadata:
  name: demo-policy-binding.uesyn.dev
spec:
  paramRef:
    name: demo-policy-param
    namespace: default
  policyName: demo-policy.uesyn.dev
  matchResources:
    namespaceSelector:
      matchLabels:
        kubernetes.io/metadata.name: "default"
```

参照されるConfigMapは以下の様になります。今回は`bar`という値であれば許可されるようにします。

```yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: demo-policy-param
  namespace: default
data:
  prefix: bar
```

上記の3つのリリースをapplyして、動作を確認します。

```sh
# barをprefixとして持つものは許可される。
$ kubectl --namespace default create deployment --image=nginx bar-deployments
deployment.apps/bar-deployments created

# bar以外をprefiとして持つものは拒否される。
$ kubectl --namespace default create deployment --image=nginx foo-deployments
error: failed to create deployment: deployments.apps "foo-deployments" is forbidden: ValidatingAdmissionPolicy 'demo-policy.uesyn.dev' with binding 'demo-policy-binding.uesyn.dev' denied request: failed expression: object.metadata.name.startsWith(params.data.prefix)
```

参照されるConfigMapの値を`baz`へ変更してみましょう。

```yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: demo-policy-param
  namespace: default
data:
  prefix: baz
```

```sh
# bazをprefixとして持つものは許可される。
$ kubectl --namespace default create deployment --image=nginx baz-deployments
deployment.apps/baz-deployments created

# baz以外をprefixとして持つものは拒否される。
$ kubectl --namespace default create deployment --image=nginx bar-deployments
error: failed to create deployment: deployments.apps "bar-deployments" is forbidden: ValidatingAdmissionPolicy 'demo-policy.uesyn.dev' with binding 'demo-policy-binding.uesyn.dev' denied request: failed expression: object.metadata.name.startsWith(params.data.prefix)
```

ちなみに参照されるキーが存在しなかったり、オブジェクトが存在しないと以下の様な挙動となります。

```sh
# キーが存在しない場合
$ kubectl --namespace default create deployment --image=nginx foo-deployments
error: failed to create deployment: deployments.apps "foo-deployments" is forbidden: ValidatingAdmissionPolicy 'demo-policy.uesyn.dev' with binding 'demo-policy-binding.uesyn.dev' denied request: expression 'object.metadata.name.startsWith(params.data.prefix)' resulted in error: no such key: data

# paramRefのオブジェクトが存在しない場合
$ kubectl --namespace default create deployment --image=nginx foo-deployments
error: failed to create deployment: deployments.apps "foo-deployments" is forbidden: ValidatingAdmissionPolicy 'demo-policy.uesyn.dev' with binding 'demo-policy-binding.uesyn.dev' denied request: failed to configure binding: demo-policy-param not found
```

Admission Webhookと同様にValidating Admission PolicyにもfailurePolicyが設定でき、デフォルトではFailのため上記の様に評価に失敗した場合ではオブジェクトの作成はされません。ここでfailurePolicyをIgnoreへ設定を変更します。

```yaml
apiVersion: admissionregistration.k8s.io/v1alpha1
kind: ValidatingAdmissionPolicy
metadata:
  name: demo-policy.uesyn.dev
spec:
  failurePolicy: Ignore
  ...(省略)
```

Ignoreを指定した場合、先ほどのオブジェクトやキーが存在しない場合でも作成が成功します。
しかしwarningなども出力されないため、これが意図しない挙動の場合は気づくことができません。

```sh
$ kubectl --namespace default create deployment --image=nginx foo-deployments
deployment.apps/foo-deployments created
```

## 最後に

CELで表現できるものであればAdmission Webhookを作ることなく手軽にValidationできるのは素敵ですね。しかし、エラーメッセージがわかりづらかったり、細かい制御をしたい場合はAdmission Webhookが必要となってきますが、積極的に利用したい機能なのは間違いありません。早くデフォルトで有効化されて欲しいですね！