Setting Helm Chart version and appVersion properties during CI/CD with GitHub Actions

Published on
Reading time
Authors

The release of Helm 3.7 sees some major changes to the way Helm behaves and the commands you work with. In addition to this, stricter adherence to Semantic Versioning (semver) can be observed for both Chart and Application versioning.

In this post I am going to look at one way you can simplify setting the version and appVersion values for your Helm Charts whilst ensuring you meet the semver 2 requirements.

TL;DR show me the code!

If you're here simply looking for the full solution then head on over to the sample GitHub Action workflow and check it out!

Read on if you want to understand the approach.

Understanding version and appVersion

The first thing we need to understand is why there appears to be two fields for a Helm Chart that look to do the same thing. The documentation of the Chart.yaml file format is great, but I'll summarise here as well and dig a bit more into each.

version

Quite simply version represents the version of the Helm Chart you are deploying. This is not the same as the version of the application you are deploying with the Chart. Charts define application components AND their configuration. If the configuration changes then the Chart version property should be updated. Updating an application component such as a Container image doesn't necessarily mean there is a configuration change.

The only way to set this value is to update it in the Chart.yaml file. This represents some challenges when automating build and deployment - either you need to submit an updated Chart.yaml file or write some scripting to update / create the Chart.yaml file.

appVersion

In many cases appVersion will be the property you want to update on every deployment as it is represents the version of the application component(s) you are deploying. This might simply be an updated container image (or set of images).

The appVersion can be set either though manually updating the Chart.yaml file (similar to updating version), with the additional capability to override it at packaging time by supplying the --app-version argument as shown below.

helm package . --app-version 1.0.0

Great, and the problem is?

I'm sure many would argue there is no problem here, but I do see a problem. You can set the version of your application regardless of the value contained in the Chart.yaml file, yet you are unable to do the same with the Chart version.

Yes, I could manually update the Chart.yaml file to denote a version change (perhaps as part of a Pull Request), but why should I have to edit a file when all I need to do is change one value? It introduce the chance of human error and I also require people to have more knowledge of the Helm ecosystem than many may ever need. Alternatively, you could argue this approach ensures the value is only updated when really necessary... but it still strikes me as odd!

Here's my approach

For this demo I am going to use GitHub Actions as my CI/CD platform, but this approach will work on any platform. I'll also use Azure Container Registry (ACR) as my Image and Chart repository, and deploy the resulting Container and Chart to Azure Kubernetes Service (AKS). While the ACR and AKS steps aren't necessary, they do form part of an end-to-end process most people are likely to use.

I'm using the sample repository for this demo, but you can use any Helm chart you have previously scaffolded with helm create.

Start by editing the Chart.yaml file and setting the version to a known value that you can parse is a script. You can also set a value for the appVersion but this will be overridden at the command line as we will see.

Next, in your GitHub Action workflow define two environment variables chartVersion and appMajorMinorVersion as shown below.

env:
  chartVersion: 0.1.0
  appMajorMinorVersion: 0.1

I have set a full semantic version for the Chart and only the major and minor versions for the application. I want full centralised control for the Chart version and the approach I have chosen means I do need to update my GitHub Action workflow if I want to change the Chart version, but I can live with that (at least it's not hidden in a source code file somewhere in the repo!)

Hopefully in future we'll have a plain text variable store for GitHub Actions which means I can define the value elsewhere and just reference in the workflow file. I don't want to use secrets because I want these values visible to anyone who looks at this workflow.

When we want to package our Chart we need to do a few additional steps. As our workflow runs on a Linux host we can utilise Linux commands to help. In this case we're going to use printf and sed to format and replace the version placeholder.

In the below workflow step we start by escaping the dots in the chartVersion environment variable. We do this is so the sed replacement works as expected on the next line.

Using this escaped value, which is held in $escaped_version, we then use the sed command to replace the version placeholder in the Chart.yaml file.

The final two commands packages up the Helm Chart, specifying the appVersion at the command-line, and then push the Chart to a remote repository (the sample uses Azure Container Registry).

- name: Helm Chart Update, Package and Push
  run: |
    cd ./content-web/charts/web 
    escaped_version=$(printf '%s\n' "${{ env.chartVersion }}" | sed -e 's/[\/.]/\\./g') 
    sed -i "s/version\: 0\.0\.0/version\: $escaped_version/" Chart.yaml
    helm package . --app-version ${{ env.appMajorMinorVersion }}.${{ env.tag }} 
    helm push web-${{ env.chartVersion }}.tgz oci://${{ env.containerRegistry }}/helm

As a result of this step we can now control the version and appVersion properties of our Chart.

More control

The thing I do like about the approach I have, is that should anyone tamper with the Chart.yaml file and change the version value the build will break and I avoid downstream issues as a result. As appVersion is overridden I can also be confident that any change made to the file will be ignored.

Having said this, the approach I am using is just one way to achieve this outcome, and it's also important to note that it doesn't suit every use case (and maybe not yours!)

It might be that you would break the GitHub Action workflow into multiple separate workflows in order to give you better control over when the two Helm Chart properties are updated. Also, the sample workflow uses a fairly basic process - it assumes every invocation should build a new Container Image, a new Helm Chart and deploy to AKS. Clearly that may not be ideal, so at a minimum you'd probably want to add Approvals or other mechanism that ensures you only build a new Chart when necessary, and only deploy the Chart when you want.

Hopefully you've picked up some useful information on working with Helm from this post, and it will help you be successful in packaging and releasing applications using Helm. If you come up with a different or better way feel free to leave a comment!

Happy days! 😎

P.S. You can check out the GitHub repository with the sample workflow.