Open6

Validating Admission Policy を触ってみる

cappyzawacappyzawa

kind で試す。

$ kind create cluster
Creating cluster "kind" ...
 ✓ Ensuring node image (kindest/node:v1.32.0) 🖼
 ✓ Preparing nodes 📦
 ✓ Writing configuration 📜
 ✓ Starting control-plane 🕹️
 ✓ Installing CNI 🔌
 ✓ Installing StorageClass 💾
Set kubectl context to "kind-kind"
You can now use your cluster with:

kubectl cluster-info --context kind-kind

Have a question, bug, or feature request? Let us know! https://kind.sigs.k8s.io/#community 🙂

$ k cluster-info
Kubernetes control plane is running at https://127.0.0.1:56902
CoreDNS is running at https://127.0.0.1:56902/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy

To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'.
$ k version
Client Version: v1.32.0
Kustomize Version: v5.5.0
Server Version: v1.32.0
cappyzawacappyzawa

とりあえずは公式ページのサンプルページ通りに進めてみる。

sample.yaml
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
  name: "demo-policy.example.com"
spec:
  failurePolicy: Fail # 条件に当てはまらない場合 Admission を失敗させる
  matchConstraints:
    resourceRules:
    # deployment の作成・更新時において replicas が 5 以下
    - apiGroups: ["apps"]
      apiVersions: ["v1"]
      operations: ["CREATE", "UPDATE"]
      resources: ["deployments"]
  validations:
  - expression: "object.spec.replicas <= 5"

---
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata:
  name: "demo-binding-test.example.com"
spec:
  policyName: "demo-policy.example.com" # 上で定義した ValidatingAdmissionPolicy
  validationActions: [Deny] # Validation に失敗した場合にリクエストを拒否する
  matchResources:
    namespaceSelector:
      matchLabels:
        environment: test
$ k apply -f sample.yaml
validatingadmissionpolicy.admissionregistration.k8s.io/demo-policy.example.com created
validatingadmissionpolicybinding.admissionregistration.k8s.io/demo-binding-test.example.com created

拒否されるような deployment を apply してみる。

deployment.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: test
  labels:
    environment: test # ValidatingAdmissionPolicyBinding で match されるように
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: sample-deployment
  namespace: test
spec:
  replicas: 6 # 5以下の場合は許可される
  selector:
    matchLabels:
      app: sample-app
  template:
    metadata:
      labels:
        app: sample-app
    spec:
      containers:
      - name: nginx
        image: nginx:1.21
        ports:
        - containerPort: 80
$ k apply -f deployment.yaml
namespace/test created
The deployments "sample-deployment" is invalid: : ValidatingAdmissionPolicy 'demo-policy.example.com' with binding 'demo-binding-test.example.com' denied request: failed expression: object.spec.replicas <= 5

簡単な validation であればこれだけでできることはわかった。

cappyzawacappyzawa

ValidatingAdmissionPolicyBinding の Validation actions を理解したい。

以下の値が指定できるらしい。

  • Deny: Validation に失敗した場合、リクエストを失敗させる
  • Warn: Validation に失敗した場合、クライアントに警告を出す
  • Audit: Validation に失敗した場合、監査イベントに記録される

先ほどの ValidatingAdmissionPolicyBinding を編集して試してみる。(先ほどは validationActions: [Deny])

Warn にしてみる。

$ k apply -f deployment.yaml
namespace/test unchanged
Warning: Validation failed for ValidatingAdmissionPolicy 'demo-policy.example.com' with binding 'demo-binding-test.example.com': failed expression: object.spec.replicas <= 5
deployment.apps/sample-deployment created

Deployment の作成自体は成功し、Warning: は黄色く太文字で表示された。
exit code も 0。

$ echo $?
0

かち合うから当たり前だけども [Deny, Warn] の両方を設定することはできない。

Deny and Warn may not be used together since this combination needlessly duplicates the validation failure both in the API response body and the HTTP warning headers.

Audit は kind のクラスタの定義を変更しないと確認できなかったりするから動作確認は飛ばす。

cappyzawacappyzawa

次は Parameter resources を見てみる。

ポリシーの一部(可変にしたいところとか) を ValidatingAdmissionPolicy と分離して定義できる。

ドキュメント通りだと、ReplicaLimit という Resource を定義しているが、CRD 自体の定義がなく動かして試すのが面倒なので、ConfigMap で代用できないか試してみる。

Namespaced なリソースは全て default におくようにして試した。

sample.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: replica-limit
  namespace: default
data:
  maxReplicas: "5" # 文字列である必要がある
---
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
  name: "demo-policy.example.com"
spec:
  failurePolicy: Fail
  paramKind:
    apiVersion: v1
    kind: ConfigMap
  matchConstraints:
    resourceRules:
    - apiGroups: ["apps"]
      apiVersions: ["v1"]
      operations: ["CREATE", "UPDATE"]
      resources: ["deployments"]
  validations:
  - expression: "object.spec.replicas <= int(params.data.maxReplicas)" # 閾値を Parameter resource から引いてくるようになっている
    reason: Invalid
---
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata:
  name: "demo-binding-test.example.com"
spec:
  policyName: "demo-policy.example.com"
  validationActions: [Deny]
  paramRef:
    name: "replica-limit"
    namespace: "default"
    parameterNotFoundAction: Deny # リソースが見つからない場合の動作を明示
$ k apply -f sample.yaml
configmap/replica-limit created
validatingadmissionpolicy.admissionregistration.k8s.io/demo-policy.example.com created
validatingadmissionpolicybinding.admissionregistration.k8s.io/demo-binding-test.example.com created
deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: sample-deployment
  namespace: default
spec:
  replicas: 6 # 5以下の場合は許可される
  selector:
    matchLabels:
      app: sample-app
  template:
    metadata:
      labels:
        app: sample-app
    spec:
      containers:
      - name: nginx
        image: nginx:1.21
        ports:
        - containerPort: 80
$ The deployments "sample-deployment" is invalid: : ValidatingAdmissionPolicy 'demo-policy.example.com' with binding 'demo-binding-test.example.com' denied request: failed expression: object.spec.replicas <= int(params.data.maxReplicas)

できた。

cappyzawacappyzawa

ConfigMap を Parameter resource として扱えることがわかったけど、すでに存在するリソースに沿ってバリデーションしたい場合結構便利かもしれない。

例えば、すでに存在する Deployment と同じイメージでしか新規の DaemonSet を作成できないというサンプルを作ってみる。

daemonset-policy.yaml
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
  name: daemonset
spec:
  failurePolicy: Fail
  paramKind:
    apiVersion: apps/v1
    kind: Deployment
  matchConstraints:
    resourceRules:
    - apiGroups: ["apps"]
      apiVersions: ["v1"]
      operations: ["CREATE"]
      resources: ["daemonsets"]
  validations:
  - expression: "object.spec.template.spec.containers.all(\n  container, \n  container.image == params.spec.template.spec.containers[0].image\n)\n"
---
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata:
  name: daemonset
spec:
  policyName: daemonset
  validationActions: [Deny]
  paramRef:
    name: sample-deployment
    namespace: default
    parameterNotFoundAction: Deny # リソースが見つからない場合の動作を明示
$ k apply -f daemonset-policy.yaml
validatingadmissionpolicy.admissionregistration.k8s.io/daemonset created
validatingadmissionpolicybinding.admissionregistration.k8s.io/daemonset created

存在する Deployment(sample-deployment) と異なる Image を利用する Daemonset を作成する

daemonset.yaml
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: sample-daemonset
  namespace: default
spec:
  selector:
    matchLabels:
      app: sample-app
  template:
    metadata:
      labels:
        app: sample-app
    spec:
      containers:
      - name: nginx
        image: nginx:1.22 # 存在するのは 1.21
$ k apply -f daemonset.yaml
The daemonsets "sample-daemonset" is invalid: : ValidatingAdmissionPolicy 'daemonset' with binding 'daemonset' denied request: failed expression: object.spec.template.spec.containers.all(
  container,
  container.image == params.spec.template.spec.containers[0].image
)

狙い通り失敗した。

一度は存在する Image で daemonset を作成し、その後書き換えるのはうまくいくはず。

$ cat daemonset.yaml| tail -n 1
        image: nginx:1.21
$ k apply -f daemonset.yaml
daemonset.apps/sample-daemonset created

# 存在しないイメージに書き換え
$ cat daemonset.yaml| tail -n 1
        image: nginx:1.22
$ k apply -f daemonset.yaml
daemonset.apps/sample-daemonset configured

OK.