Admission Webhook: Chen Vào Đường Ghi

K
Kai··5 min read

Bài 54 dùng Pod Security Admission, một admission controller dựng sẵn trong API server. Nhưng admission còn một dạng động: API server có thể gọi ra một dịch vụ bên ngoài cho mỗi request ghi, hỏi "object này có được không, hay sửa gì không". Đó là admission webhook — cách chèn logic của riêng bạn vào đường ghi mà không sửa API server. Bài này tự dựng một cái từ đầu, kể cả phần TLS.

Hai loại, theo thứ tự

Có hai loại webhook, chạy ở hai điểm khác nhau trong chặng admission:

   request ──► [Mutating webhooks] ──► [schema/builtin] ──► [Validating webhooks] ──► etcd
                  có thể SỬA object          validate            chỉ cho / không cho
                  (chèn sidecar, set default)                    (thấy object cuối cùng)

MutatingWebhookConfiguration chạy trước, được sửa object (trả về một JSONPatch) — dùng để chèn sidecar, đặt giá trị mặc định. ValidatingWebhookConfiguration chạy sau, chỉ cho hoặc không cho — dùng để thực thi chính sách trên object đã hoàn chỉnh. Bài này làm loại validating, vì nó cho thấy rõ vòng request-response mà không vướng phần vá object.

API server gọi webhook bằng một POST chứa AdmissionReview, và chờ một AdmissionReview trả về với response.uid (khớp request), response.allowed (bool), kèm response.status.message nếu từ chối. Vì là gọi HTTPS, webhook bắt buộc chạy TLS và API server phải tin được cert đó qua caBundle.

Webhook server và cert

Logic webhook ở đây gọn: từ chối mọi pod không có label team. Viết bằng Python, đọc AdmissionReview rồi trả lời:

def do_POST(self):
    body = json.loads(self.rfile.read(int(self.headers['Content-Length'])))
    req = body['request']
    labels = (req['object'].get('metadata') or {}).get('labels') or {}
    allowed = 'team' in labels
    resp = {'apiVersion':'admission.k8s.io/v1','kind':'AdmissionReview',
            'response':{'uid':req['uid'],'allowed':allowed}}
    if not allowed:
        resp['response']['status'] = {'code':403,'message':'pod phai co label "team"'}
    # ...trả JSON qua TLS...

Phần khó hơn là cert. API server gọi service qua tên webhook.webhook-demo.svc, nên cert phải có đúng SAN đó, và ký bằng một CA mà ta sẽ đưa vào webhook config:

openssl req -x509 -new -nodes -key ca.key -subj "/CN=webhook-ca" -out ca.crt
# server cert với subjectAltName = DNS:webhook.webhook-demo.svc, ký bởi CA trên
openssl x509 -req -in tls.csr -CA ca.crt -CAkey ca.key -extfile csr.cnf -out tls.crt

Đưa cert vào cụm rồi chạy server: cert thành Secret, script thành ConfigMap, một Deployment python:3.12-alpine mount cả hai và lắng nghe :8443, một Service webhook:443 trỏ vào:

kubectl -n webhook-demo create secret tls webhook-tls --cert=tls.crt --key=tls.key
kubectl -n webhook-demo create configmap webhook-src --from-file=server.py
kubectl -n webhook-demo apply -f webhook-deploy.yaml   # Deployment + Service

Đăng ký webhook

ValidatingWebhookConfiguration nối API server với service. caBundle là CA đã ký cert (để API server tin server), rules lọc request nào gọi webhook, và namespaceSelector giới hạn phạm vi — điểm quan trọng để webhook không can thiệp cả cụm:

apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata: {name: require-team-label}
webhooks:
- name: require-team.kkloud.io
  rules:
  - apiGroups: [""], apiVersions: ["v1"], operations: ["CREATE"], resources: ["pods"]
  clientConfig:
    service: {namespace: webhook-demo, name: webhook, port: 443, path: "/"}
    caBundle: <base64 ca.crt>
  admissionReviewVersions: ["v1"]
  sideEffects: None
  failurePolicy: Fail
  namespaceSelector:
    matchLabels: {webhook: enabled}

failurePolicy: Fail nghĩa là nếu webhook không trả lời, request bị từ chối — an toàn hơn Ignore (cho qua khi webhook hỏng) nhưng cũng nghĩa là webhook chết thì chặn luôn pod, nên phải để nó sẵn sàng cao. sideEffects: None khai webhook không gây tác dụng phụ ngoài việc trả lời.

Thử

Gắn nhãn webhook=enabled lên một namespace để nó vào phạm vi, rồi tạo hai pod:

kubectl label namespace wh-test webhook=enabled
kubectl -n wh-test run nolabel  --image=busybox:1.36 --command -- sleep 100000
kubectl -n wh-test run haslabel --image=busybox:1.36 --labels="team=blue" --command -- sleep 100000
Error from server: admission webhook "require-team.kkloud.io" denied the request: pod phai co label "team"
pod/haslabel created

Pod thiếu label bị từ chối với đúng message từ webhook; pod có team=blue được tạo. API server đã gọi tới Python server của ta giữa lúc xử lý request và làm theo câu trả lời. Kiểm phạm vi: tạo pod thiếu label ở default (không gắn webhook=enabled):

kubectl -n default run free --image=busybox:1.36 --command -- sleep 100000
pod/free created

Pod này tạo được — namespaceSelector khiến webhook chỉ áp cho namespace gắn nhãn. Phạm vi hẹp như vậy quan trọng: một webhook failurePolicy: Fail áp toàn cụm mà chết là chặn mọi pod, kể cả pod hệ thống.

🧹 Dọn dẹp

kubectl delete validatingwebhookconfiguration require-team-label    # xóa config TRƯỚC
kubectl delete namespace webhook-demo wh-test

Xóa ValidatingWebhookConfiguration trước khi xóa server: nếu xóa server trước mà config còn (failurePolicy Fail), mọi pod CREATE trong namespace khớp sẽ bị chặn vì webhook không còn trả lời. Manifest ở github.com/nghiadaulau/kubernetes-from-scratch, thư mục 58-admission-webhook.

Tổng kết

Admission webhook chèn logic của bạn vào đường ghi: API server POST một AdmissionReview tới một dịch vụ HTTPS và làm theo response.allowed. Mutating webhook chạy trước và sửa được object (qua JSONPatch); validating webhook chạy sau và chỉ cho/không cho. Ta dựng một validating webhook thật bằng Python — tự ký cert cho webhook.webhook-demo.svc, đưa CA vào caBundle để API server tin — bắt pod phải có label team: pod thiếu bị từ chối ngay với message từ webhook, pod có thì qua. namespaceSelector giới hạn phạm vi, failurePolicy: Fail chặn khi webhook hỏng (an toàn nhưng đòi webhook sẵn sàng cao), sideEffects: None khai không tác dụng phụ. Thứ tự dọn dẹp cũng là bài học vận hành: gỡ config trước, gỡ server sau.

CRD (Bài 57) cho cấu trúc dữ liệu, webhook (bài này) chen vào lúc ghi — nhưng cả hai vẫn thụ động, không tự làm gì khi object thay đổi. Bài 59 ghép chúng thành operator: một controller chạy vòng lặp, theo dõi custom resource và hành động để đưa thực tế về khớp với mong muốn.

Related Posts