+17

Tạo và sử dụng PersistentVolume và PersistentVolumeClaim trong Kubernetes

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ác
  • Ephemeral 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ó accessModesReadWriteOnce ý 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:

Screenshot 2023-09-24 at 6.35.54 PM.png

Đơ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é:

Screenshot 2023-09-24 at 11.26.43 PM.png

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ái containerPort: 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 ta
  • Network: http://10.244.2.250:3000: đây là địa chỉ app của chúng ta theo format Pod_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é:

Screenshot 2023-09-24 at 11.55.45 PM.png

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é:

Screenshot 2023-09-25 at 12.11.55 AM.png

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 file deployment.yml đoạn containerPort. 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ốn route 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é:

Screenshot 2023-09-25 at 10.35.52 AM.png

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:

Screenshot 2023-09-25 at 11.01.26 AM.png

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:

Screenshot 2023-09-25 at 10.35.52 AM.png

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, storageClassdo-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 😂😂):

Screenshot 2023-09-26 at 4.34.47 PM.png

Ở 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ạo get 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é

Screenshot 2023-11-06 at 12.03.25 PM.png

Ở 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é:

Screenshot 2023-09-26 at 11.40.55 PM.png

Ở 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é:

Screenshot 2023-09-26 at 11.47.34 PM.png

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é:

Screenshot 2023-09-26 at 11.52.31 PM.png

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 Pod securityContext cho Deployment của chúng ta để nó là group 1001 (chính là group nodejs, 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 group 1001(nodejs) vào user nextjs 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 group 1001(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 group nodejs(1001)
  • folder storage sẽ thuộc sở hữu của root:nodejs (volume trên k8s mount từ ngoài vào mặc định nó thuộc user root, caí này không quan trọng, miễn là nó cùng group nodejs với nextjs là đc rồi)
  • Và như vậy thì nextjs sẽ ghi được vào folder storage 😉

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é:

Screenshot 2023-09-27 at 11.22.46 AM.png

Ở 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é:

Screenshot 2023-09-27 at 11.26.10 AM.png

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:

Screenshot 2023-09-27 at 11.26.10 AM.png

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à getlist 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 đè đó):

Screenshot 2023-09-27 at 10.20.41 PM.png

Và đây là tới file thứ 7, kết quả rất rõ ràng nhé các bạn 😃:

Screenshot 2023-09-27 at 10.16.08 PM.png

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.ymlapply:

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:

Screenshot 2023-09-28 at 10.34.29 PM.png

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

Viblo
Let's register a Viblo Account to get more interesting posts.