Simplifying Helm Chart Management: A Practical Guide to Creating Wrappers for External Dependencies

Erik Åström
10 min readMay 13, 2024

--

This guide is designed for anyone operating within the Kubernetes ecosystem, who uses Helm to deploy and manage containerized applications. It provides practical insights and best practices for managing Helm charts, creating Helm chart wrappers for external dependencies, automating version control, and streamlining deployment workflows. Whether you’re a Kubernetes newcomer or seeking deployment optimizations, this guide provides tips to improve your K8s experience and streamline app deployment pipelines.

Navigating Helm charts, especially external components like Operators, can be challenging. However, you can adopt a strategic approach to handle these complexities while maintaining control over deployment and flexibility. In this guide, we’ll explore a reliable method I use for Helm chart management: creating a Helm chart as a wrapper for external dependencies.

Using this method, you retain a local copy of the Helm chart, enabling you to enforce compliance with organizational policies and governance standards. You can implement approval workflows, apply access controls, and ensure that only authorized versions of the Helm chart are deployed.

Wrapping external dependencies within your Helm chart and hosting it in your repository offers a comprehensive solution that combines the benefits of version control, customization, security, and performance optimization. This approach empowers you to effectively manage the deployment of external applications while maintaining control and compliance within your Kubernetes environment.

Let’s embark on an illustrative journey, crafting a Helm chart wrapper for the Cloud Native PostgreSQL Operator. Through this process, you’ll uncover advantages that promise to refine your deployment and maintenance workflows, rendering them notably smoother and more efficient. Below, we present an example showcasing the structure of the wrapper for the Cloud Native Postgres Operator.

cnpg
├── Chart.lock
├── Chart.yaml
├── charts
│ └── cloudnative-pg-0.21.2.tgz
├── templates
│ ├── NOTES.txt
│ ├── _helpers.tpl
│ └── tests
└── values.yaml

Begin by creating a folder named cnpg-operator as an example. Within that directory, we'll craft the Helm chart wrapper.

mkdir cnpg-operator
cd cnpg-operator
helm create cnpg

This gives us “the simple starter chart” called cnpg. Next, we'll remove all unnecessary files from this chart.

find cnpg/templates -type f -name "*yaml" -exec rm {} \;
rm cnpg/values.yaml; touch cnpg/values.yaml

I typically retain NOTES.txt, but we need to modify its content for the Helm chart to function properly. Special thanks to patorjk.com for the ASCII art:

care.tietoevry

/$$$$$$ /$$ /$$ /$$$$$$$
/$$__ $$| $$$ | $$| $$__ $$
| $$ \__/| $$$$| $$| $$ \ $$ /$$$$$$
| $$ | $$ $$ $$| $$$$$$$//$$__ $$
| $$ | $$ $$$$| $$____/| $$ \ $$
| $$ $$| $$\ $$$| $$ | $$ | $$
| $$$$$$/| $$ \ $$| $$ | $$$$$$$
\______/ |__/ \__/|__/ \____ $$
/$$ \ $$
| $$$$$$/
\______/

Now, let’s incorporate the dependencies into the Chart.yaml file.
We can find the information for the repository and chart name in the GitHub for CloudNativePG Helm Charts.

dependencies:
- name: cloudnative-pg
repository: "https://cloudnative-pg.github.io/charts"
version: "0.1.0"

In this snippet, we specify a temporary version number "0.1.0" for now. We'll adjust the version number later.

To simplify version management, I’ve created a straightforward shell script. This script automates a process for updating the version of the Operator and its dependencies, commits the changes to Git, and tags the commit with the new version. You can find the script below. I typically name it version.

# Copyright [2024] [Tietoevry Care]
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

#!/bin/bash

readonly CHART_PATH="cnpg" # replace with your actual path
readonly TEMP_REPO_NAME="temp" # This will be removed when the script is done

verify_commands_exist() {
commands=("helm" "jq" "yq" "git")
for cmd in "${commands[@]}"; do
if ! command -v $cmd &> /dev/null; then
echo "$cmd could not be found"
echo "Please install $cmd before proceeding."
case $cmd in
"helm")
echo "You can install helm using the following command:"
echo "macOS: brew install helm"
echo "Windows: choco install kubernetes-helm"
;;
"jq")
echo "You can install jq using the following command:"
echo "macOS: brew install jq"
echo "Windows: choco install jq"
;;
"yq")
echo "You can install yq using the following command:"
echo "macOS: brew install yq"
echo "Windows: choco install yq"
;;
"git")
echo "You can install git using the following command:"
echo "macOS: brew install git"
echo "Windows: choco install git"
;;
esac
exit 1
fi
done
}

remove_temp_repo() {
helm repo remove $TEMP_REPO_NAME > /dev/null
}

fetch_repo_and_chart() {
repo=$(yq eval '.dependencies[0].repository' $CHART_PATH/Chart.yaml)
chart=$(yq eval '.dependencies[0].name' $CHART_PATH/Chart.yaml)
}

add_and_update_repo() {
helm repo add $TEMP_REPO_NAME $repo > /dev/null
helm repo update > /dev/null
}

get_current_version() {
current_version=$(yq eval '.version' $CHART_PATH/Chart.yaml)
}

search_existing_versions() {
echo "No new version provided. Searching the repository for existing versions..."
printf "%-15s %-15s %s\n" "Version" "App Version" "Current"
helm search repo $TEMP_REPO_NAME/$chart -l --output json | jq -r '.[] | "\(.version) \(.app_version)"' | awk -v cv="$current_version" '{if ($1 == cv) printf "%-15s %-15s %s\n", $1, $2, "*"; else printf "%-15s %-15s %s\n", $1, $2, ""}'
remove_temp_repo
exit 0
}

verify_version_exists() {
search_results=$(helm search repo $TEMP_REPO_NAME/$chart --version "$new_version" --output json)
if [ "$(echo $search_results | jq -r '.[0].app_version')" == "null" ]; then
echo "Version does not exist"
remove_temp_repo
exit 1
fi
app_version=$(echo $search_results | jq -r '.[0].app_version')
echo "Version exists, app_version is $app_version"
}

update_chart_yaml() {
yq eval -i ".version = \"$new_version\"" $CHART_PATH/Chart.yaml
yq eval -i '(.dependencies[0].version) |= "'$new_version'"' $CHART_PATH/Chart.yaml
yq eval -i ".appVersion = \"$app_version\"" $CHART_PATH/Chart.yaml
remove_temp_repo
echo "Updated $CHART_PATH/Chart.yaml with version $new_version and appVersion $app_version"
}

update_dependencies() {
helm dependency update $CHART_PATH > /dev/null
}

check_tag_exists() {
if git rev-parse "$new_version" >/dev/null 2>&1; then
echo "Tag already exists, why do you want to add it again? Exiting..."
exit 1
fi
}

commit_and_push_changes() {
git add .
git commit -m "Changed version $new_version and appVersion $app_version"
git tag $new_version
git push origin $new_version --tags
}

# Main script
verify_commands_exist
fetch_repo_and_chart
add_and_update_repo
get_current_version

if [ -z "$1" ]; then
search_existing_versions
fi

new_version=$1
verify_version_exists
update_chart_yaml
update_dependencies
check_tag_exists
commit_and_push_changes

More on how this works comes later but first, we need to set up git.
Now, let’s add this folder to your Git repository of choice. First, ensure that your Git repository is initialized and configured:

git init
git add .
git commit -m "Initial commit: Created cnpg-operator Helm chart wrapper"

Finally, add the Git remote repository URL and push the changes:

git remote add origin <remote_repository_url>
git push -u origin main

Replace <remote_repository_url> with the URL of your Git remote repository.

With these steps completed, we have a Helm chart wrapper for your external dependency with version control through Git.

Version script

The script is a versatile tool for managing Helm chart versions and dependencies within a Kubernetes environment. Here’s a breakdown of its primary functions:

Listing Versions and Showing Current Version:

  • When executed without specifying a version, the script first determines the current version of the Helm chart being managed.
  • It then searches the Helm repository for available versions and displays them alongside their corresponding application versions.
  • Additionally, it highlights the current version.

Updating Version and Dependencies:

  • When provided with a specific version as an argument. It retrieves the repository information and chart name from the Chart.yaml file of the Helm chart.
  • The script adds the repository to Helm, updates the chart’s version in the Chart.yaml file, and fetches the corresponding application version from the repository.
  • After updating the dependencies and chart version, it stages the changes and commits them to the Git repository with a descriptive message indicating the version and application version.
  • Additionally, it applies a Git tag to the commit, ensuring version control and traceability.
  • Finally, it pushes the changes to the remote Git repository, ensuring synchronization with the latest changes.

In summary, the script provides a streamlined workflow for managing Helm chart versions and dependencies, whether it’s listing available versions or updating the chart to a specific version. Its versatility makes it a valuable tool for maintaining version control and ensuring consistency in Kubernetes deployments.

How the script works

Now we should have this

# tree
├── cnpg
│ ├── Chart.yaml
│ ├── charts
│ ├── templates
│ │ ├── NOTES.txt
│ │ ├── _helpers.tpl
│ │ └── tests
│ └── values.yaml
└── version

5 directories, 5 files

Additionally, you’ll see the commit history in Git:

# git lg
* 9a0ab81 - (HEAD -> refs/heads/main) Initial commit: Created cnpg-operator Helm chart wrapper (6 seconds ago) <erik.astrom>

Now, we can address the issue of versioning. Utilizing the version script, we can retrieve all available versions of the Operator to choose from. Refer to the documentation to select a suitable version for you.

$ ./version
No new version provided. Searching the repository for existing versions...
Version App Version Current
0.21.2 1.23.1
0.21.1 1.23.0
0.21.0 1.23.0
0.20.2 1.22.2
0.20.1 1.22.1
0.20.0 1.22.0
0.19.1 1.21.1
0.19.0 1.21.0
0.18.2 1.20.2
0.18.1 1.20.1
0.18.0 1.20.0
0.17.2 1.19.1
0.17.1 1.19.1
0.17.0 1.19.0
0.16.1 1.18.1
0.16.0 1.18.0
0.15.1 1.17.1
0.15.0 1.17.0
0.14.3 1.16.1
0.14.2 1.16.1
0.14.1 1.16.1
0.14.0 1.16.0
0.13.1 1.15.1
0.13.0 1.15.0

Now we can set version 0.21.2 of the Helm chart using the script.

$ ./version 0.21.2
Version exists, app_version is 1.23.1
Updated cnpg/Chart.yaml with version 0.21.2 and appVersion 1.23.1
[main 64c6052] Changed version 0.21.2 and appVersion 1.23.1
4 files changed, 9 insertions(+), 3 deletions(-)
create mode 100644 cnpg/Chart.lock
create mode 100644 cnpg/charts/cloudnative-pg-0.21.2.tgz

$ git lg
* 64c6052 - (HEAD -> refs/heads/main, tag: refs/tags/0.21.2) Changed version 0.21.2 and appVersion 1.23.1 (5 seconds ago) <erik.astrom>
* 9a0ab81 - Initial commit: Created cnpg-operator Helm chart wrapper (2 hours ago) <erik.astrom>

$ ./version
No new version provided. Searching the repository for existing versions...
Version App Version Current
0.21.2 1.23.1 *
0.21.1 1.23.0
0.21.0 1.23.0
0.20.2 1.22.2
0.20.1 1.22.1
0.20.0 1.22.0
0.19.1 1.21.1
0.19.0 1.21.0
0.18.2 1.20.2
0.18.1 1.20.1
0.18.0 1.20.0
0.17.2 1.19.1
0.17.1 1.19.1
0.17.0 1.19.0
0.16.1 1.18.1
0.16.0 1.18.0
0.15.1 1.17.1
0.15.0 1.17.0
0.14.3 1.16.1
0.14.2 1.16.1
0.14.1 1.16.1
0.14.0 1.16.0
0.13.1 1.15.1
0.13.0 1.15.0

The script first updates all versions in the Chart.yaml file:

apiVersion: v2
name: cnpg
description: A Helm chart for Kubernetes
# A chart can be either an 'application' or a 'library' chart.
#
# Application charts are a collection of templates that can be packaged into versioned archives
# to be deployed.
#
# Library charts provide useful utilities or functions for the chart developer. They're included as
# a dependency of application charts to inject those utilities and functions into the rendering
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
type: application
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 0.21.2
# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using.
# It is recommended to use it with quotes.
appVersion: "1.23.1"
dependencies:
- name: cloudnative-pg
repository: "https://cloudnative-pg.github.io/charts"
version: "0.21.2"

After this is done the the helm dependencies are updated:

$ helm dependency update
$ tree -a
.
├── cnpg
│ ├── .helmignore
│ ├── Chart.lock
│ ├── Chart.yaml
│ ├── charts
│ │ └── cloudnative-pg-0.21.2.tgz
│ ├── templates
│ │ ├── NOTES.txt
│ │ ├── _helpers.tpl
│ │ └── tests
│ └── values.yaml
└── version

This fetches the external chart and stores it in the project.
Next, it stages and commits all changes with a message:

git add .
git commit -m "Changed version 0.21.2 and appVersion 1.23.1"

After that, it applies a git tag:

git tag 0.21.2

Finally, the script pushes the changes:

$ git push origin main --tags

CI Pipeline

To complete the setup, we should add a CI pipeline triggered by a git tag. The configuration specifications will depend on your Git remote service. I provide examples for packaging a helm chart and push to Harbor for GitLab and GitHub.

GitLab example: create .gitlab-ci.yml file.

# Please note that you need to create secrets in your GitLab repository for 
# HARBOR_URL, HARBOR_USERNAME, HARBOR_PASSWORD, and HARBOR_OCI.
# You can create these secrets in the GitLab repository settings.
helm:
stage: build
image:
name: dtzar/helm-kubectl:latest
entrypoint: ['']
variables:
# Enable OCI support (not required since Helm v3.8.0)
HELM_EXPERIMENTAL_OCI: 1
script:
# Log in to the Helm registry
- helm registry login "${HARBOR_URL}" -u "${HARBOR_USERNAME}" -p "${HARBOR_PASSWORD}"
# Package your Helm chart, which is in the `better-ehr-server` directory
- helm package cnpg
# Your helm chart is created with <chart name>-<chart release>.tgz
# You can push all building charts to your Harbor repository
- helm push *.tgz ${HARBOR_OCI}/helm
rules:
- if: $CI_COMMIT_TAG

GitHub example: create .github/workflows/helm.yml file.

# Please note that you need to create secrets in your GitHub repository for 
# HARBOR_URL, HARBOR_USERNAME, HARBOR_PASSWORD, and HARBOR_OCI.
# You can create these secrets in the GitHub repository settings.
name: Helm Package and Push

on:
push:
tags:
- '*'

jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2

- name: Set up Helm
uses: azure/setup-helm@v1
with:
version: v3.7.0

- name: Run Helm commands
run: |
echo "${{ secrets.HARBOR_PASSWORD }}" | helm registry login "${{ secrets.HARBOR_URL }}" --username "${{ secrets.HARBOR_USERNAME }}" --password-stdin
helm package cnpg
helm push *.tgz ${HARBOR_OCI}/helm
env:
HELM_EXPERIMENTAL_OCI: 1
HARBOR_URL: ${{ secrets.HARBOR_URL }}
HARBOR_USERNAME: ${{ secrets.HARBOR_USERNAME }}
HARBOR_PASSWORD: ${{ secrets.HARBOR_PASSWORD }}
HARBOR_OCI: ${{ secrets.HARBOR_OCI }}

After incorporating these two additions, the project structure should resemble the following:

$ tree -a    
.
├── .github
│ └── workflows
│ └── helm.yml
├── .gitlab-ci.yml
├── cnpg
│ ├── .helmignore
│ ├── Chart.lock
│ ├── Chart.yaml
│ ├── charts
│ │ └── cloudnative-pg-0.21.2.tgz
│ ├── templates
│ │ ├── NOTES.txt
│ │ ├── _helpers.tpl
│ │ └── tests
│ └── values.yaml
└── version

When a new version of the helm chart is published rerun the script, pick your version and your repository will be populated with a new helm chart to test and verify.

$ version <new version here>

Modifying the deployment

If you require specific modifications to the cnpg-operator you add them to values.yaml

cnpg-operator:
replicaCount: 2

If you want to add extra features like network Policies they should be placed in the template directory.

Happy Coding!

P.S.
With Rancher and Fleet the below Yaml will create a CD pipeline.

defaultNamespace: cnpg-system
helm:
releaseName: cnpg
chart: oci://harbor.of.your.choice/cnpg
version: 0.21.2
targetNamespace: cnpg-system

--

--

Erik Åström
Erik Åström

Written by Erik Åström

Erik is a senior Infrastructure Architect working at Tietoevry Care with 10 years of experience in building containerised systems.

Responses (1)