Week 10: ConfigMaps, Secrets & App Configuration

Hardcoding a database URL or a feature flag into your image means rebuilding it for every environment. ConfigMaps and Secrets externalize configuration from the image entirely, so the exact same image runs in dev, staging and production, differing only in what's mounted at runtime. This week also clears up a widely misunderstood point: a Kubernetes Secret is not, by itself, encrypted.

Module 10 of 17 Week 10 of 18 ~3 Hours Hands-on Exercise Included

By the end of this week, you'll be able to

  • Create ConfigMaps and mount them as environment variables or files
  • Explain exactly why base64 encoding in a Secret isn't the same as encryption
  • Run the same image against dev and prod config without rebuilding it

1. ConfigMaps

A ConfigMap stores non-sensitive configuration as key-value pairs, decoupled from your Pod spec and your image entirely.

configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
data:
  LOG_LEVEL: "info"
  FEATURE_NEW_CHECKOUT: "true"
  MAX_UPLOAD_SIZE_MB: "25"
creating one imperatively (handy for quick iteration)
kubectl create configmap app-config \
  --from-literal=LOG_LEVEL=info \
  --from-literal=FEATURE_NEW_CHECKOUT=true

kubectl get configmap app-config -o yaml

Nothing here is a secret — this is exactly the kind of value that's fine to see in a Git diff or a kubectl describe output: log levels, feature flags, non-sensitive endpoint URLs, timeouts.

The dividing line: would this be fine in a public GitHub repo?

If yes, it's ConfigMap material. If leaking it would let someone impersonate your app, read your data, or bill your cloud account, it belongs in a Secret instead — which Section 3 covers honestly, including what a Secret does not protect against.

2. Mounting as Env Vars vs. Files

A ConfigMap can be consumed two ways — as individual environment variables, or as files inside a mounted volume. Each has a real tradeoff, not just a style preference:

as environment variables
spec:
  containers:
    - name: my-app
      image: myrepo/my-app:1.5.0
      envFrom:
        - configMapRef:
            name: app-config
      # every key in app-config becomes an env var: LOG_LEVEL, FEATURE_NEW_CHECKOUT, etc.
as a mounted file
spec:
  containers:
    - name: my-app
      image: myrepo/my-app:1.5.0
      volumeMounts:
        - name: config-volume
          mountPath: /etc/config
  volumes:
    - name: config-volume
      configMap:
        name: app-config
  # each key becomes a FILE at /etc/config/LOG_LEVEL, /etc/config/FEATURE_NEW_CHECKOUT, etc.

The key practical difference: environment variables are set once, at container start — updating the ConfigMap doesn't change a running Pod's env vars until it's restarted. A mounted file updates live (with a short propagation delay) without restarting the Pod, which matters for apps that watch a config file for changes.

Env vars for simple values, files for structured config

A handful of flat key-value settings is a natural fit for env vars. A whole config file — nginx.conf, an application.yaml — is a natural fit for the file-mount approach, letting you keep the same config format your app already expects.

3. Secrets, Honestly

A Secret looks almost identical to a ConfigMap, but here's the part that surprises people the first time: by default, its values are base64-encoded, not encrypted.

base64 is encoding, not encryption
echo -n "supersecretpassword" | base64
# c3VwZXJzZWNyZXRwYXNzd29yZA==

echo -n "c3VwZXJzZWNyZXRwYXNzd29yZA==" | base64 -d
# supersecretpassword    <- anyone can reverse this instantly, no key required

kubectl get secret db-credentials -o yaml
# the "data" field shows base64 — readable by anyone with permission to read the Secret

So what does a Secret actually protect against, if not that? It keeps sensitive values out of Pod specs and ConfigMaps (so they don't show up casually in kubectl describe), and Kubernetes RBAC (Role-Based Access Control) can restrict who's allowed to read Secret objects at all — that access control is the real protection, not the base64 encoding itself.

secret.yaml
apiVersion: v1
kind: Secret
metadata:
  name: db-credentials
type: Opaque
stringData:                  # "stringData" lets you write PLAIN text — Kubernetes
  DB_PASSWORD: devpassword    # base64-encodes it for you on apply, less error-prone
Real encryption-at-rest needs extra setup

By default, Secrets sit in etcd unencrypted (etcd's own disk-level encryption aside). Enabling encryption-at-rest for Secrets specifically is a control-plane configuration step, and production clusters commonly go further with an external secrets manager (Vault, AWS Secrets Manager) that Kubernetes only references — genuinely out of scope for a local learning cluster, but important to know exists.

4. One Image, Many Environments

Put it together: the exact same Deployment manifest, referencing config and secrets by name, runs correctly in every environment just by swapping which ConfigMap and Secret exist under those names.

deployment.yaml — never rebuilt per environment
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
spec:
  replicas: 3
  selector:
    matchLabels:
      app: my-app
  template:
    metadata:
      labels:
        app: my-app
    spec:
      containers:
        - name: my-app
          image: myrepo/my-app:1.5.0    # SAME image tag, every environment
          envFrom:
            - configMapRef:
                name: app-config          # dev has one app-config, prod has another
          env:
            - name: DB_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: db-credentials
                  key: DB_PASSWORD

Dev, staging and prod can each define their own app-config and db-credentials objects (typically in separate namespaces, previewed properly in later weeks) — the Deployment manifest referencing them by name never changes across environments, and neither does the image.

Looking ahead to Helm

Managing separate ConfigMap/Secret YAML per environment by hand gets tedious fast — Week 13's Helm charts template exactly this, letting one chart produce different config per environment from a single set of values files.

5. Hands-on Exercise

Hands-on

Externalize your app's config, then run "two environments" from one image

Move hardcoded values out of your Deployment into a ConfigMap and Secret, then prove the same image can behave differently per environment without a rebuild.

Part 1 — Externalize:

  1. Pick your Week 8/9 app, and identify at least two config values it uses (a log level, a feature flag, a connection string) plus one sensitive value (a password or API key, even a fake one for this exercise).
  2. Create a ConfigMap for the non-sensitive values and a Secret for the sensitive one.
  3. Update your Deployment to consume both via envFrom/env, and confirm with kubectl exec <pod> -- env that the values actually appear inside a running container.
Hint

Changing a ConfigMap/Secret referenced via env vars won't affect an already-running Pod — you'll need to delete the Pods (or trigger a new rollout) after editing the ConfigMap for the new values to actually take effect, exactly as explained in Section 2.

Part 2 — Simulate two environments:

  1. Create a second ConfigMap with a different name (e.g. app-config-prod) containing different values for the same keys — a different log level, a feature flag flipped the other way.
  2. Update your Deployment's configMapRef to point at the new ConfigMap name and reapply — confirm, via env vars inside a running Pod, that the behavior changed, without touching the image tag at all.
  3. Run kubectl get secret db-credentials -o yaml and manually base64-decode the value shown in the output, to see firsthand exactly how little protection the encoding alone provides.
  4. Write two sentences: one explaining what a Secret actually protects against, and one explaining what it does NOT protect against by default.
Hint

On macOS/Linux, decode with echo '<value>' | base64 -d; on Windows PowerShell, use [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String("<value>")).

6. Knowledge Check

Four quick questions. Expand each to check your answer.

Q1

Is a Kubernetes Secret's value encrypted by default?

No — by default it's base64-encoded, which is trivially reversible and provides no real confidentiality on its own. The actual protection comes from RBAC restricting who can read Secret objects, and optionally enabling encryption-at-rest at the control-plane level, or using an external secrets manager.

Q2

You update a ConfigMap that's mounted as environment variables on a running Pod. Does the Pod see the new values immediately?

No. Environment variables are set once at container start — a running Pod keeps its original values until it's restarted or replaced. A ConfigMap mounted as a file, by contrast, updates live inside the Pod (with a short delay), which matters for apps that watch config files for changes.

Q3

What's a good rule of thumb for deciding ConfigMap vs. Secret for a given value?

Ask whether the value would be fine to see in a public GitHub repo. If yes (a log level, a feature flag), it belongs in a ConfigMap. If leaking it would let someone impersonate your app, access data, or incur cost on your behalf (a password, an API key), it belongs in a Secret.

Q4

Why does externalizing config into ConfigMaps/Secrets mean you never need to rebuild an image per environment?

The Deployment manifest references config and secrets by NAME, not by value — each environment can define its own ConfigMap/Secret under that same name with different values. The image itself, and the manifest referencing it, stay identical across dev, staging and prod; only what's mounted at runtime changes.