The rise of Kubernetes-as-a-service services has significantly simplified the setup and maintenance of Kubernetes clusters, leading to an increasing number of enterprises migrating their applications to Kubernetes. However, migrating complex applications to Kubernetes takes a lot of work. Traditional deployment methods typically involve managing numerous YAML files, each representing different components of an application, such as services, deployments, and volumes. This approach can be error-prone, mainly because it may require separate manifest files for each environment. For instance, development environments usually have fewer replicas or even less powerful machines compared to production environments to be more cost-effective, which leads to duplication and increases maintenance complexity. Additionally, manually handling updates and rollbacks of applications might necessitate keeping track of the deployed version and can become challenging with updates targeting different environments. This is where Helm comes in to help. It simplifies the Kubernetes application definition, deployment, and update by packaging an application’s stack into a singular, manageable unit called Chart.
In this article, we’ll dig into a comprehensive exploration of Helm Charts. In this article, we’ll view the differences between private and community charts, see how to manage dependencies with other charts, see the Chart’s folder structure, and finally show their effectiveness with a practical example.
What are Helm Charts?
Developed by Deis in 2016 and later acquired by Microsoft, Helm is a package manager for Kubernetes. In Helm, the ‘packages’ are referred to as ‘charts’; each contains all the necessary files describing a set of Kubernetes resources, such as deployments, services, ingress, etc, related to a particular component or application stack. The usage of Helm charts offers several key advantages:
- Standardized Deployment Processes:Helm allows for deploying all application components in a declarative manner using a single command. This minimizes the risk of errors and significantly streamlines the deployment process.
- Simplified Management of Complexity:Helm charts abstract the complexity of configuring individual Kubernetes resources. They allow for customization through parameters without directly modifying resource files.
- Reusability:You can easily and quickly deploy the same application stack in various environments or share it across different organizations with minimal changes.
- Version Control and Rollbacks:Helm effectively tracks the versions of your deployments, enabling easy rollback to previous versions if needed.
Install Helm
Installing Helm is a straightforward process. You can install it via Package Manager, which will require the addition of the Helm repository to your system’s packages list.
$ curl https://baltocdn.com/helm/signing.asc | gpg --dearmor | sudo tee /usr/share/keyrings/helm.gpg > /dev/null
$ echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/helm.gpg] https://baltocdn.com/helm/stable/debian/ all main" | sudo tee /etc/apt/sources.list.d/helm-stable-debian.list
$ sudo apt-get update
$ sudo apt-get install helm
Or use the shell script designed by Helm’s development team, which will discover the machine’s architecture and operating system and install the latest Helm accordingly.
$ curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3
$ chmod 700 get_helm.sh
$./get_helm.sh
Anatomy of a Helm Chart
The Helm Chart has a pre-defined folder structure and files crucial for its proper functioning.
.
│.helmignore
│ LICENSE
│ Chart.yaml
│ values.yaml
│ values.schema.json
├───charts/
├───crds/
└───templates/
│ NOTES.txt
│ _helpers.tpl
└───tests/
Let’s describe each one of them one by one:
-
.helmignore
:(Optional) It tells Helm to ignore specific files and directories when packing the Chart. It works like a.gitignore file in Git. -
LICENSE
:(Optional) This file contains the license for the chart. -
Chart.yaml
:This file contains the name, description, and version of the Chart. -
values.yaml
:This file holds the default configuration values for the chart. These values can be overridden by user-supplied values when the chart is installed or upgraded. -
values.schema.yaml
:(Optional) If defined, this JSON file imposes a specific structure on the values.yaml file -
crds/
:(Optional) This directory contains the Custom Resource Definitions, which create the necessary custom resources before the rest of the components in the Helm chart are deployed. -
templates/
:This directory contains all the YAML files in charge of the application Kubernetes components (ingress, services, deployments,…). The templates may reference values from values.yaml that are replaced during the installation/Upgrade of the Chart. -
templates/tests/
:This directory contains the Kubernetes manifests of the resources in charge of testing the correctness of the Chart. -
templates/_helpers.tpl
:This file allows you to encapsulate complex logic or repetitive code in a single piece of code so that it’s easy to reuse throughout your Chart. -
templates/NOTES.txt
:This file’s content contains information about the Chart that is rendered in the command line output at the end of a Helm chart’s installation or upgrade process. -
charts/
:If defined in the Chart.yaml file, this folder will contain all the charts (known as subcharts) that this chart depends on. These charts are downloaded during the installation or upgrade of the chart.
Community and Custom Charts
One of the strengths of Helm is its vibrant community and the repository of charts created and maintained by this community. Public Helm Charts were originally stored in theHelm Hub,but in 2021, the platform was replaced byArtifactHub,A platform developed and maintained by the Cloud Native Computing Foundation (CNCF).
The platform UX is well done, and clearly describes how to install and uninstall the Charts and provides a security scan report of its vulnerabilities. Here, you can inspect the schema, default values, and templates to verify that they fit your needs.
Developing public Helm Charts allows developers outside the organization to improve them and sustain the community. However, there are scenarios where using public charts is not possible due to specific configurations, proprietary software, or complex requirements that need to be addressed by existing charts. In these cases, you can host the Helm chart in private Helm repositories running on Cloud services like Amazon S3, or use an artifact repository manager like JFrog Artifactory or GitHub Packages. Accessing the Helm chart will require authentication and only be accessible to those with the appropriate permissions.
Testing a Chart
Testing is a critical part of software development, and so it’s for the development of the Helm charts. Helm provides a dedicated mechanism for testing charts through thetemplates/tests
directory. Tests are processed via Kubernetes resources that perform specific operations to verify if the chart is working correctly. For example, a test for a web application might deploy a Pod that makes an HTTP request to the service created by your Helm chart, checking if it responds correctly.
apiVersion: batch/v1
kind: Job
metadata:
name: training-test
annotations:
"helm.sh/hook": test
spec:
template:
spec:
containers:
- name: curl
image: curlimages/curl
command:
- 'curl'
- '-s'
- 'http://{{ include "default.fullname". }}-service:{{.Values.service.port }}/'
restartPolicy: Never
backoffLimit: 1
Tests are triggered via the commandhelm test <CHART-NAME>
.
$ helm test training
NAME: training
LAST DEPLOYED: Thu Dec 21 21:00:35 2023
NAMESPACE: default
STATUS: deployed
REVISION: 1
TEST SUITE: training-test
Last Started: Thu Dec 21 21:13:53 2023
Last Completed: Thu Dec 21 21:13:58 2023
Phase: Succeeded
Helper file
Helpers are powerful elements that increase the flexibility and dynamism of the templates by encapsulating repetitive and complex template code. These helpers are defined in the_helpers.tpl
file and use thedefine
directive of the Go templating language to organize functions and logic in a more structured and reusable manner. For example:
{{- define "default.labels" -}}
helm.sh/chart: {{ include "default.chart". }}
{{ include "default.selectorLabels". }}
{{- if.Chart.AppVersion }}
app.kubernetes.io/version: {{.Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{.Release.Service }}
{{- end }}
This snippet defines a helper nameddefault.labels
that outputs several Kubernetes labels. As you can see it also accesses Built-in Objects, like.Chart.Name
and.Release.Name
,Release.Namespace
,and use an if condition to add a label according to a value. Once defined, these helpers can be used in the template with theinclude
directive. For example:
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
{{- include "default.labels". | nindent 4 }}
...
Demonstration
In this demonstration, I will create a Helm Chart and deploy a React.js application to an Azure Kubernetes Service (AKS) cluster. The application’s source code is hosted athttps://github.com/GTRekter/Training/application
and has the following architecture.
The original Kubernetes manifests used for deployment are as follows:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: training-ingress
spec:
ingressClassName: nginx
rules:
- http:
paths:
- path: "/"
pathType: Prefix
backend:
service:
name: training-service
port:
number: 80
---
apiVersion: v1
kind: Service
metadata:
name: training-service
spec:
selector:
app: training
spec:
ports:
- protocol: TCP
port: 80
targetPort: 3000
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: training-deployment
spec:
replicas: 2
selector:
matchLabels:
app: training
template:
metadata:
labels:
app: training
spec:
containers:
- name: training-container
image: "acrtrainingdev01.azurecr.io/training:1.0"
ports:
- containerPort: 3000
resources:
limits:
cpu: 100m
memory: 128Mi
requests:
cpu: 100m
memory: 128Mi
livenessProbe:
httpGet:
path: "/"
port: 3000
initialDelaySeconds: 30
periodSeconds: 30
env:
- name: REACT_APP_AUTH0_DOMAIN
valueFrom:
secretKeyRef:
name: training-secret
key: REACT_APP_AUTH0_DOMAIN
- name: REACT_APP_AUTH0_CLIENT_ID
valueFrom:
secretKeyRef:
name: training-secret
key: REACT_APP_AUTH0_CLIENT_ID
---
apiVersion: v1
kind: Secret
metadata:
name: training-secret
type: Opaque
data:
REACT_APP_AUTH0_DOMAIN: [base64-encoded-value]
REACT_APP_AUTH0_CLIENT_ID: [base64-encoded-value]
We will use these manifests as templates for the Helm Chart. They will be modified to use helpers and parameters defined in the Helm Chart to make the deployment more dynamic.
Build and Publish the Docker image
The first step involves creating and publishing a container with your application. Start by logging into the ACR instance using the following command in the Azure CLI:
$ docker login acrtrainingdev01.azurecr.io
You can find the credentials in the Access keys section of your Azure Container Registry after enabling the Admin user.
Next, build your image while maintaining the naming convention of the repository in the tag:
$ docker build -t acrtrainingdev01.azurecr.io/training:1.0.
After the build, push the image to the Azure Container Registry to make it available for Kubernetes deployments:
$ docker push acrtrainingdev01.azurecr.io/training:1.0
Create the Helm chart
Next, we will use the following command in the Helm CLI to create a new chart:
$ helm create training
This command generates a directory namedtraining
,along with the common directories and files typically used in a chart:
.
│.helmignore
│ Chart.yaml
│ values.yaml
├───charts
└───templates
│ deployment.yaml
│ hpa.yaml
│ ingress.yaml
│ NOTES.txt
│ service.yaml
│ serviceaccount.yaml
│ _helpers.tpl
└───tests
test-connection.yaml
Since we are going to use our existing manifests as starting points, replace all the files from thetemplates
directory with the manifests listed above.
NGINX Chart Dependency
To demonstrate how Helm manages dependencies, we will use the NGINX Ingress implementation for the Ingress controller. To do so, we must define the dependency in theChart.yaml
.
dependencies:
- name: nginx-ingress
version: "1.1.0"
repository: "https://helm.nginx.com/stable"
Then, to update and download the dependencies, execute the following command:
$ helm dependency update
After this, the package containing the necessary templates,Chart.yaml
file, CRDs, and more will be downloaded into thecharts
directory.
.
│.helmignore
│ Chart.lock
│ Chart.yaml
│ values.yaml
├───charts/
│ nginx-ingress-1.1.0.tgz
└───templates/
Using Helpers and Values
Helm charts utilize values and helpers to convert static manifests into dynamic templates. Specific placeholders, such as{{ include... }}
for parameters and{{.Values... }}
for values defined in thevalues.yaml
file, are replaced with corresponding values during chart installation and upgrade. For example, in our Ingress resource, we can dynamically generate resource names using helpers, eliminating the need for manual adjustments with each deployment:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: training-ingress
spec:
ingressClassName: nginx
rules:
- http:
paths:
- path: "/"
pathType: Prefix
backend:
service:
name: training-service
port:
number: 80
Let’s start by creating a helper for resource naming. In this tutorial, we will create a helper nameddefault.name
,which by default returns the Chart's name or the Release's name if specified. To simplify the function, let’s create the variable$name
and assign it the value of the predefined Helm value.Chart.Name
.
{{- define "default.name" -}}
{{- $name:= default.Chart.Name.Release.Name }}
{{- if contains $name.Release.Name }}
{{-.Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s".Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
Next, we will create helpers for metadata, adding various metadata elements related to the chart's version and the service. For convenience, we will also define separate helpers for selector labels:
{{- define "default.selectorLabels" -}}
app.kubernetes.io/name: {{ include "default.name". }}
app.kubernetes.io/instance: {{.Release.Name }}
{{- end }}
{{- define "default.labels" -}}
app.kubernetes.io/managed-by: {{.Release.Service }}
{{ include "default.selectorLabels". }}
{{- if.Chart.AppVersion }}
app.kubernetes.io/version: {{.Chart.AppVersion | quote }}
{{- end }}
{{- end }}
Then we update the Ingress to incorporate these helpers:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ include "default.name". }}-ingress
labels:
{{- include "default.labels". | nindent 4 }}
spec:
rules:
- http:
paths:
- path: "/"
pathType: Prefix
backend:
service:
name: training-service
port:
number: 80
Now, let’s examine thevalues.yaml
file, which will hold user-supplied values influencing resource behavior. In this case, we are going to create nested values to group the values related to the ingress.
ingress:
className: nginx
path: "/"
pathType: Prefix
We then apply them in the template. For the NGINX Ingress, I added thehost
field, as it is a required value for this type of Ingress configuration.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ include "default.fullname". }}-ingress
labels:
{{- include "default.labels". | nindent 4 }}
spec:
{{- if.Values.ingress.className }}
ingressClassName: {{.Values.ingress.className }}
{{- end }}
rules:
- host: {{.Values.ingress.host }}
http:
paths:
- path: {{.Values.ingress.path }}
pathType: {{.Values.ingress.pathType }}
backend:
service:
name: {{ include "default.fullname". }}-service
port:
number: {{.Values.service.port }}
Let’s repeat the process for all the files. The finalvalues.yaml
file will look like the following:
# Helpers
nameOverride: sample
fullnameOverride: training-sample
# Service configuration
service:
port: 80
targetPort: 3000
# Ingress configuration
ingress:
path: "/"
className: "nginx"
host: "ivanporta.info"
pathType: Prefix
# Secret configuration
auth0:
clientId: ""
domain: ""
# Deployment configuration
deployment:
replicaCount: 2
image:
repository: "acrtrainingdev01.azurecr.io"
name: "training"
tag: "2.0"
containerPort: 3000
resources:
limits:
cpu: 100m
memory: 128Mi
requests:
cpu: 100m
memory: 128Mi
livenessProbe:
path: "/"
port: 3000
initialDelaySeconds: 30
periodSeconds: 30
The final templates will looks like the following:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ include "default.name". }}-ingress
labels:
{{- include "default.labels". | nindent 4 }}
spec:
ingressClassName: nginx
rules:
- http:
paths:
- path: {{.Values.ingress.path }}
pathType: {{.Values.ingress.pathType }}
backend:
service:
name: {{ include "default.name". }}-service
port:
number: {{.Values.service.port }}
---
apiVersion: v1
kind: Service
metadata:
name: {{ include "default.name". }}-service
labels:
{{- include "default.labels". | nindent 4 }}
spec:
selector:
{{- include "default.selectorLabels". | nindent 4 }}
ports:
- protocol: TCP
port: {{.Values.service.port }}
targetPort: {{.Values.service.targetPort }}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "default.name". }}-deployment
labels:
{{- include "default.labels". | nindent 4 }}
spec:
replicas: {{.Values.deployment.replicaCount }}
selector:
matchLabels:
{{- include "default.selectorLabels". | nindent 6 }}
template:
metadata:
labels:
{{- include "default.selectorLabels". | nindent 8 }}
spec:
containers:
- name: {{ include "default.name". }}
image: "{{.Values.deployment.image.repository }}/{{.Values.deployment.image.name }}:{{.Values.deployment.image.tag }}"
ports:
- containerPort: {{.Values.deployment.image.containerPort }}
resources:
limits:
cpu: {{.Values.deployment.image.resources.limits.cpu }}
memory: {{.Values.deployment.image.resources.limits.memory }}
requests:
cpu: {{.Values.deployment.image.resources.requests.cpu }}
memory: {{.Values.deployment.image.resources.requests.memory }}
livenessProbe:
httpGet:
path: {{.Values.deployment.livenessProbe.path }}
port: {{.Values.deployment.livenessProbe.port }}
initialDelaySeconds: {{.Values.deployment.livenessProbe.initialDelaySeconds }}
periodSeconds: {{.Values.deployment.livenessProbe.periodSeconds }}
env:
- name: REACT_APP_AUTH0_DOMAIN
valueFrom:
secretKeyRef:
name: {{ include "default.name". }}-secret
key: REACT_APP_AUTH0_DOMAIN
- name: REACT_APP_AUTH0_CLIENT_ID
valueFrom:
secretKeyRef:
name: {{ include "default.name". }}-secret
key: REACT_APP_AUTH0_CLIENT_ID
---
apiVersion: v1
kind: Secret
metadata:
name: {{ include "default.name". }}-secret
type: Opaque
data:
REACT_APP_AUTH0_DOMAIN: {{.Values.auth0.domain | b64enc | quote }}
REACT_APP_AUTH0_CLIENT_ID: {{.Values.auth0.clientId | b64enc | quote }}
Installation of the resources defined in the Helm chart
Once the Helm chart is ready, the next step is to deploy it to the target Kubernetes cluster. The first step involves gathering the credentials to interact with the Kubernetes API and deploy the resources. In this demonstration, we will use Azure Kubernetes Service (AKS) so the commands are as follows:
$ az account set --subscription xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
$ az aks get-credentials --resource-group rg-training-dev --name aks-training-01
With the credentials set, you can proceed to install the Chart using the Helm CLI command:
$ helm install training./kubernetes/training
Here,./kubernetes/training
denotes the relative path to the directory containing the Helm Chart. After the installation, you can verify that the deployment went smoothly by checking its status:
$ helm status training
NAME: training
LAST DEPLOYED: Thu Dec 21 21:00:35 2023
NAMESPACE: default
STATUS: deployed
REVISION: 1
TEST SUITE: training-test
Last Started: Thu Dec 21 21:13:53 2023
Last Completed: Thu Dec 21 21:13:58 2023
Phase: Succeeded
Andkubectl
to check the resources in your Kubernetes cluster:
$ kubectl get all
NAME READY STATUS RESTARTS AGE
pod/training-nginx-ingress-controller-67967b6574-tfcvs 1/1 Running 0 3h56m
pod/training-sample-deployment-55486f6456-zkmxh 1/1 Running 0 3h38m
pod/training-sample-deployment-55486f6456-zq794 1/1 Running 0 3h39m
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/kubernetes ClusterIP 10.0.0.1 <none> 443/TCP 12h
service/training-nginx-ingress-controller LoadBalancer 10.0.107.101 20.8.26.146 80:30294/TCP,443:30560/TCP 3h53m
service/training-sample-service ClusterIP 10.0.25.87 <none> 80/TCP 3h56m
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/training-nginx-ingress-controller 1/1 1 1 3h56m
deployment.apps/training-sample-deployment 2/2 2 2 3h56m
NAME DESIRED CURRENT READY AGE
replicaset.apps/training-nginx-ingress-controller-67967b6574 1 1 1 3h56m
replicaset.apps/training-sample-deployment-55486f6456 2 2 2 3h39m
replicaset.apps/training-sample-deployment-6d58889f5c 0 0 0 3h46m
replicaset.apps/training-sample-deployment-b558cd9db 0 0 0 3h56m
We can now access the application by using its domain name.
Updating the Chart
Updating a Helm chart, including tasks like modifying templates, adding new resources, or adjusting configuration values (such as environment variables or replica scaling), can typically be done using a single command. This section demonstrates how to update an existing Helm Chart by adding a Horizontal Pod Autoscaler (HPA).
First create ahpa.yaml
file in the templates directory with the following content:
apiVersion: autoscaling/v1
kind: HorizontalPodAutoscaler
metadata:
name: {{ include "default.fullname". }}-hpa
spec:
maxReplicas: {{.Values.hpa.maxReplicas }}
minReplicas: {{.Values.hpa.minReplicas }}
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: {{ include "default.fullname". }}-deployment
targetCPUUtilizationPercentage: {{.Values.hpa.targetCPUUtilizationPercentage }}
Next, add the related values to thevalues.yaml
file:
hpa:
maxReplicas: 10
minReplicas: 1
targetCPUUtilizationPercentage: 50
Finally, update and redeploy the Helm chart using the command:
$ helm upgrade training./kubernetes/training
Release "training" has been upgraded. Happy Helming!
NAME: training
LAST DEPLOYED: Tue Dec 26 10:15:26 2023
NAMESPACE: default
STATUS: deployed
REVISION: 2
To verify, inspect the Kubernetes resources, and you should see the new Horizontal Autoscaler listed:
$ kubectl get hpa
NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE
training-sample-hpa Deployment/training-sample-deployment 1%/50% 1 10 2 77s
Resources
- Helm Built-in objects:https://helm.sh/docs/chart_template_guide/builtin_objects/
- Helm Official Documentation:https://helm.sh/docs/