The tpl function in Helm is not a templating engine itself, but rather a way to render another template within your Helm chart’s templates.

Let’s see it in action. Imagine you have a values.yaml like this:

appName: my-awesome-app
environment: staging

And you want to create a Kubernetes Service manifest that dynamically sets its name based on these values. You could write templates/service.yaml like this:

apiVersion: v1
kind: Service
metadata:

  name: {{ .Release.Name }}-{{ .Values.appName }}-{{ .Values.environment }}

spec:
  selector:

    app: {{ .Values.appName }}

  ports:
    - protocol: TCP
      port: 80
      targetPort: 9376

When Helm renders this, if your release name is my-release, the Service name will become my-release-my-awesome-app-staging. This is standard Helm templating.

Now, what if you wanted to define a template snippet that you reuse, and then render that snippet within another template? That’s where tpl shines.

Suppose you have a common naming convention defined in a separate file, say templates/common/_naming.tpl:


{{/*

This template defines a common naming pattern.
*/}}

{{- define "mychart.fullname" -}}


{{ .Release.Name }}-{{ .Values.appName }}-{{ .Values.environment }}


{{- end -}}

Notice the {{- define "mychart.fullname" -}} block. This defines a named template. You can then use this named template in your templates/service.yaml like this:

apiVersion: v1
kind: Service
metadata:

  name: {{ tpl (.Files.Get "templates/common/_naming.tpl") . | nindent 4 }}

spec:
  selector:

    app: {{ .Values.appName }}

  ports:
    - protocol: TCP
      port: 80
      targetPort: 9376

Here’s the breakdown:

  • {{ .Files.Get "templates/common/_naming.tpl" }}: This part reads the content of the _naming.tpl file. Helm makes all files in your templates/ directory accessible via the .Files object.

  • tpl ... .: This is the core of it. The tpl function takes two arguments:

    1. The first argument is a string containing the template content you want to render. In this case, it’s the content of _naming.tpl.
    2. The second argument is the data context to render that template against. We pass . here, which refers to the current scope (the Service manifest’s scope), giving the tpl function access to .Release.Name, .Values, etc.
  • | nindent 4: This is a standard Go template function to indent the output by 4 spaces, ensuring it’s correctly formatted within the YAML.

When Helm processes templates/service.yaml, it first reads _naming.tpl. Then, tpl takes that template definition and renders it using the current context. The output of tpl is then embedded directly into the metadata.name field.

The problem this solves is managing complex or repeated templating logic. Instead of scattering the same naming convention across multiple files, you centralize it in a .tpl file and then use tpl to pull it in where needed.

The real power comes when you have conditional logic or more complex data transformations within your named templates. For example, a common pattern is to generate labels:

In templates/common/_labels.tpl:


{{- define "mychart.labels" -}}


helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version }}


app.kubernetes.io/name: {{ .Values.appName }}


app.kubernetes.io/instance: {{ .Release.Name }}


app.kubernetes.io/version: {{ .Values.appVersion | default .Chart.AppVersion }}


{{- if .Values.commonLabels }}


{{- toYaml .Values.commonLabels | nindent 2 }}


{{- end -}}


{{- end -}}

And then in templates/deployment.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:

  name: {{ .Release.Name }}-{{ .Values.appName }}

  labels:

    {{ tpl (.Files.Get "templates/common/_labels.tpl") . | nindent 4 }}

spec:
  selector:
    matchLabels:

      app: {{ .Values.appName }}

  template:
    metadata:
      labels:

        {{ tpl (.Files.Get "templates/common/_labels.tpl") . | nindent 8 }} # Labels applied to the Pods

    spec:
      containers:

        - name: {{ .Values.appName }}


          image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"

          ports:
            - containerPort: 80

Here, we’re rendering the mychart.labels named template twice, once for the Deployment’s metadata and again, indented further, for the Pod’s metadata.

A subtle but crucial point is that tpl renders only the template content provided as the first argument. It does not automatically include the surrounding context or any other logic from the file where the define statement resides. You are explicitly telling Helm, "take this specific string of template code and execute it." This is why you often see {{- define "..." -}} ... {{- end -}} blocks within the files you Get with tpl. The tpl function is effectively saying "execute the template named '…' from the content I just read."

The common pitfall is forgetting that the second argument to tpl is the context. If you omit it or pass the wrong context, your template will not have access to .Release, .Values, etc., and you’ll get errors like function "Release" not defined.

The next concept you’ll likely grapple with is when to use include versus tpl.

Want structured learning?

Take the full Helm course →