For Kubernetes, as for any emerging technology, the need for an adequate tool for sharing, versioning, and maintaining the complexity of Kubernetes applications arises over time. On the market itself and in the Kubernetes ecosystem, Helm stood out the most as a solution to all these problems. Anyone who has ever tried to deploy an application that contains many Kubernetes resources for the first time knows how difficult it can be, especially if they are applications written by other teams. With the help of Helm, by simply specifying the configuration file, we get applications that work flawlessly. More about Kubernetes can be found in one of our previous blog posts here.
What is Helm?
HELM is an open-source package manager for Kubernetes, a powerful container orchestration platform for modern applications. We can consider charts as packages that can help us with deploying our applications. Of course, we can also write charts ourselves if we don't find suitable or official solutions that already exist.
The benefits of Helm include:
- Simplified deployment
- Automatic versioning
- Productivity boost
Currently, the Helm client is installed outside of the cluster itself. He connects to the cluster using kubectl and makes the necessary changes that are defined in the chart itself. In versions older than the Helm 3 version, we had the server side of Helm on a cluster called Tiller, but it was removed in the Helm 3 version due to the simplification of the architecture itself and the simpler reflection of Helm and the Kubernetes cluster.
Structure of Helm
The files and directories of a Helm chart each have a specific function:
YOUR-CHART-NAME/ ├── charts/ ├── templates/ ├── Chart.yaml └── values.yaml
charts: The charts directory contains other charts the main chart depends on. templates: This folder stores the manifest being deployed with the chart. For example, you may be deploying an application that needs a service, a config map, and secrets. In this case, the directory would contain a deployment.yaml, service.yaml, config.yaml, and a secrets.yaml. Each of these files would get its values from the values.yaml file. Chart.yaml: - This file holds meta information such as the name and version. values.yaml: Default configuration values for the chart.
Inside the templates folder, there is an additional file that is generated by default, _helpers.tpl. Helpers file is used for internal purposes and inside _helpers.tpl we are defining reusable functions and variables.
Deploying without Helm
In this section, we will show what it would look like to deploy two React applications with similar features without Helm, while in the next we will show what it looks like when we use Helm.
The first step is to create Kubernetes manifest files. We will go with a simple setup, every one of our applications will only have deployment.yaml and service.yaml. Let's call our applications blue app and green app. This will be the folder structure of our Kubernetes configuration.
blue-app/ ├── deployment.yaml └── service.yaml green-app/ ├── deployment.yaml └── service.yaml
Our blue app will run on port 80, it will have two environment variables:
- MODE = dev
- TIMEZONE = Europe/Belgrade
Also, we will set limits and requests for this service is 1GB. Our deployment.yaml file will look like this:
apiVersion: apps/v1 kind: Deployment metadata: name: blue-app labels: app: blue-app spec: replicas: 3 selector: matchLabels: app: blue-app template: metadata: labels: app: blue-app spec: containers: - name: blue-app image: your-registry/your-react-image:latest ports: - containerPort: 80 env: - name: MODE value: "dev" - name: TIMEZONE value: "Europe/Belgrade" resources: limits: cpu: "1" memory: "1Gi" requests: cpu: "1" memory: "1Gi"
Our service.yaml file will look like this:
apiVersion: v1 kind: Service metadata: name: blue-app-service spec: selector: app: blue-app ports: - protocol: TCP port: 80 targetPort: 80 type: ClusterIP
When we want to deploy our application, we will navigate to the blue-app folder and run the command:
kubectl apply -f .
Now lets deploy our green app. Our green app will run on port 81, it will not have environment variables. Also, we will set limits and requests for this service is 2GB. Our deployment.yaml file will look like this:
apiVersion: apps/v1 kind: Deployment metadata: name: green-app labels: app: green-app spec: replicas: 3 selector: matchLabels: app: green-app template: metadata: labels: app: green-app spec: containers: - name: green-app image: your-registry/your-react-image:latest ports: - containerPort: 81 resources: limits: cpu: "2" memory: "2Gi" requests: cpu: "2" memory: "2Gi"
Our service.yaml file will look like this:
apiVersion: v1 kind: Service metadata: name: green-app-service spec: selector: app: green-app ports: - protocol: TCP port: 80 targetPort: 81 type: ClusterIP
When we want to deploy our application, we will navigate to green-app folder and run command:
kubectl apply -f .
At this moment, we have deployed our blue app and green app in our Kubernetes cluster.
Deploying with Helm
Now, we will create our first Helm chart and we will try to deploy our blue and green app with Helm.
We need to create Helm chart with name react-app, and we will do that with command:
helm create react-app
After execution of this command, we will see that we have the base structure of our Helm chart. For the purpose of a simpler explanation, delete everything from the templates folder except _helpers.tpl file and delete everything from values.yaml file. By default, the Helm chart is creating things that we do not need for this demo. We will after that create deployment.yaml file inside the templates folder and it will look like this:
apiVersion: apps/v1 kind: Deployment metadata: name: {{ include "react-app.fullname" . }} labels: app: {{ include "react-app.name" . }} spec: replicas: {{ .Values.replicaCount }} selector: matchLabels: app: {{ include "react-app.name" . }} template: metadata: labels: app: {{ include "react-app.name" . }} spec: containers: - name: {{ .Chart.Name }} image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" ports: - containerPort: {{ .Values.containerPort }} env: {{ toYaml .Values.env | nindent 12 }} resources: {{ toYaml .Values.resources | nindent 12 }}
After that, we create service.yaml file inside the templates folder and it will look like this:
apiVersion: v1 kind: Service metadata: name: {{ include "react-app.fullname" . }} labels: app: {{ include "react-app.name" . }} spec: type: {{ .Values.service.type }} ports: - port: {{ .Values.service.port }} targetPort: {{ .Values.service.targetPort }} protocol: TCP selector: app: {{ include "react-app.name" . }}
In the template file, we can see some new things related to Helm, Helm template syntax. Everything which is defined inside double curly brackets block is part of template syntax and it will be interpreted by Helm. We can see that we are using the include keyword. Include is a function that allows bringing another template and passing the result to another template function. In this case, we are referencing react-app.fullname which is a variable defined in _helpers.tpl file. If there is no function in curly braces block and we are only referencing value with .Values.something or .Chart.something, we are just taking value from values.yaml file or Charts.yaml file.
We can also see toYaml function. This function takes the whole input that is specified and serves that as an output in yaml format. We combine that function with nindent because we need to indent that output to have valid yaml file structure.
In these two files, we only have a few template functions, of course, there are more complex functions and solutions, everything about templating functions we can find in Helm's official documentation. Finally, we need to have a default values.yaml file.
replicaCount: 1 containerPort: 80 env: [] resources: limits: cpu: 100m memory: 128Mi requests: cpu: 100m memory: 128Mi image: repository: nginx tag: latest service: type: ClusterIP port: 80 targetPort: 80
Now, when we configured our Helm chart, we will release our chart. For the simplicity of this demo, we will host our helm chart only on our local machine. Inside our Helm chart directory, we will run now:
helm package . helm repo index .
We will than serve our local repository with Python server:
python3 -m http.server
Now we will add our local repository to Helm.
helm repo add react-app http://localhost:8000
When our chart is added to Helm, we can deploy our applications with our Helm chart. We will need just two values files, one for blue app (blue.yaml) and one for green app (green.yaml).
#Blue app nameOverride: blue-app image: repository: your-registry/your-react-image tag: latest env: - name: MODE value: "dev" - name: TIMEZONE value: "Europe/Belgrade" resources: limits: cpu: "1" memory: "1Gi" requests: cpu: "1" memory: "1Gi"
#Green app nameOverride: green-app image: repository: your-registry/your-react-image tag: latest containerPort: 81 service: type: ClusterIP port: 81 targetPort: 81 resources: limits: cpu: "2" memory: "2Gi" requests: cpu: "2" memory: "2Gi"
When we create these files, we will deploy our applications with the following commands:
helm install -f blue.yaml blue-app react-app/react-app helm install -f green.yaml green-app react-app/react-app
You will see that applications are running in the same way as when we deployed them without Helm.
How Helm helped us?
Without Helm, our configuration was not centralized. The problem with that occurs when we have dozens of microservices, but it can also be when we have a smaller number of services with a more complicated Kubernetes infrastructure. If one day we decide that we have to change a certain type of Kubernetes resource and at the same time we have 20 services, each service would have to be changed individually, which would increase the amount of work as well as the probability of an error in the system itself.
Also, it is much more difficult to maintain and review the infrastructure when yaml Kubernetes manifests are used because we will have n files for each service, while with Helm we will have n files for the Helm chart and one value file for each service, which drastically reduces the number of files that need to be are maintained.
In addition to the mentioned things, there are also Helm's main features such as versioning and rollback, which give us many advantages because with Kubernetes manifests we do not have a simple option for disaster recovery.
Conclusion
To summarize, Helm is a tool that brings us many advantages in maintaining the Kubernetes architecture, and the time we need to learn it is small compared to the time we can potentially spend solving the problems that will arise if we do not use it. We hope this blog post has helped you understand what Helm is and why you should consider using it. See you in the next blog post with a new topic!
