Kubernetes: ConfigMaps, Secrets & Spring Boot Property Sources

Spring Boot reads configuration from many sources: environment variables, property files, and system properties. On Kubernetes, ConfigMaps provide non-sensitive config and Secrets provide sensitive values (passwords, API keys, TLS certificates). This article shows how to create both, inject them into your Spring Boot pods, enable live refresh without restarting, and manage secrets securely in production.

Share

Contents

ConfigMap: Creating & Using

A ConfigMap holds configuration as key-value pairs. A pod can receive it in two ways: each key as an environment variable, or each key as a file. The two ways suit different kinds of settings, so this article uses one ConfigMap for each:

YAML
# configmap.yaml
# Settings passed as environment variables
apiVersion: v1
kind: ConfigMap
metadata:
  name: payment-service-env
  namespace: payments
data:
  LOGGING_LEVEL_ROOT: "INFO"
  FEATURE_NEW_CHECKOUT: "true"
  APP_RETRY_MAX_ATTEMPTS: "3"
---
# A whole properties file, passed as a file
apiVersion: v1
kind: ConfigMap
metadata:
  name: payment-service-config
  namespace: payments
data:
  application.properties: |
    spring.jpa.show-sql=false
    app.payment.timeout-seconds=30

You can also create ConfigMaps from the command line:

Shell
# From individual values
kubectl create configmap payment-service-env   --from-literal=LOGGING_LEVEL_ROOT=INFO   --from-literal=APP_RETRY_MAX_ATTEMPTS=3   -n payments

# From an existing file; the key becomes the file name
kubectl create configmap payment-service-config   --from-file=application.properties=./k8s/application-prod.properties   -n payments

# Show what is stored
kubectl get configmap payment-service-config -n payments -o yaml

Injecting as Environment Variables

envFrom turns every key of a ConfigMap into an environment variable. env with configMapKeyRef picks a single key, and lets you give it a different name:

YAML
# In the Deployment's pod template
spec:
  containers:
    - name: payment-service
      image: myorg/payment-service:1.0.0
      # Every key of the ConfigMap as an environment variable
      envFrom:
        - configMapRef:
            name: payment-service-env
      # Or one key, under a name of your choice
      env:
        - name: LOGGING_LEVEL_COM_EXAMPLE
          valueFrom:
            configMapKeyRef:
              name: payment-service-env
              key: LOGGING_LEVEL_ROOT

Spring Boot reads environment variables as properties. To find the variable for a property, replace dots and dashes with underscores and write it in capitals: spring.datasource.url becomes SPRING_DATASOURCE_URL, and feature.new-checkout becomes FEATURE_NEW_CHECKOUT. A variable that matches no property, such as LOG_LEVEL, is simply ignored.

Injecting as Volume-Mounted Files

Mounting a ConfigMap as a volume creates one file per key. Point Spring Boot at the folder with SPRING_CONFIG_ADDITIONAL_LOCATION; it then reads application.properties from there in addition to the one in the JAR, and the mounted file wins where both set the same property:

YAML
spec:
  containers:
    - name: payment-service
      image: myorg/payment-service:1.0.0
      env:
        - name: SPRING_CONFIG_ADDITIONAL_LOCATION
          value: "optional:file:/etc/config/"
      volumeMounts:
        - name: app-config
          mountPath: /etc/config
          readOnly: true
  volumes:
    - name: app-config
      configMap:
        name: payment-service-config
When the ConfigMap changes, Kubernetes updates the mounted files within about a minute, without restarting the pod. Environment variables never change while a pod runs. But Spring Boot reads its configuration only at startup, so a changed file has no effect until the pod restarts, unless you use the reload feature described below. Files mounted with subPath are not updated at all.

Kubernetes Secrets

A Secret works like a ConfigMap but is meant for passwords, keys and certificates. Values under data are base64-encoded; values under stringData are plain text, and Kubernetes encodes them for you. Base64 is an encoding, not encryption: anyone who can read the Secret can read the values.

YAML
# secret.yaml — base64-encoded values (use Sealed Secrets or ESO in production)
apiVersion: v1
kind: Secret
metadata:
  name: payment-service-secrets
  namespace: payments
type: Opaque
data:
  # Values must be base64-encoded
  DB_PASSWORD: cGFzc3dvcmQxMjM=      # echo -n "password123" | base64
  STRIPE_API_KEY: c2tfbGl2ZV8uLi4=
stringData:
  # stringData is plain text — Kubernetes base64-encodes it
  REDIS_PASSWORD: "my-redis-password"
Shell
# Create from literals (Kubernetes does the base64 encoding)
kubectl create secret generic payment-service-secrets \
  --from-literal=DB_PASSWORD=password123 \
  --from-literal=STRIPE_API_KEY=sk_live_... \
  -n payments

# Create TLS secret
kubectl create secret tls api-tls-cert \
  --cert=tls.crt \
  --key=tls.key \
  -n payments
YAML
# Mount secrets as environment variables
spec:
  containers:
    - name: payment-service
      envFrom:
        - secretRef:
            name: payment-service-secrets
      # Or individual secret keys
      env:
        - name: DB_PASSWORD
          valueFrom:
            secretKeyRef:
              name: payment-service-secrets
              key: DB_PASSWORD

Spring Cloud Kubernetes Config

Spring Cloud Kubernetes reads ConfigMaps and Secrets directly from the Kubernetes API: no environment variable mapping needed. The ConfigMap name matches spring.application.name by default.

XML
<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-kubernetes-client-config</artifactId>
</dependency>
YAML
# application.yml
spring:
  application:
    name: payment-service   # without "name" below, a ConfigMap named "payment-service" is loaded
  config:
    import: "kubernetes:"   # needed since Spring Cloud 2020.0, which no longer uses bootstrap.yml
  cloud:
    kubernetes:
      config:
        enabled: true
        name: payment-service-config     # override name
        namespace: payments
      secrets:
        enabled: true
        name: payment-service-secrets
        namespace: payments
YAML
# RBAC — give the pod permission to read ConfigMaps and Secrets
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: config-reader
  namespace: payments
rules:
  - apiGroups: [""]
    resources: ["configmaps", "secrets", "pods"]
    verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: config-reader-binding
  namespace: payments
subjects:
  - kind: ServiceAccount
    name: default
    namespace: payments
roleRef:
  kind: Role
  name: config-reader
  apiGroup: rbac.authorization.k8s.io

This Role lets the pod read every ConfigMap and Secret in the namespace. To follow the least-privilege advice at the end of this article, list the exact objects under resourceNames, and leave out secrets if you inject Secrets as files or environment variables instead.

Auto-Refresh with @RefreshScope

Spring Cloud Kubernetes can watch ConfigMaps for changes and trigger a context refresh without restarting the pod.

YAML
# application.yml
spring:
  cloud:
    kubernetes:
      reload:
        enabled: true
        mode: polling          # or "event" for watch-based (requires RBAC watch verb)
        period: 15000          # poll every 15 seconds
        strategy: refresh      # refresh vs restart_context vs shutdown

With the refresh strategy, two kinds of beans see the new values: beans annotated with @RefreshScope, which are recreated, and @ConfigurationProperties classes, which are rebound. Values injected with @Value into other beans keep their old values.

Java
// Beans annotated with @RefreshScope are recreated after a reload
@RefreshScope
@Service
public class FeatureFlagService {

    @Value("${feature.new-checkout:false}")
    private boolean newCheckoutEnabled;

    public boolean isNewCheckoutEnabled() { return newCheckoutEnabled; }
}
The reload settings have changed between Spring Cloud Kubernetes versions, and newer versions recommend the separate Configuration Watcher service instead. Check the reference documentation for the version you use.

External Secrets Operator (ESO)

The External Secrets Operator syncs secrets from AWS Secrets Manager, HashiCorp Vault, GCP Secret Manager, or Azure Key Vault into Kubernetes Secrets, keeping the source of truth outside the cluster. The examples use the current v1 API; older operator versions only offer v1beta1, with the same fields.

YAML
# SecretStore — points to AWS Secrets Manager
apiVersion: external-secrets.io/v1
kind: SecretStore
metadata:
  name: aws-secrets-manager
  namespace: payments
spec:
  provider:
    aws:
      service: SecretsManager
      region: us-east-1
      auth:
        serviceAccount:           # use IRSA for RBAC
          name: external-secrets-sa
---
# ExternalSecret — defines which secrets to sync
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
  name: payment-service-secrets
  namespace: payments
spec:
  refreshInterval: 1h
  secretStoreRef:
    name: aws-secrets-manager
    kind: SecretStore
  target:
    name: payment-service-secrets    # resulting K8s Secret name
    creationPolicy: Owner
  data:
    - secretKey: DB_PASSWORD         # key in the K8s Secret
      remoteRef:
        key: payment-service/prod    # path in AWS Secrets Manager
        property: db_password        # field within the secret JSON
    - secretKey: STRIPE_API_KEY
      remoteRef:
        key: payment-service/prod
        property: stripe_api_key

Secrets Best Practices

  • Never commit secrets to Git: even base64-encoded K8s secret YAML. Use Sealed Secrets or the External Secrets Operator.
  • Enable encryption at rest: configure EncryptionConfiguration in your cluster to encrypt Secrets in etcd.
  • Least privilege RBAC: only grant get on specific secrets; never list or watch all secrets cluster-wide.
  • Prefer files to environment variables: environment variables are passed on to every child process, readable in /proc, and often end up in error reports and diagnostic dumps. Files mounted from a Secret are less exposed, and are updated when the Secret is rotated.
  • Rotate regularly: use ESO's refreshInterval to pull rotated secrets into Kubernetes automatically. The application still has to pick up the new value: files are updated in place, but environment variables only change when the pod restarts.
  • Audit access: enable Kubernetes audit logging for secrets get operations to detect unexpected access.