Create and Release Your Own Helm Chart

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:

helm-chart-release-files

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:

helm-chart-release-test

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:

  1. Install the helm CLI on the machine (we will use the cimg/base image as the tests executor)
  2. Install the Helm unit-test plugin
  3. Run unit tests
  4. 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 with helm package command
  5. Install Chart Releaser
  6. Release the chart in GitHub with the Chart Releaser upload command
  7. 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.

helm-chart-release-artifacthub

Finally, we can easily use the chart and deploy the Spring Boot app, e.g. with Argo CD.

Leave a Reply