Externalising configuration and protecting sensitive data in Helm charts
ConfigMaps decouple configuration from container images. Helm templates them from values.yaml or external files.
# templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "myapp.fullname" . }}-config
labels:
{{- include "myapp.labels" . | nindent 4 }}
data:
APP_ENV: {{ .Values.config.env | quote }}
LOG_LEVEL: {{ .Values.config.logLevel | quote }}
# Load an entire file from the chart
nginx.conf: |-
{{ .Files.Get "files/nginx.conf" | indent 4 }}
{{ range $path, $bytes := .Files.Glob "files/**.conf" }} to load multiple config files into a single ConfigMap automatically.env:
- name: APP_ENV
valueFrom:
configMapKeyRef:
name: {{ include "myapp.fullname" . }}-config
key: APP_ENV
# Or load all keys at once:
envFrom:
- configMapRef:
name: {{ include "myapp.fullname" . }}-config
volumes:
- name: config-vol
configMap:
name: {{ include "myapp.fullname" . }}-config
containers:
- volumeMounts:
- name: config-vol
mountPath: /etc/myapp
readOnly: true
Kubernetes supports three common Secret types: Opaque, kubernetes.io/tls, and kubernetes.io/dockerconfigjson.
# templates/secret.yaml
apiVersion: v1
kind: Secret
metadata:
name: {{ include "myapp.fullname" . }}-secret
type: Opaque
# Option A: base64-encoded data
data:
DB_PASSWORD: {{ .Values.secrets.dbPassword | b64enc | quote }}
# Option B: plaintext stringData (Kubernetes base64-encodes it)
stringData:
API_KEY: {{ .Values.secrets.apiKey | quote }}
data — Values must be base64-encoded. Use the b64enc Helm function.
stringData — Accepts plaintext. Kubernetes encodes it on creation. Convenient but shows plaintext in the template.
values.yaml, they are plaintext in Git. This is the #1 Helm security mistake.kubeseal; only the cluster can decrypthelm secrets wraps SOPS/age to encrypt values files before committing# templates/external-secret.yaml
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: {{ include "myapp.fullname" . }}-es
spec:
refreshInterval: 1h
secretStoreRef:
name: aws-secrets-manager
kind: ClusterSecretStore
target:
name: {{ include "myapp.fullname" . }}-secret
creationPolicy: Owner
data:
- secretKey: DB_PASSWORD
remoteRef:
key: prod/myapp/db
property: password
- secretKey: API_KEY
remoteRef:
key: prod/myapp/api
property: key
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "myapp.fullname" . }}-config
immutable: true
data:
APP_ENV: {{ .Values.config.env | quote }}
Immutable resources reduce API server load. To update, delete and recreate (Helm handles this on upgrade).
# templates/deployment.yaml
spec:
template:
metadata:
annotations:
checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
checksum/secret: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }}
When config changes, the annotation changes, triggering a rolling update automatically.
refreshInterval to poll the vault periodically (e.g., 15m)rotationPollInterval for mounted volumesrefreshInterval for fully automated secret rotation without manual helm upgrade calls.