Helm chart config pattern: Files.Get, tpl, and the config/ directory
Published: 2026-05-25
Helm has two ways to get external content into a chart: values.yaml and .Files.Get.
For application config files — appsettings.json, nginx.conf, log4j.properties — the
Files.Get approach beats dumping multi-line blobs into values. This post covers the
pattern we settled on after shipping ~60 .NET microservices: store config as template
files in config/, render secrets in CI, pack them into ConfigMaps with Files.Get,
and force rolling restarts via checksum annotations.
The problem with values.yaml for config
A typical temptation is to put config inside values.yaml:
yaml# Don't do this
config:
connectionString: "Server=db;Database=app;User=app;Password=secret"
rabbitHost: "rabbit.infra"
Then in the template:
yamldata:
appsettings.json: |
{
"ConnectionStrings": { "Default": "{{ .Values.config.connectionString }}" },
"RabbitMq": { "Host": "{{ .Values.config.rabbitHost }}" }
}
The problems:
values.yamlends up with 40+ keys for a moderately complex service.- Multi-line values in YAML are painful (indentation, escape sequences).
- You can't use conditionals (
if eq $env "Production") inside values. - Secrets appear in plaintext in
values-Production.yaml.
The Files.Get pattern
Instead, keep appsettings.json as a proper JSON file inside config/:
my-service/
config/
appsettings.json ← rendered by consul-template at deploy time
appsettings.ENV.json ← per-env overrides, also rendered
crt/
YC-CA.pem ← CA cert, rendered from Vault
files/
config.hcl ← consul-template source for appsettings.json
config-user.hcl ← source for appsettings.ENV.json
config-crt.hcl ← source for YC-CA.pem
The configmap.yaml template loads the rendered files with Files.Get:
yamlapiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "my-service.fullname" . }}-app-settings
data:
appsettings.json: |
{{ tpl (.Files.Get "config/appsettings.json") . | indent 4 }}
appsettings.{{ .Values.aspnetcoreEnvironment }}.json: |
{{ tpl (.Files.Get "config/appsettings.ENV.json") . | indent 4 }}
Note tpl wrapping .Files.Get: this passes the file content through Helm's template
engine, allowing {{ .Values.* }} references inside the JSON file. Useful for things
like {{ .Values.replicaCount }} or {{ .Release.Namespace }} inside config.
Why indent 4 and not nindent
indent 4 adds 4 spaces to every line. nindent 4 adds a leading newline and then
4 spaces. For the multiline string block scalar (|) in YAML, indent is correct —
the first line must start at the same indentation as the rest.
If you use nindent, you get an extra blank line at the top of the config value which
is harmless for most parsers but looks wrong in kubectl get cm -o yaml.
CA certificate ConfigMap
The CA PEM is in its own ConfigMap so you can update it independently:
yamlapiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "my-service.fullname" . }}-ca-pemstore
data:
YC-CA.pem: |
{{ .Files.Get "crt/YC-CA.pem" | indent 4 }}
No tpl here — the PEM is literal text, not a template.
Deployment volumeMounts
The two ConfigMaps mount into the container at the exact paths the .NET runtime expects:
yamlvolumeMounts:
- name: config
mountPath: /app/appsettings.json
subPath: appsettings.json
- name: config
mountPath: /app/appsettings.{{ .Values.aspnetcoreEnvironment }}.json
subPath: appsettings.{{ .Values.aspnetcoreEnvironment }}.json
- name: ca-pemstore
mountPath: /etc/ssl/certs/YC-CA.pem
subPath: YC-CA.pem
readOnly: true
volumes:
- name: config
configMap:
name: {{ include "my-service.fullname" . }}-app-settings
- name: ca-pemstore
configMap:
name: {{ include "my-service.fullname" . }}-ca-pemstore
subPath mounts a single key from the ConfigMap as a file, without replacing the entire
directory. Without subPath, mounting at /app would hide all other files in /app.
Forced rolling restart on config change
The Deployment .spec.template.metadata.annotations contains a hash of each ConfigMap:
yamlmetadata:
annotations:
checksum/configmap: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
checksum/configmap-ca: {{ include (print $.Template.BasePath "/capem-configmap.yaml") . | sha256sum }}
How it works:
include (print $.Template.BasePath "/configmap.yaml") .renders the ConfigMap template to a string.sha256sumhashes it.- The hash is stored in the Pod template annotation.
- When Vault secrets change and the pipeline re-renders
config/appsettings.json, the ConfigMap content changes, so the hash changes, so the Pod template spec changes, so Kubernetes triggers a rolling update automatically.
Without this annotation, helm upgrade would update the ConfigMap but leave existing
pods running with the old in-memory config (since Kubernetes doesn't restart pods when
a mounted ConfigMap changes unless you use a projected volume with subPath — which
also doesn't auto-reload).
The ENV placeholder
The appsettings.ENV.json file name uses the literal string ENV in git. During the
consul-template -once call in CI, the output path contains the real environment name:
bashconsul-template \
-template="chart/files/config-user.hcl:chart/config/appsettings.ENV.json" \
-once
Wait — ENV stays as-is here. The aspnetcoreEnvironment value (Development,
Production, etc.) is passed to the deployment via --set aspnetcoreEnvironment=${APP_ENV}.
The configmap template then uses {{ .Values.aspnetcoreEnvironment }} to name the key,
so the container gets /app/appsettings.Production.json mounted at runtime.
Helm lint
helm lint validates the chart templates. Since config/appsettings.json is
git-ignored (it's a render artifact), add a stub file so lint passes:
json{}
Commit this placeholder as config/appsettings.json. consul-template will overwrite it
during deploy. helm lint and local helm template invocations see an empty JSON
object, which is valid.
Summary
| Concern | Solution |
|---|---|
| Secrets in config | consul-template renders from Vault before helm upgrade |
| Config file format | Raw JSON/YAML in config/, not embedded in values.yaml |
| Helm template access | Files.Get + optional tpl |
| Mount into pod | subPath volumeMount per file |
| Rolling update on change | checksum/* annotation in Pod template |
| lint/template locally | Stub config/ files in git |