Back to Blog
    DevOps

    Helm Templates Deep Dive: Dynamic Kubernetes Deployments

    August 3, 2025
    16 min read
    By Saanj Vij

    Customizing Helm Charts with Helm Templates

    Overview

    In this guide, we’ll explore how to customize Helm charts with Helm templates. Helm templates are a powerful tool to automate the management of Kubernetes manifests. The key advantages of using Helm templates are flexibility, reusability, and the ability to avoid hardcoding values, thus simplifying the deployment and management of Kubernetes applications.

    Key Concepts Covered:

    • Why Helm Templates are necessary for dynamic, reusable deployments.
    • Helm Template Engine and how it works.
    • Working with Helm Template Data such as values, release metadata, and capabilities.
    • Customizing Helm Charts with practical examples.
    • Testing Helm Templates using various methods.
    • Umbrella Charts and Value Merging.

    Why Use Helm Templates?

    When deploying applications, it's common to have configurations that need to be customized or reused across different environments or releases. In the past, this often meant editing configuration files manually — which is error-prone and cumbersome. Helm templates solve this problem by:

    1. Avoiding hardcoded values: Instead of editing values like image versions or resource names by hand, these can be externalized and automatically replaced at install time.
    2. Generating unique Kubernetes object names: Using Helm templates, you can generate unique names for Kubernetes objects (e.g., services, config maps) based on the release name, ensuring no conflicts even if two releases are installed in the same namespace.

    Example Use Case:

    When installing two releases of the same application, the names of resources like services must be unique. Helm templates enable this by dynamically generating names like release-name-chart-name, ensuring there are no name conflicts.

    The Helm Template Engine

    Helm templates are based on the Go templating engine, which allows for dynamic content generation. Directives in Helm templates are enclosed in mustache syntax ({{ ... }}) and are replaced with values or code during the Helm chart processing.

    Key Components:

    • Go Template Syntax: Templates are processed by the Go template engine. Directives are replaced with values from various sources such as values.yaml, the Helm chart metadata, or the release runtime data.
    • Sources of Data: Data for templates comes from several places:
      • values.yaml file.
      • Chart Metadata (via .Chart).
      • Release Metadata (via .Release).
      • Kubernetes Cluster Info (via .Capabilities).
      • File Inclusion (via .Files).

    The helm install or helm upgrade commands run the Helm template engine on the client-side before sending the resulting manifest to Kubernetes. You can also test the templates using the helm template or helm install --dry-run commands.

    Testing Helm Templates

    You can test your templates in two ways:

    1. Static Method: Run helm template <chart-name> locally to render the templates without connecting to Kubernetes.
    2. Dynamic Method: Use helm install --dry-run --debug to simulate an actual installation and view the rendered manifest, along with debug information.

    Working with Helm Template Data

    Helm templates access data from several sources, mainly:

    1. Values from values.yaml: These can be defined directly or overridden via the command line using --set.
    2. Chart Metadata: Accessed via .Chart to get information about the chart itself (e.g., .Chart.Name, .Chart.Version).
    3. Release Metadata: Accessed via .Release to get data like the release name (.Release.Name).
    4. Cluster Capabilities: Accessed via .Capabilities, useful for making charts compatible with specific Kubernetes versions.
    5. File Contents: Access files in your chart using .Files.

    Example:

    Here’s an example of a Helm template that combines data from the values.yaml file, the release name, and chart metadata:

    apiVersion: v1
    kind: Service
    metadata:
      name: "{{ .Release.Name }}-{{ .Chart.Name }}"
    spec:
      selector:
        app: "{{ .Release.Name }}-{{ .Chart.Name }}"
      ports:
        - port: {{ .Values.service.port }}
          targetPort: {{ .Values.service.targetPort }}
    

    This would generate a service with a name based on the release name and chart name, and the port values would be dynamically set from values.yaml.

    Umbrella Charts and Value Merging

    In an umbrella chart (a chart containing sub-charts), values from the parent chart can override values in the sub-charts. Additionally, Helm allows for the definition of global values that are available across all charts in the umbrella chart.

    Example:

    backend:
      mongodb:
        username: "admin"
        password: "secret"
    
    # Global value accessible by all sub-charts
    global:
      imageTag: "2.0"
    

    In this case, the parent chart overrides the mongodb.username and mongodb.password in the sub-chart, and the global.imageTag can be accessed across the parent and all sub-charts.

    Practical Example: Customizing the Frontend Chart

    Let’s walk through an example where we customize the frontend chart to make it reusable and dynamic.

    1. Dynamic ConfigMap Name: Replace the hardcoded configmap name with one based on the release and chart names.

      apiVersion: v1
      kind: ConfigMap
      metadata:
        name: "{{ .Release.Name }}-{{ .Chart.Name }}"
      data:
        guestbook_name: "{{ .Values.config.guestbook_name }}"
        backend_uri: "{{ .Values.config.backend_uri }}"
      
    2. Externalize Values: In the values.yaml file, externalize the guestbook_name and backend_uri values:

      config:
        guestbook_name: "guestbook-app"
        backend_uri: "http://backend-service"
      
    3. Dynamic Image Versioning: Use the values.yaml to specify the Docker image and tag, making it easy to upgrade the application without modifying the template.

      image:
        repository: "phico/frontend"
        tag: "2.0"
      
    4. Dynamic Service Port: Externalize the service port and type:

      service:
        port: 8080
        type: ClusterIP
      
    5. Ingress: Customize the ingress to allow for dynamic values for the hostname:

      ingress:
        host: "{{ .Values.ingress.host }}"
      

    After applying these changes, DevOps can easily deploy the chart with different configurations using Helm’s --set or by modifying the values.yaml file.

    Testing and Debugging the Chart

    Once the changes are made, you can test your Helm chart with:

    1. helm template <chart-name>: To render the templates locally.
    2. helm install --dry-run --debug <chart-name>: To simulate an install and get debug information.

    If the templates are correct, you can proceed with the actual installation using helm install <chart-name>.

    Conclusion

    In this guide, we learned how to:

    • Customize Helm charts using templates.
    • Externalize values to make charts dynamic and reusable.
    • Use various testing methods to ensure templates work as expected.
    • Handle data merging in umbrella charts.

    By following these principles, you can create highly configurable and maintainable Helm charts for your Kubernetes applications.

    For further information on Helm templates, refer to the Helm documentation.

    Notes:

    • Values from values.yaml and other sources are clearly outlined.
    • Practical examples like dynamic names, externalized values, and image versioning are highlighted.
    • The structure allows for easy reference and understanding of key Helm concepts.
    • A demo is provided, along with instructions for testing and debugging.

    Helm Template Logic

    This guide explores how to enhance your Helm templates by adding logic and utilizing powerful functions. We'll cover topics such as using functions and pipelines, controlling scopes, managing whitespace and indentation, logical operators, flow control, variables, helper functions, and sub-templates. By the end of this guide, you'll have a solid understanding of how to structure and organize your Helm templates for more dynamic and reusable Kubernetes resources.


    Table of Contents


    Introduction

    Helm templates allow you to define Kubernetes resources dynamically, and you can enhance their functionality by adding logic and transformations. Initially, templates were mostly about replacing variables with values from values.yaml or other manifests. However, templates can be much more powerful with functions, logical operations, and reusable code.

    This guide will show you how to:

    • Use functions and pipelines to modify values in templates.
    • Control whitespace and indentation.
    • Implement logical operators and flow controls.
    • Define and use variables.
    • Organize reusable code with helper functions and sub-templates.

    Using Functions and Pipelines

    Functions and pipelines are two ways of implementing logic in Helm templates. Both can be used to modify or transform values in your templates.

    Function Syntax

    Functions are written by specifying the function name first, followed by arguments. For example, quote "value" wraps the value in quotes.

    Pipeline Syntax

    Pipelines work by writing the value first, then piping it through transformations. The example below demonstrates how to pipe a value through a quote function:

    {{ "value" | quote }}
    

    Both functions and pipelines allow you to chain multiple arguments and transformations. For example, combining the default function with a pipeline:

    {{ .Values.services.name | default .Chart.Name }}
    

    Common Functions

    Here are some commonly used functions in Helm templates:

    • quote: Wraps a value in quotes.
    • upper: Converts a value to uppercase.
    • lower: Converts a value to lowercase.
    • trunc: Truncates a value to a specific length.
    • b64enc: Encodes a value in Base64 (useful for secrets).
    • toYaml: Converts a value to YAML format.

    Example usage of trunc and trimSuffix to avoid exceeding character limits:

    {{ .Values.name | trunc 63 | trimSuffix "-" }}
    

    Modifying Scope with "With"

    In Helm, values are often organized hierarchically, and it can become repetitive to access deeply nested properties. The with function allows you to create a new scope and avoid repeating the full path for every property.

    Without Scope

    spec:
      service:
        name: {{ .Values.service.name }}
        port: {{ .Values.service.port }}
    

    With Scope

    {{- with .Values.service }}
      name: {{ .name }}
      port: {{ .port }}
    {{- end }}
    

    Using with simplifies the template and ensures that you don't need to reference the entire path repeatedly.


    Controlling Space and Indentation

    Helm templates preserve whitespace, which can sometimes lead to unwanted carriage returns and incorrect indentation in the generated manifest. You can control these using - in the directive.

    Example of Controlling Whitespace:

    {{- with .Values.service }}
      port: {{ .port }}
    {{- end }}
    

    The dash (-) removes unwanted spaces or newlines before or after the directive. You can adjust the whitespace in your templates by strategically placing dashes at the beginning or end of a directive.

    You can also use the indent function to adjust indentation for properties within the template.


    Logical Operators and Flow Control

    Helm templates allow you to implement logical operations and flow control using functions and conditionals.

    Logical Functions

    • and: Logical AND operation.
    • or: Logical OR operation.
    • not: Logical NOT operation.
    • equal: Checks if two values are equal.
    • ne: Checks if two values are not equal.
    • lt / gt: Checks if a value is less than or greater than another value.

    Conditional Syntax (if / else)

    The if statement is used to evaluate conditions, and you can use else for alternative branches.

    {{- if .Values.enabled }}
    # Render resource if enabled is true
    {{- else }}
    # Render alternative if enabled is false
    {{- end }}
    

    Loops (range)

    To iterate over arrays or lists, you can use the range function.

    {{- range .Values.hosts }}
      - {{ .hostname }}
    {{- end }}
    

    Using Variables

    Variables in Helm allow you to store values that can be reused multiple times within a template. This is useful for organizing complex logic or bypassing scope restrictions.

    Defining a Variable

    {{- $variable := .Values.someProperty }}
    

    Variables are especially useful when you want to access values outside the scope of with or range. You can also use Helm's built-in global variable $ to reference values from the root scope.


    Calling Helper Functions and Sub-Templates

    Helm allows you to define reusable snippets of code, known as helper functions or sub-templates. These functions can be stored in _helpers.tpl files and included in other templates.

    Defining a Helper Function

    In your _helpers.tpl file:

    {{- define "mychart.fullname" }}
    {{ .Release.Name }}-{{ .Chart.Name }}
    {{- end }}
    

    Including a Helper Function

    You can include the helper function in other templates with the include function:

    fullname: {{ include "mychart.fullname" . }}
    

    This allows you to reuse the logic and keep your templates DRY (Don't Repeat Yourself).


    Demo: Adding Template Logic

    In this demo, we'll walk through adding template logic to a Helm chart. The goal is to externalize the logic for generating the release name and to make the database connection string dynamic.

    1. Define a Helper Function:

      • Create an _helpers.tpl file to define the release name logic.
    2. Refactor Templates:

      • Use the include directive to include the helper function in your deployment, ingress, and secret templates.
    3. Build a Dynamic MongoDB URI:

      • Use with and range functions to dynamically generate the MongoDB URI, based on the release name and chart name.

    After making these improvements, you can use the helm upgrade command to deploy the changes and ensure everything works as expected. By using functions, logical operators, flow controls, variables, and helper functions, you can add powerful logic to your Helm templates. This allows you to create flexible and reusable charts that can be easily customized and shared.


    Installing Dev and Test Releases with Helm Templates

    The scenario involves deploying two releases of the same Helm chart—one for the development environment and one for the test environment. The challenge is to dynamically generate hostnames for the ingress, based on the release name, instead of hardcoding them in the values.yaml file. This approach allows flexibility and simplifies the management of multiple releases.

    Application Architecture Overview

    The application consists of:

    • Frontend: A single-page application built with Angular.
    • Backend: A backend API that handles requests from the frontend.

    When users connect, the frontend page and its JavaScript are downloaded. The frontend then calls the backend API, which must also be accessible externally through an ingress. This requires two ingresses—one for the frontend and one for the backend API.

    Dynamic Hostnames for Ingress

    To make the ingress configuration flexible, we want to dynamically set the hostnames based on the release name. This allows us to deploy both the dev and test environments without having to manually change the hostnames in the values.yaml file.

    Steps to Configure the Ingress

    1. Disable Ingress in Individual Charts:

      • In both the frontend and backend charts, the ingress is initially enabled by default for standalone deployments. However, we want to disable these in the umbrella chart.
      • This is done by using the if directive to conditionally render the ingress resources based on the ingress.enabled value.
    2. Override Ingress Settings in the Parent Chart:

      • In the umbrella chart, we disable the ingress for both the frontend and backend by overriding the ingress.enabled value to false.
      • We also define the ingress object with dynamic host definitions.
    3. Dynamic Hostnames:

      • The hostname is generated dynamically based on the release name, so each release gets a unique hostname.
      • For example, for the dev release, the frontend would be accessible via dev.frontend.minikube.local, and the backend via dev.backend.minikube.local. Similarly, the test release would use test.frontend.minikube.local and test.backend.minikube.local.

    Example: Building the Ingress Manifest

    Let's walk through how we can dynamically generate the ingress manifest using Helm templates.

    Step 1: Define the Ingress Resource in ingress.yaml

    In the templates/ingress.yaml file, we define the ingress resource like this:

    apiVersion: networking.k8s.io/v1
    kind: Ingress
    metadata:
      name: {{ .Release.Name }}-ingress
    spec:
      rules:
        {{- range .Values.ingress.hosts }}
        - host: {{ .Release.Name }}.{{ . }}.minikube.local
          http:
            paths:
            - path: /
              pathType: Prefix
              backend:
                service:
                  name: {{ .Release.Name }}-{{ . }}
                  port:
                    number: 80
        {{- end }}
    

    Step 2: Define Values in values.yaml

    In the values.yaml file for the umbrella chart, you can set the following values:

    ingress:
      enabled: true
      hosts:
        - frontend
        - backend
    

    Step 3: Generate Dynamic Hostnames

    The ingress.yaml file dynamically generates the hostnames based on the release name and the host (either frontend or backend). For example, if you install the release as dev, the frontend will be accessible at dev.frontend.minikube.local, and the backend will be accessible at dev.backend.minikube.local.

    Testing the Chart

    After creating the Helm chart with dynamic ingress, we can test it by deploying both the dev and test environments.

    Step 1: Configure DNS and Hosts

    Before deploying, ensure your DNS or hosts file points the subdomains (e.g., dev.frontend.minikube.local and test.frontend.minikube.local) to your Minikube IP. This is necessary for the browser to route the requests correctly.

    Step 2: Install the Dev and Test Releases

    1. Install the Dev Release:
    helm install dev my-umbrella-chart --set guestbook_name=DEV
    

    This will install the dev release and set the guestbook_name to DEV. The ingress should now be available at dev.frontend.minikube.local.

    1. Install the Test Release:
    helm install test my-umbrella-chart --set guestbook_name=TEST
    

    This will install the test release and set the guestbook_name to TEST. The ingress will be available at test.frontend.minikube.local.

    Step 3: Verify the Deployment

    Check the pods for both dev and test releases:

    kubectl get pods
    

    You should see six pods in total: three for the dev release and three for the test release.

    Now, test the two releases by accessing the following URLs:

    • Dev Release: dev.frontend.minikube.local (guestbook name should be DEV)
    • Test Release: test.frontend.minikube.local (guestbook name should be TEST)

    This confirms that both releases are running independently with dynamically generated hostnames.

    Conclusion

    Key Concepts Covered

    1. Dynamic Hostnames: We used the release name to generate dynamic hostnames for the ingress resource, allowing multiple environments (dev and test) to coexist with minimal configuration changes.
    2. Helm Template Functions: We applied functions like range and if to control how the ingress rules were applied based on user-defined values.
    3. Helm Values Override: We used --set to override values at install time, which allows for easy customization without modifying the values.yaml file directly.

    Next Steps

    With this approach, you've learned how to customize a Helm chart and make it more flexible by dynamically configuring resources based on the release name. The next step would be to look into managing Helm chart dependencies and publishing charts in Helm repositories for sharing and reuse across different projects.

    Note: To fully integrate this with your application, you may also need to dynamically build the backend URI in the frontend chart, similar to how the MongoDB URI was dynamically constructed in previous demos.


    Want to discuss cloud architecture? Find me on LinkedIn.

    Found this useful? Let's go deeper.

    Book a free 15-minute call to discuss your cloud, DevOps, or AI strategy challenges.

    Book a Free Call