Skip to main content

2.5. Core Concepts: Imperative vs Declarative

Mục lục​


1. Imperative và Declarative là gì​

Cùng một Deployment có thể ra đời từ một dòng kubectl create deployment, hoặc từ một file YAML rồi kubectl apply. Chọn lối nào, và vì sao, quyết định tốc độ làm việc hàng ngày, kể cả trong kỳ thi CKA.

Đến lúc này đã thấy nhiều cách khác nhau để tạo và quản lý object trong Kubernetes, từ lệnh gõ trực tiếp tới file cấu hình đối tượng (object configuration file). Trong thế giới infrastructure as code, các cách quản lý infrastructure cũng được xếp vào hai nhóm: imperative và declarative.

💡 Hình dung: giả sử muốn tới nhà một người quen 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: 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. Ngày nay, khi đặt một chuyến xe 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: không có hướng dẫn từng bước nào, chỉ khai báo (declare) đích đến, và hệ thống tự tìm ra đường đi đúng để tới đó.

Chuyện đặt xe đó ánh xạ thẳng sang 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: 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, 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ó.

Cách tiếp cận declarative chỉ khai báo yêu cầu. Ví dụ, nói rằng cần một VM tên web-server với phần mềm NGINX trên đó, port set là 8080, đường dẫn tới web file được định nghĩa, cùng nơi lưu source code của ứng dụng. Mọi việc cần làm để đưa infrastructure vào đúng trạng thái đó do hệ thống hoặc phần mềm tự thực hiện, không cần 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.

Imperative: ra lệnh từng bước — Declarative: khai báo đích đếnImperative1. Tạo VM web-server2. Cài NGINX3. Sửa port = 80804. Start serverPhải tự chỉ rõ từng bước —tự lo nếu 1 bước đã chạy trước đó.Declarativename: web-serversoftware: nginx, port: 8080Hệ thống tự tìm cáchđạt đúng trạng thái nàyChỉ khai báo đích đến — hệ thống tự lo phần còn lại,kể cả khi chạy lại nhiều lần.

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? Cung cấp lại đúng tập hướng dẫn đó chưa chắc hoàn tất được các bước còn lại: bước cấp phát VM lúc này sẽ đụng phải một VM tên web-server đã tồn tại. Để xử lý các tình huống như vậy, phải viết thêm nhiều bước, như kiểm tra xem một thứ gì đó đã tồn tại chưa, rồi hành động dựa trên kết quả kiểm tra đó. Cách declarative thì ngược lại: nếu sau này quyết định nâng cấp phiên bản phần mềm lên nginx 1.18, chỉ cần 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.

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.

Tạo và sửa một Deployment theo cách imperative. Mỗi thay đổi là một lệnh riêng, gõ thẳng vào cluster:

kubectl create deployment webapp --image=nginx:1.18

# Output:
deployment.apps/webapp created

Lệnh này tự đặt tên container theo tên image sau khi bỏ phần tag: image nginx:1.18 cho ra container tên nginx, còn webapp chỉ là tên Deployment. Hai tên đó khác nhau, và các lệnh phía sau phải gọi đúng cái cần gọi.

kubectl scale deployment webapp --replicas=3

# Output:
deployment.apps/webapp scaled
kubectl set image deployment/webapp nginx=nginx:1.19

# Output:
deployment.apps/webapp image updated

Cú pháp của kubectl set image là <tên container>=<image mới>, nên vế trái phải là nginx, tên container vừa nói ở trên. Output image updated xác nhận field spec.template.spec.containers[0].image đã được ghi đè.

kubectl expose deployment webapp --port=80 --name=webapp-service

# Output:
service/webapp-service exposed
kubectl edit deployment webapp

# Output:
deployment.apps/webapp edited

kubectl edit mở live object trong editor, lưu và thoát là thay đổi có hiệu lực ngay. Tiện, nhưng kèm theo một cái giá sẽ được mổ xẻ ở mục 2. Cùng nhóm lệnh này còn có kubectl run để tạo Pod, được liệt kê đầy đủ ở mục 5.

Năm lệnh ở trên là năm hành động rời rạc, và không chỗ nào trên cluster ghi lại rằng webapp đã ra đời theo trình tự nào.

Chạy lại đúng lệnh tạo đó lần thứ hai. Đây là điểm đau của imperative, đúng như ví dụ VM ở trên:

kubectl create deployment webapp --image=nginx:1.18

# Output:
Error from server (AlreadyExists): deployments.apps "webapp" already exists

Tiền tố Error from server cho thấy lỗi đến từ API server chứ không phải từ kubectl: request đã được gửi đi và bị từ chối vì trùng tên. Một lệnh imperative mô tả hành động, mà hành động "tạo" thì chỉ đúng một lần, nên muốn chạy lại an toàn phải tự kiểm tra object đã tồn tại chưa.

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. Có file YAML rồi nhưng vẫn là imperative, vì lệnh vẫn đang nói chính xác hành động cần làm với object: tạo, thay thế, hay xóa. Nhánh này được show đầy đủ kèm output ở mục 2 ngay sau.

Ở phía declarative, việc cần làm 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ới một lệnh kubectl apply duy nhất, Kubernetes đọc file cấu hình rồi tự quyết định cần làm gì để đưa infrastructure vào trạng thái mong muốn.

Cùng mục tiêu đó, làm theo cách declarative. Một file webapp.yaml duy nhất thay cho cả chuỗi lệnh ở trên:

apiVersion: apps/v1
kind: Deployment
metadata:
name: webapp
spec:
replicas: 3
selector:
matchLabels:
app: webapp
template:
metadata:
labels:
app: webapp
spec:
containers:
- name: nginx
image: nginx:1.19

File này ghi ra đích đến (3 replica, image nginx:1.19), không kể lại đường đi tới đó: không có dòng nào nói "tạo trước, scale sau, rồi đổi image". Field spec.selector.matchLabels phải khớp với spec.template.metadata.labels, vì đó là cách Deployment nhận ra Pod nào thuộc về nó.

kubectl apply -f webapp.yaml

# Output:
deployment.apps/webapp created

Chạy lại đúng lệnh đó lần thứ hai. Chỗ này mới là khác biệt cốt lõi so với create:

kubectl apply -f webapp.yaml

# Output:
deployment.apps/webapp unchanged

Không có AlreadyExists nào. apply mô tả trạng thái chứ không mô tả hành động, nên chạy bao nhiêu lần cũng cho ra cùng một kết quả: file trùng với live object thì báo unchanged và không đụng gì tới cluster. Sửa replicas trong file rồi chạy lại vẫn đúng lệnh đó, chỉ khác là output đổi thành configured.

Cùng một lệnh kubectl apply đó dùng cho cả việc tạo lẫn cập nhật một object: apply 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. Riêng việc xoá vẫn phải gọi kubectl delete, vì apply không tự xoá object nào trừ khi chạy kèm --prune. Cách nó làm điều đó sẽ được mổ xẻ kỹ hơn ở mục 6.

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


2. Imperative Commands trong Kubernetes​

Nhìn kỹ hơn từng cách. Cách tiếp cận imperative có 2 nhánh. Nhánh đầu 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 tạo hoặc sửa object rất nhanh vì không phải xử lý file YAML, và cũng vì vậy mà chúng hữu ích trong các kỳ thi chứng chỉ. Bù lại, chúng bị giới hạn về mặt tính năng và đòi hỏi những 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.

Hạn chế thứ hai: các lệnh này được chạy một lần rồi bị quên đi. Chúng chỉ nằm trong session history của người dùng đã chạy chúng, nên người khác gần như không có cách nào biết các object này được tạo ra sao. Đã khó theo dõi (track) như vậy thì cũng 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. File định nghĩa object, còn gọi là file cấu hình hay file manifest, ghi ra chính xác object cần trông như thế nào dưới định dạng YAML, rồi lệnh kubectl create tạo object từ file đó.

Tạo object từ object configuration file.

kubectl create -f rc-definition.yaml

# Output:
replicationcontroller/myapp-rc created

Output ghi created, không phải configured: create chỉ biết đúng một hành động là tạo mới, bất kể nội dung file có trùng với thứ đang chạy hay không.

File YAML luôn có sẵn và lưu được vào một code repository như Git, nhờ đó thiết lập được cả một quy trình quanh các file này: một thay đổi phải được review và phê duyệt trước khi áp dụng vào môi trường production.

Sau này, 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 hiện tại của object. Đây không phải file dùng để tạo object lúc đầu, mà là bản định nghĩa tương ứng đang nằm trong bộ nhớ của Kubernetes. Sửa file này, lưu và thoát, các thay đổi sẽ được áp dụng lên live object.

Chỗ này có một khác biệt rất dễ bỏ qua: live object và file định nghĩa đang có cục bộ giờ không còn giống nhau. Đặt 2 nguồn cạnh nhau là thấy ngay:

kubectl get rc myapp-rc -o jsonpath='{.spec.template.spec.containers[0].image}'

# Output:
nginx:1.18
grep 'image:' rc-definition.yaml

# Output:
image: nginx:1.16

kubectl get đọc live object trong etcd, còn grep đọc file trên đĩa, và 2 giá trị đã lệch nhau. Thay đổi thực hiện bằng lệnh kubectl edit không được ghi lại ở đâu ngoài live object, nên file định nghĩa cục bộ vẫn giữ nguyên tên image cũ.

Rủi ro của kubectl edit: local file và live object lệch nhaurc-definition.yaml (local)image: nginx:1.16(chưa từng được cập nhật)Live object (cluster)image: nginx:1.18(sửa qua kubectl edit)⚠ KHÔNGđồng bộĐồng nghiệp chạy: kubectl replace -f rc-definition.yamlLive object bị ghi đè về lại nginx:1.16— mất luôn thay đổi lên 1.18

Giả sử một đồng đội quyết định sửa object này mà không biết đã có một thay đổi 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, đúng như diagram trên minh họa.

Vậy chỉ nên dùng lệnh kubectl edit khi chắc chắn sẽ không dựa vào object configuration file đó về sau. Cách tốt hơn là sửa phiên bản cục bộ của file cấu hình object trước (cập nhật tên image ngay tại đó), rồi chạy lệnh kubectl replace để cập nhật object:

kubectl replace -f rc-definition.yaml

# Output:
replicationcontroller/myapp-rc replaced

Từ replaced mô tả đúng việc đã xảy ra: toàn bộ spec của live object bị thay bằng nội dung file, nên file phải đầy đủ chứ không được cắt bớt field. Nhờ đi qua file, mọi thay đổi về sau đều được ghi lại và theo dõi được như một phần của quy trình review thay đổi.

Đôi khi, thứ cần làm là xóa hẳn object rồi tạo lại, ví dụ khi phải sửa một field không cho phép thay đổi tại chỗ. Trường hợp đó vẫn chạy cùng lệnh, kèm option --force:

kubectl replace --force -f rc-definition.yaml

# Output:
replicationcontroller "myapp-rc" deleted
replicationcontroller/myapp-rc replaced

Output ra 2 dòng vì --force thực hiện 2 việc: xóa object cũ khỏi API rồi tạo lại từ đầu theo file. Object mới mang UID mới, nên mọi thứ gắn với object cũ đều mất, kể cả các Pod đang chạy dưới nó.

Đâ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:

kubectl delete -f rc-definition.yaml

# Output:
replicationcontroller "myapp-rc" deleted

kubectl delete -f không xóa file, nó chỉ đọc kind và metadata.name trong file để biết cần xóa object nào trên cluster.

Điều gì xảy ra nếu chạy lệnh create khi object đã tồn tại? Nó fail ngay:

kubectl create -f rc-definition.yaml

# Output:
Error from server (AlreadyExists): error when creating "rc-definition.yaml": replicationcontrollers "myapp-rc" already exists

Chiều ngược lại cũng vậy: replace đòi object phải có sẵn, chạy nó khi object chưa tồn tại thì nhận về NotFound:

kubectl replace -f rc-definition.yaml

# Output:
Error from server (NotFound): error when replacing "rc-definition.yaml": replicationcontrollers "myapp-rc" not found

Cả 2 thông báo đều mang tiền tố Error from server và kèm tên file gây lỗi trong cụm error when creating/error when replacing, vì kubectl chỉ biết kết quả sau khi API server trả lời. Vì vậy, cách tiếp cận imperative khá tốn công cho người quản trị: 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. Cách tiếp cận declarative sinh ra để gỡ đúng gánh nặng đó.

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


3. Declarative Approach với kubectl apply​

Cách tiếp cận declarative vẫn dùng đúng những object configuration file đã làm việc trước đó. Chỉ khác là thay lệnh create hay replace bằ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. Khi có nhiều object configuration file, thường là như vậy, đường dẫn truyền vào có thể là một thư mục thay vì một file duy nhất, và mọi object được tạo cùng lúc:

kubectl apply -f manifests/

# Output:
deployment.apps/myapp-deployment created
service/myapp-service created

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:

kubectl apply -f manifests/

# Output:
deployment.apps/myapp-deployment configured
service/myapp-service unchanged

Nó 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, mà luôn tự tìm ra cách tiếp cận đúng để cập nhật object. Cột configured/unchanged/created trong output là bằng chứng trực tiếp: apply biết chính xác object nào cần tạo mới, object nào chỉ cần cập nhật, và object nào giữ nguyên.

Từ đó về sau, mọi thay đổi tới ứng dụng (cập nhật image, sửa các field của file cấu hình có sẵn, hay thêm một file cấu hình mới cho object mới) đều rút về một việc: cập nhật thư mục cục bộ với các thay đổi, rồi để lệnh kubectl apply lo phần còn lại. Nhưng "tự tìm ra" đó thực chất hoạt động ra sao ở phía sau? Mục 6 sẽ mổ xẻ kỹ, trước hết là một vài tip thực chiến cho việc chọn giữa imperative và declarative.


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ó, lệnh kubectl edit thường là cách nhanh nhất. Còn với yêu cầu phức tạp, ví dụ cần nhiều container, environment variable, command trong container, thì nên tạo object từ một object configuration file. Làm vậy, lúc phát hiện đã làm sai vẫn cập nhật được file rồi apply lại, và lệnh kubectl apply trong trường hợp đó 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. Khi tự lab, luyện cả 2 hướng, nhưng ưu tiên phản xạ imperative trước, vì đó là thứ tiết kiệm thời gian nhất trong phòng thi. Phần tiếp theo tổng hợp cheatsheet đầy đủ các lệnh đó.


5. Cheatsheet: Imperative Commands với kubectl​

Dù phần lớn thời gian sẽ làm việc theo cách declarative, tức là dùng definition file, các imperative command vẫn giúp hoàn thành nhanh các tác vụ một lần (one-time task), và generate một definition template rất gọn. Việc này 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=client: 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 này: không resource nào được tạo, kết quả chỉ cho biết resource có thể được tạo không, và lệnh có đúng không.

-o yaml: 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

# Output:
pod/nginx created

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

# Output:
deployment.apps/nginx created

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

# Output:
deployment.apps/nginx scaled

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:

Đã có đủ phản xạ imperative rồi. Giờ quay lại câu hỏi bỏ ngỏ ở mục 3: kubectl apply biết "chỉ cập nhật đúng phần cần thay đổi" bằng cách nào?


6. kubectl apply hoạt động ra sao ở phía sau​

Phần trước đã thấy kubectl apply tự phân biệt object nào cần tạo mới (created), object nào chỉ cần cập nhật (configured), object nào giữ nguyên (unchanged). Điểm tạo nên khác biệt là số nguồn mà apply đem ra so sánh. Nếu chỉ so sánh local file với live object đang chạy trên cluster, apply đã đủ để phát hiện field nào thay đổi giá trị hoặc thêm mới. Vấn đề nằm ở trường hợp ngược lại: field nào đó bị xóa khỏi local file. Chỉ với 2 nguồn local file và live object, apply không thể phân biệt được 2 tình huống sau, dù chúng cần xử lý theo 2 hướng trái ngược:

  1. Field đó chưa từng do apply quản lý, ví dụ do một controller khác tự thêm vào (như HPA ghi số replicas, hay một mutating webhook tự gắn thêm annotation). Field này không nên bị động tới, dù nó không xuất hiện trong local file.
  2. Field đó từng được set qua chính local file ở lần apply trước, giờ người dùng chủ động xóa khỏi file. Field này cần bị gỡ khỏi live object, để live object phản ánh đúng ý định mới nhất trong local file.

Nhìn vào live object tại một thời điểm, không thể biết field đang thiếu trong local file thuộc trường hợp nào trong 2 trường hợp trên, vì cả hai đều chỉ đơn giản là "field có trong live, không có trong local". Vì vậy apply cần một nguồn thứ 3: last-applied-configuration, một bản chụp JSON của chính local file tại lần apply gần nhất, được lưu ngay trong live object dưới dạng annotation. Nhờ bản chụp này, apply biết chắc field nào đã từng do nó ghi ra ở lần trước, để phân biệt với field do "người khác" (controller, webhook, hay kubectl edit thủ công) thêm vào.

💡 Hình dung như thế này: last-applied-configuration giống một bản nháp bạn giữ lại mỗi khi gửi email chỉnh sửa hợp đồng cho đối tác. Lần sau muốn sửa tiếp, bạn không so sánh trực tiếp bản hợp đồng đang có hiệu lực (live object) với bản nháp mới (local file), vì đối tác cũng có thể đã tự thêm điều khoản riêng vào bản hiện tại mà bạn không hề hay biết, và bạn không có quyền xóa điều khoản đó. Bạn so cả 3 bản: nháp cũ bạn từng gửi (last-applied), bản đang hiệu lực (live), và nháp mới (local). Điều khoản nào bạn từng viết ở nháp cũ mà giờ nháp mới không còn, đó là điều khoản bạn muốn hủy. Điều khoản nào có trong bản hiệu lực nhưng chưa từng nằm trong nháp bạn viết thì không phải việc của bạn, cứ để nguyên.

Ba nguồn này giữ vai trò khác nhau và nằm ở 2 nơi hoàn toàn khác nhau:

NguồnLưu ở đâuVai trò
Local fileHệ thống file cục bộ, nơi soạn YAMLÝ định mới nhất của người dùng
Live objectBộ nhớ/etcd của Kubernetes clusterTrạng thái thật đang chạy
last-applied-configurationAnnotation bên trong chính live objectÝ định ở lần apply gần nhất, làm mốc so sánh
kubectl apply so sánh cả 3 nguồn trước khi quyết định thay đổiLocal fileimage: 1.19label "env=prod" đã bị xóaLive object(đang chạy trên cluster)last-applied-config(annotation, JSON snapshotcủa lần apply trước)kubectl apply — so sánh cả 3 nguồnLive object: image → 1.19, label "env=prod" bị gỡ bỏField khác không có trong local lẫn last-applied thì được giữ nguyênThiếu trong local nhưng có trong last-applied → field đó bị gỡ khỏi live object.

Để thấy rõ cơ chế này vận hành thế nào, hãy đi qua 2 lần chạy apply liên tiếp trên cùng một Deployment.

Lần apply thứ 1: tạo object. File cục bộ deployment.yaml khai báo image nginx:1.18 và 2 label app=nginx, env=prod:

kubectl apply -f deployment.yaml

# Output:
deployment.apps/myapp-deployment created

Vì object chưa tồn tại, apply tạo mới nó trên cluster. Ngay tại bước này, apply làm thêm một việc: chuyển nguyên nội dung local file (đúng những gì vừa viết) sang JSON, rồi lưu bản JSON đó vào chính live object vừa tạo, dưới dạng annotation tên last-applied-configuration. Sau bước này, trạng thái 3 nguồn như sau:

Nguồnimagelabel applabel env
Local file1.18nginxprod
Live object1.18nginxprod
last-applied-configuration1.18nginxprod

Cả 3 khớp nhau tuyệt đối, chuyện dễ hiểu vì last-applied vừa được sinh ra từ chính local file ở thời điểm này.

Lần apply thứ 2: sửa file rồi apply lại. Giờ chỉnh sửa deployment.yaml: bump image lên 1.19, và xóa hẳn dòng label env: prod (chỉ còn giữ app: nginx). Chạy lại:

kubectl apply -f deployment.yaml

# Output:
deployment.apps/myapp-deployment configured

apply lúc này xét từng field, so 3 nguồn cùng lúc:

FieldLocal (mới)Live (trước khi apply)last-applied (trước khi apply)Quyết định
image1.191.181.18Local khác live → cập nhật live thành 1.19
label appnginxnginxnginxKhông đổi → giữ nguyên
label env(không có)prodprodThiếu ở local, nhưng có mặt trong last-applied → nghĩa là field này từng do chính apply ghi ra, giờ bị chủ động xóa → gỡ khỏi live object

Kết quả: live object còn lại image 1.19, chỉ 1 label app=nginx. last-applied-configuration cũng được ghi đè lại thành đúng nội dung local file mới nhất, sẵn sàng làm mốc so sánh cho lần apply kế tiếp.

Giờ thử một tình huống khác để thấy vì sao apply phải so 3 nguồn thay vì 2. Giả sử sau lần apply thứ 2, một Horizontal Pod Autoscaler (hay bất kỳ controller nào khác) tự ý ghi thêm annotation autoscaling.io/last-scaled vào live object. Annotation này không nằm trong local file, và cũng không nằm trong last-applied-configuration, vì nó chưa bao giờ do apply tạo ra. Nếu chạy kubectl apply -f deployment.yaml thêm lần nữa mà không đổi gì trong file, annotation đó vẫn được giữ nguyên trên live object: apply nhận ra field này thiếu trong local, nhưng cũng thiếu luôn trong last-applied, tức chưa từng thuộc quyền quản lý của nó, nên không có căn cứ để xóa.

Vậy là rõ câu hỏi "tại sao cần last-applied-configuration": nếu chỉ so local với live, apply không thể phân biệt 2 trường hợp field "thiếu trong local" ở trên, một trường hợp cần xóa (label env), một trường hợp phải giữ nguyên (annotation của HPA). last-applied là bằng chứng cho biết field nào từng do apply ghi ra, để chỉ những field đó mới đủ điều kiện bị gỡ khi không còn xuất hiện trong local file nữa.

last-applied-configuration không lưu ở đâu xa: nó nằm ngay trong chính live object, dưới dạng annotation, nên xem được trực tiếp bằng kubectl get:

kubectl get deployment myapp-deployment -o jsonpath='{.metadata.annotations.kubectl\.kubernetes\.io/last-applied-configuration}'

# Output (rút gọn):
{"apiVersion":"apps/v1","kind":"Deployment","metadata":{"name":"myapp-deployment","labels":{"app":"nginx"}},"spec":{"template":{"spec":{"containers":[{"image":"nginx:1.19"...

Annotation này chỉ được ghi và cập nhật khi dùng kubectl apply, còn kubectl create hay kubectl replace không đụng tới nó. Vì vậy nếu object được tạo bằng create rồi sau đó mới chuyển sang quản lý bằng apply, last-applied-configuration ở lần apply đầu tiên đó sẽ trống hoặc thiếu ngữ cảnh so với live object thật, dẫn đến apply có thể tính toán sai field nào cần xóa. Đây là lý do nên chọn dứt khoát một cách tiếp cận (imperative hoặc declarative) cho một object, thay vì trộn lẫn create/edit với apply trên cùng object đó.

Đã hiểu rõ apply hoạt động ra sao. Nhưng làm sao biết chính xác field nào hợp lệ để viết vào YAML, đúng apiVersion nào cho từng loại resource, mà không cần rời khỏi terminal? Đó là việc của kubectl explain và kubectl api-resources.


7. kubectl explain và kubectl api-resources​

Bản thân kubectl mang sẵn tài liệu tra cứu về resource và field.

Để list toàn bộ resource, chỉ cần chạy lệnh:

kubectl api-resources

# Output (rút gọn):
NAME SHORTNAMES APIVERSION NAMESPACED KIND
pods po v1 true Pod
services svc v1 true Service
deployments deploy apps/v1 true Deployment
replicasets rs apps/v1 true ReplicaSet

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. Quên tên resource hay short name của nó, không chắc về API version bên dưới, như v1 hay apps/v1, hay không chắc đúng case của tên resource cần dùng khi viết definition file: cột APIVERSION trong output là câu trả lời nhanh nhất cho lỗi no matches for kind ... in version ... đã gặp ở Part 2.3.

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

# Output (rút gọn):
KIND: Pod
VERSION: v1
FIELDS:
apiVersion <string>
kind <string>
metadata <Object>
spec <Object>
status <Object>

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: apiVersion, kind, metadata, spec, và status. Kèm theo là type của từng field và mô tả nó dùng để làm gì. Nhưng đây chưa phải danh sách đầy đủ mọi field và subfield, vì 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 liệt kê các subfield bên trong spec. Nhưng lệnh vẫn chỉ trả về các sub-property của property cha vừa nhắc tới, chưa phải tất cả. Để 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

Lần này output là toàn bộ danh sách đầy đủ các field khả dụng trên resource đó, và tạo file YAML từ danh sách này thì rất nhanh.

Bộ công cụ tra cứu đã đủ dùng. Phần cuối ráp mọi lệnh imperative đã học ở trên lại thành các ví dụ thực hành, đúng dạng câu hỏi hay gặp trong kỳ thi.


8. Ví dụ thực hành: kết hợp các lệnh imperative​

Các ví dụ dưới đây ghép nhanh các lệnh imperative lại với nhau, đúng kiểu tình huống hay gặp khi tự lab hoặc trong kỳ thi: càng ít bước, càng ít gõ, càng tốt.

Tạo Pod với image và tag cụ thể.

kubectl run nginx-pod --image=nginx:alpine

# Output:
pod/nginx-pod created

Tạo Pod kèm label ngay lúc tạo bằng --labels, nhận nhiều cặp key=value cách nhau bằng dấu phẩy:

kubectl run redis --image=redis:alpine --labels=tier=db

# Output:
pod/redis created

Expose Pod thành Service, tự động lấy label làm selector. kubectl create service không nhận option --selector, nên kubectl expose luôn là lựa chọn đúng khi cần selector khớp đúng label của Pod:

kubectl expose pod redis --port=6379 --name=redis-service

# Output:
service/redis-service exposed

Kiểm tra lại để xác nhận selector đã tự động lấy đúng tier=db, và Endpoints đã phát hiện được Pod:

kubectl describe svc redis-service

# Output (rút gọn):
Selector: tier=db
Endpoints: 10.244.0.9:6379

Tạo Deployment với số replica chỉ định ngay từ đầu:

kubectl create deployment webapp --image=httpd:alpine --replicas=3

# Output:
deployment.apps/webapp created

Chỉ định container port khi tạo Pod (khác với expose port ra Service: đây chỉ là khai báo containerPort trong spec, chưa tạo Service nào):

kubectl run custom-nginx --image=nginx --port=8080

# Output:
pod/custom-nginx created

Tạo Deployment trong 1 namespace cụ thể ngay từ lệnh tạo:

kubectl create namespace devns
kubectl create deployment redis-deploy --image=redis --replicas=2 --namespace=devns

# Output:
namespace/devns created
deployment.apps/redis-deploy created

Tạo Pod và Service cùng lúc chỉ bằng 1 lệnh duy nhất: option --expose=true của kubectl run (mặc định false) tự tạo kèm 1 Service loại ClusterIP, yêu cầu phải có --port:

kubectl run httpd --image=httpd:alpine --port=80 --expose=true

# Output:
service/httpd created
pod/httpd created

Kiểm tra lại, thấy Service httpd có selector: run=httpd: nhãn run được kubectl run tự động gắn cho Pod, và Service tự dùng đúng nhãn đó.

Danh sách lệnh chính​

LệnhCông dụng
kubectl run <name> --image=<image> [--labels=k=v] [--port=<p>] [--expose=true]Tạo Pod (kèm label/port/Service nếu cần)
kubectl create deployment <name> --image=<image> [--replicas=<n>] [-n <ns>]Tạo Deployment, có thể chỉ định replica và namespace ngay
kubectl expose pod/deployment <name> --port=<p> --name=<svc-name>Tạo Service, tự lấy label làm selector
kubectl create namespace <name>Tạo Namespace
kubectl scale deployment <name> --replicas=<n>Scale Deployment
kubectl <cmd> --dry-run=client -o yamlGenerate YAML mà không tạo resource thật
kubectl api-resourcesTra apiVersion/short name của mọi loại resource
kubectl explain <resource> [--recursive]Tra chi tiết field hợp lệ của 1 resource

Tới đây, cả 5 phần Core Concepts (Cluster Architecture, Pods, ReplicaSets/Deployments, Services/Namespaces, và Imperative vs Declarative) đã đủ để bắt đầu tự tin thao tác một Kubernetes cluster hàng ngày. Các phần sau đi sâu vào từng mảng chuyên biệt hơn: Scheduling, Networking, Security, và Storage.


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), 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.