---
id: kubernetes-volumes
title: "Volume và PersistentVolume"
shortTitle: "Volumes"
track: kubernetes
trackTitle: "Kubernetes"
order: 5
level: core
status: published
estimatedMinutes: 45
prerequisites:
  - kubernetes-rollout
next: kubernetes-config-secrets
reviewedAgainst: 2026-10-15
sources:
  - title: "KodeKloud Engineer — 100 Days of DevOps"
    url: https://engineer.kodekloud.com/practice
  - title: "Volumes"
    url: https://kubernetes.io/docs/concepts/storage/volumes/
  - title: "Persistent Volumes"
    url: https://kubernetes.io/docs/concepts/storage/persistent-volumes/
---

# Volume và PersistentVolume

File trong container mất khi container restart. Hai container trong cùng Pod cũng không thấy thư mục của nhau, trừ khi cùng mount một volume. Volume là thư mục kubelet gắn vào đúng `mountPath`.

## Để làm gì

Bạn khai `volumes` ở Pod và `volumeMounts` ở từng container, phân biệt `emptyDir` (chết cùng Pod) với PVC (sống sau khi Pod thay), và đọc vì sao PVC đứng `Pending`.

## Gồm những gì

- `spec.volumes` và `containers[].volumeMounts`. Tên phải khớp.
- `emptyDir`: scratch, hoặc hai container chung thư mục.
- PV, PVC, `persistentVolumeClaim.claimName`.
- Access mode hay gặp: `ReadWriteOnce`.

`hostPath` chỉ nhắc: gắn thư mục node, Pod sang node khác là mất. Secret/ConfigMap mount ở bài sau. CSI cloud để cluster managed.

## Dùng khi nào

Log sidecar cần đọc file app viết — `emptyDir` chung. MySQL/Redis cần đĩa còn sau khi Pod thay — PVC. Đừng dùng `emptyDir` cho database rồi tưởng reboot node vẫn còn data.

## Kiến thức cốt lõi

Hai chỗ, một tên:

```yaml
# trong container
volumeMounts:
  - name: data
    mountPath: /usr/share/nginx/html
# trong spec Pod
volumes:
  - name: data
    emptyDir: {}
```

`name` trên mount phải có trong `volumes`. Sai tên thì Pod không lên, Events kiểu failed mount. `mountPath` là đường trong container, không phải đường trên máy bạn. Lab "sửa VolumeMounts" gần như luôn là lệch tên hoặc lệch path app đang đọc.

`emptyDir`: kubelet tạo thư mục trên node, sống đến khi Pod bị xóa. Restart container trong cùng Pod thì data còn. Xóa Pod, hết. Hai container mount cùng `name` thì thấy cùng file — đó là shared volume trong một Pod, không phải giữa hai Pod.

Muốn data còn khi Pod thay: PersistentVolume và PersistentVolumeClaim.

- PV: kho trong cluster (admin tạo tĩnh, hoặc StorageClass tạo động).
- PVC: lời xin dung lượng + access mode. Control plane bind một PV khớp.
- Pod không trỏ thẳng PV. Pod trỏ PVC: `persistentVolumeClaim.claimName`.

PVC xin 1Gi mà cluster chỉ có PV 512Mi thì claim đứng `Pending`. Bind một-một. Đang có Pod dùng PVC thì xóa PVC chưa mất ngay — docs gọi Storage Object in Use Protection, status `Terminating`.

Access mode (trên PV/PVC, không phải quyền Unix):

| Mode | Ai mount |
| --- | --- |
| `ReadWriteOnce` | Một node, read-write. Hay gặp nhất |
| `ReadOnlyMany` | Nhiều node, chỉ đọc |
| `ReadWriteMany` | Nhiều node, đọc ghi — cần loại đĩa hỗ trợ |
| `ReadWriteOncePod` | Đúng một Pod |

Lab tĩnh thường có sẵn PV `hostPath`. `hostPath` dính một node. Scheduler phải xếp Pod đúng node đó, hoặc Pod `Pending`. Đừng coi hostPath như đĩa cloud.

## Lệnh và code mẫu

Hai container, một `emptyDir` (cùng ý shared volume; app và sidecar):

```yaml
apiVersion: v1
kind: Pod
metadata:
  name: share-vol
spec:
  containers:
    - name: writer
      image: busybox:1.36
      command: ["sh", "-c", "echo hello > /data/out.txt && sleep 3600"]
      volumeMounts:
        - name: data
          mountPath: /data
    - name: reader
      image: busybox:1.36
      command: ["sh", "-c", "sleep 3600"]
      volumeMounts:
        - name: data
          mountPath: /data
  volumes:
    - name: data
      emptyDir: {}
```

```bash
kubectl apply -f share-vol.yaml
kubectl exec share-vol -c reader -- cat /data/out.txt
```

PVC + Pod (PV phải có sẵn trên cluster lab, hoặc StorageClass default). Mẫu claim:

```yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: lab-pvc
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi
```

Trong Pod:

```yaml
volumes:
  - name: data
    persistentVolumeClaim:
      claimName: lab-pvc
```

```bash
kubectl get pv,pvc
kubectl describe pvc lab-pvc
```

`Pending` thì đọc Events: chưa có PV khớp size/mode/StorageClass.

## Thực hành

1. Apply Pod `emptyDir` hai container. `exec` container reader, `cat` file writer đã ghi.
2. `kubectl delete pod share-vol`. Tạo lại cùng YAML. File `hello` không còn — đúng với `emptyDir`.
3. Trên cluster lab có PV: tạo PVC, `get pvc` tới Bound, gắn vào một Pod, ghi file, xóa Pod, tạo Pod mới cùng PVC, đọc lại file.
4. Cố ý đổi `volumeMounts.name` lệch `volumes`. Đọc `describe` rồi sửa.

## Kết quả mong đợi

- Reader đọc được `hello` khi còn cùng Pod.
- Pod tạo lại với `emptyDir`: file mất.
- PVC Bound thì file còn sau khi thay Pod.
- Lệch tên mount: Events nói mount fail, không phải 404 app.

## Checklist

- [ ] `volumeMounts.name` = `volumes[].name`.
- [ ] `emptyDir` chết cùng Pod; PVC thì không.
- [ ] Pod khai `claimName`, không khai tên PV.
- [ ] PVC `Pending`: `describe`, đối size và access mode.
- [ ] Shared volume = hai mount cùng volume trong **một** Pod.

## Thẻ ôn

### kubernetes-volumes-name
kind: trap
cue: `volumeMounts.name` phải trùng `volumes[].name`. Sai tên thì fail mount, app không thấy path.

Q: Container mount `name: html` tại `/usr/share/nginx/html`. `volumes` chỉ có `name: www`. Nginx lên nhưng trang trống. Chữa chỗ nào?

A: Đổi cho khớp một tên, cả hai chỗ. Kubernetes không đoán `html` = `www`. `describe pod` Events: mount/volume. Đừng sửa `index.html` trong image — đó không phải lỗi Nginx.

### kubernetes-volumes-emptydir
kind: distinguish
cue: emptyDir sống hết đời Pod. Restart container còn file. Xóa Pod mất. Hai container cùng name thì thấy nhau.

Q: Sidecar đọc log app trong `/var/log/app`. Dùng volume gì trong cùng Pod, và node reboot thì file còn không?

A: `emptyDir`, hai container `volumeMounts` cùng `name`. Chung filesystem lúc Pod còn. Node reboot hoặc Pod bị thay: hết. Log cần giữ thì PVC hoặc ship ra ngoài (bài sidecar).

### kubernetes-volumes-pvc
kind: distinguish
cue: PV là kho cluster. PVC là lời xin. Pod chỉ ghi `claimName` của PVC.

Q: Manifest Pod để `volumes: [{ name: data, persistentVolumeClaim: { claimName: mysql-pv } }]` vì PV tên `mysql-pv`. Bind thế nào?

A: Sai tầng. `claimName` là tên **PVC**. PV do control plane gắn vào claim. Tạo PVC xin size/mode, đợi Bound, rồi `claimName: <tên-pvc>`. Trỏ thẳng tên PV trong Pod không phải cách docs dạy.

### kubernetes-volumes-pending
kind: decide
cue: PVC Pending = chưa có PV khớp size, mode, hoặc StorageClass. `describe pvc`.

Q: MySQL Pod `Pending`, PVC cũng `Pending`. Xem object nào trước, và tăng replica MySQL có giúp không?

A: `describe pvc` trước. Chưa Bound thì Pod không schedule xong. Tăng replica chỉ thêm Pod chờ cùng claim (RWO thường một node một mount). Thêm PV khớp, hoặc sửa request storage/accessMode.

### kubernetes-volumes-two-pods
kind: trap
cue: emptyDir không chia giữa hai Pod. Hai Pod cần chung đĩa thì PVC loại đĩa cho phép, hoặc NFS/RWX.

Q: Hai replica Deployment cùng `emptyDir` tên `data`. File Pod A có trên Pod B không?

A: Không. Mỗi Pod một `emptyDir` riêng trên node của nó. Replica không chia scratch. Cần chung thì PVC `ReadWriteMany` (nếu storage hỗ trợ), hoặc một workload một đĩa (StatefulSet / một replica).

## Lỗi thường gặp

**`mountPath` là path trên laptop.** Path trong container. App đọc `/usr/share/nginx/html` thì mount đúng chỗ đó.

**Quên khối `volumes`, chỉ viết `volumeMounts`.** API hoặc kubelet fail. Hai khối đủ cặp.

**Database trên `emptyDir`.** Pod thay, data đi. Dùng PVC trước khi ghi thật.

**Xóa PVC đang có Pod.** Object `Terminating` đến khi không còn Pod dùng.

## Nguồn

1. [KodeKloud Engineer — 100 Days of DevOps](https://engineer.kodekloud.com/practice) — đề cương ngày Resolve VolumeMounts, Kubernetes Shared Volumes, Persistent Volumes. Trang practice cần đăng nhập. Bài này viết lại lệnh, không chép lab.
2. [Volumes](https://kubernetes.io/docs/concepts/storage/volumes/) — `volumes` / `volumeMounts`, `emptyDir`, chia filesystem trong Pod.
3. [Persistent Volumes](https://kubernetes.io/docs/concepts/storage/persistent-volumes/) — PV, PVC, bind, access mode, protection khi đang dùng.

## Tiếp theo

[Biến môi trường và Secret](./12-config-secrets.md)
