3.3. Scheduling - Multiple Schedulers and Admission Controllers
Mục lục
- 1. Multiple Schedulers
- 2. Scheduler Profiles
- 3. Admission Controllers
- 4. Validating và Mutating Admission Webhooks
1. Multiple Schedulers
Phần này nói về việc deploy nhiều scheduler trong một Kubernetes cluster. Đã thấy scheduler mặc định hoạt động thế nào trong Kubernetes ở các phần trước — nó có một thuật toán phân bổ pod đều lên các node, đồng thời xem xét các điều kiện đã chỉ định qua taints và tolerations, node affinity, v.v. Nhưng nếu không điều gì trong số đó đáp ứng nhu cầu? Giả sử có một application cụ thể yêu cầu các thành phần của nó được đặt lên node sau khi thực hiện thêm một số kiểm tra bổ sung — khi đó có thể quyết định có một thuật toán lập lịch riêng để đặt pod lên node, để có thể thêm các điều kiện và kiểm tra tùy chỉnh của riêng mình vào đó.
Kubernetes có tính mở rộng cao. Có thể viết chương trình Kubernetes scheduler của riêng mình, đóng gói, và deploy nó làm scheduler mặc định hoặc làm một scheduler bổ sung trong Kubernetes cluster. Theo cách đó, tất cả các application khác vẫn dùng scheduler mặc định. Tuy nhiên, một số application cụ thể có thể chọn dùng scheduler tùy chỉnh của riêng mình. Vậy Kubernetes cluster có thể có nhiều scheduler cùng lúc. Khi tạo một Pod hoặc Deployment, có thể chỉ định cho Kubernetes biết Pod đó nên được lập lịch bởi scheduler nào cụ thể.
Khi có nhiều scheduler, chúng phải có tên khác nhau để có thể nhận diện là các scheduler riêng biệt. Scheduler mặc định có tên default-scheduler, và tên này được cấu hình trong kube-scheduler configuration file. Scheduler mặc định thực ra không cần file này vì nếu không chỉ định tên, nó tự set tên thành default-scheduler. Nhưng đây là cách file đó sẽ trông như thế nào nếu tự tạo một file:
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ự.
Bắt đầu với cách đơn giản nhất để deploy một scheduler bổ sung. Ở phần trước đã thấy cách deploy Kubernetes kube-scheduler — tải binary kube-scheduler và chạy nó như một service với một tập hợp option. Để deploy một scheduler bổ sung, có thể dùng cùng binary kube-scheduler, hoặc dùng một binary tự build riêng — đó là điều cần làm nếu cần scheduler hoạt động khác đi. Trong trường hợp này, dùng cùng binary để deploy scheduler bổ sung, và lần này trỏ configuration đến file config tùy chỉnh vừa tạo. Vậy mỗi scheduler dùng một configuration file riêng, mỗi file có tên scheduler riêng. Cũng lưu ý còn có các option khác cần truyền vào, như file kubeconfig để xác thực với Kubernetes API, nhưng bỏ qua chi tiết đó để giữ mọi thứ đơn giản.
Đây không phải cách deploy custom scheduler được dùng 99% thời gian hiện nay, vì với thiết lập bằng kubeadm, tất cả các control plane component đều chạy dưới dạng Pod hoặc Deployment trong Kubernetes cluster. Vậy xem cách deploy scheduler như một Pod. Tạo một pod definition file và chỉ định thuộc tính kubeconfig, là đường dẫn tới file scheduler.conf chứa thông tin xác thực để kết nối tới Kubernetes API server. Sau đó truyền custom kube-scheduler configuration file vào làm option config cho scheduler. Lưu ý scheduler name được chỉ định trong file, đó là cách tên được kube-scheduler nhận diện.
Một option quan trọng khác cần xem ở đây là leader-elect. Option này thuộc kube-scheduler configuration. Option leader-elect được dùng khi có nhiều bản sao của scheduler chạy trên các master node khác nhau, như trong thiết lập high availability (HA) với nhiều master node và process kube-scheduler chạy trên cả hai. Nếu có nhiều bản sao của cùng một scheduler chạy trên các node khác nhau, chỉ có thể có một bản sao hoạt động (active) tại một thời điểm. Đó là lúc option leader-elect giúp chọn ra một leader chịu trách nhiệm cho các hoạt động lập lịch. Sẽ bàn thêm về thiết lập HA ở phần khác. Trong trường hợp có nhiều master, chỉ cần nhớ có thể truyền thêm một tham số để set tên lock object, nhằm phân biệt custom scheduler mới với leader election process mặc định.
Tiếp theo, xem cách deploy scheduler bổ sung dưới dạng một Deployment, theo trang tài liệu Kubernetes về cấu hình nhiều scheduler. Trang này trước tiên chỉ cách nếu muốn tự build scheduler của mình — clone Kubernetes repo, chỉnh sửa kube-scheduler, build và đóng gói thành một Docker image. Sau đó là config file để tạo scheduler dưới dạng một Deployment — về cơ bản tương tự Pod đã xem, chỉ khác là nó là một Deployment. Ở đây có image, chính là custom kube-scheduler image, và config file như đã bàn, chứa custom my-scheduler config file, còn binary là kube-scheduler binary.
Để điều này hoạt động, có một số điều kiện tiên quyết bổ sung — như service account và ClusterRoleBinding, dùng cho mục đích xác thực (đã bàn về ClusterRoleBindings, ClusterRoles, và ServiceAccounts ở phần Security). Điều còn lại là cách file config được truyền vào Deployment — có thể tạo file này cục bộ rồi truyền vào như một volume mount như thường làm với các Pod khác, hoặc dùng cách tiếp cận khác là tạo một ConfigMap. Đây chính là kube-scheduler configuration đã bàn, với option leaderElect set là false (vì chỉ có một replica), và có schedulerName ở đây. Config file này sau đó được truyền vào như một volume, và nội dung của ConfigMap đó được map vào một volume tại vị trí cụ thể — từ đó có file YAML với đúng nội dung này.
Khi chạy lệnh get pods trong namespace kube-system, sẽ thấy custom scheduler mới đang chạy (nếu chạy dưới dạng Pod). Nếu chạy dưới dạng Deployment, sẽ thấy naming convention hơi khác một chút, nhưng vẫn thấy được Pod đó — chỉ cần kiểm tra đúng namespace.
Sau khi đã deploy custom scheduler, bước tiếp theo là cấu hình một Pod hoặc Deployment để dùng scheduler mới này. Vậy dùng custom scheduler như thế nào? Trong pod definition file, thêm một trường mới gọi là schedulerName và chỉ định tên của scheduler mới — về cơ bản là vậy. Theo cách này, khi Pod được tạo, đúng scheduler sẽ được chọn để lập lịch. Tạo Pod bằng lệnh kubectl create. Nếu scheduler chưa được cấu hình đúng, Pod sẽ tiếp tục ở trạng thái Pending. Nếu mọi thứ ổn, Pod sẽ ở trạng thái Running. Nếu Pod ở trạng thái Pending, có thể xem log qua lệnh kubectl describe pod, và thường sẽ thấy scheduler chưa được cấu hình đúng.
Vậy làm sao biết scheduler nào đã đảm nhận việc lập lịch một Pod cụ thể, khi có nhiều scheduler? Có thể xem trong các event bằng lệnh kubectl get events với option -o wide — lệnh này liệt kê tất cả event trong namespace hiện tại, tìm event Scheduled. Sẽ thấy source của event chính là custom scheduler vừa tạo — đó là tên đã đặt cho custom scheduler. Thông báo sẽ ghi rằng đã gán thành công. Đó là dấu hiệu cho biết mọi thứ đang hoạt động. Cũng có thể xem log của scheduler nếu gặp vấn đề — dùng lệnh kubectl logs, cung cấp tên Pod hoặc Deployment của scheduler, cùng đúng namespace.
Lab: Thực hành Multiple Schedulers
Tên của pod triển khai scheduler Kubernetes mặc định trong môi trường này là gì? Chạy kubectl get pods -n kube-system — Pod là kube-scheduler-control-plane.
Image nào được dùng để deploy kube-scheduler? kubectl describe pod kube-scheduler -n kube-system — image là k8s.gcr.io/kube-scheduler:v1.23 (tại thời điểm ghi hình bài giảng).
Service account và ClusterRoleBinding mà custom scheduler sẽ dùng đã được tạo sẵn — có thể kiểm tra bằng kubectl get serviceaccount my-scheduler -n kube-system, thấy service account my-scheduler trong namespace kube-system. Tương tự cũng có ClusterRoleBindings đã tạo sẵn.
Tạo một ConfigMap mà scheduler mới sẽ dùng, thông qua khái niệm ConfigMap như một volume. Tạo ConfigMap tên my-scheduler-config từ nội dung của một file đã cho sẵn — file này là configuration sẽ được truyền vào scheduler mới sắp tạo:
kubectl create configmap my-scheduler-config --from-file=<đường-dẫn-file> -n kube-system
Xác nhận đã tạo.
Deploy một scheduler bổ sung vào cluster theo specification đã cho, dùng manifest file tại /root/my-scheduler.yaml. File này đã có sẵn hầu hết cấu hình — liveness probe, readiness probe đều đã được set, không cần lo về phần đó. Chỉ cần thêm image — dùng đúng image mà scheduler hiện tại đang dùng. Xác định image đó bằng kubectl describe pod kube-scheduler-control-plane -n kube-system để lấy image ID, rồi đưa vào file thay cho placeholder.
Nếu xem phần còn lại của file, đã có sẵn volume — chính là ConfigMap vừa tạo — được map vào làm volume mount. (Phần ConfigMap sẽ được bàn kỹ hơn ở các phần sau nếu chưa học qua.) Tạo scheduler:
kubectl create -f /root/my-scheduler.yaml
Xác nhận đã tạo — thấy my-scheduler trong namespace kube-system.
Bước cuối cùng là tạo một Pod. Đã có sẵn một pod definition file — dùng nó để tạo pod với custom scheduler mới. Đây là file nginx definition — một Pod nginx đơn giản với image nginx. Cần chỉ định custom scheduler, chính là my-scheduler. Thêm trường mới gọi là schedulerName:
spec:
schedulerName: my-scheduler
Lưu lại, tạo Pod, đảm bảo pod đang chạy.
2. Scheduler Profiles
Phần này nói về scheduler profiles. 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.
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.
Điều đầu tiên xảy ra khi các pod này được tạo là các pod kết thúc trong một scheduling queue — nơi các pod chờ được lập lịch. Ở giai đoạn này, pod được sắp xếp (sorted) dựa trên priority định nghĩa trên các Pod. Trong trường hợp này, Pod của mình có priority cao được set. Để set priority, trước tiên phải tạo một PriorityClass:
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: high-priority
value: 1000000
Đặt tên và set giá trị priority — ở đây set là 1 triệu, tức priority rất cao. Đây là cách các Pod với priority cao hơn được đưa lên đầu hàng đợi để lập lịch trước — việc sắp xếp đó diễn ra trong scheduling queue.
Sau đó Pod bước vào filter phase. Đây là giai đoạn lọc ra các node không thể chạy được Pod. Trong trường hợp này, hai node đầu tiên không có đủ tài nguyên — không có đủ 10 CPU còn lại — nên chúng bị lọc ra.
Giai đoạn tiếp theo là scoring phase. Đây là nơi các node được chấm điểm (score) với các trọng số khác nhau. Từ hai node còn lại, scheduler gán một điểm số cho mỗi node dựa trên khoảng trống còn lại sau khi dành riêng CPU cần cho Pod đó. Trong trường hợp này, node đầu tiên sẽ còn lại 2, và node thứ hai sẽ còn lại 6. Vậy node thứ hai nhận điểm cao hơn, và đó là node được chọn.
Cuối cùng, ở binding phase, Pod được gắn (bound) vào node có điểm số cao nhất.
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, chính plugin NodeResourcesFit xác định các node có đủ tài nguyên cần thiết cho pod và lọc ra các node không đủ. Một số ví dụ plugin khác ở giai đoạn này: plugin NodeName kiểm tra xem Pod có tên node được ghi trong pod spec hay không, và lọc ra tất cả các node không khớp tên đó. Một ví dụ khác là plugin NodeUnschedulable, lọc ra các node có cờ unschedulable set là true — đây là khi chạy lệnh cordon trên một node (sẽ bàn ở phần sau) — plugin này đảm bảo không có pod nào được lập lịch lên các node có cờ unschedulable set là true.
Trong giai đoạn scoring, plugin NodeResourcesFit cũng xuất hiện lại — nó gán điểm cho mỗi node dựa trên tài nguyên khả dụng sau khi Pod được phân bổ vào đó. Vậy có thể thấy một plugin duy nhất có thể tham gia vào nhiều giai đoạn khác nhau. Một ví dụ khác của plugin trong giai đoạn này là ImageLocality, gán điểm cao hơn cho các node đã có sẵn container image mà Pod sử dụng. Lưu ý ở giai đoạn này, các plugin không thực sự từ chối (reject) việc đặt Pod lên một node cụ thể — ví dụ với điểm số ImageLocality, nó chỉ đảm bảo Pod được đặt lên node đã có sẵn image, nhưng nếu không có node nào đáp ứng, Pod vẫn sẽ được đặt lên một node dù node đó không có sẵn image. Vậy đây chỉ là việc chấm điểm ở giai đoạn này.
Cuối cùng, ở binding phase, có plugin DefaultBinder cung cấp cơ chế binding.
Tính chất mở rộng cao của Kubernetes cho phép tùy biến những plugin nào chạy ở đâu, và cho phép tự viết plugin riêng rồi gắn vào đây — điều này đạt được nhờ các extension point. Ở mỗi giai đoạn, có một extension point để một plugin có thể được gắn vào. Trong scheduling queue, có extension queue sort, nơi plugin PrioritySort được gắn vào. Rồi có extension filter, score, và bind, nơi mỗi plugin vừa nói ở trên được gắn vào tương ứng.
Thực tế còn nhiều hơn thế — có các extension point trước giai đoạn filter gọi là PreFilter và sau giai đoạn filter gọi là PostFilter. Rồi có pre-score trước extension point score, và reserve sau extension point score. Rồi có permit và pre-bind trước bind, và post-bind sau giai đoạn binding. Vậy có rất nhiều tùy chọn khả dụng — về cơ bản có thể chạy code tùy chỉnh của riêng mình ở bất kỳ đâu trong các extension point này, chỉ cần tạo một plugin và gắn nó vào đúng extension point tương ứng muốn gắn.
Có thêm chi tiết về một số plugin bổ sung mặc định có sẵn, được gắn với các extension point khác nhau. Một số plugin trải rộng qua nhiều extension point, một số chỉ nằm trong một extension point cụ thể.
Vậy đó là scheduling plugin và extension point — tính mở rộng cao của Kubernetes cho phép tùy biến cách các plugin này được gọi, và tự viết scheduling plugin riêng nếu cần. Học được điều đó, xem cách thay đổi hành vi mặc định của việc gọi các plugin này, và cách gắn plugin riêng vào nếu thực sự cần.
Nhìn lại một chút, trước đây đã bàn về việc deploy ba scheduler riêng biệt, mỗi cái với một scheduler binary riêng — scheduler mặc định, my-scheduler, và my-scheduler-2. Đây là ba scheduler binary riêng biệt, mỗi cái chạy với một scheduler config file riêng. Đó là một cách để deploy nhiều scheduler. Vấn đề ở đây là vì đây là các process riêng biệt, cần thêm công sức để duy trì các process riêng biệt này. Và quan trọng hơn, vì chúng là các process riêng biệt, chúng có thể gặp race condition khi ra quyết định lập lịch — ví dụ, một scheduler có thể lập lịch một workload lên một node mà không biết rằng có một scheduler khác đang lập lịch một workload lên cùng node đó tại cùng thời điểm.
Với bản phát hành 1.18 của Kubernetes, một tính năng hỗ trợ nhiều profile trong một scheduler duy nhất đã được giới thiệu. Giờ đây có thể cấu hình nhiều profile trong một scheduler duy nhất, trong scheduler configuration file, bằng cách thêm nhiều entry vào danh sách profiles, và với mỗi profile, chỉ định một tên scheduler riêng:
apiVersion: kubescheduler.config.k8s.io/v1
kind: KubeSchedulerConfiguration
profiles:
- schedulerName: my-scheduler-2
- schedulerName: my-scheduler-3
Điều này tạo ra một profile riêng cho mỗi scheduler, mỗi profile hoạt động như một scheduler riêng biệt, ngoại trừ việc giờ đây nhiều scheduler chạy trong cùng một binary, thay vì tạo binary riêng cho mỗi scheduler.
Vậy làm sao cấu hình các scheduler profile này để hoạt động khác nhau? Vì hiện tại tất cả chỉ đơn giản có tên khác nhau, chúng sẽ hoạt động giống hệt scheduler mặc định. Làm sao cấu hình chúng để hoạt động khác đi? Dưới mỗi scheduler profile, có thể cấu hình các plugin theo ý muốn. Ví dụ, với profile MyScheduler2, có thể disable các plugin nhất định như plugin TaintToleration, và enable các plugin tùy chỉnh của riêng mình. Với profile MyScheduler3, có thể disable tất cả các plugin PreScore và Score. Cấu trúc sẽ trông 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.
Đó là tổng quan về cách scheduler và scheduler profile hoạt động, và cách cấu hình nhiều scheduler profile trong Kubernetes. Để đọc thêm, xem Kubernetes Enhancement Proposal đã giới thiệu tính năng multi-scheduling profile — đó là KEP 1451, và bài viết về scheduling framework.
3. Admission Controllers
Phần này nói về admission controllers. Từ trước đến giờ đã chạy các lệnh từ command line bằng tiện ích kubectl để thực hiện nhiều thao tác khác nhau trên Kubernetes cluster. Mỗi khi gửi một request, ví dụ để tạo một Pod, request đó đi đến api server, rồi Pod được tạo, và thông tin cuối cùng được lưu (persisted) vào etcd database.
Khi request đến api server, đã học rằng nó đi qua một quá trình authentication, thường được thực hiện qua certificate. Nếu request được gửi qua kubectl, kubeconfig file có certificate đã được cấu hình, và quá trình authentication chịu trách nhiệm xác định người dùng đã gửi request và đảm bảo người dùng đó hợp lệ. Sau đó request đi qua quá trình authorization — đây là lúc kiểm tra xem user có quyền thực hiện thao tác đó không, và điều này đạt được thông qua Role-Based Access Control (RBAC).
Ví dụ, nếu user được gán role developer, user được phép list, get, create, update, hoặc delete Pods. Nếu request gửi đến khớp với bất kỳ điều kiện nào trong số này — trong trường hợp này là request tạo một pod — nó được cho phép đi qua; ngược lại, nó bị từ chối. Đó là authorization với role-based access control.
Với RBAC, có thể đặt các giới hạn khác nhau, như cho phép hoặc từ chối những người có một role cụ thể được, ví dụ, tạo, list, hoặc xóa các loại object khác nhau như pods, deployments, hoặc services. Thậm chí có thể giới hạn quyền truy cập với các tên resource cụ thể, ví dụ một developer chỉ được thao tác trên các pod tên blue hoặc orange, hoặc giới hạn quyền truy cập chỉ trong một namespace. Có thể thấy, hầu hết các rule tạo được bằng RBAC đều ở mức Kubernetes API — user nào được phép truy cập những loại thao tác API nào, và không đi xa hơn thế.
Nhưng nếu muốn làm nhiều hơn là chỉ định loại quyền truy cập một user có với một object thì sao? Ví dụ, khi có request tạo một Pod, muốn xem xét lại configuration file, xem tên image, và nói rằng không cho phép image từ một public Docker Hub registry, chỉ cho phép image từ một registry nội bộ cụ thể. Hoặc để bắt buộc không bao giờ dùng tag latest cho bất kỳ image nào. Hoặc, ví dụ, muốn nói nếu container đang chạy dưới quyền root user, thì không muốn cho phép request đó. Hoặc chỉ cho phép một số capability nhất định. Hoặc bắt buộc metadata luôn phải chứa labels.
Đây là những điều không thể đạt được chỉ với RBAC hiện có — và đó là lúc Admission Controllers phát huy tác dụng. Admission Controllers giúp triển khai các biện pháp bảo mật tốt hơn để bắt buộc cách một cluster được sử dụng. Ngoài việc đơn giản chỉ validate configuration, admission controller có thể làm nhiều hơn thế, như thay đổi chính request đó, hoặc thực hiện các thao tác bổ sung trước khi Pod được tạo.
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: giả sử muốn tạo một Pod trong một namespace tên blue không tồn tại. Chạy lệnh này sẽ ném ra lỗi: the namespace blue is not found. Điều đang xảy ra ở đây là request được authenticate, rồi authorize, sau đó nó đi qua các admission controller. Admission controller NamespaceExists xử lý request và kiểm tra xem namespace blue có khả dụng không. Nếu không, request bị từ chối.
NamespaceExists là một admission controller có sẵn (built-in), được enable mặc định. Có một admission controller khác không được enable mặc định, gọi là NamespaceAutoProvision. Admission controller này sẽ tự động tạo namespace nếu nó chưa tồn tại.
Để xem danh sách các admission controller được enable mặc định, chạy lệnh kube-apiserver -h và grep cho enable-admission-plugins:
kube-apiserver -h | grep enable-admission-plugins
Danh sách này liệt kê các admission controller được enable mặc định, hiển thị nổi bật (highlighted). Lưu ý nếu đang chạy trong thiết lập dùng kubeadm, phải chạy lệnh này bên trong pod kube-apiserver-control-plane bằng lệnh kubectl exec trước.
Để thêm một admission controller, cập nhật flag --enable-admission-plugins trên kube-apiserver service để thêm admission controller mới. Nếu đang ở thiết lập kubeadm, cập nhật flag trong manifest file của kube-apiserver (/etc/kubernetes/manifests/kube-apiserver.yaml). Cách một là nếu cập nhật kube-apiserver service, cách kia là nếu api server đang chạy dưới dạng Pod trong thiết lập kubeadm.
Tương tự, để disable admission controller plugin, dùng flag --disable-admission-plugins.
Sau khi cập nhật, lần tiếp theo chạy lệnh tạo một pod trong một namespace chưa tồn tại, request đi qua authentication, rồi authorization, rồi bộ điều khiển NamespaceAutoProvision, lúc đó nó nhận ra namespace chưa tồn tại — vậy nó tự động tạo namespace, và request đi qua thành công để tạo Pod. Nếu liệt kê các namespace lúc này, sẽ thấy namespace blue được tự động tạo.
Đó là một ví dụ về cách một admission controller hoạt động — nó không chỉ validate và từ chối request từ người dùng, mà còn có thể thực hiện các thao tác ở backend hoặc thay đổi chính request đó.
Lưu ý: NamespaceAutoProvision và NamespaceExists đã deprecated (không dùng nữa) và giờ được thay thế bởi admission controller NamespaceLifecycle. NamespaceLifecycle sẽ đảm bảo các request đến một namespace không tồn tại bị từ chối, và các namespace mặc định như default, kube-system, và kube-public không thể bị xóa.
Lab: Thực hành Admission Controllers
Cái nào dưới đây không phải là chức năng của admission controllers? Đáp án đúng là authenticate users — admission controller không xử lý bất kỳ phần nào của authentication, việc đó diễn ra trước khi tới admission controller.
Admission controller nào không được enable mặc định? Lấy thông tin này bằng cách kiểm tra option --enable-admission-plugins trong help của kube-apiserver:
kubectl get pods -n kube-system
kubectl exec -it kube-apiserver-control-plane -n kube-system -- kube-apiserver -h | grep enable-admission-plugins
Output là danh sách tất cả các plugin được enable mặc định. Có thể thấy NamespaceLifecycle được enable mặc định — không phải đáp án. Kiểm tra MutatingAdmissionWebhook — nếu xuất hiện trong danh sách, nghĩa là được enable mặc định — đúng vậy, có MutatingAdmissionWebhook, rồi có ValidatingAdmissionWebhook. Tuy nhiên, không thấy NamespaceAutoProvision trong danh sách này — vậy NamespaceAutoProvision không được enable mặc định.
Admission controller nào được enable trong cluster này, nhưng thường thì bị disable? Kiểm tra file config /etc/kubernetes/manifests/kube-apiserver.yaml, tìm dòng enable-admission-plugins. Thấy plugin nào được enable mà không phải mặc định — đó là NodeRestriction. Cũng có thể dùng lệnh:
grep enable-admission-plugins /etc/kubernetes/manifests/kube-apiserver.yaml
Tạo một nginx Pod trong namespace blue, lưu ý namespace blue chưa được tạo, và yêu cầu rõ ràng là không được tạo namespace trước. Chạy lệnh tạo pod — báo lỗi vì namespace blue không được tìm thấy. Lý do request cuối cùng thất bại và ném ra lỗi đó là vì Kubernetes có admission controller NamespaceExists được enable, từ chối các request đến namespace không tồn tại. Nếu muốn nó tự động tạo namespace, có thể enable admission controller NamespaceAutoProvision.
Enable admission controller NamespaceAutoProvision. Quay lại file /etc/kubernetes/manifests/kube-apiserver.yaml, tìm dòng --enable-admission-plugins — hiện chỉ có NodeRestriction. Thêm dấu phẩy, rồi thêm tên admission controller mới:
--enable-admission-plugins=NodeRestriction,NamespaceAutoProvision
Lưu lại. Sau khi lưu, kube-apiserver sẽ restart — cần chờ vài phút trước khi chạy validation vì mất một khoảng thời gian để restart.
Chạy lại lệnh tạo pod nginx trong namespace blue — lần này namespace blue vẫn chưa tồn tại trước đó, nhưng khi chạy lệnh, nó sẽ tự động tạo namespace blue. Kiểm tra kubectl get namespace — thấy namespace blue vừa được tạo cách đây vài giây, nhờ admission controller đó.
Câu hỏi tiếp theo chỉ ra rằng NamespaceExists và admission controller NamespaceAutoProvision đã deprecated và được thay thế bởi NamespaceLifecycle.
Disable admission controller DefaultStorageClass. Lại vào file /etc/kubernetes/manifests/kube-apiserver.yaml. Giống như đã có --enable-admission-plugins, thêm một dòng --disable-admission-plugins:
--disable-admission-plugins=DefaultStorageClass
Sau khi thay đổi, kube-apiserver lại phải restart — chờ vài phút rồi mới chạy validation.
Câu hỏi cuối cùng chỉ ra rằng kube-apiserver đang chạy dưới dạng một pod, nên có thể kiểm tra process để xem các plugin đã enable và disable — copy lệnh được cho để liệt kê tất cả các plugin đã enable và disable.
4. Validating và Mutating Admission Webhooks
Phần này xem xét kỹ hơn các loại admission controller khác nhau và cách cấu hình admission controller của riêng mình. Đã xem admission controller NamespaceExists hay NamespaceLifecycle — nó giúp validate xem một namespace đã tồn tại chưa và từ chối request nếu không tồn tại. Đây được gọi là một validating admission controller.
Xem một loại admission controller plugin khác, gọi là plugin DefaultStorageClass — plugin này được enable mặc định. Ví dụ, khi gửi một request tạo PVC, request đi qua authentication, authorization, và cuối cùng là admission controller. Admission controller DefaultStorageClass sẽ theo dõi request tạo PVC và kiểm tra xem nó có storage class được ghi trong đó không. Nếu không — đúng như trường hợp này — nó sẽ chỉnh sửa (modify) request để thêm storage class mặc định vào đó. Đây có thể là bất kỳ StorageClass nào được cấu hình làm StorageClass mặc định trong cluster.
Vậy khi PVC được tạo và kiểm tra lại, sẽ thấy storageClassName: default được thêm vào, dù chưa từng chỉ định điều đó lúc tạo. Đây là loại admission controller được gọi là Mutating Admission Controller — nó có thể thay đổi hoặc "mutate" chính object đó trước khi nó được tạo.
Vậy đó là hai loại admission controller: mutating admission controllers là những controller có thể thay đổi request, và validating admission controllers là những controller có thể validate request và cho phép hoặc từ chối nó. Cũng có thể có admission controller làm cả hai — vừa mutate vừa validate một request.
Nói chung, mutating admission controllers được gọi trước, sau đó mới đến validating admission controllers. Điều này để đảm bảo bất kỳ thay đổi nào do mutating admission controller thực hiện đều được xem xét trong quá trình validation. Trong ví dụ này, admission controller NamespaceAutoProvision, là một mutating admission controller, được chạy trước, theo sau bởi validating controller NamespaceExists. Nếu chạy theo thứ tự ngược lại, admission controller NamespaceExists sẽ luôn từ chối request đến một namespace không tồn tại, và NamespaceAutoProvisioningAdmissionController sẽ không bao giờ được gọi để tạo namespace còn thiếu.
Khi một request đi qua các admission controller này, nếu bất kỳ admission controller nào từ chối request, request bị từ chối và một thông báo lỗi được hiển thị cho user. Đây đều là các admission controller có sẵn (built-in), là một phần của Kubernetes source code, được compile và ship cùng Kubernetes.
Vậy nếu muốn admission controller của riêng mình, với logic mutation và validation riêng thì sao? Để hỗ trợ admission controller bên ngoài (external), có hai admission controller đặc biệt: MutatingAdmissionWebhook và ValidatingAdmissionWebhook.
Có thể cấu hình các webhook này để trỏ đến một server, được host bên trong Kubernetes cluster hoặc bên ngoài nó. Server đó sẽ chạy admission webhook service của riêng mình với logic và code riêng. Sau khi một request đi qua tất cả các admission controller có sẵn, nó chạm tới webhook được cấu hình. Khi chạm vào webhook, nó gọi tới admission webhook server bằng cách truyền vào một object AdmissionReview ở định dạng JSON. Object này chứa toàn bộ chi tiết về request, như user gửi request, loại thao tác user đang cố thực hiện, thực hiện trên object nào, và chi tiết về chính object đó.
Khi nhận request, admission webhook server phản hồi lại bằng một object AdmissionReview với kết quả cho biết request có được phép hay không. Nếu trường allowed trong response set là true, request được cho phép; nếu set là false, nó bị từ chối.
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 của riêng mình — có thể xây dựng trên bất kỳ nền tảng nào. Một ví dụ mã nguồn viết bằng Go có thể tìm thấy trong trang tài liệu Kubernetes. Có thể tự phát triển server bằng các ngôn ngữ khác nếu cần. Yêu cầu duy nhất là nó phải chấp nhận các cuộc gọi API Mutating và Validating, và phản hồi bằng object JSON mà api server mong đợi.
Ví dụ pseudocode của một webhook server viết bằng Python có hai cuộc gọi: một cuộc gọi validate và một cuộc gọi mutate. Cuộc gọi validate nhận validation webhook request, và trong ví dụ này, so sánh tên của object và tên của user đã gửi request, rồi từ chối request nếu chúng trùng tên — chỉ là một use case đơn giản minh họa những gì có thể làm với các request đến. Còn cuộc gọi mutate lấy username và phản hồi bằng một thao tác JSONPatch thêm username vào làm một label cho bất kỳ request nào được ai đó gửi lên.
Nếu xem kỹ đoạn code này, một patch object là một danh sách các thao tác patch, mỗi thao tác là add, remove, replace, move, copy, hoặc test. Sau đó chỉ định đường dẫn (path) trong object JSON cần được nhắm tới để thay đổi — trong trường hợp này là /metadata/labels/user, và giá trị cần thêm nếu đó là thao tác add. Lấy username từ request, đó chính là giá trị của label này. Kết quả này sau đó được gửi đi dưới dạng một object mã hóa Base64 như một phần của response.
Lưu ý bên lề — từ góc độ kỳ thi, sẽ không được yêu cầu viết code như thế này. Đừng lo nếu không hiểu hết đoạn code này. Điều cần rút ra chỉ là admission webhook server là một server tự deploy, chứa logic hoặc code để cho phép hoặc từ chối một request. Và nó phải có khả năng nhận và phản hồi bằng đúng response mà webhook mong đợi. Đây chỉ là một ví dụ đơn giản minh họa những gì có thể làm hoặc code và triển khai trong webhook server tự deploy.
Sau khi đã phát triển webhook server của riêng mình, bước tiếp theo là host nó — chạy nó như một server ở đâu đó, hoặc container hóa và deploy nó trong chính một Kubernetes cluster dưới dạng một Deployment. Nếu deploy dưới dạng một Deployment trong Kubernetes cluster, nó cần một Service để có thể truy cập được — ví dụ một Service tên webhook-service.
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"]
apiVersion là admissionregistration.k8s.io/v1. kind là ValidatingWebhookConfiguration — nếu đang cấu hình một mutating webhook, đây sẽ là MutatingWebhookConfiguration. Đặt tên cho nó.
Dưới webhooks, cấu hình các webhook khác nhau. Một webhook có một name, một clientConfig, và một tập hợp rules. Đặt tên là pod-policy.example.com. clientConfig là nơi cấu hình vị trí của admission webhook server. Nếu deploy server này bên ngoài, không phải một phần của Deployment trong Kubernetes cluster, có thể đơn giản cung cấp một URL đến server đó. Thay vào đó, nếu deploy server dưới dạng một service khác trong chính cluster, có thể dùng cấu hình service và cung cấp namespace cùng tên của service, trong trường hợp này là webhook-service.
Đương nhiên, giao tiếp giữa api server và webhook server phải qua TLS. Vậy cần cấu hình một certificate CA bundle — server phải được cấu hình với một cặp certificate, rồi một certificate CA bundle được tạo và truyền vào clientConfig.caBundle.
Tiếp theo, phải chỉ định khi nào gọi webhook server. Có thể chỉ định rule để cấu hình chính xác khi nào muốn webhook server được gọi để validate. Có thể không muốn điều đó xảy ra với tất cả các cuộc gọi — ví dụ, chỉ muốn nó được gọi khi tạo Pod, hoặc xóa Pod, hoặc tạo Deployment, v.v. Điều này có thể thêm vào dưới section rules bằng apiGroups, apiVersions, operations, và resources. Trong ví dụ này, chỉ gọi webhook configuration này khi có các lệnh gọi tạo Pod.
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.
Lab: Thực hành Validating và Mutating Admission Webhooks
Kết hợp nào dưới đây là đúng cho mutating và validating admission controllers? Đáp án: admission controller NamespaceAutoProvision là mutating, và NamespaceExists là validating. Vì NamespaceAutoProvision tự động tạo một namespace, tức là thay đổi thứ gì đó — đó là mutating; còn NamespaceExists là validating, vì nó validate xem namespace có tồn tại hay không.
Thứ tự gọi (flow of invocation) của các admission controller là gì? Trước tiên là mutate, sau đó là validate.
Tạo một namespace tên webhook-demo.
kubectl create ns webhook-demo
Xác nhận bằng kubectl get ns.
Tạo một TLS secret tên webhook-server-tls cho giao tiếp webhook an toàn, trong namespace webhook-demo. Đã có sẵn certificate và key với đường dẫn tương ứng cho sẵn. Tạo TLS secret:
kubectl create -n webhook-demo secret tls webhook-server-tls \
--cert=<đường-dẫn-cert> \
--key=<đường-dẫn-key>
Tạo một webhook Deployment. Đã có sẵn một sample deployment definition (webhook-deployment.yaml) — có thể xem bằng cat webhook-deployment.yaml. Deploy nó:
kubectl apply -f webhook-deployment.yaml
Tạo Service để webhook có thể giao tiếp được. Đã có sẵn một service configuration (webhook-service.yaml):
kubectl apply -f webhook-service.yaml
Một MutatingWebhookConfiguration đã được cung cấp tại webhook-configuration.yaml. Nếu áp dụng configuration này, nó sẽ ảnh hưởng đến resource action nào? Xem section rules trong file — thấy nó áp dụng cho thao tác create trên resource pods — tức Pod với thao tác create.
Deploy webhook configuration:
kubectl apply -f webhook-configuration.yaml
Ở bước trước, đã deploy một demo webhook thực hiện như sau: nó từ chối mọi request tạo Pod chạy dưới quyền root trong container nếu không có security context được cung cấp. Nếu không có giá trị nào được set cho runAsNonRoot, giá trị mặc định true sẽ được áp dụng, và user ID mặc định là 1234. Ngoài ra, nó cho phép một container chạy dưới quyền root nếu runAsNonRoot được set rõ ràng là false trong security context.
Thử nghiệm bằng cách deploy một pod không có security context. Đã có sẵn một configuration tại pod-with-defaults.yaml — không cung cấp gì về security context, nên nó sẽ dùng giá trị mặc định:
kubectl apply -f pod-with-defaults.yaml
Giá trị runAsNonRoot và runAsUser cho security context của pod vừa tạo là gì? Vì không cung cấp rõ ràng giá trị này, kiểm tra bằng:
kubectl get pod pod-with-defaults -o yaml
Trong section security context, khi không cung cấp gì, nó mặc định runAsNonRoot: true và user là 1234.
Deploy một pod với security context cho phép chạy dưới quyền root một cách rõ ràng. Definition file cho việc này đã có sẵn — pod-with-override.yaml, với security context set runAsNonRoot: false:
kubectl apply -f pod-with-override.yaml
Deploy một pod với security context xung đột. File pod-with-conflict.yaml có security context với runAsNonRoot: true nhưng runAsUser: 0:
kubectl apply -f pod-with-conflict.yaml
Request này bị từ chối, vì đang yêu cầu runAsNonRoot set là true, nhưng lại yêu cầu runAsUser set là 0 — chính là root user. Không thể set runAsNonRoot là true mà lại yêu cầu chạy dưới quyền root user — sẽ phải set nó là false. Vậy validation thành công và nó từ chối việc tạo pod này.
Nguồn tham khảo
Nguồn gốc: Khóa "Certified Kubernetes Administrator (CKA)" — module "3. Scheduling", nền tảng KodeKloud.