Building a Helmfile CI/CD Pipeline

In my previous post, I described a Helmfile deployment for observability around website monitoring. Now, as your monitoring needs grow, that Helmfile deployment will undergo more and more changes. To properly handle this at a larger scale, CI/CD is a must. In this post, we'll take a quick run through of a basic CI/CD pipeline for a Helmfile deployment.

Now, as we'll recall, our Helmfile configuration is as follows:

 1repositories:
 2- name: grafana
 3  url: https://grafana.github.io/helm-charts
 4- name: prometheus-community
 5  url: https://prometheus-community.github.io/helm-charts
 6
 7releases:
 8  - name: grafana
 9    namespace: observability
10    chart: grafana/grafana
11    values:
12    - ./grafana/values.yaml
13
14  - name: prometheus-operator
15    namespace: observability
16    chart: prometheus-community/kube-prometheus-stack
17    values:
18    - ./prometheus-operator/values.yaml
19
20  - name: prometheus-blackbox-exporter
21    namespace: observability
22    chart: prometheus-community/prometheus-blackbox-exporter
23    values:
24    - ./prometheus-blackbox-exporter/values.yaml
25    needs:
26    - observability/prometheus-operator

As our needs grow, we may be adding more endpoints to our blackbox exporter deployment, adding custom jobs to our Prometheus Operator, or maybe even deploying new data sources to Grafana. All of these changes can be done through our Helmfile configuration. But once these changes start happening more, we want to start adding some process around changes. To do this, we'll be building a simple GitLab CI pipeline that does the following:

  • On a feature branch (i.e. not the main branch), we'll run a helmfile diff on our configuration
  • On the main branch, we'll run a helmfile apply
  • In between these two steps, we'll have a merge request process where both the code changes and the helmfile diff can be reviewed

Since this is a relatively short pipeline, let's get it all down now and go through each piece.

 1---
 2stages:
 3  - diff
 4  - apply
 5
 6default:
 7  image: chatwork/helmfile
 8  tags:
 9    - home-lab
10
11diff:
12  stage: diff
13  script:
14  - |
15    cd helm-charts
16    helmfile diff
17  except:
18    - main
19
20apply:
21  stage: apply
22  script:
23  - |
24    cd helm-charts
25    helmfile apply
26  only:
27    - main

So, as stated before, we have 2 stages, "diff" and "apply". In our default block, we define a few sensible defaults. In our case, the image for both of our stages (i.e. an image that has helmfile on it) and we also specify our runner tag.

From there, the stages are actually pretty simple. In both stages, we'll change our directory to the directory with the helmfile.yaml, and in one we run diff, the other we run apply. The important parts in these two stages to make our merge request process possible, are the except and only blocks. On the diff block, we use except to only run on branches that aren't named "main". Similarly, for apply, we'll use only to restrict that stage to the main branch.

There's not much more to it than that honestly. One nice thing I'll note in this gitlab-ci.yml file, is our multi-line shorthand - |. This is an alternative to doing something like this:

1script:
2- foo
3- bar
4- foobar

Both achieve the same thing, but personally, I like the method I've gone with much more :-)

And with that, I'll tie a bow on this post. Thanks for reading, and stay tuned for more!