Tạo và sử dụng PersistentVolume và PersistentVolumeClaim trong Kubernetes
Cập nhật gần nhất: 26/10/2024
Hello các bạn lại là mình đây 👋👋
Mấy tháng nay nhìn vào series K8S này lưng chửng chưa hoàn thiện lúc cũng thấy cân cấn, mà chưa có thời gian tập trung vào viết bài. Đợt này lại quyết tâm lên dây cót tinh thần, quyết tâm đưa ra 1 cái routine đều đặn viết bài, vừa là để giữ nhịp ra bài cho series này, vừa là để giữ cảm hứng sáng tác, 🤣🤣
Hôm nay chúng ta cùng nhau tìm hiểu về PersistentVolume và PersistentVolumeClaim trong K8S để lưu trữ lại data nhé.
Triển thôi 🚀🚀
Lấy K8S Session
Vẫn là bước quen thuộc, trước khi vào bài này các bạn lấy cho mình K8S session để lát nữa ta làm thực hành nhé. Xem lại bài cũ giúp mình nhé
Âu cây bắt đầu hoy
Review lại series Docker chút
Ở series học Docker của mình, hầu như khi phải deploy một app hoàn chỉnh giống như thật, thì ta luôn phải mount 1 hoặc 1 vài volume
nào đó vào container để lưu trữ lại data.
Các bạn tưởng tượng image
giống như 1 Class trong Java, container
giống như 1 object được khởi tạo từ class đó, mỗi container
là một môi trường độc lập, ở mỗi lần mà ta khởi tạo nó ở bất kì đâu. Do vậy nếu container của ta có sinh ra data nào trong lúc chạy, kiểu logs/files.... thì sau khi khởi động lại thì mọi thứ sẽ bị xoá sạch bong yêu lại từ đầu 😄
Vậy nên ta cần phải tạo ra volume
để lưu lại data giữa các lần khởi tạo container, ở đó volume có thể là Docker volume
(volume được quản lý bởi Docker) hoặc local volume
(volume do ta quản lý, ta định nghĩa folder nào là volume và mount nó vào container).
K8S Volume
Điều tương tự xảy ra khi ta làm việc với K8S, mỗi khi mà container của chúng ta crash,stop hoặc có lỗi gì đó với Pod, thì kubelet
sẽ restart lại container hoặc reschedule lại Pod mới và mọi thứ lại bị reset sạch bong. Do vậy ta cần phải dùng tới Volume để có thể lưu lại data.
Có 2 loại mà ta hay sử dụng nhất:
PersistentVolume
: volume tồn tại kể cả khi pod bị destroy, có thể được mount sang pod khácEphemeral Volume
: volume ăn theo 1 Pod, khi Pod restart thì vẫn còn đó, nhưng nếu Pod destroy thì volume này cũng bay màu luôn
Ở bài này ta sẽ tập trung vào PersistentVolume
nhé
Lý thuyết vỡ lòng
Muốn vô thực hành lắm rồi nhưng vẫn phải rượt qua lý thuyết tí đã nhé các bạn 🤣
- PersistentVolume: là 1 phần storage mà được cấp bởi admin, hoặc là được cấp động (dynamic) bằng StorageClass. PersistentVolume độc lập với vòng đời của Pod, ý là Pod có bị destroy thì PV vẫn còn đó (hoặc có thể bị delete theo tuỳ ta cấu hình).
- PersistentVolumeClaim: là 1 request để "xin" được sử dụng storage. Pod muốn sử dụng volume thì cần phải có Claim.
Từ giờ trở đi xuyên suốt series ta sẽ gọi tắt PersistentVolume là PV
và PersistentVolumeClaim là PVC
, cái này cộng đồng cũng gọi vậy cho tiện chứ không phải mình tự quy ước đâu nhé 🤣 (K8S các command họ cũng viết tắt là thế luôn kubectl get pvc, get pv
).
1 PVC thì có thể được sử dụng bởi 1 hoặc nhiều Node tuỳ chúng ta cấu hình, và nó có thể được dùng để read
hoặc write
hoặc cả read and write
.
Chú ý quan trọng là 1 PV sẽ chỉ được bound bởi duy nhất 1 PVC nhé các bạn, do vậy ta sẽ không tạo được 2 PersistentVolumeClaim mà Bound vào 1 PersistentVolume đâu
Ví dụ:
# PV
apiVersion: v1
kind: PersistentVolume
metadata:
name: block-pv
spec:
capacity:
storage: 10Gi # tối đa 10GB
accessModes:
- ReadWriteOnce
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: block-pvc
spec:
accessModes:
- ReadWriteOnce # read + write nhưng chỉ dành cho 1 Node trên cluster
resources:
requests:
storage: 10Gi # "xin" hết 10 GB
Như ở trên ta có 1 PV và PVC có accessModes
là ReadWriteOnce
ý bảo là khi sử dụng thì chỉ có 1 node được dùng PV/PVC này. Và ở trên các bạn thấy là PVC "xin" hết cả 10GB của PV, chú ý rằng PVC không được xin quá lượng storage của PV mà ta định nghĩa.
Ở trên mình có nói tới StorageClass, thì Volume của chúng ta có thể được cấp động thông qua StorageClass, hầu như mình thấy thì chúng ta ít khi phải dùng tới StorageClass, nhưng có một số trường hợp ta muốn dùng các loại storage khác nhau cho các PV khác nhau, ví dụ: PV 1 cần dùng storage xịn (premium) để tốc độ read/write nhanh hơn, PV 2 thì storage lởm lởm cũng được, các cloud provider (GCP, AWS, Azure... họ cũng có những loại storage định sẵn cho chúng ta nữa). Mình mới phải đụng đến cái này 1 lần ở 1 project khi mà mình phải tự chạy MongoDB trên K8S, và muốn tốc độ đọc ghi data nhanh hơn thì mình dùng loại premium. Còn lại thì hầu như mình không định nghĩa gì về StorageClass thì K8S dùng loại mặc định của Cloud provider người ta định nghĩa sẵn, ở đây ta đang dùng Digital Ocean thì nó là dạng disk thông thường
Oke hòm hòm lý thuyết vậy, ta zô thực hành để xem đầu đuôi nó thế nào nhỉ 😎
Chạy Demo app
Ở bài này ta sẽ có 1 demo app, cho phép upload file, hiển thị các file đã upload và có thể xoá file vừa upload:
Đơn giản không có gì mấy 🤣
Ta thử deploy và chạy app này lên nhé. Ở bài này các bạn tạo cho mình một folder để ta làm việc đặt tên là viblo-k8s-pv-demo
nhé.
Đầu tiên các bạn tạo cho mình file deployment.yml
(ko cần nói các bạn đoán đc ta sẽ có gì ở đây rồi đúng ko ):
apiVersion: apps/v1
kind: Deployment
metadata:
name: demoapp
labels:
app.kubernetes.io/name: viblo-pv-demo
spec:
selector:
matchLabels:
app: demoapp
template:
metadata:
labels:
app: demoapp
spec:
containers:
- name: demoapp
image: maitrungduc1410/viblo-k8s-pv-demo:latest
ports:
- containerPort: 3000
name: http
resources:
requests:
memory: "128Mi"
cpu: "64m"
limits:
memory: "750Mi"
cpu: "500m"
Ở trên ta có 1 deployment, khi apply
sẽ tạo 1 ra 1 pod, trong pod đó có 1 container tên là demoapp
, bên cạnh đó ta có 1 số cấu hình như là containerPort
hay yêu cầu resources
tối thiểu và tối đa cho container của chúng ta, phần này nếu các bạn chưa rõ thì xem lại bài về Deloyment của mình nhé.
Có 1 chú ý nhỏ là ở trên, mình đặt
labels
cho deployment của mình làapp.kubernetes.io/name: viblo-pv-demo
, cái này các bạn có thể đặt là bất kì giá trị nào, nhưng mình follow theo cách khuyên dùng từ Kubernetes nhé
Tiếp đó, chúng ta để luôn file k8s session chung vào folder viblo-k8s-pv-demo
nhé:
Oke rồi chúng ta apply
để tạo deployment này nhé:
kubectl apply -f deployment.yml --kubeconfig=kubernetes-config
Nếu thấy báo deployment.apps/demoapp created
là ngon rồi.
Ta thử get deploy
xem nhé:
kubectl get deploy --kubeconfig=kubernetes-config
------
NAME READY UP-TO-DATE AVAILABLE AGE
demoapp 1/1 1 1 114s
Ở trên ta thấy deployment của chúng ta READY rồi, check xem pod của nó thế nào nhé:
kubectl get pod --kubeconfig=kubernetes-config
------
NAME READY STATUS RESTARTS AGE
demoapp-75497867c-dgmkq 1/1 Running 0 4m21s
Pod của chúng ta cũng RUNNING rồi, 1/1
container trong pod đã READY.
Tiếp đó ta check log xem container của chúng ta có gì nhé (thay tên pod của các bạn vào cho đúng nhé):
kubectl logs demoapp-75497867c-dgmkq --kubeconfig=kubernetes-config
------
▲ Next.js 13.5.2
- Local: http://demoapp-75497867c-dgmkq:3000
- Network: http://10.244.2.250:3000
✓ Ready in 405ms
Oke ngon nghẻ rồi.
À giải thích 1 chút về 2 dòng logs ta có:
Local: http://demoapp-75497867c-dgmkq:3000
: đây là địa chỉ local app của chúng ta, nó sẽ lấy bằng tên của pod và cổng mà app của chúng ta chạy, chú ý rằng cổng 3000 này là từ trong app của chúng ta chứ không phải cáicontainerPort: 3000
ở file deployment nhé. Ở ví dụ bài này thì mình định nghĩa ở đây, cái này khi làm thật nếu ta không rõ ràng port nào ra port nào là mệt đấy 🤣.containerPort
là cái mà ta muốn expose ra trong K8S cluster, cái này thường sẽ phải match với port mà ta định nghĩa trong app của chúng taNetwork: http://10.244.2.250:3000
: đây là địa chỉ app của chúng ta theo formatPod_ClusterIP:<cổng của app>
, ở đây pod của chúng ta cóClusterIP=10.244.2.250
Ủa check pod IP dư lào quên rồi 😅
Để xem thông tin về Pod ta chạy command sau (thay tên pod của các bạn vào cho đúng nhé):
kubectl describe pod demoapp-75497867c-dgmkq --kubeconfig=kubernetes-config
Các bạn sẽ thấy như sau, dòng mình highlight bên dưới là IP của Pod nhé:
Ta thử "chui" vào container vào và thử curl
xem có trả về nội dung webapp của chúng ta không nhé:
kubectl exec -it demoapp-75497867c-dgmkq --kubeconfig=kubernetes-config -- sh
Ở trên các bạn thấy rằng ta không cần chỉ đích danh là ta muốn "chui" vào container nào trong pod bởi vì pod của ta có mỗi 1 container, trường hợp ta có nhiều hơn 1 container trong pod thì ta có thể thêm option
-c <container_name>
để chỉ định là ta muốn chui vào đâu nhé .kubectl exec -it demoapp-75497867c-dgmkq -c my_container.....
Sau khi đã vào trong container thì ta chạy curl
thử xem nhé:
curl http://demoapp-75497867c-dgmkq:3000
Ở đây ta dùng địa chỉ Local
mà mình đã nói khi nãy nhé (vì ta đang ở trong chính container rồi, các bạn cũng có thể dùng địa chỉ Network
cũng được, nếu thấy như sau là oke rồi nhé:
Oke rồi, CTRL + D để thoát khỏi container ra ngoài thôi.
Tiếp đó, để app của chúng ta có thể truy cập được công khai (public) từ trình duyệt thì ta sẽ cần phải có service, và type là LoadBalancer nhé các bạn, lí do là vì ta cần 1 public IP có thể truy cập được từ bên ngoài, phổ biến, tiện và (thường) hợp lý nhất thì sẽ là LoadBalancer, các bạn có thể xem lại bài về Service của mình nhé.
Các bạn tạo cho mình file svc.yml
(bên K8S người ta hay viết tắt service=svc ) với nội dung như sau:
apiVersion: v1
kind: Service
metadata:
name: demoapp
spec:
type: LoadBalancer
ports:
- name: http # tên port của service
protocol: TCP
port: 3001
targetPort: http # tên port của container bên file deployment
selector:
app: demoapp
- Ở trên ta có 1 service tên là
demoapp
(đặt là gì cũng được, ở đây mình đặt giống với tên deployment) - Service này có 1 port ta đặt tên là
http
, cổng của service này là 3001, sẽ target vào port tên làhttp
mà ta định nghĩa bên filedeployment.yml
đoạncontainerPort
. Việc đặt tên cho service làhttp
thì sau này khi ta cần dùng tới nó, ví dụ ở bài ingress thì sẽ tốt hơn thay vì dùng số - Dưới cùng ta có
selector
để chỉ định rằng ta muốnroute
traffic vào pod mà có labels ta mong muốn
Tiếp theo ta apply
service này nhé:
kubectl apply -f svc.yml --kubeconfig=kubernetes-config
------
service/demoapp configured
Sau khi apply
ta get
tất cả các service trong namespace xem có gì nhé:
kubectl get svc --kubeconfig=kubernetes-config
------
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
demoapp LoadBalancer 10.245.112.177 <pending> 3001:30917/TCP 4s
Ở trên các bạn thấy rằng service của chúng ta đã lên, và EXTERNAL-IP đang Pending
vì sẽ mất vài phút để cloud provider (ở đây là Digital ocean) tạo Load balancer cho service này.
Đợi một chút làm cốc cafe rồi get
lại service ta sẽ thấy có EXTERNAL-IP nhé:
kubectl get svc --kubeconfig=kubernetes-config
------
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
demoapp LoadBalancer 10.245.112.177 146.190.202.195 3001:30917/TCP 3m52s
Khi đã có EXTERNAL-IP thì ta có thể truy cập được app rồi, ta mở trình duyệt ở địa chỉ EXTERNAL-IP:3001
nhé:
Pòm pòm chíu chíu 🎆🎆🎆🎆💪💪💪💪
Tiếp đó chúng ta thử upload vài file lên xem nhé, các bạn có thể upload bất kì file nào, limit là 200MB 1 file, sau khi upload thì file sẽ hiển thị ở table bên dưới:
Ta F5 thoải mái file đã được lưu trên server.
Giờ ta thử restart lại deployment để tạo lại pod mới xem nhé:
kubectl rollout restart deploy demoapp --kubeconfig=kubernetes-config
------
deployment.apps/demoapp restarted
Chờ 1 chút để pod mới được tạo, sau đó ta quay lại trình duyệt F5:
Ui bỏ mịa, mất sạch file 🫣🫣, chắc chắn do cá mập cắn cáp quang 🦈🦈🦈
Tạo PV và PVC
Ở đây ta muốn rằng mỗi khi mà container của chúng ta khởi tạo (do lỗi container, lỗi Pod, restart deployment,....) thì data của chúng ta vẫn phải còn ở đó, chứ production mà mất data thì xong phim 😂😂
Và như đầu bài ta đã nói thì ta sẽ dùng PersistentVolume (PV) và PersistentVolumeClaim (PVC) cho việc này nhé.
Đầu tiên các bạn tạo cho mình file tên là pvc.yml
cho PersistentVolumeClaim:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: demo-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: "2Gi"
Sau đó ta tạo PersistentVolumeClaim này nhé:
kubectl apply -f pvc.yml --kubeconfig=kubernetes-config
------
persistentvolumeclaim/demo-pvc created
Sau đó ta thử get
PersistentVolumeClaim mà ta vừa tạo xem nhé:
kubectl get pvc --kubeconfig=kubernetes-config
------
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
demo-pvc Bound pvc-93715847-269c-40bd-9407-7a11a61514a4 2Gi RWO do-block-storage 61s
Như ở trên các bạn thấy là PVC demo-pvc
của ta đã được tạo thành công và "ăn"(bound) vào PV pvc-93715847-269c-40bd-9407-7a11a61514a4
, storageClass
là do-block-storage
, đây là storageClass mặc định mà DigitalOcean tạo sẵn và manage (quản lý) thay chúng ta.
Nếu các bạn dùng K8S ở cloud khác (AWS, GCP, Azure,...) sẽ có thể có các storageClass khác nhé
Ôi dừngggggggggggggggggg!!!!!!!!!!!!!!!!!!!!!!!!! 😲😲😲
Ủa, đầu bài như ta đã nói, để có thể tạo được PVC thì ta cần có:
- storage class: cái này ở đây thì Digital Ocean tạo sẵn cho ta rồi
- PV: cái này ta đã tạo đâu????? từ đâu nó lại tự tạo sẵn cho ta cái PV tên là
pvc-93715847-269c-40bd-9407-7a11a61514a4
vậy nhỉ???????
Thì đây là mặc định của cloud provider các bạn à, thường khi ta dùng các provider lớn, họ sẽ cung cấp sẵn PV được provision (được họ cung cấp và quản lý thay ta), bởi vì những phần cấu hình đó nhiều khi không dễ nên họ sẽ "abstract" phần đó luôn cho ta.
Ở đây khi ta tạo 1 PVC thì Digital Ocean sẽ tương ứng tạo cho ta 1 PV với Capacity tương ứng (2Gi), storage class là do-block-storage
, đây là 1 cái Volume ở trên DigitalOcean (nó là disk như ta vẫn biết)
Khi ta xem ở trên DigitalOcean nó sẽ như sau (đương nhiên các bạn ko xem được, mình xem được thôi 😂😂):
Ở trên các bạn thấy là volume này chưa được mount vào Pod nào cả nên vẫn có nút
Attach to Droplet
kia, Droplet là 1 Node, Pod thì ở trong Node
Giờ ta quay lại terminal và thử get PV xem nhé:
kubectl get pv pvc-93715847-269c-40bd-9407-7a11a61514a4 --kubeconfig=kubernetes-config
------
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
pvc-93715847-269c-40bd-9407-7a11a61514a4 2Gi RWO Delete Bound learnk8s-8174d9/demo-pvc do-block-storage 22m
Ở trên các bạn sẽ thấy PV của các bạn nhé
Nhưng chú ý là, vì PV là cluster-object, tức là nó không thuộc về namespace (khác với PVC, PVC thuộc về 1 namespace), do vậy khi các bạn get pv
thì nếu có quyền nó sẽ show toàn bộ pv có trong cluster, cả PV của người khác, nhưng ở đây các bạn không có quyền đó 🤣🤣, và với các bạn không delete được đâu vì mình đã limit permission rồi, chỉ xem được thôi .
Các bạn không có quyền
get pv
(get tất cả PV) nhưng có quyền get PV do các bạn tạoget pv <PV_ID>
Èoooooooo, bài nói về PersistentVolume và PersistentVolumeClaim, ấy xong tạo được mỗi PVC, PV có tự tạo được đâu 😒😒
Nếu các bạn vẫn tò mò thì ta thử xem có gì trong cấu hình của 1 PV có gì mà nãy giờ mình cứ bảo khoai vậy nhỉ
kubectl get pv pvc-93715847-269c-40bd-9407-7a11a61514a4 -o yaml --kubeconfig=kubernetes-
config
nhớ thay tên PV của các bạn vào cho đúng nhé
Ở trên các bạn thấy là ngoài một số thông số mặc định mà object nào cũng có (uid, resourceVersion, creationTimestamp...) thì để cấu hình một PV nhiều thứ cách rách vãi cả chưởng , và nó còn có các thông số phụ thuộc vào từng cloud provider nữa, đó là lí do vì sao các cloud provider họ thường "abstract" sẵn cho ta luôn.
Mount PVC vào Pod
Giờ ta sẽ tiến hành mount PVC vào Pod và file upload lên app của chúng ta sẽ được lưu vào PVC này nhé.
Các bạn mở cho mình file deployment.yml
và sửa như sau nhé:
apiVersion: apps/v1
kind: Deployment
metadata:
name: demoapp
labels:
app.kubernetes.io/name: viblo-pv-demo
spec:
selector:
matchLabels:
app: demoapp
template:
metadata:
labels:
app: demoapp
spec:
containers:
- name: demoapp
image: maitrungduc1410/viblo-k8s-pv-demo:latest
ports:
- containerPort: 3000
name: http
resources:
requests:
memory: "128Mi"
cpu: "64m"
limits:
memory: "750Mi"
cpu: "500m"
volumeMounts:
- name: data
mountPath: /app/storage
# readOnly: true # nếu như ta muốn chỉ cho phép read ko write
volumes:
- name: data
persistentVolumeClaim:
claimName: demo-pvc
# readOnly: true # override readOnly ở volumeMounts
Ở trên các bạn thấy là mình đã thêm vào volumes
(ở dưới cùng của file), ta đặt tên volume này là data
(các bạn thích đặt là gì cũng được nhé), và volumeSource
(nguồn gốc của volume này) ta lấy từ PVC với tên là demo-pvc
Sau đó ở bên trong phần cấu hình cho container demoapp
ta thêm volumeMounts
, chỉ định tên của volume mà ta muốn mount vào container, ở đây tên là data
(mà ta vừa định nghĩa ở mục volumes
), tiếp theo ta chỉ định đường dẫn nào mà ta muốn mount với mountPath
Ta cũng có thể chỉ định rằng volume này chỉ được đọc bằng cách thêm readOnly: true
, ở phần cấu hình cho persistentVolumeClaim
ta cũng có thể thêm readOnly: true
, nó sẽ ghi đè lên readOnly
ở phần volumeMounts
(nếu có)
Oke rồi giờ ta apply
lại deployment này nhé:
kubectl apply -f deployment.yml --kubeconfig=kubernetes-config
Chờ một lúc cho Pod mới được tạo lại, sau đó ta quay lại trình duyệt F5 nhé:
Ở trên các bạn thấy rằng app chúng ta vẫn chạy bình thường nhưng tự nhiên có 1 cái tên là lost+found
, ủa gì z trời??????🤔🤔🤔
Thì đây là một folder mặc định được sinh ra khi ta mount volume vào container thôi các bạn à, cũng không có gì đặc biệt mấy đâu, nó giống như lúc ta upgrade MacOS và sau khi upgrade thì ta cũng có thể có folder lost+found
đó nếu như có 1 vài file mà MacOS nó không biết nên để vào đâu.
Âu cây rồi, giờ ta thử upload file mới lên xem để kiểm tra rằng app của chúng ta vẫn chạy ổn áp không nhé:
Bùm 🧨🧨🧨, lỗi 500????? Ủa ban đầu nãy vẫn chạy ngon mà nhỉ? Mount được cái volume vào là hỏng luôn? Kiểu này khả năng liên quan tới permission các thứ rồi 🤔🤔
Ta thử xem logs container của ta có gì nhé:
kubectl logs demoapp-ff5cb7fd-7vwfl --kubeconfig=kubernetes-config
Thay tên Pod của bạn vào cho đúng nhé
[Error: EACCES: permission denied, open '/app/storage/ecea77d730bb21afb0472d001'] {
errno: -13,
code: 'EACCES',
syscall: 'open',
path: '/app/storage/ecea77d730bb21afb0472d001'
}
Ều biết ngay lỗi permission mà 😪😪😪
Giờ ta sẽ "chui" vào container xem permission của files/folders trong app của chúng ta như thế nào nhé (Thay tên Pod của bạn vào cho đúng nhé):
kubectl exec -it demoapp-ff5cb7fd-7vwfl --kubeconfig=kubernetes-config -- sh
Sau khi vào trong container các bạn chạy ls -la
để xem tất cả các files/folders ta có trong app nhé:
Ta quay trở lại bên trên đoạn lỗi in ra, lỗi đó NodeJS nói là "không có quyền được mở file ecea77d730bb21afb0472d001
bên trong đường dẫn /app/storage
" để ghi. Và xem ảnh mà ta vừa ls -la
thì ta thấy rằng folder storage
đang thuộc sở hữu của root:root
(user=root, group=root)
Giờ ta thử check xem user hiện tại là gì nhé, ta đơn giản là gõ id
(Enter):
id
------
uid=1001(nextjs) gid=65533(nogroup) groups=65533(nogroup)
Ở trên các bạn thấy rằng user của ta là nextjs
có userid=1001, primary groupid=65533 (group chính, vì 1 user có thể thuộc về nhiều group), trong khi folder storage
thuộc sở hữu của root:root (uid=0,gid=0)
(ta có thể xem kĩ hơn khi chạy command cat /etc/passwd
)
Qua đây ta thấy rõ ràng rằng user nextjs
không có quyền ghi vào folder storage
. Vậy thì giờ ta chỉ cần đổi quyền của storage
về thuộc sở hữu của nextjs:nogroup
là được rồi
Ấy vậy mà không giống như khi ta dùng Docker hay Docker Compose, khi đó ta có thể exec
lại vào container với option -u root
(chui vào container với vai root
), rồi đổi quyền của folder ta muốn. Bây giờ ta đang dùng Kubernetes và ta không thể làm thế được 😭😭😭😭
Thứ nữa là vì user của ta không thuộc group nào cả nên mặc định nó được gán là nogroup
. Và nếu các bạn xem ở Dockerfile của mình, thì mình có tạo ra 1 group tên là nodejs
nhưng không gán nó vào cho user nextjs
ở Dockerfile nhé, mình chỉ dùng group nodejs
để đặt permission cho các file và folder trong đó thôi.
Vậy bây giờ thứ ta muốn là gì????? Nãy giờ nói luyên thuyên hoài 😏😏😏
Đương nhiên là thứ ta muốn là nextjs
có quyền ghi vào folder storage
rồi😂😂
Oke vậy bằng cách nào????? lòng và lòng vòng 😡
Với Kubernetes thì ta sẽ dùng Pod securityContext
để làm điều đó nhé, cụ thể như sau:
- Ta chỉ cần set
fsGroup
của PodsecurityContext
cho Deployment của chúng ta để nó là group1001
(chính là groupnodejs
, các bạn có thể xem đoạn mình tạo group này trong Dockerfile) fsGroup
này nó là 1 group "bổ trợ" đặc biệt, được thêm vào tất cả các containers chạy trong 1 Pod, cụ thể là trường hợp này thì K8S sẽ thêm group1001
(nodejs) vào usernextjs
cho ta- Đồng thời K8S sẽ đổi toàn bộ files/folders có trong đường dẫn mà ta đang mount, ở đây là
storage
về thuộc sở hữu của group1001
(nodejs), việc này nhằm đảm bảo các containers bên trong pod có đủ quyền để truy cập (đọc/ghi) vào volume
Và sau đó kết quả ta sẽ có là gì:
- user
nextjs
sẽ thuộc groupnodejs
(1001) - folder
storage
sẽ thuộc sở hữu củaroot:nodejs
(volume trên k8s mount từ ngoài vào mặc định nó thuộc userroot
, caí này không quan trọng, miễn là nó cùng groupnodejs
vớinextjs
là đc rồi) - Và như vậy thì
nextjs
sẽ ghi được vào folderstorage
Oke tạm tạm hiểu là vậy, nói dài qúaaaaaa, giờ thực hành cái coi.
À trước khi thực hành thì các bạn nhờ là bên trên khi chạy id
trong container thì user nextjs
của ta như sau nhé:
uid=1001(nextjs) gid=65533(nogroup) groups=65533(nogroup)
Tiếp theo bạn sửa lại file deployment.yml
như sau:
apiVersion: apps/v1
kind: Deployment
metadata:
name: demoapp
labels:
app.kubernetes.io/name: viblo-pv-demo
spec:
selector:
matchLabels:
app: demoapp
template:
metadata:
labels:
app: demoapp
spec:
securityContext: # --> ở đây
fsGroup: 1001 # --> và đây
containers:
- name: demoapp
image: maitrungduc1410/viblo-k8s-pv-demo:latest
ports:
- containerPort: 3000
name: http
resources:
requests:
memory: "128Mi"
cpu: "64m"
limits:
memory: "750Mi"
cpu: "500m"
volumeMounts:
- name: data
mountPath: /app/storage
# readOnly: true
volumes:
- name: data
persistentVolumeClaim:
claimName: demo-pvc
# readOnly: true # nếu như ta muốn chỉ cho phép read ko write
Ở trên, ta thêm vào đúng 2 dòng đoạn securityContext
, mình gọi đây là Pod securityContext
bởi vì nó được áp dụng cho tất cả các container trong pod, chúng ta cũng có cả container securityContext
cũng xêm xêm nhưng là áp dụng cho từng container
Việc dùng
securityContext
này khá là hữu ích khi ta chạy app ở production để tăng thêm tính bảo mật cho pod/container của ta trên K8S Cluster, hầu như mình luôn dùng nó, cái này ta sẽ nói kĩ thêm ở các bài sau nhé
Giờ ta apply
lại deployment nhé:
kubectl apply -f deployment.yml --kubeconfig=kubernetes-config
Sau khi apply
các bạn chờ tẹo cho Pod mới được tạo, sau đó ta hãy khoan sử dụng app để upload file, mà "chui" vô container xem tí đã nhé:
kubectl exec -it demoapp-7b95bd5d66-f86qd --kubeconfig=kubernetes-config -- sh
Thay tên pod của các bạn vào cho đúng nhé
Sau khi vào bên trong thì các bạn chạy command ls -la
để xem files/folders nhé:
Ở trên ta thấy rằng folder storage
đã thuộc sở hữu của root:nodejs
Tiếp theo ta check xem user hiện ta như thế nào nhé, các bạn chạy id
:
/app $ id
uid=1001(nextjs) gid=65533(nogroup) groups=1001(nodejs),65533(nogroup)
Đó, giờ ta thấy là nextjs
đã thuộc về thêm 1 group là 1001
(tên là nodejs
Điều đó có nghĩa là nextjs
sẽ có quyền ghi vào folder storage
rồi 😎😎😎
Ta thoát ra ngoài (CTRL+D) và quay trở lại trình duyệt F5 và thử upload file nhé:
Yeahhh, upload ngon nghẻ rồi. Giờ ta thử restart deployment để Pod mới được tạo ra xem files của ta còn không nhé
kubectl rollout restart deploy demoapp --kubeconfig=kubernetes-config
------
deployment.apps/demoapp restarted
Chờ 1 tẹo cho Pod mới được tạo (các bạn get pod
xem trạng thái pod mới như thế nào nhé), sau đó quay lại trình duyệt và F5:
Yayy, files vẫn còn 🥳🥳🥳🥳🥳Pằng pằng chíu chíu
Kể ra nó cũng không khó và không khác lắm so với việc ta vẫn mount volume khi làm việc với Docker nhỉ ? Ta vọc thêm chút nhé
RECLAIM POLICY
Ta thử get
persistent volume xem nhé:
kubectl get pv --kubeconfig=kubernetes-config
------
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
pvc-1f23bfc0-5d62-432c-8706-84bc72b1680f 2Gi RWO Delete Bound learnk8s-bde84e/demo-pvc do-block-storage 24m
Ở bên trên có 1 cột tên là RECLAIM POLICY
và nó đang show là Delete
, ý ở đây là PV này sẽ bị xoá cùng với PVC khi ta thực hiện xoá PVC
RECLAIM POLICY
có thể có 3 giá trị sau:
- Delete: xoá PV cùng với PVC khi PVC bị xoá
- Retain: xoá PVC, PV vẫn còn. Trường hợp này thường dùng khi data của ta quan trọng, ví dụ database (mysql, mongodb...)
- Recycle: deprecated (lỗi thời), nhưng vẫn được support ở 1 vài phiên bản K8S, recycle nói với K8S thực hiện 1 vài thao tác cơ bản để dọn dẹp volume và chuẩn bị cho lầm claim sau, nhưng việc này không đảm bảo chắc chắn toàn bộ data bị dọn sạch và có thể vẫn được recover bởi những người có ý đồ ác (nghe rùng rợn ghê ha 🤣🤣). Hiện nay thì ta nên dùng
Delete
thay vìRecycle
, Delete sẽ đảm bảo Volume bị xoá vĩnh viễn
Với volume được "provision" (như ta đang dùng volume của DigitalOcean) thì mặc định policy là
Delete
Để thay đổi Reclaim policy cho PV thì ta làm như sau (thay tên PV của các bạn vào cho đúng nhé):
kubectl patch pv pvc-1f23bfc0-5d62-432c-8706-84bc72b1680f -p '{"spec":{"persistentVolumeReclaimPolicy":"Retain"}}' --kubeconfig=kubernetes-config
Pùm, lỗi:
Error from server (Forbidden): persistentvolumes "pvc-1f23bfc0-5d62-432c-8706-84bc72b1680f" is forbidden: User "learnk8s-bde84e" cannot patch resource "persistentvolumes" in API group "" at the cluster scope
Lỗi bảo là ta không có quyền được patch
sửa volume, à đúng rồi mà , như ban đầu mình nói là vì PV là cluster-objects (kiểu global, chung, không thuộc về namespace nào), nên mình chỉ để quyền cho các bạn là get
và list
thôi 😁😁
Readonly Volume
Trong trường hợp ta mount volume vào và muốn "force" (bắt, ép buộc) chỉ cho đọc chứ không cho ghi vào volume thì ta làm như sau:
apiVersion: apps/v1
kind: Deployment
metadata:
name: demoapp
labels:
app.kubernetes.io/name: viblo-pv-demo
spec:
selector:
matchLabels:
app: demoapp
template:
metadata:
labels:
app: demoapp
spec:
securityContext:
fsGroup: 1001
containers:
- name: demoapp
image: maitrungduc1410/viblo-k8s-pv-demo:latest
ports:
- containerPort: 3000
name: http
resources:
requests:
memory: "128Mi"
cpu: "64m"
limits:
memory: "750Mi"
cpu: "500m"
volumeMounts:
- name: data
mountPath: /app/storage
readOnly: true # ---->> thêm vào đây
volumes:
- name: data
persistentVolumeClaim:
claimName: demo-pvc
# readOnly: true # nếu như ta muốn chỉ cho phép read ko write
Sau đó ta apply lại deployment này nhé:
kubectl apply -f deployment.yml --kubeconfig=kubernetes-config
Chờ tẹo cho Pod mới được tạo, quay trở lại trình duyệt, F5, thử upload file mới lên ta sẽ thấy lỗi, kiểm tra logs của container ta sẽ thấy như sau:
kubectl logs demoapp-745f777bff-2g6d4 -n learnk8s-bde84e --kubeconfig=kubernetes-config
------
▲ Next.js 13.5.2
- Local: http://demoapp-745f777bff-2g6d4:3000
- Network: http://10.244.2.252:3000
✓ Ready in 396ms
[Error: EROFS: read-only file system, open '/app/storage/a7205fe6eb0105240ed478500'] {
errno: -30,
code: 'EROFS',
syscall: 'open',
path: '/app/storage/a7205fe6eb0105240ed478500'
}
Ở trên ta thấy rằng volume hiện tại là read-only
và không thể ghi vào được
Các câu hỏi thường gặp
Nếu lưu lượng data vượt quá capacity PVC?
Ở trong bài này PVC của ta có capacity là 2Gi, vậy nếu ta upload file lên và làm cho folder storage
vượt quá 2Gi thì sao?
Ta cùng xem nhé
Để dễ demo thì ta sẽ xoá PVC đi tạo lại 1 cái mới với capacity nhỏ hơn cho dễ demo nhé, ta sẽ lấy 1Gi.
Đầu tiên ta sẽ xoá deployment và PVC hiện tại đã:
kubectl delete -f deployment.yml --kubeconfig=kubernetes-config
kubectl delete -f pvc.yml --kubeconfig=kubernetes-config
Sau đó ở file pvc.yml
ta sửa lại:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: demo-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: "1Gi" # ---> sửa ở đây
Ở trên ta set về 1Gi vì trên Digital Ocean K8S thì 1 PVC tối thiểu phải là 1Gi.
Tiếp theo các bạn mở file deployment.yml
và xoá chỗ có readOnly
đi nhé, vì giờ ta cần ghi mà
Sau đó ta apply
PVC và deployment nhé:
kubectl apply -f pvc.yml --kubeconfig=kubernetes-config
kubectl apply -f deployment.yml --kubeconfig=kubernetes-config
Okay rồi sau khi app chạy lên, các bạn tải video demo này của mình về, file video này 150Mb, tức là upload đến file thứ 7 thì dung lượng folder storage
sẽ nhiều hơn capacity 1Gi của PVC.
Ở hình dưới upload 6 file lên ta thấy vẫn bình thường (mỗi lần các bạn nhớ thay tên đi không là nó ghi đè đó):
Và đây là tới file thứ 7, kết quả rất rõ ràng nhé các bạn :
App đang chạy có xoá được PVC hay không?
Ta thử xem nhé :
kubectl delete pvc demo-pvc --kubeconfig=kubernetes-config
------
persistentvolumeclaim "demo-pvc" deleted
Message pvc deleted
nhưng terminal bị treo luôn, app vẫn chạy bình thường, PV không sao cả (vì retain policy là Delete
)
Từ đây, câu trả lời là: PVC sẽ không bị xoá nếu vẫn còn đang được sử dụng bởi bất kì Pod nào, kể cả nếu admin có cố gắng xoá PV trong lúc đó thì PVC cũng không bị xoá
Bây giờ ta sẽ xoá deployment nhé, các bạn mở thêm 1 cửa sổ terminal nữa, cứ để cửa sổ kia bị treo, ở cửa sổ mới ta chạy:
kubectl delete -f deployment.yml --kubeconfig=kubernetes-config
------
deployment.apps "demoapp" deleted
Giờ ta quay lại cửa sổ bị treo kia sẽ thấy nó hết treo, PVC và PV bị xoá thành công 👍️
Tăng capacity của PVC?
Giờ nếu ta đã tạo PVC với capacity là 2Gi, và ta muốn tăng lên 4Gi có được không?
Ta lại thử xem nha ,nhưng trước tiên ta xoá deployment và pvc hiện tại đi đã:
kubectl delete -f deployment.yml --kubeconfig=kubernetes-config
kubectl delete -f pvc.yml --kubeconfig=kubernetes-config
Sau đó ta vẫn giữ capacity là 1Gi trong file pvc.yml
và apply
:
kubectl apply -f pvc.yml --kubeconfig=kubernetes-config
------
persistentvolumeclaim/demo-pvc created
Check thử PVC xem đã lên chưa nàooo:
kubectl get pvc --kubeconfig=kubernetes-config
------
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
demo-pvc Bound pvc-e382cfe8-82a3-4e08-97bb-58b5c83c9846 1Gi RWO do-block-storage 3s
Oke ngon rồi, giờ ta tăng capacity của PVC lên thành 2Gi:
#pvc.yml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: demo-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: "2Gi" # ---> update ở đây
Sau đó ta apply
:
kubectl apply -f pvc.yml --kubeconfig=kubernetes-config
------
persistentvolumeclaim/demo-pvc configured
Ta lại tiếp tục get pvc
xem nó đã được cập nhật chưa nhé:
kubectl get pvc --kubeconfig=kubernetes-config
------
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
demo-pvc Bound pvc-e382cfe8-82a3-4e08-97bb-58b5c83c9846 1Gi RWO do-block-storage 2m21s
Ủa gì z ?????? Vẫn là 1Gi à??? 🧐🧐🧐. Oke thử xem PV thế nào nàoooo:
kubectl get pv --kubeconfig=kubernetes-config
------
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
pvc-e382cfe8-82a3-4e08-97bb-58b5c83c9846 2Gi RWO Delete Bound learnk8s-f0dd2b/demo-pvc do-block-storage 3m10s
Ủa gì z trời???? PV lên 2Gi rồi mà???? là sao ta 🤔🤔🤔
Lạ nhỉ, giờ làm gì tiếp nhỉ???
Thôi thì cứ chạy deployment lên xem đã rồi tính:
kubectl apply -f deployment.yml --kubeconfig=kubernetes-config
Chờ tẹo cho Pod lên, app lên ngon nghẻ rồi thử get pvc
lại xem:
kubectl get pvc --kubeconfig=kubernetes-config
------
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
demo-pvc Bound pvc-e382cfe8-82a3-4e08-97bb-58b5c83c9846 2Gi RWO do-block-storage 8m13s
Ố ồ ô.... update lên 2Gi rồi này 🤔🤔🤔
Lí do ở đây là vì PVC sẽ không resize cho tới khi nó được Bound
vào 1 Pod nào đó, và ngay sau khi Pod của ta lên sóng, thì PVC được bound -> resize từ 1 -> 2Gi
Ở đây ta đang test với trường hợp ta resize PVC trước cả khi ta tạo Deployment. Giờ nếu ta resize khi ta đã có Deployment và app đang chạy ngon thì kịch bản cũng tương tự, ta phải restart Deployment thì PVC mới được update. Các bạn tự thử xem sao nhé
Các bạn chú ý rằng các cloud provider khác nhau thì có thể nó sẽ có sự khác nhau đôi chút, ở đây ta đang dùng DigitalOcean nhé
Giảm capacity của PVC thì data có bị mất?
Giả sử PVC của ta là 2Gi, ta đang chứa khoảng 1.5Gi dữ liệu, vậy giờ nếu ta thử resize xuống 1Gi PVC thì sao? data của ta có bị mất???
Ta lại thử xem nhé
(suốt ngày thử thử, nói luôn đi 😪😪😪-------gắng tí nữa sắp hết bài rồi các bạn ạ )
Đầu tiên ta lại xoá pvc và deployment làm lại từ đầu cho sạch nhé (nhớ phải xoá deployment trước PVC đó):
kubectl delete -f deployment.yml --kubeconfig=kubernetes-config
kubectl delete -f pvc.yml --kubeconfig=kubernetes-config
Sau đó ta để PVC là 2Gi:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: demo-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: "2Gi"
Tiếp theo ta apply
PVC và Deployment:
kubectl apply -f pvc.yml --kubeconfig=kubernetes-config
kubectl apply -f deployment.yml --kubeconfig=kubernetes-config
Và ta sẽ lại dùng file video (150Mi) của mình nhé.
Ở đây ta sẽ upload 8 lần file này lên server (mỗi lần các bạn nhớ thay tên đi không là nó ghi đè nhé), thì như vậy ta sẽ có cỡ 1.5Gi data, PVC 2Gi là đủ, và sau đó ta sẽ resize xuống 1Gi PVC:
Oke đủ 8 file rồi, giờ ta resize PVC xuống 1Gi:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: demo-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: "1Gi"
apply
thôi nào:
kubectl apply -f pvc.yml --kubeconfig=kubernetes-config
------
The PersistentVolumeClaim "demo-pvc" is invalid: spec.resources.requests.storage: Forbidden: field can not be less than previous value
Gòi ta thấy kết quả gòi nhé 🤣🤣, không thể resize xuống nhỏ capacity cũ, tức là kể cả lượng data của ta có ít hơn 1 Gi, nhưng ta cũng không thể resize từ 2Gi xuống 1Gi, điều này nhằm đảm bảo data không bị mất trong quá trình resize
Kết bài
Hi vọng qua bài này các bạn đã biết thêm về PV và PVC trên K8S là gì, thực ra cách dùng nó cũng không có gì khó và khác lắm với việc ta vẫn mount volume khi làm với Docker nhỉ?
Chỉ là những thử râu ria thì hơi nhiều và ta phải để ý khi chạy app trên K8S cho được mượt thôi
Các bạn tự vọc vạch thêm nhé. Hẹn gặp lại các bạn ở các bài sau 👋👋👋
All rights reserved