Spring Boot is a major Framework for Java app development, and Kubernetes is one of the de facto standards for app deployment in cloud environments. However, deploying apps into a Kubernetes cluster can become tedious, with repetitive configurations in various Kubernetes descriptors. Even worse if multiple environments such as development, test, and production are used. Which hopefully everyone does.
Helm addresses these issues with specialized templating, packaging, versioning, and infrastructure for sharing and reusing them. This article briefly introduces Helm and how to utilize it for Spring Boot applications.
Why do you need Helm?
Deploying Spring Boot applications on Kubernetes requires creating a set of manifest files. In Kubernetes, a manifest file is a yaml file that defines the configuration of an application or service you want to deploy. These manifest files can contain configuration details, such as the used container image, the number of replicas to run, and the resources to allocate to a container. Manifest files can quickly become large and unwieldy as you add more and more resources and configurations. For example, suppose you have a manifest file that defines a Deployment resource for an application. You might have another manifest file that defines a service resource for the same application. Both manifest files will most likely contain similar configurations, such as labels and resource annotations, and also will use a similar name schema. The maintenance of such files can be tedious and prone to errors, mainly when the need arises to run multiple environments. In this case, separate manifest files must be created and maintained for each environment, adding a layer of complexity.
Helm addresses these issues by allowing you to create templates for your manifest files. In addition, Helm will enable you to package these templates, versioning and uploading them for simple reuse by others.
How to use Helm
What is a Helm chart, and how is it organized?
A Helm chart is a package that contains all the resource definitions necessary to run an application on a Kubernetes cluster. It is a collection of templates, Kubernetes manifests, and other resources that describe the desired state of an application. A chart is organized as a directory containing several files and subdirectories. The main files and folders are:
- Chart.yaml: This file contains metadata about the chart, such as the chart's name, version, and description.
- values.yaml: This file contains default values for the chart's configuration options.
- templates: This folder contains the templates defining the Kubernetes resources that comprise the application. The templates use the Go template language, enabling you to use variables and control structures to generate the final manifest files.
For example, a chart might have the following file structure:
How does the templating mechanism in Helm work?
The templating mechanism in Helm uses Go's text/template library to process template files and generate Kubernetes manifests. The templates define the application's resources, such as Deployments, Services, and Ingresses, while using placeholders to represent variables set at runtime. Helm allows defining variables and use them in the templates with the following syntax:`.Values.variableName`. The `.` refers to the root of the Helm chart, `Values` is the top-level key that contains all the user-defined values, and `variableName` is the name of the variable to define. When you render the chart, Helm replaces the variable `variableName` with its corresponding value in the yaml output. The variable values can come from various sources, such as the default values.yaml file, user-supplied values passed to the Helm CLI as an additional values.yaml or CLI parameters.
For example, consider the following template file for a Deployment:
In this example, the placeholders`.Values.replicaCount`, `.Values.appName` and `.Values.image.repository : .Values.image.tag` are replaced by actual values at runtime. In the simplest case, these values are provided directly via a values.yaml, which might look like this:
When you run the `helm install` or `helm template` command, Helm processes the templates fills the variables with actual values. The generated manifests are sent to the Kubernetes API server with the `helm install` command and displayed in the standard output with the `helm template` command. For example, the manifest generated by the above template will look like this:
How can Helm transform values?
The built-in functions of Helm process and manipulate template variables before rendering them into Kubernetes manifests. Functions are simple, single-purpose operations that perform a specific task, such as formatting a string, converting a value to a different type, or performing a mathematical operation. For example, the `quote` function encloses a string in double quotes. You can use it as follows:
You can use the pipeline syntax to chain template expressions, which allows for expressing a series of transformations on a variable. A pipeline takes the output of a preceding template expression and passes it to the following function. The following example shows pipelines that pass the appName value through multiple functions to manipulate the value. The `split` function separates the appName string into an array of substrings using the `-` character as a delimiter, and the index function selects the second element of that array:
In addition to the built-in functions, you can create custom functions for your templates. In your chart, you can define these functions inline in a template or a .tpl file. For example, it is common practice to define custom functions in a `_helper.tpl` file within the templates folder of a Helm chart because it allows you to organize your custom functions in a single location and reuse them across multiple templates in your chart. Here's an example of a custom function that capitalizes a given string and encloses it in double quotes:
If the above example is defined in `templates/helper.tpl`, it can be used like this:
How can Helm execute templates conditionally?
You can use if statements in Helm to conditionally execute certain parts of a template based on the value of a variable. They allow enabling or disabling a feature or conditionally creating resources. They enable you to design more flexible templates which adapt to different environments. The basic syntax of an if statement in Helm is as follows:
The minus signs `-` remove leading and trailing white spaces in the output.
What are the steps to create a Helm chart for a Spring Boot application?
Several prerequisites must be met to package an application into a Helm chart. The application you want to package into the chart needs to be containerized. For the following example, you can use the publicly available image public.ecr.aws/viadee/k8s-demo-app. Also, you'll need access to a Kubernetes cluster where the chart can be deployed, and the Helm CLI must be installed on your local machine. Once the necessary prerequisites have been met, you can create your custom Helm chart. The process involves the following steps:
- Create the chart scaffold
- Edit the chart metadata
- Edit the default chart values
- Edit the templates
- Package the chart and push it to a chart repository
1. Create the chart scaffold
Starting a new Helm chart can be done in several ways, but one of the most efficient methods is to use the `helm create` command. This command generates a basic file structure and sample files for a new chart, eliminating the need to set it up manually. The command offers several advantages, including Helm's best practices for chart development, consistency across different charts, and fewer manual errors. It is also a solid starting point for understanding how each part of the chart works. The `helm create` command expects the chart name as an argument:
The created file structure looks as follows:
2. Edit the charts metadata
Although not necessary, it can be helpful to edit the generated Chart.yaml. It contains the chart metadata, including its name, version, and description. The `helm create` command sets the name field, so it is unlikely that you need to change it in the Chart.yaml, but it is possible if necessary. The name identifies the chart when you upload it into a chart repository. It is also used to name the resources in the template folder. The description field contains a brief description of the chart. The version field should follow the semantic versioning standard, for example, "1.10.3". The version allows for specifying the chart version in a chart repository. Finally, the appVersion field defines the version of the application that the chart deploys. This field is referenced in the default templates to set the image version of the Deployment Kubernetes resource.
Here is an example of a Chart.yaml file:
3. Edit the default chart value
The first value to change is the image.repository in the values.yaml file. It specifies the container image used for the Deployment and should be updated to the desired image. To follow this example you can set the image.repository value to public.ecr.aws/viadee/k8s-demo-app.
Because the specified appVersion is also a valid image tag, you do not need to set the image.tag value. The generated template/deployment.yaml file uses the expressions `.Values.image.repository : .Values.image.tag | default .Chart.AppVersion` to specify the container image for the Deployment. The line consists of two expressions separated by a colon `:`. The expression `.Values.image.repository` retrieves the value of the repository field from the image section in the values.yaml file. The expression `.Values.image.tag | default .Chart.AppVersion` returns the appVersion value if the value image.tag is not defined or empty.
In addition to the image.repository value, you also want to change the service.port value in the values.yaml file. In a generated chart this value defines the port the Service will expose for the Deployment. It also controls where Kubernetes will expect liveness and readiness probes. By default, the service.port is 80, whereas the default port of a Spring Boot application is 8080. If the ports dont map the Pod will get stuck in a CrashLoopBackOff.
These are all necessary changes to run your own Spring Boot container image. Beyond that, the chart supports features such as adjusting the resource limits, adding image pull secrets, referencing an existing Service account, and enabling an Ingress. You can use the `helm template` command to quickly test changes to your Helm chart before installing it. The command shows if your chart has syntax errors. If the syntax is correct, you can check whether the created Kubernetes manifest yaml meets your expectations.
The template command takes two parameters: the release's name and the path to the chart directory. In our example, `my-release` is the name of the generated release, and `.` sets the current directory as the chart directory. A Helm release is a chart created by installing the chart to a Kubernetes cluster using `helm install` or `helm upgrade`. A release has a unique name associated with a specific set of configuration values. In this example, the `helm template` command generates the yaml manifest for a release named `my-release`. It uses the configuration values specified in the values.yaml file.
Here is an excerpt from the generated yaml manifest:
You can use the `helm install` command with the same two parameters to deploy the chart to a Kubernetes cluster. The command creates the resources in the cluster and a new version of the Helm release. Helm keeps track of the installed release version and allows rollback to an older version.
4. Edit the templates
At this point, our generated Deployment yaml contains a liveness and readiness probe pointing to the base path of the Spring Boot application. A better approach would be to use the dedicated actuator endpoints. From Spring Boot version 2.3, the LivenessStateHealthIndicator and ReadinessStateHealthIndicator classes have been added to display the application's actual liveness and readiness state. Spring Boot will automatically register these health indicators when deployed to Kubernetes. The /actuator/health/liveness and /actuator/health/readiness endpoints serve as liveness and readiness probes.
Now that the Chart allows you to start your application in Kubernetes, the next step is to add the ability to configure your application. Spring Boot applications manage configuration in application.properties or application.yaml files. These files contain key-value pairs defining various properties like the database connection URL, the server port, and other settings. Typically, the configuration needs to change based on the environment in which the application is running. For example, the database connection URL may differ for development, staging, and production environments. When deploying an application across multiple environments, it is desirable to use the same build while still having the capability to switch between different configuration sets. Without this approach, it would require rebuilding the application code each time a configuration change is needed. Luckily, Spring Boot has a feature called "externalized configuration" that supports setting application properties via environment variables. that supports setting application properties via environment variables. By externalizing the configuration, you allow using the same artifact for different environments while still being able to switch between different sets of configuration values.
In addition, the external configuration allows for configuring the Spring Boot application with Kubernetes native mechanisms. In Kubernetes, you can use either ConfigMaps or Secrets to store your configuration. ConfigMaps store pairs of data that are not sensitive such as application configurations, feature flags, and database connection strings. Secrets are used to store sensitive data, such as passwords and tokens. Pods can access the stored configuration in ConfigMaps or Secrets by loading it as environment variables. To integrate this mechanism in your helm chart, add a ConfigMap template and extend the existing Deployment template to load the ConfigMap as environment variables.
First, create a new file for a ConfigMap template in the ./templates directory.
Next, you replace the name of the ConfigMap with the same expression the other resources used to set their name:
Now you need to load the ConfigMap as environment variables in the Deployment. This can be achieved by adding an `envFrom` property to the container definition of the deployment.yaml.
To be able to control the configuration via the chart, a new value must be added to values.yaml and this must be referenced in the ConfigMap template. For this example, add the properties welcome.text and nav.bgcolor, which change the visible behavior of the public.ecr.aws/viadee/k8s-demo-app. But there is a problem: Operating systems typically enforce strict naming conventions for environment variables. For instance, Linux shell variables can only contain letters (uppercase or lowercase), numbers (0-9), or underscores. Unfortunately, these contradict the naming system of spring. Luckily, Spring Boot's flexible property binding considers the naming restrictions as much as possible. But to be safe, you may want to rename the properties according to the following rules.
- Replace dots with underscores.
- Remove dashes.
- Convert to uppercase.
You can use the expression `- toYaml .Values.properties | nindent 4` to integrate the yaml block in the configmap. The `toYaml` function is a template function provided by Helm to convert an object to a yaml string. The nindent function indents the output by a specified number of spaces. The dash `-` symbol before the toYaml function is a shorthand for "no whitespace," meaning it eliminates any leading whitespaces in the output. It is not sufficient to use the expression `.Values.properties` to access the value because it returns a complex datatype that helm renders to yaml as `map[NAV_BGCOLOR:lightblue WELCOME_TEXT:Hello world.]`.
At this point, the Spring Boot application would not notice if you change the content of the properties value because the ConfigMap is injected as environment variables and is only reloaded if the pod restarts. To force a pod restart, the Deployment spec itself needs to change. You can achieve this by adding an annotation to the pod template spec that changes when the config changes. Then, Kubernetes will notify the changes and restart.
The expression `.Values.properties | quote | sha256sum` takes the properties value, quotes it, and calculates its SHA-256 hash. The result of the expression is the unique identifier for the properties field, which is used for detecting changes to the configuration.
5. Package the chart and add it to a repository
. The chart repository maintained by the community can be found at Artifact Hub, but you can also run your own chart repository. Some other popular chart repositories include:
Chart Museum: A simple open-source chart repository that can be deployed on-premise or in the cloud.
Harbor: An open-source registry with advanced features such as role-based access control and image signing.
GitHub Pages: This method requires creating a repository with a
gh-pagesbranch and storing charts in that branch.
To add the chart to a repository, you first need to package the chart. The
helm package command will create a .tgz file that contains all the necessary files and metadata for the chart. Once you have packaged the chart, you can push it to the chart repository using various methods. Some popular chart repositories, such as Harbor, allow you to upload charts using a web interface, while others, such as Chart Museum, require you to use a CLI tool. An example of how to use GitHub pages to host a repository can be found here: https://github.com/viadee/spring-boot-helm-chart/blob/main/.github/workflows/publish.yml. To install a Helm chart from a chart repository, you can use the helm install command, but instead of referencing the local chart, you point to a repository.
Helm is a valuable tool for managing applications in a Kubernetes cluster. If you are working in an environment where you need to deploy and manage multiple applications and services, Helm can help streamline this process and reduce the time and effort required. However, while Helm can simplify the deployment process, it can also add complexity to your environment by introducing another abstraction layer. If your application has simple deployment requirements, using Helm may not be necessary. Ultimately, choosing Helm for applications requires considering your use case and environment. Even for small applications, Helm can make your deployment process more accessible. However, it will add complexity and if you have a lot of automation in place already, it may be better to deploy your applications directly to your cluster without using Helm or use a leaner solution like kustomize.
Back to blog overview