Helm 3 - Crafting a Chart
This post focuses on creating and releasing a chart, not consuming from a Helm Chart Repository.
Helm is an advanced tool used by kubernetes people, some "lingo" (jargon) is used here. Please leave a comment if you want more information.
Helm allows to "package" kubernetes applications, it simplifies the distribution and installation. While doing so, it checks dependencies versions and some other validations.
Version used
$ helm version --short
v3.2.4+g0ad800e
Helm Chart
A Chart is a Helm package. It contains all of the resource definitions necessary to run an application, tool, or service inside of a Kubernetes cluster. Think of it as the Kubernetes equivalent of a Homebrew formula, an apt dpkg, or a Yum RPM file [0].
For OOP people:
- Chart ~= class
- Release ~= instance
This is important to remember, we are going to be building a package.
Development of a Chart
Creating a new Chart
My recommendation is to create a charts/
folder in the root of your project(s).
This way each of your projects could become a "chart repository", similar to how hub.helm.sh consumes respositories from different sources in a descentralized way.
You could do the same for your projects. Each git project becomes a descentralized chart repository, or you can publish to a centralized chart repository like artifactory or your own github repo.
In any case, calling it charts/
is informative and flexible enough to choose any option.
Inside charts/
, we are going to use helm
to create the first boilerplate of our app.
helm create <package_name>
Example
Along the post we'll use auth-service
as our project example name.
mkdir auth-service
cd auth-service/
mkdir charts
cd charts/
helm create auth-service
Structure
auth-service/
└── charts/
└── auth-service/
├── charts/
├── Chart.yaml
├── templates/
│ ├── deployment.yaml
│ ├── _helpers.tpl
│ ├── hpa.yaml
│ ├── ingress.yaml
│ ├── NOTES.txt
│ ├── serviceaccount.yaml
│ ├── service.yaml
│ └── tests/
│ └── test-connection.yaml
└── values.yaml
Notes
-
appVersion
insideChart.yaml
references the application version [1] -
image.tag
insidevalues.yaml
references the docker image version/tag. - if
image.tag
is skipped,appVersion
is used instead. - recommendation: use a tool to automatically bump the version, like commitizen, during the CI execution, and push back to the repo.
- Whether to use
image.tag
orappVersion
is still under debate, you can read more in the github issue - If you use
appVersion
you can usehelm history <release_name>
to get info on the versions per revision. - You can re-use the same chart to deploy multiple django/rails applications, seems like a common practice
Chart customization
I recommend to start with the default helm chart and from there, start adding any extra stuff that you need.
Templating
If you have used other template systems like jinja
, or Django's template engine,
Helm's system is not that different: you can apply functions using a pipeline |
.
food: {{ .Values.favorite.food | upper | quote }}
Avoid adding complex template tags; the purpose of yaml
is to be readable.
By using templates, we make things more complex, and less readable, touch only when necessary.
templates/_helpers.tpl
contains custom functions for your templates, like generating the release name based on values.
To find problems with you charts, run:
helm lint <package_name>
Values
Place the "configuration" that you want to expose to the users of the chart in
the values.yaml
, even if it's you who's gonna end up using it.
There's no need to parametrize everything, and try to use sensible defaults.
A good rule is to expose only the things you are going to use and make new parameters only when you have to.
Let developers specify unconventional aspects of the application.
You can also define a values.schema.json
which will be used by helm to validate
the parameters given to Helm [2].
Using custom values
values.yaml
is used as default and any extra values provided through --set
or --values
will be merged into the default values.yaml
inside the chart.
There are 2 approaches to deal with custom values that I know.
Centralized values
The first one is to have a centralized place with all the configuration. At the
moment, I know helmfile is being used for this.
You'd specify every configuration per environment per chart in a helmfile.yaml
.
Per repository
This is the most popular approach. Each "project" is responsible to set the values
per enviroment (production
, staging
).
If you are going to modify small aspects of your app, using --set
should be enough.
A common practice, is to place the production and staging files inside the chart folder, but in my opinion this should be avoided when possible.
A Helm chart is a package: Helm is a package manager. Like apt, pip or npm. When we use tools like Docker, for example, we provide env variables from outside, they are not packaged inside the image. This gives the container a lot of flexibility and the same principle applies to Helm. There's an interesting discussion in the helm repo about this.
Ideally, your custom values should live outside the chart, and they should be given to the chart.
Let's see a setup example for the auth-service
.
auth-service/
├── charts/
│ └── auth-service/
├── charts-values/
│ ├── production/
│ │ ├── redis.yaml
│ │ └── auth-service.yaml
│ └── staging/
│ └── auth-service.yaml
└── src/
The installation command would look like
helm install --values charts-values/production/auth-service.yaml auth-service-prod ./auth-service
I'm not 100% happy with the above setup, mainly with the naming. But it allows having multiple values per chart per environment. We could easily add values for a redis pulled from the official Helm hub. I'd like to hear opinions about it. How'd you do it?
Release
A Release is an instance of a chart running in a Kubernetes cluster. One chart can often be installed many times into the same cluster. And each time it is installed, a new release is created. Consider a MySQL chart. If you want two databases running in your cluster, you can install that chart twice. Each one will have its own release, which will in turn have its own release name [3].
New release
helm install <release_name> <package_name>
Deploy a new release to the cluster.
We can also run a dry-run to check what's going to happen:
helm install <release_name> <package_name> --dry-run
Example
helm install auth-service-prod ./auth-service
Notes
-
package_name
can be a folder, a.tgz
or a url. -
release_name
: the name of this particular release. If the name is different another "instance" will be deployed. So for redis instances it may be worth using differentrelease_name
s, but for your JavaScript app it may not. - The output of
templates/NOTES.txt
is shown in the prompt when making a new release, useful for CI logs. - If you don't want to provide a
<release_name>
, use--generate-name
and it will assign a random<release_name>
. - Helm stores release config per namespace, so if you want to release 2 redis instances in the same namespace, they should have different
<release_name>
s [4]. - Helm does not wait until all of the resources are running before it exits [5].
-
personal: use different
release_name
s per environment (production
,staging
). Even though it may not be necessary, giving that extra information in the name is useful and cheap. - Use
helm get values <release_name>
to get the values used for the release, useful to check if our custom values were applied properly.
Check release status
After it is installed, we want to know if everything went well.
helm status <release_name>
Example
helm status auth-service-prod
An upgrade takes an existing release and upgrades it according to the information you provide. Because Kubernetes charts can be large and complex, Helm tries to perform the least invasive upgrade. It will only update things that have changed since the last release. [6]
Uprgrade release
helm upgrade -f <custom_values.yaml> <release_name> <package_name>
Example
helm upgrade -f charts-values/production/auth-service.yaml auth-service-prod ./auth-service
Rollback release
helm rollback <release_name> <revision>
Example
helm rollback auth-service-prod 1
Notes
- Any release version increment will produce a
revision
number. It goes from 1..N. - Use
helm history <release_name>
to see the revisions of yourrelease_name
.
Uninstall release
helm uninstall <release_name>
I won't go deep into this, but just know it exists, and you can remove an existing release.
Automating release cycle
A recommended best practice to avoid running helm install
and helm upgrade
[7] is to use:
helm upgrade --install <release_name> --values <custom_values.yaml> <package_name>
This can be a benefit in an automated CI/CD pipeline. We let Helm perform the check to know if it's a first time, or a release upgrade.
Example
helm upgrade --install auth-service-prod --values charts-values/production/auth-service.yaml ./auth-service
Notes
- Use
--atomic
to get automatic rollback on failures.[8]
Complex Charts with Many Dependencies
The current best practice for composing a complex application from discrete parts is to create a top-level umbrella chart that exposes the global configurations, and then use the charts/ subdirectory to embed each of the components.5
I think this is an improving point; I haven't understood it by reading the documentation yet.
Hey, hello 👋
If you are interested in what I write, follow me on twitter