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.