# Helm

Helm is a package manager for Kubernetes. The packages themselves are called Charts. Installed chart is called a Release.

# Why?

  • Typically, K8s applications require multiple objects: deployments, services, ingresses, storage, configs, etc. Usually, these are defined in separate YAML files.
    • we'd like to treat these objects "atomically" - they are all parts of one app
    • the order of objects creation might matter (e.g. config before deployment)
    • versioning is not straightforward - we have to remember the differences between two versions and manually apply them

With Helm, we get the following:

  • We treat the app as a single entity, "forgetting" that it consists of multiple objects - an app in a package
  • Packages are versioned, making it much easier to upgrade/downgrade apps
  • Templates allow extracting variables from YAML files for customizability
  • Management of dependencies

# Helm Charts

A Chart is a package.

# Structure

A chart is an archive containing a single folder:

my-chart
|  charts (optional)
|  |  mongodb-7.8.4.tgz
|  Chart.yaml
|  README.md
|  requirements.yaml (optional)
|  templates
|  |  deployment.yaml
|  |  _helpers.tpl
|  |  ingress.yaml
|  |  service.yaml
|  |  NOTES.txt
|  .helmignore
|  values.yaml
|  values.schema.json

TIP

Files in /templates prefixed with _ are not rendered as manifests.

  • the name of the folder is the name of the chart.
  • the Chart.yaml contains metadata about the chart (like a version, dependencies, etc.)
  • dependencies can be also included as sub-charts, inside of the charts folder, or listed in the requirements.yaml (see note below)
  • the templates folder contains customizable YAML files (or just "normal" K8s-ready YAMLs if customization is not needed).
  • the documentation should be placed in the README.md file
  • the message to be displayed after chart installation should be placed in the templates/NOTES.txt file. It is a template, so it can make use of placeholders to dynamically include some values.
  • the template values can be specified in the values.yaml file. It contains the default values.
  • the values.schema.json file defines the structure of the values.yaml file as JSON schema
  • the .helmignore file allows us to specify files that should be ignored by Helm during templates processing
  • the _helpers.tpl file is often used to store user-defined functions/snippets

requirements.yaml

The requirements.yaml file is supported, but it's not a recommended way of adding dependencies. They should be listed in the Chart.yaml file nowadays.

An example of Chart.yaml:

apiVersion: v2
name: my-chart
description: A Helm chart for Kubernetes
keywords:
  - demo
type: application
version: 0.1.0 # SemVer 2.0
appVersion: 1.16.0
dependencies:
  ...

# Umbrella Chart

Some charts might be more complex, and they might contain a few major components, like a frontend, a backend, and a database. Each of these is a separate part of the application, with its own set of YAMLs. We can define them as separate charts, and then place them inside of the charts directory of the "umbrella" chart - a chart that brings the sub-components together.

Each sub-chart may contain its own values.yaml file. The umbrella chart's value.yaml may overwrite the sub-chart's values.

The values defined in the sub-charts can also be made available to the parent child. The sub-chart has to export values, and the parent's values.yaml has to import-values. There is also a way that does not require the sub-chart to define values inside of export. This functionality, in general, is rarely used, so I'm not describing it here.

# Types

Not all charts are apps. Some charts act as libraries of functions for other charts to consume. We can denote it by specifying type: library in the Chart.yaml file. Applications use type: application instead.

# Versioning

The Chart.yaml file specifies multiple version strings:

  • apiVersion - v2 is for Helm 3 (obviously)
  • appVersion - version of the app to be installed
  • version - version of the chart itself

An example: We could be preparing a chart for Discourse. The version of Discourse to be deployed could be "2.8.0.beta1". That would be the appVersion. Chart version could be 0.0.1 - version. If I change something in the charts (e.g. enhance the deployment properties somehow), I'd increment the version property. The appVersion would not change (unlesss a new version of Discourse came up and I wanted to include it in my chart as well).

Can I change appVersion without changing the version? I think that I shouldn't be able to do so.

Can I upgrade a release to a chart with major version change? A major change means breaking changes, so I am not able to upgrade?

# Releases

A Release is an installed chart on some cluster. Usually, we install a chart once, creating a single release. However, we could also install a chart multiple times creating multiple instances in a single cluster (e.g. "dev" and "test" environments or multiple instaces of some app, like a DB, for completely different purposes).

# Release Revision

We can update a deployed release. It can be due to:

  • desire to upgrade to a different version of the chart
  • change in some values of the release

We can upgrade existing release to a new revision with helm upgradae {release} {chart}. We can also go back in history with helm rollback {release} {revision}. We can print the history of revisions with helm history {release}.

# Templating

Helm allows us to prepare YAML files with placeholders for values - these are templates. Thanks to that, releases can be customized. Some values specified by the chart author can be replaced, while others could use the default values.

The placeholders in templates are wrapped in {{}}.

The placeholders in the templates are replaced with values when running Helm CLI commands (like install or upgrade).

TIP

Helm's template engine is based on the Go Template engine.

# Example

Template (for a service):

apiVersion: v1
kind: Service
metadata:
    name: {{ .Release.Name }}-{{ .Chart.Name }}
    labels:
        app.kubernetes.io/name: {{ .Chart.Name }}
spec:
    type: {{ .Values.service.type }}
    ports:
        - port: {{ .Values.service.port }}
          targetPort: 80
          protocol: TCP
selector:
    app.kubernetes.io/name: {{ .Chart.Name }}

values.yaml:

service:
    type: ClusterIP
    port: 80

# Sources of Data

Values for the templates can be supplied from various sources:

  • .Values:
    • values.yaml - chart's author places default values there
    • command line parameters (e.g. helm install --set foo=bar) - overwrites data from values.yaml
    • any other YAML file (then we need to specify it in the helm install -f some-file.yaml command) - overrided values in values.yaml
  • Chart.yaml - placeholder starts from .Chart.
  • other files - placeholder start from .Files.Get {file-name}
  • Template file itself - placeholder starts from .Template..
  • Release runtime data - template starts from .Release. (e.g. Release.Name, Release.Revision).
  • Cluster metadata - template starts from .Capabilities (e.g. Capabilities.KubeVersion)

PascalCase

Even though the properties in the files could be in camelCase, in the templates we have to refer to them in the PascalCase.

The values.yaml can be as convoluted as needed (it can contain objects, arrays). In the templates we can refer to these values. Examples:

  • .Values.service.name
  • .Values.service.names[0].displayName

Globals

If we define some data within the global key of values.yaml, the data will be available in any sub-chart under .Values.global.

# Schema

If we define the schema in the values.schema.json file, Helm will validate the values.yaml file before doing any actual operation (like install).

# With scope

In our template, we could have lots of repetitions in our placeholders (e.g. all of them starting with .Values.config). In such a case, we could use with to specify that prefix just once:

spec:
  {{ with .Values.config }}
  type: {{ .type }} # .Values.config.type
  ports:
    - port: {{ .port }} # .Values.config.port
      tagetPort: 80
  {{ end }}

New Lines

The "with" and end cause the resulting manifests to have additional newlines added in their place, which might be an issue. It can be solved with -, which removes new lines.

WARNING

Inside of the "with" scope, we cannot refer to anything outside of that scope. We have to use Variables.

# Dry Run

We can see how the templates will be turned into manifests with the helm template {chart-name} command (it doesn't even require K8s cluster connection).

We can also use the helm install {release-name} {char-name} --dry-run --debug.

# Logic

We may use functions in the templates when some additional logic is needed. There are 2 syntaxes:

  • function - quote(value)
    • arguments separated by commas
  • pipeline - value | quote
    • arguments separated by spaces, and placed after the function - first_arg | fn other_arg

Functions come from:

  • Go templates
  • Sprig project
  • Helm itself

# Function Examples

  • default - returns a value if exists, if not, returns some provided default
  • quote - adds quotes
  • upper - transforms to upper-case
  • b64enc - encodes to base64
  • trunc - truncates value to specified amount of characters (useful for labels, which are limited to 63 characters)

Operators are also functions:

  • eq
  • ne
  • gt
  • and
  • etc.

# Conditionals

{{ if and .adminEmail (or .serviceAccountJson .existingSecret) }}

It translates to: "if (adminEmail AND (serviceAccountJson OR existingSecret))"

Example of usage:

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  labels:
    app.kubernetes.io/name:
{{- if .Values.service.name -}} # - removes newline
{{ .Values.service.name | trimSuffix "- "}}
{{- else -}}
{{ .Chart.Name }}
{{- end -}}

We can build the manifest differently depending on some values.

# Loops

An example:

apiVersion: extensions/v1beta1
kind: Ingress
spec:
{{- range .Values.ingress.hosts }}
  - host: {{ .hostname | quote }}
    http:
      paths:
      {{- range .paths }}
        - path: {{ .path }}
          backend:
            serviceName: {{ .service }}
            servicePort: http
      {{- end }}
{{- end}}

values.yaml:

ingress:
  hosts:
    - hostname: frontend.local
      paths:
        - path: "/public"
          service: "frontend"
        - path: "/admin"
          service: "admin"
    - hostname: backend.local
      paths: []

WARNING

Inside of the loop, we cannot refer to anything outside of the range's scope. We have to use Variables.

# Variables

Variables allow us to define values within templates and name them. They are useful when using the "with" scoping or loops, since within these we cannot refer to anything outside of their scope. However, we can refer to variables.

 






 


{{ $defaultPortName := .Values.defaultPortName }} # variable definition
spec:
  {{ with .Values.config }}
  type: {{ .type }}
  ports:
    - port: {{ .port }}
      tagetPort: 80
      name: {{ $defaultPortName }} # we use a variable to get a value outside of .Values.config, that we're scoped to
  {{ end }}

TIP

Variables may also point to some scope of the source and we can traverse that scope when using the variable.

{{ $currentHost := . }} # catches current scope
{{ range .paths }}
  # we can use {{ $currentHost.whatever }} here to break out of the .paths scope
{{ end }}

# Named Templates

We can create a _helpers.tpl (just a convention name) file to store various snippets that we want to reuse (like some conditionals, loops, or just manifest parts). The contents should be wrapped in the define function which allows us to name the function.

TIP

Global Names The names of named templates are global. It's a good practice to prefix the name with chart's name. This way we can skip potential conflicts in sub-charts.

To use these named templates we need to include them in other templates. We could also use Go's template instead to include it, but it is not as functional (we can't pipe the output to another function).

Libraries

A chart of type "library" would contain functions only, without any templates.

# Dependencies

Chart's dependencies can be included in the following ways:

  • by placing chart's folders in /charts of the main chart
  • by placing archives of charts in /charts of the main chart
  • dependencies array in the Chart.yaml file.
  • dependencies array in the requirements.yaml file - OBSOLETE! The Chart.yaml file is recommended nowadays.

The helm dependency update {chart-name} fetches latest versions of dependencies matching ranges defined in the Chart.yaml. Helm downloads dependencies as tgz archives and places them in the /charts directory.

Chart.lock

Similarly as in npm, Helm has the Chart.lock file, which lists the exact versions of dependencies that have been pulled. Running helm dependency build {chart-name} downloads dependencies as defined in that file.

Helm 2 used the requirements.lock file for the same purpose.

# Conditions

Dependencies can be installed conditionally, based on configuration values (values.yaml). An example:

dependencies:
  - name: mongodb
    version: 7.8.x
    repository: https://some-repo.com
    condition: database.enabled # values.yaml

Another way is to tag dependencies with tag and set that tag to true/false in the values.yaml. It's useful if multiple dependencies shoud be enabled/disabled at once.

Priority

Condition overwrite tags

# Repositories

There are repositories of charts. We can add them to Helm CLI for it to be able to fetch charts. There is an official repository at https://kubernetes-charts.storage.googleapis.com (opens new window).

By default, Helm CLI does not have any repositories configured.

Repos can be added with helm repo add repo-name repo-url.

A repository is just an HTTP server containing the charts and the index.yaml file describing charts.

Chartmuseum

There's an HTTP server implementation dedicated for hosting Helm chart repositories - Chartmuseum (opens new window)

# Publishing

Before publishing our charts, we need to package them into archives. It can be done with the helm package {chart-name} command. It results in a .tgz archive being created.

# Internals

Helm stores details of releases in the K8s cluster itself - as secrets. They are stored in the same namespace as the application/release.

Helm 2

In Helm 2, there was also a server-side component running on the cluster - Tiller. Helm CLI would communicate with Tiller.

In Helm 3, Tiller no longer exists. Helm CLI communicates with the K8s API only.

# Three-Way Merge Patch

Helm applies upgrades/downgrades by looking at the following:

  • last installed chart version
  • chart version to be installed
  • current state of the app

It's possible that the release has been modified outside of Helm control (e.g., manually via kubectl or some operator). Even in such situations, Helm is able to persist these manual changes, as long as they don't conflict with the chart to be installed. Before applying the change, it marges the target state with the "external" modifications.

# Namespaces

By default, Helm installs resources into the "default" namespace. However, we can specify another namespace while installing.

# Commands

# Install a Chart

helm install {release-name} {chart-name}

# Uninstall a Chart

To remove a release, together with Helm-managed secrets metadata, run helm uninstall {release-name}.

# Get Manifest

We can have a look at the YAMLs of the deployed release with helm get manifest {release-name}.

Last Updated: 3/12/2023, 5:06:29 PM