Simplifying Kubernetes Resource Management with Helm 3

Posted by Fabrizio Lazzaretti on Friday, April 12, 2024

Kubernetes is an excellent tool for container orchestration, but managing configurations and deployments manually can become cumbersome as your environment grows. Helm simplifies this process by introducing the concept of “charts.” These charts encapsulate Kubernetes manifests, offering a reusable and templated solution for orchestrating deployments, services, config maps, and other Kubernetes resources. Overall, Helm makes deployments configurable, simpler, repeatable, and manageable.

Helm helps you to deploy your own code, but also to manage and deploy necessary infrastructure components - such as Redis, Prometheus, cert-manager, MinIO, Nginx, or PostgreSQL - with ease.

This blog post provides a beginner-friendly introduction to Helm 3, highlighting its key features and benefits. We’ll explore the distinctions from Helm 2, its potential benefits, and steps to begin using it. Towards the conclusion, we’ll delve into recommended practices for seamlessly integrating it into your current DevOps framework.

No Helm Without Kubernetes

To commence our explanation of Helm, it’s essential to briefly examine Kubernetes, often abbreviated as K8s, and grasp some fundamental principles of its operation.

Kubernetes is an open-source container orchestration system for automated software deployment. The project is Cloud Native Computing Foundation (CNCF​) project, like Helm itself.

The Concept of “Desired State”

Kubernetes employs the foundational concept of a “desired state,” where in resources are specified, typically in YAML format, to reflect their intended configuration. Kubernetes then endeavors to reconcile the current state with the desired state outlined in the manifests.

For example, you normally deploy a simple microservice with a Deployment. In a Deployment, you set the amount of desired replicas. When you apply this Deployment, the app will first have no replicas at all, then Kubernetes will try to go to the desired state and deploy the needed Pods for the deployment.

By manually deleting a Pod, Kubernetes will recreate a Pod to get to the desired number of replicas.

The Challenge With Kubernetes Manifests

While YAML files provide a powerful way to configure Kubernetes resources, managing them can become complex and error-prone as the number of files and configurations increases.

What is a Kubernetes manifest?

A Kubernetes manifest is a YAML (sometimes also represented as a JSON file) that defines the desired state of Kubernetes resources such as a Pod, Deployment, or Service.

File Size

The files get huge, and it’s hard to keep track of what is important. The issue lies in the API’s low-level nature, lacking built-in abstraction.

Copy-Paste Problem

I’ve come across many errors and peculiar setups due to the direct copying of manifest files. For instance, engineers would duplicate an existing deployment to create a new one and only update certain values, leaving others untouched. Consequently, unintentionally, the CPU and RAM limits from the previous Deployment were reused. As a result of this copy-paste approach, it became unclear which values were intentionally set and which ones were merely copied over.

Zombies

Another big problem are “zombie” resources: When you use YAML files to deploy multiple resources as Kubernetes manifests, you normally just do a kubectl apply. This will create and update resources. However, if you rename or delete resources, the old ones won’t be deleted. To accomplish this, you’ll require a kubectl delete command, which must be executed explicitly because Kubernetes lacks awareness of the resources contained in the file beforehand.

In fact, not removing unused resources on Kubernetes is one of the biggest factors for overspending on Kubernetes, according to the latest FinOps + Cloud Financial Management Microsurvey of the CNCF.

The Solution

Helm solves these problems by introducing the concept of charts.

What Is Helm?

Helm is a graduate project of the CNCF that simplifies Kubernetes deployments using charts. A chart is a bundle of Kubernetes configurations, similar to an apt-get package on Ubuntu. Helm uses these packages called charts to deploy resources onto a Kubernetes cluster.

In essence, it handles three main tasks: creating, updating, and deleting Kubernetes resources. So, it basically does kubectl apply, kubectl delete (to simplify).

Did you know what the original meaning of “Helm” is?

“A helmsman” or “helm” is a person who steers a ship, sailboat, submarine, other types of maritime vessel, or spacecraft."1

Helm Charts

Helm charts are the packages of Helm. A simple package has the following structure:2

mychart/​
  Chart.yaml          # A YAML file containing information about the chart
  values.yaml         # The default configuration values for this chart
  templates/          # A directory of templates that, when combined with values,
                      # will generate valid Kubernetes manifest files.

The format is more powerful and can contain more, as you can see below.2 However, our focus will be on the most crucial files at the top.

mychart/​
  Chart.yaml          # A YAML file containing information about the chart
  LICENSE             # OPTIONAL: A plain text file containing the license for the chart
  README.md           # OPTIONAL: A human-readable README file
  values.yaml         # The default configuration values for this chart
  values.schema.json  # OPTIONAL: A JSON Schema for imposing a structure on the values.yaml file
  charts/             # A directory containing any charts upon which this chart depends.
  crds/               # Custom Resource Definitions
  templates/          # A directory of templates that, when combined with values,
                      # will generate valid Kubernetes manifest files.
  templates/NOTES.txt # OPTIONAL: A plain text file containing short usage notes

When distributing the charts, you can opt for either an artifact or an Open Container Initiative (OCI) image.

Chart.yaml File

The Chart.yaml file contains metadata of the chart.3

Below is a quite minimal version of the Chart.yaml file to show the intent of the chart:

apiVersion: v2
name: mychart
type: application
version: 0.1.0

values.yaml File

The values.yaml file contains the values of the chart, which are then utilized within the templates to configure the manifest files.

Templates

The template files in the templates/ folder are rendered with the go template syntax. However, helm extended the Go template language with some extra functions and wrappers.4 They look as follows:

apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ include "mychart.fullname" . }}-config-map
  labels:
    {{- include "mychart.labels" . | nindent 4 }}
data:
  config: {{ .Values.config }}

The Go templating engine is quite powerful. You have functions, pipes (|), nesting, logical operations and loops.

Helm Version 3: A Major Improvement With a New Architecture

Before we dive into Helm 3, it’s essential to note that Helm underwent significant changes in version 3. In Helm 2, there was a Tiller component running in the Kubernetes cluster, but Helm 3 removed this, making Helm a client-only application. This change simplified Helm usage, removed security concerns5, and allowed users to start using Helm without deploying additional components.

Helm 3 vs Helm 2: The architectural difference

Helm 3 vs Helm 2: The architectural difference

Security

Helm 2 used a single Tiller installation for all deployments on the cluster. This meant that all role-based access controls (RBACs), which were required for any deployment in Helm, needed to be given to Tiller. With Helm 3, you can give each developer (or continuous deployment (CD) pipeline user) only the access she/he/it needs for the deployment.

Practical Usage: Deploying an Nginx Server

Let’s walk through a simple example of deploying an Nginx server using Helm. Nginx is a simple HTTP server that is commonly used for web applications on Kubernetes.

Here, we’ll briefly cover how we can accomplish this. In the next section we will further dive into the details of what happened.

  1. Find the Chart: Helm applications are installed with charts. You can find charts really easily on the Artifact Hub. The Nginx chart can be found here: https://artifacthub.io/packages/helm/bitnami/nginx

  2. Install the Chart:

    helm install hello-world oci://registry-1.docker.io/bitnamicharts/nginx​
    

    Here, we install the chart from an OCI source.

    hello-world is the name of the release that we have chosen for this example.

And that’s it!

After installation, we can use our web server. To test it briefly, we can initiate a port forwarding: kubectl port-forward svc/hello-world-nginx 8000:80.

Demo of how to deploy a chart (here, an Nginx server).

You can try that yourself with the GitHub sample demo-1.sh.

Deploy a Chart: What Happens Exactly?

Now let’s go into the details on what happened exactly when we deployed the chart:

1. Pull

The first thing the helm install command does, is pulling the chart (if the chart is stored remotely). In this case, we use an OCI registry. Traditionally, Helm charts were stored as artifacts, but there’s a growing trend toward utilizing OCI registries. This streamlines hosting, as you can utilize your Docker registry as both a Docker and a Helm registry simultaneously.6

Helm install: pull image

The image gets pulled from the registry to the local computer.

You can try that yourself with the GitHub sample demo-2.sh.

2. Template

After the image is pulled, it gets templated. Templating is the process of rendering the Go template-based charts to YAML-based Kubernetes manifest files, which can then be sent to the Kubernetes API.

Unrendered Chart (head):

{{- /*
Copyright VMware, Inc.
SPDX-License-Identifier: APACHE-2.0
*/}}

apiVersion: {{ include "common.capabilities.deployment.apiVersion" . }}
kind: Deployment
metadata:
  name: {{ include "common.names.fullname" . }}
  namespace: {{ include "common.names.namespace" . | quote }}
  labels: {{- include "common.labels.standard" ( dict "customLabels" .Values.commonLabels "context" $ ) | nindent 4 }}
  {{- if .Values.commonAnnotations }}
  annotations: {{- include "common.tplvalues.render" ( dict "value" .Values.commonAnnotations "context" $ ) | nindent 4 }}
  {{- end }}

Rendered chart (head):

---
# Source: nginx/templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: hello-world-nginx
  namespace: "knative-debug"
  labels:
    app.kubernetes.io/instance: hello-world
    app.kubernetes.io/managed-by: Helm
    app.kubernetes.io/name: nginx
    app.kubernetes.io/version: 1.25.4
    helm.sh/chart: nginx-15.14.0

You can try that yourself with the GitHub sample demo-3.sh.

3. Install

Now, the rendered manifest files can be deployed on Kubernetes, which is simplified the same as a kubectl apply.

Helm install: install

Install the chart on Kubernetes

4. The Deployment​

When the chart is deployed on Kubernetes, the reconciliation loop will try to achieve the desired state. In the case of the deployment, Kubernetes will try to start the Pods.

You can try that yourself with the GitHub sample demo-4.sh.

Helm install: deploy

Kubernetes deploys the desired resources.

Make Changes to the Chart

In the previous example, the deployment of a chart was illustrated. We initially installed the chart exactly as provided (only with default values). However, the true strength of Helm lies in customizing these charts to suit specific needs.

If we look again at the Nginx chart, we can find all the parameters that can be configured on the Artifact Hub: https://artifacthub.io/packages/helm/bitnami/nginx#parameters

Now, let’s change the configuration replicaCount. This will change the number of replicas that will be deployed. We have two methods to alter the configuration: either by passing a parameter or by applying a file containing the corresponding values.

  • Passing a value by parameter

    A parameter can be passed to the chart by adding --set with the appropriated parameter:

    helm upgrade hello-world oci://registry-1.docker.io/bitnamicharts/nginx \
      --set replicaCount=2
    
  • Passing a value by file

    You need to create a YAML file containing the value. For our example, we call it myvalues.yaml, containing:

    replicaCount: 2
    

    Afterward, we can upgrade the chart by passing a reference to our file:

      helm upgrade hello-world oci://registry-1.docker.io/bitnamicharts/nginx \
        -f myvalues.yaml
    

Once the change is deployed, you’ll observe with kubectl get po that we now have two Nginx Pods.

Revisions

These upgrades (e.g., a value change from just before) to a release will always create a new revision. We can see that by listing the releases:

$ helm ls
NAME            NAMESPACE       REVISION        UPDATED                                 STATUS          CHART           APP VERSION
hello-world     default         2               2024-04-02 22:30:04.156194 +0200 CEST   deployed        nginx-15.14.2   1.25.4     

If a change was a mistake or something is not working anymore, we can simply roll back the change to the previous revision:

$ helm rollback hello-world
Rollback was a success! Happy Helming!

You can try that yourself with the GitHub sample demo-10.sh.

There are numerous charts available for Helm beyond just Nginx; you can find some popular by viewing the list on Artifact Hub Stats. Here are some noteworthy highlights that you may find useful:

kube-prometheus-stack: kube-prometheus-stack collects Kubernetes manifests, Grafana dashboards, and Prometheus rules combined with documentation and scripts to provide easy to operate end-to-end Kubernetes cluster monitoring with Prometheus using the Prometheus Operator.

— Open in Artifact Hub

postgresql: PostgreSQL (Postgres) is an open source object-relational database known for reliability and data integrity. ACID-compliant, it supports foreign keys, joins, views, triggers and stored procedures.

— Open in Artifact Hub

argo-cd: A Helm chart for Argo CD, a declarative, GitOps continuous delivery tool for Kubernetes.

— Open in Artifact Hub

Create an Own Chart

In many scenarios, you’ll likely need to deploy your own code rather than solely relying on charts from others. Let’s explore how we can create our own chart for that purpose.

Create a Chart From a Template

To start with the creation of your own chart, it’s really useful to generate it from the template. To do so, just execute helm create mychart. This will create a folder with the needed structure for a new chart. It also creates a deployment, an ingress, a service, etc. That’s an excellent way to begin. The fully generated structure looks as follows:

mychart
├── Chart.yaml
├── charts
├── templates
│   ├── NOTES.txt
│   ├── _helpers.tpl
│   ├── deployment.yaml
│   ├── hpa.yaml
│   ├── ingress.yaml
│   ├── service.yaml
│   ├── serviceaccount.yaml
│   └── tests
│       └── test-connection.yaml
└── values.yaml

After the chart is configured as needed, it can be installed as before with helm install myapp ./mychart.

Publish a Chart

Publishing a chart is really simple using the OCI format. You can just use any OCI/Docker registry (e.g., Docker Hub or Amazon ECR)7 for it:

  1. Pack the chart

    helm package ./mychart

  2. Publish the chart

    helm push mychart-0.1.0.tgz oci://my.registry/project/mychart

Publishing a chart to a Helm chart repository as an artifact is also an option, though it’s a more complex process and beyond the scope of this post. For more details, see the official docs.

Best Practices

Now that we understand the basics, let’s look into some best practices when working with Helm.

Complexity With Many Charts

When you realize how Helm can streamline your processes and start applying it to all your resources, managing multiple Helm releases can become challenging. How should we handle this complexity?

Chart Dependencies

Helm charts can depend on other Helm charts. In a Chart.yaml file, you can declare these dependencies:

# umbrella-chart/Chart.yaml​
dependencies:​
  - name: svc1​
    repository: "https://charts.ex.com/repo"    version: 1.2.3  - name: svc2​
    repository: "https://charts.x.com/repo"    version: 2.3.4

Umbrella Charts

If you aim to deploy all your charts at once, enabling deployment with a single helm install, you can achieve this by creating an umbrella chart. An umbrella chart is a chart that contains all other charts as a depencencies.

@startuml
top to bottom direction
(<<Chart>>\nUmbrella Chart) as (U)
(<<Chart>>\nMicroService 1) as (MS1)
(U) --> (MS1)
(U) --> (<<Chart>>\nMicroService 2)
(MS1) --> (<<Chart>>\nDB)
@enduml

The Problem With Dependencies

Here are some problems with “the big umbrella chart” approach:

  • No separately deployable units While having dependencies between Helm charts can be beneficial, deploying numerous Helm charts with dependencies as one atomic unit can introduce instability. The deployment will be slow, and the rollback is bigger, as we always need to roll back all charts together.
  • Installation of subcharts Subcharts still need to be installed first before you can use helm upgrade. This makes a CI deployment more difficult.
  • Release name isolation There is no name isolation on subcharts. Thus, when MicroService 2 of the previous example will have a dependency called DB for example, there will be a naming collision with the MicroService 1 dependency DB.
  • Kubernetes namespaces You cannot set a Kubernetes namespace for each chart; instead, you need to select a single namespace for the umbrella chart.

There is a solution to all these problems: helmfile. This is an additional tool that encapsulates Helm’s functionality.

CI/CD & GitOps

It’s good practice to have a continuous integration (CI) and continuous deployment (CD) pipeline in order to have tested increments that can be deployed reproducable and fast. This approach fosters a culture of small, frequent deployments.

A simple approach to achieve this with Helm is to create a GitOps flow like the one in the image below:

  1. You start with the current deployed version (here v1).
  2. A change is created in a separate branch (e.g., a feature or a fix).
  3. A helm diff can be executed to see the changes that will be deployed when the branch is merged. (Helm Diff is a plugin of Helm, it will shortly be introduced in Helm Diff)
  4. helm lint can analyze static config problems.
  5. Once the change looks fine, it can be approved and merged.
  6. This will result in a new version of the deployment.
  7. Finally, this version can be released with a helm upgrade in the CD pipeline.
Possible CI/CD integration of Helm in git

Possible CI/CD integration of Helm in git

Terraform

If you are using Terraform for your deployments, you can also integrate Helm into your existing workflow. Terraform has a Helm provider that lets you deploy Helm charts with your Terraform workflow.

provider "helm" {
  kubernetes {
    config_path = "~/.kube/config"
  }
}

resource "helm_release" "hello-world" {
  name    = "hello-world"
  chart   = "oci://registry-1.docker.io/bitnamicharts/nginx"
  version = "15.4.2"
  set {
    name  = "replicaCount"
    value = "2"
  }
}

Sample of a Helm deployment in Terraform of the already known hello-world sample.

You can try that yourself with the GitHub sample and the command terraform init && terraform apply.

The Pain Points of Helm and Some Solutions

Even though Helm is a great tool and makes many things simpler, it also adds some new problems:

  • Go templating render problems Debugging can be challenging when developing new, complex Go templating rendering logic for a chart.

    helm template --debug <chart name> can provide assistance, the process may still not be straightforward at times.

  • Big upgrades of third party charts The challenge of upgrading resources isn’t necessarily reduced with Helm. Even with Helm, ensuring a smooth transition and safeguarding against data loss (especially if the chart contains data within the cluster, e.g., a database) can be a complex process.

  • Not fully declarative Integrating Helm into the CI pipeline introduces an additional step, as you must first execute a helm install followed by a helm upgrade for new charts.

  • Dependency problems Check out the previous section, where we delved deeper into this matter.

Useful Extensions

You can add some extensions and other tools to overcome some of the pain points mentioned before.

Helm Diff

The plugin Helm Diff can help to visualize the changes that are made between releases. This can be really helpful to view differences between parameter changes or upgrades.

For instance, this can be used before we upgrade the replicaCount, as shown in Make Changes to the Chart.

Instead of instantly doing an upgrade with:

helm upgrade hello-world oci://registry-1.docker.io/bitnamicharts/nginx \
  --set replicaCount=2

We can first do a diff:

helm diff upgrade hello-world oci://registry-1.docker.io/bitnamicharts/nginx \
    --set replicaCount=2

This will provide us with a diff of the resources that will be changed (excerpted output):

default, hello-world-nginx, Deployment (apps) has changed:
  # Source: nginx/templates/deployment.yaml
  apiVersion: apps/v1
  kind: Deployment
  metadata:
    name: hello-world-nginx
    namespace: "default"
    labels:
      app.kubernetes.io/instance: hello-world
      app.kubernetes.io/managed-by: Helm
      app.kubernetes.io/name: nginx
      app.kubernetes.io/version: 1.25.4
      helm.sh/chart: nginx-16.0.1
  spec:
-   replicas: 1
+   replicas: 2
    revisionHistoryLimit: 10

Helmfile

Helmfile can help to simplify multiple Helm deployments at once and make it more declarative.

However, delving into the specifics is beyond the scope of this article. You can watch the talk An introduction to Helm where I explain Helmfile.

Conclusion

Helm empowers developers to manage Kubernetes resources efficiently. By leveraging charts and the Helm CLI, you can streamline deployments, ensure consistency, and simplify your Kubernetes workflow.

Give it a try yourself!

I created a small Helm demo on GitHub. You can run the demo on your machine or in GitHub Codespaces. With GitHub Codespaces, no installation is needed, and you can try Helm and Kubernetes within your browser.

Lazzaretti/helm-demo - GitHub

Additional Resources


Special thanks to the reviewers, Dr. Annegret Junker and Hannah Li Hägi, for dedicating their time and effort to providing constructive and valuable feedback for this post.