2.5. Core Concepts - Imperative vs Declarative
Mục lục
- 1. Imperative và Declarative là gì
- 2. Imperative Commands trong Kubernetes
- 3. Declarative Approach với kubectl apply
- 4. Tip cho kỳ thi: khi nào dùng imperative, khi nào dùng file cấu hình
- 5. Cheatsheet: Imperative Commands với kubectl
- 6. kubectl apply hoạt động ra sao ở phía sau
- 7. kubectl explain và kubectl api-resources
- 8. Lab: giải bài thực hành về Imperative Commands
1. Imperative và Declarative là gì
Phần này bàn về cách tiếp cận imperative và declarative trong Kubernetes. Về cuối phần này, sẽ có một số tip có thể dùng trong kỳ thi.
Đến lúc này đã thấy nhiều cách khác nhau để tạo và quản lý object trong Kubernetes — tạo object trực tiếp bằng cách chạy lệnh, cũng như dùng file cấu hình đối tượng (object configuration file). Trong thế giới infrastructure as code, có nhiều cách tiếp cận khác nhau để quản lý infrastructure, và chúng được phân loại thành imperative và declarative.
💡 Hình dung: giả sử muốn tới nhà một người bạn trên đường B. Trước đây, sẽ thuê một taxi và đưa hướng dẫn từng bước cho tài xế về cách tới đích — như rẽ phải vào đường B, rồi rẽ trái để tới đường C, rồi rẽ trái tiếp và rẽ phải để tới đường D và dừng tại nhà đó. Chỉ rõ làm gì và, quan trọng hơn, làm thế nào, chính là cách tiếp cận imperative. Mặt khác, ngày nay, khi đặt một chuyến xe, ví dụ qua Uber, chỉ cần chỉ định điểm đến cuối cùng, như nhà của Tom — đó là cách tiếp cận declarative. Trong trường hợp này, không đưa hướng dẫn từng bước, chỉ đơn giản chỉ định điểm đến cuối cùng — khai báo (declare) điểm đến cuối cùng, và hệ thống tự tìm ra đường đi đúng để tới đích. Chỉ rõ làm gì, không phải làm thế nào, chính là cách tiếp cận declarative.
Vậy điều này liên quan gì tới những gì đang học? Trong thế giới infrastructure as code, một ví dụ về cách tiếp cận imperative khi cấp phát (provision) infrastructure là một tập hướng dẫn viết từng bước — như cấp phát một VM tên web-server, cài phần mềm NGINX lên đó, chỉnh file cấu hình để dùng port 8080, và set đường dẫn tới các web file, tải source code repository từ Git, và cuối cùng, khởi động NGINX server. Ở đây, đang nói rõ điều gì cần và cả cách để làm được nó.
Trong cách tiếp cận declarative, khai báo yêu cầu. Ví dụ, chỉ nói rằng cần một VM tên web-server với phần mềm NGINX trên đó, với port set là 8080 và đường dẫn tới web file được định nghĩa, cùng nơi lưu source code của ứng dụng. Và mọi thứ cần làm để đưa infrastructure vào đúng trạng thái đó được thực hiện bởi hệ thống hoặc phần mềm — không cần cung cấp hướng dẫn từng bước. Các công cụ orchestration như Ansible, Puppet, Chef, hay Terraform thuộc nhóm này.
Trong cách tiếp cận imperative, điều gì xảy ra nếu lần đầu chỉ một nửa các bước được thực thi? Điều gì xảy ra nếu cung cấp lại đúng tập hướng dẫn để hoàn tất các bước còn lại? Để xử lý các tình huống như vậy, sẽ cần thêm nhiều bước, như kiểm tra xem một thứ gì đó đã tồn tại chưa, và hành động dựa trên kết quả kiểm tra đó. Ví dụ, khi cấp phát VM, điều gì xảy ra nếu một VM tên web-server đã tồn tại? Tương tự với việc tạo database hay import dữ liệu — nên fail hay nên tiếp tục vì VM đã có sẵn? Nếu trong tương lai quyết định nâng cấp phiên bản phần mềm lên, ví dụ, nginx 1.18, thì nên chỉ đơn giản là cập nhật version của nginx trong file cấu hình, và hệ thống tự lo phần còn lại. Lý tưởng nhất, hệ thống nên đủ thông minh để biết điều gì đã được thực hiện và chỉ áp dụng các thay đổi cần thiết. Đó là cách declarative để thực hiện mọi thứ.
Trong thế giới Kubernetes, cách imperative để quản lý infrastructure là dùng các lệnh như kubectl run để tạo Pod, kubectl create deployment để tạo Deployment, kubectl expose để tạo một Service expose một Deployment, và kubectl edit để sửa một object có sẵn. Để scale một deployment hay replica set, dùng kubectl scale. Và để cập nhật image trên một deployment, dùng kubectl set image.
Cũng đã dùng object configuration file để quản lý object, như tạo một object bằng lệnh kubectl create -f với option -f để chỉ định object configuration file, sửa một object bằng lệnh kubectl replace, và xóa một object bằng lệnh kubectl delete. Tất cả đều là cách tiếp cận imperative để quản lý object trong Kubernetes — đang nói chính xác cách đưa infrastructure vào đúng nhu cầu, bằng cách tạo, cập nhật, hoặc xóa object.
Cách tiếp cận declarative sẽ là tạo một tập file định nghĩa trạng thái mong muốn của ứng dụng và service trên một Kubernetes cluster, và với một lệnh kubectl apply duy nhất, Kubernetes có thể đọc file cấu hình và tự quyết định cần làm gì để đưa infrastructure vào trạng thái mong muốn. Nói cách khác, trong cách tiếp cận declarative, sẽ chạy lệnh kubectl apply để tạo, cập nhật, hoặc xóa một object. Lệnh apply sẽ nhìn vào cấu hình hiện có và tự tìm ra thay đổi nào cần được thực hiện lên hệ thống.
2. Imperative Commands trong Kubernetes
Nhìn kỹ hơn từng cách. Trong cách tiếp cận imperative, có 2 cách. Đầu tiên là dùng imperative commands, như run, create, hay expose để tạo object mới, và edit, scale, set để cập nhật object đã có. Các lệnh này giúp nhanh chóng tạo hoặc sửa object vì không phải xử lý file YAML — và chúng hữu ích trong các kỳ thi chứng chỉ. Tuy nhiên, chúng bị giới hạn về mặt tính năng và sẽ đòi hỏi phải viết các lệnh dài, phức tạp cho các use case nâng cao, như tạo một multi-container Pod hay Deployment.
Thứ hai, các lệnh này được chạy một lần rồi bị quên đi. Chúng chỉ khả dụng trong session history của người dùng đã chạy chúng — nên rất khó để người khác biết các object này được tạo ra sao. Vì vậy, khó theo dõi (track), và khó làm việc với các lệnh này trong môi trường lớn hoặc phức tạp.
Đó là lúc việc quản lý object bằng object configuration file phát huy tác dụng. Tạo file định nghĩa object hay file cấu hình, hay còn gọi là file manifest, giúp viết ra chính xác object cần trông như thế nào dưới định dạng YAML, và dùng lệnh kubectl create để tạo object. Giờ luôn có sẵn file YAML, và có thể lưu nó vào một code repository như Git — có thể thiết lập một quy trình review và phê duyệt thay đổi xung quanh các file này, để một thay đổi được review và phê duyệt trước khi áp dụng vào môi trường production.
Trong tương lai, nếu cần thay đổi, ví dụ sửa tên image sang phiên bản khác, có nhiều cách để thực hiện. Một cách là dùng lệnh kubectl edit, chỉ định tên object. Khi chạy lệnh này, nó mở một file định nghĩa YAML tương tự file dùng để tạo object, nhưng kèm thêm vài field, như field status dùng để lưu trạng thái của Pod. Đây không phải file dùng để tạo object — đây là một pod definition file tương tự nằm trong bộ nhớ của Kubernetes. Có thể sửa file này, lưu và thoát, các thay đổi sẽ được áp dụng lên live object.
Tuy nhiên, cần lưu ý: có sự khác biệt giữa live object và file định nghĩa đang có cục bộ. Thay đổi thực hiện bằng lệnh kubectl edit không thực sự được ghi lại ở đâu cả. Sau khi thay đổi được áp dụng, chỉ còn file định nghĩa cục bộ, vốn thực tế vẫn giữ tên image cũ trong đó. Trong tương lai, giả sử một đồng đội quyết định sửa object này mà không biết một thay đổi đã được thực hiện bằng lệnh kubectl edit — khi thay đổi mới được áp dụng, thay đổi trước đó về image sẽ bị mất.
Vậy có thể dùng lệnh kubectl edit nếu đang thực hiện thay đổi mà chắc chắn sẽ không dựa vào object configuration file trong tương lai. Cách tiếp cận tốt hơn là trước tiên sửa phiên bản cục bộ của file cấu hình object với các thay đổi cần thiết — cập nhật tên image tại đây — rồi chạy lệnh kubectl replace để cập nhật object. Nhờ vậy, về sau, các thay đổi được thực hiện sẽ được ghi lại và có thể theo dõi như một phần của quy trình review thay đổi.
Đôi khi, có thể muốn xóa hoàn toàn và tạo lại object. Trong trường hợp đó, có thể chạy cùng lệnh, nhưng kèm option --force, như thế này:
kubectl replace --force -f <file>
Đây vẫn là cách tiếp cận imperative, vì vẫn đang chỉ thị cho Kubernetes cách tạo hay cập nhật các object đó. Trước tiên chạy lệnh kubectl create để tạo object, rồi chạy lệnh replace để thay thế object hoặc lệnh delete để xóa object.
Điều gì xảy ra nếu chạy lệnh create khi object đã tồn tại? Nó sẽ fail với lỗi báo Pod đã tồn tại. Khi cập nhật một object, luôn phải đảm bảo object đã tồn tại trước khi chạy lệnh replace. Nếu object không tồn tại, lệnh replace sẽ fail với thông báo lỗi. Vì vậy, cách tiếp cận imperative khá tốn công cho người quản trị, vì luôn phải nắm rõ cấu hình hiện tại và thực hiện kiểm tra để đảm bảo mọi thứ đúng vị trí trước khi thực hiện thay đổi.
3. Declarative Approach với kubectl apply
Cách tiếp cận declarative dùng cùng object configuration file đã làm việc trước đó. Nhưng thay vì lệnh create hay replace, dùng lệnh kubectl apply để quản lý object. Lệnh kubectl apply đủ thông minh để tạo object nếu nó chưa tồn tại. Nếu có nhiều object configuration file, thường sẽ có, có thể chỉ định một thư mục làm đường dẫn thay vì một file duy nhất — nhờ vậy, mọi object được tạo cùng lúc.
Giờ khi cần thực hiện thay đổi, chỉ đơn giản cập nhật object configuration file và chạy lệnh kubectl apply lại. Lần này, nó biết object đã tồn tại, và chỉ cập nhật object với các thay đổi mới. Nó sẽ không bao giờ báo lỗi rằng object đã tồn tại, hoặc không thể áp dụng các cập nhật — nó luôn tự tìm ra cách tiếp cận đúng để cập nhật object.
Vậy, tiến về sau, bất kỳ thay đổi nào tới ứng dụng — dù là cập nhật image hay các field của file cấu hình có sẵn, hoặc thêm file cấu hình mới hoàn toàn cho object mới — tất cả những gì cần làm chỉ đơn giản là cập nhật thư mục cục bộ với các thay đổi, rồi lệnh kubectl apply sẽ lo phần còn lại.
Cách kubectl apply hoạt động thực sự ở phía sau sẽ được bàn kỹ hơn ở lecture sau.
4. Tip cho kỳ thi: khi nào dùng imperative, khi nào dùng file cấu hình
Từ góc độ kỳ thi, có thể dùng cách tiếp cận imperative để tiết kiệm tối đa thời gian. Ví dụ, nếu câu hỏi chỉ yêu cầu tạo một Pod hoặc một Deployment với image cho trước, một trong các lệnh imperative có thể giúp đạt được điều đó nhanh chóng. Vì vậy, quan trọng là luyện tập các lệnh imperative.
Nếu cần sửa một property của object đã có, dùng lệnh kubectl edit có thể là cách nhanh nhất. Nếu có yêu cầu phức tạp, ví dụ cần nhiều container, environment variable, command trong container, v.v., thì dùng một object configuration file để tạo object sẽ được ưu tiên hơn. Cách này giúp nếu phát hiện đã làm sai, có thể dễ dàng cập nhật file và apply lại — và dùng lệnh kubectl apply trong trường hợp đó sẽ là lựa chọn tốt hơn.
Để biết thêm chi tiết về các cách tiếp cận khác nhau khi quản lý một Kubernetes cluster, hãy tự làm quen với trang tài liệu Kubernetes. Và trong các lab thực hành sắp tới, hãy cố gắng dùng cách tiếp cận imperative khi giải bài.
5. Cheatsheet: Imperative Commands với kubectl
Dù phần lớn thời gian sẽ làm việc theo cách declarative — dùng definition file — các imperative command có thể giúp hoàn thành các tác vụ một lần (one-time task) nhanh chóng, cũng như dễ dàng generate một definition template. Việc này giúp tiết kiệm đáng kể thời gian trong kỳ thi.
Trước khi bắt đầu, làm quen với 2 option hữu ích khi làm việc với các lệnh dưới đây:
--dry-run: mặc định, ngay khi lệnh được chạy, resource sẽ được tạo. Nếu chỉ muốn test lệnh, dùng option --dry-run=client. Việc này sẽ không tạo resource — thay vào đó, nó cho biết resource có thể được tạo không, và lệnh có đúng không.
-o yaml: option này sẽ output resource definition ra màn hình dưới định dạng YAML.
Dùng kết hợp 2 option trên để nhanh chóng generate một file resource definition, rồi có thể chỉnh sửa và tạo resource khi cần, thay vì tạo file từ đầu.
POD
Tạo một Pod NGINX:
kubectl run nginx --image=nginx
Generate file YAML định nghĩa POD (-o yaml). Không tạo (--dry-run):
kubectl run nginx --image=nginx --dry-run=client -o yaml
Deployment
Tạo một deployment:
kubectl create deployment --image=nginx nginx
Generate file YAML cho Deployment (-o yaml). Không tạo (--dry-run):
kubectl create deployment --image=nginx nginx --dry-run=client -o yaml
Generate Deployment với 4 Replica:
kubectl create deployment nginx --image=nginx --replicas=4
Cũng có thể scale một deployment dùng lệnh kubectl scale:
kubectl scale deployment nginx --replicas=4
Một cách khác để làm việc này là lưu định nghĩa YAML ra file rồi sửa:
kubectl create deployment nginx --image=nginx --dry-run=client -o yaml > nginx-deployment.yaml
Sau đó có thể cập nhật file YAML với số replica hoặc bất kỳ field nào khác trước khi tạo deployment.
Service
Tạo một Service tên redis-service loại ClusterIP để expose pod redis trên port 6379:
kubectl expose pod redis --port=6379 --name redis-service --dry-run=client -o yaml
(Cách này sẽ tự động dùng label của pod làm selector.)
Hoặc:
kubectl create service clusterip redis --tcp=6379:6379 --dry-run=client -o yaml
(Cách này sẽ không dùng label của pod làm selector, mà thay vào đó giả định selector là app=redis. Không thể truyền selector như một option. Vì vậy nó không hoạt động tốt nếu pod có tập label khác. Nên generate file rồi sửa lại selector trước khi tạo service.)
Tạo một Service tên nginx loại NodePort để expose port 80 của pod nginx trên port 30080 của các node:
kubectl expose pod nginx --type=NodePort --port=80 --name=nginx-service --dry-run=client -o yaml
(Cách này sẽ tự động dùng label của pod làm selector, nhưng không thể chỉ định node port. Phải generate definition file rồi tự thêm node port bằng tay trước khi tạo service.)
Hoặc:
kubectl create service nodeport nginx --tcp=80:80 --node-port=30080 --dry-run=client -o yaml
(Cách này sẽ không dùng label của pod làm selector.)
Cả 2 lệnh trên đều có giới hạn riêng — một cái không nhận selector, cái kia không nhận node port. Khuyến nghị dùng lệnh kubectl expose. Nếu cần chỉ định node port, generate definition file bằng cùng lệnh đó rồi tự nhập nodeport bằng tay trước khi tạo service.
Tham khảo:
- kubernetes.io/docs/reference/generated/kubectl/kubectl-commands
- kubernetes.io/docs/reference/kubectl/conventions
6. kubectl apply hoạt động ra sao ở phía sau
Phần trước đã thấy lệnh kubectl apply có thể dùng để quản lý object theo cách declarative. Phần này xem sâu hơn một chút về cách lệnh này hoạt động ở bên trong.
Lệnh apply xem xét cả 3 thứ trước khi quyết định thay đổi nào cần thực hiện: local configuration file, live object definition trên Kubernetes, và last-applied configuration. Khi chạy lệnh apply, nếu object chưa tồn tại, object được tạo. Khi object được tạo, một cấu hình object tương tự những gì đã tạo cục bộ được tạo bên trong Kubernetes, nhưng kèm thêm các field để lưu trạng thái của object. Đây là live configuration của object trên Kubernetes cluster — đây là cách Kubernetes lưu trữ nội bộ thông tin về một object, bất kể cách tiếp cận nào được dùng để tạo object.
Nhưng khi dùng lệnh kubectl apply để tạo một object, nó làm thêm một điều: phiên bản YAML của local object configuration file vừa viết được chuyển sang định dạng JSON, và sau đó được lưu lại dưới dạng last-applied-configuration. Về sau, với bất kỳ cập nhật nào cho object, cả 3 thứ này được so sánh để xác định thay đổi nào cần thực hiện trên live object.
Ví dụ, khi image nginx được cập nhật lên 1.19 trong file cục bộ, khi chạy lệnh kubectl apply, giá trị này được so sánh với giá trị trong live configuration. Nếu có khác biệt, live configuration được cập nhật với giá trị mới. Sau mỗi thay đổi, last-applied configuration ở định dạng JSON luôn được cập nhật để phản ánh phiên bản mới nhất, để nó luôn được cập nhật kịp thời.
Vậy tại sao thực sự cần last-applied configuration? Nếu một field bị xóa — ví dụ label app bị xóa — và giờ chạy lệnh kubectl apply, sẽ thấy last-applied configuration có label đó, nhưng nó không còn xuất hiện trong local configuration. Điều này nghĩa là field đó cần được gỡ khỏi live configuration.
Nếu một field có mặt trong live configuration, nhưng không có trong local hoặc last-applied configuration, nó sẽ được giữ nguyên như hiện có. Nhưng nếu một field bị thiếu khỏi file cục bộ, và nó có mặt trong last-applied configuration, thì nghĩa là ở bước trước — hay lần cuối lệnh kubectl apply được chạy — field đó đã có mặt, và giờ đang bị gỡ bỏ. Vậy last-applied configuration giúp xác định field nào đã bị gỡ khỏi file cục bộ, để field đó cũng được gỡ khỏi live configuration thực tế.
Vậy đã thấy 3 tập file. Local file được lưu trên hệ thống cục bộ, live object configuration nằm trong bộ nhớ của Kubernetes cluster. Nhưng file JSON chứa last-applied configuration được lưu ở đâu? Nó được lưu ngay trong live object configuration trên Kubernetes cluster, dưới dạng một annotation tên last-applied-configuration.
Lưu ý, điều này chỉ được thực hiện khi dùng lệnh kubectl apply — lệnh kubectl create hay kubectl replace không lưu last-applied configuration theo cách này. Vì vậy cần lưu ý không trộn lẫn cách tiếp cận imperative và declarative khi quản lý Kubernetes object. Một khi đã dùng lệnh apply, mọi thay đổi về sau, lệnh apply sẽ so sánh cả 3 phần: local object configuration file, live object configuration, và last-applied configuration lưu bên trong live object configuration, để quyết định thay đổi nào cần thực hiện lên live configuration, tương tự như đã thấy ở trên.
7. kubectl explain và kubectl api-resources
Có rất nhiều thứ có thể tìm hiểu về resource và field mà không cần rời khỏi terminal.
Để list toàn bộ resource, chỉ cần chạy lệnh:
kubectl api-resources
Lệnh này đặc biệt hữu ích khi không tìm ra một resource trên trang tài liệu. Nếu quên tên resource hay short name của nó, hoặc không chắc về API version bên dưới, như v1 hay apps/v1, hoặc không chắc đúng case của tên resource cần dùng khi viết definition file, có thể dùng lệnh kubectl api-resources.
Sau khi xác định tên resource từ danh sách, và cần thêm thông tin về resource, field, và type bên dưới nó, có thể chạy lệnh kubectl explain, cung cấp tên resource cần giải thích:
kubectl explain pods
Trong trường hợp này, dùng pods sẽ liệt kê toàn bộ field cấp cao nhất (top-level field) của pod — là apiVersion, kind, metadata, spec, và status. Có thể thấy type của từng field, đó là apiVersion là một string, kèm mô tả nó dùng để làm gì. Nhưng đây chưa phải danh sách đầy đủ mọi field và subfield — nó chỉ liệt kê các item cấp cao nhất.
Để đi sâu hơn, chạy cùng lệnh nhưng thêm .field vào cuối, ví dụ:
kubectl explain pods.spec
Việc này sẽ liệt kê các subfield bên trong spec. Nhưng lệnh này lại chỉ liệt kê các sub-property của property cha vừa nhắc tới — vẫn chưa đầy đủ. Để liệt kê toàn bộ field theo đúng cách sẽ đặt chúng trong file YAML, dùng flag --recursive:
kubectl explain pods --recursive
Giờ nó sẽ output toàn bộ danh sách đầy đủ các field khả dụng trên resource đó — và giờ rất dễ để tạo file YAML từ danh sách này.
8. Lab: giải bài thực hành về Imperative Commands
Trong lab này, thực hành hands-on việc tạo Kubernetes object theo cách imperative — nội dung này chủ yếu để hỗ trợ chuẩn bị cho kỳ thi, nên cần làm quen thật kỹ với các lệnh imperative.
Câu hỏi 1 — Deploy một Pod tên nginx-pod dùng image nginx:alpine
kubectl run nginx-pod --image=nginx:alpine
Câu hỏi 2 — Deploy một Pod redis dùng image Redis alpine, với label tier=DB
Kiểm tra help của kubectl run xem có option để chỉ định label không — có option --labels=, nhận key-value pair, và có thể chỉ định nhiều label cách nhau bằng dấu phẩy.
kubectl run redis --image=redis:alpine --labels=tier=DB
Câu hỏi 3 — Tạo một Service tên redis-service để expose ứng dụng redis trong cluster trên port 6379
Type là ClusterIP, port là 6379. Có thể dùng lệnh kubectl create service, nhưng lệnh đó không cho phép chỉ định selector. Cách phù hợp nhất là dùng lệnh kubectl expose, vì nó sẽ tự động phát hiện label và dùng làm selector cho Service. (Chỉ nên dùng kubectl create service khi cần chỉ định một NodePort cụ thể.)
kubectl expose pod redis --port=6379 --name=redis-service
Kiểm tra lại, thấy label và selector đúng (tier=db được tự động lấy), và endpoint đã được phát hiện tự động.
Câu hỏi 4 — Tạo một Deployment tên webapp dùng image httpd:alpine, với 3 replica
kubectl create deployment webapp --image=httpd:alpine --replicas=3
Kiểm tra, thấy 3/3 Ready.
Câu hỏi 5 — Tạo một Pod mới tên custom-nginx dùng image nginx, expose trên container port 8080
Đây là chỉ định port trên container, không phải expose port bằng cách tạo Service — dùng option --port:
kubectl run custom-nginx --image=nginx --port=8080
Câu hỏi 6 — Tạo một namespace mới tên devns
kubectl create namespace devns
Câu hỏi 7 — Tạo một deployment mới tên redis-deploy trong namespace devns, dùng image redis, với 2 replica
kubectl create deployment redis-deploy --image=redis --replicas=2 --namespace=devns
Kiểm tra, thấy deployment redis-deploy trong namespace devns với 2 replica.
Câu hỏi 8 — Tạo một Pod tên httpd dùng image httpd:alpine trong namespace default, và tạo một Service loại ClusterIP cùng tên, với targetPort là 80
Yêu cầu thực hiện với càng ít bước càng tốt. Có 2 cách: tạo Pod bằng kubectl run, rồi dùng lệnh kubectl expose để tạo Service — nhưng cũng có thể kết hợp cả 2 ngay trong lệnh run. Một trong các option của lệnh run, nếu xem help, là --expose — mặc định là false; nếu set true, tạo một Service loại ClusterIP gắn liền với pod, yêu cầu phải có field --port.
kubectl run httpd --image=httpd:alpine --port=80 --expose=true
Lệnh này tự động tạo một Service loại ClusterIP, expose Pod nội bộ. Kiểm tra lại, thấy có Pod httpd và Service httpd. Xem chi tiết Service, thấy nó có selector run=httpd, và các Endpoints đã được phát hiện.
Nguồn tham khảo
Nguồn gốc: Khóa "Certified Kubernetes Administrator (CKA)" — phần "Core Concepts" (Imperative vs Declarative approach, imperative commands, kubectl apply, kubectl apply internals, kubectl explain/api-resources, cheatsheet imperative commands, lab imperative commands), nền tảng KodeKloud. Giảng viên: Mumshad Mannambeth.
Repo ghi chú, link tài liệu, và đáp án các practice question của toàn bộ khóa học: kodekloudhub/certified-kubernetes-administrator-course.