Admission Policy bằng CEL

K
Kai··5 min read

Part XIII khép phần vận hành. Part XIV soi một nhóm khác: những tính năng vừa graduate lên ổn định ở chính phiên bản v1.36 mà cụm đang chạy — cụm tự dựng là chỗ lý tưởng để thử chúng ngay. Bắt đầu với cái nối thẳng vào Bài 58: admission không cần webhook.

Bài 58 dựng một validating webhook, và phần khó nhất không phải logic mà là hạ tầng: một server HTTPS, một cert ký bởi CA cụm, caBundle, failurePolicy phòng khi server chết. Với những luật đơn giản — chặn trường này, đặt mặc định kia — toàn bộ bộ máy đó là gánh nặng. v1.36 cho cách khác: viết luật bằng CEL (Common Expression Language) ngay trong API server, qua hai object ValidatingAdmissionPolicyMutatingAdmissionPolicy (Mutating vừa lên GA ở 1.36). Không server, không cert, không caBundle.

Hai object: policy và binding

Giống RBAC tách Role khỏi RoleBinding, admission policy tách luật khỏi phạm vi áp:

   ValidatingAdmissionPolicy / MutatingAdmissionPolicy   — luật CEL + matchConstraints (resource nào)
            │
   ...Binding                                            — áp policy vào đâu (namespaceSelector),
                                                            validationActions (Deny/Warn/Audit)

Tách vậy để một policy viết một lần, dùng lại ở nhiều binding với phạm vi khác nhau. CEL chạy trong tiến trình API server nên không có vòng gọi mạng ra ngoài như webhook — nhanh hơn và không có điểm chết, đổi lại bị giới hạn trong những gì CEL biểu đạt được (không gọi được dịch vụ ngoài, không tra cứu trạng thái phức tạp).

Validating: chặn image :latest

Một ValidatingAdmissionPolicy từ chối Deployment nào có container dùng tag :latest — luật là một biểu thức CEL trả về true khi hợp lệ:

apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata: {name: deny-latest-tag}
spec:
  failurePolicy: Fail
  matchConstraints:
    resourceRules:
    - apiGroups: ["apps"], apiVersions: ["v1"], operations: ["CREATE","UPDATE"], resources: ["deployments"]
  validations:
  - expression: "object.spec.template.spec.containers.all(c, !c.image.endsWith(':latest'))"
    message: "cấm image tag :latest (CEL policy)"
---
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata: {name: deny-latest-binding}
spec:
  policyName: deny-latest-tag
  validationActions: ["Deny"]
  matchResources:
    namespaceSelector:
      matchLabels: {vap: enabled}

object.spec.template.spec.containers.all(c, !c.image.endsWith(':latest')) đọc thẳng object đang được tạo — macro all() của CEL kiểm mọi container. Gắn nhãn vap=enabled lên namespace rồi thử:

kubectl -n vap-test create deployment bad  --image=nginx:latest
kubectl -n vap-test create deployment good --image=busybox:1.36
error: failed to create deployment: deployments.apps "bad" is forbidden:
  ValidatingAdmissionPolicy 'deny-latest-tag' with binding 'deny-latest-binding' denied request:
  cấm image tag :latest (CEL policy)
deployment.apps/good created

Deployment :latest bị từ chối với message từ policy, tag cụ thể thì qua — y hệt kết quả webhook ở Bài 58, nhưng không có pod webhook nào chạy, không cert, không Service. validationActions cho chọn Deny (chặn), Warn (cảnh báo), hay Audit (ghi log) — cùng một policy, đổi mức xử lý ở binding.

Mutating: tự tiêm label

MutatingAdmissionPolicy (GA ở 1.36) sửa object, thay cho mutating webhook. Dùng ApplyConfiguration — một biểu thức CEL dựng phần object cần thêm vào, kiểu server-side apply:

apiVersion: admissionregistration.k8s.io/v1
kind: MutatingAdmissionPolicy
metadata: {name: add-team-label}
spec:
  matchConstraints:
    resourceRules:
    - apiGroups: [""], apiVersions: ["v1"], operations: ["CREATE"], resources: ["pods"]
  reinvocationPolicy: Never
  mutations:
  - patchType: ApplyConfiguration
    applyConfiguration:
      expression: >
        Object{ metadata: Object.metadata{ labels: {"injected-by": "mutating-cel-policy"} } }

(kèm một MutatingAdmissionPolicyBinding chọn namespace cel=enabled.) Biểu thức Object{...} dựng một bản vá chỉ chứa label cần thêm; API server merge nó vào pod. Tạo một pod không khai label nào:

kubectl -n cel-demo run p --image=busybox:1.36 --command -- sleep 100000
kubectl -n cel-demo get pod p -o jsonpath='{.metadata.labels}'
{"injected-by":"mutating-cel-policy","run":"p"}

Label injected-by: mutating-cel-policy xuất hiện dù ta không khai — policy đã tiêm lúc admission. Với webhook (Bài 58) việc này cần một server trả JSONPatch; ở đây là một biểu thức CEL trong manifest.

Khi nào CEL, khi nào webhook

Hai cách không loại trừ nhau, mà chia theo độ phức tạp của luật:

CEL admission policy Admission webhook (Bài 58)
Hạ tầng Không — chỉ object policy Server HTTPS + cert + caBundle + giữ HA
Tốc độ Trong tiến trình apiserver Một vòng gọi mạng mỗi request
Sức biểu đạt Giới hạn trong CEL Tùy ý (code, gọi dịch vụ ngoài, tra DB)
Hợp cho Luật trên chính object (cấm trường, đặt mặc định, ràng buộc giá trị) Logic cần dữ liệu/dịch vụ ngoài

Quy tắc thực dụng: luật chỉ nhìn vào object đang xét thì dùng CEL policy — gọn, nhanh, không có server để hỏng. Luật cần gọi ra ngoài (tra registry, hỏi một API khác) thì mới cần webhook. Phần lớn policy "chặn cấu hình xấu" thuộc nhóm đầu, nên CEL policy thay được đa số webhook validating/mutating thường gặp.

🧹 Dọn dẹp

kubectl delete validatingadmissionpolicybinding deny-latest-binding
kubectl delete validatingadmissionpolicy deny-latest-tag
kubectl delete mutatingadmissionpolicybinding add-team-binding
kubectl delete mutatingadmissionpolicy add-team-label
kubectl delete namespace vap-test cel-demo

Tất cả là object trong API server, không cài gì; xóa policy + binding + namespace là sạch. Manifest ở github.com/nghiadaulau/kubernetes-from-scratch, thư mục 68-cel-admission-policy.

Tổng kết

Từ v1.36, admission làm được bằng CEL ngay trong API server, không cần webhook server: ValidatingAdmissionPolicy chặn/cảnh báo (ta chặn Deployment dùng :latest bằng containers.all(c, !c.image.endsWith(':latest'))), MutatingAdmissionPolicy (vừa GA) sửa object qua ApplyConfiguration (ta tiêm label injected-by). Mỗi loại tách thành policy (luật + matchConstraints) và binding (phạm vi qua namespaceSelector + validationActions Deny/Warn/Audit), giống cách RBAC tách Role/RoleBinding. So với webhook Bài 58: CEL policy không có server/cert/caBundle để dựng và giữ sống, chạy trong tiến trình nên nhanh và không điểm chết — đổi lại chỉ biểu đạt được trong CEL, không gọi ra ngoài. Luật nhìn vào chính object thì chọn CEL; luật cần dịch vụ ngoài mới cần webhook.

Bài 69 sang một tính năng v1.36 khác chạm tới điều tưởng như bất biến của pod: đổi CPU/memory của một pod đang chạy mà không tạo lại nó — in-place pod resize, đối trọng "không cần restart" cho phần scale dọc của Bài 40.

Related Posts