dissected.io
Lesson 5 of 15
Intermediatestorageconfigmapssecretsvolumespersistent-volumes10 min read

ConfigMaps, Secrets, and Storage

Containers are ephemeral by design. When a pod dies, everything inside it disappears. That's fine for stateless web servers, but most real applications need configuration files, database credentials, and persistent data that survives restarts. Kubernetes solves each of these with different primitives — and understanding which one to use when is the difference between a resilient deployment and a 3 AM incident.

Kubernetes Storage — From Ephemeral to Persistent

ConfigMaps — Externalize Your Configuration

A ConfigMap stores non-sensitive configuration data as key-value pairs. Instead of baking config into your container image (which would force a rebuild for every environment), you inject it at runtime.

apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
data:
  DATABASE_HOST: "db.internal"
  LOG_LEVEL: "info"
  config.yaml: |
    server:
      port: 8080
      workers: 4

You can consume ConfigMaps two ways:

  • Environment variables: Simple key-value injection. Good for flat config.
  • Volume mounts: Mount the entire ConfigMap as files in a directory. Better for config files that your app reads from disk.
# As environment variables
envFrom:
  - configMapRef:
      name: app-config

# As a mounted volume
volumes:
  - name: config-vol
    configMap:
      name: app-config
volumeMounts:
  - name: config-vol
    mountPath: /etc/config

When you update a ConfigMap, mounted volumes eventually reflect the change (kubelet syncs periodically). Environment variables do not — the pod needs a restart.

Secrets — Sensitive Data (With Caveats)

Secrets look like ConfigMaps but are intended for sensitive data: passwords, API keys, TLS certificates. The critical thing to understand is that Secrets are not encrypted by default. They're base64-encoded — which is encoding, not encryption. Anyone with API access to the namespace can read them.

apiVersion: v1
kind: Secret
metadata:
  name: db-credentials
type: Opaque
data:
  username: YWRtaW4=        # base64("admin")
  password: c3VwZXJzZWNyZXQ= # base64("supersecret")

To actually secure Secrets, you need to:

  1. Enable encryption at rest in etcd (EncryptionConfiguration)
  2. Use RBAC to restrict who can read Secrets
  3. Consider external secret managers like HashiCorp Vault, AWS Secrets Manager, or the External Secrets Operator that syncs cloud secrets into K8s Secrets

Volume Types — The Storage Spectrum

emptyDir

A temporary directory created when a pod starts and deleted when it stops. Useful for scratch space and sharing data between containers in the same pod.

volumes:
  - name: scratch
    emptyDir: {}
    # emptyDir with medium: Memory uses tmpfs (RAM-backed)

hostPath

Mounts a directory from the host node's filesystem. Dangerous in production — ties your pod to a specific node and creates security risks. Mostly used for system-level tools like log collectors.

PersistentVolume (PV) and PersistentVolumeClaim (PVC)

This is the real storage system. PVs represent actual storage (an EBS volume, a GCE persistent disk, an NFS share). PVCs are requests for storage that bind to matching PVs.

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: data-pvc
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 10Gi
  storageClassName: fast-ssd

Access modes control how the volume can be attached:

  • ReadWriteOnce (RWO): One node can mount read-write. Most common.
  • ReadOnlyMany (ROX): Many nodes can mount read-only.
  • ReadWriteMany (RWX): Many nodes can mount read-write. Requires NFS or a distributed filesystem.

StorageClasses — Dynamic Provisioning

Without StorageClasses, an admin has to manually create PVs before developers can claim them. StorageClasses automate this: define a class with a provisioner and parameters, then PVCs that reference it get volumes created automatically.

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: fast-ssd
provisioner: ebs.csi.aws.com
parameters:
  type: gp3
  iops: "3000"
reclaimPolicy: Delete
volumeBindingMode: WaitForFirstConsumer

WaitForFirstConsumer is important — it delays volume creation until a pod actually needs it, ensuring the volume is created in the same availability zone as the pod.

reclaimPolicy controls what happens when the PVC is deleted: Delete removes the underlying volume (good for ephemeral workloads), Retain keeps it (good for databases where you want to manually recover data).

StatefulSets and Persistent Storage

StatefulSets are the answer for stateful workloads like databases. Each replica gets:

  • A stable hostname: db-0, db-1, db-2 (not random hashes like Deployments)
  • A dedicated PVC created from volumeClaimTemplates that survives pod restarts
  • Ordered deployment: pods start in sequence (0, 1, 2) and terminate in reverse
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: db
spec:
  serviceName: db-svc
  replicas: 3
  template:
    spec:
      containers:
        - name: postgres
          volumeMounts:
            - name: data
              mountPath: /var/lib/postgresql/data
  volumeClaimTemplates:
    - metadata:
        name: data
      spec:
        accessModes: ["ReadWriteOnce"]
        storageClassName: fast-ssd
        resources:
          requests:
            storage: 50Gi

Each pod gets its own PVC: data-db-0, data-db-1, data-db-2. If db-1 crashes and restarts, it reattaches to data-db-1 — the data is still there. This is what makes StatefulSets suitable for databases, message queues, and any workload where identity and data persistence matter.

Choosing the Right Primitive

NeedUse
App configurationConfigMap (env vars or volume mount)
Passwords, API keysSecret + encryption at rest + RBAC
Scratch space between containersemptyDir
Persistent data (single writer)PVC with RWO + StorageClass
Shared persistent dataPVC with RWX (NFS/EFS)
Databases, stateful appsStatefulSet + volumeClaimTemplates

Enjoyed this breakdown?

Get notified when new lessons go live.