When you're building applications for Kubernetes, one of the first challenges you'll face is: "Where do I put my configuration data?" Should database connection strings live in your Docker image? What about API keys? How do you handle different configurations for development, staging, and production environments?
This is where Kubernetes ConfigMaps and Secrets come to the rescue. In this post, I would like to explore, with you, these essential Kubernetes resources that separate your application code from its configuration, making your deployments more secure, flexible, and maintainable.
The Problem: Hardcoded Configuration
Before we dive into the solution, let's understand the problem. Consider this simple Node.js application:
// app.js - DON'T DO THIS
const express = require('express');
const mysql = require('mysql2');
const app = express();
// Hardcoded configuration - BAD PRACTICE
const config = {
port: 3000,
database: {
host: 'mysql-prod.company.com',
user: 'admin',
password: 'super-secret-password',
database: 'production_db'
},
apiKey: 'sk-1234567890abcdef'
};
const connection = mysql.createConnection(config.database);
app.listen(config.port);
This approach has several critical problems:
- Security Risk: Sensitive data like passwords and API keys are embedded in your code
- Environment Rigidity: The same image can't work across different environments
- Secret Exposure: Passwords end up in your Git repository and Docker images
- Deployment Complexity: Changing configuration requires rebuilding and redeploying your entire application
The Kubernetes Solution: Externalizing Configuration
Kubernetes provides two primary resources for managing configuration:
- ConfigMaps: For non-sensitive configuration data
- Secrets: For sensitive information like passwords, tokens, and keys
Both resources follow the same core principle: separate configuration from application code.
ConfigMaps: Your Configuration Data Store
What is a ConfigMap?
A ConfigMap is a Kubernetes API object that stores configuration data as key-value pairs. Think of it as a dictionary or hash map that your applications can reference at runtime.
Creating ConfigMaps
There are several ways to create ConfigMaps. Let's explore each method:
Method 1: Using kubectl with Literal Values
kubectl create configmap app-config \
--from-literal=PORT=3000 \
--from-literal=DATABASE_HOST=mysql.default.svc.cluster.local \
--from-literal=DATABASE_NAME=myapp \
--from-literal=LOG_LEVEL=info
Method 2: From a Configuration File
First, create a configuration file:
PORT=3000
DATABASE_HOST=mysql.default.svc.cluster.local
DATABASE_NAME=myapp
LOG_LEVEL=info
DEBUG_MODE=false
Then create the ConfigMap:
kubectl create configmap app-config --from-file=app.properties
Method 3: Using YAML Manifests (Recommended)
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
namespace: default
data:
PORT: "3000"
DATABASE_HOST: "mysql.default.svc.cluster.local"
DATABASE_NAME: "myapp"
LOG_LEVEL: "info"
DEBUG_MODE: "false"
# You can also include entire configuration files
app.properties: |
PORT=3000
DATABASE_HOST=mysql.default.svc.cluster.local
DATABASE_NAME=myapp
LOG_LEVEL=info
DEBUG_MODE=false
Apply the ConfigMap:
kubectl apply -f configmap.yaml
Consuming ConfigMaps in Your Applications
There are three primary ways to use ConfigMaps in your pods:
1. Environment Variables
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
replicas: 3
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
spec:
containers:
- name: myapp
image: myapp:latest
env:
# Individual environment variables
- name: PORT
valueFrom:
configMapKeyRef:
name: app-config
key: PORT
- name: DATABASE_HOST
valueFrom:
configMapKeyRef:
name: app-config
key: DATABASE_HOST
# Or load all ConfigMap keys as environment variables
envFrom:
- configMapRef:
name: app-config
2. Volume Mounts
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
replicas: 3
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
spec:
containers:
- name: myapp
image: myapp:latest
volumeMounts:
- name: config-volume
mountPath: /etc/config
readOnly: true
volumes:
- name: config-volume
configMap:
name: app-config
With this approach, your configuration files will be available at /etc/config/ inside the container.
3. Command Line Arguments
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
template:
spec:
containers:
- name: myapp
image: myapp:latest
command: ["./myapp"]
args:
- "--port=$(PORT)"
- "--database-host=$(DATABASE_HOST)"
env:
- name: PORT
valueFrom:
configMapKeyRef:
name: app-config
key: PORT
- name: DATABASE_HOST
valueFrom:
configMapKeyRef:
name: app-config
key: DATABASE_HOST
Secrets: Protecting Sensitive Data
What is a Secret?
A Secret is similar to a ConfigMap, but specifically designed for sensitive data. Kubernetes stores Secrets in base64 encoding and provides additional security features like:
- Secrets are stored in etcd in encrypted form (when encryption at rest is enabled)
- Secrets are only sent to nodes that have pods requiring them
- Secrets are stored in memory (tmpfs) and never written to disk
- Access can be controlled with RBAC policies
Types of Secrets
Kubernetes supports several Secret types:
- Opaque: Generic secrets for arbitrary user data (default)
- kubernetes.io/dockerconfigjson: Docker registry credentials
- kubernetes.io/tls: TLS certificates and keys
- kubernetes.io/service-account-token: Service account tokens
Creating Secrets
Method 1: Using kubectl with Literal Values
kubectl create secret generic app-secrets \
--from-literal=DATABASE_PASSWORD=super-secret-password \
--from-literal=API_KEY=sk-1234567890abcdef \
--from-literal=JWT_SECRET=my-jwt-secret-key
Method 2: From Files
# Create files with sensitive data
echo -n 'super-secret-password' > db-password.txt
echo -n 'sk-1234567890abcdef' > api-key.txt
kubectl create secret generic app-secrets \
--from-file=DATABASE_PASSWORD=db-password.txt \
--from-file=API_KEY=api-key.txt
# Clean up the files
rm db-password.txt api-key.txt
Method 3: Using YAML Manifests
# secret.yaml
apiVersion: v1
kind: Secret
metadata:
name: app-secrets
namespace: default
type: Opaque
data:
# Values must be base64 encoded
DATABASE_PASSWORD: c3VwZXItc2VjcmV0LXBhc3N3b3Jk # super-secret-password
API_KEY: c2stMTIzNDU2Nzg5MGFiY2RlZg== # sk-1234567890abcdef
JWT_SECRET: bXktand0LXNlY3JldC1rZXk= # my-jwt-secret-key
Pro Tip: Use the stringData field to avoid manual base64 encoding:
apiVersion: v1
kind: Secret
metadata:
name: app-secrets
type: Opaque
stringData:
DATABASE_PASSWORD: super-secret-password
API_KEY: sk-1234567890abcdef
JWT_SECRET: my-jwt-secret-key
Consuming Secrets
Secrets are consumed in the same ways as ConfigMaps:
Environment Variables
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
template:
spec:
containers:
- name: myapp
image: myapp:latest
env:
- name: DATABASE_PASSWORD
valueFrom:
secretKeyRef:
name: app-secrets
key: DATABASE_PASSWORD
- name: API_KEY
valueFrom:
secretKeyRef:
name: app-secrets
key: API_KEY
envFrom:
- secretRef:
name: app-secrets
Volume Mounts
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
template:
spec:
containers:
- name: myapp
image: myapp:latest
volumeMounts:
- name: secret-volume
mountPath: /etc/secrets
readOnly: true
volumes:
- name: secret-volume
secret:
secretName: app-secrets
defaultMode: 0400 # Read-only for owner only
In the next post we’ll see how to use secrets and ConfigMap in a complete application setup.