← Articulet Kubernetes Zero to Hero Chapter 5.2
Module 5 Storage, Configuration, and Secrets

ConfigMaps and Secrets

In the previous chapter, you gave Pods persistent storage — the external lockers of our Kubernetes city. But applications also need configuration: database hostnames, feature flags, log levels. Some of it is sensitive —...

Chapter 12 of 22

In the previous chapter, you gave Pods persistent storage — the external lockers of our Kubernetes city. But applications also need configuration: database hostnames, feature flags, log levels. Some of it is sensitive — passwords, API tokens, TLS keys. This chapter covers ConfigMaps for non-sensitive data and Secrets for sensitive.


5.2.1 Configuration Separation Principle

The Twelve-Factor App methodology states: store config in environment, not in code. Embed a password into an image and it is permanently etched into every layer, discoverable by anyone who pulls.

Externalizing configuration means the same image runs everywhere. Kubernetes provides ConfigMaps for non-sensitive data and Secrets for sensitive data.


5.2.2 ConfigMaps: The Public Bulletin Board

A ConfigMap stores non-sensitive data as key-value pairs or files. It has two fields: data (UTF-8 text) and binaryData (base64-encoded binary). Hard limit: 1MB (etcd constraint). Don't stuff large files in; use an init container to download at runtime.

apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
data:
  DATABASE_HOST: "mysql.default.svc.cluster.local"
  LOG_LEVEL: "INFO"
  app.properties: |
    max.connections=100
    timeout.seconds=30

Consumption Pattern 1: Environment Variables

spec:
  containers:
    - name: app
      image: myapp:1.0
      envFrom:
        - configMapRef:
            name: app-config

Each key becomes an environment variable. For selective injection, use valueFrom.

Consumption Pattern 2: Volume Mounts

spec:
  containers:
    - name: app
      image: myapp:1.0
      volumeMounts:
        - name: config-vol
          mountPath: /etc/config
  volumes:
    - name: config-vol
      configMap:
        name: app-config

Each key becomes a file; each value becomes the file contents. This is the only pattern that supports hot-reload — files refresh automatically when the ConfigMap changes, without restarting the Pod. Environment variables are set at startup and never change.

Consumption Pattern 3: Command-Line Arguments

spec:
  containers:
    - name: app
      image: myapp:1.0
      command: ["/bin/myapp"]
      args: ["--log-level=$(LOG_LEVEL)"]
      env:
        - name: LOG_LEVEL
          valueFrom:
            configMapKeyRef:
              name: app-config
              key: LOG_LEVEL

Visual Description: ConfigMap Consumption Patterns

graph LR subgraph "ConfigMap [app-config]" K1[DATABASE_HOST] K2[LOG_LEVEL] K3[app.properties] end subgraph "Pod [web-app]" C[Container] end K1 -->|envFrom| C K2 -->|valueFrom| C K3 -->|volumeMount| C style K1 fill:#ce93d8 style K2 fill:#ce93d8 style K3 fill:#ce93d8 style C fill:#90caf9

🛑 PAUSE & RECALL — 2 minutes

Without looking back:

  1. What is the size limit for a ConfigMap, and why does it exist?
  2. Which consumption pattern supports hot-reloading without a Pod restart?
  3. What is the difference between the data and binaryData fields?

Rate your confidence (0–4).


5.2.3 Secrets: The Filing Cabinet

A Secret stores sensitive data — passwords, tokens, keys — using the same structure as a ConfigMap. Kubernetes Secrets use base64 encoding, not encryption. Base64 is transport encoding, not security. Anyone with read access can decode instantly: echo 'cGFzc3dvcmQxMjM=' | base64 -d. No key, no decryption.

⚠️ Common Misconception: "I put my password in a Secret, so it's secure." No. The Secret type offers RBAC scoping, but base64 alone provides zero confidentiality.

# Create a Secret
kubectl create secret generic db-password \
  --from-literal=password=Sup3rS3cret!

# View encoded, then decode — anyone can do this
kubectl get secret db-password -o jsonpath='{.data.password}'
kubectl get secret db-password -o jsonpath='{.data.password}' | base64 -d

Built-in types: Opaque (default), kubernetes.io/tls, kubernetes.io/dockerconfigjson, kubernetes.io/service-account-token.

Secrets support the same three consumption patterns. Prefer volume mounts over env vars — env vars appear in process listings and crash dumps; mounted files stay out of the process environment.

Visual Description: Encoding vs. Encryption

graph TD subgraph "Base64 Encoding [NOT Security]" A1[secret123] -->|encode| B1[c2VjcmV0MTIz] B1 -->|anyone decodes| A1 end subgraph "Encryption at Rest [Real Security]" A2[secret123] -->|encrypt with key| B2[AES-256 ciphertext] B2 -->|no key = useless| C2[garbled data] end style A1 fill:#ef9a9a style A2 fill:#a5d6a7 style C2 fill:#a5d6a7

5.2.4 Secret Security: Beyond Base64

To actually secure Secrets, use three layers:

Encryption at rest. Configure the API server with EncryptionConfiguration (AES-GCM or KMS). GKE enables this by default with Google-managed keys; upgrade to CMEK for more control.

RBAC restriction. Never grant blanket Secret read access. Use namespace-scoped Roles. Audit with kubectl auth can-i --list.

External secret management. Use External Secrets Operator to sync from Google Secret Manager or Vault — like moving documents from a filing cabinet to a bank safety deposit box with rotation and audit logging.

Avoiding exposure. Never print Secrets in logs. Prefer volume mounts. Ensure logging libraries redact known Secret patterns.


🛑 PAUSE & RECALL — 3 minutes

Without looking back:

  1. If base64 is not encryption, what does it actually do?
  2. Name two reasons volume mounts are safer than env vars for Secrets.
  3. What is the purpose of an external secret manager in a Kubernetes workflow?

Rate your confidence (0–4).


5.2.5 GKE Integration

GKE Note: GKE provides powerful integrations for config and secret management.

Workload Identity is the GKE best practice for Pod-to-GCP authentication without storing service account keys. Bind a Kubernetes ServiceAccount to a Google IAM ServiceAccount. GKE's metadata server exchanges the Kubernetes identity for a short-lived OAuth2 token at runtime. No keys stored, no rotation needed.

sequenceDiagram participant Pod as Pod participant WI as GKE Metadata Server participant IAM as Google IAM participant API as GCP API Pod->>WI: Request access token WI->>IAM: Exchange K8s identity IAM-->>WI: Short-lived OAuth2 token WI-->>Pod: Return token Pod->>API: Call with token

Google Secret Manager CSI Driver mounts secrets directly from Secret Manager into Pods as volumes. Secrets never touch etcd — they flow from vault to filesystem, with automatic rotation.

Config Connector manages GCP resources as Kubernetes objects, keeping infrastructure and app config in the same Git workflow.


5.2.6 Analogy: Community Bulletin Boards and Sealed Envelopes

Analogy: Community Bulletin Boards and Sealed Envelopes

Imagine the Kubernetes cluster as a residential community. In the lobby, a community bulletin board posts notices anyone can read: trash schedules, pool hours, Wi-Fi names. This is your ConfigMap — public, non-sensitive, meant for broad consumption. Any resident (Pod) walks up and reads it.

Behind the reception desk, a locked filing cabinet holds sealed envelopes: SSNs, bank details, spare keys. This is your Secret — sensitive, access-controlled. Only the building manager with the right keycard (RBAC-authorized ServiceAccount) can open it.

For the most sensitive items, the community uses an off-site bank vault with armed guards and MFA — your external secret manager with rotation, audit logs, and disaster recovery.

Analogy Check: Where does this stretch? Bulletin boards have limited space — that 1MB limit. A base64-encoded Secret is more like an envelope with a transparent window; anyone can read it without real encryption.


5.2.7 Visual Description: Office Filing System

Visual Description: Office Filing System Diagram

graph TD subgraph "Community Lobby" CM[ConfigMap<br/>Bulletin Board<br/>— Anyone can read] end subgraph "Admin Office" SEC[Secret<br/>Locked Filing Cabinet<br/>— RBAC-restricted] end subgraph "Off-Site Bank Vault" ESM[Google Secret Manager<br/>— IAM + encrypted at rest] end subgraph "Residents [Pods]" P1[Pod A] -->|read| CM P2[Pod B] -->|volumeMount<br/>restricted| SEC P3[Pod C] -->|CSI driver| ESM end style CM fill:#ce93d8 style SEC fill:#ef9a9a style ESM fill:#90caf9 style P1 fill:#a5d6a7 style P2 fill:#a5d6a7 style P3 fill:#a5d6a7

This shows the spectrum: ConfigMaps on the open board, Secrets in the locked cabinet, external managers in the bank vault.


🤔 TRY BEFORE YOU SEE

You're designing a three-tier app on GKE: web frontend, API backend, MySQL database. Classify each item — ConfigMap, Kubernetes Secret, or Google Secret Manager? Write before checking.

  1. MySQL root password
  2. API log level (DEBUG/INFO/WARN)
  3. GCP service account JSON key for Cloud Storage
  4. Database connection hostname
  5. TLS certificate and private key
  6. Feature flags for beta UI

Reveal:

  1. Google Secret Manager — highest sensitivity; rotates frequently
  2. ConfigMap — non-sensitive operational tuning
  3. Workload Identity — never store a JSON key! Use WI instead
  4. ConfigMap — non-sensitive infrastructure address
  5. Secret (type: tls) — built-in type fits; rotate regularly
  6. ConfigMap — non-sensitive behavior flags

Chose Secret for item 3? Workload Identity eliminates stored GCP keys entirely.


Lab: LAB-5.2 — Configuration and Secrets (45 min)

Step 1: Create and Consume a ConfigMap (10 min)

kubectl create configmap app-config \
  --from-literal=LOG_LEVEL=DEBUG \
  --from-literal=MAX_CONNECTIONS=100

kubectl apply -f - <<'EOF'
apiVersion: v1
kind: Pod
metadata:
  name: config-demo
spec:
  containers:
    - name: demo
      image: busybox:1.36
      command: ["sh", "-c", "env && ls /etc/config && sleep 3600"]
      envFrom:
        - configMapRef:
            name: app-config
      volumeMounts:
        - name: cfg-vol
          mountPath: /etc/config
  volumes:
    - name: cfg-vol
      configMap:
        name: app-config
EOF

kubectl wait --for=condition=Ready pod/config-demo
kubectl logs config-demo

Expected: LOG_LEVEL=DEBUG and MAX_CONNECTIONS=100 in env vars, plus files under /etc/config.

Step 2: Verify ConfigMap Hot-Reload (5 min)

kubectl patch configmap app-config --type merge -p '{"data":{"LOG_LEVEL":"INFO"}}'
sleep 5
kubectl exec config-demo -- cat /etc/config/LOG_LEVEL

Expected: INFO — file updated without restart. Env var still shows DEBUG (set at startup, immutable).

Step 3: Create and Inspect a Secret (10 min)

kubectl create secret generic db-creds --from-literal=password=SuperSecret123!

# Decode — NOT decryption
kubectl get secret db-creds -o jsonpath='{.data.password}' | base64 -d

# Mount as volume
kubectl apply -f - <<'EOF'
apiVersion: v1
kind: Pod
metadata:
  name: secret-demo
spec:
  containers:
    - name: demo
      image: busybox:1.36
      command: ["sh", "-c", "cat /etc/secrets/password && sleep 3600"]
      volumeMounts:
        - name: sec-vol
          mountPath: /etc/secrets
          readOnly: true
  volumes:
    - name: sec-vol
      secret:
        secretName: db-creds
EOF

kubectl wait --for=condition=Ready pod/secret-demo
kubectl exec secret-demo -- cat /etc/secrets/password

Expected: SuperSecret123! — base64 is trivially reversible.

Step 4: Configure Workload Identity (20 min)

Requires GKE with Workload Identity enabled.

export GSA_NAME=app-workload-sa
export PROJECT_ID=$(gcloud config get-value project)

# Create Google IAM SA
gcloud iam service-accounts create $GSA_NAME \
  --display-name="Workload Identity SA"
gcloud projects add-iam-policy-binding $PROJECT_ID \
  --member="serviceAccount:$GSA_NAME@$PROJECT_ID.iam.gserviceaccount.com" \
  --role="roles/storage.objectViewer"

# Create K8s SA and annotate for Workload Identity
kubectl create serviceaccount app-ksa
kubectl annotate serviceaccount app-ksa \
  iam.gke.io/gcp-service-account="$GSA_NAME@$PROJECT_ID.iam.gserviceaccount.com"

# Bind IAM SA to K8s SA
gcloud iam service-accounts add-iam-policy-binding \
  "$GSA_NAME@$PROJECT_ID.iam.gserviceaccount.com" \
  --role="roles/iam.workloadIdentityUser" \
  --member="serviceAccount:$PROJECT_ID.svc.id.goog[default/app-ksa]"

# Test — zero stored keys
kubectl run wi-test --rm -it --serviceaccount=app-ksa \
  --image=google/cloud-sdk:slim -- bash -c "gcloud auth list && gsutil ls"

Expected: gcloud auth list shows the IAM SA email. gsutil ls works with zero keys.

Step 5: Cleanup

kubectl delete pod/config-demo pod/secret-demo
kubectl delete configmap app-config
kubectl delete secret db-creds
kubectl delete serviceaccount app-ksa
gcloud iam service-accounts delete "$GSA_NAME@$PROJECT_ID.iam.gserviceaccount.com" --quiet

Chapter Summary

ConfigMaps and Secrets externalize configuration from container images. ConfigMaps store non-sensitive data on the public bulletin board and support hot-reload via volume mounts. Secrets store sensitive data in the locked filing cabinet, but base64 is encoding, not encryption. Real security requires encryption at rest, RBAC, and external secret managers. On GKE, Workload Identity eliminates stored GCP service account keys.


📇 KEY CONCEPT CARDS

  1. Q: What is the fundamental difference between a ConfigMap and a Secret?
    A: ConfigMaps store non-sensitive configuration in plain text. Secrets store sensitive data base64-encoded (not encrypted by default). Both support the same three consumption patterns: environment variables, volume mounts, and command-line arguments.
  1. Q: Why are mounted ConfigMap volumes preferred over env vars for configuration that might change?
    A: Volume-mounted ConfigMaps update automatically in running Pods when modified (hot-reload). Environment variables are set at container startup and never change.
  1. Q: What is the critical difference between base64 encoding and encryption?
    A: Base64 encoding is a reversible transformation with no key — anyone can decode it instantly. Encryption requires a key and is hard to reverse without it. Kubernetes Secrets use base64 by default; encryption at rest must be explicitly configured.
  1. Q: How does GKE Workload Identity allow Pods to access GCP APIs without storing service account keys?
    A: Workload Identity binds a Kubernetes ServiceAccount to a Google IAM ServiceAccount. The GKE metadata server exchanges the K8s identity for a short-lived OAuth2 token at runtime. No keys are stored or exposed.