Open10

KubernetesのCELによるValidationを試す

yashirookyashirook

kindでKubernetesクラスタ v1.31.0 (2024年9月時点で最新)を作成

~ ❯ kind version
kind v0.24.0 go1.22.6 darwin/arm64

~ ❯ kind create cluster --image=kindest/node:v1.31.0
Creating cluster "kind" ...
 ✓ Ensuring node image (kindest/node:v1.31.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 nice day! 👋

~ kind-kind/❯ k get node
NAME                 STATUS   ROLES           AGE   VERSION
kind-control-plane   Ready    control-plane   76s   v1.31.0

~ kind-kind/❯ k version
Client Version: v1.31.0
Kustomize Version: v5.4.2
Server Version: v1.31.0
yashirookyashirook

validationに関連するapi-resource

~ kind-kind/❯ k api-resources | grep validating
validatingadmissionpolicies                      admissionregistration.k8s.io/v1   false        ValidatingAdmissionPolicy
validatingadmissionpolicybindings                admissionregistration.k8s.io/v1   false        ValidatingAdmissionPolicyBinding
validatingwebhookconfigurations                  admissionregistration.k8s.io/v1   false        ValidatingWebhookConfiguration

validatingadmissionpolicies, validatingadmissionpolicybindingsのAPIがそれぞれ admissionregistration.k8s.io グループにおいて v1になっていることがわかる。

yashirookyashirook

試しにValidatingAdmissionPolicyValidatingAdmissionPolicyBindingを作ってみる。

validatingadmissionpolicy/replicas-under-5-policy.yaml
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
  name: replicas-under-5
spec:
  failurePolicy: Fail
  matchConstraints:
    resourceRules:
      - apiGroups: ["apps"]
        apiVersions: ["v1"]
        resources: ["deployments"]
        operations: ["CREATE", "UPDATE"]
  validations:
    - expression: "object.spec.replicas <= 5"
      message: "Replicas must be less than or equal to 5"
~/workspace/test-kubernetes-validating-admission-policy/validatingadmissionpolicy kind-kind/❯ k apply -f replicas-policy.yaml
validatingadmissionpolicy.admissionregistration.k8s.io/replicas-under-5 created
~/workspace/test-kubernetes-validating-admission-policy/validatingadmissionpolicy kind-kind/❯ k get
 validatingadmissionpolicies.admissionregistration.k8s.io 
NAME               VALIDATIONS   PARAMKIND   AGE
replicas-under-5   1             <unset>     41s

demo namespaceでpolicyを適用するようにした。

validatingadmissionpolicy/replicas-under-5-binding.yaml
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata:
  name: replicas-under-5-binding
spec:
  policyName: replicas-under-5
  validationActions: [Deny]
  matchResources:
    namespaceSelector:
      matchLabels:
        environment: test

~/workspace/test-kubernetes-validating-admission-policy/validatingadmissionpolicy kind-kind/❯ k apply -f replicas-under-5-binding.yaml                               
validatingadmissionpolicybinding.admissionregistration.k8s.io/replicas-under-5-binding created

~/workspace/test-kubernetes-validating-admission-policy/validatingadmissionpolicy kind-kind/❯ k get validatingadmissionpolicybindings.admissionregistration.k8s.io   
NAME                       POLICYNAME         PARAMREF   AGE
replicas-under-5-binding   replicas-under-5   <unset>    3s
yashirookyashirook

default namespaceにラベルをつけて、有効/無効なDeploymentリソースをapplyしてみる。

ラベルを付与

~/workspace/test-kubernetes-validating-admission-policy/validatingadmissionpolicy kind-kind/❯ k label ns default environment=test
namespace/default labeled

~/workspace/test-kubernetes-validating-admission-policy/validatingadmissionpolicy kind-kind/❯ k get ns default --show-labels 
NAME      STATUS   AGE   LABELS
default   Active   94m   environment=test,kubernetes.io/metadata.name=default

有効なDeployment

valid-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: valid-deployment
  namespace: default
spec:
  replicas: 5
  selector:
    matchLabels:
      app: valid-deployment
  template:
    metadata:
      labels:
        app: valid-deployment
    spec:
      containers:
      - name: valid-deployment
        image: nginx:latest
        ports:
        - containerPort: 80

applyすると成功することを確認。

~/workspace/test-kubernetes-validating-admission-policy kind-kind/❯ k apply -f deployment/valid-deployment.yaml
deployment.apps/valid-deployment created

~/workspace/test-kubernetes-validating-admission-policy kind-kind/❯ k get po
NAME                               READY   STATUS    RESTARTS   AGE
valid-deployment-57d6875f7-2l6tq   1/1     Running   0          11s
valid-deployment-57d6875f7-8fzmq   1/1     Running   0          11s
valid-deployment-57d6875f7-dz6tw   1/1     Running   0          11s
valid-deployment-57d6875f7-nhplm   1/1     Running   0          11s
valid-deployment-57d6875f7-t7nxp   1/1     Running   0          11s

無効なDeployment

invalid-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: invalid-deployment
  namespace: default
spec:
  replicas: 10
  selector:
    matchLabels:
      app: invalid-deployment
  template:
    metadata:
      labels:
        app: invalid-deployment
    spec:
      containers:
      - name: invalid-deployment
        image: nginx:latest
        ports:
        - containerPort: 80

applyすると失敗することを確認。

~/workspace/test-kubernetes-validating-admission-policy kind-kind/❯ k apply -f deployment/invalid-deployment.yaml
The deployments "invalid-deployment" is invalid: : ValidatingAdmissionPolicy 'replicas-under-5' with binding 'replicas-under-5-binding' denied request: Replicas must be less than or equal to 5

namespaceに付与したラベルを外すと適用可能なことを確認する。

~/workspace/test-kubernetes-validating-admission-policy kind-kind/❯ k label ns default environm
ent-
namespace/default unlabeled

~/workspace/test-kubernetes-validating-admission-policy kind-kind/❯ k apply -f deployment/invalid-deployment.yaml
deployment.apps/invalid-deployment created

以降も検証を続けるので、再度ラベルを付与しておく

~/workspace/test-kubernetes-validating-admission-policy/validatingadmissionpolicy kind-kind/❯ k label ns default environment=test
namespace/default labeled
yashirookyashirook

ValidatingAdmissionPolicyの適用状況をモニタリングできるよう、Prometheusとkube-state-metricsをインストールしていく。

Prometheus

~ kind-kind/❯ helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
"prometheus-community" has been added to your repositories

~ kind-kind/❯ helm repo update

Hang tight while we grab the latest from your chart repositories...
...Successfully got an update from the "ub-helm-repo" chart repository
...Successfully got an update from the "prometheus-community" chart repository
Update Complete. ⎈Happy Helming!⎈

~ kind-kind/❯ helm install prometheus prometheus-community/kube-prometheus-stack --namespace monitoring --create-namespace
NAME: prometheus
LAST DEPLOYED: Sat Sep  7 09:20:31 2024
NAMESPACE: monitoring
STATUS: deployed
REVISION: 1
NOTES:
kube-prometheus-stack has been installed. Check its status by running:
  kubectl --namespace monitoring get pods -l "release=prometheus"

Visit https://github.com/prometheus-operator/kube-prometheus for instructions on how to create & configure Alertmanager and Prometheus 
instances using the Operator.

~ kind-kind/❯ k get po -n monitoring
NAME                                                     READY   STATUS    RESTARTS   AGE
alertmanager-prometheus-kube-prometheus-alertmanager-0   2/2     Running   0          62s
prometheus-grafana-7f7b8c64d-c2qgq                       3/3     Running   0          78s
prometheus-kube-prometheus-operator-648f8c94ff-k92zs     1/1     Running   0          78s
prometheus-kube-state-metrics-5694684fbc-z76jg           1/1     Running   0          78s
prometheus-prometheus-kube-prometheus-prometheus-0       2/2     Running   0          62s
prometheus-prometheus-node-exporter-hclrs                1/1     Running   0          78s

Prometheusとともに、kube-state-metricsもデプロイできていることを確認できました。

NodePort経由でServiceにアクセスできるようにします。

prometheus-nodeport-svc
apiVersion: v1
kind: Service
metadata:
  name: prometheus-nodeport
  namespace: monitoring
spec:
  type: NodePort
  ports:
  - port: 9090
    targetPort: 9090
    nodePort: 30090
  selector:
    app.kubernetes.io/name: prometheus
~/workspace/test-kubernetes-validating-admission-policy kind-kind/❯ k apply -f monitoring/prometheus-nodeport-svc.yaml
service/prometheus-nodeport created

~/workspace/test-kubernetes-validating-admission-policy kind-kind/❯ k get svc -n monitoring | grep NodePort
prometheus-nodeport                       NodePort    10.96.194.39    <none>        9090:30090/TCP               2m

~ kind-kind/❯ kubectl get nodes -o wide
NAME                 STATUS   ROLES           AGE   VERSION   INTERNAL-IP     EXTERNAL-IP   OS-IMAGE                         KERNEL-VERSION                        CONTAINER-RUNTIME
kind-control-plane   Ready    control-plane   11h   v1.31.0   192.168.247.2   <none>        Debian GNU/Linux 12 (bookworm)   6.10.6-orbstack-00249-g92ad2848917c   containerd://1.7.18

http://<INTERNAL-IP>:30090/ にブラウザからアクセスすると、PrometheusのUIが参照できます。
PrometheusのWeb UI

yashirookyashirook

kube-apiserverのログを確認してみる。

デフォルトでログが十分でなかったので、起動引数に -v=5オプションを変更してみる

I0907 04:28:15.287521       1 queueset.go:417] QS(global-default): Dispatching request &request.RequestInfo{IsResourceRequest:true, Path:"/apis/apps/v1/namespaces/default/deployments/invalid-deployment", Verb:"get", APIPrefix:"apis", APIGroup:"apps", APIVersion:"v1", Namespace:"default", Resource:"deployments", Subresource:"", Name:"invalid-deployment", Parts:[]string{"deployments", "invalid-deployment"}, FieldSelector:"", LabelSelector:""} &user.DefaultInfo{Name:"kubernetes-admin", UID:"", Groups:[]string{"kubeadm:cluster-admins", "system:authenticated"}, Extra:map[string][]string(nil)} from its queue
I0907 04:28:15.287615       1 handler.go:153] kube-aggregator: GET "/apis/apps/v1/namespaces/default/deployments/invalid-deployment" satisfied by nonGoRestful
I0907 04:28:15.287632       1 pathrecorder.go:250] kube-aggregator: "/apis/apps/v1/namespaces/default/deployments/invalid-deployment" satisfied by prefix /apis/apps/v1/
I0907 04:28:15.287649       1 handler.go:143] kube-apiserver: GET "/apis/apps/v1/namespaces/default/deployments/invalid-deployment" satisfied by gorestful with webservice /apis/apps/v1
I0907 04:28:15.288943       1 httplog.go:134] "HTTP" verb="GET" URI="/apis/apps/v1/namespaces/default/deployments/invalid-deployment" latency="1.714279ms" userAgent="kubectl/v1.31.0 (darwin/arm64) kubernetes/9edcffc" audit-ID="3d677934-99da-4858-bc78-778370bb4e53" srcIP="192.168.247.1:42544" apf_pl="global-default" apf_fs="global-default" apf_iseats=1 apf_fseats=0 apf_additionalLatency="0s" apf_execution_time="1.324031ms" resp=404
I0907 04:28:15.290341       1 queueset.go:417] QS(global-default): Dispatching request &request.RequestInfo{IsResourceRequest:true, Path:"/apis/apps/v1/namespaces/default/deployments", Verb:"create", APIPrefix:"apis", APIGroup:"apps", APIVersion:"v1", Namespace:"default", Resource:"deployments", Subresource:"", Name:"", Parts:[]string{"deployments"}, FieldSelector:"", LabelSelector:""} &user.DefaultInfo{Name:"kubernetes-admin", UID:"", Groups:[]string{"kubeadm:cluster-admins", "system:authenticated"}, Extra:map[string][]string(nil)} from its queue

applyしたリソースがどのpolicyに抵触しているのかについてはログで出力されていないようだ。

yashirookyashirook

PrometheusのWebUIでたとえば以下のようなクエリを実行すると、validationのチェックが行われた数の時系列データが見られる

sum by (policy, enforcement_action, namespace) (increase(apiserver_validating_admission_policy_check_total[1m]))

data

運用目線では、どんなリソースがどれほどvalidationに引っかかっているのか把握をしてアクションを打てるようにしておきたい気持ちはあるが、メトリクスに付与されたラベルからは特定することは難しそうに見えた。

yashirookyashirook

コンテナをProduction Readyに運用するためには、コンテナに対するProbeを設定することが望ましいので、Liveness Probeを例に取り、LivenessProbeが設定されていないコンテナを含むDeploymentを作成しようとした時にValidationにより失敗するルールを作ってみる。

ValidatingAdmissionPolicy をつくる

ValidatingAdmissionPolicy

apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
  name: probe-must-be-set
spec:
  failurePolicy: Fail
  matchConstraints:
    resourceRules:
      - apiGroups: ["apps"]
        apiVersions: ["v1"]
        resources: ["deployments"]
        operations: ["CREATE", "UPDATE"]
  variables:
    - name: containers
      expression: "object.spec.template.spec.containers"
  validations:
    - expression: "variables.containers.all(c, has(c.livenessProbe))"
      message: "All containers must have a liveness probe"

ここで、2点解説しておく

  • variables
    • 繰り返しアクセスする、もしくはexpressionの可読性を高めるためにvariablesを定義できる。今回は、containersをvariablesとして定義して、validationでvariables.validationでアクセスしている。
  • containers.all
    • containersは配列が入るので、allメソッドを呼び出すことでそれぞれの配列要素に対して検証を実行し、全てがpassした場合のみtrueを返すように書くことができる。Podで起動するコンテナの数によらず適用できるようにした。

ValidatingAdmissionPolicyBinding

apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata:
  name: probe-must-be-set-binding
spec:
  policyName: probe-must-be-set
  validationActions: [Deny]
  matchResources:
    namespaceSelector:
      matchLabels:
        environment: test

検証

以下のDeploymentを作成する

apiVersion: apps/v1
kind: Deployment
metadata:
  name: no-probe-deployment
  namespace: default
spec:
  replicas: 5
  selector:
    matchLabels:
      app: no-probe-deployment
  template:
    metadata:
      labels:
        app: no-probe-deployment
    spec:
      containers:
      - name: app
        image: nginx:latest
        ports:
        - containerPort: 80
        livenessProbe:
          httpGet:
            path: /
            port: 80
      - name: sidecar
        image: nginx:latest
        ports:
        - containerPort: 8000

~/workspace/test-kubernetes-validating-admission-policy kind-kind/❯ k apply -f deployment/no-probe-deployment.yaml             
The deployments "no-probe-deployment" is invalid: : ValidatingAdmissionPolicy 'probe-must-be-set' with binding 'probe-must-be-set-binding' denied request: All containers must have a liveness probe

ここでは、2つコンテナを起動するPodを起動するような設定内容になっていますが、2つ目のsidecarコンテナに livenessProbe が設定されていないため、applyが弾かれるようになっています。

2つ目のコンテナに livenessProbeを追加すると、applyが通るようになります。

yashirookyashirook

次はprevilegedコンテナの起動を防ぐようにしてみます。

ValidatingAdmissionPolicy をつくる

ValidatingAdmissionPolicy

apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
  name: previleged
spec:
  failurePolicy: Fail
  matchConstraints:
    resourceRules:
      - apiGroups: ["apps"]
        apiVersions: ["v1"]
        resources: ["deployments"]
        operations: ["CREATE", "UPDATE"]
  variables:
    - name: containers
      expression: "object.spec.template.spec.containers"
    - name: securityContexts
      expression: 'variables.containers.map(c, c.?securityContext)'
  validations:
    - expression: "variables.securityContexts.all(sc, sc.?allowPrivilegeEscalation != optional.of(true))"
      message: "Containers must not allow privilege escalation"

今回は、optionalなプロパティの値のvalidationを行った。
optional.of(bool)を使うと、実現可能なのでそう構成した。

ValidatingAdmissionPolicyBinding

apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata:
  name: previleged-binding
spec:
  policyName: previleged
  validationActions: [Deny]
  matchResources:
    namespaceSelector:
      matchLabels:
        environment: test

検証

以下のDeploymentを作成する

apiVersion: apps/v1
kind: Deployment
metadata:
  name: previleged-deployment
  namespace: default
spec:
  replicas: 5
  selector:
    matchLabels:
      app: previleged-deployment
  template:
    metadata:
      labels:
        app: previleged-deployment
    spec:
      containers:
      - name: app
        image: nginx:latest
        ports:
        - containerPort: 80
        livenessProbe:
          httpGet:
            path: /
            port: 80
        securityContext:
          allowPrivilegeEscalation: true
      - name: sidecar
        image: nginx:latest
        ports:
        - containerPort: 8000
        livenessProbe:
          httpGet:
            path: /
            port: 8000

~/workspace/test-kubernetes-validating-admission-policy kind-kind/❯ k apply -f deployment/previleged-deployment.yaml
The deployments "previleged-deployment" is invalid: : ValidatingAdmissionPolicy 'previleged' with binding 'previleged-binding' denied request: Containers must not allow privilege escalation

appコンテナに、allowPrivilegeEscalationが有効化されているので、リクエストが拒否されていることを確認できました。

yashirookyashirook

一通り試して、シュッとKubernetes Officialの機能のみを利用してリクエストのValidationが行えることを確認した。

運用面で気になったこと

  • メトリクスから原因になっているリソースを追うことは現状難しそう
  • 軽く調べた限り、Kubernetesのマニフェストベースでテストをするフレームワーク的なものは存在していないように見えた。CEL Playbookは使ってテストして、それをマニフェストに記載するのが現時点ではお手軽かなと思いましたが、やはりCIには組み込みたいなと感じた

| メトリクスから原因になっているリソースを追うことは現状難しそう
これは validationActionsAuditが存在しているので、kube-apiserverのauditを有効にするといい感じにログベースでモニタリングはできるかもしれないと感じた。
また検証してみたいトピックなので、取り組んだ際は何かしらの形で公開したいなと思います。