Skip to main content

3.3. Scheduling: Multiple Schedulers and Admission Controllers

Mục lục​


1. Vì sao cần nhiều scheduler trong một cluster?​

Scheduler mặc định đã xuất hiện ở các phần trước: nó phân bổ pod đều lên các node, đồng thời tôn trọng những điều kiện đã khai qua taints và tolerations, node affinity, nodeSelector. Nhưng có những yêu cầu mà không cơ chế nào trong số đó diễn đạt được. Giả sử một application đòi các thành phần của nó chỉ được đặt lên node sau khi đã qua vài phép kiểm tra riêng của đội vận hành, chẳng hạn tra một hệ thống bên ngoài để biết node có đang trong cửa sổ bảo trì hay không. Lúc đó cần một thuật toán lập lịch riêng, nơi có thể nhét thêm điều kiện của chính mình vào.

💡 Hình dung: Giống một thành phố có nhiều hãng taxi cùng hoạt động. Gọi xe qua tổng đài chung (default-scheduler) thì bất kỳ xe nào phù hợp nhất cũng được điều đến đón. Nhưng khi cần một dịch vụ đặc thù, chẳng hạn xe chở hàng cồng kềnh hoặc xe có ghế trẻ em, khách chỉ định đích danh một hãng riêng (schedulerName) ngay từ lúc đặt xe. Tổng đài chung sẽ không đụng đến cuốc xe đó, để đúng hãng riêng lo liệu.

Pod schedulerName Routes to the Right SchedulerPod Ano schedulerNamePod BschedulerName: my-schedulerdefault-schedulermy-schedulerNode chosenNode chosen

Kubernetes mở đường cho việc này: có thể viết một chương trình scheduler riêng, đóng gói, rồi deploy nó làm scheduler mặc định hoặc làm scheduler bổ sung trong cluster. Phần lớn application vẫn dùng scheduler mặc định, chỉ vài application chọn scheduler riêng. Một cluster vì vậy chạy được nhiều scheduler cùng lúc, và lúc tạo Pod hay Deployment thì khai rõ Pod đó muốn scheduler nào lập lịch cho mình.

Nhiều scheduler thì buộc phải có tên khác nhau, vì tên là thứ duy nhất phân biệt chúng. Scheduler mặc định tên default-scheduler, khai trong kube-scheduler configuration file. Bản thân scheduler mặc định không cần file này: không khai tên thì nó tự lấy default-scheduler. Còn nếu tự viết file, nội dung tối giản trông như sau:

apiVersion: kubescheduler.config.k8s.io/v1
kind: KubeSchedulerConfiguration
profiles:
- schedulerName: default-scheduler

Với các scheduler khác, có thể tạo một configuration file riêng và set schedulerName tương tự.

Deploy scheduler bổ sung: binary, Pod hay Deployment​

Cách đơn giản nhất trước. Phần trước đã dựng kube-scheduler theo lối tải binary về rồi chạy nó như một service kèm một tập option. Scheduler bổ sung dùng lại đúng binary đó, hoặc một binary tự build nếu cần nó xử sự khác đi. Ở đây dùng lại binary có sẵn, chỉ đổi đường dẫn config sang file tùy chỉnh vừa viết. Mỗi scheduler như vậy có configuration file riêng, và trong đó có tên scheduler riêng. Còn vài option khác cũng phải truyền vào, chẳng hạn file kubeconfig để xác thực với Kubernetes API, nhưng tạm gác lại cho gọn.

Trên thực tế gần như không ai deploy custom scheduler theo lối đó nữa, vì với cluster dựng bằng kubeadm thì mọi control plane component đều chạy dưới dạng Pod hoặc Deployment trong chính cluster. Nên xem tiếp cách deploy scheduler như một Pod. Viết một pod definition file, khai thuộc tính kubeconfig trỏ tới file scheduler.conf chứa thông tin xác thực để kết nối tới Kubernetes API server, rồi truyền custom kube-scheduler configuration file vào qua option config. Tên scheduler nằm trong file config đó, và đó là tên mà kube-scheduler tự nhận về mình.

Một option nữa cần nắm là leader-elect, cũng thuộc kube-scheduler configuration. Nó dùng khi có nhiều bản sao của cùng một scheduler chạy trên các master node khác nhau, tức thiết lập high availability (HA) với process kube-scheduler chạy đồng thời trên vài node. Nhiều bản sao nhưng chỉ được phép một bản sao hoạt động tại một thời điểm, nếu không hai scheduler sẽ cùng ra quyết định cho một Pod. leader-elect là cơ chế chọn ra bản sao nắm quyền lập lịch đó. Sẽ bàn thêm về thiết lập HA ở phần khác. Với nhiều master node, cần cho custom scheduler một lock object riêng để nó không tranh leader election với scheduler mặc định. Trong scheduler configuration file, ba field phụ trách việc đó là leaderElection.resourceLock (loại object dùng làm lock), leaderElection.resourceName (tên object) và leaderElection.resourceNamespace (namespace chứa object).

Cách thứ ba là deploy scheduler bổ sung dưới dạng một Deployment, đúng như trang tài liệu Kubernetes về cấu hình nhiều scheduler hướng dẫn. Trang đó mở đầu bằng quy trình tự build scheduler: clone Kubernetes repo, sửa kube-scheduler, build rồi đóng gói thành Docker image. Phần sau là config file tạo scheduler dưới dạng Deployment, về cấu trúc gần giống Pod vừa xem. Trong đó có image (chính là custom kube-scheduler image vừa build), có config file chứa cấu hình my-scheduler, và binary được chạy vẫn là kube-scheduler.

Để chạy được còn vài điều kiện tiên quyết: một service account và một ClusterRoleBinding phục vụ xác thực (ClusterRoleBinding, ClusterRole và ServiceAccount đã bàn ở phần Security). Việc còn lại là đưa file config vào Deployment. Có hai lối: tạo file cục bộ trên node rồi mount vào như một volume thông thường, hoặc gói nó thành một ConfigMap. Nội dung ConfigMap chính là kube-scheduler configuration đã bàn, với leaderElect đặt false vì chỉ chạy một replica, kèm schedulerName. ConfigMap đó được mount vào Pod như một volume tại đúng đường dẫn mà option config trỏ tới, nên bên trong container lại thành một file YAML bình thường.

Chạy lệnh get pods trong namespace kube-system sẽ thấy custom scheduler mới, nếu nó được deploy dưới dạng Pod. Deploy dưới dạng Deployment thì tên Pod có thêm hậu tố sinh tự động, nhưng vẫn nằm ở đó. Nhớ kiểm tra đúng namespace.

Bộ file YAML mẫu để tự practice​

Phần trên mô tả ba lối deploy, dưới đây là toàn bộ file cần thiết để dựng thật một scheduler bổ sung tên my-scheduler trên cluster kubeadm. Thứ tự apply đúng như thứ tự liệt kê. Image dùng lại đúng version mà scheduler mặc định đang chạy, kiểm tra bằng kubectl describe pod kube-scheduler-controlplane -n kube-system | grep Image, rồi thay v1.28.0 bên dưới cho khớp.

1. my-scheduler-config.yaml: đây là kube-scheduler configuration file, chỗ duy nhất khai tên scheduler. Chạy một replica nên leaderElect đặt false:

apiVersion: kubescheduler.config.k8s.io/v1
kind: KubeSchedulerConfiguration
metadata:
name: my-scheduler
profiles:
- schedulerName: my-scheduler
leaderElection:
leaderElect: false

File này không nằm trong Pod spec mà được đưa vào container qua một ConfigMap:

kubectl create configmap my-scheduler-config \
--from-file=my-scheduler-config.yaml \
-n kube-system

# Output:
configmap/my-scheduler-config created

2. my-scheduler-rbac.yaml: scheduler là một client của api server, nên nó cần danh tính và quyền. Thiếu file này thì Pod scheduler vẫn Running nhưng log đầy lỗi forbidden, và không Pod nào được lập lịch:

apiVersion: v1
kind: ServiceAccount
metadata:
name: my-scheduler
namespace: kube-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: my-scheduler-as-kube-scheduler
subjects:
- kind: ServiceAccount
name: my-scheduler
namespace: kube-system
roleRef:
kind: ClusterRole
name: system:kube-scheduler
apiGroup: rbac.authorization.k8s.io
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: my-scheduler-as-volume-scheduler
subjects:
- kind: ServiceAccount
name: my-scheduler
namespace: kube-system
roleRef:
kind: ClusterRole
name: system:volume-scheduler
apiGroup: rbac.authorization.k8s.io
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: my-scheduler-extension-apiserver-authentication-reader
namespace: kube-system
roleRef:
kind: Role
name: extension-apiserver-authentication-reader
apiGroup: rbac.authorization.k8s.io
subjects:
- kind: ServiceAccount
name: my-scheduler
namespace: kube-system

Hai ClusterRole system:kube-scheduler và system:volume-scheduler có sẵn trong mọi cluster, đây chính là quyền mà scheduler mặc định đang dùng, nên chỉ cần bind lại cho ServiceAccount mới chứ không phải tự viết rule.

3. my-scheduler-pod.yaml: lối deploy dạng Pod, gọn nhất để thử:

apiVersion: v1
kind: Pod
metadata:
name: my-scheduler
namespace: kube-system
labels:
component: my-scheduler
tier: control-plane
spec:
serviceAccountName: my-scheduler
containers:
- name: kube-scheduler
image: registry.k8s.io/kube-scheduler:v1.28.0
command:
- /usr/local/bin/kube-scheduler
- --config=/etc/kubernetes/my-scheduler/my-scheduler-config.yaml
livenessProbe:
httpGet:
path: /healthz
port: 10259
scheme: HTTPS
initialDelaySeconds: 15
volumeMounts:
- name: config-volume
mountPath: /etc/kubernetes/my-scheduler
readOnly: true
volumes:
- name: config-volume
configMap:
name: my-scheduler-config

Ba chỗ phải khớp nhau, sai một chỗ là scheduler crash ngay lúc khởi động: tên ConfigMap ở volumes, mountPath của volume, và đường dẫn trong --config. Đường dẫn --config luôn là mountPath cộng thêm tên key của ConfigMap, ở đây là my-scheduler-config.yaml do --from-file lấy tên file làm key.

Không truyền --kubeconfig vì Pod chạy ngay trong cluster: kube-scheduler tự dùng in-cluster config lấy từ token của ServiceAccount đã khai ở serviceAccountName. Còn khi chạy scheduler ngoài cluster thì mới phải trỏ clientConnection.kubeconfig trong config file tới /etc/kubernetes/scheduler.conf.

4. my-scheduler-deployment.yaml: lối deploy dạng Deployment, đúng như tài liệu Kubernetes hướng dẫn. Khác bản Pod ở chỗ có controller trông coi, Pod chết thì được tạo lại:

apiVersion: apps/v1
kind: Deployment
metadata:
name: my-scheduler
namespace: kube-system
labels:
component: my-scheduler
tier: control-plane
spec:
replicas: 1
selector:
matchLabels:
component: my-scheduler
tier: control-plane
template:
metadata:
labels:
component: my-scheduler
tier: control-plane
spec:
serviceAccountName: my-scheduler
containers:
- name: kube-scheduler
image: registry.k8s.io/kube-scheduler:v1.28.0
command:
- /usr/local/bin/kube-scheduler
- --config=/etc/kubernetes/my-scheduler/my-scheduler-config.yaml
livenessProbe:
httpGet:
path: /healthz
port: 10259
scheme: HTTPS
initialDelaySeconds: 15
readinessProbe:
httpGet:
path: /healthz
port: 10259
scheme: HTTPS
resources:
requests:
cpu: "0.1"
securityContext:
privileged: false
volumeMounts:
- name: config-volume
mountPath: /etc/kubernetes/my-scheduler
readOnly: true
hostNetwork: false
hostPID: false
volumes:
- name: config-volume
configMap:
name: my-scheduler-config

5. nginx-my-scheduler.yaml: Pod để kiểm chứng, chỉ khác Pod thường ở một dòng schedulerName:

apiVersion: v1
kind: Pod
metadata:
name: nginx-my-scheduler
spec:
schedulerName: my-scheduler
containers:
- name: nginx
image: nginx

Apply và kiểm chứng. Bốn lệnh dưới đây là toàn bộ vòng thực hành:

kubectl apply -f my-scheduler-rbac.yaml
kubectl apply -f my-scheduler-pod.yaml
kubectl get pods -n kube-system | grep scheduler

# Output:
kube-scheduler-controlplane 1/1 Running 0 10d
my-scheduler 1/1 Running 0 12s
kubectl apply -f nginx-my-scheduler.yaml
kubectl get pod nginx-my-scheduler -o wide

# Output:
NAME READY STATUS RESTARTS AGE NODE
nginx-my-scheduler 1/1 Running 0 6s node01
kubectl get events -o wide | grep nginx-my-scheduler

# Output:
7s Normal Scheduled pod/nginx-my-scheduler Successfully assigned default/nginx-my-scheduler to node01 my-scheduler

Cột cuối ghi my-scheduler nghĩa là bài thực hành đã đạt. Nếu Pod kẹt Pending thì chẩn đoán theo hai bước: kubectl describe pod nginx-my-scheduler để xem có scheduler nào nhận việc chưa, rồi kubectl logs my-scheduler -n kube-system để đọc lỗi của chính scheduler. Ba nguyên nhân chiếm gần hết số ca: schedulerName trong Pod không khớp schedulerName trong ConfigMap, đường dẫn --config không trùng chỗ mount ConfigMap, và thiếu RBAC ở bước 2.

Biến thể HA nhiều master. Khi chạy nhiều replica trên nhiều control plane node, bật leader election và cấp cho custom scheduler một lock object riêng để nó không tranh quyền với scheduler mặc định:

apiVersion: kubescheduler.config.k8s.io/v1
kind: KubeSchedulerConfiguration
profiles:
- schedulerName: my-scheduler
leaderElection:
leaderElect: true
resourceLock: leases
resourceName: my-scheduler
resourceNamespace: kube-system

Lock object mới này lại cần quyền riêng, nên bổ sung thêm một ClusterRole và bind nó cho cùng ServiceAccount:

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: my-scheduler-leader-election
rules:
- apiGroups: ["coordination.k8s.io"]
resources: ["leases"]
verbs: ["create"]
- apiGroups: ["coordination.k8s.io"]
resources: ["leases"]
resourceNames: ["my-scheduler"]
verbs: ["get", "update"]

Biến thể chạy bằng binary. Muốn thử lối cũ, tức chạy scheduler như một service trên node thay vì trong cluster, thì config file thêm clientConnection.kubeconfig để scheduler biết cách xác thực:

apiVersion: kubescheduler.config.k8s.io/v1
kind: KubeSchedulerConfiguration
clientConnection:
kubeconfig: /etc/kubernetes/scheduler.conf
profiles:
- schedulerName: my-scheduler
leaderElection:
leaderElect: false

Rồi chạy đúng binary mà kubeadm đã đặt trên node, trỏ vào file config đó:

/usr/local/bin/kube-scheduler --config=/etc/kubernetes/my-scheduler-config.yaml

Dọn dẹp sau khi practice xong, để cluster không còn scheduler lạ và những Pod phụ thuộc nó:

kubectl delete pod nginx-my-scheduler
kubectl delete -f my-scheduler-pod.yaml
kubectl delete -f my-scheduler-rbac.yaml
kubectl delete configmap my-scheduler-config -n kube-system

Gán Pod cho scheduler mới và kiểm chứng ai đã lập lịch​

Sau khi đã deploy custom scheduler, bước tiếp theo là trỏ một Pod hoặc Deployment sang nó. Việc này gọn đến mức gây bất ngờ: thêm vào pod definition file một field tên schedulerName với giá trị là tên scheduler mới, thế là xong. Lúc Pod được tạo, đúng scheduler đó nhận việc. Tạo Pod bằng kubectl create. Cấu hình scheduler đúng thì Pod chuyển sang Running, còn sai ở đâu đó thì Pod nằm mãi ở Pending. Với Pod Pending, kubectl describe pod cho biết nguyên nhân, và lý do thường gặp nhất vẫn là scheduler chưa được cấu hình đúng.

Với nhiều scheduler trong cluster, làm sao biết cái nào đã lập lịch cho một Pod cụ thể? Câu trả lời nằm trong event: chạy kubectl get events kèm option -o wide để liệt kê mọi event trong namespace hiện tại, rồi tìm event Scheduled. Cột source của event ghi đúng tên custom scheduler đã đặt, kèm thông báo gán thành công. Khi có sự cố thì đọc thêm log của chính scheduler bằng kubectl logs, truyền tên Pod hoặc Deployment của scheduler cùng đúng namespace.

Demo: deploy một custom scheduler và gán Pod cho nó​

Xem scheduler mặc định đang chạy như thế nào. Trên cluster kubeadm, nó là một static Pod (đã học ở Part 3.2):

kubectl get pods -n kube-system
kubectl describe pod kube-scheduler-controlplane -n kube-system

# Output (rút gọn):
Image: registry.k8s.io/kube-scheduler:v1.28.0

Tạo ConfigMap chứa config cho scheduler mới. Nội dung của nó là file KubeSchedulerConfiguration với schedulerName: my-scheduler, đúng như my-scheduler-config.yaml ở mục trên:

kubectl create configmap my-scheduler-config --from-file=/root/my-scheduler-config.yaml -n kube-system

# Output:
configmap/my-scheduler-config created

Deploy scheduler mới dưới dạng Pod, dùng đúng image mà scheduler mặc định đang dùng, mount ConfigMap vừa tạo làm volume config. File /root/my-scheduler.yaml ở đây chính là my-scheduler-pod.yaml đã liệt kê đầy đủ ở mục trên, và RBAC phải được apply trước:

kubectl create -f /root/my-scheduler.yaml
kubectl get pods -n kube-system

# Output:
NAME READY STATUS RESTARTS AGE
my-scheduler 1/1 Running 0 5s
kube-scheduler-controlplane 1/1 Running 0 10d

Cả 2 scheduler cùng chạy song song mà không xung đột, vì mỗi cái chỉ nhận những Pod có schedulerName khớp đúng tên mình.

Gán Pod cho scheduler mới bằng field schedulerName:

apiVersion: v1
kind: Pod
metadata:
name: nginx
spec:
schedulerName: my-scheduler
containers:
- name: nginx
image: nginx
kubectl create -f nginx.yaml
kubectl get events -o wide | grep Scheduled

# Output:
5s Normal Scheduled pod/nginx Successfully assigned default/nginx to node01 my-scheduler

Cột cuối trong dòng event là bằng chứng cần tìm: giá trị ở đó là my-scheduler, không phải default-scheduler như mọi Pod bình thường khác. Nếu Pod kẹt Pending, kubectl describe pod cho biết ngay scheduler nào đã cố xử lý và thất bại, hoặc cho biết chẳng scheduler nào nhận việc cả.

Deploy nhiều scheduler binary riêng biệt như trên chạy được, nhưng kèm hai cái giá. Thứ nhất, phải tự trông từng process một. Thứ hai, nghiêm trọng hơn: vì là các process độc lập không biết gì về nhau, chúng có thể rơi vào race condition, cùng lúc quyết định đặt Pod lên một node mà mỗi bên đều tưởng node đó còn trống. Từ Kubernetes 1.18 có lối gọn hơn nhiều: chạy nhiều "profile" trong cùng một scheduler binary.


2. Scheduler Profiles: nhiều scheduler trong một binary​

Trước tiên nhắc lại cách Kubernetes scheduler hoạt động, qua ví dụ đơn giản về việc lập lịch một Pod lên một trong bốn node thuộc Kubernetes cluster.

💡 Hình dung: Giống một dây chuyền phỏng vấn tuyển dụng nhiều vòng. Vòng đầu xếp hồ sơ theo độ ưu tiên (queue sort), vòng tiếp loại thẳng ứng viên không đạt yêu cầu tối thiểu (filter), vòng sau chấm điểm những người còn lại (score), vòng cuối ký hợp đồng với người điểm cao nhất (bind). Mỗi vòng là một trạm cố định trong quy trình, nên công ty muốn siết thêm một tiêu chí thì không phải viết lại cả dây chuyền: chỉ cắm một bài kiểm tra riêng (custom plugin) vào đúng trạm cần can thiệp.

Scheduling Pipeline: Extension Points and PluginsQueue Sortplugin:PrioritySortorders pods byPriorityClassFilterplugin:NodeResourcesFitdrops nodes withoutenough resourcesScoreplugin:ImageLocalityranks remainingnodes by fitBindplugin:DefaultBinderassigns Pod totop-scored nodeCustom plugins attach to any extension point (PreFilter, PostFilter, PreScore, Reserve, Permit...).A scheduler profile just chooses which plugins run at each stage: same binary, different behavior.

Pod đi qua những giai đoạn nào trước khi được gắn vào node?​

Có một pod definition file, và Pod đang chờ được lập lịch lên một trong bốn node. Pod có resource requirement là 10 CPU. Vậy nó chỉ được lập lịch lên một node có 10 CPU còn lại. Trên node 1, cũng có một số pod khác đang chờ được lập lịch.

Việc đầu tiên xảy ra khi các pod này được tạo là chúng rơi vào một scheduling queue, tức chỗ pod nằm chờ đến lượt được lập lịch. Tại đây pod được xếp thứ tự theo priority khai trên từng Pod, và Pod trong ví dụ này có priority cao. Muốn đặt priority thì trước tiên phải tạo một PriorityClass:

apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: high-priority
value: 1000000

Object này chỉ cần một cái tên và một giá trị priority, ở đây là 1 triệu, tức rất cao. Nhờ giá trị đó mà Pod được đẩy lên đầu hàng đợi và lập lịch trước những Pod khác, và việc xếp thứ tự ấy diễn ra ngay trong scheduling queue.

Sau đó Pod bước vào filter phase, giai đoạn gạt bỏ những node không thể chạy được Pod. Ở ví dụ này, hai node đầu tiên không còn đủ 10 CPU nên bị loại ngay.

Giai đoạn tiếp theo là scoring phase, nơi các node còn sống sót được chấm điểm theo những trọng số khác nhau. Với hai node còn lại, scheduler tính điểm dựa trên khoảng trống còn lại sau khi đã trừ ra 10 CPU cấp cho Pod: node thứ nhất còn 2, node thứ hai còn 6. Node thứ hai điểm cao hơn nên thắng.

Cuối cùng, ở binding phase, Pod được gắn (bound) vào node có điểm số cao nhất.

Plugin và extension point: nơi cắm code của riêng mình​

Tất cả những hoạt động này được thực hiện bởi các plugin cụ thể. Ví dụ, trong lúc ở scheduling queue, chính plugin PrioritySort sắp xếp các Pod theo thứ tự dựa trên priority được cấu hình trên Pod. Đây là cách các pod có PriorityClass nhận priority cao hơn các pod khác khi lập lịch.

Trong giai đoạn filtering, plugin NodeResourcesFit là cái xác định node nào đủ tài nguyên cho pod và gạt phần còn lại. Cùng giai đoạn này còn có plugin NodeName, đọc tên node ghi trong pod spec rồi loại mọi node không khớp tên đó. Và plugin NodeUnschedulable loại những node có cờ unschedulable đặt true, tức những node vừa bị cordon (lệnh này sẽ bàn ở phần sau); nhờ nó mà node đã cordon không nhận thêm pod mới nào.

Trong giai đoạn scoring, plugin NodeResourcesFit xuất hiện lần nữa, lần này để gán điểm cho mỗi node dựa trên tài nguyên còn lại sau khi Pod đã được phân bổ vào đó. Cách chấm điểm đó có thể đổi được bằng cách chọn một trong ba strategy: LeastAllocated (mặc định) cho điểm cao cho node còn trống nhiều nhất, MostAllocated làm điều ngược lại để dồn workload vào ít node hơn, và RequestedToCapacityRatio chấm theo tỉ lệ giữa lượng tài nguyên yêu cầu và dung lượng node. Ví dụ tính điểm vừa nói ở trên (node còn 6 CPU thắng node còn 2 CPU) chính là hành vi của LeastAllocated. Một plugin như vậy tham gia được vào nhiều giai đoạn khác nhau. Cũng ở giai đoạn scoring còn có ImageLocality, cho điểm cao hơn những node đã sẵn container image mà Pod cần. Cần phân biệt rõ vai trò của giai đoạn này: plugin score không có quyền từ chối một node. ImageLocality chỉ nghiêng cán cân về phía node đã có image, nhưng nếu chẳng node nào có sẵn image thì Pod vẫn được đặt lên một node nào đó và image được pull về sau. Giai đoạn này chấm điểm, không loại ai.

Cuối cùng, ở binding phase, plugin DefaultBinder lo phần gắn Pod vào node.

Việc chọn plugin nào chạy ở đâu, và việc tự viết plugin rồi gắn vào, đều dựa trên một cơ chế duy nhất: các extension point. Mỗi giai đoạn của pipeline phơi ra một extension point để plugin cắm vào. Scheduling queue có extension point queue sort, chỗ PrioritySort đang đứng. Tương tự có filter, score và bind, nơi các plugin vừa kể lần lượt cắm vào.

Danh sách thật ra dài hơn bốn cái đó. Trước filter có PreFilter, sau filter có PostFilter. Trước score có PreScore, sau score có Reserve. Trước bind có Permit và PreBind, sau bind có PostBind. Nghĩa là gần như mọi thời điểm trong hành trình của một Pod đều có chỗ để chen code của mình vào: viết một plugin rồi khai nó ở đúng extension point cần can thiệp.

Tài liệu Kubernetes còn liệt kê nhiều plugin mặc định khác gắn vào các extension point khác nhau. Có plugin trải qua vài extension point cùng lúc, có plugin chỉ phục vụ đúng một chỗ.

Nắm được plugin và extension point rồi thì phần còn lại là cú pháp: khai ở đâu để đổi hành vi mặc định, và khai thế nào để gắn plugin của mình vào.

Khai nhiều profile trong một scheduler configuration file​

Quay lại mô hình đã bàn ở phần trên: ba scheduler riêng biệt gồm scheduler mặc định, my-scheduler và my-scheduler-2, mỗi cái là một binary chạy với config file riêng. Cách đó dùng được, nhưng trả giá ở hai chỗ. Phải tự bảo trì ba process độc lập. Và vì chúng độc lập với nhau, chúng có thể vướng race condition lúc ra quyết định: một scheduler đặt workload lên một node mà không hay biết đúng lúc đó có scheduler khác cũng đang đặt workload lên chính node ấy.

Kubernetes 1.18 giới thiệu tính năng hỗ trợ nhiều profile trong một scheduler duy nhất, và đó là cách dứt điểm cả hai vấn đề trên. Trong scheduler configuration file, thêm nhiều entry vào danh sách profiles, mỗi entry một tên scheduler riêng:

apiVersion: kubescheduler.config.k8s.io/v1
kind: KubeSchedulerConfiguration
profiles:
- schedulerName: my-scheduler-2
- schedulerName: my-scheduler-3

Mỗi entry thành một profile, và với người dùng bên ngoài thì mỗi profile hành xử như một scheduler độc lập. Khác biệt duy nhất nằm ở bên trong: tất cả chúng chạy trong cùng một binary, một process, thay vì mỗi cái một binary.

Ở dạng vừa khai thì các profile này chỉ khác nhau cái tên, nên hành vi của chúng giống hệt scheduler mặc định. Muốn chúng xử sự khác nhau thì phải can thiệp vào plugin của từng profile. Chẳng hạn với my-scheduler-2, tắt plugin TaintToleration và bật plugin tùy chỉnh của mình vào chỗ đó. Với my-scheduler-3, tắt sạch mọi plugin PreScore và Score để nó bỏ luôn bước chấm điểm. Cấu trúc như sau:

profiles:
- schedulerName: my-scheduler-2
plugins:
filter:
disabled:
- name: TaintToleration
enabled:
- name: MyCustomPlugin
- schedulerName: my-scheduler-3
plugins:
preScore:
disabled:
- name: '*'
score:
disabled:
- name: '*'

Dưới section plugins, chỉ định extension point và enable hoặc disable các plugin theo tên, hoặc theo một pattern như trong trường hợp này.

Có ba ràng buộc của cơ chế profile mà người mới rất dễ vướng vào.

Mọi profile phải dùng chung một plugin queueSort, với cùng tham số cấu hình. Lý do nằm ở kiến trúc: khai bao nhiêu profile thì cả binary vẫn chỉ có duy nhất một hàng đợi pod đang chờ. Một hàng đợi không thể xếp theo hai quy tắc cùng lúc, nên toàn bộ profile buộc phải thống nhất ở extension point này. Hệ quả khi vận hành: mỗi profile được tự do có bộ plugin filter và score riêng, nhưng không thể để profile A ưu tiên pod theo PriorityClass trong khi profile B xếp thứ tự theo tiêu chí khác. Cũng vì vậy queueSort chỉ bật được đúng một plugin tại một thời điểm.

Phải giữ lại một profile tên default-scheduler. Pod nào được tạo mà không khai spec.schedulerName thì chính kube-apiserver điền sẵn giá trị default-scheduler vào đó. Nên nếu viết lại file cấu hình và đổi hết tên profile sang tên riêng, mọi Pod bình thường trong cluster sẽ trỏ tới một profile không tồn tại rồi kẹt Pending vô thời hạn. Đây là kiểu lỗi khó truy, vì file cấu hình đọc lên vẫn hợp lệ và scheduler vẫn khởi động bình thường.

Tên profile phải là duy nhất. Hai profile trùng tên khiến scheduler không khởi động được.

Còn một field tiết kiệm được nhiều dòng YAML khi bắt đầu tùy biến nhiều plugin: multiPoint. Một plugin như NodeResourcesFit có mặt ở ba extension point (preFilter, filter, score), nên tắt nó theo lối thường phải liệt kê đủ cả ba chỗ, thiếu một chỗ là nó vẫn chạy. Với multiPoint, khai tên plugin đúng một lần là nó được bật hoặc tắt ở mọi extension point mà nó có mặt:

apiVersion: kubescheduler.config.k8s.io/v1
kind: KubeSchedulerConfiguration
profiles:
- schedulerName: multipoint-scheduler
plugins:
multiPoint:
enabled:
- name: MyCustomPlugin
disabled:
- name: NodeResourcesFit

Muốn đào sâu hơn thì đọc Kubernetes Enhancement Proposal đã khai sinh tính năng multi-scheduling profile, tức KEP 1451, cùng bài viết về scheduling framework trong tài liệu chính thức.

Scheduler quyết định Pod đi node nào, nhưng đó chỉ là một chặng trong hành trình của request. Trước khi Pod kịp tồn tại để scheduler nhìn thấy, lệnh tạo nó đã phải lọt qua một loạt cửa kiểm soát khác ngay tại api server. Chặng hay bị bỏ sót nhất trong loạt cửa đó là admission controller.


3. Admission Controllers: chặng kiểm soát cuối trước etcd​

Từ đầu khóa đến giờ, mọi thao tác trên cluster đều đi qua tiện ích kubectl ở command line. Mỗi request, ví dụ một lệnh tạo Pod, đều đi tới api server, rồi Pod được tạo, rồi thông tin được lưu xuống etcd database.

Chặng đầu tiên khi request chạm api server là authentication, thường dựa trên certificate. Với request gửi từ kubectl, certificate đã nằm sẵn trong kubeconfig file, và việc của chặng này là xác định ai gửi request cùng với việc người đó có hợp lệ hay không. Chặng thứ hai là authorization, nơi hệ thống xét xem user đó có quyền làm thao tác vừa yêu cầu, thông qua Role-Based Access Control (RBAC).

💡 Hình dung: Giống việc vào một tòa nhà an ninh cao. Cổng đầu tiên kiểm tra thẻ ra vào xem có đúng là nhân viên thật không (authentication). Cổng thứ hai kiểm tra thẻ đó có quyền vào khu vực đang định tới không (authorization/RBAC). Nhưng ngay trước cửa phòng làm việc còn một trạm nữa, nơi bảo vệ soi hành lý và ghi lại mục đích ra vào (admission controller). Trạm này có hai kiểu xử lý: từ chối thẳng nếu hành lý vi phạm quy định riêng của tòa nhà, hoặc chỉ ghi thêm vài dòng vào sổ rồi vẫn cho qua.

Request Flow: Authentication → Authorization → AdmissionkubectlAuthenticationwho are you?AuthorizationRBAC allowed?Admission Controlmutating → validatingcan modify or rejectetcdrejected at any stage→ error returned to user

Ví dụ, user được gán role developer thì được phép list, get, create, update hoặc delete Pod. Request gửi đến khớp một trong các quyền đó, ở đây là request tạo pod, thì đi qua; không khớp thì bị từ chối. Đó là authorization với role-based access control.

RBAC đặt được khá nhiều loại giới hạn: cho phép hay từ chối người mang một role nào đó tạo, list hoặc xóa từng loại object như pod, deployment, service. Nó còn khoanh được tới tên resource cụ thể, chẳng hạn một developer chỉ thao tác được trên pod tên blue hoặc orange, hoặc chỉ trong một namespace. Nhưng để ý kỹ thì mọi rule kiểu đó đều dừng ở mức Kubernetes API: user nào gọi được thao tác API nào. RBAC không đọc vào bên trong object.

Những rule mà RBAC không diễn đạt được​

RBAC trả lời được câu hỏi "ai được làm gì với loại object nào", nhưng nhiều yêu cầu vận hành lại nằm ở nội dung của object, chỗ RBAC không nhìn tới. Vài ví dụ quen thuộc. Khi có request tạo Pod, muốn đọc luôn configuration file, xem tên image, rồi chặn mọi image kéo từ public Docker Hub và chỉ nhận image từ một registry nội bộ. Muốn cấm tuyệt đối tag latest trên mọi image. Muốn từ chối request nếu container định chạy dưới quyền root. Muốn giới hạn container chỉ được xin một vài Linux capability nhất định. Muốn bắt mọi object phải khai đủ bộ label quy định trong metadata.

Không yêu cầu nào trong số đó diễn đạt được bằng RBAC, và đây là phần việc của Admission Controllers. Chúng cho phép áp các quy tắc bảo mật chặt hơn lên cách cluster được sử dụng. Phạm vi của chúng cũng rộng hơn chuyện validate: một admission controller có thể sửa chính request đó, hoặc làm thêm vài thao tác ở phía sau trước khi Pod kịp được tạo.

Các admission controller có sẵn và cách bật/tắt​

Có một số admission controller có sẵn (pre-built) trong Kubernetes, ví dụ:

  • AlwaysPullImages: đảm bảo mỗi khi một Pod được tạo, image luôn được pull.
  • DefaultStorageClass: quan sát việc tạo PVC và tự động thêm một storage class mặc định vào nếu chưa được chỉ định.
  • EventRateLimit: giúp set giới hạn số lượng request mà api server có thể xử lý tại một thời điểm, để ngăn api server bị lụt request.
  • NamespaceExists: từ chối các request đến namespace không tồn tại.

Và còn nhiều admission controller khác.

Ví dụ chi tiết hơn về NamespaceExists: thử tạo một Pod trong namespace tên blue chưa hề tồn tại. Lệnh trả về lỗi the namespace blue is not found. Diễn biến bên trong: request được authenticate, rồi authorize, rồi đi qua dây admission controller. Đến lượt mình, NamespaceExists kiểm tra namespace blue có tồn tại hay không, và vì không, nó từ chối request tại đó.

NamespaceExists là một admission controller có sẵn (built-in), nhưng không nằm trong danh sách bật mặc định. Trên cluster mặc định, chính NamespaceLifecycle mới là plugin sinh ra lỗi vừa thấy, vì nó bao trùm luôn phép kiểm tra namespace tồn tại. Một plugin built-in khác cũng không bật mặc định là NamespaceAutoProvision, và nó làm điều ngược lại: tự động tạo namespace nếu namespace đó chưa tồn tại.

Xem danh sách admission controller đang bật mặc định bằng cách chạy kube-apiserver -h rồi grep enable-admission-plugins:

kube-apiserver -h | grep enable-admission-plugins

Phần default trong output chính là danh sách đang bật sẵn. Với cluster dựng bằng kubeadm, kube-apiserver không phải binary nằm trên PATH của node, nên phải kubectl exec vào pod kube-apiserver-controlplane rồi mới chạy được lệnh này.

Để thêm một admission controller, bổ sung tên nó vào flag --enable-admission-plugins. Chỗ sửa flag đó tùy cách cluster được dựng: api server chạy như một systemd service thì sửa trong định nghĩa service, còn api server chạy dưới dạng Pod theo lối kubeadm thì sửa trong manifest file /etc/kubernetes/manifests/kube-apiserver.yaml.

Tương tự, để disable admission controller plugin, dùng flag --disable-admission-plugins.

Sau khi cập nhật, thử lại lệnh tạo pod trong namespace chưa tồn tại. Request lần lượt qua authentication, authorization, rồi tới NamespaceAutoProvision. Plugin này thấy namespace còn thiếu nên tự tạo nó ra, và request đi tiếp để tạo Pod thành công. Liệt kê namespace lúc này sẽ thấy blue đã có mặt.

Ví dụ này cho thấy đúng hai mặt của một admission controller: nó vừa validate rồi từ chối request, vừa làm được những thao tác ở phía sau hoặc sửa thẳng vào request.

Một lưu ý về cluster thật: việc chặn request đến namespace không tồn tại ngày nay do NamespaceLifecycle đảm nhiệm, không phải NamespaceExists. NamespaceLifecycle nằm trong danh sách bật mặc định và làm ba việc: từ chối request đến namespace không tồn tại, chặn tạo object mới trong namespace đang ở trạng thái Terminating, và không cho xóa ba namespace hệ thống default, kube-system, kube-public. Còn NamespaceExists và NamespaceAutoProvision vẫn là plugin hợp lệ, vẫn nằm trong tài liệu chính thức; chúng chỉ không bật mặc định, muốn dùng thì phải tự bật.

Demo: bật/tắt admission controller và quan sát hiệu ứng​

Xem danh sách admission controller đang bật. Vì kube-apiserver chạy dưới dạng static Pod (kubeadm), phải exec vào đúng container để chạy -h:

kubectl exec -it kube-apiserver-controlplane -n kube-system -- kube-apiserver -h | grep enable-admission-plugins

# Output (rút gọn):
--enable-admission-plugins strings ... default: NamespaceLifecycle,LimitRanger,
ServiceAccount,DefaultStorageClass,DefaultTolerationSeconds,MutatingAdmissionWebhook,
ValidatingAdmissionWebhook,MutatingAdmissionPolicy,ValidatingAdmissionPolicy,
ResourceQuota,PodSecurity ...

Ba cái tên NamespaceLifecycle, MutatingAdmissionWebhook và ValidatingAdmissionWebhook đều có mặt, nghĩa là bật sẵn. NamespaceAutoProvision thì không, nên muốn dùng phải tự thêm vào.

Tạo Pod trong một namespace chưa tồn tại để thấy NamespaceLifecycle chặn request ngay tại api server, trước khi request kịp chạm tới scheduler:

kubectl run nginx --image=nginx -n blue

# Output:
Error from server (NotFound): namespaces "blue" not found

Bật NamespaceAutoProvision để namespace được tạo tự động thay vì bị từ chối, bằng cách sửa trực tiếp static Pod manifest của kube-apiserver:

grep enable-admission-plugins /etc/kubernetes/manifests/kube-apiserver.yaml

# Output:
- --enable-admission-plugins=NodeRestriction
sed -i 's/enable-admission-plugins=NodeRestriction/enable-admission-plugins=NodeRestriction,NamespaceAutoProvision/' /etc/kubernetes/manifests/kube-apiserver.yaml

Sửa manifest trong /etc/kubernetes/manifests/ là kubelet tự phát hiện thay đổi và khởi động lại kube-apiserver, đúng cơ chế static Pod đã học ở Part 3.2. Chờ khoảng 30-60s rồi thử lại:

kubectl run nginx --image=nginx -n blue
kubectl get namespace blue

# Output:
pod/nginx created
NAME STATUS AGE
blue Active 4s

Lần này namespace blue tự được tạo, đúng hành vi mutating của NamespaceAutoProvision. Lỗi namespaces "blue" not found ở bước trước do NamespaceLifecycle sinh ra, vì đó là plugin bật mặc định trên cluster này.

Tắt một admission controller dùng đúng cú pháp tương tự, chỉ đổi tên flag:

sed -i 's/enable-admission-plugins=\(.*\)/&\n - --disable-admission-plugins=DefaultStorageClass/' /etc/kubernetes/manifests/kube-apiserver.yaml

Vẫn cơ chế restart tự động đó, nên chờ một lát rồi xác nhận lại bằng cách exec vào kube-apiserver và xem process đang chạy với những flag nào.

Bộ admission controller có sẵn giải quyết được phần lớn nhu cầu chuẩn: namespace, storage class, resource quota. Nhưng những luật riêng của từng tổ chức, chẳng hạn chặn container chạy root, thì không có plugin nào sẵn sàng làm hộ. Lúc đó phải tự viết một webhook server.


4. Webhook: đưa logic admission ra ngoài api server​

Các plugin NamespaceExists và NamespaceLifecycle vừa xem đều làm một việc: kiểm tra namespace có tồn tại hay không rồi từ chối request nếu không. Loại plugin chỉ xét rồi cho qua hoặc chặn lại như vậy gọi là validating admission controller.

💡 Hình dung: Giống việc thuê một chuyên gia thẩm định độc lập bên ngoài thay vì tự quyết. Công ty (api server) không tự phán xét hồ sơ: nó đóng gói trọn bộ hồ sơ (AdmissionReview request) rồi gửi cho chuyên gia (webhook server). Chuyên gia trả lại đúng bộ hồ sơ đó kèm phán quyết (AdmissionReview response), với ba khả năng: cho qua, từ chối thẳng, hoặc yêu cầu sửa vài chi tiết rồi mới cho đi tiếp.

Giờ xem một plugin thuộc loại khác: DefaultStorageClass, cũng bật mặc định. Khi có request tạo PVC, request lần lượt qua authentication, authorization, rồi tới dây admission controller. DefaultStorageClass theo dõi các request tạo PVC và kiểm tra xem PVC đó có khai storage class hay không. Nếu không khai, nó sửa request để nhét storage class mặc định vào, lấy đúng StorageClass nào đang được đánh dấu là mặc định của cluster.

Nên khi PVC đã tạo xong và đọc lại nó, sẽ thấy storageClassName: default nằm trong spec, dù lúc gửi đi chưa từng khai field này. Loại plugin làm được việc đó gọi là Mutating Admission Controller, vì nó sửa hay "mutate" chính object trước khi object được tạo.

Vậy có hai loại admission controller: mutating sửa được request, còn validating chỉ xét request rồi cho qua hoặc từ chối. Một admission controller cũng có thể kiêm cả hai vai, vừa mutate vừa validate cùng một request.

Thứ tự giữa hai loại không phải chuyện tùy ý: mutating luôn chạy trước, rồi mới đến validating. Có vậy thì mọi thay đổi do mutating gây ra mới nằm trong tầm mắt của bước validation. Cặp plugin ở phần trên minh họa rõ điều này: NamespaceAutoProvision là mutating nên chạy trước, NamespaceExists là validating nên chạy sau. Đảo thứ tự lại thì NamespaceExists sẽ từ chối mọi request đến namespace không tồn tại, và NamespaceAutoProvision chẳng bao giờ có cơ hội tạo namespace còn thiếu.

Trên cả dây admission controller đó, chỉ cần một plugin từ chối là request bị chặn và user nhận về thông báo lỗi. Toàn bộ những plugin vừa kể đều là loại built-in: chúng nằm trong Kubernetes source code, được compile và phát hành cùng Kubernetes.

Nhưng luật riêng của từng tổ chức thì không thể trông vào plugin có sẵn. Để mở đường cho admission controller bên ngoài, Kubernetes dành ra hai plugin đặc biệt: MutatingAdmissionWebhook và ValidatingAdmissionWebhook.

AdmissionReview: api server và webhook server nói gì với nhau​

Hai plugin này được cấu hình để trỏ tới một server, đặt bên trong cluster hoặc ở ngoài cũng được. Server đó chạy admission webhook service với logic và code của chính mình. Dòng chảy như sau: request đi hết dây admission controller built-in, rồi tới lượt webhook. Lúc đó api server gọi sang admission webhook server, gửi kèm một object AdmissionReview ở định dạng JSON. Object này chở theo trọn bộ ngữ cảnh của request: ai gửi, thao tác gì, trên loại object nào, và nội dung chi tiết của chính object đó.

Nhận được request, admission webhook server trả về cũng một object AdmissionReview, lần này mang phán quyết. Field allowed đặt true thì request đi tiếp, đặt false thì request bị từ chối.

Rút gọn lại, request và response trông như sau (chỉ giữ những field quan trọng nhất để dễ hình dung):

// Request minh họa — api server gửi cho webhook server
{
"apiVersion": "admission.k8s.io/v1",
"kind": "AdmissionReview",
"request": {
"uid": "705ab4f5-6393-11e8-b7cc-42010a800002",
"operation": "CREATE",
"userInfo": { "username": "alice", "groups": ["developers"] },
"resource": { "group": "", "version": "v1", "resource": "pods" },
"object": { "metadata": { "name": "nginx" }, "spec": { "containers": ["..."] } }
}
}
// Response minh họa — webhook server trả lại cho api server
{
"apiVersion": "admission.k8s.io/v1",
"kind": "AdmissionReview",
"response": {
"uid": "705ab4f5-6393-11e8-b7cc-42010a800002",
"allowed": false,
"status": { "message": "container image must come from internal registry" }
}
}

(Output minh họa — cấu trúc và tên field đúng theo Kubernetes API, số liệu cụ thể chỉ mang tính ví dụ.)

Để ý field uid trong response: nó phải trùng khớp với uid của request, vì đó là cách duy nhất để api server biết response này đang trả lời request nào. Nếu là mutating webhook thay vì validating, response có thêm hai field đi kèm nhau: patchType và patch. patchType hiện chỉ nhận đúng một giá trị là JSONPatch, còn patch chứa mảng các thao tác JSON patch đã được mã hóa Base64.

Webhook Call: AdmissionReview Request/Responsekube-apiserverafter built-in admissioncontrollers pass the requestWebhook Serveryour own logic,inside or outside the clusterAdmissionReview requestAdmissionReview response: allowed / deniedSame uid links request and response — api server matches them and applies the verdict.

Viết webhook server: hai cuộc gọi validate và mutate​

Vậy thiết lập điều này thế nào? Trước tiên phải deploy admission webhook server của riêng mình, chứa logic riêng. Sau đó cấu hình webhook trên Kubernetes bằng cách tạo một webhook configuration object.

Bước đầu tiên là deploy webhook server, và nó dựng được trên bất kỳ nền tảng nào. Tài liệu Kubernetes có sẵn một ví dụ mã nguồn viết bằng Go, nhưng dùng ngôn ngữ khác cũng chẳng sao. Yêu cầu duy nhất: server phải nhận được các cuộc gọi Mutating và Validating, rồi trả về đúng object JSON mà api server mong đợi.

Hình dung một webhook server viết bằng Python với hai endpoint: validate và mutate. Endpoint validate nhận validation webhook request, so tên object với tên user đã gửi request, rồi từ chối nếu hai cái trùng nhau. Luật này chẳng để làm gì thực tế, nó chỉ minh họa rằng mọi thông tin trong request đều đọc được. Endpoint mutate lấy username từ request rồi trả về một thao tác JSONPatch gắn username đó vào làm label cho object.

Cấu trúc của một patch object khá đơn giản: nó là danh sách các thao tác, mỗi thao tác thuộc một trong các loại add, remove, replace, move, copy hoặc test. Mỗi thao tác khai đường dẫn trong object JSON cần nhắm tới, ở ví dụ này là /metadata/labels/user, cùng giá trị cần ghi vào nếu là thao tác add. Giá trị đó lấy từ username trong request. Cả danh sách sau đó được mã hóa Base64 rồi gửi đi trong response.

Một lưu ý bên lề cho người ôn thi: kỳ thi CKA không yêu cầu viết code kiểu này, nên không cần hiểu từng dòng. Phần cần nhớ gọn hơn nhiều: admission webhook server là một server do mình tự deploy, chứa logic cho phép hay từ chối request, và nó phải nhận rồi trả về đúng định dạng response mà webhook mong đợi.

Viết xong webhook server thì phải host nó ở đâu đó: chạy như một server độc lập bên ngoài, hoặc container hóa rồi deploy vào chính cluster dưới dạng Deployment. Chọn lối Deployment thì nó cần thêm một Service để api server gọi tới được, chẳng hạn Service tên webhook-service.

Đăng ký webhook bằng ValidatingWebhookConfiguration​

Bước tiếp theo là cấu hình cluster để gọi đến service và validate hoặc mutate request. Để làm điều này, tạo một object ValidatingWebhookConfiguration. Bắt đầu với apiVersion, kind, metadata, và section webhooks:

apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
name: pod-policy-webhook
webhooks:
- name: pod-policy.example.com
clientConfig:
service:
namespace: webhook-namespace
name: webhook-service
caBundle: "<ca-bundle>"
rules:
- apiGroups: [""]
apiVersions: ["v1"]
operations: ["CREATE"]
resources: ["pods"]
admissionReviewVersions: ["v1"]
sideEffects: None
timeoutSeconds: 5

apiVersion ở đây là admissionregistration.k8s.io/v1, còn kind là ValidatingWebhookConfiguration. Cấu hình một mutating webhook thì đổi kind thành MutatingWebhookConfiguration, mọi thứ còn lại giữ nguyên.

Dưới webhooks là danh sách các webhook, mỗi cái gồm một name, một clientConfig và một tập rules. Tên ở ví dụ này là pod-policy.example.com. clientConfig khai chỗ tìm admission webhook server, và có hai lối khai tùy theo server đặt ở đâu: server nằm ngoài cluster thì đưa thẳng một URL, còn server chạy thành Service trong cluster thì khai namespace kèm tên Service, ở đây là webhook-service.

Giao tiếp giữa api server và webhook server bắt buộc đi qua TLS, nên phần certificate không thể bỏ. Webhook server được cấu hình với một cặp certificate và key, sau đó CA bundle tương ứng được truyền vào clientConfig.caBundle để api server tin được cert của nó.

Phần còn lại là khai khi nào webhook được gọi. Gọi webhook cho mọi request là điều không ai muốn, vì mỗi lần gọi đều cộng thêm độ trễ; thường chỉ cần nó can thiệp vào đúng vài thao tác, chẳng hạn lúc tạo Pod, lúc xóa Pod, hay lúc tạo Deployment. Phạm vi đó khai dưới section rules qua bốn field apiGroups, apiVersions, operations và resources. Ở ví dụ này, webhook chỉ được gọi khi có lệnh tạo Pod.

Ba field cuối trong file YAML trên hay bị bỏ sót nhưng không thể thiếu ở admissionregistration.k8s.io/v1:

  • admissionReviewVersions là field bắt buộc, khai báo những version của object AdmissionReview mà webhook server hiểu được. Api server sẽ gửi version đầu tiên trong danh sách mà bản thân nó cũng hỗ trợ.
  • sideEffects cũng bắt buộc, nhận một trong hai giá trị: None nghĩa là gọi webhook không gây thay đổi gì ra ngoài object đang xét, NoneOnDryRun nghĩa là webhook có thay đổi ra ngoài nhưng biết tự bỏ qua chúng khi nhận request dryRun: true. Request dryRun là request chạy thử: nó đi qua trọn bộ authentication, authorization và admission như thật, nhưng api server không ghi kết quả vào etcd, nên dùng để xem một lệnh sẽ được nhận hay bị từ chối mà không thay đổi gì trên cluster (kubectl apply --dry-run=server). Khai sai field này thì các request dry-run sẽ không được gửi tới webhook và api server trả về lỗi.
  • timeoutSeconds quy định api server chờ webhook trả lời bao lâu, giá trị nằm trong khoảng 1 đến 30 giây và mặc định là 10 giây. Vì mọi lệnh tạo Pod giờ phải đi qua webhook, mỗi giây chờ ở đây cộng thẳng vào độ trễ của lệnh kubectl create.

Còn một field nữa không có trong ví dụ nhưng cần biết trước khi đưa webhook lên cluster thật: failurePolicy, quy định api server xử sự thế nào khi gọi webhook thất bại (lỗi mạng, timeout, webhook trả về non-2xx). Giá trị mặc định là Fail, tức fail-closed: webhook server chết thì mọi lệnh tạo Pod trong phạm vi rules đều bị từ chối theo. Đặt Ignore thì lỗi được bỏ qua và request đi tiếp. Cần phân biệt hai chuyện: webhook không gọi được thì failurePolicy quyết định, còn webhook gọi được và chủ động trả allowed: false thì request luôn bị từ chối, không phụ thuộc failurePolicy.

Một khi object này được tạo, mỗi khi tạo một Pod, một cuộc gọi sẽ được thực hiện đến webhook service, và tùy vào response, nó sẽ được cho phép hoặc từ chối.

Demo: triển khai một webhook chặn container chạy root​

Kịch bản: đã có sẵn một webhook server viết trước và container hóa, thực hiện đúng một rule là từ chối tạo Pod nào chạy container dưới quyền root mà không khai báo rõ ràng security context.

Chuẩn bị hạ tầng cho webhook: namespace, TLS secret, Deployment, Service.

kubectl create ns webhook-demo

kubectl create -n webhook-demo secret tls webhook-server-tls \
--cert=/root/certs/tls.crt \
--key=/root/certs/tls.key

kubectl apply -f webhook-deployment.yaml
kubectl apply -f webhook-service.yaml

# Output:
namespace/webhook-demo created
secret/webhook-server-tls created
deployment.apps/webhook-server created
service/webhook-service created

Webhook giao tiếp với api server qua TLS, nên secret chứa cert và key hợp lệ phải có trước khi đăng ký webhook. Thiếu bước này thì api server không gọi nổi webhook, và với failurePolicy mặc định là Fail, hậu quả là mọi lệnh tạo Pod trong phạm vi rule đều bị chặn.

Đăng ký webhook với cluster bằng MutatingWebhookConfiguration (đã soạn sẵn, chỉ áp dụng):

kubectl apply -f webhook-configuration.yaml

# Output:
mutatingwebhookconfiguration.admissionregistration.k8s.io/demo-webhook created

File này khai rules: operations: ["CREATE"], resources: ["pods"], nghĩa là từ giờ mọi lệnh tạo Pod đều được chuyển cho webhook server xem xét trước khi Pod thực sự ra đời.

Kiểm chứng hành vi qua 3 kịch bản.

Pod không khai securityContext, nên webhook tự gán mặc định an toàn cho nó (runAsNonRoot: true, runAsUser: 1234):

kubectl apply -f pod-with-defaults.yaml
kubectl get pod pod-with-defaults -o yaml | grep -A2 securityContext

# Output:
securityContext:
runAsNonRoot: true
runAsUser: 1234

Pod khai rõ ràng rằng nó muốn chạy root (runAsNonRoot: false), và webhook cho qua, vì đây là lựa chọn tường minh chứ không phải hệ quả của việc quên khai:

kubectl apply -f pod-with-override.yaml

# Output:
pod/pod-with-override created

Pod khai mâu thuẫn với chính nó: runAsNonRoot: true nhưng runAsUser: 0, mà 0 đúng là UID của root. Webhook từ chối ngay:

kubectl apply -f pod-with-conflict.yaml

# Output:
Error from server: error when creating "pod-with-conflict.yaml":
admission webhook "pod-policy.example.com" denied the request:
runAsNonRoot specified, but runAsUser set to 0 (the root user)

Ba kịch bản trên cho thấy đúng hai loại admission controller đang phối hợp. Mutating chạy trước, lấp chỗ trống bằng default an toàn khi Pod không khai gì. Validating chạy sau, chặn những cấu hình tự mâu thuẫn. Khớp đúng thứ tự đã nói ở phần trên: mutate trước, validate sau.

One Webhook, Three Pod Specs, Three OutcomesPod Ano securityContextPod BrunAsNonRoot: falsePod CrunAsNonRoot: truerunAsUser: 0Mutating steppatches safe defaults inValidating stepexplicit opt-in is allowedValidating stepspec contradicts itselfCreatedrunAsUser: 1234 addedCreatedruns as root on purposeDeniedadmission webhook errorMutating admission runs first, so validating admission always sees the patched object.A missing field gets a safe default; a self-contradicting field is rejected outright.

5. ValidatingAdmissionPolicy: thay webhook bằng CEL​

Webhook cho toàn quyền viết logic, nhưng cái giá đi kèm khá nặng: viết một server, container hóa nó, phát hành rồi gia hạn cert TLS, deploy Deployment cùng Service, và chấp nhận rằng server đó chết là cả cluster không tạo được Pod nữa (failurePolicy: Fail). Với một rule gọn như "Deployment không được quá 5 replica", chuỗi công việc đó quá nặng tay.

Kubernetes có lối đi ngắn hơn cho đúng nhóm nhu cầu này: ValidatingAdmissionPolicy, đạt trạng thái stable (GA) từ Kubernetes 1.30. Thay vì gọi ra một server bên ngoài, luật validation được viết bằng CEL (Common Expression Language) và chạy in-process ngay trong api server. Không server, không cert, không network hop, và cũng không có failure mode của một service riêng.

💡 Hình dung: Giống việc chuyển từ thuê chuyên gia thẩm định bên ngoài sang dán thẳng quy định lên tường phòng tiếp nhận hồ sơ. Chuyên gia bên ngoài (webhook) xét được cả những trường hợp rắc rối nhất, nhưng mỗi hồ sơ đều phải gọi điện và chờ máy, còn hôm nào chuyên gia nghỉ là cả quầy đứng bánh. Quy định dán trên tường (CEL policy) chỉ diễn đạt được những điều kiện gọn nhẹ, bù lại nhân viên đọc là biết ngay, không phải gọi ai và không bao giờ mất liên lạc.

Two Ways to Enforce the Same RuleValidating Admission Webhookkube-apiserverwebhook serverneeds TLS cert and CA bundleneeds a Deployment and Serviceadds a network hop per requestfailurePolicy defaults to Fail, soa dead server blocks every PodValidatingAdmissionPolicy (CEL)kube-apiserverCEL rule runs in-processobject.spec.replicas <= 5no server and no certificateno network hop, no extra latencystable (GA) since Kubernetes v1.30Condition readable from the object itself: write a policy. Need to call out to another system: write a webhook.MutatingAdmissionPolicy covers the mutating side the same way, also with CEL.

Một policy hoàn chỉnh gồm hai object bắt buộc, tách riêng phần luật và phần phạm vi áp dụng.

Object thứ nhất, ValidatingAdmissionPolicy, mô tả luật ở dạng trừu tượng: áp lên resource nào, ở operation nào, và biểu thức CEL nào phải đúng.

apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
name: "demo-policy.example.com"
spec:
failurePolicy: Fail
matchConstraints:
resourceRules:
- apiGroups: ["apps"]
apiVersions: ["v1"]
operations: ["CREATE", "UPDATE"]
resources: ["deployments"]
validations:
- expression: "object.spec.replicas <= 5"

Trong expression, object là chính object mà request đang tạo hoặc sửa. Biểu thức trả về false thì request bị xử lý theo failurePolicy.

Object thứ hai, ValidatingAdmissionPolicyBinding, trả lời câu hỏi "luật đó áp ở đâu":

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

Binding này giới hạn luật chỉ chạy trong những namespace có label environment: test, còn validationActions: [Deny] nói rõ vi phạm thì từ chối request. Việc tách làm hai object cho phép viết luật một lần rồi bind nó vào nhiều phạm vi khác nhau, chẳng hạn chỉ cảnh báo ở namespace dev nhưng từ chối thẳng ở namespace production. Có policy mà thiếu binding thì policy hoàn toàn không có tác dụng.

Tạo một Deployment 10 replica trong namespace bị bind, api server trả về đúng biểu thức đã vi phạm:

kubectl apply -f deployment-10-replicas.yaml

# Output minh họa
error: ValidatingAdmissionPolicy 'demo-policy.example.com' with binding
'demo-binding-test.example.com' denied request: failed expression:
object.spec.replicas <= 5

(Output minh họa — cấu trúc thông báo đúng theo tài liệu Kubernetes, tên policy và binding chỉ mang tính ví dụ.)

Thông báo lỗi nêu thẳng biểu thức bị vi phạm, nên không phải mò vào log của một webhook server nào cả.

Phía mutate cũng có đối trọng tương ứng là MutatingAdmissionPolicy, cũng viết bằng CEL và cũng được serve ở admissionregistration.k8s.io/v1. Nó nằm trong danh sách admission plugin bật mặc định của các bản Kubernetes gần đây.

Vậy khi vận hành thì chọn cái nào? Ranh giới khá rõ. Luật chỉ cần đọc từ chính object trong request thì viết ValidatingAdmissionPolicy, vì nó rẻ hơn webhook ở mọi mặt: không hạ tầng, không cert, không thêm độ trễ mạng, không thêm một thứ có thể chết. Chỉ khi cần gọi ra ngoài api server để tra một hệ thống khác, cần giữ state riêng, hoặc cần logic vượt quá sức diễn đạt của một biểu thức CEL, thì mới đáng dựng webhook server. Riêng với kỳ thi CKA, phần cần nắm chắc vẫn là cơ chế admission controller và thứ tự mutate trước validate sau.


Nguồn tham khảo​

Nguồn gốc: Khóa "Certified Kubernetes Administrator (CKA)" — module "3. Scheduling", nền tảng KodeKloud.

Ghi chú cập nhật/đính chính:

  • Registry k8s.gcr.io đã ngừng cập nhật từ 4/2023, khuyến nghị dùng registry.k8s.io — 2026-09, Kubernetes blog: image registry redirect
  • Multi-scheduler profile trong một binary duy nhất được giới thiệu từ Kubernetes 1.18 (KEP 1451) — 2026-09, KEP 1451
  • apiVersion: kubescheduler.config.k8s.io/v1 là bản GA/stable hiện tại; v1beta3 đã bị xóa hẳn từ Kubernetes 1.29 — 2026-09, Kubernetes Scheduler Configuration docs
  • NamespaceLifecycle là plugin bật mặc định, từ chối request đến namespace không tồn tại, chặn tạo object trong namespace đang Terminating và chặn xóa default/kube-system/kube-public; NamespaceExists và NamespaceAutoProvision là plugin hợp lệ nhưng không bật mặc định, và không được tài liệu đánh dấu deprecated (chỉ AlwaysAdmit và AlwaysDeny bị deprecated từ v1.13) — 2026-09, Kubernetes Admission Controllers Reference
  • admissionReviewVersions và sideEffects là field bắt buộc của webhook configuration ở admissionregistration.k8s.io/v1; sideEffects nhận None hoặc NoneOnDryRun; timeoutSeconds nằm trong khoảng 1–30 giây, mặc định 10 giây; failurePolicy nhận Ignore hoặc Fail với mặc định là Fail — 2026-09, Kubernetes: Dynamic Admission Control
  • Mutating webhook response dùng patchType: JSONPatch (giá trị duy nhất được hỗ trợ) kèm patch là mảng JSON patch mã hóa Base64 — 2026-09, Kubernetes: Dynamic Admission Control
  • Mọi scheduler profile phải dùng cùng một plugin queueSort với cùng tham số vì scheduler chỉ có một hàng đợi pod đang chờ; queueSort chỉ bật được đúng một plugin; kube-apiserver tự set schedulerName: default-scheduler cho Pod không khai field này; field multiPoint bật/tắt plugin trên mọi extension point của nó — 2026-09, Kubernetes Scheduler Configuration
  • NodeResourcesFit chấm điểm theo một trong ba strategy: LeastAllocated (mặc định), MostAllocated, RequestedToCapacityRatio — 2026-09, Kubernetes Scheduler Configuration
  • Field leaderElection.resourceLock/resourceName/resourceNamespace xác định lock object của leader election trong KubeSchedulerConfiguration — 2026-09, kube-scheduler Configuration (v1) API
  • ValidatingAdmissionPolicy đạt stable (GA) từ Kubernetes v1.30, dùng CEL và chạy in-process trong api server; cần cả ValidatingAdmissionPolicy lẫn ValidatingAdmissionPolicyBinding để policy có tác dụng — 2026-09, Kubernetes: Validating Admission Policy
  • MutatingAdmissionPolicy được serve ở admissionregistration.k8s.io/v1 và nằm trong danh sách admission plugin bật mặc định của bản Kubernetes hiện tại — 2026-09, Kubernetes: Mutating Admission Policy