Learning Platform
Глоссарий Troubleshooting
Урок 08.01 · 20 мин
Средний
VolumeemptyDirhostPathprojected volumetmpfsPod scope

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).
Жизненный цикл Volume vs контейнер
PodPod — единица планирования. Создаётся kubelet-ом, имеет свой network namespace, IPC namespace и volumes. Все его контейнеры разделяют эти ресурсы.
внутри Pod
Container AКонтейнер может рестартовать (Liveness probe failed, exit code != 0 при restartPolicy: Always). Файловая система контейнера пересоздаётся.
VolumeVolume переживает рестарт контейнера: kubelet не удаляет данные, новый процесс контейнера видит те же файлы. Но при удалении самого Pod-а volume удаляется.
Container BВторой контейнер видит тот же volume по тому же mountPath, если указано в его volumeMounts. Это базовый pattern shared scratch space между sidecar и app.
Pod удалён
Volume gonekubelet вызывает TearDown через volume plugin, удаляет директорию из /var/lib/kubelet/pods/pod-uid/volumes. Все данные пропадают (для эфемерных типов).

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-ов есть отдельный механизм)
WARNING

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.

DANGER

В 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.

projected volume: композиция в одну точку
ConfigMapИсточник 1: ConfigMap в namespace. Items выбирают конкретные ключи и задают их paths в mount.
SecretИсточник 2: Secret. Декодируется (base64) и проецируется как файл в волюме (in-memory tmpfs).
downwardAPIИсточник 3: метаданные Pod-а через fieldRef / resourceFieldRef.
SA tokenИсточник 4: bound service account token с audience и expirationSeconds. Современная замена legacy automountServiceAccountToken. kubelet refresh-ит токен.
projected sources
Один mountPathВсе источники проецируются в одну директорию. Контейнер не отличает, откуда что — для него это просто файлы. Удобно для приложений, ожидающих 'all config in one folder'.

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-а.


Проверка знанийKnowledge check
Pod использует emptyDir с medium: Memory и без sizeLimit. У Pod-а memory limit 1Gi. Приложение пишет в этот volume 800Mi данных, плюс держит heap 300Mi. Что произойдёт и почему?
ОтветAnswer
Pod будет OOMKilled (terminated с reason OOMKilled, exit code 137). Причина: emptyDir с medium: Memory использует tmpfs — файловую систему в RAM. Память, занятая файлами в tmpfs, считается в общий memory usage Pod-а (через cgroup memory accounting). Итого: 800Mi tmpfs + 300Mi heap = 1100Mi, что превышает limit 1Gi (1024Mi). cgroup memory controller отправляет SIGKILL процессу, kubelet рапортует OOMKilled. Это типичный gotcha: разработчик использует tmpfs для скорости, не понимая, что это часть memory budget Pod-а, а не отдельная зона. Решение: (1) задать emptyDir.sizeLimit меньше memory limit, оставив запас для heap; (2) использовать emptyDir на disk (без medium: Memory) — он не считается в memory; (3) поднять memory limit, осознавая, что workload требует и RAM, и in-memory storage.

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 5. Pod имеет три контейнера и один emptyDir volume. Контейнер A пишет файл в /shared/data.txt. Контейнер B читает /shared/data.txt. Контейнер A крашится и перезапускается. Что увидит контейнер B после рестарта A?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 5