10 minute read

Who couldn’t use their very own personal helm chart repository these days? I know I sure could. But I know what you’re thinking, “welp, there’s another fee periodically dinging me for something I rarely use – I already haven’t used my Peacock Plus subscription since I finished watching MacGruber months ago…“, right?

Wrong
Photo credit: Paste Magazine

Wrong. Thanks to the features GitHub provides us free of charge we can host our helm chart repo, we can store our docker images, we can cut new releases, we can have automated CI processes, and with the money we’re saving we can even afford to hang on to that otherwise worthless Peacock Plus subscription until the next season of MacGruber drops! Let’s take a look at the building blocks first, and then we’ll walk through the steps to set up a real world example:

  • GitHub Repo will store our code and configuration of the other features.
  • GitHub Pages will provide us with a public endpoint and an HTTP server for our chart repo.
  • GitHub Actions will provide us with pipelines for our automated workflows.
  • Chart Releaser is an open source tool that will make this all a breeze (and inspired this article).
  • GitHub Releases will store our packaged helm chart releases (versions).
  • GitHub Container Registry will store our custom docker images (versions).

Step 1: Create a GitHub Repo to house everything

My intention is for this repo to be a central home for all of my personal helm charts, not just a single chart, so I’m going to create a new public repo generically named helm-charts, taking my inspiration from the prometheus-community repo which is home to a multitude of charts in a single repo. Also, just to prepare, if you read the documentation for Chart Releaser which we’ll be using in a bit, it instructs us to create a branch in our new repo explicitly named gh-branch used by GitHub Pages, and also to keep our charts in a directory explicitly named charts (this is configurable if you desire a different name) in the main branch.

I created a new helm-charts repo in my web browser and cloned it to the machine I’m working on:

git clone https://github.com/gerkElznik/helm-charts.git && cd helm-charts

Step 2: Set up GitHub Pages for a repo endpoint

Create a new branch named gh-pages (it will contain separate content and lineage than the main branch and they’re never intended to be merged):

git checkout --orphan gh-pages
git rm -rf .
git commit -m "Initial commit" --allow-empty
git push --set-upstream origin gh-pages

After creating the gh-pages branch you can see GitHub Pages is automatically enabled and contains a URL for your site by going to Settings >> Pages:

Pages

Step 3: Automate it all with Workflows in GitHub Actions

There will also be a new GitHub Actions Workflow named pages-build-deploy created automatically after you created the gh-pages branch:

Workflows

Usually, this workflow only runs after the index.yaml file for our helm repo is updated in the gh-pages branch for us by the Chart Releaser tool, but to add some instructions to our helm repo’s public endpoint (in the event a user visits it with a browser), we can add a README.md to the root of the gh-pages branch and push it (you could alternatively write an index.html if you prefer working with HTML over markdown), this should trigger your pages-build-deploy workflow and when it completes you can browse to your site and see your instructions.

Example README.md will result in this instructional page https://gerkelznik.github.io/helm-charts:

Helm Chart Repo Site


Now we’ll make use of the Chart Releaser tool’s free marketplace GitHub Action by creating a new Workflow (eg: .github/workflows/release.yml in the main branch) with the following content:

name: Release Charts

on:
  push:
    branches:
      - main

jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v2
        with:
          fetch-depth: 0

      - name: Configure Git
        run: |
          git config user.name "$GITHUB_ACTOR"
          git config user.email "$GITHUB_ACTOR@users.noreply.github.com"

      - name: Install Helm
        uses: azure/setup-helm@v1
        with:
          version: v3.8.1

      - name: Run chart-releaser
        uses: helm/chart-releaser-action@v1.4.0
        env:
          CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}"

This uses @helm/chart-releaser-action to turn your GitHub project into a self-hosted Helm chart repo. It does this – during every push to main – by checking each chart in your project, and whenever there’s a new chart version, creates a corresponding GitHub release named for the chart version, adds Helm chart artifacts to the release, and creates or updates an index.yaml file with metadata about those releases, which is then hosted on GitHub Pages. You do not need an index.yaml file in main at all because it is managed in the gh-pages branch.

Commit and push the workflow YAML file and you should see your new workflow run, but since we don’t have any helm charts in our /charts directory yet there’s nothing for it to do – we’ll add one in just a second.

Release Charts Workflow

Step 4: Containerize a sample web app

Before we add a helm chart, we’ll create a very simple web app packaged as a docker image. We’ll also create another new GitHub repo to house our web app since in the real world it is common to keep our application code in a separate repo from our helm charts – the sample I created for this demo is macgruber-app.

Once you’re able to successfully build a docker image containing your app and verified it runs as expected locally, then we can add a GitHub Actions Workflow to this new repo that will handle pushing new versions of our docker image to GitHub Container Registry each time we cut a new release.

Example .github/workflows/publish-image.yml

name: Create and publish a Docker image

on:
  release:
    types: [published]

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  build-and-push-image:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write

    steps:
      - name: Checkout repository
        uses: actions/checkout@v3

      - name: Log in to the Container registry
        uses: docker/login-action@v1
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Extract metadata (tags, labels) for Docker
        id: meta
        uses: docker/metadata-action@v3
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          tags: |
            type=semver,pattern={{version}}

      - name: Build and push Docker image
        uses: docker/build-push-action@v2
        with:
          context: .
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}

The above workflow is triggered by a release being published in your repo. It checks out the GitHub repository, and uses the login-action to log in to the Container registry. It then extracts labels and tags for the Docker image. Finally, it uses the build-push-action action to build the image and publish it on the Container registry.

Now we will cut a release for our sample app, I’ll use the GitHub CLI as it leaves a nice paper trail to follow, but feel free to do the same from your web browser.

If you don’t already have it and you’re on macOS, install GitHub CLI using Homebrew and peak at their getting-started instructions:

brew install gh

Now execute these GitHub CLI commands from the root directory of your sample app repo (for me this is macgruber-app):

# interactive setup
gh auth login

gh release create v0.1.0 --notes "Initial release of macgruber-app"

As soon as your new release is published you should be able to see your workflow created above automatically triggering a run:

Publish Workflow

When the workflow completes you should see the image added to your Packages:

Packages

When you click on the new package (see the red arrow above), you’ll be able to see a version tag created (in this case 0.1.0) for the image that corresponds to the release we cut. Unfortunately, I found that the package (image) was published as “Private” meaning an anonymous user would be unable to pull this image, and that’s not what I want for the applications my personal helm charts will package up since I desire to be able to share them freely, and at the time of this writing I was unable to find a way to automate changing it to “Public” – however, the package’s visibility can be manually changed by clicking on “Package settings” as the red arrow below shows:

Package Settings

Then in the “Danger Zone” change the visibility to Public:

Package Public

GitHub Packages using Container Registry (ghcr.io) is still pretty new so I will keep an eye on the API and be on the lookout for a way to automate Public visibility and will update the article when this capability is available.

Now your image should be public to the world and any anonymous user can pull it like so:

docker pull ghcr.io/gerkelznik/macgruber-app:0.1.0

Step 5: Publish a helm chart

Be sure you have helm installed, I’m using the following version:

$ helm version
version.BuildInfo{Version:"v3.8.1", GitCommit:"5cb9af4b1b271d11d7a97a71df3ac337dd94ad37", GitTreeState:"clean", GoVersion:"go1.17.8"}

Switch back to the helm-charts repo we created earlier and make sure you’re on the main branch (or at least not the gh-pages branch), then create our new helm chart within a directory named charts (remember from earlier, the @helm/chart-releaser-action action looks for new chart versions in this directory, otherwise it will find nothing to do):

$ mkdir -p charts && cd charts
$ helm create macgruber
$ tree
.
└── macgruber
    ├── Chart.yaml
    ├── charts
    ├── templates
    │   ├── NOTES.txt
    │   ├── _helpers.tpl
    │   ├── deployment.yaml
    │   ├── hpa.yaml
    │   ├── ingress.yaml
    │   ├── service.yaml
    │   ├── serviceaccount.yaml
    │   └── tests
    │       └── test-connection.yaml
    └── values.yaml

4 directories, 10 files

To keep this example simple, we’ll delete everything in the macgruber directory that was generated for us except:

  • Chart.yaml
  • values.yaml
  • templates/_helpers.tpl
  • templates/deployment.yaml
  • templates/service.yaml
  • templates/serviceaccount.yaml

Most of the defaults are okay for us to keep, we just need to make the following replacements:

Charts.yaml - Change the appVersion value to the version of the release we cut for our simple app (should match the tag of the docker image, so don’t include the “v” prefix):

appVersion: "0.1.0"

values.yaml - Change the image.repository value to the registry/repo we pushed our docker image to earlier:

image:
  repository: ghcr.io/gerkelznik/macgruber-app

Now just push your changes to the main branch and the Release Charts followed by the pages-build-deployment workflows will execute:

Release Helm Charts

Once these are complete you should be able to see the new helm chart release:

Releases

And to verify the site is hosting the backbone of your helm chart repo, the index.yaml, just add it to the end of your GitHub Pages URL and visit it in a browser, for example, https://gerkelznik.github.io/helm-charts/index.yaml

Step 6: Verify the end-user experience

We’re finally ready to see if our hard work paid off and we can share our bundled-up application as a helm chart.

  1. The first thing an end-user needs to do is add our repo using the GitHub Pages URL:
    $ helm repo add gerkelznik https://gerkelznik.github.io/helm-charts
    "gerkelznik" has been added to your repositories
    
  2. Then they can search for charts in the repo:
    $ helm search repo gerkelznik
    NAME                	CHART VERSION	APP VERSION	DESCRIPTION
    gerkelznik/macgruber	0.1.0        	0.1.0      	A Helm chart for a MacGruber punch!
    
  3. They need a Kubernetes cluster to release the chart to, so we’ll create a test environment by starting a local minikube cluster:
    $ minikube version
    minikube version: v1.25.2
    commit: 362d5fdc0a3dbee389b3d3f1034e8023e72bd3a7
    $ minikube config set driver docker
    ❗  These changes will take effect upon a minikube delete and then a minikube start
    $ minikube start
    😄  minikube v1.25.2 on Darwin 12.3.1
    ✨  Using the docker driver based on user configuration
    👍  Starting control plane node minikube in cluster minikube
    🚜  Pulling base image ...
    💾  Downloading Kubernetes v1.23.3 preload ...
     > preloaded-images-k8s-v17-v1...: 505.68 MiB / 505.68 MiB  100.00% 37.66 Mi
     > gcr.io/k8s-minikube/kicbase: 379.06 MiB / 379.06 MiB  100.00% 24.76 MiB p
    🔥  Creating docker container (CPUs=2, Memory=8100MB) ...
    🐳  Preparing Kubernetes v1.23.3 on Docker 20.10.12 ...
     ▪ kubelet.housekeeping-interval=5m
     ▪ Generating certificates and keys ...
     ▪ Booting up control plane ...
     ▪ Configuring RBAC rules ...
    🔎  Verifying Kubernetes components...
     ▪ Using image gcr.io/k8s-minikube/storage-provisioner:v5
    🌟  Enabled addons: storage-provisioner, default-storageclass
    🏄  Done! kubectl is now configured to use "minikube" cluster and "default" namespace by default
    
  4. And finally they can install the helm chart:
    $ helm install macgruber gerkelznik/macgruber
    NAME: macgruber
    LAST DEPLOYED: Wed Apr  6 21:54:16 2022
    NAMESPACE: default
    STATUS: deployed
    REVISION: 1
    TEST SUITE: None
    

Voila, we now verified that an end-user can install our new helm chart and we have a pod running a container using the simple app image we built earlier:

MacGruber Pod Running

Just for fun, we can port forward local port 8080 to the Service our helm chart created (which listens on port 80):

kubectl port-forward service/macgruber 8080:80

And then see our awesome new app in action from a browser at http://localhost:8080:

MacGruber App Punching

In Conclusion

The GitHub ecosystem provides a nice free option for hosting helm chart repos, in addition, it also provides automated workflows managed with pipeline-as-code, docker image repos, and of course application code repos.

In this article, we saw there are several steps necessary to get to the point where you have a shareable public helm chart. I found that GitHub has free features checking all the boxes to get you there. I’m interested to hear your feedback on experiences you’ve had with various other tools that accomplish the same.

For my next post, I’ll continue where we left off in this article and I’ll show how we can add another GitHub Actions Workflow to our helm-chart repo to lint and test pull requests for our charts using an action called @helm/chart-testing-action. This will spin up an ephemeral kind Kubernetes cluster and use a Chart Testing tool to automate testing our helm charts. Stay tuned by following me.

If you enjoyed this post I’d appreciate some claps for it over on Medium!

Updated: