Create and Release Your Own Helm Chart
In this article, you will learn how to create your Helm chart and release it in the public repository. We will prepare a Helm chart for the typical Spring Boot REST-based app as an exercise. Our goal is to have a fully automated process to build, test, and release it. In order to do that, we will define a pipeline in CircleCI. This CI/CD pipeline will publish the Helm chart in the public Artifact Hub.
If you are interested in the Helm chart and CI/CD process you may refer to the following article. It shows how to design your continuous development process on Kubernetes and use Helm charts in the GitOps approach.
Source Code
If you would like to try it by yourself, you may always take a look at my source code. In order to do that you need to clone my GitHub repository. Then you should just follow my instructions.
Create Helm Chart
In this part of the exercise, we will use the helm
CLI. Helm installed locally is not required in our whole process, but helps you understand what will happen in the next steps. Therefore, it is worth installing it. Please refer to the official Helm docs to find an installation approach suitable for your needs.
In the first step, we are going to create a sample chart. It is a typical chart for web apps. For example, it exposes the 8080 port outside of the containers or allows us to define liveness and readiness probes checking HTTP endpoints. This Helm chart should not be too complicated, but also not too simple, since we want to create automated tests for it.
Here’s our Deployment
template. It adds some standard labels to the Deployment manifest (1). It also sets resource requests and limits (2). As I mentioned before, our chart is adding liveness probe (3), readiness probe (4), and exposes port 8080 outside of the container (5). We may also set environment variables (6), or inject them from ConfigMap
(7) and Secret
(8).
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .Values.app.name }}
labels: # (1)
app: {{ .Values.app.name }}
env: {{ .Values.app.environment }}
owner: {{ .Values.app.owner }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
app: {{ .Values.app.name }}
template:
metadata:
labels:
app: {{ .Values.app.name }}
env: {{ .Values.app.environment }}
spec:
containers:
- name: {{ .Values.app.name }}
image: {{ .Values.image.repository }}:{{ .Values.image.tag }}
resources: # (2)
{{- toYaml .Values.resources | nindent 12 }}
livenessProbe: # (3)
initialDelaySeconds: {{ .Values.liveness.initialDelaySeconds }}
httpGet:
port: {{ .Values.liveness.port }}
path: {{ .Values.liveness.path }}
failureThreshold: {{ .Values.liveness.failureThreshold }}
successThreshold: {{ .Values.liveness.successThreshold }}
timeoutSeconds: {{ .Values.liveness.timeoutSeconds }}
periodSeconds: {{ .Values.liveness.periodSeconds }}
readinessProbe: # (4)
initialDelaySeconds: {{ .Values.readiness.initialDelaySeconds }}
httpGet:
port: {{ .Values.readiness.port }}
path: {{ .Values.readiness.path }}
failureThreshold: {{ .Values.readiness.failureThreshold }}
successThreshold: {{ .Values.readiness.successThreshold }}
timeoutSeconds: {{ .Values.readiness.timeoutSeconds }}
periodSeconds: {{ .Values.readiness.periodSeconds }}
ports: # (5)
{{- range .Values.ports }}
- containerPort: {{ .value }}
name: {{ .name }}
{{- end }}
{{- if .Values.envs }}
env: # (6)
{{- range .Values.envs }}
- name: {{ .name }}
value: {{ .value }}
{{- end }}
{{- end }}
{{- if or .Values.extraEnvVarsConfigMap .Values.extraEnvVarsSecret }}
envFrom:
{{- if or .Values.extraEnvVarsConfigMap }}
- configMapRef:
name: {{ .Values.extraEnvVarsConfigMap }}
{{- end }}
{{- if or .Values.extraEnvVarsSecret }}
- secretRef:
name: {{ .Values.extraEnvVarsSecret }}
{{- end }}
{{- end }}
securityContext:
runAsNonRoot: true
We also have a template for the Service
object.
apiVersion: v1
kind: Service
metadata:
name: {{ .Values.app.name }}
labels:
app: {{ .Values.app.name }}
env: {{ .Values.app.environment }}
owner: {{ .Values.app.owner }}
spec:
type: {{ .Values.service.type }}
selector:
app: {{ .Values.app.name }}
ports:
{{- range .Values.ports }}
- port: {{ .value }}
name: {{ .name }}
{{- end }}
Now, we can fill our templates with the default values. The following values.yaml
file is available in the sample repository.
replicaCount: 1
app:
name: sample-spring-boot-api
environment: dev
owner: default
image:
repository: piomin/sample-spring-kotlin-microservice
tag: "1.1"
nameOverride: ""
fullnameOverride: ""
service:
type: ClusterIP
ports:
- name: http
value: 8080
resources:
limits:
cpu: 1000m
memory: 512Mi
requests:
cpu: 100m
memory: 256Mi
liveness:
initialDelaySeconds: 10
port: http
path: /actuator/health/liveness
failureThreshold: 3
successThreshold: 1
timeoutSeconds: 3
periodSeconds: 5
readiness:
initialDelaySeconds: 10
port: http
path: /actuator/health/readiness
failureThreshold: 3
successThreshold: 1
timeoutSeconds: 3
periodSeconds: 5
envs:
- name: INFO
value: Spring Boot REST API
You can easily test the newly created templates with the helm
CLI. In order to do that, just execute the following command in the repository root directory. As a result, you will see the YAML manifests created from our sample templates.
$ helm template charts/spring-boot-api-app
Such a testing method is fine… but just to run it locally during chart development. Assuming we need to create a delivery pipeline, we need a more advanced tool.
Unit Testing of Helm Charts
From my perspective, the most important thing in the CI/CD pipeline is automated testing. Without it, we are releasing unverified software, which may potentially result in many complications. The single Helm chart can be by several apps, so we should make every effort to carefully test it. Fortunately, there are some tools dedicated to Helm chart testing. My choice fell on the helm-unittest. It allows us to write unit tests file in pure YAML. We can install it as a Helm plugin or run it inside the Docker container. Let’s just it locally to verify our test work before pushing it to the Git repository:
$ helm plugin install https://github.com/helm-unittest/helm-unittest
We should place the unit test inside the test
directory in our chart. Here’s the structure of our chart repository:
In the first step, we are creating the unit test file. As mentioned before, we can create a test using the YAML notation. It is pretty intuitive. We need to pass a location of the values file (1) and a location of the tested Helm template (2). In the test section, we have to define a list of asserts (3). I will not get into the details of the helm-unittest
tool – for more information please refer to its docs. The important thing is that I can easily test each path of the YAML manifest. It can be an exact comparison or regex. It also supports JsonPath for mappings and arrays. Here’s our test in the deployment_test.yaml
:
suite: test deployment
values:
- ./values/test.yaml # (1)
templates:
- templates/deployment.yaml # (2)
chart:
version: 0.3.4+test
appVersion: 1.0.0
tests:
- it: should pass all kinds of assertion
template: templates/deployment.yaml
documentIndex: 0
asserts: # (3)
- equal:
path: spec.template.spec.containers[?(@.name == "sample-spring-boot-api")].image
value: piomin/sample-spring-kotlin-microservice:1.1
- equal:
path: metadata.labels.app
value: sample-spring-boot-api
- equal:
path: metadata.labels.env
value: dev
- equal:
path: metadata.labels.owner
value: default
- matchRegex:
path: metadata.name
pattern: ^.*-api$
- contains:
path: spec.template.spec.containers[?(@.name == "sample-spring-boot-api")].ports
content:
containerPort: 8080
name: http
- notContains:
path: spec.template.spec.containers[?(@.name == "sample-spring-boot-api")].ports
content:
containerPort: 80
- isNotEmpty:
path: spec.template.spec.containers[?(@.name == "sample-spring-boot-api")].livenessProbe
- isNotEmpty:
path: spec.template.spec.containers[?(@.name == "sample-spring-boot-api")].readinessProbe
- isKind:
of: Deployment
- isAPIVersion:
of: apps/v1
Now, we can verify the test locally by executing the following command from the project root directory:
$ helm unittest charts/*
Currently, we have only a single chart in the charts
directory. Assuming we would have more, it runs the tests for all charts. Here’s our result:
We can change something in the test to break it. Now, the result of the same will look like that:
Helm Chart Release Pipeline in CircleCI
Once we have a chart and tests created we may proceed to the delivery pipeline. In our CircleCI pipeline, we have to do not only the same steps as before, but also need to include a release part. First of all, we will use GitHub Releases and GitHub Pages to release and host our charts. In order to simplify the process, we may use a dedicated tool for releasing Helm charts – Chart Releaser.
We also need to create a personal token to pass to the Helm Chart Release workflow. Visit Settings > Developer Settings > Personal Access Token
. Generate Personal the token with the repo
scope. Then, we should put this token into the CircleCI context. You can choose any name for the context, but the name of the environment variable has to be CR_TOKEN
. That name is required by the Chart Releaser. The name of my context is GitHub
.
Here’s the list of steps we need to do in our pipeline:
- Install the
helm
CLI on the machine (we will use thecimg/base
image as the tests executor) - Install the Helm
unit-test
plugin - Run unit tests
- Only if we make a change in the
master
branch, then we proceed to the release part. In the first step, we need to package the chart withhelm package
command - Install Chart Releaser
- Release the chart in GitHub with the Chart Releaser
upload
command - Generate chart
index.yaml
and publish it on GitHub Pages
Let’s define our CircleCI pipeline. First, we need to create the .circleci
directory in the repository root and place the config.yml
file there. We can use the helm
orb to simplify the process of the helm CLI installation (1) (2). Once we install the helm CLI, we can install the unit-test
plugin and run the unit tests (3). Then we define a rule for filtering the master
branch (4). If the change is pushed to the master
branch we package the chart as the TAR archive and place it in the .deploy
directory (5). Then we install Chart Releaser and create a GitHub release (6). In the last step, we generate the index.yaml file using Chart Releaser and commit it to the gh-pages
branch (7).
version: 2.1
orbs:
helm: circleci/helm@2.0.1 # (1)
jobs:
build:
docker:
- image: cimg/base:2023.02
steps:
- checkout
- helm/install-helm-client # (2)
- run:
name: Install Helm unit-test
command: helm plugin install https://github.com/helm-unittest/helm-unittest
- run: # (3)
name: Run unit tests
command: helm unittest charts/*
- when:
condition: # (4)
equal: [ master, << pipeline.git.branch >> ]
steps:
- run:
name: Package chart # (5)
command: helm package charts/* -d .deploy
- run:
name: Install chart releaser
command: |
curl -L -o /tmp/cr.tgz https://github.com/helm/chart-releaser/releases/download/v1.5.0/chart-releaser_1.5.0_linux_amd64.tar.gz
tar -xv -C /tmp -f /tmp/cr.tgz
mv /tmp/cr ~/bin/cr
- run:
name: Release chart # (6)
command: cr upload -o piomin -r helm-charts -p .deploy
- run:
name: Create index on GitHub pages # (7)
command: |
git config user.email "job@circleci.com"
git config user.name "CircleCI"
git checkout --orphan gh-pages
cr index -i ./index.yaml -p .deploy -o piomin -r helm-charts
git add index.yaml
git commit -m "New release"
git push --set-upstream --force origin gh-pages
workflows:
helm_test:
jobs:
- build:
context: GitHub
Execute Helm Chart Release Pipeline
Once we push a change to the helm-charts
repository our pipeline is starting. Here is our result. As you see the pipeline finishes with success. We were releasing the 0.3.5
version of our chart.
Let’s see a list of GitHub releases. As you see, the 0.3.5
version has already been released.
How to access our Helm repository. In order to check it go to the repository Settings > Pages. The address of the GitHub Pages for that repository is the address of our Helm repository. We publish there the index.yaml
file that contains a definition of charts inside the repository. As you see, the address of the Helm repository is piomin.github.io/helm-charts
.
We can see the structure of the index.yaml
file just by calling the following URL: https://piomin.github.io/helm-charts/index.yaml
. Here’s the fragment of index.yaml
for the currently published version.
apiVersion: v1
entries:
spring-boot-api-app:
- apiVersion: v2
appVersion: 1.0.0
created: "2023-02-28T13:06:28.835693321Z"
description: A Helm chart for Kubernetes
digest: b308cbdf9f93f79baf5b39de8a7c509834d7b858f33d79d0c76b528e0cd7ca11
name: spring-boot-api-app
type: application
urls:
- https://github.com/piomin/helm-charts/releases/download/spring-boot-api-app-0.3.5/spring-boot-api-app-0.3.5.tgz
version: 0.3.5
Assuming we want to use our Helm chart we can easily access it. First, let’s add the Helm repository using CLI:
$ helm repo add piomin https://piomin.github.io/helm-charts/
Then, we can verify a list of Helm charts existing inside the repository:
$ helm search repo piomin
NAME CHART VERSION APP VERSION DESCRIPTION
piomin/spring-boot-api-app 0.3.5 1.0.0 A Helm chart for Kubernetes
Publish Helm Chart to Artifact Hub
In order to publish your Helm repository and charts on Artifact Hub you need to go to that site and create an account. Once you do it, you can add a new repository just by clicking the button. Then you just need to choose the name of your repo and put the right address.
Now, we can find our spring-boot-api-app
chart on the list of packages.
We can see its details. It’s worth publishing documentation in the README.md
file. Once you do it, you can view it in the chart details on Artifact Hub.
Finally, we can easily use the chart and deploy the Spring Boot app, e.g. with Argo CD.
Leave a Reply