ConfigMap and Secret in Kubernetes: Separating Configuration from Image

1. Overview

When packaging your application as a container image, you shouldn’t hardcode values like database URLs and API keys into the image. This article helps you understand ConfigMap and Secret in Kubernetes — two objects used to separate configuration from your image, how to create them, how to load them into a Pod via environment variables or volumes, and how to integrate them with your Spring Boot application. After reading, you’ll be confident configuring a Deployment that reads configuration dynamically without needing to rebuild the image every time a value changes.

2. What is ConfigMap

ConfigMap is an object that stores configuration data as key-value pairs, unencrypted, for non-sensitive data: service names, feature flags, properties files…

Data in a ConfigMap exists independently from the Pod — you can modify the ConfigMap without rebuilding the image; you just need to restart the Pod (or use a reload mechanism if your application supports it).

3. Creating ConfigMap

3.1. From a YAML file

apiVersion: v1
kind: ConfigMap
metadata:
  name: order-service-config
data:
  APP_ENV: "production"
  LOG_LEVEL: "INFO"
  application.properties: |
    server.port=8080
    spring.jpa.hibernate.ddl-auto=validate

Here, data contains two types: simple keys (APP_ENV, LOG_LEVEL) and a key containing entire file content (application.properties). Apply it using:

kubectl apply -f order-service-config.yaml

3.2. From literal or existing file

kubectl create configmap order-service-config \
  --from-literal=APP_ENV=production \
  --from-file=application.properties=./config/application.properties

This command creates a ConfigMap directly from the command line without writing YAML, which is convenient when scripting in CI/CD.

4. Using ConfigMap in a Pod

4.1. Loading as environment variables

apiVersion: v1
kind: Pod
metadata:
  name: order-service
spec:
  containers:
    - name: order-service
      image: nonladev/order-service:1.0
      envFrom:
        - configMapRef:
            name: order-service-config

envFrom loads all keys in the ConfigMap as environment variables with the same names. The container reads APP_ENV, LOG_LEVEL just like regular environment variables.

If you only want to select a specific key:

      env:
        - name: APP_ENVIRONMENT
          valueFrom:
            configMapKeyRef:
              name: order-service-config
              key: APP_ENV

This approach lets you change the environment variable name in the container (APP_ENVIRONMENT) to differ from the original key in ConfigMap (APP_ENV).

4.2. Loading as a volume

      volumeMounts:
        - name: config-volume
          mountPath: /app/config
  volumes:
    - name: config-volume
      configMap:
        name: order-service-config

Each key in the ConfigMap becomes a file in /app/config (for example /app/config/application.properties). This approach is suitable when your application needs to read entire configuration files instead of individual environment variables.

Note: When mounting as a volume, Kubernetes will automatically update the files when the ConfigMap changes (with a lag of seconds to minutes depending on the kubelet sync period). With environment variables (env/envFrom), the Pod does not automatically reload — you must restart the Pod.

5. What is Secret and how it differs from ConfigMap

Secret has a structure similar to ConfigMap but is intended for sensitive data: passwords, tokens, private keys. Key differences:

Criteria ConfigMap Secret
Encryption No Base64-encoded (not true encryption)
Stored in etcd Plain text Can enable encryption at rest
Shown via kubectl get Shows values Hides values (only shows base64)
Purpose Regular config Sensitive data

Note: Secret only base64-encodes data; it doesn’t encrypt by default. Anyone with permission to get secret in the namespace can decode it. For true security, you need to enable [encryption at rest] in etcd or use an external secret manager (Vault, AWS Secrets Manager) integrated via the External Secrets Operator.

6. Creating Secret

6.1. From YAML (must base64-encode manually)

echo -n "P@ssw0rd123" | base64
# UEBzc3cwcmQxMjM=
apiVersion: v1
kind: Secret
metadata:
  name: order-service-secret
type: Opaque
data:
  DB_PASSWORD: UEBzc3cwcmQxMjM=

The data field requires values that are already base64-encoded. If you don’t want to encode manually, use stringData:

stringData:
  DB_PASSWORD: "P@ssw0rd123"

Kubernetes will auto-encode when storing. Use stringData for readability when writing manifests manually.

6.2. From the command line

kubectl create secret generic order-service-secret \
  --from-literal=DB_PASSWORD=P@ssw0rd123 \
  --from-literal=DB_USERNAME=order_app

7. Using Secret in a Pod

Loading works exactly like ConfigMap:

      envFrom:
        - secretRef:
            name: order-service-secret

or mount a volume:

      volumeMounts:
        - name: secret-volume
          mountPath: /app/secret
          readOnly: true
  volumes:
    - name: secret-volume
      secret:
        secretName: order-service-secret

When mounting Secret as a volume, you should always set readOnly: true and restrict file access permissions using defaultMode if needed:

      secret:
        secretName: order-service-secret
        defaultMode: 0400

8. Integration with Spring Boot

Spring Boot reads environment variables using relaxed binding, so you don’t need special code — just use the correct naming convention.

      envFrom:
        - configMapRef:
            name: order-service-config
        - secretRef:
            name: order-service-secret
      env:
        - name: SPRING_DATASOURCE_USERNAME
          valueFrom:
            secretKeyRef:
              name: order-service-secret
              key: DB_USERNAME
        - name: SPRING_DATASOURCE_PASSWORD
          valueFrom:
            secretKeyRef:
              name: order-service-secret
              key: DB_PASSWORD

Spring Boot automatically maps SPRING_DATASOURCE_USERNAME → spring.datasource.username. In your code, you just need regular @ConfigurationProperties:

@ConfigurationProperties(prefix = "spring.datasource")
public record DataSourceProperties(String url, String username, String password) {
}
@Service
public class PaymentService {

    private final DataSourceProperties dataSourceProperties;

    public PaymentService(DataSourceProperties dataSourceProperties) {
        this.dataSourceProperties = dataSourceProperties;
    }

    public String describeConnection() {
        return "Connecting as %s".formatted(dataSourceProperties.username());
    }
}

PaymentService knows nothing about Kubernetes — it only receives values that Spring Boot has already bound from environment variables provided by ConfigMap/Secret.

8.1. Testing without cluster dependency

You can test the logic by setting mock environment variables via @TestPropertySource, without needing a real Kubernetes cluster:

@SpringBootTest
@TestPropertySource(properties = {
    "spring.datasource.username=order_app",
    "spring.datasource.password=test-pass"
})
class PaymentServiceTest {

    @Autowired
    private PaymentService paymentService;

    @Test
    void shouldDescribeConnectionUsingBoundProperties() {
        assertThat(paymentService.describeConnection())
            .isEqualTo("Connecting as order_app");
    }
}

This test confirms that DataSourceProperties is correctly bound and PaymentService uses the correct values — completely separate from whether those values come from a real ConfigMap or Secret on a cluster.

PaymentServiceTest > shouldDescribeConnectionUsingBoundProperties() PASSED

9. Dynamic updates without Pod restart

If you mount ConfigMap/Secret as a volume, files will auto-update, but Spring Boot won’t automatically re-read them unless you:

  • Use Spring Cloud Kubernetes (spring-cloud-starter-kubernetes-client-config) to poll ConfigMap and refresh @RefreshScope beans.
  • Or implement your own file watcher to re-read mounted files.
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-kubernetes-client-config</artifactId>
</dependency>
spring.cloud.kubernetes.config.enabled=true
spring.cloud.kubernetes.reload.enabled=true
spring.cloud.kubernetes.reload.mode=polling

This configuration enables Spring Cloud Kubernetes to automatically poll ConfigMap on a schedule and refresh beans marked with @RefreshScope, without manual Pod restart.

Note: Continuous polling consumes API server resources. For large clusters, consider using spring.cloud.kubernetes.reload.mode=event (watch API) instead of polling.

10. Common mistakes and when not to use

  • Putting passwords in ConfigMap: ConfigMap isn’t encrypted; kubectl get configmap -o yaml shows values plainly. Always use Secret for sensitive data.
  • Forgetting to base64-encode when writing data in Secret YAML: causes illegal base64 data errors. Use stringData if you don’t want to encode manually.
  • Changing ConfigMap but Pod doesn’t pick up new values: because loading via env/envFrom doesn’t auto-reload — you must kubectl rollout restart deployment.
  • Mounting Secret volume without readOnly: true: the container can overwrite files, causing confusion when debugging.
  • Using Secret as a long-term secret store: Secret in Kubernetes is suitable for runtime configuration, but doesn’t replace dedicated secret managers (Vault, AWS Secrets Manager) when you need audit logs, automatic rotation, or strict versioning.
  • ConfigMap/Secret too large: Kubernetes limits object size to about 1 MiB (due to etcd limits). If you need large config files, consider mounting a PersistentVolume or using an init container to load from object storage.

11. Conclusion

ConfigMap and Secret help you separate configuration from your image, deploying the same image across multiple environments (dev, staging, production) by just changing the configuration objects. ConfigMap is for public data, Secret is for sensitive data — but remember that Secret only base64-encodes by default, it doesn’t truly encrypt. With Spring Boot, you leverage relaxed binding to load values from environment variables without changing business logic code, and you can enable Spring Cloud Kubernetes if you need dynamic configuration reload without restarting the Pod.

kubernetesconfigmapsecretspring-bootdevops