Certified Kubernetes Application Developer (CKAD) #13 ConfigMap and Secret in Depth: volume vs env, Auto-Refresh

In #12 Observability you picked up the tools for looking inside a running app. Now it’s time to handle what configuration that app runs with. If you bake database addresses, feature flags, API keys, and passwords into the manifest or the image, you have to rebuild the image for every environment and sensitive data leaks into the code repository. To pull these configuration values out of code, Kubernetes provides ConfigMap (general configuration) and Secret (sensitive data).

In CKAD, ConfigMap and Secret are the heart of the largest domain, Application Environment, Configuration and Security (25%). The exam goes beyond simply creating them and asks whether to inject via env or volume and when a changed value takes effect. The fundamentals were covered in K8s practical track #6, so this post focuses on the exam-task patterns you’ll use right away.

Creating a ConfigMap #

A ConfigMap is an object that holds configuration as key-value pairs. In the exam, instead of hand-writing YAML, it’s faster to create one instantly with the three source options of kubectl create configmap.

# 1) literal: specify key=value directly
k create configmap app-config \
  --from-literal=APP_COLOR=blue \
  --from-literal=APP_MODE=prod

# 2) file: one file becomes one key (key=filename, value=file contents)
k create configmap nginx-conf --from-file=nginx.conf

# 3) env file: KEY=VALUE lines become key-value pairs all at once
k create configmap app-env --from-env-file=app.properties

The three options produce different structures. With --from-literal and --from-env-file, each entry becomes an individual key, but with --from-file=nginx.conf the filename is the key and the entire file content is a single value. This difference determines how many files get unpacked later when you mount it as a volume, so you have to distinguish them precisely.

Inspect the created ConfigMap with k get configmap app-config -o yaml and you’ll see APP_COLOR: blue and APP_MODE: prod under data as key-value pairs.

Creating a Secret #

A Secret has nearly the same structure as a ConfigMap, but it holds sensitive data such as passwords, tokens, and certificates. Depending on the type, you’ll frequently use three generators.

# generic: ordinary key-value (passwords, API keys, etc.)
k create secret generic db-secret \
  --from-literal=DB_USER=admin \
  --from-literal=DB_PASS=s3cr3t

# docker-registry: private registry credentials (used via imagePullSecrets)
k create secret docker-registry regcred \
  --docker-server=registry.example.com \
  --docker-username=ci \
  --docker-password=token123

# tls: certificate and key pair (Ingress TLS, etc.)
k create secret tls web-tls \
  --cert=tls.crt --key=tls.key

base64 is not encryption #

When you query a Secret with k get secret db-secret -o yaml, the value appears base64-encoded, like DB_PASS: czNjcjN0. The point you must absolutely grasp here is that base64 is encoding, not encryption. Just run echo czNjcjN0 | base64 -d and anyone can recover the original value. base64 is merely a format for moving binary data as text; it guarantees no security. To actually encrypt the data at rest, you need etcd encryption at rest or an external secret-management tool. The exam frequently includes questions probing the concept that “base64 is not encryption.”

When hand-writing YAML, if base64 encoding is a hassle, use stringData instead of data so you can write values in plaintext and Kubernetes encodes them on storage.

Injection method 1: individual keys via env #

To inject one specific key from a ConfigMap or Secret as an environment variable, use valueFrom. ConfigMap uses configMapKeyRef, Secret uses secretKeyRef.

apiVersion: v1
kind: Pod
metadata:
  name: app
spec:
  containers:
    - name: app
      image: nginx
      env:
        - name: APP_COLOR            # environment variable name inside the container
          valueFrom:
            configMapKeyRef:
              name: app-config       # ConfigMap name
              key: APP_COLOR         # the key inside it
        - name: DB_PASSWORD
          valueFrom:
            secretKeyRef:
              name: db-secret
              key: DB_PASS

The environment variable name (name) and the source key (key) can differ. Inside the container you read it as APP_COLOR, but staying aware that the ConfigMap’s key name is separate keeps you from getting confused.

Injection method 2: the whole thing via envFrom #

When there are many keys, writing valueFrom for each one gets long. To unpack all keys at once from a ConfigMap or Secret as environment variables, use envFrom.

apiVersion: v1
kind: Pod
metadata:
  name: app
spec:
  containers:
    - name: app
      image: nginx
      envFrom:
        - configMapRef:
            name: app-config       # every key becomes the env name as-is
        - secretRef:
            name: db-secret

In this case the ConfigMap’s key names become the environment variable names directly, so the key names must be valid as environment variable names, like APP_COLOR. If you want to add a prefix, add prefix: CONFIG_ to the item. You have to remember the exact naming difference: envFrom uses configMapRef , secretRef, while valueFrom uses configMapKeyRef , secretKeyRef.

Injection method 3: file mount via volume #

There are cases where you need to receive configuration as files rather than env. Values that an application reads directly from a path — like an nginx config file or a certificate — are like this. In that case you mount the ConfigMap or Secret as a volume.

apiVersion: v1
kind: Pod
metadata:
  name: app
spec:
  containers:
    - name: app
      image: nginx
      volumeMounts:
        - name: config-vol
          mountPath: /etc/app        # a file is created per key in this directory
  volumes:
    - name: config-vol
      configMap:
        name: app-config

With this mount, one file per ConfigMap key appears under /etc/app/. That is, /etc/app/APP_COLOR (content blue) and /etc/app/APP_MODE (content prod) are created. A Secret mounts the same way, using secret (with the field secretName) instead of configMap.

  volumes:
    - name: secret-vol
      secret:
        secretName: db-secret

subPath: mounting a single file only #

When you mount a volume wholesale, the existing files in the mountPath directory get hidden. For example, if you mount a config directory onto /etc/nginx, all the other files inside it appear to vanish. To insert just one file at a specific path and leave the rest of the directory untouched, use subPath.

      volumeMounts:
        - name: config-vol
          mountPath: /etc/nginx/nginx.conf   # specify down to the file path
          subPath: nginx.conf                # mount only this one key at this path

When you mount with subPath, only the ConfigMap’s nginx.conf key goes into that file path and the other files in the directory are preserved. One caveat: a file mounted via subPath does not auto-refresh, a difference we cover in the very next section.

env vs volume: auto-refresh (an exam regular) #

The same ConfigMap injected via env versus mounted via volume behaves differently when the value changes. This difference comes up repeatedly in CKAD.

Injection methodReflection when ConfigMap/Secret is modified
env (valueFrom / envFrom)Not reflected. Keeps the old value until the Pod restarts
volume mountFile content auto-refreshes after a delay (kubelet sync)
volume + subPathNo auto-refresh (unlike a full mount)

A value injected via env is baked into the process environment once when the container starts, and that’s the end of it. So even if you modify the ConfigMap, the environment variables of an already-running container don’t change, and you have to recreate the Pod to see the new value (restart the Deployment with kubectl rollout restart).

A file mounted via volume, on the other hand, is synced periodically by the kubelet, so when you modify the ConfigMap the mounted file content automatically changes after a delay. However, the application has to re-read that file for the new value to take effect — the file may be refreshed, but it’s meaningless if the process doesn’t reload it. The subPath mount seen earlier is also excluded from this auto-refresh. The “I changed the ConfigMap, why didn’t it change?” trap is almost always either an env injection or a subPath mount.

immutable and optional #

immutable: an unchangeable ConfigMap/Secret #

If a configuration rarely changes, you can set immutable: true to prevent modification. A ConfigMap or Secret marked immutable cannot have its data changed (only delete-and-recreate is possible), and in return the kubelet doesn’t need to watch for changes, which benefits cluster performance.

apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
data:
  APP_MODE: prod
immutable: true

optional: starting up even when absent #

By default, if a referenced ConfigMap or key is absent, the Pod can’t start and waits. If a value might not exist, set optional: true to skip it when it’s absent.

      env:
        - name: FEATURE_FLAG
          valueFrom:
            configMapKeyRef:
              name: app-config
              key: FEATURE_FLAG
              optional: true        # starts up even if the key is absent

downwardAPI in one line #

ConfigMap and Secret aren’t the only values you can inject via env or volume. There’s also the downwardAPI, which injects a Pod’s own metadata (name, namespace, labels, node name, resource limits, and so on) into the container. You use it when the app needs to know information about the Pod rather than external configuration. We cover the details together with projected volumes in #17 Volumes.

Exam points #

  • Distinguish the three sources of the ConfigMap/Secret generator: --from-literal (key=value), --from-file (filename=key), --from-env-file (many lines at once).
  • Drill the three Secret types: generic , docker-registry , tls. Questions probing the concept that base64 is encoding, not encryption come up often.
  • Write the field names of the two env injection methods precisely. For individual keys it’s valueFrom’s configMapKeyRef , secretKeyRef; for the whole thing it’s envFrom’s configMapRef , secretRef.
  • A volume mount creates a file per key, and subPath inserts a single file while preserving the rest of the directory.
  • Auto-refresh is the biggest regular. env doesn’t change until the Pod restarts, a volume refreshes after a delay, and a subPath mount does not refresh.
  • Quick verification is done with kubectl exec. Check environment variables with k exec app -- env, and a mounted file with k exec app -- cat /etc/app/APP_COLOR.

Wrap-up #

What this post locked in:

  • Configuration and sensitive data out of code. Separate them into ConfigMap (general) and Secret (sensitive), and recognize that a Secret’s base64 is not encryption.
  • Creating. The three generator sources (--from-literal , --from-file , --from-env-file) and the three Secret types (generic , docker-registry , tls).
  • Three injection methods. Individual-key env (valueFrom), whole env (envFrom), and file mount (volume , subPath).
  • The auto-refresh difference. env is fixed until restart, volume auto-refreshes, subPath is excluded from refresh.
  • immutable , optional. Immutable configuration, and optional references that still start up when absent.

Next — ServiceAccount and RBAC #

You’ve learned how to put configuration and sensitive data inside the container. Now it’s time to handle what that app is allowed to do against the cluster API — that is, permissions.

In #14 ServiceAccount and RBAC (app perspective), we’ll build it ourselves and lay out which ServiceAccount a Pod runs as, how to grant in-namespace permissions with Role and RoleBinding, the difference from ClusterRole, and the exam-task pattern for verifying permissions with kubectl auth can-i.

X