Skip to content
docktionary

Kubernetes

Config, secrets & storage

Keeping configuration, sensitive values, and persistent data out of your container images, so the same image can run anywhere.

ConfigMap

configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
data:
  LOG_LEVEL: "info"
  FEATURE_FLAGS: "new-search,dark-mode"

Consume it as environment variables, or mount it as files:

yaml
spec:
  containers:
    - name: app
      image: docktionary/app:1.4.0
      envFrom:
        - configMapRef:
            name: app-config
      volumeMounts:
        - name: config-files
          mountPath: /etc/config
  volumes:
    - name: config-files
      configMap:
        name: app-config

Secret

The same shape as a ConfigMap, for sensitive values. Values are base64-encoded in data — that is encoding, not encryption. Encryption at rest for etcd, or an external secrets manager (Vault, AWS Secrets Manager via the External Secrets Operator), is what actually protects a Secret's contents. Who can even read it in the first place is a separate question, answered by RBAC, not by anything on the Secret itself.

terminal
kubectl create secret generic db-credentials \
  --from-literal=username=app \
  --from-literal=password='correct horse battery staple'
yaml
spec:
  containers:
    - name: app
      image: docktionary/app:1.4.0
      env:
        - name: DB_PASSWORD
          valueFrom:
            secretKeyRef:
              name: db-credentials
              key: password

Secret types

type doesn’t change how a Secret is stored — every type is still just base64 under the hood — but it does determine what keys Kubernetes expects and validates, and it's how other tooling (an Ingress controller looking for TLS, the kubelet looking for registry credentials) knows which Secret to use for what.

  • Opaque — the default; arbitrary key-value data, used above.
  • kubernetes.io/tls — expects tls.crt/tls.key; what an Ingress’s tls.secretName points at.
  • kubernetes.io/dockerconfigjson — registry credentials, referenced from a Pod's imagePullSecrets so the kubelet can pull a private image.
terminal
kubectl create secret docker-registry regcred \
  --docker-server=ghcr.io \
  --docker-username=you \
  --docker-password=$GITHUB_TOKEN

Immutable ConfigMaps and Secrets

immutable: true locks the object's data after creation — any edit has to go through creating a new object instead. In exchange, the API server stops watching it for changes, which measurably reduces load on kube-apiserver in clusters with a lot of ConfigMaps. Pair it with a name that includes a content hash (what Kustomize's configMapGenerator does automatically) so a Deployment picks up a "change" by referencing a new object name, which also happens to trigger a proper rolling restart — something editing an existing ConfigMap in place never reliably does.

yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config-a1b2c3
immutable: true
data:
  LOG_LEVEL: "info"

PersistentVolume & PersistentVolumeClaim

A PersistentVolumeClaim (PVC) is how a Pod asks for storage without knowing the details of the underlying disk. A PersistentVolume (PV) represents that actual storage, either provisioned ahead of time or created on demand by a StorageClass.

pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: db-data
spec:
  accessModes: ["ReadWriteOnce"]
  storageClassName: standard
  resources:
    requests:
      storage: 10Gi
yaml
spec:
  containers:
    - name: postgres
      image: postgres:16-alpine
      volumeMounts:
        - name: data
          mountPath: /var/lib/postgresql/data
  volumes:
    - name: data
      persistentVolumeClaim:
        claimName: db-data

StorageClass

A StorageClass tells the cluster how to provision a volume automatically when a PVC asks for one — which provisioner to use (an EBS volume, a Persistent Disk, an Azure Disk) and with what parameters.

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

WaitForFirstConsumer delays provisioning until a Pod actually needs the volume, so the disk gets created in the same availability zone the Pod was scheduled into.

Checking it all from the command line

terminal
kubectl get configmaps,secrets
kubectl get pvc,pv,storageclass
kubectl describe pvc db-data