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 secretin 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@RefreshScopebeans. - 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 yamlshows values plainly. Always use Secret for sensitive data. - Forgetting to base64-encode when writing
datain Secret YAML: causesillegal base64 dataerrors. UsestringDataif you don’t want to encode manually. - Changing ConfigMap but Pod doesn’t pick up new values: because loading via
env/envFromdoesn’t auto-reload — you mustkubectl 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.