Storage#

Managing persistent and ephemeral data in Kubernetes — from temporary scratch space to durable database volumes, plus configuration and secrets injection.


Volume Types Overview#

Volume TypePersistenceUse Case
emptyDirPod lifetime onlyScratch space, inter-container sharing
hostPathNode lifetimeNode-level logs, dev/testing only
configMapCluster lifetimeConfiguration files mounted as volumes
secretCluster lifetimeSensitive data mounted as files
persistentVolumeClaimIndependent of podDatabases, stateful workloads
nfsExternal NFS serverShared read-write across pods
csiDepends on driverAny storage backend via CSI standard

Volumes (Ephemeral)#

emptyDir#

Created when a pod is assigned to a node. Deleted when the pod is removed.

apiVersion: v1
kind: Pod
metadata:
  name: data-processor
spec:
  containers:
    - name: writer
      image: busybox
      command: ['sh', '-c', 'echo "data" > /cache/output.txt && sleep 3600']
      volumeMounts:
        - name: shared-cache
          mountPath: /cache
    - name: reader
      image: busybox
      command: ['sh', '-c', 'cat /cache/output.txt && sleep 3600']
      volumeMounts:
        - name: shared-cache
          mountPath: /cache
          readOnly: true
  volumes:
    - name: shared-cache
      emptyDir:
        sizeLimit: 500Mi    # Optional size limit

Persistent Storage Model#

Kubernetes decouples storage provisioning from consumption using three abstractions:

graph LR
    SC[StorageClass] -->|provisions| PV[PersistentVolume]
    PVC[PersistentVolumeClaim] -->|binds to| PV
    Pod -->|references| PVC
ResourceWho Creates ItPurpose
StorageClassCluster adminDefines how to provision storage (type, parameters)
PersistentVolume (PV)Admin or dynamic provisionerA piece of actual storage (EBS volume, NFS share)
PersistentVolumeClaim (PVC)DeveloperA request for storage (size, access mode)

PersistentVolume#

apiVersion: v1
kind: PersistentVolume
metadata:
  name: postgres-pv
spec:
  capacity:
    storage: 50Gi
  accessModes:
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  storageClassName: gp3
  csi:
    driver: ebs.csi.aws.com
    volumeHandle: vol-0abc123def456

PersistentVolumeClaim#

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: postgres-data
spec:
  accessModes:
    - ReadWriteOnce
  storageClassName: gp3
  resources:
    requests:
      storage: 50Gi

Using a PVC in a Pod#

apiVersion: v1
kind: Pod
metadata:
  name: postgres
spec:
  containers:
    - name: postgres
      image: postgres:16
      volumeMounts:
        - name: data
          mountPath: /var/lib/postgresql/data
  volumes:
    - name: data
      persistentVolumeClaim:
        claimName: postgres-data

Access Modes#

ModeAbbreviationDescription
ReadWriteOnceRWOMounted read-write by a single node
ReadOnlyManyROXMounted read-only by many nodes
ReadWriteManyRWXMounted read-write by many nodes (NFS, EFS)
ReadWriteOncePodRWOPMounted read-write by a single pod (K8s 1.27+)

Reclaim Policies#

PolicyBehavior When PVC Deleted
RetainPV is preserved (manual cleanup required)
DeletePV and underlying storage are deleted
RecycleDeprecated — do not use

StorageClasses and Dynamic Provisioning#

With dynamic provisioning, you don’t create PVs manually — the StorageClass handles it automatically when a PVC is created.

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: gp3
  annotations:
    storageclass.kubernetes.io/is-default-class: "true"
provisioner: ebs.csi.aws.com
parameters:
  type: gp3
  iops: "3000"
  throughput: "125"
  encrypted: "true"
reclaimPolicy: Delete
volumeBindingMode: WaitForFirstConsumer   # Bind PV only when pod is scheduled
allowVolumeExpansion: true                 # Allow PVC resize
Volume Binding ModeBehavior
ImmediatePV provisioned as soon as PVC is created
WaitForFirstConsumerPV provisioned in the same AZ as the pod (recommended for cloud)

CSI Drivers#

The Container Storage Interface (CSI) is the standard for integrating external storage systems with Kubernetes.

Cloud ProviderCSI DriverStorage Types
AWSebs.csi.aws.comEBS (gp3, io2, st1)
AWSefs.csi.aws.comEFS (shared filesystem)
GCPpd.csi.storage.gke.ioPersistent Disk
Azuredisk.csi.azure.comAzure Disk
Azurefile.csi.azure.comAzure Files

Install CSI drivers as cluster add-ons (usually Helm charts or cloud provider integrations).


ConfigMaps#

Store non-sensitive configuration data as key-value pairs or entire files.

Creating a ConfigMap#

apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
data:
  # Simple key-value
  DATABASE_HOST: "postgres-svc"
  LOG_LEVEL: "info"
  
  # File-like key (multiline)
  nginx.conf: |
    server {
      listen 80;
      location / {
        proxy_pass http://backend:8080;
      }
    }

Using ConfigMaps#

As environment variables:

spec:
  containers:
    - name: app
      image: myapp:1.0
      envFrom:
        - configMapRef:
            name: app-config     # All keys become env vars
      env:
        - name: DB_HOST          # Single key
          valueFrom:
            configMapKeyRef:
              name: app-config
              key: DATABASE_HOST

As mounted files:

spec:
  containers:
    - name: nginx
      image: nginx:1.27
      volumeMounts:
        - name: config
          mountPath: /etc/nginx/conf.d
  volumes:
    - name: config
      configMap:
        name: app-config
        items:
          - key: nginx.conf
            path: default.conf    # Mounted as /etc/nginx/conf.d/default.conf

Secrets#

Store sensitive data (passwords, tokens, certificates). Base64-encoded at rest (not encrypted by default — enable encryption at rest in production).

Creating Secrets#

apiVersion: v1
kind: Secret
metadata:
  name: db-credentials
type: Opaque
stringData:                    # Use stringData for plain text input
  username: admin
  password: "s3cur3-p@ssw0rd"
# Imperative creation
kubectl create secret generic db-credentials \
  --from-literal=username=admin \
  --from-literal=password='s3cur3-p@ssw0rd'

# From file
kubectl create secret generic tls-cert \
  --from-file=cert.pem=./server.crt \
  --from-file=key.pem=./server.key

Mounting Secrets#

spec:
  containers:
    - name: app
      image: myapp:1.0
      env:
        - name: DB_PASSWORD
          valueFrom:
            secretKeyRef:
              name: db-credentials
              key: password
      volumeMounts:
        - name: certs
          mountPath: /etc/tls
          readOnly: true
  volumes:
    - name: certs
      secret:
        secretName: tls-cert
        defaultMode: 0400     # Read-only for owner

Secret Types#

TypeUse Case
OpaqueArbitrary user-defined data (default)
kubernetes.io/tlsTLS certificate and key
kubernetes.io/dockerconfigjsonDocker registry credentials
kubernetes.io/service-account-tokenServiceAccount token

External Secret Operators#

Native Kubernetes Secrets have limitations — they’re base64 (not encrypted), stored in etcd, and hard to sync with external vaults. External operators solve this:

OperatorSyncs FromKey Feature
External Secrets OperatorAWS SSM/Secrets Manager, Vault, GCP SM, Azure KVMost popular, multi-backend
Sealed SecretsEncrypted in git, decrypted in-clusterGitOps-friendly, no external dependency
Vault Secrets OperatorHashiCorp VaultTight Vault integration, dynamic secrets

External Secrets Operator Example#

apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: db-credentials
spec:
  refreshInterval: 1h
  secretStoreRef:
    name: aws-secrets-manager
    kind: ClusterSecretStore
  target:
    name: db-credentials        # K8s Secret to create
    creationPolicy: Owner
  data:
    - secretKey: username
      remoteRef:
        key: /production/database
        property: username
    - secretKey: password
      remoteRef:
        key: /production/database
        property: password

Key Takeaways#

  1. emptyDir is for scratch space; PersistentVolumeClaims are for durable data that survives pod restarts.
  2. Dynamic provisioning via StorageClasses eliminates manual PV creation — the cluster provisions storage on demand.
  3. WaitForFirstConsumer binding mode prevents zone mismatch between pods and their volumes.
  4. ConfigMaps for non-sensitive config, Secrets for sensitive data — but native Secrets are only base64 encoded, not encrypted.
  5. Mount as files when config format matters (nginx.conf); use env vars for simple key-value settings.
  6. External Secrets Operators bridge the gap between Kubernetes and enterprise secret stores — essential for production.
  7. Always set reclaim policy to Retain for production databases — accidental PVC deletion shouldn’t destroy your data.