Kubernetes Workloads & Configuration

ConfigMaps

18 min Lesson 1 of 32

ConfigMaps

Every application has configuration: database hostnames, feature flags, thread-pool sizes, log levels, trusted CORS origins. How that configuration is delivered to the application is one of the most consequential infrastructure decisions you will make. Hardcoding it into the container image — the naive approach — couples your release cycle to your configuration cycle. Changing a log level requires rebuilding and redeploying. Worse, you end up with "environment-specific images," which destroys the fundamental portability guarantee of containers.

Kubernetes solves this with ConfigMaps: a first-class API object that holds arbitrary non-sensitive configuration data as key-value pairs or entire file contents. The image stays environment-agnostic; the ConfigMap holds the environment-specific values. The same image runs in dev, staging, and production — only the ConfigMap differs. This is the 12-Factor App principle III ("Store config in the environment") expressed natively in Kubernetes.

Creating ConfigMaps

ConfigMaps can be created imperatively (useful for scripting and quick debugging) or declared as YAML manifests (the production-grade approach, stored in version control). You should always store the manifest in Git — the imperative form is ephemeral and untraceable.

# --- Imperative creation (useful for testing, not for production) --- # From literal key=value pairs kubectl create configmap app-config \ --from-literal=LOG_LEVEL=info \ --from-literal=MAX_CONNECTIONS=100 \ --from-literal=FEATURE_DARK_MODE=true # From an existing .env file (each LINE becomes one key) kubectl create configmap app-config --from-env-file=.env # From a whole file (the filename becomes the key, the file content is the value) kubectl create configmap nginx-conf --from-file=nginx.conf # From every file in a directory kubectl create configmap html-pages --from-file=./html/ # Inspect the result kubectl get configmap app-config -o yaml kubectl describe configmap app-config

The declarative YAML form is what you commit to Git and deploy through your CI pipeline. A ConfigMap manifest looks like this:

# app-configmap.yaml apiVersion: v1 kind: ConfigMap metadata: name: app-config namespace: production labels: app: my-api env: production data: # Simple key-value pairs (consumed as env vars) LOG_LEVEL: "info" MAX_CONNECTIONS: "100" FEATURE_DARK_MODE: "true" DB_HOST: "postgres.production.svc.cluster.local" # Multi-line file content (consumed as mounted file) nginx.conf: | server { listen 80; location /healthz { return 200 "ok"; add_header Content-Type text/plain; } location / { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; } } app.properties: | thread.pool.size=16 cache.ttl.seconds=300 metrics.enabled=true
ConfigMaps vs. Secrets: ConfigMaps are for non-sensitive data. Database passwords, API tokens, TLS private keys belong in Secrets (covered in Lesson 2). A ConfigMap is stored in etcd in plaintext — it is readable by anyone with get configmap RBAC permission in that namespace. Never put credentials in a ConfigMap.

Consuming ConfigMaps: Environment Variables

The most common consumption pattern injects individual keys as environment variables. The application reads them through its standard os.Getenv / process.env / System.getenv mechanism — zero code changes required if the app already reads from environment variables.

# pod-with-configmap-env.yaml apiVersion: v1 kind: Pod metadata: name: my-api spec: containers: - name: api image: my-api:v2.4.1 # Option 1: inject individual keys by name env: - name: LOG_LEVEL valueFrom: configMapKeyRef: name: app-config key: LOG_LEVEL optional: false # Pod fails to start if key is missing (default false) - name: DB_HOST valueFrom: configMapKeyRef: name: app-config key: DB_HOST # Option 2: inject ALL keys from the ConfigMap at once envFrom: - configMapRef: name: app-config optional: false # If both env and envFrom are used, env takes precedence for conflicting keys
Production pitfall — envFrom with untrusted ConfigMaps: Using envFrom injects every key in the ConfigMap as an environment variable. If someone adds a key that collides with a system variable your application depends on (like PATH, HOME, or a library-internal variable), the behaviour is undefined and often silent. At big-tech scale, prefer explicit env[].valueFrom.configMapKeyRef entries so the contract between ConfigMap and application is explicit and auditable in code review.

Consuming ConfigMaps: Volume Mounts

The volume mount pattern is essential when your application reads from a configuration file rather than environment variables — common with NGINX, Prometheus, Kafka, Redis, and legacy applications you cannot modify. Each key in the ConfigMap becomes a separate file inside the mounted directory. The file name is the key; the file content is the value.

# pod-with-configmap-volume.yaml apiVersion: v1 kind: Pod metadata: name: nginx spec: volumes: - name: nginx-config-vol configMap: name: app-config # Mount only specific keys (omit 'items' to mount ALL keys as files) items: - key: nginx.conf path: nginx.conf # file name inside the mount path - key: app.properties path: app.properties containers: - name: nginx image: nginx:1.27-alpine volumeMounts: - name: nginx-config-vol mountPath: /etc/nginx/conf.d # entire directory is replaced readOnly: true # Alternatively, mount a single key to a specific file path # (does NOT replace the whole directory): # volumeMounts: # - name: nginx-config-vol # mountPath: /etc/nginx/conf.d/default.conf # subPath: nginx.conf # readOnly: true
Use subPath for surgical mounts: Without subPath, mounting a ConfigMap volume onto a directory replaces the entire directory. If /etc/nginx/conf.d/ already has default configs you need to preserve, use subPath: nginx.conf to mount only that one file without clobbering the rest of the directory. The trade-off: subPath mounts do not receive live updates when the ConfigMap changes — see the update behaviour section below.
ConfigMap consumption: env vars vs. volume mount ConfigMap app-config LOG_LEVEL: info DB_HOST: postgres… nginx.conf: | server { … } Pod Container (api) ENV: LOG_LEVEL=info ENV: DB_HOST=postgres… via env[].valueFrom or envFrom: Container (nginx) /etc/nginx/conf.d/ nginx.conf ← key via volumeMount live-updated (no subPath) env var volume mount Update behaviour Env vars: NOT updated — Pod must restart to pick up changes Volume (no subPath): updated within ~60s (kubelet sync period) Volume (subPath): NOT updated — behaves like env var
Two ConfigMap consumption patterns and their live-update behaviour inside a Pod.

Update Behaviour: The Critical Difference

This is where engineers get burned in production. The update behaviour of ConfigMaps differs depending on how you consume them, and understanding this prevents outages:

  • Environment variables (env / envFrom): Injected at container start time. They are never updated in a running container. To apply a ConfigMap change, you must restart the Pod — either by deleting it (a ReplicaSet respawns it) or by triggering a rolling update.
  • Volume mounts without subPath: Kubernetes updates the mounted files automatically. The kubelet's sync period (default --sync-frequency=1m on most managed clusters, configurable via kubelet-config) means changes propagate within roughly 60 seconds. The application still needs to watch the file for changes — a process that reads its config only once at startup gains nothing from this.
  • Volume mounts with subPath: Changes are not propagated. This is a known Kubernetes limitation. Treat these mounts as static — the same as environment variables.
Forcing a rolling restart after a ConfigMap update: The idiomatic way to trigger a re-read when you are using environment variables is kubectl rollout restart deployment/my-api. For GitOps workflows (ArgoCD, Flux), the cleanest approach is to annotate the Deployment with a hash of the ConfigMap content — the Helm sha256sum trick — so that a ConfigMap change automatically triggers a new rollout without manual intervention.
# Trigger a rolling restart of a Deployment after editing a ConfigMap kubectl rollout restart deployment/my-api -n production # Monitor the rollout kubectl rollout status deployment/my-api -n production # The GitOps / Helm sha256sum annotation trick: # In your Deployment's pod template annotations: # annotations: # checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} # Helm re-renders this on every `helm upgrade`, so the Deployment gets a new # pod template hash whenever the ConfigMap changes — triggering a rolling update automatically. # Verify live ConfigMap content kubectl get configmap app-config -n production -o jsonpath='{.data.LOG_LEVEL}' # Edit a ConfigMap in-place (for emergency changes — always follow up with a Git commit) kubectl edit configmap app-config -n production

Immutable ConfigMaps

Kubernetes 1.21+ supports marking a ConfigMap as immutable by setting immutable: true in the manifest. Immutable ConfigMaps cannot be updated — you must delete and recreate them. In exchange, Kubernetes stops watching them for changes, which measurably reduces API server and kubelet load at large scale (clusters with tens of thousands of ConfigMaps). Google SRE teams at Kubernetes scale use immutable ConfigMaps as a practice: configuration changes are version-bumped (e.g., app-config-v42app-config-v43) and Deployments are updated to reference the new name. This pattern gives you full configuration history in Git, instant rollback by reverting the Deployment reference, and zero risk of a live ConfigMap being accidentally modified.

# Immutable ConfigMap — cannot be edited after creation apiVersion: v1 kind: ConfigMap metadata: name: app-config-v42 namespace: production immutable: true data: LOG_LEVEL: "warn" MAX_CONNECTIONS: "200"
Naming conventions in production: Large engineering organisations enforce a naming scheme for ConfigMaps: {app}-{component}-{env} (e.g., payments-api-production) and store them in a dedicated namespace per environment. Combined with RBAC that grants the payments ServiceAccount only get access to ConfigMaps in the payments namespace, you achieve least-privilege configuration access — a Pod cannot read another team's ConfigMap even if it tries.