ConfigMaps and Secrets decouple configuration from container images, making your applications portable and configurable across environments. ConfigMaps store non-sensitive data, while Secrets store sensitive data like passwords, API keys, and certificates.

Why Separate Configuration?

flowchart TB
    BadImage["Without ConfigMaps/Secrets<br/>Image: myapp:v1<br/>DB_HOST=db.prod<br/>DB_PASS=hunter2"]

    GoodImage["With ConfigMaps/Secrets<br/>Image: myapp:v1<br/>(no config)"]
    GoodImage --> Config["ConfigMap<br/>DB_HOST=db.prod"]
    GoodImage --> Secret["Secret<br/>DB_PASS=***"]

    classDef risky fill:#2a1717,stroke:#d94f4f,color:#f8d7da
    classDef safe fill:#102a22,stroke:#2ab67d,color:#d8fff0
    class BadImage risky
    class GoodImage,Config,Secret safe
ProblemConfigMap/Secret Solution
Config baked into imageConfiguration injected at runtime
Different config per environmentSame image, different ConfigMap per namespace
Secrets in code or env filesSecrets stored separately with RBAC controls and encryption at rest
Config change = new image buildUpdate ConfigMap, restart pod (no rebuild)

ConfigMaps

ConfigMaps store non-sensitive configuration as key-value pairs. Pods consume them as environment variables, command-line arguments, or mounted files.

Creating ConfigMaps

# From literal values
kubectl create configmap app-config \
  --from-literal=DB_HOST=db.production.internal \
  --from-literal=DB_PORT=5432 \
  --from-literal=LOG_LEVEL=info
 
# From a file
kubectl create configmap nginx-config \
  --from-file=nginx.conf=./nginx.conf
 
# From a directory (each file becomes a key)
kubectl create configmap app-configs \
  --from-file=config/   # config/app.json, config/settings.yaml
 
# From an env file
kubectl create configmap env-config \
  --from-env-file=.env
 
# From YAML
kubectl apply -f configmap.yaml

ConfigMap YAML

apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
data:
  DB_HOST: "db.production.internal"
  DB_PORT: "5432"
  LOG_LEVEL: "info"
  # Multi-line value
  app.properties: |
    cache.enabled=true
    cache.ttl=3600
    max.connections=100

Using ConfigMaps in Pods

As environment variables:

apiVersion: v1
kind: Pod
metadata:
  name: my-app
spec:
  containers:
    - name: app
      image: my-app:v1
      envFrom:
        - configMapRef:
            name: app-config      # All keys become env vars
      # OR specific keys:
      env:
        - name: DATABASE_HOST     # Env var name in container
          valueFrom:
            configMapKeyRef:
              name: app-config    # ConfigMap name
              key: DB_HOST        # Key in ConfigMap

As mounted files:

apiVersion: v1
kind: Pod
metadata:
  name: my-app
spec:
  containers:
    - name: app
      image: my-app:v1
      volumeMounts:
        - name: config-volume
          mountPath: /etc/config
          readOnly: true
  volumes:
    - name: config-volume
      configMap:
        name: app-config

Tip: Mounted ConfigMaps are updated automatically when the ConfigMap changes (with a slight delay). Environment variables are NOT updated — the pod must be restarted to pick up new env var values.

ConfigMap Consumption Methods

MethodUpdates Live?Best For
Environment variablesNo (requires pod restart)Simple config values, startup config
Mounted volumeYes (auto-updated within ~60s)Config files, dynamic settings
Command-line argumentsNo (requires pod restart)Entrypoint parameters

Secrets

Secrets store sensitive data. In GKE, Secrets are encrypted at rest by default.

Secret Types

TypePurposeExample
OpaqueArbitrary user data (default)API keys, passwords
kubernetes.io/tlsTLS certificatestls.crt, tls.key
kubernetes.io/dockerconfigjsonContainer registry credentials~/.docker/config.json
kubernetes.io/basic-authBasic authenticationUsername, password
kubernetes.io/ssh-authSSH keysssh-privatekey

Creating Secrets

# From literal values (base64-encoded automatically)
kubectl create secret generic db-credentials \
  --from-literal=username=admin \
  --from-literal=password='S3cureP@ssw0rd!'
 
# From files
kubectl create secret generic tls-secret \
  --from-file=tls.crt=server.crt \
  --from-file=tls.key=server.key
 
# Docker registry credentials
kubectl create secret docker-registry gcr-secret \
  --docker-server=gcr.io \
  --docker-username=_json_key \
  --docker-password="$(cat keyfile.json)" \
  [email protected]
 
# TLS secret
kubectl create secret tls my-tls-secret \
  --cert=cert.pem \
  --key=key.pem

Secret YAML

apiVersion: v1
kind: Secret
metadata:
  name: db-credentials
type: Opaque
data:
  username: YWRtaW4=              # base64 of "admin"
  password: UzNjdXJlUEBzc3cwcmQh   # base64 of "S3cureP@ssw0rd!"

Note: When writing YAML directly, you must base64-encode values. Use echo -n 'admin' | base64. When using kubectl create secret --from-literal, encoding is automatic.

Using Secrets in Pods

As environment variables:

apiVersion: v1
kind: Pod
metadata:
  name: my-app
spec:
  containers:
    - name: app
      image: my-app:v1
      env:
        - name: DB_USERNAME
          valueFrom:
            secretKeyRef:
              name: db-credentials
              key: username
        - name: DB_PASSWORD
          valueFrom:
            secretKeyRef:
              name: db-credentials
              key: password

As mounted files:

apiVersion: v1
kind: Pod
metadata:
  name: my-app
spec:
  containers:
    - name: app
      image: my-app:v1
      volumeMounts:
        - name: secret-volume
          mountPath: /etc/secrets
          readOnly: true
  volumes:
    - name: secret-volume
      secret:
        secretName: db-credentials

As image pull secrets (private registries):

apiVersion: v1
kind: Pod
metadata:
  name: private-app
spec:
  imagePullSecrets:
    - name: gcr-secret
  containers:
    - name: app
      image: gcr.io/my-project/my-app:v1

ConfigMap vs Secret

AspectConfigMapSecret
PurposeNon-sensitive configurationSensitive data
StoragePlain Kubernetes API object dataBase64-encoded Kubernetes API object data, encrypted at rest in GKE
Size limit1 MiB1 MiB
Access controlRBAC on the objectRBAC on the object (more restrictive recommended)
Best practiceApp config, feature flagsPasswords, API keys, certificates

Warning: Base64 encoding is NOT encryption. Always enable encryption at rest for Secrets in GKE (enabled by default). For production, consider using Google Secret Manager.

Secret Security in GKE

Encryption at Rest

GKE encrypts Secrets at rest by default using Google-managed keys. For additional control:

# Enable Customer-Managed Encryption Keys (CMEK) for application-layer secrets encryption
gcloud container clusters update my-cluster \
  --zone=us-central1-a \
  --database-encryption-key=projects/PROJECT/locations/REGION/keyRings/KEYRING/cryptoKeys/KEY
Encryption LevelKey ManagementSecurity
DefaultGoogle-managed keysGood — encrypted at rest
CMEKYour Cloud KMS keyBetter — you control the key
Secret ManagerGoogle Secret ManagerBest — separate from cluster, audit logging

Using Google Secret Manager

For production workloads, store secrets in Google Secret Manager and access them from pods:

# Enable Secret Manager CSI driver in GKE
gcloud container clusters update my-cluster \
  --zone=us-central1-a \
  --enable-secret-manager-csi-driver
 
# Create a secret in Secret Manager
echo -n 'S3cureP@ssw0rd!' | gcloud secrets create db-password --data-file=-
# Pod using Secret Manager CSI driver
apiVersion: v1
kind: Pod
metadata:
  name: my-app
  annotations:
    secrets-store.csi.k8s.io/used: "true"
spec:
  containers:
    - name: app
      image: my-app:v1
      volumeMounts:
        - name: secrets-store
          mountPath: /etc/secrets
          readOnly: true
  volumes:
    - name: secrets-store
      csi:
        driver: secrets-store.csi.k8s.io
        readOnly: true
        volumeAttributes:
          secretProviderClass: "gcp-secret-manager"

Key Insight: Secret Manager provides versioning, audit logging, and fine-grained IAM access control. Use it for secrets that need governance and compliance tracking.

Secret Security Best Practices

PracticeWhyHow
Use Secret Manager for productionBetter governance, audit, rotationCSI driver integration
Enable Workload Identity Federation for GKENo service account keys in pods--workload-pool=PROJECT.svc.id.goog
Restrict Secret RBACLimit who can read secretskubectl get secrets should be restricted
Use immutable: truePrevents accidental modificationAdd immutable: true to Secret and ConfigMap
Never commit Secrets to GitPrevents credential leaksUse Secret Manager or external secret operators
Rotate secrets regularlyReduces blast radius of leaksAutomate rotation with Secret Manager

Immutable ConfigMaps and Secrets

For performance-sensitive workloads, you can mark ConfigMaps and Secrets as immutable:

apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
immutable: true       # Cannot be changed after creation
data:
  LOG_LEVEL: "info"

Tip: Immutable ConfigMaps and Secrets get removed from the kubelet’s watch loop, reducing API server load. Use them for config that never changes at runtime.

Common Commands

CommandPurpose
kubectl get configmapsList ConfigMaps
kubectl describe configmap NAMEView ConfigMap contents
kubectl create configmap NAME --from-literal=key=valueCreate from literal
kubectl create configmap NAME --from-file=pathCreate from file
kubectl get secretsList Secrets
kubectl describe secret NAMEView Secret metadata
kubectl get secret NAME -o jsonpath='{.data.password}' | base64 -dDecode a Secret value
kubectl create secret generic NAME --from-literal=key=valueCreate a Secret
kubectl edit configmap NAMEEdit a ConfigMap in-place

Warning: kubectl describe secret shows metadata but redacts values. kubectl get secret -o yaml shows base64-encoded values. Restrict access appropriately.

Common Pitfalls

PitfallConsequenceFix
Storing secrets in ConfigMapsSensitive data in plaintextUse Secrets (or Secret Manager)
Not setting readOnly: true on mountsPod can modify mounted configAlways use readOnly: true for config volumes
Huge ConfigMaps (>1 MiB)API server rejects creationSplit into multiple ConfigMaps
Hardcoding config in DockerfilesConfig baked into imageUse ConfigMaps for all environment-specific config
Updating ConfigMap env varsPods don’t pick up changesRestart pods after ConfigMap update
Not using immutable flagUnnecessary API server loadUse immutable: true for static config

TL;DR

  • ConfigMaps for non-sensitive config (DB host, log level, feature flags)
  • Secrets for sensitive data (passwords, API keys, certificates)
  • Consume via environment variables (simple, static) or mounted volumes (auto-updated)
  • GKE encrypts Secrets at rest by default; use CMEK or Secret Manager for production
  • Never store secrets in ConfigMaps, code, or container images
  • Use immutable: true for config that never changes at runtime

Resources