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:
- 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.
- 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.yamlfile.- 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:
- Static Method: Run
helm template <chart-name>locally to render the templates without connecting to Kubernetes. - Dynamic Method: Use
helm install --dry-run --debugto 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:
- Values from
values.yaml: These can be defined directly or overridden via the command line using--set. - Chart Metadata: Accessed via
.Chartto get information about the chart itself (e.g.,.Chart.Name,.Chart.Version). - Release Metadata: Accessed via
.Releaseto get data like the release name (.Release.Name). - Cluster Capabilities: Accessed via
.Capabilities, useful for making charts compatible with specific Kubernetes versions. - 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.
-
Dynamic ConfigMap Name: Replace the hardcoded
configmapname 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 }}" -
Externalize Values: In the
values.yamlfile, externalize theguestbook_nameandbackend_urivalues:config: guestbook_name: "guestbook-app" backend_uri: "http://backend-service" -
Dynamic Image Versioning: Use the
values.yamlto specify the Docker image and tag, making it easy to upgrade the application without modifying the template.image: repository: "phico/frontend" tag: "2.0" -
Dynamic Service Port: Externalize the service port and type:
service: port: 8080 type: ClusterIP -
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:
helm template <chart-name>: To render the templates locally.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.yamland 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
- Using Functions and Pipelines
- Modifying Scope with "With"
- Controlling Space and Indentation
- Logical Operators and Flow Control
- Using Variables
- Calling Helper Functions and Sub-Templates
- Demo: Adding Template Logic
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.
-
Define a Helper Function:
- Create an
_helpers.tplfile to define the release name logic.
- Create an
-
Refactor Templates:
- Use the
includedirective to include the helper function in your deployment, ingress, and secret templates.
- Use the
-
Build a Dynamic MongoDB URI:
- Use
withandrangefunctions to dynamically generate the MongoDB URI, based on the release name and chart name.
- Use
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
-
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
ifdirective to conditionally render the ingress resources based on theingress.enabledvalue.
-
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.enabledvalue tofalse. - We also define the ingress object with dynamic host definitions.
- In the umbrella chart, we disable the ingress for both the frontend and backend by overriding the
-
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 viadev.backend.minikube.local. Similarly, the test release would usetest.frontend.minikube.localandtest.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
- 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.
- 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 beDEV) - Test Release:
test.frontend.minikube.local(guestbook name should beTEST)
This confirms that both releases are running independently with dynamically generated hostnames.
Conclusion
Key Concepts Covered
- 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.
- Helm Template Functions: We applied functions like
rangeandifto control how the ingress rules were applied based on user-defined values. - Helm Values Override: We used
--setto override values at install time, which allows for easy customization without modifying thevalues.yamlfile 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.