Volumes: эфемерное хранилище в Pod
Контейнерная файловая система эфемерна: всё, что контейнер записал в свой layer, исчезает при его рестарте. Это нормально для stateless workloads, но плохо в трёх типичных кейсах: (1) контейнеру нужно scratch space, который переживёт рестарт самого контейнера, (2) двум контейнерам в одном Pod нужно обмениваться данными через файловую систему, (3) приложению нужны данные с node (например, host-логи) или с сетевого storage.
Эти задачи решает абстракция Volume. Volume — это директория, доступная контейнерам в Pod, со своим жизненным циклом, привязанным к Pod-у, а не к контейнеру.
В этом уроке — обзор эфемерных типов volumes: тех, что живут вместе с Pod-ом. PV/PVC (persistent storage, который переживает Pod) — в следующих уроках.
Три типа mount: bind, volume, tmpfs
Что такое Volume
Volume — это поле spec.volumes[] в Pod spec, плюс ссылка из контейнера через spec.containers[].volumeMounts[].
apiVersion: v1
kind: Pod
metadata:
name: scratch-demo
spec:
containers:
- name: app
image: busybox:1.36
command: ["sh", "-c", "echo hello > /scratch/file && sleep 3600"]
volumeMounts:
- name: scratch
mountPath: /scratch
volumes:
- name: scratch
emptyDir: {}
Ключевые свойства:
- Volume определяется на уровне Pod, не контейнера. Один volume может быть смонтирован в несколько контейнеров одного Pod-а.
- Жизненный цикл volume = жизненный цикл Pod (для большинства типов). Pod удалён — volume исчез.
- Контейнер при рестарте сохраняет содержимое volume — потому что volume живёт на уровне Pod, а не контейнера.
- Pod scope означает: данные shareable между контейнерами Pod-а, но не между Pod-ами (даже на одной node).
emptyDir: scratch space
emptyDir — самый простой тип volume. Создаётся пустой при создании Pod-а, существует пока Pod жив.
volumes:
- name: cache
emptyDir: {}
По умолчанию хранится на node disk — в директории /var/lib/kubelet/pods/<pod-uid>/volumes/kubernetes.io~empty-dir/cache.
Типичные use cases:
- Scratch space для приложения: temp files, downloaded artifacts, intermediate processing results
- Shared FS между контейнерами одного Pod: например, init container скачал файл, app container его читает; или sidecar-агент пишет логи, которые app читает
- Кэш для приложения, который не критичен для сохранения
sizeLimit
С v1.20+ emptyDir.sizeLimit ограничивает максимальный размер:
volumes:
- name: cache
emptyDir:
sizeLimit: 1Gi
Если приложение превысит лимит, kubelet выселит (evict) Pod с reason ContainerEvicted или Evicted. Без лимита приложение может заполнить весь node disk и положить kubelet — это типовая проблема в production.
medium: Memory (tmpfs)
emptyDir может быть смонтирован не на disk, а в RAM через tmpfs:
volumes:
- name: fast-cache
emptyDir:
medium: Memory
sizeLimit: 256Mi
Это даёт очень быстрый read/write, потому что всё в page cache. Удобно для:
- High-throughput кэшей
- Передачи больших файлов между контейнерами без disk IO
- Когда secret или ключ должен жить только в RAM (хотя для secret-ов есть отдельный механизм)
emptyDir с medium: Memory занимает место в memory usage Pod-а и считается в его memory limit. Если у Pod limit 512Mi и в tmpfs записано 400Mi, а контейнер использует ещё 200Mi heap — это 600Mi → OOMKilled. Всегда задавайте sizeLimit для memory emptyDir, иначе можете получить трудно-диагностируемые OOM-ы. Это самый частый killer-момент при работе с volumes.
hostPath: монтирование с node
hostPath монтирует директорию или файл с самой node в Pod:
volumes:
- name: node-logs
hostPath:
path: /var/log
type: Directory
containers:
- name: log-reader
volumeMounts:
- name: node-logs
mountPath: /host-logs
readOnly: true
Поле type определяет проверку при mount: Directory, DirectoryOrCreate, File, FileOrCreate, Socket, CharDevice, BlockDevice, или пустое (без проверки).
Когда использовать
hostPath — это низкоуровневый escape hatch. Используется редко и для специфичных задач:
- DaemonSet-ы для node-уровня сбора данных: log collectors (Fluentd, Promtail) монтируют
/var/log/; node-exporter монтирует/proc,/sysдля метрик - Доступ к device files: GPU drivers, custom hardware через
/dev/... - Доступ к socket-у kubelet или container runtime:
/var/run/docker.sock(security кошмар, но иногда требуется),/run/containerd/containerd.sock
Почему опасно
hostPath даёт Pod-у доступ к файловой системе хоста. Это крупная security проблема в multi-tenant кластерах:
- Pod может прочитать
/etc/shadow, kubelet credentials, конфиги соседних Pod-ов через/var/lib/kubelet/ - Pod может смонтировать
/, escape наружу контейнера, скомпрометировать всю node - hostPath обходит storage quotas — Pod может заполнить корневой раздел node
В production обычно hostPath блокируется на уровне Pod Security Standards (Baseline и Restricted profile) или через admission webhook. Использовать стоит только когда других вариантов нет и есть строгий audit.
В production кластерах, где запускаются untrusted workloads, hostPath должен быть запрещён на уровне admission control. Pod с hostPath: / эквивалентен root-доступу к node. Для CKAD задач hostPath встречается, но в реальной работе старайтесь искать альтернативу: CSI driver, NFS/CephFS, или local PV (который тоже требует осторожности).
Special volumes: configMap, secret, downwardAPI
Это специальные volume types, которые подставляют данные из API-объектов:
configMap
volumes:
- name: app-config
configMap:
name: my-config
items:
- key: app.yaml
path: config/app.yaml
kubelet читает ConfigMap из API server и проецирует его как файлы в volume. Контейнер видит /etc/config/config/app.yaml с содержимым ключа app.yaml.
secret
volumes:
- name: tls-cert
secret:
secretName: web-tls
defaultMode: 0400
Secret монтируется как tmpfs (in-memory), чтобы данные не попадали на disk. defaultMode задаёт permissions файлов.
downwardAPI
volumes:
- name: pod-info
downwardAPI:
items:
- path: name
fieldRef:
fieldPath: metadata.name
- path: namespace
fieldRef:
fieldPath: metadata.namespace
Inject метаданные о самом Pod-е в файлы: имя, namespace, labels, annotations, IP. Удобно для приложений, которым нужно знать свой identity (для логирования, registry, и т.п.).
Подробно про configMap, secret, downwardAPI — в предыдущем модуле и в разделах по конфигурации.
projected volume: композиция источников
projected — особый тип, который объединяет несколько источников в один mount path:
volumes:
- name: all-config
projected:
sources:
- configMap:
name: app-config
items:
- key: app.yaml
path: app.yaml
- secret:
name: db-creds
items:
- key: password
path: db-password
- downwardAPI:
items:
- path: pod-name
fieldPath: metadata.name
- serviceAccountToken:
path: token
audience: api.example.com
expirationSeconds: 3600
containers:
- name: app
volumeMounts:
- name: all-config
mountPath: /etc/all
Контейнер видит в /etc/all/:
app.yaml
db-password
pod-name
token
Без projected пришлось бы делать четыре отдельных volumeMounts. Это особенно полезно для bound service account tokens (audience-scoped JWT для cloud IAM, vault, etc.) — современная замена legacy automountServiceAccountToken.
Generic Ephemeral Volumes
С v1.23 GA — ephemeral тип volume, который позволяет использовать любой CSI driver для эфемерного storage, привязанного к Pod-у. Это как inline PVC: создаётся при создании Pod, удаляется при удалении.
volumes:
- name: scratch
ephemeral:
volumeClaimTemplate:
metadata:
labels:
type: scratch
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: fast-ssd
resources:
requests:
storage: 10Gi
Под капотом создаётся PVC <pod-name>-<volume-name>, привязывается к PV (через StorageClass), монтируется в Pod. При удалении Pod-а PVC удаляется через ownerReferences → PV cleanup через reclaimPolicy.
Use case: per-Pod большой scratch space на SSD/EBS, который не нужен после завершения Pod-а — например, для batch processing, ML training.
Lifecycle: что когда удаляется
| Тип volume | Удаляется при… | Можно ли restart Pod без потери? |
|---|---|---|
emptyDir | удалении Pod | да, но не пересоздании Pod (UID меняется) |
hostPath | никогда (живёт на node) | да; данные остаются на node |
configMap / secret | удалении Pod | да; при изменении CM/Secret kubelet update-ит файлы (с задержкой) |
projected | удалении Pod | да |
ephemeral (CSI) | удалении Pod (через PVC reclaim) | да |
persistentVolumeClaim | НЕ удаляется при удалении Pod | да; данные сохраняются |
Ключевое различие: Pod restart (рестарт контейнера kubelet-ом) vs Pod recreation (удаление Pod-а и создание нового объекта с тем же именем). Volume переживает первое, но не второе — за исключением PVC (и hostPath, что не volume в нормальном смысле).
kubectl: проверка volumes
# Посмотреть volumes Pod-а
kubectl get pod scratch-demo -o jsonpath='{.spec.volumes}' | jq
# Все volume mounts всех контейнеров
kubectl get pod scratch-demo -o jsonpath='{range .spec.containers[*]}{.name}{"\n"}{.volumeMounts}{"\n\n"}{end}'
# Посмотреть фактически смонтированное внутри
kubectl exec scratch-demo -- mount | grep -E "(emptyDir|configmap|secret)"
# Размер emptyDir (если есть du)
kubectl exec scratch-demo -- du -sh /scratch
На node данные лежат в /var/lib/kubelet/pods/<pod-uid>/volumes/<plugin>/<volume-name>. Это полезно знать для отладки — kubelet там же делает unmount/cleanup при удалении Pod-а.