ConfigMaps, Secrets & Storage

Keep configuration out of your images and data safe when pods die. ConfigMaps, Secrets (and how to handle them safely), volumes, PersistentVolumeClaims and StorageClasses.

Intermediate⏱ 5 min readLesson 6 of 8#kubernetes#configmap#secrets#storage#pvc

Part 1: Configuration

The big idea

A travelling musician carries the same guitar to every venue, but each venue gives them a different set list and sound settings. The guitar is the container image; the set list is the configuration.

Build the image once, configure it per environment. Never bake "staging database URL" into an image.

One image, different configuration per environmentOne image, different configuration per environment

ConfigMaps: non-secret settings

apiVersion: v1
kind: ConfigMap
metadata:
  name: shop-api-config
data:
  LOG_LEVEL: "info"
  FEATURE_NEW_CHECKOUT: "true"
  PAYMENTS_URL: "http://payments-api"
  app-settings.json: |
    { "currency": "EUR", "pageSize": 20 }

Use it in a pod as environment variables or as files:

spec:
  containers:
    - name: api
      image: registry.example.com/shop-api:1.9.0
      envFrom:
        - configMapRef: { name: shop-api-config }       # every key → an env var
      env:
        - name: LOG_LEVEL
          valueFrom:
            configMapKeyRef: { name: shop-api-config, key: LOG_LEVEL }  # a single key
      volumeMounts:
        - name: settings
          mountPath: /etc/shop                          # → /etc/shop/app-settings.json
  volumes:
    - name: settings
      configMap: { name: shop-api-config, items: [{ key: app-settings.json, path: app-settings.json }] }
Mounted asUpdates when the ConfigMap changes?
Environment variables❌ Only when the pod restarts
Files (volume)✅ Eventually (within about a minute), if your app re-reads the file

💡 A common trick: put a hash of the config in a pod annotation, so changing the config triggers a rolling restart. Helm charts often do this.

Secrets: sensitive values

apiVersion: v1
kind: Secret
metadata:
  name: shop-api-secrets
type: Opaque
stringData:                           # plain text here; stored base64-encoded
  DATABASE_URL: "postgres://shop:s3cr3t@postgres:5432/shop"
  STRIPE_API_KEY: "sk_live_…"
envFrom:
  - secretRef: { name: shop-api-secrets }

⚠️ Base64 is not encryption. Anyone who can read the Secret object can decode it in one command. Protect Secrets properly:

DoWhy
Enable encryption at rest for etcdSecrets are stored encrypted
Restrict access with RBACOnly the apps and people that need them can read them
Never commit plain Secret YAML to gitGit history is forever
Use External Secrets Operator, Vault or a cloud secret managerThe real values live in a proper vault and are synced in
Or Sealed Secrets / SOPSEncrypted files that are safe to commit
Prefer mounting as files over env varsEnv vars leak into logs and crash dumps more easily
Drawing diagram…

Part 2: Storage

The big idea

Containers are disposable: when a pod is deleted or moves to another node, everything written inside its filesystem is gone. That's perfect for stateless apps, and a disaster for databases.

Kubernetes separates "I need 20 GB of fast disk" (a claim) from "here's an actual disk" (a volume), like booking a hotel room by type instead of by room number.

Volume types at a glance

VolumeLives as long as…Use for
emptyDirThe podScratch space, sharing files between containers in a pod
configMap / secret–Config files and credentials
hostPathThe node⚠️ Rarely: node-level agents only
PersistentVolumeClaimIndependent of pods ✅Databases, uploads, anything that must survive

PV, PVC and StorageClass

Drawing diagram…
  • PersistentVolume (PV): a real piece of storage (a cloud disk, an NFS share).
  • PersistentVolumeClaim (PVC): a pod's request for storage: size, access mode, class.
  • StorageClass: a template telling Kubernetes how to create PVs on demand (dynamic provisioning).
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: uploads
spec:
  accessModes: [ReadWriteOnce]
  storageClassName: fast
  resources:
    requests:
      storage: 20Gi
---
# In the pod spec:
#   volumes:
#     - name: uploads
#       persistentVolumeClaim: { claimName: uploads }
#   containers[].volumeMounts:
#     - { name: uploads, mountPath: /app/uploads }

Access modes

ModeMeaningTypical backing
ReadWriteOnce (RWO)Mounted read-write by one nodeCloud block disks (EBS, Persistent Disk)
ReadOnlyMany (ROX)Read-only by many nodesShared datasets
ReadWriteMany (RWX)Read-write by many nodesNFS, EFS, Azure Files, CephFS
ReadWriteOncePodExactly one podStrict single-writer workloads

StatefulSets and storage

A StatefulSet gives each replica its own PVC that follows it across restarts and rescheduling:

volumeClaimTemplates:
  - metadata: { name: data }
    spec:
      accessModes: [ReadWriteOnce]
      storageClassName: fast
      resources: { requests: { storage: 50Gi } }
Drawing diagram…

Pod postgres-1 always gets data-postgres-1 back, even after moving to another node.

Reclaim policy: what happens when the claim is deleted?

  • Delete (default for dynamic volumes): the disk is deleted too. ⚠️ Be careful in production.
  • Retain: the disk is kept for manual recovery. Safer for important data.

💡 For uploads and user files, object storage (S3, GCS, Azure Blob) accessed through its API is often simpler and more scalable than volumes. It keeps your pods stateless.

Key takeaways

  • ConfigMaps hold non-secret config; Secrets hold sensitive values. Both are injected as env vars or files, so one image works everywhere.
  • Secrets are only base64-encoded: add encryption at rest, RBAC, and an external secret manager; never commit them.
  • Pod filesystems are disposable; use PersistentVolumeClaims for data that must survive.
  • StorageClasses create volumes on demand; mind the access modes and reclaim policy.
  • StatefulSets give each replica its own persistent volume; object storage keeps pods stateless.