HomeBlogHelm: How to Reference Variables From values?
TechnologiesHelm

Helm: How to Reference Variables From values?

Audio article by AppRecode

0:00/7:49

Summarize with:

ChatGPT iconclaude iconperplexity icongrok icongemini icon
24 mins
27.08.2026
Volodymyr Shynkar CEO and Co-Founder of AppRecode

Volodymyr Shynkar

CEO/CTO

Helm is a package manager for Kubernetes that helps you manage and deploy complex applications. In Helm, variables are used to store values that can be used throughout your chart. These values can be customized for each deployment, allowing you to deploy your application in different environments with different configurations.

Background

Helm is a package manager for Kubernetes that helps you manage and deploy complex applications.

In Helm, variables are used to store values that can be used throughout your chart. These values can be customized for each deployment, allowing you to deploy your application in different environments with different configurations.

Helm provides a straightforward template language that makes it simple to refer to configuration settings that are defined in a “values file.”

e.g. The “name” config value from the “values file” is referred to in the Helm chart template above. The template will eventually be produced like follows, assuming the string “world” is the value of the configuration field “name”:

Referencing a “value” in a Helm chart template appears tidy and easy. So how might we use the Helm chart’s “values file” to refer to other values?

Why?

An example “values file” that provides a collection of API endpoint URLs is shown below:

apiOneUrl: http://example.com/apiOne/v0

 

apiTwoUrl: http://example.com/apiTwo/v3

 

apiThreeUrl: http://example.com/apiThree/v7

You could see that the string “http://example.com/” appears in every configuration field. If we could provide the “values file” like follows, that would be nice:

baseUrl: http://example.com

 

apiOneUrl: “{{ .Values.baseUrl }}/apiOne/v0”

 

apiTwoUrl: “{{ .Values.baseUrl }}/apiTwo/v3”

 

apiThreeUrl: “{{ .Values.baseUrl }}/apiThree/v7”

That will not only eliminate duplication and save a ton of typing, but it will also make updating configuration variables much simpler in the future.

However, since Helm’s template engine won’t parse the “values file,” referencing “.Values.apiOneUrl” in your template will simply return the original string “{{ .Values.baseUrl }}/apiOne/v0”

tpl Function to Rescue

Here is where the Helm chart tpl function comes in in. It enables developers to evaluate texts as templates inside of templates. The function anticipates two inputs:

  • A template string that has to be processed is the first argument.
  • The context data to be used when processing the template string is the final argument. We frequently succeed. This is up-to-date context information to ensure that all configuration settings are accessible throughout template processing.

In your template, you may attempt the following:

MyApiOneUrl: {{ tpl .Values.apiOneUrl . }}

you will find .Values.apiOneUrl ‘s value “{{ .Values.baseUrl }}/apiOne/v0” will be converted into “http://example.com//apiOne/v0” and the above template will be rendered as:

MyApiOneUrl: http://example.com/apiOne/v0

Problem with Non-string Types

When using basic string type configuration settings, the technique works effectively. What happens, though, if the configuration value is of a non-string type, such as a “map” or “list”?

Have a look at the values file below:

baseUrl: http://example.com

apiUrls:

apiOneUrl: “{{ .Values.baseUrl }}/apiOne/v0”

apiTwoUrl: “{{ .Values.baseUrl }}/apiTwo/v3”

apiThreeUrl: “{{ .Values.baseUrl }}/apiThree/v7”

In your template, you may attempt the following:

MyApiUrls: {{ tpl .Values.apiUrls . }}

The following template error will appear:

wrong type for value; expected string; got map[string]interface {}

The error message makes sense because the tpl function anticipates a template string as the first parameter. So how might we address the problem and make our solution applicable to this typical use case?

One option is to feed the non-string type value (in this example, map) to the toYaml function first and then convert it to a “YAML” string:

MyApiUrls:

{{ tpl (.Values.apiUrls | toYaml) . | indent 2 }}

If you use the aforementioned template, it will be appropriately displayed as:

MyApiUrls:

apiOneUrl: http://example.com/apiOne/v0

apiTwoUrl: http://example.com/apiTwo/v3

apiThreeUrl: http://example.com/apiThree/v7

The Complete Solution

We may construct a “named template” to recognize the configuration value type and use the appropriate logic, making the solution portable:

{{ define “render-value” }}

{{- if kindIs “string” .value }}

{{- tpl .value .context }}

{{- else }}

{{- tpl (.value | toYaml) .context }}

{{- end }}

{{- end }}

You may use the include function to call the “named template” in a template:

MyApiOneUrl: {{ include “render-value” ( dict “value” .Values.apiOneUrl “context” .) }}

MyApiUrls:

{{ include “render-value” ( dict “value” .Values.apiUrls “context” .) | indent 2}}

The following will be created from the aforementioned template:

MyApiOneUrl: http://example.com/apiOne/v0

MyApiUrls:

apiOneUrl: http://example.com/apiOne/v0

apiTwoUrl: http://example.com/apiTwo/v3

apiThreeUrl: http://example.com/apiThree/v7

You may also utilize Bitnami’s “common” library chart if you want to avoid having to define your own “named template”. Just add the upcoming dependency to your Chart.yaml to start using it :

dependencies:

– name: common

version: 1.11.1

repository: https://charts.bitnami.com/bitnami

Then, using the same reasoning as the following, you may call the built-in “named template”

MyApiOneUrl: {{ include “common.tplvalues.render” ( dict “value” .Values.apiOneUrl “context” .) }}

Best Practices

Now, let’s explore some best practices that will help you get the maximum out of using the Helm Chart. These are largely based on our experience with managing such charts, defining Helm variables, and other services, such as DevOps maintenance support services

1. When something must be dynamic, chuck it into tpl

Sometimes you get a value that needs to expand at runtime. Then the move is:

{{ tpl .Values.image.full . }}

 

But don’t overuse this or you’ll regret it later—the debugging is… unpleasant.

2. Break big things into small pieces

Composing a URL, an image name, whatever—don’t try to Frankenstein it inside values.yaml.

Let the templates put the puzzle pieces together. Helm is happier that way, and so are future you and your teammates.

3. If you repeat something more than twice, helpers exist for a reason

_helpers.tpl is where repeated logic should live. No need to copy/paste a 4-line snippet in every template. That always backfires in six months.

4. YAML anchors are boring but reliable

YAML anchors don’t “compute” anything. They just clone chunks of YAML. Such an approach is useful when you’re tired of writing the same block three times.

decoration

Unsure whether Helm chart values reference other values or need help with cleaning your YAML file?

As a team with exceptional expertise in Helm charts and container orchestration consulting, we are ready to help you!

Let's talk!

Troubleshooting: Common Mistakes and Simple Fixes

Here are some more tips that will help you avoid common mistakes with the Helm chart.

“Why does Helm ignore my template in values.yaml?”

Because it always does. Move the logic into a template and call tpl if absolutely needed.

“Wrong type for value; expected string; got map”

Happens when you send a map into tpl. It only digests strings.

Subcharts can’t see each other’s values

They’re isolated. Use global.* if they must share something.

YAML anchors don’t magically build dynamic things

They’re copy/paste, nothing else. Good for repetition only.

Overusing tpl

Makes debugging harder than it has to be. Use it sparingly, not by default.

Our Expertise with the Helm Chart

We’ve dealt with Helm charts that started as neat little projects and somehow turned into 800-line YAML creatures with duplicated values, tangled subcharts, and “temporary” fixes from two years ago. While we cannot make Helm values reference other values directly, we can help you with many templates that provide the functionality you need.

Whether you need help with structuring your Helm chart, making Helm reference values in values with templates you need, or help with configuring CI/CD pipelines, our team will be there.

And if you need structure, migration advice, or someone to introduce sanity into CI/CD,  our team’s been there. We have strong expertise in DeOps development services and Kubernetes consulting that will help you configure a solution tailored to your needs. Our specialists will provide you with reliable help in managing Helm values variables.

We will also ensure that your Helm defines variable patterns correctly, ensuring your charts stay flexible, maintainable, and ready for any environment.

To know more about our expertise, make sure to check out our Clutch profile. You can also review this independent DataArt vs AppRecode expert comparison for a side-by-side view.

 

Expert Quote:

“Most Helm problems don’t come from Helm at all. They come from trying to make Helm behave like something it’s not. Keep it simple, don’t expect Helm values file reference other values directly, and suddenly everything works again.”

Volodymyr Shynkar, Founder at AppRecode

LinkedIn

Summary

We can combine configuration data into Helm template variables thanks to the solution’s straightforward yet effective template engine. But, occasionally, we could also wish to use Helm’s “values file” as a configuration file. The “named template” implementation of a tpl function-based solution is introduced in this article.

If you are unfamiliar with Helm’s “named template” function, you may want to read Helm official to learn more about what it can accomplish.

In Apprecode, we are always ready to consult you about implementing the DevOps methodology. Please contact us for more information.

FAQ

Can I reference one value from another in values.yaml?

Short answer: Helm will store such a reference, but it will not resolve it on its own. Think of values.yaml as the chart’s input form. Helm reads that form as YAML before it renders any Kubernetes templates. So an entry such as apiUrl: "{{ .Values.baseUrl }}/api" remains text. Printing .Values.apiUrl in a manifest usually prints the braces too; it does not quietly turn the entry into https://example.com/api.
I usually give the operator two plain knobs here: baseUrl and apiPath. The manifest owns the joining step. It might use printf{{ printf "%s%s" .Values.baseUrl .Values.apiPath | quote }}—but the particular function is less important than the location of the rule. Anyone reading the template can spot the join, while either input remains easy to replace through another values file or --set. There is no hidden second rendering pass to remember.
There is an escape hatch for charts that really want expressions in user input: tpl. The chart template, not the values file, invokes it—for example, {{ tpl .Values.apiUrl . }}. In other words, the maintainer must opt a field into this behavior. I would keep the list of such fields short, document them, and avoid surprising users who reasonably assumed that a value was just a string.
For subcharts, a parent can override values under the dependency’s key, while deliberately shared settings can live under global. YAML anchors can copy a scalar or structure within YAML, but they cannot calculate a new Helm value. In short, combine ordinary values in a template, or explicitly evaluate a documented string field with tpl.

Why doesn’t Helm allow templating directly inside values.yaml?

Helm separates input data from template execution. Files such as values.yaml, user-supplied -f files, and --set arguments contribute data to the .Values object. Files under templates/ are the resources that Helm sends through its template engine. This boundary makes the order of evaluation understandable: values are merged first, and templates consume the final result.
If Helm automatically evaluated every value as a template, an apparently ordinary string could execute template functions, depend on rendering scope, or change meaning after an override. Multiple evaluation passes would raise further questions: should a newly produced template expression run again, and when should evaluation stop? Helm avoids that implicit behavior. The chart author chooses where computation is allowed.
This does not mean templated values are impossible. The tpl function is an explicit opt-in mechanism. It takes a template string and a context, for example {{ tpl .Values.message . }}. A maintainer can therefore expose templating for one documented field without silently treating all configuration as executable input. For routine composition—names, URLs, labels, or image references—building the result in a normal chart template is usually easier to review, validate, and debug.

How do I reuse values across subcharts?

Start with the normal parent-to-subchart override mechanism. If a dependency is named payments, the parent chart can provide values under the payments: key in its own values.yaml. Inside the payments subchart, those entries appear in the subchart’s local scope: its template reads .Values.replicaCount, not .Values.payments.replicaCount. This is usually the right approach when a setting belongs to one dependency.
Use global only for values that several charts genuinely share. A parent might define global.imageRegistry, global.clusterDomain, or a common environment identifier. Parent and subchart templates can then read the same path, such as .Values.global.imageRegistry. Global values must be declared explicitly; Helm does not automatically promote ordinary parent values into a child chart.
There is a third option for reusable rendering logic: named templates or a library chart. Defined templates are compiled across a chart and its dependencies, so their names should be namespaced, for example mycompany.commonLabels, to avoid collisions. A library chart is often a better home for standardized labels, pod fragments, or other shared helpers used by several independent charts.
Avoid making a subchart secretly depend on arbitrary parent values. Application subcharts are designed to be usable on their own, and the parent controls them through their documented values. Reserve global for a small, stable contract; otherwise unrelated components become coupled and future upgrades become harder to reason about.

How do I correctly use tpl?

Reach for tpl only when the chart deliberately lets a values author write an expression. Read a call from left to right: first comes the string; after it comes the rendering context. As a small example, put welcome: "Hello {{ .Values.customerName }}" in the values file. The ConfigMap can render the field with message: {{ tpl .Values.welcome . | quote }}. Notice the final dot. Remove or replace it and the expression may no longer see the .Values or .Release objects you expected.
Scope causes a surprising number of failures. Once execution is inside range or with, the dot can mean the current list item or nested object instead of the chart root. Keep the original root in $ and pass that when the expression needs the whole chart: {{ tpl .Values.someString $ }}. Before calling the function, decide what an absent value means. Skip the field, supply a default, or fail with a useful message; letting an empty value wander into evaluation rarely helps the person debugging the chart.
Structured input needs one extra decision. If only a single nested string is dynamic, render that leaf. If the whole map is intentionally templated, serialize the map to YAML, run the resulting text through tpl, and then restore the indentation required by its parent key:

data:
{{ tpl (toYaml .Values.config) . | nindent 2 }}

Do not apply tpl to every value by default. Document which fields accept expressions, quote scalar output where the Kubernetes field expects a string, and test several override combinations with helm template. Remember that evaluating a user-supplied template grants access to the functions and objects available in that rendering context. Treat tpl as a purposeful chart API, not as a universal workaround for configuration design.

Why does Helm complain about wrong types with tpl?

This error usually tells you that YAML and the template disagree about the shape of a value. tpl works on text. A nested config: block, by contrast, reaches Helm as a map; a sequence arrives as a list, and an unquoted number arrives as a numeric value. Calling {{ tpl .Values.config . }} on that nested block therefore fails with a message such as expected string; got map[string]interface {}. Helm has not reached the rendering step because it cannot treat the map itself as template text.
If the intended output is YAML, convert the structure to text before evaluation and then indent the rendered block correctly:

spec:
  configuration:
{{ tpl (toYaml .Values.config) . | nindent 4 }}

The same idea works for a JSON configuration, using toJson or toPrettyJson. Still, serializing the whole object should not become a reflex. When only config.endpoint contains an expression, it is often cleaner to render that one leaf and leave the remaining keys as ordinary typed data. You get better validation and a much smaller patch to inspect when something goes wrong.
Check the source value as well. YAML reads bare true, 42, and null as special types, not as the character strings an application might expect. Add quotes only when the destination really expects text. Then run helm template --debug and look at the generated field rather than guessing from the source file; follow it with helm lint in the normal chart checks. In practice, fixing this message means either passing an actual string or deliberately converting a supported structure before evaluation.

Can I reference local variables in a subchart?

Inside a subchart template, local variables work normally. You can save the original context with {{- $root := . -}}, keep a release name in $name, or hold the current item while looping. The important word is “local.” A variable belongs to the template scope in which it was declared. Moving into range, with, or a named helper changes what the dot refers to, and a variable created inside a block should not be assumed to exist elsewhere. Chart authors commonly keep $ as the route back to the root context.
A subchart cannot reach into a parent template and read the parent’s local variables. It renders with its own context and its own .Values scope. The parent can supply configuration through the subchart’s values key, and both charts can access explicitly declared .Values.global entries. Those mechanisms pass data; they do not share local template variables.
Reusable named templates are slightly different. A caller can pass a context or a dictionary to include, for example {{ include "mychart.label" (dict "root" $ "component" "api") }}. The helper then reads the keys that were deliberately provided. This explicit contract is safer than assuming the helper sees every variable at the call site. If information must cross a chart boundary, model it as a documented value or helper argument rather than relying on local scope.

Are YAML anchors a decent workaround?

YAML anchors are useful for literal reuse, but they are not a substitute for Helm templating. An anchor can assign a name to a scalar, list, or mapping and an alias can copy that value elsewhere in the same YAML document. This can reduce repetition when two values are exactly the same. It cannot concatenate a base URL with a path, call a Helm function, inspect .Release, or calculate a value from another field.
There is also a lifecycle caveat. Helm’s documentation notes that when YAML is decoded and then re-encoded, aliases are expanded and the anchor information is discarded. Helm and Kubernetes frequently perform this kind of round trip. The resulting data can remain correct, but the anchor itself should not be treated as a durable abstraction that downstream tools will preserve.
For a short internal values file, an anchor may be acceptable when it improves readability. For reusable logic inside one chart, prefer a named helper under _helpers.tpl. For reusable chart components shared across projects, consider a library chart. And for a value that must be composed dynamically, build it in a template. Choose anchors for static duplication only; using them to imitate a configuration or inheritance system usually leaves future maintainers with a fragile file and unclear override behavior.

How do I clean up charts with repeated URLs or prefixes?

First decide whether the repeated text is configuration or presentation logic. Store user-controlled pieces—such as scheme, host, port, and an optional base path—as ordinary values. Assemble the final URL in a template or a chart-specific helper. This avoids duplicating a hostname while keeping environment overrides straightforward.
A helper can make the rule reusable:

{{- define "mychart.baseUrl" -}}
{{- printf "%s://%s" .Values.endpoint.scheme .Values.endpoint.host -}}
{{- end -}}

A manifest can then use {{ include "mychart.baseUrl" . }}/v1/orders. Prefix helper names with the chart name because defined template names are global across the parent chart and its dependencies. Use quote, urlquery, or other functions where the target field requires them, and handle optional ports or paths explicitly rather than relying on accidental slash placement.
If chart consumers must provide an entire templated URL, expose one clearly named field and evaluate only that field with tpl. Do not make every string executable. For values shared by multiple dependencies, a carefully chosen global entry may be appropriate, although each subchart still needs a template that consumes it.
Do not stop after the default example renders. Try a plain hostname, a base path, a missing optional port, and at least one environment override. Looking at helm template --debug output usually exposes the mundane bugs—a doubled slash, a value in the wrong indentation level, or a string that lost its quotes. Removing repetition helps only while the rule stays obvious. If the helper needs a page of explanation, repeating two readable lines may be the cheaper maintenance choice.

What are tpl’s limitations in Helm 3?

The important limitations are about types, scope, validation, security, and maintainability—not a documented rule that tpl performs “no deep evaluation.” The function accepts template text as a string plus a context. Passing a map or list directly fails unless it is serialized first. Passing the wrong context, especially from inside range or with, can make .Values or other objects unavailable.
tpl also moves some errors from values parsing to rendering. A values schema can validate that a field is a string, but it generally cannot prove that the embedded template will render valid YAML or produce a value accepted by Kubernetes. Quoting and indentation still matter after evaluation. Mistakes may appear only for a particular combination of user overrides.
Evaluation is a power boundary. A string processed by tpl can use the template functions and objects exposed in its context. The exact risk depends on the chart, Helm version, permissions, and available cluster access; for example, templates using lookup may query cluster resources when rendering is performed with server access. Chart authors should not treat untrusted templated values as inert data.
Finally, broad use of tpl makes charts harder to document and troubleshoot because users must reason about two languages at once: YAML and Go templates. Keep the supported surface narrow, pass scope deliberately, validate the rendered manifests in CI, and prefer ordinary helper templates when the chart author—not the user—owns the composition logic.

Is there a helper library to render values the “clean way”?

Bitnami’s Common Library Chart is one established option. It packages named templates that other charts can call, including helpers used by Bitnami charts to deal with values that may contain template expressions. This can save a team from maintaining the same rendering snippet in many repositories. There is a catch worth taking seriously: helper names and their expected dictionaries can change between releases. Pin the library version and read the matching documentation before borrowing an example from a blog post.
A library chart does not deploy an application of its own. Instead, the consuming chart calls its named templates with include. The arrangement starts paying off when several charts share label rules, image naming, validation messages, or configuration rendering. One library update can fix all of them, though it can also change all of them. Treat that dependency like code: version it, review upgrades, and render the consumers in CI.
For one modest chart, I would usually keep the logic nearby in _helpers.tpl. Name it with the chart prefix—mychart.renderValue, for example—because template names are global across dependencies. Pass the value and root context on purpose so the helper’s inputs are visible at the call site. A short local helper is easier to remove later than a general-purpose renderer that has quietly made every value executable.
Whichever route you choose, “clean” should mean predictable rather than merely short. Pin dependency versions, review changes before upgrading, render the chart in CI, and test string, map, empty, and override cases. A shared helper can remove repetition, but it cannot decide whether a field should allow templating in the first place. That remains part of the chart’s public configuration contract.

How can I debug templated values before installing a Helm chart?

Render locally first. helm lint checks the chart for common problems and convention issues, while helm template --debug RELEASE ./chart -f test-values.yaml renders manifests without installing them. Examine the exact field that uses tpl: confirm that expressions were evaluated, strings are quoted where required, and multiline YAML is indented beneath the correct parent key.
Test more than the default values. Include a normal override, an empty optional value, a map or list if the chart supports structured input, and a string containing template syntax. When a template runs inside range or with, include a case that proves the correct root context is passed. A small collection of values fixtures makes these checks repeatable in CI.
helm install --dry-run --debug provides an install-style render without creating the release. A client-side dry run does not contact the cluster. If a template intentionally uses lookup, Helm’s documentation points to server-side dry-run behavior for cluster-aware rendering; use that only in an appropriately restricted test environment because it requires cluster access.
When invalid YAML prevents useful output, temporarily comment out the problematic section and render again, a debugging technique described in the official Helm guide. After the YAML renders, validate the generated Kubernetes objects with the tools used by your delivery pipeline. The goal is not merely to make tpl stop failing: verify that every supported input produces valid, reviewable manifests before a real upgrade can reach the cluster.

Does using tpl create a security risk?

It can. Without tpl, a value is normally data inserted where the chart author chooses. Once a chart evaluates that value as a template, the author allows it to call functions and read objects available in the supplied context. This is more capability than an ordinary string. The practical impact depends on how the chart is rendered, which functions are available, whether lookup can contact a cluster, and what permissions the rendering identity has.
The safest design is to avoid evaluating untrusted text unless the feature is genuinely required. Expose templating only for specific documented fields, pass the smallest useful context when practical, and do not automatically run tpl across an entire values tree. Review user-supplied values in the same way you would review a template change, especially in shared CI/CD systems.
Cluster access deserves particular attention. Local helm template rendering normally works without contacting Kubernetes, whereas server-aware rendering and some install or upgrade paths may allow functions such as lookup to query resources using the caller’s permissions. Apply least privilege to deployment credentials and avoid printing rendered Secrets or sensitive values into logs or public CI artifacts.
Finally, pin chart and dependency versions, lint and render with controlled fixtures, and require review for changes to templated fields. tpl is not inherently unsafe; Helm provides it for legitimate use cases. The risk comes from treating executable template input as harmless configuration. A narrow contract and restricted deployment identity keep that risk understandable.

When should I compose a value in a template instead of using tpl?

Compose the value in a regular chart template when the chart author knows the rule. If a URL always consists of a scheme, host, and path, or an image reference always combines a registry, repository, and tag, the chart should own that logic. Ordinary templates and named helpers are easier to read, validate, and change without asking users to write Go-template expressions inside YAML.
Choose tpl when users genuinely need to provide an expression whose structure the chart author cannot define in advance. Examples include a configurable message that refers to release metadata or an external configuration file deliberately exposed as a template. Even then, make templating an explicit feature of a specific field and show a tested example in the chart documentation.
Several warning signs favor normal composition: the same expression is copied into every environment file; users must understand internal helper names; errors frequently involve scope; or the values schema can no longer describe meaningful constraints. These indicate that the chart is exporting implementation details instead of a stable configuration interface.
A practical rule is simple: keep data in values and keep predictable behavior in templates. Use include and namespaced helpers for repeated chart-owned logic. Reserve tpl for controlled extension points where user-authored templating provides real value. This division makes overrides easier to audit, produces clearer error messages, and reduces the chance that a harmless-looking values change alters rendering in an unexpected way.

Did you like the article?

60 ratings, average 4.8 out of 5

Comments

Loading...

Blog

OUR SERVICES

REQUEST A SERVICE

651 N Broad St, STE 205, Middletown, Delaware, 19709
Ukraine, Lviv, Studynskoho 14

Get in touch

We'll get back to you within 1 business day.

No commitment · reply within 24 hours

AppRecode Ai Assistant