Notes on Helm
Published: 2025-06-20 (updated: 2025-06-27)
These are some notes I made during my journey developing Helm charts, and some things I wish I knew when I started. The examples are reminders to myself on how to implement common patterns when building Helm charts.
What is Helm
Helm is a package manager for Kubernetes that provides an easy way to find, share and deploy software.
Software is packaged up into a chart
, which can then be deployed into a Kubernetes cluster by running helm install <chart name>
.
A chart can consist of many services such web servers, application servers, and database servers. All deployment and application logic is encapsulated into the chart - This is extremely powerful, because it enables the deployment of the application in seconds, without the need to understand the advanced concepts of how to deploy the application.
A chart consists of a set of templates
written in YAML format. These are essentially Kubernetes resource definition files that typically contain logic and variables that are replaced during the rendering process.
When a chart is deployed, the Helm rendering engine takes the values defined in values.yaml
and combines them with the templates. The result of this is a set of Kubernetes manifests that can be applied to a cluster.
Artifact Hub
ArtifactHub is a good source for finding and installing Helm charts.
To install a chart, you typically need to add a repository to your local repository list before installing it. Here is an example taken from ArtifactHub.
helm repo add bitnami https://charts.bitnami.com/bitnami
helm install my-nginx bitnami/nginx
You can also search ArtifactHub directly from the Helm command.
helm search hub nginx
Download a chart
You can download a chart using the pull
command.
Notice that the chart is located in the docker.io registry? Yes, that's right, you can publish your charts in OCI compliant registries!
helm pull oci://registry-1.docker.io/bitnamicharts/nginx
Using "template"
You can render a Helm chart into it's resulting yaml without installing it using the helm template
command. This can be useful when developing charts to see what the results will be.
You can specify .
instead of the chart directory name if you navigate into your Helm chart directory.
helm template .
Using --set " to define a value
You can override values in values.yaml
using --set
.
helm install . --set image.tag=v1.0.0
If logic
A simple if
statement that creates a service account, if the key CreateSecretStore
is found in values.yaml
.
{{ if .Values.CreateSecretStore }}
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ .Release.Name }}-externalsecrets-vault
{{ end }}
Insert yaml from values.yaml using "with"
The following is an example of using the with
statement. When this is passed through the Helm rendering engine, the contents of allowedServiceAccounts
and allowedNamespaces
from values.yaml
will be inserted using the toYaml
function. The list, slice, dict or array will be indented by the specified indent width.
nindent
prefixes a new line to the beginning of the thing being inserted.
apiVersion: k8s.faster.ice.mod.gov.uk/v1
kind: VaultRole
metadata:
name: {{ .Release.Name }}-externalsecrets-vault
spec:
authPath: {{ .Values.kubernetesAuth.authPath }}
authType: kubernetes
serviceAccountNames:
- {{ .Release.Name }}-externalsecrets-vault-access
{{- with .Values.kubernetesAuth.allowedServiceAccounts }}
{{- toYaml . | nindent 4 }}
{{- end }}
serviceAccountNamespaces:
- {{ .Release.Namespace }}
{{- with .Values.kubernetesAuth.allowedNamespaces }}
{{- toYaml . | nindent 4 }}
{{- end }}
policy:
- {{ .Release.Name }}-readonly
Using "range" to loop over a list
The range
function is like a for
loop. This example iterates over additionalNamespaces
and inserts the item where the {{ . }}
variable has been defined:
{{- if .Values.additionalNamespaces }}
{{- range .Values.additionalNamespaces }}
apiVersion: v1
kind: Namespace
metadata:
name: {{ . }}
---
{{- end }}
{{- end }}
Accessing the global scope using "$"
When using the range
function, the scope changes to the context of each item in the collection. Here is an example which would result in an error:
{{ if gt (len .Values.kubernetesAuth.allowedNamespaces) 0 }}
{{ range .Values.kubernetesAuth.allowedNamespaces }}
- kind: ServiceAccount
name: {{ .Release.Name }}-externalsecrets-vault-access
namespace: {{ . }}
{{ end }}
{{ end }}
If you were to deploy the chart with the above logic, you would encounter the following error:
Error: template: vault-secret-engine/templates/rbac/ClusterRoleBinding.yaml:16:21: executing "vault-secret-engine/templates/rbac/ClusterRoleBinding.yaml" at <.Release.Name>: can't evaluate field Release in type interface {}
To fix this, use $
to access the root scope:
{{ if gt (len .Values.kubernetesAuth.allowedNamespaces) 0 }}
{{ range .Values.kubernetesAuth.allowedNamespaces }}
- kind: ServiceAccount
name: {{ $.Release.Name }}-externalsecrets-vault-access
namespace: {{ . }}
{{ end }}
{{ end }}
Debugging with --dry-run and --debug
Using --dry-run
with --debug
can be extremely helpful when developing Helm charts.
helm install my-nginx bitnami/nginx --dry-run --debug
Library charts and dependencies
You can run into some frustrating issues when using common library charts and dependencies, and it's worth investigating these before you walk that path.
Here is the scenario I encountered.
A "base chart" was created that included things like deployments and services.
Each application in our stack had a Helm chart that used the base chart as a dependency, with some applications pinned to specific versions of the base chart. Each application resided in it's own git repository.
To simplify the deployment of our application stack, an application stack chart was created. The dependency chain looked something like this:
Application "svc-01" chart:
apiVersion: v2
name: svc-01
type: application
version: 1.0.0
dependencies:
- name: base-chart
version: 0.1.0
repository: https://foo.bar/charts
Application "svc-02" chart:
apiVersion: v2
name: svc-02
type: application
version: 1.0.0
dependencies:
- name: base-chart
version: 0.2.0
repository: https://foo.bar/charts
Application stack chart:
apiVersion: v2
name: Application-stack
type: application
version: 1.0.0
dependencies:
- name: svc-01
repository: https://foo.bar/charts
- name: svc-02
repository: https://foo.bar/charts
WARNING
This does not work, because Helm fails to correctly resolve the dependencies, as it is not possible to have version 0.1.0
and 0.2.0
of base-chart
in the same release - Helm is only capable of using a single version of base-chart.
If svc-02
is using newer features in base-chart 0.2.0
, but Helm only has access to version 0.1.0
within it's dependencies chain, then you will encounter errors when trying to install the chart.
This creates an issue whereby if the base chart needs to be updated with new features, all of your applications that use base chart as a dependency need to be configured to use the same version, and this becomes difficult to maintain when you have a large number of micro-services. You can of course deploy all of your charts separately, but then this can also become an administrative nightmare to main across multiple environments.