Helm Templates in Files Explained: Customize ConfigMaps and Secrets Content

If you have ever built a Helm chart that includes configuration files, scripts, or property files inside a ConfigMap or Secret, you have probably hit the same wall: the default templating engine only processes YAML files inside the templates/ directory. Everything else is treated as static content.

This is a problem because real-world applications rarely deploy with hardcoded configuration. You need environment-specific values in your .properties files, tokens in your JSON configs, or dynamic hostnames in your shell scripts. Helm provides three core functions to handle this: .Files.Get, .Files.Glob, and tpl. Each solves a different piece of the puzzle, and combining them is where things get powerful.

This guide covers every practical pattern you will need, from the simplest .Files.Get call to the advanced tpl + .Files.Glob combination that gives you full templating inside external files. For a broader look at Helm packaging, see our definitive Helm package management guide.

Understanding the Helm Chart File Structure

Before diving into the functions, it is important to understand what Helm considers a “file” and where it can access them. A typical chart looks like this:

my-chart/
├── Chart.yaml
├── values.yaml
├── templates/
│   ├── configmap.yaml
│   ├── secret.yaml
│   └── deployment.yaml
├── config/
│   ├── app.properties
│   ├── logging.json
│   └── init.sh
└── files/
    └── zones.json

Files inside templates/ go through the full Helm template engine. Files outside it (like config/app.properties or files/zones.json) are accessible via the .Files object, but they are not templated automatically. This is the key distinction that catches most people off guard.

.Files.Get: Reading a Single File

The .Files.Get function reads the content of a specific file by its path, relative to the chart root. This is the simplest way to include file content in a ConfigMap or Secret.

Basic ConfigMap Example with .Files.Get

apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ .Release.Name }}-app-config
  namespace: {{ .Release.Namespace }}
data:
  app.properties: |
{{ .Files.Get "config/app.properties" | indent 4 }}

This reads config/app.properties from the chart root and injects it into the ConfigMap, preserving the original content. The indent 4 ensures correct YAML indentation.

Secret Example with .Files.Get and b64enc

For Secrets, Kubernetes expects base64-encoded values in the data field. Combine .Files.Get with b64enc:

apiVersion: v1
kind: Secret
metadata:
  name: {{ .Release.Name }}-tls-config
  namespace: {{ .Release.Namespace }}
type: Opaque
data:
  tls.crt: {{ .Files.Get "certs/tls.crt" | b64enc }}
  tls.key: {{ .Files.Get "certs/tls.key" | b64enc }}

.Files.Get Path Limitations: Why “../” Does Not Work

A very common question is whether you can use .Files.Get to access files outside the chart directory, for example .Files.Get "../shared/config.yaml". The answer is no. Helm restricts file access to the chart root for security reasons. Any path that tries to escape the chart directory with ../ will silently return an empty string.

If you need to share files between charts, the recommended patterns are:

  • Use a library chart or dependency that includes the shared files
  • Copy shared files into each chart during your CI/CD pipeline before packaging
  • Pass the content through values.yaml using a parent chart

Also note that files inside the templates/ directory and Chart.yaml itself are not accessible through .Files. Only files that are packaged with the chart and not in templates/ can be read.

.Files.Glob: Working with Multiple Files

When you have multiple configuration files to include, .Files.Glob lets you match files using glob patterns and iterate over them. This is especially useful when your chart ships with several config files that all need to end up in the same ConfigMap.

.Files.Glob with .AsConfig Example: ConfigMap from Multiple Files

The .AsConfig helper takes a set of matched files and formats them as ConfigMap data entries, where each filename becomes the key and the file content becomes the value:

apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ .Release.Name }}-all-configs
  namespace: {{ .Release.Namespace }}
data:
{{ (.Files.Glob "config/*").AsConfig | indent 2 }}

If your config/ directory contains app.properties, logging.json, and init.sh, the result would be:

data:
  app.properties: |
    server.port=8080
    db.host=postgres
  logging.json: |
    {"level": "info", "format": "json"}
  init.sh: |
    #!/bin/bash
    echo "Initializing..."

This is the cleanest approach when you want to include all files from a directory without modifying their content.

.Files.Glob with .AsSecrets Example

.AsSecrets works identically to .AsConfig, but automatically base64-encodes each file’s content for use in Secret resources:

apiVersion: v1
kind: Secret
metadata:
  name: {{ .Release.Name }}-credentials
  namespace: {{ .Release.Namespace }}
type: Opaque
data:
{{ (.Files.Glob "secrets/*").AsSecrets | indent 2 }}

Iterating with range: The Explicit Approach

For more control over how each file is processed, you can iterate explicitly using range:

apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ .Release.Name }}-scripts
  namespace: {{ .Release.Namespace }}
data:
  {{- range $path, $_ := .Files.Glob "scripts/*.sh" }}
  {{ base $path }}: |
{{ $.Files.Get $path | indent 4 }}
  {{- end }}

Notice two important details here. First, base $path extracts just the filename (e.g., init.sh) from the full path (scripts/init.sh). Second, inside a range loop the context changes, so you must use $. (dollar-dot) to access the root scope when calling $.Files.Get.

The tpl Function: Full Templating Inside External Files

This is where things get really interesting. The .Files.Get and .Files.Glob functions read file content as-is. If your app.properties file contains {{ .Values.database.host }}, it will be included literally as that string, not replaced with the actual value.

The tpl function solves this by passing a string through the Helm template engine. When you combine tpl with .Files.Get, your external files get the same templating power as files inside templates/.

tpl + .Files.Get: Templated ConfigMap

Consider this config/app.properties file in your chart:

# config/app.properties
server.port={{ .Values.app.port | default 8080 }}
server.host={{ .Values.app.host }}
database.url=jdbc:postgresql://{{ .Values.database.host }}:{{ .Values.database.port }}/{{ .Values.database.name }}
database.pool.size={{ .Values.database.poolSize | default 10 }}
logging.level={{ .Values.logging.level | default "INFO" }}

And the corresponding template that processes it:

apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ .Release.Name }}-app-config
  namespace: {{ .Release.Namespace }}
data:
  app.properties: |
{{ tpl (.Files.Get "config/app.properties") . | indent 4 }}

The tpl function takes two arguments: the string to process and the context (.). It runs the content through the template engine, replacing all {{ .Values.* }} references with actual values. The result is a fully dynamic configuration file.

tpl + .Files.Glob: Templating Multiple Files

To template every file matched by a glob pattern, combine the range iteration with tpl:

apiVersion: v1
kind: Secret
metadata:
  name: {{ .Release.Name }}-dynamic-secrets
  namespace: {{ .Release.Namespace }}
type: Opaque
data:
  {{- range $path, $_ := .Files.Glob "secrets/*.json" }}
  {{ base $path }}: {{ tpl ($.Files.Get $path) $ | b64enc }}
  {{- end }}

This iterates over all .json files in the secrets/ directory, passes each through the template engine (so {{ .Values.* }} references are resolved), base64-encodes the result, and includes it in the Secret. This is the most powerful pattern for managing multiple dynamic configuration files.

Common Pitfalls and Troubleshooting

Working with .Files in Helm can produce confusing errors or silent failures. Here are the most common issues and how to fix them.

.Files.Get Returns Empty String

If .Files.Get returns nothing, check these three things:

  • Wrong path: The path is relative to the chart root, not to the templates directory. Use .Files.Get "config/app.properties", not .Files.Get "../config/app.properties".
  • File excluded by .helmignore: Check your .helmignore file. If it matches the file’s path, the file will not be packaged with the chart and .Files.Get will return empty.
  • File in templates/ directory: Files inside templates/ are not accessible via .Files. Move them to a different directory.

YAML Indentation Errors

The most frequent rendering error is incorrect indentation. Always use indent N (or nindent N) when including file content in YAML. The difference between indent and nindent is that nindent adds a newline before the content, which is cleaner when using {{- to trim whitespace:

# Using indent (requires | on the previous line)
data:
  app.properties: |
{{ .Files.Get "config/app.properties" | indent 4 }}

# Using nindent (cleaner, self-contained)
data:
  app.properties: {{ .Files.Get "config/app.properties" | nindent 4 }}

Chart Size Limit

Helm charts stored in Kubernetes as Secrets or ConfigMaps are subject to the 1 MB limit imposed by etcd. If your chart includes many large files, you may hit this limit during helm install. The error typically reads release: invalid or etcd: request is too large. In that case, consider mounting files via persistent volumes or external config management instead of embedding them in the chart.

Context Issues Inside range Loops

Inside a range block, . refers to the current iteration item, not the root context. This means .Files.Get will fail. Use $. to access the root context:

# Wrong: . is the loop item, not the root context
{{- range $path, $_ := .Files.Glob "config/*" }}
  {{ .Files.Get $path }}  {{/* This will fail */}}
{{- end }}

# Correct: use $ to access root context
{{- range $path, $_ := .Files.Glob "config/*" }}
  {{ $.Files.Get $path }}  {{/* This works */}}
{{- end }}

Real-World Pattern: Multi-Environment Configuration

A practical pattern that combines everything above is organizing environment-specific configuration files and selecting them dynamically:

# Chart structure
my-chart/
├── config/
│   ├── application.yaml      # Shared base config
│   ├── db-pool.properties    # Database pool settings
│   └── logging.xml           # Log4j/Logback config
├── templates/
│   └── configmap.yaml
└── values.yaml

The ConfigMap template processes all config files with templating:

apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ .Release.Name }}-config
  namespace: {{ .Release.Namespace }}
  labels:
    {{- include "my-chart.labels" . | nindent 4 }}
data:
  {{- range $path, $_ := .Files.Glob "config/*" }}
  {{ base $path }}: |
{{ tpl ($.Files.Get $path) $ | indent 4 }}
  {{- end }}

This way, every file in config/ is automatically included, templated, and properly formatted. Adding a new configuration file is as simple as dropping it into the directory. No template changes required.

Quick Reference: .Files Functions Summary

Here is a quick reference of all file-related functions available in Helm:

FunctionWhat it doesExample
.Files.GetReturns the content of a single file as a string.Files.Get "config/app.yaml"
.Files.GlobReturns all files matching a glob pattern.Files.Glob "config/*.json"
.AsConfigFormats matched files as ConfigMap data entries(.Files.Glob "config/*").AsConfig
.AsSecretsFormats matched files as base64-encoded Secret data(.Files.Glob "secrets/*").AsSecrets
tplPasses a string through the Helm template enginetpl (.Files.Get "f.yaml") .
.Files.LinesReturns file content as a list of lines.Files.Lines "config/hosts.txt"
.Files.AsBytesReturns file content as a byte array.Files.Get "bin/tool" \| b64enc

Conclusion

Helm file handling follows a clear escalation path depending on what you need. Use .Files.Get when you need a single file as-is. Use .Files.Glob with .AsConfig or .AsSecrets when you have multiple files that need no modification. And use tpl combined with either function when your files need dynamic values from values.yaml.

The most common mistake is forgetting that .Files only reads files — it does not template them. The moment you need {{ .Values.* }} inside an external file, tpl is the function you are looking for. For more Helm patterns and advanced tips, explore our guides on Helm hooks, Helm loops, and Helm dependencies.

Frequently Asked Questions

Can .Files.Get access files outside the chart directory?

No. Helm restricts .Files.Get to files within the chart root directory. Paths containing ../ will silently return an empty string. This is a security constraint to prevent charts from reading arbitrary files from the filesystem. If you need shared files across charts, use library charts, copy files during CI/CD, or pass content through parent chart values.

What is the difference between .AsConfig and .AsSecrets in Helm?

.AsConfig formats files as plain text ConfigMap data entries (key: filename, value: file content). .AsSecrets does the same but automatically base64-encodes each file’s content, which is required for the data field in Kubernetes Secret resources. Both are called on the result of .Files.Glob.

How do I use Helm values inside a properties file or JSON config?

Use the tpl function. Instead of .Files.Get "config/file.json", use tpl (.Files.Get "config/file.json") .. This passes the file content through the Helm template engine, so any {{ .Values.* }} references in the file will be resolved against your chart’s values.

Why does .Files.Get return an empty string?

Three common causes: the file path is wrong (paths are relative to the chart root, not the templates directory), the file is excluded by .helmignore, or the file is inside the templates/ directory which is not accessible via .Files. Run helm template locally and check the output to debug.

Is there a size limit for files included via .Files.Get?

Helm itself does not impose a file size limit, but the packaged chart is stored as a Kubernetes Secret or ConfigMap which is subject to the etcd 1 MB size limit. If your chart with all its files exceeds this, helm install will fail. For large files, consider external config management or persistent volumes instead of embedding them in the chart.

Leave a Comment